diff --git a/.github/actions/setup-haxeshit/action.yml b/.github/actions/setup-haxeshit/action.yml index bf81e0d6d..6b565bfa2 100644 --- a/.github/actions/setup-haxeshit/action.yml +++ b/.github/actions/setup-haxeshit/action.yml @@ -24,5 +24,5 @@ runs: haxelib git haxelib https://github.com/HaxeFoundation/haxelib.git haxelib version haxelib --global install hmm - haxelib --global run hmm install + haxelib --global run hmm install --quiet shell: bash diff --git a/Project.xml b/Project.xml index 4ffb0355c..f34c9bc06 100644 --- a/Project.xml +++ b/Project.xml @@ -181,6 +181,14 @@ +
+ + +
diff --git a/hmm.json b/hmm.json index 348e261df..74a1f57a1 100644 --- a/hmm.json +++ b/hmm.json @@ -4,21 +4,21 @@ "name": "discord_rpc", "type": "git", "dir": null, - "ref": "2d83fa8", + "ref": "2d83fa863ef0c1eace5f1cf67c3ac315d1a3a8a5", "url": "https://github.com/Aidan63/linc_discord-rpc" }, { "name": "flixel", "type": "git", "dir": null, - "ref": "32cee07", + "ref": "32cee07a0e5f21e590a4b21234603b2cd5898b10", "url": "https://github.com/EliteMasterEric/flixel" }, { "name": "flixel-addons", "type": "git", "dir": null, - "ref": "f107166", + "ref": "f107166de3e830648e8fbf3da5526d4b94aa7dfc", "url": "https://github.com/EliteMasterEric/flixel-addons" }, { @@ -30,7 +30,7 @@ "name": "flxanimate", "type": "git", "dir": null, - "ref": "a913635", + "ref": "a9136359271cae6ea3016b7fd9023c5c42562933", "url": "https://github.com/ninjamuffin99/flxanimate" }, { @@ -42,23 +42,16 @@ "name": "haxeui-core", "type": "git", "dir": null, - "ref": "4b927f5", + "ref": "3590c94858fc6dbcf9b4d522cd644ad571269677", "url": "https://github.com/haxeui/haxeui-core/" }, { "name": "haxeui-flixel", "type": "git", "dir": null, - "ref": "999fadd", + "ref": "999faddf862d8a1584ae3794d932c55e94fc65cc", "url": "https://github.com/haxeui/haxeui-flixel" }, - { - "name": "hmm", - "type": "git", - "dir": null, - "ref": "3ef9522", - "url": "https://github.com/steviegt6/hmm" - }, { "name": "hscript", "type": "haxelib", @@ -66,8 +59,10 @@ }, { "name": "hxCodec", - "type": "haxelib", - "version": "3.0.1" + "type": "git", + "dir": null, + "ref": "c8c47e706ad82a423783006ed901b6d93c89a421", + "url": "https://github.com/polybiusproxy/hxCodec" }, { "name": "hxcpp", @@ -93,21 +88,21 @@ "name": "lime", "type": "git", "dir": null, - "ref": "2447ae6", - "url": "https://github.com/elitemastereric/lime" + "ref": "acb0334c59bd4618f3c0277584d524ed0b288b5f", + "url": "https://github.com/EliteMasterEric/lime" }, { "name": "openfl", "type": "git", "dir": null, - "ref": "d33d489", + "ref": "d33d489a137ff8fdece4994cf1302f0b6334ed08", "url": "https://github.com/EliteMasterEric/openfl" }, { "name": "polymod", "type": "git", "dir": null, - "ref": "6594dd8", + "ref": "631a3637f30997e47cd37bbab3cb6a75636a4b2a", "url": "https://github.com/larsiusprime/polymod" }, { @@ -118,7 +113,7 @@ { "name": "tink_json", "type": "haxelib", - "version": null + "version": "0.11.0" } ] } diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx index 608898a5f..9cc68c462 100644 --- a/source/funkin/FreeplayState.hx +++ b/source/funkin/FreeplayState.hx @@ -901,6 +901,7 @@ class FreeplayState extends MusicBeatSubState } } + @:haxe.warning("-WDeprecated") override function switchTo(nextState:FlxState):Bool { clearDaCache(songs[curSelected].songName); diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index bed63d1d8..1ff2c0caa 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -20,7 +20,7 @@ class PolymodHandler { /** * The API version that mods should comply with. - * Format this with Semantic Versioning; ... + * Format this with Semantic Versioning; ... * Bug fixes increment the patch version, new features increment the minor version. * Changes that break old mods increment the major version. */ @@ -29,7 +29,9 @@ class PolymodHandler /** * Where relative to the executable that mods are located. */ - static final MOD_FOLDER = "mods"; + static final MOD_FOLDER:String = #if (REDIRECT_ASSETS_FOLDER && macos) "../../../../../../../example_mods" #elseif REDIRECT_ASSETS_FOLDER "../../../../example_mods" #else "mods" #end; + + static final CORE_FOLDER:Null = #if (REDIRECT_ASSETS_FOLDER && macos) "../../../../../../../assets" #elseif REDIRECT_ASSETS_FOLDER "../../../../assets" #else null #end; public static function createModRoot() { @@ -202,9 +204,10 @@ class PolymodHandler { return { assetLibraryPaths: [ - "songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2", - "week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1", - ] + "default" => "preload", "shared" => "", "songs" => "songs", "tutorial" => "tutorial", "week1" => "week1", "week2" => "week2", "week3" => "week3", + "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1", + ], + coreAssetRedirect: CORE_FOLDER, } } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index c0705bd96..3e7325ae4 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -639,6 +639,12 @@ class PlayState extends MusicBeatState currentStage.resetStage(); + playerStrumline.vwooshNotes(); + opponentStrumline.vwooshNotes(); + + playerStrumline.clean(); + opponentStrumline.clean(); + // Delete all notes and reset the arrays. regenNoteData(); @@ -966,6 +972,7 @@ class PlayState extends MusicBeatState * This function is called whenever Flixel switches switching to a new FlxState. * @return Whether to actually switch to the new state. */ + @:haxe.warning("-WDeprecated") override function switchTo(nextState:FlxState):Bool { var result:Bool = super.switchTo(nextState); diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index 6d67cfbbd..bcb73d543 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -58,6 +58,7 @@ class BaseCharacter extends Bopper */ public var dropNoteCounts(default, null):Array; + @:allow(funkin.ui.animDebugShit.DebugBoundingState) final _data:CharacterData; final singTimeSec:Float; diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 454ec13e1..ab9cfdec5 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -53,6 +53,9 @@ class Strumline extends FlxSpriteGroup var noteSplashes:FlxTypedSpriteGroup; var noteHoldCovers:FlxTypedSpriteGroup; + var notesVwoosh:FlxTypedSpriteGroup; + var holdNotesVwoosh:FlxTypedSpriteGroup; + final noteStyle:NoteStyle; var noteData:Array = []; @@ -76,10 +79,18 @@ class Strumline extends FlxSpriteGroup this.holdNotes.zIndex = 20; this.add(this.holdNotes); + this.holdNotesVwoosh = new FlxTypedSpriteGroup(); + this.holdNotesVwoosh.zIndex = 21; + this.add(this.holdNotesVwoosh); + this.notes = new FlxTypedSpriteGroup(); this.notes.zIndex = 30; this.add(this.notes); + this.notesVwoosh = new FlxTypedSpriteGroup(); + this.notesVwoosh.zIndex = 31; + this.add(this.notesVwoosh); + this.noteHoldCovers = new FlxTypedSpriteGroup(0, 0, 4); this.noteHoldCovers.zIndex = 40; this.add(this.noteHoldCovers); @@ -201,6 +212,54 @@ class Strumline extends FlxSpriteGroup return null; } + /** + * Call this when resetting the playstate. + */ + public function vwooshNotes():Void + { + for (note in notes.members) + { + if (note == null) continue; + if (!note.alive) continue; + + notes.remove(note); + notesVwoosh.add(note); + + var targetY:Float = FlxG.height + note.y; + if (PreferencesMenu.getPref('downscroll')) targetY = 0 - note.height; + FlxTween.tween(note, {y: targetY}, 0.5, + { + ease: FlxEase.expoIn, + onComplete: function(twn) { + note.kill(); + notesVwoosh.remove(note, true); + note.destroy(); + } + }); + } + + for (holdNote in holdNotes.members) + { + if (holdNote == null) continue; + if (!holdNote.alive) continue; + + holdNotes.remove(holdNote); + holdNotesVwoosh.add(holdNote); + + var targetY:Float = FlxG.height + holdNote.y; + if (PreferencesMenu.getPref('downscroll')) targetY = 0 - holdNote.height; + FlxTween.tween(holdNote, {y: targetY}, 0.5, + { + ease: FlxEase.expoIn, + onComplete: function(twn) { + holdNote.kill(); + holdNotesVwoosh.remove(holdNote, true); + holdNote.destroy(); + } + }); + } + } + /** * For a note's strumTime, calculate its Y position relative to the strumline. * NOTE: Assumes Conductor and PlayState are both initialized. @@ -396,6 +455,38 @@ class Strumline extends FlxSpriteGroup return heldKeys[dir]; } + /** + * Called when the song is reset. + * Removes any special animations and the like. + * Doesn't reset the notes from the chart, that's handled by the PlayState. + */ + public function clean():Void + { + for (note in notes.members) + { + if (note == null) continue; + killNote(note); + } + + for (holdNote in holdNotes.members) + { + if (holdNote == null) continue; + holdNote.kill(); + } + + for (splash in noteSplashes) + { + if (splash == null) continue; + splash.kill(); + } + + for (cover in noteHoldCovers) + { + if (cover == null) continue; + cover.kill(); + } + } + public function applyNoteData(data:Array):Void { this.notes.clear(); @@ -423,6 +514,7 @@ class Strumline extends FlxSpriteGroup public function killNote(note:NoteSprite):Void { + if (note == null) return; note.visible = false; notes.remove(note, false); note.kill(); diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx index fdd613667..72d22191b 100644 --- a/source/funkin/play/notes/SustainTrail.hx +++ b/source/funkin/play/notes/SustainTrail.hx @@ -289,6 +289,20 @@ class SustainTrail extends FlxSprite missedNote = false; } + public override function revive():Void + { + super.revive(); + + strumTime = 0; + noteDirection = 0; + sustainLength = 0; + fullSustainLength = 0; + noteData = null; + + hitNote = false; + missedNote = false; + } + override public function destroy():Void { vertices = null; diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 4cbf1ade3..8f8e24a71 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -47,7 +47,7 @@ class Song implements IPlayStateScriptedClass difficultyIds = []; difficulties = new Map(); - _metadata = SongDataParser.parseSongMetadata(songId); + _metadata = SongDataParser.loadSongMetadata(songId); if (_metadata == null || _metadata.length == 0) { throw 'Could not find song data for songId: $songId'; diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx index c2a701ce9..c44180b20 100644 --- a/source/funkin/play/song/SongData.hx +++ b/source/funkin/play/song/SongData.hx @@ -1,7 +1,7 @@ package funkin.play.song; -import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEventDispatcher; +import funkin.modding.events.ScriptEvent; import flixel.util.typeLimit.OneOfTwo; import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEventDispatcher; @@ -120,12 +120,21 @@ class SongDataParser } } + /** + * A list of all the song IDs available to the game. + * @return The list of song IDs. + */ public static function listSongIds():Array { return songCache.keys().array(); } - public static function parseSongMetadata(songId:String):Array + /** + * Loads the song metadata for a particular song. + * @param songId The ID of the song to load. + * @return The song metadata for each variation, or an empty array if the song was not found. + */ + public static function loadSongMetadata(songId:String):Array { var result:Array = []; @@ -147,19 +156,13 @@ class SongDataParser result.push(songMetadata); - var variations = songMetadata.playData.songVariations; + var variations:Array = songMetadata.playData.songVariations; for (variation in variations) { - var variationJsonStr:String = loadSongMetadataFile(songId, variation); - var variationJsonData:Dynamic = null; - try - { - variationJsonData = Json.parse(variationJsonStr); - } - catch (e) {} - var variationSongMetadata:SongMetadata = SongMigrator.migrateSongMetadata(variationJsonData, '${songId}-${variation}'); - variationSongMetadata = SongValidator.validateSongMetadata(variationSongMetadata, '${songId}-${variation}'); + var variationRawJson:String = loadSongMetadataFile(songId, variation); + var variationSongMetadata:SongMetadata = SongMigrator.migrateSongMetadata(variationRawJson, '${songId}_${variation}'); + variationSongMetadata = SongValidator.validateSongMetadata(variationSongMetadata, '${songId}_${variation}'); if (variationSongMetadata != null) { variationSongMetadata.variation = variation; @@ -176,7 +179,7 @@ class SongDataParser var rawJson:String = Assets.getText(songMetadataFilePath).trim(); - while (!rawJson.endsWith("}")) + while (!rawJson.endsWith('}') && rawJson.length > 0) { rawJson = rawJson.substr(0, rawJson.length - 1); } @@ -214,7 +217,7 @@ class SongDataParser return rawJson; } - public static function parseSongChartData(songId:String, variation:String = ""):SongChartData + public static function parseSongChartData(songId:String, variation:String = ''):SongChartData { var rawJson:String = loadSongChartDataFile(songId, variation); var jsonData:Dynamic = null; @@ -222,7 +225,11 @@ class SongDataParser { jsonData = Json.parse(rawJson); } - catch (e) {} + catch (e) + { + trace('Failed to parse song chart data: ${songId} (${variation})'); + trace(e); + } var songChartData:SongChartData = SongMigrator.migrateSongChartData(jsonData, songId); songChartData = SongValidator.validateSongChartData(songChartData, songId); @@ -242,7 +249,7 @@ class SongDataParser var rawJson:String = Assets.getText(songChartDataFilePath).trim(); - while (!rawJson.endsWith("}")) + while (!rawJson.endsWith('}') && rawJson.length > 0) { rawJson = rawJson.substr(0, rawJson.length - 1); } @@ -310,7 +317,7 @@ abstract SongMetadata(RawSongMetadata) public function clone(?newVariation:String = null):SongMetadata { - var result = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation); + var result:SongMetadata = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation); result.version = this.version; result.timeFormat = this.timeFormat; result.divisions = this.divisions; @@ -391,12 +398,12 @@ abstract SongNoteData(RawSongNoteData) */ public var time(get, set):Float; - public function get_time():Float + function get_time():Float { return this.t; } - public function set_time(value:Float):Float + function set_time(value:Float):Float { return this.t = value; } @@ -406,7 +413,7 @@ abstract SongNoteData(RawSongNoteData) */ public var stepTime(get, never):Float; - public function get_stepTime():Float + function get_stepTime():Float { return Conductor.getTimeInSteps(abstract.time); } @@ -416,12 +423,12 @@ abstract SongNoteData(RawSongNoteData) */ public var data(get, set):Int; - public function get_data():Int + function get_data():Int { return this.d; } - public function set_data(value:Int):Int + function set_data(value:Int):Int { return this.d = value; } @@ -524,7 +531,7 @@ abstract SongNoteData(RawSongNoteData) return this.k; } - public function set_kind(value:String):String + function set_kind(value:String):String { if (value == 'normal' || value == '') value = null; return this.k = value; @@ -628,55 +635,55 @@ abstract SongEventData(RawSongEventData) public var time(get, set):Float; - public function get_time():Float + function get_time():Float { return this.t; } - public function set_time(value:Float):Float + function set_time(value:Float):Float { return this.t = value; } public var stepTime(get, never):Float; - public function get_stepTime():Float + function get_stepTime():Float { return Conductor.getTimeInSteps(abstract.time); } public var event(get, set):String; - public function get_event():String + function get_event():String { return this.e; } - public function set_event(value:String):String + function set_event(value:String):String { return this.e = value; } public var value(get, set):Dynamic; - public function get_value():Dynamic + function get_value():Dynamic { return this.v; } - public function set_value(value:Dynamic):Dynamic + function set_value(value:Dynamic):Dynamic { return this.v = value; } public var activated(get, set):Bool; - public function get_activated():Bool + function get_activated():Bool { return this.a; } - public function set_activated(value:Bool):Bool + function set_activated(value:Bool):Bool { return this.a = value; } @@ -755,7 +762,7 @@ abstract SongEventData(RawSongEventData) abstract SongPlayableChar(RawSongPlayableChar) { - public function new(girlfriend:String, opponent:String, inst:String = "") + public function new(girlfriend:String, opponent:String, inst:String = '') { this = { @@ -767,36 +774,36 @@ abstract SongPlayableChar(RawSongPlayableChar) public var girlfriend(get, set):String; - public function get_girlfriend():String + function get_girlfriend():String { return this.g; } - public function set_girlfriend(value:String):String + function set_girlfriend(value:String):String { return this.g = value; } public var opponent(get, set):String; - public function get_opponent():String + function get_opponent():String { return this.o; } - public function set_opponent(value:String):String + function set_opponent(value:String):String { return this.o = value; } public var inst(get, set):String; - public function get_inst():String + function get_inst():String { return this.i; } - public function set_inst(value:String):String + function set_inst(value:String):String { return this.i = value; } @@ -842,6 +849,35 @@ abstract SongChartData(RawSongChartData) return (result == 0.0) ? 1.0 : result; } + + public function setScrollSpeed(value:Float, diff:String = 'default'):Float + { + return this.scrollSpeed.set(diff, value); + } + + public function getNotes(diff:String):Array + { + var result:Array = this.notes.get(diff); + + if (result == null && diff != 'normal') return getNotes('normal'); + + return (result == null) ? [] : result; + } + + public function setNotes(value:Array, diff:String):Array + { + return this.notes.set(diff, value); + } + + public function getEvents():Array + { + return this.events; + } + + public function setEvents(value:Array):Array + { + return this.events = value; + } } typedef RawSongTimeChange = @@ -902,12 +938,12 @@ abstract SongTimeChange(RawSongTimeChange) from RawSongTimeChange public var timeStamp(get, set):Float; - public function get_timeStamp():Float + function get_timeStamp():Float { return this.t; } - public function set_timeStamp(value:Float):Float + function set_timeStamp(value:Float):Float { return this.t = value; } @@ -926,43 +962,43 @@ abstract SongTimeChange(RawSongTimeChange) from RawSongTimeChange public var bpm(get, set):Float; - public function get_bpm():Float + function get_bpm():Float { return this.bpm; } - public function set_bpm(value:Float):Float + function set_bpm(value:Float):Float { return this.bpm = value; } public var timeSignatureNum(get, set):Int; - public function get_timeSignatureNum():Int + function get_timeSignatureNum():Int { return this.n; } - public function set_timeSignatureNum(value:Int):Int + function set_timeSignatureNum(value:Int):Int { return this.n = value; } public var timeSignatureDen(get, set):Int; - public function get_timeSignatureDen():Int + function get_timeSignatureDen():Int { return this.d; } - public function set_timeSignatureDen(value:Int):Int + function set_timeSignatureDen(value:Int):Int { return this.d = value; } public var beatTuplets(get, set):Array; - public function get_beatTuplets():Array + function get_beatTuplets():Array { if (Std.isOfType(this.bt, Int)) { @@ -974,7 +1010,7 @@ abstract SongTimeChange(RawSongTimeChange) from RawSongTimeChange } } - public function set_beatTuplets(value:Array):Array + function set_beatTuplets(value:Array):Array { return this.bt = value; } @@ -982,7 +1018,7 @@ abstract SongTimeChange(RawSongTimeChange) from RawSongTimeChange enum abstract SongTimeFormat(String) from String to String { - var TICKS = "ticks"; - var FLOAT = "float"; - var MILLISECONDS = "ms"; + var TICKS = 'ticks'; + var FLOAT = 'float'; + var MILLISECONDS = 'ms'; } diff --git a/source/funkin/play/song/SongDataUtils.hx b/source/funkin/play/song/SongDataUtils.hx index f75972ee7..750d5f54b 100644 --- a/source/funkin/play/song/SongDataUtils.hx +++ b/source/funkin/play/song/SongDataUtils.hx @@ -101,9 +101,11 @@ class SongDataUtils * * Offset the provided array of notes such that the first note is at 0 milliseconds. */ - public static function buildNoteClipboard(notes:Array):Array + public static function buildNoteClipboard(notes:Array, ?timeOffset:Int = null):Array { - return offsetSongNoteData(sortNotes(notes), -Std.int(notes[0].time)); + if (notes.length == 0) return notes; + if (timeOffset == null) timeOffset = -Std.int(notes[0].time); + return offsetSongNoteData(sortNotes(notes), timeOffset); } /** @@ -111,9 +113,11 @@ class SongDataUtils * * Offset the provided array of events such that the first event is at 0 milliseconds. */ - public static function buildEventClipboard(events:Array):Array + public static function buildEventClipboard(events:Array, ?timeOffset:Int = null):Array { - return offsetSongEventData(sortEvents(events), -Std.int(events[0].time)); + if (events.length == 0) return events; + if (timeOffset == null) timeOffset = -Std.int(events[0].time); + return offsetSongEventData(sortEvents(events), timeOffset); } /** diff --git a/source/funkin/play/song/SongMigrator.hx b/source/funkin/play/song/SongMigrator.hx index 1872585d0..f561c4d3e 100644 --- a/source/funkin/play/song/SongMigrator.hx +++ b/source/funkin/play/song/SongMigrator.hx @@ -1,7 +1,11 @@ package funkin.play.song; +import funkin.play.song.formats.FNFLegacy; import funkin.play.song.SongData.SongChartData; +import funkin.play.song.SongData.SongEventData; import funkin.play.song.SongData.SongMetadata; +import funkin.play.song.SongData.SongNoteData; +import funkin.play.song.SongData.SongPlayableChar; import funkin.util.VersionUtil; class SongMigrator @@ -11,13 +15,22 @@ class SongMigrator * Handle breaking changes by incrementing this value * and adding migration to the SongMigrator class. */ - public static final CHART_VERSION:String = "2.0.0"; + public static final CHART_VERSION:String = '2.0.0'; - public static final CHART_VERSION_RULE:String = "2.0.x"; + /** + * Version rule for which chart versions are compatible with the current version. + */ + public static final CHART_VERSION_RULE:String = '2.0.x'; + /** + * Migrate song data from an older chart version to the current version. + * @param jsonData The song metadata to migrate. + * @param songId The ID of the song (only used for error reporting). + * @return The migrated song metadata, or null if the migration failed. + */ public static function migrateSongMetadata(jsonData:Dynamic, songId:String):SongMetadata { - if (jsonData.version) + if (jsonData.version != null) { if (VersionUtil.validateVersion(jsonData.version, CHART_VERSION_RULE)) { @@ -32,10 +45,11 @@ class SongMigrator trace('Song (${songId}) metadata version (${jsonData.version}) is outdated.'); switch (jsonData.version) { - // TODO: Add migration functions as cases here. + case '1.0.0': + return migrateSongMetadataFromLegacy(jsonData); default: - // Unknown version. - trace('Song (${songId}) unknown metadata version: ${jsonData.version}'); + trace('Song (${songId}) has unknown metadata version (${jsonData.version}), assuming FNF Legacy.'); + return migrateSongMetadataFromLegacy(jsonData); } } } @@ -46,6 +60,12 @@ class SongMigrator return null; } + /** + * Migrate song chart data from an older chart version to the current version. + * @param jsonData The song chart data to migrate. + * @param songId The ID of the song (only used for error reporting). + * @return The migrated song chart data, or null if the migration failed. + */ public static function migrateSongChartData(jsonData:Dynamic, songId:String):SongChartData { if (jsonData.version) @@ -76,4 +96,161 @@ class SongMigrator } return null; } + + /** + * Migrate song metadata from FNF Legacy chart version to the current version. + * @param jsonData The song metadata to migrate. + * @param songId The ID of the song (only used for error reporting). + * @return The migrated song metadata, or null if the migration failed. + */ + public static function migrateSongMetadataFromLegacy(jsonData:Dynamic, difficulty:String = 'normal'):SongMetadata + { + trace('Migrating song metadata from FNF Legacy.'); + + var songData:FNFLegacy = cast jsonData; + + var songMetadata:SongMetadata = new SongMetadata('Import', 'Kawai Sprite', 'default'); + + var hadError:Bool = false; + + // Set generatedBy string for debugging. + songMetadata.generatedBy = 'Chart Editor Import (FNF Legacy)'; + + try + { + // Set the song's BPM. + songMetadata.timeChanges[0].bpm = songData.song.bpm; + } + catch (e) + { + trace("Couldn't parse BPM!"); + hadError = true; + } + + try + { + // Set the song's stage. + songMetadata.playData.stage = songData.song.stageDefault; + } + catch (e) + { + trace("Couldn't parse stage!"); + hadError = true; + } + + try + { + // Set's the song's name. + songMetadata.songName = songData.song.song; + } + catch (e) + { + trace("Couldn't parse song name!"); + hadError = true; + } + + songMetadata.playData.difficulties = []; + if (songData.song != null && songData.song.notes != null) + { + if (Std.isOfType(songData.song.notes, Array)) + { + // One difficulty of notes. + songMetadata.playData.difficulties.push(difficulty); + } + else + { + // Multiple difficulties of notes. + var songNoteDataDynamic:haxe.DynamicAccess = cast songData.song.notes; + for (difficultyKey in songNoteDataDynamic.keys()) + { + songMetadata.playData.difficulties.push(difficultyKey); + } + } + } + else + { + trace("Couldn't parse difficulties!"); + hadError = true; + } + + songMetadata.playData.songVariations = []; + + // Set the song's song variations. + songMetadata.playData.playableChars = {}; + try + { + Reflect.setField(songMetadata.playData.playableChars, songData.song.player1, new SongPlayableChar('', songData.song.player2)); + } + catch (e) + { + trace("Couldn't parse characters!"); + hadError = true; + } + + return songMetadata; + } + + /** + * Migrate song chart data from FNF Legacy chart version to the current version. + * @param jsonData The song data to migrate. + * @param songId The ID of the song (only used for error reporting). + * @param difficulty The difficulty to migrate. + * @return The migrated song chart data, or null if the migration failed. + */ + public static function migrateSongChartDataFromLegacy(jsonData:Dynamic, difficulty:String = 'normal'):SongChartData + { + trace('Migrating song chart data from FNF Legacy.'); + + var songData:FNFLegacy = cast jsonData; + + var songChartData:SongChartData = new SongChartData(1.0, [], []); + + var songEventsEmpty:Bool = songChartData.getEvents() == null || songChartData.getEvents().length == 0; + if (songEventsEmpty) songChartData.setEvents(migrateSongEventDataFromLegacy(songData.song.notes)); + songChartData.setNotes(migrateSongNoteDataFromLegacy(songData.song.notes), difficulty); + songChartData.setScrollSpeed(songData.song.speed, difficulty); + + return songChartData; + } + + static function migrateSongNoteDataFromLegacy(sections:Array):Array + { + var songNotes:Array = []; + + for (section in sections) + { + // Skip empty sections. + if (section.sectionNotes.length == 0) continue; + + for (note in section.sectionNotes) + { + songNotes.push(new SongNoteData(note.time, note.getData(section.mustHitSection), note.length, note.kind)); + } + } + + return songNotes; + } + + static function migrateSongEventDataFromLegacy(sections:Array):Array + { + var songEvents:Array = []; + + var lastSectionWasMustHit:Null = null; + for (section in sections) + { + // Skip empty sections. + if (section.sectionNotes.length == 0) continue; + + if (section.mustHitSection != lastSectionWasMustHit) + { + lastSectionWasMustHit = section.mustHitSection; + + var firstNote:LegacyNote = section.sectionNotes[0]; + + songEvents.push(new SongEventData(firstNote.time, 'FocusCamera', {char: section.mustHitSection ? 0 : 1})); + } + } + + return songEvents; + } } diff --git a/source/funkin/play/song/SongSerializer.hx b/source/funkin/play/song/SongSerializer.hx index 968a7a1f5..a08b722da 100644 --- a/source/funkin/play/song/SongSerializer.hx +++ b/source/funkin/play/song/SongSerializer.hx @@ -82,9 +82,9 @@ class SongSerializer * Save a SongChartData object as a JSON file to an automatically generated path. * Works great on HTML5 and desktop. */ - public static function exportSongChartData(data:SongChartData) + public static function exportSongChartData(data:SongChartData, songId:String) { - var path = 'chart.json'; + var path = '${songId}-chart.json'; exportSongChartDataAs(path, data); } @@ -92,9 +92,9 @@ class SongSerializer * Save a SongMetadata object as a JSON file to an automatically generated path. * Works great on HTML5 and desktop. */ - public static function exportSongMetadata(data:SongMetadata) + public static function exportSongMetadata(data:SongMetadata, songId:String) { - var path = 'metadata.json'; + var path = '${songId}-metadata.json'; exportSongMetadataAs(path, data); } diff --git a/source/funkin/play/song/formats/FNFLegacy.hx b/source/funkin/play/song/formats/FNFLegacy.hx new file mode 100644 index 000000000..a64e461bd --- /dev/null +++ b/source/funkin/play/song/formats/FNFLegacy.hx @@ -0,0 +1,131 @@ +package funkin.play.song.formats; + +typedef FNFLegacy = +{ + var song:LegacySongData; +} + +typedef LegacySongData = +{ + var player1:String; // Boyfriend + var player2:String; // Opponent + + var speed:Float; + var stageDefault:String; + var bpm:Float; + var notes:Array; + var song:String; // Song name +}; + +typedef LegacyScrollSpeeds = +{ + var easy:Float; + var normal:Float; + var hard:Float; +}; + +typedef LegacyNoteData = +{ + /** + * The easy difficulty. + */ + var ?easy:Array; + + /** + * The normal difficulty. + */ + var ?normal:Array; + + /** + * The hard difficulty. + */ + var ?hard:Array; +}; + +typedef LegacyNoteSection = +{ + /** + * Whether the section is a must-hit section. + * If true, 0-3 are boyfriends notes, 4-7 are opponents notes. + * If false, 0-3 are opponents notes, 4-7 are boyfriends notes. + */ + var mustHitSection:Bool; + + /** + * Array of note data: + * - Direction + * - Time (ms) + * - Sustain Duration (ms) + * - Note kind (true = "alt", or string) + */ + var sectionNotes:Array; + + var typeOfSection:Int; + var lengthInSteps:Int; +} + +/** + * Notes in the old format are stored as an Array + */ +abstract LegacyNote(Array) +{ + public var time(get, set):Float; + + function get_time():Float + { + return this[0]; + } + + function set_time(value:Float):Float + { + return this[0] = value; + } + + public var data(get, set):Int; + + function get_data():Int + { + return this[1]; + } + + function set_data(value:Int):Int + { + return this[1] = value; + } + + public function getData(mustHitSection:Bool):Int + { + if (mustHitSection) return this[1]; + + return (this[1] + 4) % 8; + } + + public var length(get, set):Float; + + function get_length():Float + { + if (this.length < 3) return 0.0; + return this[2]; + } + + function set_length(value:Float):Float + { + return this[2] = value; + } + + public var kind(get, set):String; + + function get_kind():String + { + if (this.length < 4) return 'normal'; + + if (Std.isOfType(this[3], Bool)) return this[3] ? 'alt' : 'normal'; + + return this[3]; + } + + function set_kind(value:String):String + { + return this[3] = value; + } +} diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index 26c3a0ff2..5fb1022fe 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -85,6 +85,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass return globalOffsets = value; } + @:allow(funkin.ui.animDebugShit.DebugBoundingState) var animOffsets(default, set):Array = [0, 0]; public var originalPosition:FlxPoint = new FlxPoint(0, 0); diff --git a/source/funkin/ui/animDebugShit/DebugBoundingState.hx b/source/funkin/ui/animDebugShit/DebugBoundingState.hx index 5a7e555de..4e3d1dbf4 100644 --- a/source/funkin/ui/animDebugShit/DebugBoundingState.hx +++ b/source/funkin/ui/animDebugShit/DebugBoundingState.hx @@ -1,5 +1,7 @@ package funkin.ui.animDebugShit; +import funkin.util.SerializerUtil; +import funkin.play.character.CharacterData; import flixel.FlxCamera; import flixel.FlxSprite; import flixel.FlxState; @@ -32,6 +34,9 @@ import openfl.net.FileReference; import openfl.net.URLLoader; import openfl.net.URLRequest; import openfl.utils.ByteArray; +import funkin.input.Cursor; +import funkin.play.character.CharacterData.CharacterDataParser; +import funkin.util.SortUtil; using flixel.util.FlxSpriteUtil; @@ -71,7 +76,7 @@ class DebugBoundingState extends FlxState { Paths.setCurrentLevel('week1'); - var str = Paths.xml('ui/offset-editor-view'); + var str = Paths.xml('ui/animation-editor/offset-editor-view'); uiStuff = RuntimeComponentBuilder.fromAsset(str); // uiStuff.findComponent("btnViewSpriteSheet").onClick = _ -> curView = SPRITESHEET; @@ -109,6 +114,8 @@ class DebugBoundingState extends FlxState initSpritesheetView(); initOffsetView(); + Cursor.show(); + uiStuff.cameras = [hudCam]; add(uiStuff); @@ -124,7 +131,7 @@ class DebugBoundingState extends FlxState spriteSheetView = new FlxGroup(); add(spriteSheetView); - var tex = Paths.getSparrowAtlas('characters/temp'); + var tex = Paths.getSparrowAtlas('characters/BOYFRIEND'); // tex.frames[0].uv bf = new FlxSprite(); @@ -238,11 +245,15 @@ class DebugBoundingState extends FlxState txtOffsetShit.cameras = [hudCam]; offsetView.add(txtOffsetShit); - animDropDownMenu = new FlxUIDropDownMenu(630, 20, FlxUIDropDownMenu.makeStrIdLabelArray(['weed'], true)); + animDropDownMenu = new FlxUIDropDownMenu(0, 0, FlxUIDropDownMenu.makeStrIdLabelArray(['weed'], true)); animDropDownMenu.cameras = [hudCam]; + // Move to bottom right corner + animDropDownMenu.x = FlxG.width - animDropDownMenu.width - 20; + animDropDownMenu.y = FlxG.height - animDropDownMenu.height - 20; offsetView.add(animDropDownMenu); - var characters:Array = CoolUtil.coolTextFile(Paths.txt('characterList')); + var characters:Array = CharacterDataParser.listCharacterIds(); + characters.sort(SortUtil.alphabetically); var charDropdown:DropDown = cast uiStuff.findComponent('characterDropdown'); for (char in characters) @@ -264,19 +275,16 @@ class DebugBoundingState extends FlxState { if (FlxG.mouse.justPressed) { - mouseOffset.set(FlxG.mouse.x - -swagChar.offset.x, FlxG.mouse.y - -swagChar.offset.y); - // oldPos.set(swagChar.offset.x, swagChar.offset.y); - // oldPos.set(FlxG.mouse.x, FlxG.mouse.y); + mouseOffset.set(FlxG.mouse.x - -swagChar.animOffsets[0], FlxG.mouse.y - -swagChar.animOffsets[1]); } if (FlxG.mouse.pressed) { - swagChar.offset.x = (FlxG.mouse.x - mouseOffset.x) * -1; - swagChar.offset.y = (FlxG.mouse.y - mouseOffset.y) * -1; + swagChar.animOffsets = [(FlxG.mouse.x - mouseOffset.x) * -1, (FlxG.mouse.y - mouseOffset.y) * -1]; - swagChar.animationOffsets.set(animDropDownMenu.selectedLabel, [Std.int(swagChar.offset.x), Std.int(swagChar.offset.y)]); + swagChar.animationOffsets.set(animDropDownMenu.selectedLabel, swagChar.animOffsets); - txtOffsetShit.text = 'Offset: ' + swagChar.offset; + txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets; } } } @@ -291,6 +299,11 @@ class DebugBoundingState extends FlxState swagText.text = str + ": " + Std.string(value); } + function clearInfo() + { + txtGrp.clear(); + } + function checkLibrary(library:String) { trace(Assets.hasLibrary(library)); @@ -320,7 +333,7 @@ class DebugBoundingState extends FlxState { var lv:DropDown = cast uiStuff.findComponent("swapper"); lv.selectedIndex = 1; - curView = OFFSETSHIT; + curView = ANIMATIONS; if (swagChar != null) { FlxG.camera.focusOn(swagChar.getMidpoint()); @@ -334,7 +347,7 @@ class DebugBoundingState extends FlxState spriteSheetView.visible = true; offsetView.visible = false; offsetView.active = false; - case OFFSETSHIT: + case ANIMATIONS: spriteSheetView.visible = false; offsetView.visible = true; offsetView.active = true; @@ -344,6 +357,8 @@ class DebugBoundingState extends FlxState if (FlxG.keys.justPressed.H) hudCam.visible = !hudCam.visible; + if (FlxG.keys.justPressed.F4) FlxG.switchState(new MainMenuState()); + CoolUtil.mouseCamDrag(); CoolUtil.mouseWheelZoom(); @@ -364,14 +379,14 @@ class DebugBoundingState extends FlxState + 1); else animDropDownMenu.selectedId = Std.string(0); - animDropDownMenu.callback(animDropDownMenu.selectedId); + playCharacterAnimation(animDropDownMenu.selectedId, true); } if (FlxG.keys.justPressed.LBRACKET || FlxG.keys.justPressed.Q) { if (Std.parseInt(animDropDownMenu.selectedId) - 1 >= 0) animDropDownMenu.selectedId = Std.string(Std.parseInt(animDropDownMenu.selectedId) - 1); else animDropDownMenu.selectedId = Std.string(animDropDownMenu.length - 1); - animDropDownMenu.callback(animDropDownMenu.selectedId); + playCharacterAnimation(animDropDownMenu.selectedId, true); } // Keyboards controls for general WASD "movement" @@ -379,16 +394,29 @@ class DebugBoundingState extends FlxState // and then it's just played and updated from the animDropDownMenu callback, which is set in the loadAnimShit() function probabbly if (FlxG.keys.justPressed.W || FlxG.keys.justPressed.S || FlxG.keys.justPressed.D || FlxG.keys.justPressed.A) { - var missShit:String = ''; + var suffix:String = ''; + var targetLabel:String = ''; - if (FlxG.keys.pressed.SHIFT) missShit = 'miss'; + if (FlxG.keys.pressed.SHIFT) suffix = 'miss'; - if (FlxG.keys.justPressed.W) animDropDownMenu.selectedLabel = 'singUP' + missShit; - if (FlxG.keys.justPressed.S) animDropDownMenu.selectedLabel = 'singDOWN' + missShit; - if (FlxG.keys.justPressed.A) animDropDownMenu.selectedLabel = 'singLEFT' + missShit; - if (FlxG.keys.justPressed.D) animDropDownMenu.selectedLabel = 'singRIGHT' + missShit; + if (FlxG.keys.justPressed.W) targetLabel = 'singUP$suffix'; + if (FlxG.keys.justPressed.S) targetLabel = 'singDOWN$suffix'; + if (FlxG.keys.justPressed.A) targetLabel = 'singLEFT$suffix'; + if (FlxG.keys.justPressed.D) targetLabel = 'singRIGHT$suffix'; - animDropDownMenu.callback(animDropDownMenu.selectedId); + if (targetLabel != animDropDownMenu.selectedLabel) + { + // Play the new animation if the IDs are the different. + // Override the onion skin. + animDropDownMenu.selectedLabel = targetLabel; + playCharacterAnimation(animDropDownMenu.selectedId, true); + } + else + { + // Replay the current animation if the IDs are the same. + // Don't override the onion skin. + playCharacterAnimation(animDropDownMenu.selectedId, false); + } } if (FlxG.keys.justPressed.F) @@ -400,16 +428,16 @@ class DebugBoundingState extends FlxState if (FlxG.keys.justPressed.SPACE) { animDropDownMenu.selectedLabel = 'idle'; - animDropDownMenu.callback(animDropDownMenu.selectedId); + playCharacterAnimation(animDropDownMenu.selectedId, true); } // Playback the animation - if (FlxG.keys.justPressed.ENTER) animDropDownMenu.callback(animDropDownMenu.selectedId); + if (FlxG.keys.justPressed.ENTER) playCharacterAnimation(animDropDownMenu.selectedId, false); if (FlxG.keys.justPressed.RIGHT || FlxG.keys.justPressed.LEFT || FlxG.keys.justPressed.UP || FlxG.keys.justPressed.DOWN) { var animName = animDropDownMenu.selectedLabel; - var coolValues:Array = swagChar.animationOffsets.get(animName); + var coolValues:Array = swagChar.animationOffsets.get(animName).copy(); var multiplier:Int = 5; @@ -432,18 +460,38 @@ class DebugBoundingState extends FlxState if (FlxG.keys.justPressed.ESCAPE) { - var outputString:String = ""; - - for (i in swagChar.animationOffsets.keys()) - { - outputString += i + " " + swagChar.animationOffsets.get(i)[0] + " " + swagChar.animationOffsets.get(i)[1] + "\n"; - } - - outputString.trim(); - saveOffsets(outputString); + var outputString = FlxG.keys.pressed.CONTROL ? buildOutputStringOld() : buildOutputStringNew(); + saveOffsets(outputString, FlxG.keys.pressed.CONTROL ? swagChar.characterId + "Offsets.txt" : swagChar.characterId + ".json"); } } + function buildOutputStringOld():String + { + var outputString:String = ""; + + for (i in swagChar.animationOffsets.keys()) + { + outputString += i + " " + swagChar.animationOffsets.get(i)[0] + " " + swagChar.animationOffsets.get(i)[1] + "\n"; + } + + outputString.trim(); + + return outputString; + } + + function buildOutputStringNew():String + { + var charData:CharacterData = Reflect.copy(swagChar._data); + + for (charDataAnim in charData.animations) + { + var animName:String = charDataAnim.name; + charDataAnim.offsets = swagChar.animationOffsets.get(animName); + } + + return SerializerUtil.toJSON(charData, true); + } + var swagChar:BaseCharacter; /* @@ -466,35 +514,51 @@ class DebugBoundingState extends FlxState generateOutlines(swagChar.frames.frames); bf.pixels = swagChar.pixels; - var animThing:Array = []; + clearInfo(); + addInfo(swagChar._data.assetPath, ""); + addInfo('Width', bf.width); + addInfo('Height', bf.height); + + characterAnimNames = []; for (i in swagChar.animationOffsets.keys()) { - animThing.push(i); + characterAnimNames.push(i); trace(i); trace(swagChar.animationOffsets[i]); } - animDropDownMenu.setData(FlxUIDropDownMenu.makeStrIdLabelArray(animThing, true)); + animDropDownMenu.setData(FlxUIDropDownMenu.makeStrIdLabelArray(characterAnimNames, true)); animDropDownMenu.callback = function(str:String) { + playCharacterAnimation(str, true); + }; + txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets; + dropDownSetup = true; + } + + private var characterAnimNames:Array; + + function playCharacterAnimation(str:String, setOnionSkin:Bool = true) + { + if (setOnionSkin) + { // clears the canvas onionSkinChar.pixels.fillRect(new Rectangle(0, 0, FlxG.width * 2, FlxG.height * 2), 0x00000000); - onionSkinChar.stamp(swagChar, Std.int(swagChar.x - swagChar.offset.x), Std.int(swagChar.y - swagChar.offset.y)); + onionSkinChar.stamp(swagChar, Std.int(swagChar.x), Std.int(swagChar.y)); onionSkinChar.alpha = 0.6; + } - var animName = animThing[Std.parseInt(str)]; - swagChar.playAnimation(animName, true); // trace(); - trace(swagChar.animationOffsets.get(animName)); + var animName = characterAnimNames[Std.parseInt(str)]; + swagChar.playAnimation(animName, true); // trace(); + trace(swagChar.animationOffsets.get(animName)); - txtOffsetShit.text = 'Offset: ' + swagChar.offset; - }; - dropDownSetup = true; + txtOffsetShit.text = 'Offset: ' + swagChar.animOffsets; } var _file:FileReference; - function saveOffsets(saveString:String) + function saveOffsets(saveString:String, fileName:String) { if ((saveString != null) && (saveString.length > 0)) { @@ -502,7 +566,7 @@ class DebugBoundingState extends FlxState _file.addEventListener(Event.COMPLETE, onSaveComplete); _file.addEventListener(Event.CANCEL, onSaveCancel); _file.addEventListener(IOErrorEvent.IO_ERROR, onSaveError); - _file.save(saveString, swagChar.characterId + "Offsets.txt"); + _file.save(saveString,); } } @@ -542,5 +606,5 @@ class DebugBoundingState extends FlxState enum abstract ANIMDEBUGVIEW(String) { var SPRITESHEET; - var OFFSETSHIT; + var ANIMATIONS; } diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index 9453c8c94..bb08e8d6b 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -1,14 +1,22 @@ package funkin.ui.debug.charting; +import funkin.play.character.CharacterData; +import funkin.util.Constants; +import funkin.util.SerializerUtil; +import funkin.play.song.SongData.SongChartData; +import funkin.play.song.SongData.SongMetadata; import flixel.util.FlxTimer; import funkin.util.SortUtil; import funkin.input.Cursor; import funkin.play.character.BaseCharacter; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.song.Song; +import funkin.play.song.SongMigrator; +import funkin.play.song.SongValidator; import funkin.play.song.SongData.SongDataParser; import funkin.play.song.SongData.SongPlayableChar; import funkin.play.song.SongData.SongTimeChange; +import funkin.util.FileUtil; import haxe.io.Path; import haxe.ui.components.Button; import haxe.ui.components.DropDown; @@ -41,6 +49,9 @@ class ChartEditorDialogHandler static final CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata-chargroup'); 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_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'); /** @@ -72,42 +83,31 @@ class ChartEditorDialogHandler // // Create Song Wizard // + openCreateSongWizard(state, false); + } - // Step 1. Upload Instrumental - var uploadInstDialog:Dialog = openUploadInstDialog(state, false); - uploadInstDialog.onDialogClosed = function(_event) { - state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) - { - // Step 2. Song Metadata - var songMetadataDialog:Dialog = openSongMetadataDialog(state); - songMetadataDialog.onDialogClosed = function(_event) { - state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) - { - // Step 3. Upload Vocals - // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. - openUploadVocalsDialog(state, false); // var uploadVocalsDialog:Dialog - } - else - { - // User cancelled the wizard! Back to the welcome dialog. - openWelcomeDialog(state); - } - }; - } - else - { - // User cancelled the wizard! Back to the welcome dialog. - openWelcomeDialog(state); - } - }; + var linkImportChartLegacy:Link = dialog.findComponent('splashImportChartLegacy', Link); + linkImportChartLegacy.onClick = function(_event) { + // Hide the welcome dialog + dialog.hideDialog(DialogButton.CANCEL); + + // Open the "Import Chart" dialog + openImportChartWizard(state, 'legacy', false); + }; + + var buttonBrowse:Button = dialog.findComponent('splashBrowse', Button); + buttonBrowse.onClick = function(_event) { + // Hide the welcome dialog + dialog.hideDialog(DialogButton.CANCEL); + + // Open the "Open Chart" dialog + openBrowseWizard(state, false); } var splashTemplateContainer:VBox = dialog.findComponent('splashTemplateContainer', VBox); var songList:Array = SongDataParser.listSongIds(); - songList.sort(SortUtil.alphabetical); + songList.sort(SortUtil.alphabetically); for (targetSongId in songList) { @@ -132,6 +132,120 @@ class ChartEditorDialogHandler return dialog; } + /** + * Open the wizard for opening an existing chart from individual files. + * @param state + * @param closable + */ + public static function openBrowseWizard(state:ChartEditorState, closable:Bool):Void + { + // Open the "Open Chart" wizard + // Step 1. Open Chart + var openChartDialog:Dialog = openChartDialog(state); + openChartDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 2. Upload instrumental + var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); + uploadInstDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 3. Upload Vocals + // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. + var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog + uploadVocalsDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + state.postLoadInstrumental(); + } + } + else + { + // User cancelled the wizard! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + else + { + // User cancelled the wizard! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + + public static function openImportChartWizard(state:ChartEditorState, format:String, closable:Bool):Void + { + // Open the "Open Chart" wizard + // Step 1. Open Chart + var openChartDialog:Dialog = openImportChartDialog(state, format); + openChartDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 2. Upload instrumental + var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); + uploadInstDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 3. Upload Vocals + // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. + var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog + uploadVocalsDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + state.postLoadInstrumental(); + } + } + else + { + // User cancelled the wizard! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + else + { + // User cancelled the wizard! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + + public static function openCreateSongWizard(state:ChartEditorState, closable:Bool):Void + { + // Step 1. Upload Instrumental + var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); + uploadInstDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 2. Song Metadata + var songMetadataDialog:Dialog = openSongMetadataDialog(state); + songMetadataDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 3. Upload Vocals + // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. + openUploadVocalsDialog(state, false); // var uploadVocalsDialog:Dialog + } + else + { + // User cancelled the wizard! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + else + { + // User cancelled the wizard! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + /** * Builds and opens a dialog where the user uploads an instrumental for the current song. * @param state The current chart editor state. @@ -216,11 +330,20 @@ class ChartEditorDialogHandler } else { + var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext)) + { + 'File format (${path.ext}) not supported for instrumental track (${path.file}.${path.ext})'; + } + else + { + 'Failed to load instrumental track (${path.file}.${path.ext})'; + } + // Tell the user the load was successful. NotificationManager.instance.addNotification( { title: 'Failure', - body: 'Failed to load instrumental track (${path.file}.${path.ext})', + body: message, type: NotificationType.Error, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); @@ -420,12 +543,6 @@ class ChartEditorDialogHandler moveCharGroup(event.data.id); }; - if (key == null) - { - // Find the next available player character. - trace(charGroupPlayer.dataSource.data); - } - var charGroupOpponent:DropDown = charGroup.findComponent('charGroupOpponent', DropDown); charGroupOpponent.onChange = function(event:UIEvent) { charData.opponent = event.data.id; @@ -483,8 +600,8 @@ class ChartEditorDialogHandler for (charKey in charIdsForVocals) { trace('Adding vocal upload for character ${charKey}'); - var charMetadata:BaseCharacter = CharacterDataParser.fetchCharacter(charKey); - var charName:String = charMetadata.characterName; + var charMetadata:CharacterData = CharacterDataParser.fetchCharacterData(charKey); + var charName:String = charMetadata != null ? charMetadata.name : charKey; var vocalsEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT); @@ -511,11 +628,20 @@ class ChartEditorDialogHandler } else { + var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext)) + { + 'File format (${path.ext}) not supported for vocal track (${path.file}.${path.ext})'; + } + else + { + 'Failed to load vocal track (${path.file}.${path.ext})'; + } + // Vocals failed to load. NotificationManager.instance.addNotification( { title: 'Failure', - body: 'Failed to load vocal track (${path.file}.${path.ext})', + body: message, type: NotificationType.Error, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); @@ -552,6 +678,284 @@ class ChartEditorDialogHandler return dialog; } + /** + * Builds and opens a dialog where the user upload the JSON files for a song. + * @param state The current chart editor state. + * @param closable Whether the dialog can be closed by the user. + * @return The dialog that was opened. + */ + @:haxe.warning('-WVarInit') + public static function openChartDialog(state:ChartEditorState, ?closable:Bool = true):Dialog + { + var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_OPEN_CHART_LAYOUT, true, closable); + + var buttonCancel:Button = dialog.findComponent('dialogCancel', Button); + buttonCancel.onClick = function(_event) { + dialog.hideDialog(DialogButton.CANCEL); + } + + var chartContainerA:Component = dialog.findComponent('chartContainerA'); + var chartContainerB:Component = dialog.findComponent('chartContainerB'); + + var songMetadata:Map = []; + var songChartData:Map = []; + + var buttonContinue:Button = dialog.findComponent('dialogContinue', Button); + buttonContinue.onClick = function(_event) { + state.loadSong(songMetadata, songChartData); + + dialog.hideDialog(DialogButton.APPLY); + } + + var onDropFileMetadataVariation:String->Label->String->Void; + var onClickMetadataVariation:String->Label->UIEvent->Void; + var onDropFileChartDataVariation:String->Label->String->Void; + var onClickChartDataVariation:String->Label->UIEvent->Void; + + var constructVariationEntries:Array->Void = function(variations:Array) { + // Clear the chart container. + while (chartContainerB.getComponentAt(0) != null) + { + chartContainerB.removeComponent(chartContainerB.getComponentAt(0)); + } + + // Build an entry for -chart.json. + var songDefaultChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); + var songDefaultChartDataEntryLabel:Label = songDefaultChartDataEntry.findComponent('chartEntryLabel', Label); + songDefaultChartDataEntryLabel.text = 'Drag and drop -chart.json file, or click to browse.'; + + songDefaultChartDataEntry.onClick = onClickChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel); + addDropHandler(songDefaultChartDataEntry, onDropFileChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel)); + chartContainerB.addComponent(songDefaultChartDataEntry); + + for (variation in variations) + { + // Build entries for -metadata-.json. + var songVariationMetadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); + var songVariationMetadataEntryLabel:Label = songVariationMetadataEntry.findComponent('chartEntryLabel', Label); + songVariationMetadataEntryLabel.text = 'Drag and drop -metadata-${variation}.json file, or click to browse.'; + + songVariationMetadataEntry.onClick = onClickMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel); + addDropHandler(songVariationMetadataEntry, onDropFileMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel)); + chartContainerB.addComponent(songVariationMetadataEntry); + + // Build entries for -chart-.json. + var songVariationChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); + var songVariationChartDataEntryLabel:Label = songVariationChartDataEntry.findComponent('chartEntryLabel', Label); + songVariationChartDataEntryLabel.text = 'Drag and drop -chart-${variation}.json file, or click to browse.'; + + songVariationChartDataEntry.onClick = onClickChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel); + addDropHandler(songVariationChartDataEntry, onDropFileChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel)); + chartContainerB.addComponent(songVariationChartDataEntry); + } + } + + onDropFileMetadataVariation = function(variation:String, label:Label, pathStr:String) { + var path:Path = new Path(pathStr); + trace('Dropped JSON file (${path})'); + + var songMetadataJson:Dynamic = FileUtil.readJSONFromPath(path.toString()); + var songMetadataVariation:SongMetadata = SongMigrator.migrateSongMetadata(songMetadataJson, 'import'); + songMetadataVariation = SongValidator.validateSongMetadata(songMetadataVariation, 'import'); + + songMetadata.set(variation, songMetadataVariation); + + // Tell the user the load was successful. + NotificationManager.instance.addNotification( + { + title: 'Success', + body: 'Loaded metadata file (${path.file}.${path.ext})', + type: NotificationType.Success, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + + label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}'; + + if (variation == Constants.DEFAULT_VARIATION) constructVariationEntries(songMetadataVariation.playData.songVariations); + }; + + onClickMetadataVariation = function(variation:String, label:Label, _event:UIEvent) { + Dialogs.openBinaryFile('Open Chart ($variation) Metadata', [ + {label: 'JSON File (.json)', extension: 'json'}], function(selectedFile) { + if (selectedFile != null) + { + trace('Selected file: ' + selectedFile.name); + + var songMetadataJson:Dynamic = SerializerUtil.fromJSONBytes(selectedFile.bytes); + var songMetadataVariation:SongMetadata = SongMigrator.migrateSongMetadata(songMetadataJson, 'import'); + songMetadataVariation = SongValidator.validateSongMetadata(songMetadataVariation, 'import'); + songMetadataVariation.variation = variation; + + songMetadata.set(variation, songMetadataVariation); + + // Tell the user the load was successful. + NotificationManager.instance.addNotification( + { + title: 'Success', + body: 'Loaded metadata file (${selectedFile.name})', + type: NotificationType.Success, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + + label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}'; + + if (variation == Constants.DEFAULT_VARIATION) constructVariationEntries(songMetadataVariation.playData.songVariations); + } + }); + } + + onDropFileChartDataVariation = function(variation:String, label:Label, pathStr:String) { + var path:Path = new Path(pathStr); + trace('Dropped JSON file (${path})'); + + var songChartDataJson:Dynamic = FileUtil.readJSONFromPath(path.toString()); + var songChartDataVariation:SongChartData = SongMigrator.migrateSongChartData(songChartDataJson, 'import'); + songChartDataVariation = SongValidator.validateSongChartData(songChartDataVariation, 'import'); + + songChartData.set(variation, songChartDataVariation); + + // Tell the user the load was successful. + NotificationManager.instance.addNotification( + { + title: 'Success', + body: 'Loaded chart data file (${path.file}.${path.ext})', + type: NotificationType.Success, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + + label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}'; + }; + + onClickChartDataVariation = function(variation:String, label:Label, _event:UIEvent) { + Dialogs.openBinaryFile('Open Chart ($variation) Metadata', [ + {label: 'JSON File (.json)', extension: 'json'}], function(selectedFile) { + if (selectedFile != null) + { + trace('Selected file: ' + selectedFile.name); + + var songChartDataJson:Dynamic = SerializerUtil.fromJSONBytes(selectedFile.bytes); + var songChartDataVariation:SongChartData = SongMigrator.migrateSongChartData(songChartDataJson, 'import'); + songChartDataVariation = SongValidator.validateSongChartData(songChartDataVariation, 'import'); + + songChartData.set(variation, songChartDataVariation); + + // Tell the user the load was successful. + NotificationManager.instance.addNotification( + { + title: 'Success', + body: 'Loaded chart data file (${selectedFile.name})', + type: NotificationType.Success, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + + label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}'; + } + }); + } + + var metadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); + var metadataEntryLabel:Label = metadataEntry.findComponent('chartEntryLabel', Label); + metadataEntryLabel.text = 'Drag and drop -metadata.json file, or click to browse.'; + + metadataEntry.onClick = onClickMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel); + addDropHandler(metadataEntry, onDropFileMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel)); + + chartContainerA.addComponent(metadataEntry); + + return dialog; + } + + /** + * Builds and opens a dialog where the user can import a chart from an existing file format. + * @param state The current chart editor state. + * @param format The format to import from. + * @param closable + * @return Dialog + */ + public static function openImportChartDialog(state:ChartEditorState, format:String, ?closable:Bool = true):Dialog + { + var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT, true, closable); + + var prettyFormat:String = switch (format) + { + case 'legacy': 'FNF Legacy'; + default: 'Unknown'; + } + + var fileFilter = switch (format) + { + case 'legacy': {label: 'JSON Data File (.json)', extension: 'json'}; + default: null; + } + + dialog.title = 'Import Chart - ${prettyFormat}'; + + var buttonCancel:Button = dialog.findComponent('dialogCancel', Button); + + buttonCancel.onClick = function(_event) { + dialog.hideDialog(DialogButton.CANCEL); + } + + var importBox:Box = dialog.findComponent('importBox', Box); + + importBox.onMouseOver = function(_event) { + importBox.swapClass('upload-bg', 'upload-bg-hover'); + Cursor.cursorMode = Pointer; + } + + importBox.onMouseOut = function(_event) { + importBox.swapClass('upload-bg-hover', 'upload-bg'); + Cursor.cursorMode = Default; + } + + var onDropFile:String->Void; + + importBox.onClick = function(_event) { + Dialogs.openBinaryFile('Import Chart - ${prettyFormat}', [fileFilter], function(selectedFile:SelectedFileInfo) { + if (selectedFile != null) + { + trace('Selected file: ' + selectedFile.fullPath); + var selectedFileJson:Dynamic = SerializerUtil.fromJSONBytes(selectedFile.bytes); + var songMetadata:SongMetadata = SongMigrator.migrateSongMetadataFromLegacy(selectedFileJson); + var songChartData:SongChartData = SongMigrator.migrateSongChartDataFromLegacy(selectedFileJson); + + state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]); + + dialog.hideDialog(DialogButton.APPLY); + NotificationManager.instance.addNotification( + { + title: 'Success', + body: 'Loaded chart file (${selectedFile.name})', + type: NotificationType.Success, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + } + }); + } + + onDropFile = function(pathStr:String) { + var path:Path = new Path(pathStr); + var selectedFileJson:Dynamic = FileUtil.readJSONFromPath(path.toString()); + var songMetadata:SongMetadata = SongMigrator.migrateSongMetadataFromLegacy(selectedFileJson); + var songChartData:SongChartData = SongMigrator.migrateSongChartDataFromLegacy(selectedFileJson); + + state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]); + + dialog.hideDialog(DialogButton.APPLY); + NotificationManager.instance.addNotification( + { + title: 'Success', + body: 'Loaded chart file (${path.file}.${path.ext})', + type: NotificationType.Success, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + }; + + addDropHandler(importBox, onDropFile); + + return dialog; + } + /** * Builds and opens a dialog displaying the user guide, providing guidance and help on how to use the chart editor. * @@ -571,6 +975,8 @@ class ChartEditorDialogHandler static function openDialog(state:ChartEditorState, key:String, modal:Bool = true, closable:Bool = true):Dialog { var dialog:Dialog = cast state.buildComponent(key); + if (dialog == null) return null; + dialog.destroyOnClose = true; dialog.closable = closable; dialog.showDialog(modal); diff --git a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx index bc68709c5..768e0be52 100644 --- a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx +++ b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx @@ -31,7 +31,7 @@ class ChartEditorEventSprite extends FlxSprite /** * The image used for all song events. Cached for performance. */ - var eventGraphic:BitmapData; + static var eventSpriteBasic:BitmapData; public function new(parent:ChartEditorState) { diff --git a/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx b/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx new file mode 100644 index 000000000..27951f079 --- /dev/null +++ b/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx @@ -0,0 +1,144 @@ +package funkin.ui.debug.charting; + +import funkin.play.song.SongData.SongEventData; +import funkin.play.song.SongData.SongNoteData; +import flixel.math.FlxMath; +import flixel.FlxSprite; +import flixel.util.FlxColor; +import flixel.util.FlxSpriteUtil; + +/** + * Handles the note scrollbar preview in the chart editor. + */ +class ChartEditorNotePreview extends FlxSprite +{ + // + // Constants + // + static final NOTE_WIDTH:Int = 5; + static final NOTE_HEIGHT:Int = 1; + static final WIDTH:Int = NOTE_WIDTH * 9; + + static final BG_COLOR:FlxColor = FlxColor.GRAY; + static final LEFT_COLOR:FlxColor = 0xFFFF22AA; + static final DOWN_COLOR:FlxColor = 0xFF00EEFF; + static final UP_COLOR:FlxColor = 0xFF00CC00; + static final RIGHT_COLOR:FlxColor = 0xFFCC1111; + static final EVENT_COLOR:FlxColor = 0xFF111111; + + var previewHeight:Int; + + public function new(height:Int) + { + super(0, 0); + this.previewHeight = height; + buildBackground(); + } + + /** + * Build the initial sprite for the preview. + */ + function buildBackground():Void + { + makeGraphic(WIDTH, 0, BG_COLOR); + } + + /** + * Erase all notes from the preview. + */ + public function erase():Void + { + drawRect(0, 0, WIDTH, previewHeight, BG_COLOR); + } + + /** + * Add a single note to the preview. + * @param note The data for the note. + * @param songLengthInMs The total length of the song in milliseconds. + */ + public function addNote(note:SongNoteData, songLengthInMs:Int):Void + { + var noteDir:Int = note.getDirection(); + var mustHit:Bool = note.getStrumlineIndex() == 0; + drawNote(noteDir, mustHit, Std.int(note.time), songLengthInMs); + } + + /** + * Add a song event to the preview. + * @param event The data for the event. + * @param songLengthInMs The total length of the song in milliseconds. + */ + public function addEvent(event:SongEventData, songLengthInMs:Int):Void + { + drawNote(-1, false, Std.int(event.time), songLengthInMs); + } + + /** + * Add an array of notes to the preview. + * @param notes The data for the notes. + * @param songLengthInMs The total length of the song in milliseconds. + */ + public function addNotes(notes:Array, songLengthInMs:Int):Void + { + for (note in notes) + { + addNote(note, songLengthInMs); + } + } + + /** + * Add an array of events to the preview. + * @param events The data for the events. + * @param songLengthInMs The total length of the song in milliseconds. + */ + public function addEvents(events:Array, songLengthInMs:Int):Void + { + for (event in events) + { + addEvent(event, songLengthInMs); + } + } + + /** + * Draws a note on the preview. + * @param dir Note data. + * @param mustHit False if opponent, true if player. + * @param strumTimeInMs Time in milliseconds to strum the note. + * @param songLengthInMs Length of the song in milliseconds. + */ + function drawNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int):Void + { + var color:FlxColor = switch (dir) + { + case 0: LEFT_COLOR; + case 1: DOWN_COLOR; + case 2: UP_COLOR; + case 3: RIGHT_COLOR; + default: EVENT_COLOR; + }; + + var noteX:Float = NOTE_WIDTH * dir; + if (mustHit) noteX += NOTE_WIDTH * 4; + if (dir == -1) noteX = NOTE_WIDTH * 8; + + var noteY:Float = FlxMath.remapToRange(strumTimeInMs, 0, songLengthInMs, 0, previewHeight); + + drawRect(noteX, noteY, NOTE_WIDTH, NOTE_HEIGHT, color); + } + + function eraseNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int):Void + { + var noteX:Float = NOTE_WIDTH * dir; + if (mustHit) noteX += NOTE_WIDTH * 4; + if (dir == -1) noteX = NOTE_WIDTH * 8; + + var noteY:Float = FlxMath.remapToRange(strumTimeInMs, 0, songLengthInMs, 0, previewHeight); + + drawRect(noteX, noteY, NOTE_WIDTH, NOTE_HEIGHT, BG_COLOR); + } + + inline function drawRect(noteX:Float, noteY:Float, width:Int, height:Int, color:FlxColor):Void + { + FlxSpriteUtil.drawRect(this, noteX, noteY, width, height, color); + } +} diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 2238fff3f..e1a55f947 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -7,16 +7,21 @@ import flixel.group.FlxSpriteGroup; import flixel.input.keyboard.FlxKey; import flixel.math.FlxPoint; import flixel.math.FlxRect; -import flixel.sound.FlxSound; +import flixel.system.FlxSound; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.tweens.misc.VarTween; import flixel.util.FlxColor; import flixel.util.FlxSort; import flixel.util.FlxTimer; import funkin.audio.visualize.PolygonSpectogram; import funkin.audio.VoicesGroup; import funkin.data.notestyle.NoteStyleRegistry; +import funkin.data.notestyle.NoteStyleRegistry; import funkin.input.Cursor; import funkin.input.TurboKeyHandler; import funkin.modding.events.ScriptEvent; +import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.HealthIcon; import funkin.play.notes.NoteSprite; import funkin.play.notes.Strumline; @@ -26,22 +31,26 @@ import funkin.play.song.SongData.SongDataParser; import funkin.play.song.SongData.SongEventData; import funkin.play.song.SongData.SongMetadata; import funkin.play.song.SongData.SongNoteData; +import funkin.play.song.SongData.SongPlayableChar; import funkin.play.song.SongDataUtils; import funkin.ui.debug.charting.ChartEditorCommand; +import funkin.ui.debug.charting.ChartEditorCommand; import funkin.ui.debug.charting.ChartEditorThemeHandler.ChartEditorTheme; import funkin.ui.debug.charting.ChartEditorToolboxHandler.ChartEditorToolMode; import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.HaxeUIState; +import funkin.util.Constants; import funkin.util.DateUtil; import funkin.util.FileUtil; import funkin.util.SerializerUtil; import funkin.util.SortUtil; import funkin.util.WindowUtil; import haxe.DynamicAccess; +import haxe.io.Bytes; import haxe.io.Path; import haxe.ui.components.Label; import haxe.ui.components.Slider; -import haxe.ui.containers.dialogs.Dialog; +import haxe.ui.containers.dialogs.CollapsibleDialog; import haxe.ui.containers.menus.MenuItem; import haxe.ui.containers.TreeView; import haxe.ui.containers.TreeViewNode; @@ -51,6 +60,7 @@ import haxe.ui.events.DragEvent; import haxe.ui.events.UIEvent; import haxe.ui.notifications.NotificationManager; import haxe.ui.notifications.NotificationType; +import openfl.Assets; import openfl.display.BitmapData; import openfl.geom.Rectangle; @@ -65,6 +75,7 @@ using Lambda; * @author MasterEric */ // Give other classes access to private instance fields + @:allow(funkin.ui.debug.charting.ChartEditorCommand) @:allow(funkin.ui.debug.charting.ChartEditorDialogHandler) @:allow(funkin.ui.debug.charting.ChartEditorThemeHandler) @@ -76,19 +87,19 @@ class ChartEditorState extends HaxeUIState */ // ============================== // XML Layouts - static final CHART_EDITOR_LAYOUT = Paths.ui('chart-editor/main-view'); + static final CHART_EDITOR_LAYOUT:String = Paths.ui('chart-editor/main-view'); - static final CHART_EDITOR_NOTIFBAR_LAYOUT = Paths.ui('chart-editor/components/notifbar'); - static final CHART_EDITOR_PLAYBARHEAD_LAYOUT = Paths.ui('chart-editor/components/playbar-head'); + static final CHART_EDITOR_NOTIFBAR_LAYOUT:String = Paths.ui('chart-editor/components/notifbar'); + static final CHART_EDITOR_PLAYBARHEAD_LAYOUT:String = Paths.ui('chart-editor/components/playbar-head'); - static final CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT = Paths.ui('chart-editor/toolbox/tools'); - static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT = Paths.ui('chart-editor/toolbox/notedata'); - static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT = Paths.ui('chart-editor/toolbox/eventdata'); - static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT = Paths.ui('chart-editor/toolbox/metadata'); - static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT = Paths.ui('chart-editor/toolbox/difficulty'); - static final CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT = Paths.ui('chart-editor/toolbox/characters'); - static final CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT = Paths.ui('chart-editor/toolbox/player-preview'); - static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT = Paths.ui('chart-editor/toolbox/opponent-preview'); + static final CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:String = Paths.ui('chart-editor/toolbox/tools'); + static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata'); + static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata'); + static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata'); + static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/toolbox/difficulty'); + static final CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:String = Paths.ui('chart-editor/toolbox/characters'); + static final CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/player-preview'); + static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:String = Paths.ui('chart-editor/toolbox/opponent-preview'); // Validation static final SUPPORTED_MUSIC_FORMATS:Array = ['ogg']; @@ -107,7 +118,7 @@ class ChartEditorState extends HaxeUIState /** * Number of notes in each player's strumline. */ - public static final STRUMLINE_SIZE = 4; + public static final STRUMLINE_SIZE:Int = 4; /** * The height of the menu bar in the layout. @@ -134,10 +145,10 @@ class ChartEditorState extends HaxeUIState */ static final NOTIFICATION_DISMISS_TIME:Int = 5000; - // Start performing rapid undo after this many seconds. - static final RAPID_UNDO_DELAY:Float = 0.4; - // Perform a rapid undo every this many seconds. - static final RAPID_UNDO_INTERVAL:Float = 0.1; + /** + * Duration, in seconds, for the scroll easing animation. + */ + static final SCROLL_EASE_DURATION:Float = 0.2; // UI Element Colors // Background color tint. @@ -380,6 +391,11 @@ class ChartEditorState extends HaxeUIState */ var currentOpponentCharacterPlayer:CharacterPlayer = null; + /** + * The currently selected live input style. + */ + var currentLiveInputStyle:LiveInputStyle = LiveInputStyle.None; + /** * Whether the current view is in downscroll mode. */ @@ -473,6 +489,22 @@ class ChartEditorState extends HaxeUIState return selectedDifficulty; } + /** + * The character ID for the character which is currently selected. + */ + var selectedCharacter(default, set):String = Constants.DEFAULT_CHARACTER; + + function set_selectedCharacter(value:String):String + { + selectedCharacter = value; + + // Make sure view is updated when the character changes. + noteDisplayDirty = true; + notePreviewDirty = true; + + return selectedCharacter; + } + /** * Whether the user is currently in Pattern Mode. * This overrides the chart editor's normal behavior. @@ -548,6 +580,18 @@ class ChartEditorState extends HaxeUIState */ var characterSelectDirty:Bool = true; + /** + * Whether the player preview toolbox have been modified and need to be updated. + * This happens when we switch characters. + */ + var playerPreviewDirty:Bool = true; + + /** + * Whether the opponent preview toolbox have been modified and need to be updated. + * This happens when we switch characters. + */ + var opponentPreviewDirty:Bool = true; + var isInPlaytestMode:Bool = false; /** @@ -626,7 +670,7 @@ class ChartEditorState extends HaxeUIState * The Dialog components representing the currently available tool windows. * Dialogs are retained here even when collapsed or hidden. */ - var activeToolboxes:Map = new Map(); + var activeToolboxes:Map = new Map(); /** * AUDIO AND SOUND DATA @@ -638,6 +682,11 @@ class ChartEditorState extends HaxeUIState */ var audioInstTrack:FlxSound; + /** + * The raw byte data for the instrumental audio track. + */ + var audioInstTrackData:Bytes = null; + /** * The audio track for the vocals. */ @@ -650,7 +699,7 @@ class ChartEditorState extends HaxeUIState * * When switching characters, the elements of the VoicesGroup will be swapped to match the new character. */ - var audioVocalTracks:Map = new Map(); + var audioVocalTrackData:Map = []; /** * CHART DATA @@ -685,7 +734,7 @@ class ChartEditorState extends HaxeUIState function get_currentSongMetadata():SongMetadata { - var result = songMetadata.get(selectedVariation); + var result:SongMetadata = songMetadata.get(selectedVariation); if (result == null) { result = new SongMetadata('Dad Battle', 'Kawai Sprite', selectedVariation); @@ -707,7 +756,7 @@ class ChartEditorState extends HaxeUIState function get_currentSongChartData():SongChartData { - var result = songChartData.get(selectedVariation); + var result:SongChartData = songChartData.get(selectedVariation); if (result == null) { result = new SongChartData(1.0, [], []); @@ -729,7 +778,7 @@ class ChartEditorState extends HaxeUIState function get_currentSongChartScrollSpeed():Float { - var result = currentSongChartData.scrollSpeed.get(selectedDifficulty); + var result:Null = currentSongChartData.scrollSpeed.get(selectedDifficulty); if (result == null) { // Initialize to the default value if not set. @@ -752,11 +801,12 @@ class ChartEditorState extends HaxeUIState function get_currentSongChartNoteData():Array { - var result = currentSongChartData.notes.get(selectedDifficulty); + var result:Array = currentSongChartData.notes.get(selectedDifficulty); if (result == null) { // Initialize to the default value if not set. result = []; + trace('Initializing blank note data for difficulty ' + selectedDifficulty); currentSongChartData.notes.set(selectedDifficulty, result); return result; } @@ -864,6 +914,59 @@ class ChartEditorState extends HaxeUIState return currentSongMetadata.artist = value; } + var currentSongPlayableCharacters(get, null):Array; + + function get_currentSongPlayableCharacters():Array + { + return currentSongMetadata.playData.playableChars.keys().array(); + } + + var currentSongCharacterPlayer(get, set):String; + + function get_currentSongCharacterPlayer():String + { + // Validate selected character before returning it. + if (!currentSongPlayableCharacters.contains(selectedCharacter)) + { + trace('Invalid character selected: ' + selectedCharacter); + selectedCharacter = currentSongPlayableCharacters[0]; + } + + return selectedCharacter; + } + + function set_currentSongCharacterPlayer(value:String):String + { + if (!currentSongPlayableCharacters.contains(value)) + { + trace('Invalid character selected: ' + value); + return value; + } + + return selectedCharacter = value; + } + + var currentSongCharacterOpponent(get, set):String; + + function get_currentSongCharacterOpponent():String + { + // Validate selected character before returning it. + if (!currentSongPlayableCharacters.contains(selectedCharacter)) + { + trace('Invalid character selected: ' + selectedCharacter); + selectedCharacter = currentSongPlayableCharacters[0]; + } + + var playableCharData:SongPlayableChar = currentSongMetadata.playData.playableChars.get(selectedCharacter); + return playableCharData.opponent; + } + + function set_currentSongCharacterOpponent(value:String):String + { + var playableCharData:SongPlayableChar = currentSongMetadata.playData.playableChars.get(selectedCharacter); + return playableCharData.opponent = value; + } + /** * RENDER OBJECTS */ @@ -911,17 +1014,11 @@ class ChartEditorState extends HaxeUIState */ var gridSpectrogram:PolygonSpectogram; - /** - * The rectangle used for the note preview area. - * Should span the full height of the song. We scribble on this to draw the preview. - */ - var notePreviewBitmap:BitmapData; - /** * The sprite used to display the note preview area. * We move this up and down to scroll the preview. */ - var notePreviewSprite:FlxSprite; + var notePreview:ChartEditorNotePreview; /** * The rectangular sprite used for rendering the selection box. @@ -987,6 +1084,8 @@ class ChartEditorState extends HaxeUIState currentTheme = ChartEditorTheme.Light; buildGrid(); + // buildSpectrogram(audioInstTrack); + buildNotePreview(); buildSelectionBox(); // Add the HaxeUI components after the grid so they're on top. @@ -1048,7 +1147,7 @@ class ChartEditorState extends HaxeUIState gridGhostEvent = new ChartEditorEventSprite(this); gridGhostEvent.alpha = 0.6; - gridGhostEvent.eventData = new SongEventData(-1, "", {}); + gridGhostEvent.eventData = new SongEventData(-1, '', {}); gridGhostEvent.visible = false; add(gridGhostEvent); @@ -1062,15 +1161,15 @@ class ChartEditorState extends HaxeUIState gridPlayhead = new FlxSpriteGroup(); add(gridPlayhead); - var playheadWidth = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2); - var playheadBaseYPos = MENU_BAR_HEIGHT + GRID_TOP_PAD; + var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2); + var playheadBaseYPos:Float = MENU_BAR_HEIGHT + GRID_TOP_PAD; gridPlayhead.setPosition(gridTiledSprite.x, playheadBaseYPos); - var playheadSprite = new FlxSprite().makeGraphic(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR); + var playheadSprite:FlxSprite = new FlxSprite().makeGraphic(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR); playheadSprite.x = -PLAYHEAD_SCROLL_AREA_WIDTH; playheadSprite.y = 0; gridPlayhead.add(playheadSprite); - var playheadBlock = ChartEditorThemeHandler.buildPlayheadBlock(); + var playheadBlock:FlxSprite = ChartEditorThemeHandler.buildPlayheadBlock(); playheadBlock.x = -PLAYHEAD_SCROLL_AREA_WIDTH; playheadBlock.y = -PLAYHEAD_HEIGHT / 2; gridPlayhead.add(playheadBlock); @@ -1100,7 +1199,7 @@ class ChartEditorState extends HaxeUIState setSelectionBoxBounds(); } - function setSelectionBoxBounds(?bounds:FlxRect = null):Void + function setSelectionBoxBounds(bounds:FlxRect = null):Void { if (bounds == null) { @@ -1118,17 +1217,21 @@ class ChartEditorState extends HaxeUIState } } + function buildNotePreview():Void + { + var height:Int = FlxG.height - MENU_BAR_HEIGHT - GRID_TOP_PAD - 200; + notePreview = new ChartEditorNotePreview(height); + notePreview.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; + add(notePreview); + } + function buildSpectrogram(target:FlxSound):Void { - gridSpectrogram = new PolygonSpectogram(target, SPECTROGRAM_COLOR, FlxG.height / 2, Math.floor(FlxG.height / 2)); - // Halfway through the grid. - // gridSpectrogram.x = gridTiledSprite.x + STRUMLINE_SIZE * GRID_SIZE; - // gridSpectrogram.y = gridTiledSprite.y; - gridSpectrogram.x = 200; - gridSpectrogram.y = 200; - gridSpectrogram.visType = STATIC; // We move the spectrogram manually. + gridSpectrogram = new PolygonSpectogram(FlxG.sound.music, FlxColor.RED, FlxG.height / 2, Math.floor(FlxG.height / 2)); + gridSpectrogram.x += 170; + gridSpectrogram.scrollFactor.set(); gridSpectrogram.waveAmplitude = 50; - gridSpectrogram.scrollFactor.set(0, 0); + gridSpectrogram.visType = UPDATED; add(gridSpectrogram); } @@ -1169,7 +1272,7 @@ class ChartEditorState extends HaxeUIState playbarHead.allowFocus = false; playbarHead.width = FlxG.width; playbarHead.height = 10; - playbarHead.styleString = "padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px;"; + playbarHead.styleString = 'padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px;'; playbarHead.onDragStart = function(_:DragEvent) { playbarHeadDragging = true; @@ -1198,11 +1301,17 @@ class ChartEditorState extends HaxeUIState if (playbarHeadDraggingWasPlaying) { playbarHeadDraggingWasPlaying = false; - startAudioPlayback(); + // Disabled code to resume song playback on drag. + // startAudioPlayback(); } } add(playbarHeadLayout); + + // Setup notifications. + @:privateAccess + // NotificationManager.GUTTER_SIZE = 56; + NotificationManager.GUTTER_SIZE = 20; } /** @@ -1221,8 +1330,10 @@ class ChartEditorState extends HaxeUIState // Add functionality to the menu items. addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true)); + addUIClickListener('menubarItemOpenChart', _ -> ChartEditorDialogHandler.openBrowseWizard(this, true)); addUIClickListener('menubarItemSaveChartAs', _ -> exportAllSongData()); addUIClickListener('menubarItemLoadInst', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true)); + addUIClickListener('menubarItemImportChart', _ -> ChartEditorDialogHandler.openImportChartDialog(this, 'legacy', true)); addUIClickListener('menubarItemUndo', _ -> undoLastCommand()); @@ -1230,10 +1341,21 @@ class ChartEditorState extends HaxeUIState addUIClickListener('menubarItemCopy', function(_) { // Doesn't use a command because it's not undoable. + + // Calculate a single time offset for all the notes and events. + var timeOffset:Null = currentNoteSelection.length > 0 ? Std.int(currentNoteSelection[0].time) : null; + if (currentEventSelection.length > 0) + { + if (timeOffset == null || currentEventSelection[0].time < timeOffset) + { + timeOffset = Std.int(currentEventSelection[0].time); + } + } + SongDataUtils.writeItemsToClipboard( { - notes: SongDataUtils.buildNoteClipboard(currentNoteSelection), - events: SongDataUtils.buildEventClipboard(currentEventSelection), + notes: SongDataUtils.buildNoteClipboard(currentNoteSelection, timeOffset), + events: SongDataUtils.buildEventClipboard(currentEventSelection, timeOffset), }); }); @@ -1271,6 +1393,10 @@ class ChartEditorState extends HaxeUIState // addUIClickListener('menubarItemSelectBeforeCursor', _ -> doSomething()); // addUIClickListener('menubarItemSelectAfterCursor', _ -> doSomething()); + addUIChangeListener('menubarItemInputStyleGroup', function(event:UIEvent) { + trace('Change input style: ${event.target}'); + }); + addUIClickListener('menubarItemAbout', _ -> ChartEditorDialogHandler.openAboutDialog(this)); addUIClickListener('menubarItemUserGuide', _ -> ChartEditorDialogHandler.openUserGuideDialog(this)); @@ -1314,11 +1440,13 @@ class ChartEditorState extends HaxeUIState var playbackSpeedLabel:Label = findComponent('menubarLabelPlaybackSpeed', Label); addUIChangeListener('menubarItemPlaybackSpeed', function(event:UIEvent) { var pitch:Float = event.value * 2.0 / 100.0; + pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25. #if FLX_PITCH if (audioInstTrack != null) audioInstTrack.pitch = pitch; if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch; #end - playbackSpeedLabel.text = 'Playback Speed - ${Std.int(pitch * 100) / 100}x'; + var pitchDisplay:Float = Std.int(pitch * 100) / 100; // Round to 2 decimal places. + playbackSpeedLabel.text = 'Playback Speed - ${pitchDisplay}x'; }); addUIChangeListener('menubarItemToggleToolboxTools', @@ -1427,21 +1555,7 @@ class ChartEditorState extends HaxeUIState // DEBUG #if debug - if (FlxG.keys.justPressed.F) - { - NotificationManager.instance.addNotification( - { - title: 'This is a Notification', - body: 'Hello, world!', - type: NotificationType.Info, - expiryMs: NOTIFICATION_DISMISS_TIME - // styleNames: 'cssStyleName', - // icon: 'assetPath', - // actions: ['action1', 'action2'] - }); - } - - if (FlxG.keys.justPressed.E) + if (FlxG.keys.justPressed.E && !isHaxeUIDialogOpen) { currentSongMetadata.timeChanges[0].timeSignatureNum = (currentSongMetadata.timeChanges[0].timeSignatureNum == 4 ? 3 : 4); } @@ -1450,9 +1564,9 @@ class ChartEditorState extends HaxeUIState // Right align the BF health icon. // Base X position to the right of the grid. - var baseHealthIconXPos = gridTiledSprite.x + GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + 15; + var baseHealthIconXPos:Float = gridTiledSprite.x + GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + 15; // Will be 0 when not bopping. When bopping, will increase to push the icon left. - var healthIconOffset = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5); + var healthIconOffset:Float = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5); healthIconBF.x = baseHealthIconXPos - healthIconOffset; } @@ -1486,8 +1600,9 @@ class ChartEditorState extends HaxeUIState healthIconBF.onStepHit(Conductor.currentStep); } - // if (shouldPlayMetronome) - // playMetronomeTick(false); + // Updating these every step keeps it more accurate. + // playerPreviewDirty = true; + // opponentPreviewDirty = true; return true; } @@ -1500,29 +1615,36 @@ class ChartEditorState extends HaxeUIState // Don't scroll when the cursor is over the UI. if (isCursorOverHaxeUI) return; - // Amount to scroll the grid. - var scrollAmount:Float = 0; - // Amount to scroll the playhead relative to the grid. - var playheadAmount:Float = 0; - var shouldPause:Bool = false; + var scrollAmount:Float = 0; // Amount to scroll the grid. + var playheadAmount:Float = 0; // Amount to scroll the playhead relative to the grid. + var shouldPause:Bool = false; // Whether to pause the song when scrolling. + var shouldEase:Bool = false; // Whether to ease the scroll. // Up Arrow = Scroll Up - if (upKeyHandler.activated) + if (upKeyHandler.activated && currentLiveInputStyle != LiveInputStyle.WASD) { scrollAmount = -GRID_SIZE * 0.25 * 5.0; shouldPause = true; } // Down Arrow = Scroll Down - if (downKeyHandler.activated) + if (downKeyHandler.activated && currentLiveInputStyle != LiveInputStyle.WASD) { scrollAmount = GRID_SIZE * 0.25 * 5.0; shouldPause = true; } - // PAGE UP = Jump Up 1 Measure + // PAGE UP = Jump up to nearest measure if (pageUpKeyHandler.activated) { - scrollAmount = -GRID_SIZE * 4 * Conductor.beatsPerMeasure; + var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure; + var targetScrollPosition:Float = Math.floor(scrollPositionInPixels / measureHeight) * measureHeight; + // If we would move less than one grid, instead move to the top of the previous measure. + if (Math.abs(targetScrollPosition - scrollPositionInPixels) < GRID_SIZE) + { + targetScrollPosition -= GRID_SIZE * 4 * Conductor.beatsPerMeasure; + } + scrollAmount = targetScrollPosition - scrollPositionInPixels; + shouldPause = true; } if (playbarButtonPressed == 'playbarBack') @@ -1532,10 +1654,18 @@ class ChartEditorState extends HaxeUIState shouldPause = true; } - // PAGE DOWN = Jump Down 1 Measure + // PAGE DOWN = Jump down to nearest measure if (pageDownKeyHandler.activated) { - scrollAmount = GRID_SIZE * 4 * Conductor.beatsPerMeasure; + var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure; + var targetScrollPosition:Float = Math.ceil(scrollPositionInPixels / measureHeight) * measureHeight; + // If we would move less than one grid, instead move to the top of the next measure. + if (Math.abs(targetScrollPosition - scrollPositionInPixels) < GRID_SIZE) + { + targetScrollPosition += GRID_SIZE * 4 * Conductor.beatsPerMeasure; + } + scrollAmount = targetScrollPosition - scrollPositionInPixels; + shouldPause = true; } if (playbarButtonPressed == 'playbarForward') @@ -1613,12 +1743,26 @@ class ChartEditorState extends HaxeUIState shouldPause = true; } - // Apply the scroll amount. - this.scrollPositionInPixels += scrollAmount; - this.playheadPositionInPixels += playheadAmount; + if (Math.abs(scrollAmount) > GRID_SIZE * 8) + { + shouldEase = true; + } // Resync the conductor and audio tracks. - if (scrollAmount != 0 || playheadAmount != 0) moveSongToScrollPosition(); + if (scrollAmount != 0 || playheadAmount != 0) + { + this.playheadPositionInPixels += playheadAmount; + if (shouldEase) + { + easeSongToScrollPosition(this.scrollPositionInPixels + scrollAmount); + } + else + { + // Apply the scroll amount. + this.scrollPositionInPixels += scrollAmount; + moveSongToScrollPosition(); + } + } if (shouldPause) stopAudioPlayback(); } @@ -1664,8 +1808,8 @@ class ChartEditorState extends HaxeUIState function handleCursor():Void { // Note: If a menu is open in HaxeUI, don't handle cursor behavior. - var shouldHandleCursor = !isCursorOverHaxeUI || (selectionBoxStartPos != null); - var eventColumn = (STRUMLINE_SIZE * 2 + 1) - 1; + var shouldHandleCursor:Bool = !isCursorOverHaxeUI || (selectionBoxStartPos != null); + var eventColumn:Int = (STRUMLINE_SIZE * 2 + 1) - 1; if (shouldHandleCursor) { @@ -1675,7 +1819,7 @@ class ChartEditorState extends HaxeUIState var cursorX:Float = FlxG.mouse.screenX - gridTiledSprite.x; var cursorY:Float = FlxG.mouse.screenY - gridTiledSprite.y; - var overlapsSelectionBorder = overlapsGrid + var overlapsSelectionBorder:Bool = overlapsGrid && (cursorX % 40) < (GRID_SELECTION_BORDER_WIDTH / 2) || (cursorX % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2)) || (cursorY % 40) < (GRID_SELECTION_BORDER_WIDTH / 2) || (cursorY % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2)); @@ -1690,6 +1834,13 @@ class ChartEditorState extends HaxeUIState { selectionBoxStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); } + else + { + trace('Clicked outside grid, deselecting all items.'); + + // Deselect all items. + performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)); + } } if (gridPlayheadScrollAreaPressed) @@ -1823,6 +1974,13 @@ class ChartEditorState extends HaxeUIState else { // We made a selection box, but it didn't select anything. + + if (!FlxG.keys.pressed.CONTROL) + { + trace('Clicked and dragged outside grid, deselecting all items.'); + // Deselect all items. + performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)); + } } } else @@ -1836,8 +1994,26 @@ class ChartEditorState extends HaxeUIState } else { + // Scroll the screen if the mouse is above or below the grid. + if (FlxG.mouse.screenY < MENU_BAR_HEIGHT) + { + // Scroll up. + var diff:Float = MENU_BAR_HEIGHT - FlxG.mouse.screenY; + scrollPositionInPixels -= diff * 0.5; // Too fast! + trace('Scroll up: ' + diff); + moveSongToScrollPosition(); + } + else if (FlxG.mouse.screenY > playbarHeadLayout.y) + { + // Scroll down. + var diff:Float = FlxG.mouse.screenY - playbarHeadLayout.y; + scrollPositionInPixels += diff * 0.5; // Too fast! + trace('Scroll down: ' + diff); + moveSongToScrollPosition(); + } + // Render the selection box. - var selectionRect = new FlxRect(); + var selectionRect:FlxRect = new FlxRect(); selectionRect.x = Math.min(FlxG.mouse.screenX, selectionBoxStartPos.x); selectionRect.y = Math.min(FlxG.mouse.screenY, selectionBoxStartPos.y); selectionRect.width = Math.abs(FlxG.mouse.screenX - selectionBoxStartPos.x); @@ -1921,7 +2097,14 @@ class ChartEditorState extends HaxeUIState } else { - // If we clicked and released outside the grid, do nothing. + // If we clicked and released outside the grid. + + if (!FlxG.keys.pressed.CONTROL) + { + trace('Clicked outside grid, deselecting all items.'); + // Deselect all items. + performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)); + } } } } @@ -2413,8 +2596,8 @@ class ChartEditorState extends HaxeUIState playbarHeadLayout.x = 4; playbarHeadLayout.y = FlxG.height - 48 - 8; - var songPos = Conductor.songPosition; - var songRemaining = songLengthInMs - songPos; + var songPos:Float = Conductor.songPosition; + var songRemaining:Float = Math.max(songLengthInMs - songPos, 0.0); // Move the playhead to match the song position, if we aren't dragging it. if (!playbarHeadDragging) @@ -2559,7 +2742,7 @@ class ChartEditorState extends HaxeUIState difficultySelectDirty = false; // Manage the Select Difficulty tree view. - var difficultyToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); + var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); if (difficultyToolbox == null) return; var treeView:TreeView = difficultyToolbox.findComponent('difficultyToolboxTree'); @@ -2568,30 +2751,28 @@ class ChartEditorState extends HaxeUIState // Clear the tree view so we can rebuild it. treeView.clearNodes(); - var treeSong = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName', icon: "haxeui-core/styles/default/haxeui_tiny.png"}); + var treeSong:TreeViewNode = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName', icon: 'haxeui-core/styles/default/haxeui_tiny.png'}); treeSong.expanded = true; for (curVariation in availableVariations) { var variationMetadata:SongMetadata = songMetadata.get(curVariation); - var treeVariation = treeSong.addNode( + var treeVariation:TreeViewNode = treeSong.addNode( { id: 'stv_variation_$curVariation', - text: 'V: ${curVariation.toTitleCase()}', - // icon: "haxeui-core/styles/default/haxeui_tiny.png" + text: 'V: ${curVariation.toTitleCase()}' }); treeVariation.expanded = true; - var difficultyList = variationMetadata.playData.difficulties; + var difficultyList:Array = variationMetadata.playData.difficulties; for (difficulty in difficultyList) { - var treeDifficulty = treeVariation.addNode( + var _treeDifficulty:TreeViewNode = treeVariation.addNode( { id: 'stv_difficulty_${curVariation}_$difficulty', - text: 'D: ${difficulty.toTitleCase()}', - // icon: "haxeui-core/styles/default/haxeui_tiny.png" + text: 'D: ${difficulty.toTitleCase()}' }); } } @@ -2604,25 +2785,71 @@ class ChartEditorState extends HaxeUIState function handlePlayerPreviewToolbox():Void { // Manage the Select Difficulty tree view. - var charPreviewToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); + var charPreviewToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); if (charPreviewToolbox == null) return; var charPlayer:CharacterPlayer = charPreviewToolbox.findComponent('charPlayer'); if (charPlayer == null) return; currentPlayerCharacterPlayer = charPlayer; + + if (playerPreviewDirty) + { + playerPreviewDirty = false; + + if (currentSongCharacterPlayer != charPlayer.charId) + { + healthIconBF.characterId = currentSongCharacterPlayer; + + charPlayer.loadCharacter(currentSongCharacterPlayer); + charPlayer.characterType = CharacterType.BF; + charPlayer.flip = true; + charPlayer.targetScale = 0.5; + + charPreviewToolbox.title = 'Player Preview - ${charPlayer.charName}'; + } + + if (charPreviewToolbox != null && !charPreviewToolbox.minimized) + { + charPreviewToolbox.width = charPlayer.width + 32; + charPreviewToolbox.height = charPlayer.height + 64; + } + } } function handleOpponentPreviewToolbox():Void { // Manage the Select Difficulty tree view. - var charPreviewToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); + var charPreviewToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); if (charPreviewToolbox == null) return; var charPlayer:CharacterPlayer = charPreviewToolbox.findComponent('charPlayer'); if (charPlayer == null) return; currentOpponentCharacterPlayer = charPlayer; + + if (opponentPreviewDirty) + { + opponentPreviewDirty = false; + + if (currentSongCharacterOpponent != charPlayer.charId) + { + healthIconDad.characterId = currentSongCharacterOpponent; + + charPlayer.loadCharacter(currentSongCharacterOpponent); + charPlayer.characterType = CharacterType.DAD; + charPlayer.flip = false; + charPlayer.targetScale = 0.5; + + charPreviewToolbox.title = 'Opponent Preview - ${charPlayer.charName}'; + } + + if (charPreviewToolbox != null && !charPreviewToolbox.minimized) + { + charPreviewToolbox.width = charPlayer.width + 32; + charPreviewToolbox.height = charPlayer.height + 64; + } + } } public override function dispatchEvent(event:ScriptEvent):Void @@ -2667,7 +2894,8 @@ class ChartEditorState extends HaxeUIState if (treeView == null) return null; - var result = treeView.findNodeByPath('stv_song/stv_variation_$selectedVariation/stv_difficulty_${selectedVariation}_$selectedDifficulty', 'id'); + var result:TreeViewNode = treeView.findNodeByPath('stv_song/stv_variation_$selectedVariation/stv_difficulty_${selectedVariation}_$selectedDifficulty', + 'id'); if (result == null) return null; @@ -2691,8 +2919,8 @@ class ChartEditorState extends HaxeUIState switch (targetNode.data.id.split('_')[1]) { case 'difficulty': - var variation = targetNode.data.id.split('_')[2]; - var difficulty = targetNode.data.id.split('_')[3]; + var variation:String = targetNode.data.id.split('_')[2]; + var difficulty:String = targetNode.data.id.split('_')[3]; if (variation != null && difficulty != null) { @@ -2739,12 +2967,10 @@ class ChartEditorState extends HaxeUIState { notePreviewDirty = false; - var PREVIEW_WIDTH:Int = GRID_SIZE * 2; - var STEP_HEIGHT:Int = 1; - var PREVIEW_HEIGHT:Int = Std.int(Conductor.getTimeInSteps(audioInstTrack.length) * STEP_HEIGHT); - - notePreviewBitmap = new BitmapData(PREVIEW_WIDTH, PREVIEW_HEIGHT, true); - notePreviewBitmap.fillRect(new Rectangle(0, 0, PREVIEW_WIDTH, PREVIEW_HEIGHT), PREVIEW_BG_COLOR); + // TODO: Only update the notes that have changed. + notePreview.erase(); + notePreview.addNotes(currentSongChartNoteData, Std.int(songLengthInMs)); + notePreview.addEvents(currentSongChartEventData, Std.int(songLengthInMs)); } } @@ -2773,7 +2999,7 @@ class ChartEditorState extends HaxeUIState { // Disable the Undo button. undoButton.disabled = true; - undoButton.text = "Undo"; + undoButton.text = 'Undo'; } else { @@ -2784,7 +3010,7 @@ class ChartEditorState extends HaxeUIState } else { - trace("undoButton is null"); + trace('undoButton is null'); } var redoButton:MenuItem = findComponent('menubarItemRedo', MenuItem); @@ -2795,7 +3021,7 @@ class ChartEditorState extends HaxeUIState { // Disable the Redo button. redoButton.disabled = true; - redoButton.text = "Redo"; + redoButton.text = 'Redo'; } else { @@ -2806,7 +3032,7 @@ class ChartEditorState extends HaxeUIState } else { - trace("redoButton is null"); + trace('redoButton is null'); } } } @@ -2822,13 +3048,16 @@ class ChartEditorState extends HaxeUIState { // If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat! - var oldStepTime = Conductor.currentStepTime; - var oldSongPosition = Conductor.songPosition; + var oldStepTime:Float = Conductor.currentStepTime; + var oldSongPosition:Float = Conductor.songPosition; Conductor.update(audioInstTrack.time); handleHitsounds(oldSongPosition, Conductor.songPosition); // Resync vocals. - if (Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) audioVocalTrackGroup.time = audioInstTrack.time; - var diffStepTime = Conductor.currentStepTime - oldStepTime; + if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) + { + audioVocalTrackGroup.time = audioInstTrack.time; + } + var diffStepTime:Float = Conductor.currentStepTime - oldStepTime; // Move the playhead. playheadPositionInPixels += diffStepTime * GRID_SIZE; @@ -2838,12 +3067,14 @@ class ChartEditorState extends HaxeUIState else { // Else, move the entire view. - var oldSongPosition = Conductor.songPosition; + var oldSongPosition:Float = Conductor.songPosition; Conductor.update(audioInstTrack.time); handleHitsounds(oldSongPosition, Conductor.songPosition); // Resync vocals. - if (audioVocalTrackGroup != null - && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) audioVocalTrackGroup.time = audioInstTrack.time; + if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) + { + audioVocalTrackGroup.time = audioInstTrack.time; + } // We need time in fractional steps here to allow the song to actually play. // Also account for a potentially offset playhead. @@ -2909,7 +3140,6 @@ class ChartEditorState extends HaxeUIState { if (audioInstTrack != null) audioInstTrack.play(); if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(); setComponentText('playbarPlay', '||'); } @@ -2918,7 +3148,6 @@ class ChartEditorState extends HaxeUIState { if (audioInstTrack != null) audioInstTrack.pause(); if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); setComponentText('playbarPlay', '>'); } @@ -2940,23 +3169,42 @@ class ChartEditorState extends HaxeUIState function handlePlayhead():Void { // Place notes at the playhead. - // TODO: Add the ability to switch modes. - if (true) + switch (currentLiveInputStyle) { - if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(0); - if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(1); - if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(2); - if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(3); - if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(4); - if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(5); - if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(6); - if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(7); + case LiveInputStyle.WASD: + if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(0); + if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(1); + if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(2); + if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(3); + + if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(4); + if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(5); + if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(6); + if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(7); + case LiveInputStyle.NumberKeys: + if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(0); + if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(1); + if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(2); + if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(3); + + if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(4); + if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(5); + if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(6); + if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(7); + case LiveInputStyle.None: + // Do nothing. } } function placeNoteAtPlayhead(column:Int):Void { - var gridSnappedPlayheadPos = scrollPositionInPixels - (scrollPositionInPixels % GRID_SIZE); + var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; + var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / (16 / noteSnapQuant); + var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); + var playheadPosMs:Float = playheadPosStep * Conductor.stepLengthMs * (16 / noteSnapQuant); + + var newNoteData:SongNoteData = new SongNoteData(playheadPosMs, column, 0, selectedNoteKind); + performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); } function set_scrollPositionInPixels(value:Float):Float @@ -2967,7 +3215,7 @@ class ChartEditorState extends HaxeUIState // but the playhead is in the middle, move the playhead up. if (playheadPositionInPixels > 0) { - var amount = scrollPositionInPixels - value; + var amount:Float = scrollPositionInPixels - value; playheadPositionInPixels -= amount; } @@ -2978,6 +3226,9 @@ class ChartEditorState extends HaxeUIState if (value == scrollPositionInPixels) return value; + // Difference in pixels. + var diff:Float = value - scrollPositionInPixels; + this.scrollPositionInPixels = value; // Move the grid sprite to the correct position. @@ -2994,12 +3245,9 @@ class ChartEditorState extends HaxeUIState renderedHoldNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y); renderedEvents.setPosition(gridTiledSprite.x, gridTiledSprite.y); renderedSelectionSquares.setPosition(gridTiledSprite.x, gridTiledSprite.y); - if (gridSpectrogram != null) - { - // Move the spectrogram to the correct position. - gridSpectrogram.y = gridTiledSprite.y; - gridSpectrogram.setPosition(0, 0); - } + + // Offset the selection box start position, if we are dragging. + if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff; return this.scrollPositionInPixels; } @@ -3036,12 +3284,19 @@ class ChartEditorState extends HaxeUIState */ public function loadInstrumentalFromBytes(bytes:haxe.io.Bytes, fileName:String = null):Bool { + if (bytes == null) + { + return false; + } + var openflSound:openfl.media.Sound = new openfl.media.Sound(); openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length); audioInstTrack = FlxG.sound.load(openflSound, 1.0, false); audioInstTrack.autoDestroy = false; audioInstTrack.pause(); + audioInstTrackData = bytes; + postLoadInstrumental(); return true; @@ -3059,6 +3314,8 @@ class ChartEditorState extends HaxeUIState { audioInstTrack = instTrack; + audioInstTrackData = Assets.getBytes(path); + postLoadInstrumental(); return true; } @@ -3066,7 +3323,7 @@ class ChartEditorState extends HaxeUIState return false; } - function postLoadInstrumental():Void + public function postLoadInstrumental():Void { // Prevent the time from skipping back to 0 when the song ends. audioInstTrack.onComplete = function() { @@ -3077,11 +3334,8 @@ class ChartEditorState extends HaxeUIState songLengthInMs = audioInstTrack.length; gridTiledSprite.height = songLengthInPixels; - if (gridSpectrogram != null) - { - gridSpectrogram.setSound(audioInstTrack); - gridSpectrogram.generateSection(0, songLengthInMs / 1000); - } + + buildSpectrogram(audioInstTrack); scrollPositionInPixels = 0; playheadPositionInPixels = 0; @@ -3092,8 +3346,9 @@ class ChartEditorState extends HaxeUIState * Loads a vocal track from an absolute file path. * @param path The absolute path to the audio file. * @param charKey The character to load the vocal track for. + * @return Success or failure. */ - public function loadVocalsFromPath(path:Path, charKey:String = null):Bool + public function loadVocalsFromPath(path:Path, charKey:String = 'default'):Bool { #if sys var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString()); @@ -3114,13 +3369,20 @@ class ChartEditorState extends HaxeUIState /** * Load a vocal track for a given song and character and add it to the voices group. + * + * @param path ID of the asset. + * @param charKey Character to load the vocal track for. + * @return Success or failure. */ - public function loadVocalsFromAsset(path:String, charKey:String = null):Bool + public function loadVocalsFromAsset(path:String, charKey:String = 'default'):Bool { var vocalTrack:FlxSound = FlxG.sound.load(path, 1.0, false); if (vocalTrack != null) { audioVocalTrackGroup.add(vocalTrack); + + audioVocalTrackData.set(charKey, Assets.getBytes(path)); + return true; } return false; @@ -3131,10 +3393,11 @@ class ChartEditorState extends HaxeUIState */ public function loadVocalsFromBytes(bytes:haxe.io.Bytes, charKey:String = null):Bool { - var openflSound = new openfl.media.Sound(); + var openflSound:openfl.media.Sound = new openfl.media.Sound(); openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length); var vocalTrack:FlxSound = FlxG.sound.load(openflSound, 1.0, false); audioVocalTrackGroup.add(vocalTrack); + audioVocalTrackData.set(charKey, bytes); return true; } @@ -3147,28 +3410,27 @@ class ChartEditorState extends HaxeUIState if (song == null) { - // showNotification('Failed to load song.'); return; } // Load the song metadata. var rawSongMetadata:Array = song.getRawMetadata(); - var songName:String = rawSongMetadata[0].songName; - - this.songMetadata = new Map(); + var songMetadata:Map = []; + var songChartData:Map = []; for (metadata in rawSongMetadata) { var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation; - this.songMetadata.set(variation, Reflect.copy(metadata)); + songMetadata.set(variation, Reflect.copy(metadata)); + songChartData.set(variation, SongDataParser.parseSongChartData(songId, metadata.variation)); } - this.songChartData = new Map(); + loadSong(songMetadata, songChartData); - for (metadata in rawSongMetadata) + if (audioInstTrack != null) { - var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation; - this.songChartData.set(variation, SongDataParser.parseSongChartData(songId, metadata.variation)); + audioInstTrack.stop(); + audioInstTrack = null; } Conductor.forceBPM(null); // Disable the forced BPM. @@ -3189,12 +3451,42 @@ class ChartEditorState extends HaxeUIState NotificationManager.instance.addNotification( { title: 'Success', - body: 'Loaded song ($songName)', + body: 'Loaded song (${rawSongMetadata[0].songName})', type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); } + /** + * Loads song metadata and chart data into the editor. + * @param newSongMetadata The song metadata to load. + * @param newSongChartData The song chart data to load. + */ + public function loadSong(newSongMetadata:Map, newSongChartData:Map):Void + { + this.songMetadata = newSongMetadata; + this.songChartData = newSongChartData; + + Conductor.forceBPM(null); // Disable the forced BPM. + Conductor.mapTimeChanges(currentSongMetadata.timeChanges); + + difficultySelectDirty = true; + opponentPreviewDirty = true; + playerPreviewDirty = true; + + // Remove instrumental and vocal tracks, they will be loaded next. + if (audioInstTrack != null) + { + audioInstTrack.stop(); + audioInstTrack = null; + } + if (audioVocalTrackGroup != null) + { + audioVocalTrackGroup.stop(); + audioVocalTrackGroup.clear(); + } + } + /** * When setting the scroll position, except when automatically scrolling during song playback, * we need to update the conductor's current step time and the timestamp of the audio tracks. @@ -3213,6 +3505,39 @@ class ChartEditorState extends HaxeUIState noteDisplayDirty = true; } + var currentScrollEase:VarTween; + + function easeSongToScrollPosition(targetScrollPosition:Float):Void + { + if (currentScrollEase != null) cancelScrollEase(currentScrollEase); + + currentScrollEase = FlxTween.tween(this, {scrollPositionInPixels: targetScrollPosition}, SCROLL_EASE_DURATION, + { + ease: FlxEase.quintInOut, + onUpdate: this.onScrollEaseUpdate, + onComplete: this.cancelScrollEase, + type: ONESHOT + }); + } + + function onScrollEaseUpdate(_:FlxTween):Void + { + moveSongToScrollPosition(); + } + + function cancelScrollEase(_:FlxTween):Void + { + if (currentScrollEase != null) + { + @:privateAccess + var targetScrollPosition:Float = currentScrollEase._properties.scrollPositionInPixels; + + currentScrollEase.cancel(); + currentScrollEase = null; + this.scrollPositionInPixels = targetScrollPosition; + } + } + /** * Perform (or redo) a command, then add it to the undo stack. * @@ -3331,11 +3656,11 @@ class ChartEditorState extends HaxeUIState */ public function exportAllSongData(?force:Bool = false, ?tmp:Bool = false):Void { - var zipEntries = []; + var zipEntries:Array = []; for (variation in availableVariations) { - var variationId = variation; + var variationId:String = variation; if (variation == '' || variation == 'default' || variation == 'normal') { variationId = ''; @@ -3343,21 +3668,25 @@ class ChartEditorState extends HaxeUIState if (variationId == '') { - var variationMetadata = songMetadata.get(variation); + var variationMetadata:SongMetadata = songMetadata.get(variation); zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata.json', SerializerUtil.toJSON(variationMetadata))); - var variationChart = songChartData.get(variation); + var variationChart:SongChartData = songChartData.get(variation); zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart.json', SerializerUtil.toJSON(variationChart))); } else { - var variationMetadata = songMetadata.get(variation); + var variationMetadata:SongMetadata = songMetadata.get(variation); zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata-$variationId.json', SerializerUtil.toJSON(variationMetadata))); - var variationChart = songChartData.get(variation); + var variationChart:SongChartData = songChartData.get(variation); zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart-$variationId.json', SerializerUtil.toJSON(variationChart))); } } - // TODO: Add audio files to the ZIP. + zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', audioInstTrackData)); + for (charId in audioVocalTrackData.keys()) + { + zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-$charId.ogg', audioVocalTrackData.get(charId))); + } trace('Exporting ${zipEntries.length} files to ZIP...'); @@ -3390,3 +3719,10 @@ class ChartEditorState extends HaxeUIState FileUtil.saveMultipleFiles(zipEntries, onSave, onCancel, '$currentSongId-chart.zip'); } } + +enum LiveInputStyle +{ + None; + NumberKeys; + WASD; +} diff --git a/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx index 7bdf366bf..40c797169 100644 --- a/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx @@ -26,7 +26,7 @@ class ChartEditorThemeHandler // An enum of typedefs or something? // ================================ static final BACKGROUND_COLOR_LIGHT:FlxColor = 0xFF673AB7; - static final BACKGROUND_COLOR_DARK:FlxColor = 0xFF673AB7; + static final BACKGROUND_COLOR_DARK:FlxColor = 0xFF361E60; // Color 1 of the grid pattern. Alternates with Color 2. static final GRID_COLOR_1_LIGHT:FlxColor = 0xFFE7E6E6; @@ -43,13 +43,11 @@ class ChartEditorThemeHandler // Vertical divider between characters. static final GRID_STRUMLINE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111; static final GRID_STRUMLINE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4; - // static final GRID_STRUMLINE_DIVIDER_WIDTH:Float = 2; static final GRID_STRUMLINE_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH; // Horizontal divider between measures. static final GRID_MEASURE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111; static final GRID_MEASURE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4; - // static final GRID_MEASURE_DIVIDER_WIDTH:Float = 2; static final GRID_MEASURE_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH; // Border on the square highlighting selected notes. @@ -66,6 +64,12 @@ class ChartEditorThemeHandler static final PLAYHEAD_BLOCK_BORDER_COLOR:FlxColor = 0xFF9D0011; static final PLAYHEAD_BLOCK_FILL_COLOR:FlxColor = 0xFFBD0231; + static final TOTAL_COLUMN_COUNT:Int = ChartEditorState.STRUMLINE_SIZE * 2 + 1; + + /** + * When the theme is changed, this function updates all of the UI elements to match the new theme. + * @param state The ChartEditorState to update. + */ public static function updateTheme(state:ChartEditorState):Void { updateBackground(state); @@ -73,6 +77,10 @@ class ChartEditorThemeHandler updateSelectionSquare(state); } + /** + * Updates the tint of the background sprite to match the current theme. + * @param state The ChartEditorState to update. + */ static function updateBackground(state:ChartEditorState):Void { state.menuBG.color = switch (state.currentTheme) @@ -85,7 +93,7 @@ class ChartEditorThemeHandler /** * Builds the checkerboard background image of the chart editor, and adds dividing lines to it. - * @param dark Whether to draw the grid in a dark color instead of a light one. + * @param state The ChartEditorState to update. */ static function updateGridBitmap(state:ChartEditorState):Void { @@ -107,8 +115,8 @@ class ChartEditorThemeHandler // 2 * (Strumline Size) + 1 grid squares wide, by (4 * quarter notes per measure) grid squares tall. // This gets reused to fill the screen. - var gridWidth:Int = Std.int(ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2 + 1)); - var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * (Conductor.stepsPerMeasure)); + var gridWidth:Int = Std.int(ChartEditorState.GRID_SIZE * TOTAL_COLUMN_COUNT); + var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.stepsPerMeasure); state.gridBitmap = FlxGridOverlay.createGrid(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE, gridWidth, gridHeight, true, gridColor1, gridColor2); // Selection borders @@ -143,7 +151,7 @@ class ChartEditorThemeHandler selectionBorderColor); // Selection borders across the middle. - for (i in 1...(ChartEditorState.STRUMLINE_SIZE * 2 + 1)) + for (i in 1...TOTAL_COLUMN_COUNT) { state.gridBitmap.fillRect(new Rectangle((ChartEditorState.GRID_SIZE * i) - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), 0, ChartEditorState.GRID_SELECTION_BORDER_WIDTH, state.gridBitmap.height), @@ -167,7 +175,7 @@ class ChartEditorThemeHandler // Divider at top state.gridBitmap.fillRect(new Rectangle(0, 0, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor); // Divider at bottom - var dividerLineBY = state.gridBitmap.height - (GRID_MEASURE_DIVIDER_WIDTH / 2); + var dividerLineBY:Float = state.gridBitmap.height - (GRID_MEASURE_DIVIDER_WIDTH / 2); state.gridBitmap.fillRect(new Rectangle(0, dividerLineBY, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor); // Draw dividers between the strumlines. @@ -180,10 +188,10 @@ class ChartEditorThemeHandler }; // Divider at 1 * (Strumline Size) - var dividerLineAX = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2); + var dividerLineAX:Float = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2); state.gridBitmap.fillRect(new Rectangle(dividerLineAX, 0, GRID_STRUMLINE_DIVIDER_WIDTH, state.gridBitmap.height), gridStrumlineDividerColor); // Divider at 2 * (Strumline Size) - var dividerLineBX = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2); + var dividerLineBX:Float = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2); state.gridBitmap.fillRect(new Rectangle(dividerLineBX, 0, GRID_STRUMLINE_DIVIDER_WIDTH, state.gridBitmap.height), gridStrumlineDividerColor); if (state.gridTiledSprite != null) diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx index 5a903481e..5cace2ff6 100644 --- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx @@ -1,6 +1,5 @@ package funkin.ui.debug.charting; -import haxe.ui.data.ArrayDataSource; import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.event.SongEvent; import funkin.play.event.SongEventData; @@ -12,15 +11,17 @@ import haxe.ui.components.CheckBox; import haxe.ui.components.DropDown; import haxe.ui.components.Label; import haxe.ui.components.NumberStepper; -import haxe.ui.components.NumberStepper; import haxe.ui.components.Slider; import haxe.ui.components.TextField; -import haxe.ui.containers.dialogs.Dialog; import haxe.ui.containers.Box; -import haxe.ui.containers.Frame; import haxe.ui.containers.Grid; import haxe.ui.containers.Group; +import haxe.ui.containers.VBox; +import haxe.ui.containers.dialogs.CollapsibleDialog; +import haxe.ui.containers.dialogs.Dialog.DialogButton; +import haxe.ui.containers.dialogs.Dialog.DialogEvent; import haxe.ui.core.Component; +import haxe.ui.data.ArrayDataSource; import haxe.ui.events.UIEvent; /** @@ -32,18 +33,26 @@ enum ChartEditorToolMode Place; } +/** + * Static functions which handle building themed UI elements for a provided ChartEditorState. + */ class ChartEditorToolboxHandler { public static function setToolboxState(state:ChartEditorState, id:String, shown:Bool):Void { - if (shown) showToolbox(state, id); + if (shown) + { + showToolbox(state, id); + } else + { hideToolbox(state, id); + } } - public static function showToolbox(state:ChartEditorState, id:String) + public static function showToolbox(state:ChartEditorState, id:String):Void { - var toolbox:Dialog = state.activeToolboxes.get(id); + var toolbox:CollapsibleDialog = state.activeToolboxes.get(id); if (toolbox == null) toolbox = initToolbox(state, id); @@ -59,7 +68,7 @@ class ChartEditorToolboxHandler public static function hideToolbox(state:ChartEditorState, id:String):Void { - var toolbox:Dialog = state.activeToolboxes.get(id); + var toolbox:CollapsibleDialog = state.activeToolboxes.get(id); if (toolbox == null) toolbox = initToolbox(state, id); @@ -73,13 +82,27 @@ class ChartEditorToolboxHandler } } - public static function minimizeToolbox(state:ChartEditorState, id:String):Void {} - - public static function maximizeToolbox(state:ChartEditorState, id:String):Void {} - - public static function initToolbox(state:ChartEditorState, id:String):Dialog + public static function minimizeToolbox(state:ChartEditorState, id:String):Void { - var toolbox:Dialog = null; + var toolbox:CollapsibleDialog = state.activeToolboxes.get(id); + + if (toolbox == null) return; + + toolbox.minimized = true; + } + + public static function maximizeToolbox(state:ChartEditorState, id:String):Void + { + var toolbox:CollapsibleDialog = state.activeToolboxes.get(id); + + if (toolbox == null) return; + + toolbox.minimized = false; + } + + public static function initToolbox(state:ChartEditorState, id:String):CollapsibleDialog + { + var toolbox:CollapsibleDialog = null; switch (id) { case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT: @@ -95,9 +118,9 @@ class ChartEditorToolboxHandler case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT: toolbox = buildToolboxCharactersLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT: - toolbox = buildToolboxPlayerPreviewLayout(state); + toolbox = null; // buildToolboxPlayerPreviewLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT: - toolbox = buildToolboxOpponentPreviewLayout(state); + toolbox = null; // buildToolboxOpponentPreviewLayout(state); default: // This happens if you try to load an unknown layout. trace('ChartEditorToolboxHandler.initToolbox() - Unknown toolbox ID: $id'); @@ -114,9 +137,15 @@ class ChartEditorToolboxHandler return toolbox; } - public static function getToolbox(state:ChartEditorState, id:String):Dialog + /** + * Retrieve a toolbox by its layout's asset ID. + * @param state The ChartEditorState instance. + * @param id The asset ID of the toolbox layout. + * @return The toolbox. + */ + public static function getToolbox(state:ChartEditorState, id:String):CollapsibleDialog { - var toolbox:Dialog = state.activeToolboxes.get(id); + var toolbox:CollapsibleDialog = state.activeToolboxes.get(id); // Initialize the toolbox without showing it. if (toolbox == null) toolbox = initToolbox(state, id); @@ -124,9 +153,9 @@ class ChartEditorToolboxHandler return toolbox; } - static function buildToolboxToolsLayout(state:ChartEditorState):Dialog + static function buildToolboxToolsLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT); if (toolbox == null) return null; @@ -134,15 +163,15 @@ class ChartEditorToolboxHandler toolbox.x = 50; toolbox.y = 50; - toolbox.onDialogClosed = (event:DialogEvent) -> { + toolbox.onDialogClosed = function(event:DialogEvent) { state.setUICheckboxSelected('menubarItemToggleToolboxTools', false); } - var toolsGroup:Group = toolbox.findComponent("toolboxToolsGroup", Group); + var toolsGroup:Group = toolbox.findComponent('toolboxToolsGroup', Group); if (toolsGroup == null) return null; - toolsGroup.onChange = (event:UIEvent) -> { + toolsGroup.onChange = function(event:UIEvent) { switch (event.target.id) { case 'toolboxToolsGroupSelect': @@ -157,9 +186,9 @@ class ChartEditorToolboxHandler return toolbox; } - static function buildToolboxNoteDataLayout(state:ChartEditorState):Dialog + static function buildToolboxNoteDataLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT); if (toolbox == null) return null; @@ -167,16 +196,16 @@ class ChartEditorToolboxHandler toolbox.x = 75; toolbox.y = 100; - toolbox.onDialogClosed = (event:DialogEvent) -> { + toolbox.onDialogClosed = function(event:DialogEvent) { state.setUICheckboxSelected('menubarItemToggleToolboxNotes', false); } - var toolboxNotesNoteKind:DropDown = toolbox.findComponent("toolboxNotesNoteKind", DropDown); - var toolboxNotesCustomKindLabel:Label = toolbox.findComponent("toolboxNotesCustomKindLabel", Label); - var toolboxNotesCustomKind:TextField = toolbox.findComponent("toolboxNotesCustomKind", TextField); + var toolboxNotesNoteKind:DropDown = toolbox.findComponent('toolboxNotesNoteKind', DropDown); + var toolboxNotesCustomKindLabel:Label = toolbox.findComponent('toolboxNotesCustomKindLabel', Label); + var toolboxNotesCustomKind:TextField = toolbox.findComponent('toolboxNotesCustomKind', TextField); - toolboxNotesNoteKind.onChange = (event:UIEvent) -> { - var isCustom = (event.data.id == '~CUSTOM~'); + toolboxNotesNoteKind.onChange = function(event:UIEvent) { + var isCustom:Bool = (event.data.id == '~CUSTOM~'); if (isCustom) { @@ -194,16 +223,16 @@ class ChartEditorToolboxHandler } } - toolboxNotesCustomKind.onChange = (event:UIEvent) -> { + toolboxNotesCustomKind.onChange = function(event:UIEvent) { state.selectedNoteKind = toolboxNotesCustomKind.text; } return toolbox; } - static function buildToolboxEventDataLayout(state:ChartEditorState):Dialog + static function buildToolboxEventDataLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT); if (toolbox == null) return null; @@ -211,12 +240,12 @@ class ChartEditorToolboxHandler toolbox.x = 100; toolbox.y = 150; - toolbox.onDialogClosed = (event:DialogEvent) -> { + toolbox.onDialogClosed = function(event:DialogEvent) { state.setUICheckboxSelected('menubarItemToggleToolboxEvents', false); } - var toolboxEventsEventKind:DropDown = toolbox.findComponent("toolboxEventsEventKind", DropDown); - var toolboxEventsDataGrid:Grid = toolbox.findComponent("toolboxEventsDataGrid", Grid); + var toolboxEventsEventKind:DropDown = toolbox.findComponent('toolboxEventsEventKind', DropDown); + var toolboxEventsDataGrid:Grid = toolbox.findComponent('toolboxEventsDataGrid', Grid); toolboxEventsEventKind.dataSource = new ArrayDataSource(); @@ -227,7 +256,7 @@ class ChartEditorToolboxHandler toolboxEventsEventKind.dataSource.add({text: event.getTitle(), value: event.id}); } - toolboxEventsEventKind.onChange = (event:UIEvent) -> { + toolboxEventsEventKind.onChange = function(event:UIEvent) { var eventType:String = event.data.value; trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event type changed: $eventType'); @@ -281,9 +310,9 @@ class ChartEditorToolboxHandler numberStepper.value = field.defaultValue; input = numberStepper; case BOOL: - var checkBox = new CheckBox(); + var checkBox:CheckBox = new CheckBox(); checkBox.id = field.name; - checkBox.selected = field.defaultValue == true; + checkBox.selected = field.defaultValue; input = checkBox; case ENUM: var dropDown:DropDown = new DropDown(); @@ -293,7 +322,7 @@ class ChartEditorToolboxHandler // Add entries to the dropdown. for (optionName in field.keys.keys()) { - var optionValue = field.keys.get(optionName); + var optionValue:String = field.keys.get(optionName); trace('$optionName : $optionValue'); dropDown.dataSource.add({value: optionValue, text: optionName}); } @@ -314,7 +343,7 @@ class ChartEditorToolboxHandler target.addComponent(input); - input.onChange = (event:UIEvent) -> { + input.onChange = function(event:UIEvent) { trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${event.target.value}'); if (event.target.value == null) state.selectedEventData.remove(event.target.id); @@ -324,9 +353,9 @@ class ChartEditorToolboxHandler } } - static function buildToolboxDifficultyLayout(state:ChartEditorState):Dialog + static function buildToolboxDifficultyLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); if (toolbox == null) return null; @@ -334,36 +363,36 @@ class ChartEditorToolboxHandler toolbox.x = 125; toolbox.y = 200; - toolbox.onDialogClosed = (event:DialogEvent) -> { + toolbox.onDialogClosed = function(event:UIEvent) { state.setUICheckboxSelected('menubarItemToggleToolboxDifficulty', false); } - var difficultyToolboxSaveMetadata:Button = toolbox.findComponent("difficultyToolboxSaveMetadata", Button); - var difficultyToolboxSaveChart:Button = toolbox.findComponent("difficultyToolboxSaveChart", Button); - var difficultyToolboxSaveAll:Button = toolbox.findComponent("difficultyToolboxSaveAll", Button); - var difficultyToolboxLoadMetadata:Button = toolbox.findComponent("difficultyToolboxLoadMetadata", Button); - var difficultyToolboxLoadChart:Button = toolbox.findComponent("difficultyToolboxLoadChart", Button); + var difficultyToolboxSaveMetadata:Button = toolbox.findComponent('difficultyToolboxSaveMetadata', Button); + var difficultyToolboxSaveChart:Button = toolbox.findComponent('difficultyToolboxSaveChart', Button); + var difficultyToolboxSaveAll:Button = toolbox.findComponent('difficultyToolboxSaveAll', Button); + var difficultyToolboxLoadMetadata:Button = toolbox.findComponent('difficultyToolboxLoadMetadata', Button); + var difficultyToolboxLoadChart:Button = toolbox.findComponent('difficultyToolboxLoadChart', Button); - difficultyToolboxSaveMetadata.onClick = (event:UIEvent) -> { - SongSerializer.exportSongMetadata(state.currentSongMetadata); + difficultyToolboxSaveMetadata.onClick = function(event:UIEvent) { + SongSerializer.exportSongMetadata(state.currentSongMetadata, state.currentSongId); }; - difficultyToolboxSaveChart.onClick = (event:UIEvent) -> { - SongSerializer.exportSongChartData(state.currentSongChartData); + difficultyToolboxSaveChart.onClick = function(event:UIEvent) { + SongSerializer.exportSongChartData(state.currentSongChartData, state.currentSongId); }; - difficultyToolboxSaveAll.onClick = (event:UIEvent) -> { + difficultyToolboxSaveAll.onClick = function(event:UIEvent) { state.exportAllSongData(); }; - difficultyToolboxLoadMetadata.onClick = (event:UIEvent) -> { + difficultyToolboxLoadMetadata.onClick = function(event:UIEvent) { // Replace metadata for current variation. SongSerializer.importSongMetadataAsync(function(songMetadata) { state.currentSongMetadata = songMetadata; }); }; - difficultyToolboxLoadChart.onClick = (event:UIEvent) -> { + difficultyToolboxLoadChart.onClick = function(event:UIEvent) { // Replace chart data for current variation. SongSerializer.importSongChartDataAsync(function(songChartData) { state.currentSongChartData = songChartData; @@ -376,9 +405,9 @@ class ChartEditorToolboxHandler return toolbox; } - static function buildToolboxMetadataLayout(state:ChartEditorState):Dialog + static function buildToolboxMetadataLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); if (toolbox == null) return null; @@ -386,13 +415,13 @@ class ChartEditorToolboxHandler toolbox.x = 150; toolbox.y = 250; - toolbox.onDialogClosed = (event:DialogEvent) -> { + toolbox.onDialogClosed = function(event:UIEvent) { state.setUICheckboxSelected('menubarItemToggleToolboxMetadata', false); } var inputSongName:TextField = toolbox.findComponent('inputSongName', TextField); - inputSongName.onChange = (event:UIEvent) -> { - var valid = event.target.text != null && event.target.text != ""; + inputSongName.onChange = function(event:UIEvent) { + var valid:Bool = event.target.text != null && event.target.text != ''; if (valid) { @@ -404,10 +433,11 @@ class ChartEditorToolboxHandler state.currentSongMetadata.songName = null; } }; + inputSongName.value = state.currentSongMetadata.songName; var inputSongArtist:TextField = toolbox.findComponent('inputSongArtist', TextField); - inputSongArtist.onChange = (event:UIEvent) -> { - var valid = event.target.text != null && event.target.text != ""; + inputSongArtist.onChange = function(event:UIEvent) { + var valid:Bool = event.target.text != null && event.target.text != ''; if (valid) { @@ -419,28 +449,31 @@ class ChartEditorToolboxHandler state.currentSongMetadata.artist = null; } }; + inputSongArtist.value = state.currentSongMetadata.artist; var inputStage:DropDown = toolbox.findComponent('inputStage', DropDown); - inputStage.onChange = (event:UIEvent) -> { - var valid = event.data != null && event.data.id != null; + inputStage.onChange = function(event:UIEvent) { + var valid:Bool = event.data != null && event.data.id != null; if (valid) { state.currentSongMetadata.playData.stage = event.data.id; } }; + inputStage.value = state.currentSongMetadata.playData.stage; var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown); - inputNoteSkin.onChange = (event:UIEvent) -> { + inputNoteSkin.onChange = function(event:UIEvent) { if (event.data.id == null) return; state.currentSongMetadata.playData.noteSkin = event.data.id; }; + inputNoteSkin.value = state.currentSongMetadata.playData.noteSkin; var inputBPM:NumberStepper = toolbox.findComponent('inputBPM', NumberStepper); - inputBPM.onChange = (event:UIEvent) -> { + inputBPM.onChange = function(event:UIEvent) { if (event.value == null || event.value <= 0) return; - var timeChanges = state.currentSongMetadata.timeChanges; + var timeChanges:Array = state.currentSongMetadata.timeChanges; if (timeChanges == null || timeChanges.length == 0) { timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])]; @@ -454,28 +487,30 @@ class ChartEditorToolboxHandler state.currentSongMetadata.timeChanges = timeChanges; }; + inputBPM.value = state.currentSongMetadata.timeChanges[0].bpm; var inputScrollSpeed:Slider = toolbox.findComponent('inputScrollSpeed', Slider); - inputScrollSpeed.onChange = (event:UIEvent) -> { - var valid = event.target.value != null && event.target.value > 0; + inputScrollSpeed.onChange = function(event:UIEvent) { + var valid:Bool = event.target.value != null && event.target.value > 0; if (valid) { inputScrollSpeed.removeClass('invalid-value'); - state.currentSongChartData.scrollSpeed = event.target.value; + state.currentSongChartScrollSpeed = event.target.value; } else { - state.currentSongChartData.scrollSpeed = null; + state.currentSongChartScrollSpeed = 1.0; } }; + inputScrollSpeed.value = state.currentSongChartData.scrollSpeed; return toolbox; } - static function buildToolboxCharactersLayout(state:ChartEditorState):Dialog + static function buildToolboxCharactersLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT); if (toolbox == null) return null; @@ -483,16 +518,16 @@ class ChartEditorToolboxHandler toolbox.x = 175; toolbox.y = 300; - toolbox.onDialogClosed = (event:DialogEvent) -> { + toolbox.onDialogClosed = function(event:DialogEvent) { state.setUICheckboxSelected('menubarItemToggleToolboxCharacters', false); } return toolbox; } - static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Dialog + static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); if (toolbox == null) return null; @@ -500,23 +535,23 @@ class ChartEditorToolboxHandler toolbox.x = 200; toolbox.y = 350; - toolbox.onDialogClosed = (event:DialogEvent) -> { + toolbox.onDialogClosed = function(event:DialogEvent) { state.setUICheckboxSelected('menubarItemToggleToolboxPlayerPreview', false); } var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer'); // TODO: We need to implement character swapping in ChartEditorState. charPlayer.loadCharacter('bf'); - // charPlayer.setScale(0.5); - charPlayer.setCharacterType(CharacterType.BF); + charPlayer.characterType = CharacterType.BF; charPlayer.flip = true; + charPlayer.targetScale = 0.5; return toolbox; } - static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):Dialog + static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):CollapsibleDialog { - var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); + var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); if (toolbox == null) return null; @@ -531,9 +566,9 @@ class ChartEditorToolboxHandler var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer'); // TODO: We need to implement character swapping in ChartEditorState. charPlayer.loadCharacter('dad'); - // charPlayer.setScale(0.5); - charPlayer.setCharacterType(CharacterType.DAD); + charPlayer.characterType = CharacterType.DAD; charPlayer.flip = false; + charPlayer.targetScale = 0.5; return toolbox; } diff --git a/source/funkin/ui/haxeui/components/CharacterPlayer.hx b/source/funkin/ui/haxeui/components/CharacterPlayer.hx index c6beca123..0e6981535 100644 --- a/source/funkin/ui/haxeui/components/CharacterPlayer.hx +++ b/source/funkin/ui/haxeui/components/CharacterPlayer.hx @@ -1,25 +1,19 @@ package funkin.ui.haxeui.components; -import flixel.FlxSprite; -import flixel.graphics.frames.FlxAtlasFrames; -import flixel.graphics.frames.FlxFramesCollection; -import flixel.math.FlxRect; -import funkin.modding.events.ScriptEvent; -import funkin.modding.IScriptedClass.IPlayStateScriptedClass; +import funkin.modding.events.ScriptEvent.GhostMissNoteScriptEvent; +import funkin.modding.events.ScriptEvent.NoteScriptEvent; +import funkin.modding.events.ScriptEvent.SongTimeScriptEvent; +import funkin.modding.events.ScriptEvent.UpdateScriptEvent; +import haxe.ui.core.IDataComponent; import funkin.play.character.BaseCharacter; import funkin.play.character.CharacterData.CharacterDataParser; import haxe.ui.containers.Box; import haxe.ui.core.Component; -import haxe.ui.core.IDataComponent; -import haxe.ui.data.DataSource; import haxe.ui.events.AnimationEvent; -import haxe.ui.events.UIEvent; import haxe.ui.geom.Size; import haxe.ui.layouts.DefaultLayout; -import haxe.ui.styles.Style; -import openfl.Assets; -private typedef AnimationInfo = +typedef AnimationInfo = { var name:String; var prefix:String; @@ -29,6 +23,10 @@ private typedef AnimationInfo = var flipY:Null; // default false } +/** + * A variant of SparrowPlayer which loads a BaseCharacter instead. + * This allows it to play appropriate animations based on song events. + */ @:composite(Layout) class CharacterPlayer extends Box { @@ -37,7 +35,7 @@ class CharacterPlayer extends Box public function new(?defaultToBf:Bool = true) { super(); - this._overrideSkipTransformChildren = false; + _overrideSkipTransformChildren = false; if (defaultToBf) { @@ -45,52 +43,39 @@ class CharacterPlayer extends Box } } - var _charId:String; - public var charId(get, set):String; function get_charId():String { - return _charId; + return character.characterId; } function set_charId(value:String):String { - _charId = value; - loadCharacter(_charId); + loadCharacter(value); return value; } - var _redispatchLoaded:Bool = false; // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... needs thinking about, is it smart to "collect and redispatch"? Not sure - var _redispatchStart:Bool = false; // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... needs thinking about, is it smart to "collect and redispatch"? Not sure + public var charName(get, null):String; - public override function onReady() + function get_charName():String { - super.onReady(); - - invalidateComponentLayout(); - - if (_redispatchLoaded) - { - _redispatchLoaded = false; - dispatch(new AnimationEvent(AnimationEvent.LOADED)); - } - - if (_redispatchStart) - { - _redispatchStart = false; - dispatch(new AnimationEvent(AnimationEvent.START)); - } - - parentComponent._overrideSkipTransformChildren = false; + return character.characterName; } - public function loadCharacter(id:String) + // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... is it smart to "collect and redispatch"? Not sure + var _redispatchLoaded:Bool = false; + // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... is it smart to "collect and redispatch"? Not sure + var _redispatchStart:Bool = false; + var _characterLoaded:Bool = false; + + /** + * Loads a character by ID. + * @param id The ID of the character to load. + */ + public function loadCharacter(id:String):Void { - if (id == null) - { - return; - } + if (id == null) return; if (character != null) { @@ -99,32 +84,24 @@ class CharacterPlayer extends Box character = null; } - var newCharacter:BaseCharacter = CharacterDataParser.fetchCharacter(id); - - if (newCharacter == null) - { - return; - } + // Prevent script issues by fetching with debug=true. + var newCharacter:BaseCharacter = CharacterDataParser.fetchCharacter(id, true); + if (newCharacter == null) return; // Fail if character doesn't exist. + // Assign character. character = newCharacter; - if (_characterType != null) - { - character.characterType = _characterType; - } - if (flip) - { - character.flipX = !character.flipX; - } - character.scale.x *= _scale; - character.scale.y *= _scale; + // Set character properties. + if (characterType != null) character.characterType = characterType; + if (flip) character.flipX = !character.flipX; + if (targetScale != 1.0) character.setScale(targetScale); - character.animation.callback = function(name:String = "", frameNumber:Int = -1, frameIndex:Int = -1) { + character.animation.callback = function(name:String = '', frameNumber:Int = -1, frameIndex:Int = -1) { @:privateAccess character.onAnimationFrame(name, frameNumber, frameIndex); dispatch(new AnimationEvent(AnimationEvent.FRAME)); }; - character.animation.finishCallback = function(name:String = "") { + character.animation.finishCallback = function(name:String = '') { @:privateAccess character.onAnimationFinished(name); dispatch(new AnimationEvent(AnimationEvent.END)); @@ -143,28 +120,15 @@ class CharacterPlayer extends Box } } - override function repositionChildren() + /** + * The character type (such as BF, Dad, GF, etc). + */ + public var characterType(default, set):CharacterType; + + function set_characterType(value:CharacterType):CharacterType { - super.repositionChildren(); - - @:privateAccess - var animOffsets = character.animOffsets; - - character.x = this.screenX + ((this.width / 2) - (character.frameWidth / 2)); - character.x -= animOffsets[0]; - character.y = this.screenY + ((this.height / 2) - (character.frameHeight / 2)); - character.y -= animOffsets[1]; - } - - var _characterType:CharacterType; - - public function setCharacterType(value:CharacterType) - { - _characterType = value; - if (character != null) - { - character.characterType = value; - } + if (character != null) character.characterType = value; + return characterType = value; } public var flip(default, set):Bool; @@ -181,89 +145,133 @@ class CharacterPlayer extends Box return flip = value; } - var _scale:Float = 1.0; + public var targetScale(default, set):Float = 1.0; - public function setScale(value) + function set_targetScale(value:Float):Float { - _scale = value; + if (value == targetScale) return value; + if (character != null) { - character.scale.x *= _scale; - character.scale.y *= _scale; + character.setScale(value); } + + return targetScale = value; } - public function onUpdate(event:UpdateScriptEvent) + function onFrame(name:String, frameNumber:Int, frameIndex:Int):Void + { + dispatch(new AnimationEvent(AnimationEvent.FRAME)); + } + + function onFinish(name:String):Void + { + dispatch(new AnimationEvent(AnimationEvent.END)); + } + + override function repositionChildren():Void + { + super.repositionChildren(); + character.x = this.screenX; + character.y = this.screenY; + + // Apply animation offsets, so the character is positioned correctly based on the animation. + @:privateAccess var animOffsets:Array = character.animOffsets; + + character.x -= animOffsets[0] * targetScale * (flip ? -1 : 1); + character.y -= animOffsets[1] * targetScale; + } + + /** + * Called when an update event is hit in the song. + * Used to play character animations. + * @param event The event. + */ + public function onUpdate(event:UpdateScriptEvent):Void { if (character != null) character.onUpdate(event); } + /** + * Called when an beat is hit in the song + * Used to play character animations. + * @param event The event. + */ public function onBeatHit(event:SongTimeScriptEvent):Void { if (character != null) character.onBeatHit(event); - - this.repositionChildren(); } + /** + * Called when a step is hit in the song + * Used to play character animations. + * @param event The event. + */ public function onStepHit(event:SongTimeScriptEvent):Void { if (character != null) character.onStepHit(event); } + /** + * Called when a note is hit in the song + * Used to play character animations. + * @param event The event. + */ public function onNoteHit(event:NoteScriptEvent):Void { if (character != null) character.onNoteHit(event); - - this.repositionChildren(); } + /** + * Called when a note is missed in the song + * Used to play character animations. + * @param event The event. + */ public function onNoteMiss(event:NoteScriptEvent):Void { if (character != null) character.onNoteMiss(event); - - this.repositionChildren(); } + /** + * Called when a key is pressed but no note is hit in the song + * Used to play character animations. + * @param event The event. + */ public function onNoteGhostMiss(event:GhostMissNoteScriptEvent):Void { if (character != null) character.onNoteGhostMiss(event); - - this.repositionChildren(); } } @:access(funkin.ui.haxeui.components.CharacterPlayer) private class Layout extends DefaultLayout { - public override function repositionChildren() + public override function resizeChildren():Void { - var player = cast(_component, CharacterPlayer); - var sprite:BaseCharacter = player.character; - if (sprite == null) + super.resizeChildren(); + + var player:CharacterPlayer = cast(_component, CharacterPlayer); + var character:BaseCharacter = player.character; + if (character == null) { - return super.repositionChildren(); + return super.resizeChildren(); } - @:privateAccess - var animOffsets = sprite.animOffsets; - - sprite.x = _component.screenLeft + ((_component.width / 2) - (sprite.frameWidth / 2)); - sprite.x += animOffsets[0]; - sprite.y = _component.screenTop + ((_component.height / 2) - (sprite.frameHeight / 2)); - sprite.y += animOffsets[1]; + character.cornerPosition.set(0, 0); + // character.setGraphicSize(Std.int(innerWidth), Std.int(innerHeight)); } public override function calcAutoSize(exclusions:Array = null):Size { - var player = cast(_component, CharacterPlayer); - var sprite = player.character; - if (sprite == null) + var player:CharacterPlayer = cast(_component, CharacterPlayer); + var character:BaseCharacter = player.character; + if (character == null) { return super.calcAutoSize(exclusions); } - var size = new Size(); - size.width = sprite.frameWidth + paddingLeft + paddingRight; - size.height = sprite.frameHeight + paddingTop + paddingBottom; + var size:Size = new Size(); + size.width = character.width + paddingLeft + paddingRight; + size.height = character.height + paddingTop + paddingBottom; return size; } } diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index a0063741b..2d38059db 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -173,9 +173,33 @@ class Constants public static final MS_PER_SEC:Float = 1000; /** - * The number of steps in one beat. - * - * Each beat represents ONE quarter note, so one step is one sixteenth note! + * The number of microseconds in a millisecond. + */ + public static final US_PER_MS:Int = 1000; + + /** + * The number of microseconds in a second. + */ + public static final US_PER_SEC:Int = US_PER_MS * MS_PER_SEC; + + /** + * The number of nanoseconds in a microsecond. + */ + public static final NS_PER_US:Int = 1000; + + /** + * The number of nanoseconds in a millisecond. + */ + public static final NS_PER_MS:Int = NS_PER_US * US_PER_MS; + + /** + * The number of nanoseconds in a second. + */ + public static final NS_PER_SEC:Int = NS_PER_US * US_PER_MS * MS_PER_SEC; + + /** + * Number of steps in a beat. + * One step is one 16th note and one beat is one quarter note. */ public static final STEPS_PER_BEAT:Int = 4; @@ -284,6 +308,13 @@ class Constants */ public static final COUNTDOWN_VOLUME:Float = 0.6; + /** + * The horizontal offset of the strumline from the left edge of the screen. + */ public static final STRUMLINE_X_OFFSET:Float = 48; + + /** + * The vertical offset of the strumline from the top edge of the screen. + */ public static final STRUMLINE_Y_OFFSET:Float = 24; } diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx index 57dc7b12e..3494e620b 100644 --- a/source/funkin/util/FileUtil.hx +++ b/source/funkin/util/FileUtil.hx @@ -26,9 +26,9 @@ class FileUtil ?dialogTitle:String):Bool { #if desktop - var filter = convertTypeFilter(typeFilter); + var filter:String = convertTypeFilter(typeFilter); - var fileDialog = new FileDialog(); + var fileDialog:FileDialog = new FileDialog(); if (onSelect != null) fileDialog.onSelect.add(onSelect); if (onCancel != null) fileDialog.onCancel.add(onCancel); @@ -54,9 +54,9 @@ class FileUtil ?dialogTitle:String):Bool { #if desktop - var filter = convertTypeFilter(typeFilter); + var filter:String = convertTypeFilter(typeFilter); - var fileDialog = new FileDialog(); + var fileDialog:FileDialog = new FileDialog(); if (onSelect != null) fileDialog.onSelect.add(onSelect); if (onCancel != null) fileDialog.onCancel.add(onCancel); @@ -81,9 +81,9 @@ class FileUtil ?dialogTitle:String):Bool { #if desktop - var filter = convertTypeFilter(typeFilter); + var filter:String = convertTypeFilter(typeFilter); - var fileDialog = new FileDialog(); + var fileDialog:FileDialog = new FileDialog(); if (onSelect != null) fileDialog.onSelectMultiple.add(onSelect); if (onCancel != null) fileDialog.onCancel.add(onCancel); @@ -109,9 +109,9 @@ class FileUtil ?dialogTitle:String):Bool { #if desktop - var filter = convertTypeFilter(typeFilter); + var filter:String = convertTypeFilter(typeFilter); - var fileDialog = new FileDialog(); + var fileDialog:FileDialog = new FileDialog(); if (onSelect != null) fileDialog.onSelect.add(onSelect); if (onCancel != null) fileDialog.onCancel.add(onCancel); @@ -136,29 +136,29 @@ class FileUtil public static function openFile(?typeFilter:Array, ?onOpen:Bytes->Void, ?onCancel:Void->Void, ?defaultPath:String, ?dialogTitle:String):Bool { #if desktop - var filter = convertTypeFilter(typeFilter); + var filter:String = convertTypeFilter(typeFilter); - var fileDialog = new FileDialog(); + var fileDialog:FileDialog = new FileDialog(); if (onOpen != null) fileDialog.onOpen.add(onOpen); if (onCancel != null) fileDialog.onCancel.add(onCancel); fileDialog.open(filter, defaultPath, dialogTitle); return true; #elseif html5 - var onFileLoaded = function(event) { + var onFileLoaded:Event->Void = function(event) { var loadedFileRef:FileReference = event.target; trace('Loaded file: ' + loadedFileRef.name); onOpen(loadedFileRef.data); } - var onFileSelected = function(event) { + var onFileSelected:Event->Void = function(event) { var selectedFileRef:FileReference = event.target; trace('Selected file: ' + selectedFileRef.name); selectedFileRef.addEventListener(Event.COMPLETE, onFileLoaded); selectedFileRef.load(); } - var fileRef = new FileReference(); + var fileRef:FileReference = new FileReference(); fileRef.addEventListener(Event.SELECT, onFileSelected); fileRef.browse(typeFilter); return true; @@ -177,18 +177,18 @@ class FileUtil public static function saveFile(data:Bytes, ?onSave:String->Void, ?onCancel:Void->Void, ?defaultFileName:String, ?dialogTitle:String):Bool { #if desktop - var filter = defaultFileName != null ? Path.extension(defaultFileName) : null; + var filter:String = defaultFileName != null ? Path.extension(defaultFileName) : null; - var fileDialog = new FileDialog(); + var fileDialog:FileDialog = new FileDialog(); if (onSave != null) fileDialog.onSelect.add(onSave); if (onCancel != null) fileDialog.onCancel.add(onCancel); fileDialog.save(data, filter, defaultFileName, dialogTitle); return true; #elseif html5 - var filter = defaultFileName != null ? Path.extension(defaultFileName) : null; + var filter:String = defaultFileName != null ? Path.extension(defaultFileName) : null; - var fileDialog = new FileDialog(); + var fileDialog:FileDialog = new FileDialog(); if (onSave != null) fileDialog.onSave.add(onSave); if (onCancel != null) fileDialog.onCancel.add(onCancel); @@ -213,7 +213,7 @@ class FileUtil { #if desktop // Prompt the user for a directory, then write all of the files to there. - var onSelectDir = function(targetPath:String) { + var onSelectDir:String->Void = function(targetPath:String):Void { var paths:Array = []; for (resource in resources) { @@ -230,7 +230,7 @@ class FileUtil writeBytesToPath(filePath, resource.data, force ? Force : Skip); } } - catch (e:Dynamic) + catch (_) { trace('Failed to write file (probably already exists): $filePath' + filePath); continue; @@ -240,7 +240,7 @@ class FileUtil onSaveAll(paths); } - browseForDirectory(null, onSelectDir, onCancel, defaultPath, "Choose directory to save all files to..."); + browseForDirectory(null, onSelectDir, onCancel, defaultPath, 'Choose directory to save all files to...'); return true; #elseif html5 @@ -260,14 +260,14 @@ class FileUtil ?force:Bool = false):Bool { // Create a ZIP file. - var zipBytes = createZIPFromEntries(resources); + var zipBytes:Bytes = createZIPFromEntries(resources); - var onSave = function(path:String) { + var onSave:String->Void = function(path:String) { onSave([path]); }; // Prompt the user to save the ZIP file. - saveFile(zipBytes, onSave, onCancel, defaultPath, "Save files as ZIP..."); + saveFile(zipBytes, onSave, onCancel, defaultPath, 'Save files as ZIP...'); return true; } @@ -282,7 +282,7 @@ class FileUtil { #if desktop // Create a ZIP file. - var zipBytes = createZIPFromEntries(resources); + var zipBytes:Bytes = createZIPFromEntries(resources); // Write the ZIP. writeBytesToPath(path, zipBytes, force ? Force : Skip); @@ -293,13 +293,70 @@ class FileUtil #end } + /** + * Read string file contents directly from a given path. + * Only works on desktop. + * + * @param path The path to the file. + * @return The file contents. + */ + public static function readStringFromPath(path:String):String + { + #if sys + return sys.io.File.getContent(path); + #else + return null; + #end + } + + /** + * Read bytes file contents directly from a given path. + * Only works on desktop. + * + * @param path The path to the file. + * @return The file contents. + */ + public static function readBytesFromPath(path:String):Bytes + { + #if sys + return Bytes.ofString(sys.io.File.getContent(path)); + #else + return null; + #end + } + + /** + * Read JSON file contents directly from a given path. + * Only works on desktop. + * + * @param path The path to the file. + * @return The JSON data. + */ + public static function readJSONFromPath(path:String):Dynamic + { + #if sys + try + { + return SerializerUtil.fromJSON(sys.io.File.getContent(path)); + } + catch (ex) + { + return null; + } + #else + return null; + #end + } + /** * Write string file contents directly to a given path. * Only works on desktop. * + * @param path The path to the file. + * @param data The string to write. * @param mode Whether to Force, Skip, or Ask to overwrite an existing file. */ - public static function writeStringToPath(path:String, data:String, mode:FileWriteMode = Skip) + public static function writeStringToPath(path:String, data:String, mode:FileWriteMode = Skip):Void { #if sys createDirIfNotExists(Path.directory(path)); @@ -336,9 +393,11 @@ class FileUtil * Write byte file contents directly to a given path. * Only works on desktop. * + * @param path The path to the file. + * @param data The bytes to write. * @param mode Whether to Force, Skip, or Ask to overwrite an existing file. */ - public static function writeBytesToPath(path:String, data:Bytes, mode:FileWriteMode = Skip) + public static function writeBytesToPath(path:String, data:Bytes, mode:FileWriteMode = Skip):Void { #if sys createDirIfNotExists(Path.directory(path)); @@ -374,8 +433,11 @@ class FileUtil /** * Write string file contents directly to the end of a file at the given path. * Only works on desktop. + * + * @param path The path to the file. + * @param data The string to append. */ - public static function appendStringToPath(path:String, data:String) + public static function appendStringToPath(path:String, data:String):Void { #if sys sys.io.File.append(path, false).writeString(data); @@ -387,8 +449,10 @@ class FileUtil /** * Create a directory if it doesn't already exist. * Only works on desktop. + * + * @param dir The path to the directory. */ - public static function createDirIfNotExists(dir:String) + public static function createDirIfNotExists(dir:String):Void { #if sys if (!sys.FileSystem.exists(dir)) @@ -404,6 +468,8 @@ class FileUtil /** * Get the path to a temporary directory we can use for writing files. * Only works on desktop. + * + * @return The path to the temporary directory. */ public static function getTempDir():String { @@ -421,9 +487,11 @@ class FileUtil if (path != null) break; } - return tempDir = Path.join([path, 'funkin/']); + tempDir = Path.join([path, 'funkin/']); + return tempDir; #else - return tempDir = '/tmp/funkin/'; + tempDir = '/tmp/funkin/'; + return tempDir; #end #else return null; @@ -438,9 +506,9 @@ class FileUtil */ public static function createZIPFromEntries(entries:Array):Bytes { - var o = new haxe.io.BytesOutput(); + var o:haxe.io.BytesOutput = new haxe.io.BytesOutput(); - var zipWriter = new haxe.zip.Writer(o); + var zipWriter:haxe.zip.Writer = new haxe.zip.Writer(o); zipWriter.write(entries.list()); return o.getBytes(); @@ -455,8 +523,20 @@ class FileUtil */ public static function makeZIPEntry(name:String, content:String):Entry { - var data = haxe.io.Bytes.ofString(content, UTF8); + var data:Bytes = haxe.io.Bytes.ofString(content, UTF8); + return makeZIPEntryFromBytes(name, data); + } + + /** + * Create a ZIP file entry from a file name and its string contents. + * + * @param name The name of the file. You can use slashes to create subdirectories. + * @param data The byte data of the file. + * @return The resulting entry. + */ + public static function makeZIPEntryFromBytes(name:String, data:haxe.io.Bytes):Entry + { return { fileName: name, fileSize: data.length, @@ -474,15 +554,15 @@ class FileUtil static function convertTypeFilter(typeFilter:Array):String { - var filter = null; + var filter:String = null; if (typeFilter != null) { - var filters = []; + var filters:Array = []; for (type in typeFilter) { - filters.push(StringTools.replace(StringTools.replace(type.extension, "*.", ""), ";", ",")); + filters.push(StringTools.replace(StringTools.replace(type.extension, '*.', ''), ';', ',')); } - filter = filters.join(";"); + filter = filters.join(';'); } return filter; diff --git a/source/funkin/util/SerializerUtil.hx b/source/funkin/util/SerializerUtil.hx index 662c05250..9452b7785 100644 --- a/source/funkin/util/SerializerUtil.hx +++ b/source/funkin/util/SerializerUtil.hx @@ -44,7 +44,16 @@ class SerializerUtil */ public static function fromJSONBytes(input:Bytes):Dynamic { - return Json.parse(input.toString()); + try + { + return Json.parse(input.toString()); + } + catch (e:Dynamic) + { + trace('An error occurred while parsing JSON from byte data'); + trace(e); + return null; + } } /** diff --git a/source/funkin/util/SortUtil.hx b/source/funkin/util/SortUtil.hx index 1b38edd28..61418c299 100644 --- a/source/funkin/util/SortUtil.hx +++ b/source/funkin/util/SortUtil.hx @@ -31,9 +31,12 @@ class SortUtil /** * Sort predicate for sorting strings alphabetically. */ - public static function alphabetical(a:String, b:String):Int + public static function alphabetically(a:String, b:String) { + a = a.toUpperCase(); + b = b.toUpperCase(); + // Sort alphabetically. Yes that's how this works. - return a > b ? 1 : -1; + return a == b ? 0 : a > b ? 1 : -1; } }