diff --git a/Project.xml b/Project.xml index e0677b026..74da3b749 100644 --- a/Project.xml +++ b/Project.xml @@ -111,6 +111,7 @@ + @@ -127,7 +128,7 @@ - + diff --git a/art b/art index 1656bea53..03e7c2a23 160000 --- a/art +++ b/art @@ -1 +1 @@ -Subproject commit 1656bea5370c65879aaeb323e329f403c78071c5 +Subproject commit 03e7c2a2353b184e45955c96d763b7cdf1acbc34 diff --git a/assets b/assets index 198c3ab87..a3e2277e6 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 198c3ab87e401e595be50814af0f667eb1be25e7 +Subproject commit a3e2277e6f12208f9a976b80883db67c54a2a897 diff --git a/hmm.json b/hmm.json index 57fbbb555..d461edd24 100644 --- a/hmm.json +++ b/hmm.json @@ -54,14 +54,14 @@ "name": "haxeui-core", "type": "git", "dir": null, - "ref": "e765a3e0b7a653823e8dec765e04623f27f573f8", + "ref": "5086e59e7551d775ed4d1fb0188e31de22d1312b", "url": "https://github.com/haxeui/haxeui-core" }, { "name": "haxeui-flixel", "type": "git", "dir": null, - "ref": "7a517d561eff49d8123c128bf9f5c1123b84d014", + "ref": "2b9cff727999b53ed292b1675ac1c9089ac77600", "url": "https://github.com/haxeui/haxeui-flixel" }, { diff --git a/source/Main.hx b/source/Main.hx index 5fbb6747b..86e520e69 100644 --- a/source/Main.hx +++ b/source/Main.hx @@ -112,5 +112,6 @@ class Main extends Sprite Toolkit.theme = 'dark'; // don't be cringe Toolkit.autoScale = false; funkin.input.Cursor.registerHaxeUICursors(); + haxe.ui.tooltips.ToolTipManager.defaultDelay = 200; } } diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index 7b34bffe2..05c23108f 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -11,6 +11,7 @@ import funkin.data.song.SongDataUtils; * A core class which handles musical timing throughout the game, * both in gameplay and in menus. */ +@:nullSafety class Conductor { // onBeatHit is called every quarter note @@ -28,29 +29,53 @@ class Conductor // 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second // 7/8 = 3.5 beats per measure = 14 steps per measure + /** + * The current instance of the Conductor. + * If one doesn't currently exist, a new one will be created. + * + * You can also do stuff like store a reference to the Conductor and pass it around or temporarily replace it, + * or have a second Conductor running at the same time, or other weird stuff like that if you need to. + */ + public static var instance:Conductor = new Conductor(); + + /** + * Signal fired when the current Conductor instance advances to a new measure. + */ + public static var measureHit(default, null):FlxSignal = new FlxSignal(); + + /** + * Signal fired when the current Conductor instance advances to a new beat. + */ + public static var beatHit(default, null):FlxSignal = new FlxSignal(); + + /** + * Signal fired when the current Conductor instance advances to a new step. + */ + public static var stepHit(default, null):FlxSignal = new FlxSignal(); + /** * The list of time changes in the song. * There should be at least one time change (at the beginning of the song) to define the BPM. */ - static var timeChanges:Array = []; + var timeChanges:Array = []; /** * The most recent time change for the current song position. */ - public static var currentTimeChange(default, null):Null; + public var currentTimeChange(default, null):Null; /** * The current position in the song in milliseconds. - * Update this every frame based on the audio position using `Conductor.update()`. + * Update this every frame based on the audio position using `Conductor.instance.update()`. */ - public static var songPosition(default, null):Float = 0; + public var songPosition(default, null):Float = 0; /** * Beats per minute of the current song at the current time. */ - public static var bpm(get, never):Float; + public var bpm(get, never):Float; - static function get_bpm():Float + function get_bpm():Float { if (bpmOverride != null) return bpmOverride; @@ -62,9 +87,9 @@ class Conductor /** * Beats per minute of the current song at the start time. */ - public static var startingBPM(get, never):Float; + public var startingBPM(get, never):Float; - static function get_startingBPM():Float + function get_startingBPM():Float { if (bpmOverride != null) return bpmOverride; @@ -78,14 +103,14 @@ class Conductor * The current value set by `forceBPM`. * If false, BPM is determined by time changes. */ - static var bpmOverride:Null = null; + var bpmOverride:Null = null; /** * Duration of a measure in milliseconds. Calculated based on bpm. */ - public static var measureLengthMs(get, never):Float; + public var measureLengthMs(get, never):Float; - static function get_measureLengthMs():Float + function get_measureLengthMs():Float { return beatLengthMs * timeSignatureNumerator; } @@ -93,9 +118,9 @@ class Conductor /** * Duration of a beat (quarter note) in milliseconds. Calculated based on bpm. */ - public static var beatLengthMs(get, never):Float; + public var beatLengthMs(get, never):Float; - static function get_beatLengthMs():Float + function get_beatLengthMs():Float { // Tied directly to BPM. return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC); @@ -104,25 +129,25 @@ class Conductor /** * Duration of a step (sixtennth note) in milliseconds. Calculated based on bpm. */ - public static var stepLengthMs(get, never):Float; + public var stepLengthMs(get, never):Float; - static function get_stepLengthMs():Float + function get_stepLengthMs():Float { return beatLengthMs / timeSignatureNumerator; } - public static var timeSignatureNumerator(get, never):Int; + public var timeSignatureNumerator(get, never):Int; - static function get_timeSignatureNumerator():Int + function get_timeSignatureNumerator():Int { if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_NUM; return currentTimeChange.timeSignatureNum; } - public static var timeSignatureDenominator(get, never):Int; + public var timeSignatureDenominator(get, never):Int; - static function get_timeSignatureDenominator():Int + function get_timeSignatureDenominator():Int { if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_DEN; @@ -132,44 +157,44 @@ class Conductor /** * Current position in the song, in measures. */ - public static var currentMeasure(default, null):Int = 0; + public var currentMeasure(default, null):Int = 0; /** * Current position in the song, in beats. */ - public static var currentBeat(default, null):Int = 0; + public var currentBeat(default, null):Int = 0; /** * Current position in the song, in steps. */ - public static var currentStep(default, null):Int = 0; + public var currentStep(default, null):Int = 0; /** * Current position in the song, in measures and fractions of a measure. */ - public static var currentMeasureTime(default, null):Float = 0; + public var currentMeasureTime(default, null):Float = 0; /** * Current position in the song, in beats and fractions of a measure. */ - public static var currentBeatTime(default, null):Float = 0; + public var currentBeatTime(default, null):Float = 0; /** * Current position in the song, in steps and fractions of a step. */ - public static var currentStepTime(default, null):Float = 0; + public var currentStepTime(default, null):Float = 0; /** * An offset tied to the current chart file to compensate for a delay in the instrumental. */ - public static var instrumentalOffset:Float = 0; + public var instrumentalOffset:Float = 0; /** * The instrumental offset, in terms of steps. */ - public static var instrumentalOffsetSteps(get, never):Float; + public var instrumentalOffsetSteps(get, never):Float; - static function get_instrumentalOffsetSteps():Float + function get_instrumentalOffsetSteps():Float { var startingStepLengthMs:Float = ((Constants.SECS_PER_MIN / startingBPM) * Constants.MS_PER_SEC) / timeSignatureNumerator; @@ -179,19 +204,19 @@ class Conductor /** * An offset tied to the file format of the audio file being played. */ - public static var formatOffset:Float = 0; + public var formatOffset:Float = 0; /** * An offset set by the user to compensate for input lag. */ - public static var inputOffset:Float = 0; + public var inputOffset:Float = 0; /** * The number of beats in a measure. May be fractional depending on the time signature. */ - public static var beatsPerMeasure(get, never):Float; + public var beatsPerMeasure(get, never):Float; - static function get_beatsPerMeasure():Float + function get_beatsPerMeasure():Float { // NOTE: Not always an integer, for example 7/8 is 3.5 beats per measure return stepsPerMeasure / Constants.STEPS_PER_BEAT; @@ -201,30 +226,15 @@ class Conductor * The number of steps in a measure. * TODO: I don't think this can be fractional? */ - public static var stepsPerMeasure(get, never):Int; + public var stepsPerMeasure(get, never):Int; - static function get_stepsPerMeasure():Int + function get_stepsPerMeasure():Int { // TODO: Is this always an integer? return Std.int(timeSignatureNumerator / timeSignatureDenominator * Constants.STEPS_PER_BEAT * Constants.STEPS_PER_BEAT); } - /** - * Signal fired when the Conductor advances to a new measure. - */ - public static var measureHit(default, null):FlxSignal = new FlxSignal(); - - /** - * Signal fired when the Conductor advances to a new beat. - */ - public static var beatHit(default, null):FlxSignal = new FlxSignal(); - - /** - * Signal fired when the Conductor advances to a new step. - */ - public static var stepHit(default, null):FlxSignal = new FlxSignal(); - - function new() {} + public function new() {} /** * Forcibly defines the current BPM of the song. @@ -235,7 +245,7 @@ class Conductor * WARNING: Avoid this for things like setting the BPM of the title screen music, * you should have a metadata file for it instead. */ - public static function forceBPM(?bpm:Float = null) + public function forceBPM(?bpm:Float = null) { if (bpm != null) { @@ -246,7 +256,7 @@ class Conductor // trace('[CONDUCTOR] Resetting BPM to default'); } - Conductor.bpmOverride = bpm; + this.bpmOverride = bpm; } /** @@ -256,29 +266,29 @@ class Conductor * @param songPosition The current position in the song in milliseconds. * Leave blank to use the FlxG.sound.music position. */ - public static function update(?songPosition:Float) + public function update(?songPos:Float) { - if (songPosition == null) + if (songPos == null) { // Take into account instrumental and file format song offsets. - songPosition = (FlxG.sound.music != null) ? (FlxG.sound.music.time + instrumentalOffset + formatOffset) : 0.0; + songPos = (FlxG.sound.music != null) ? (FlxG.sound.music.time + instrumentalOffset + formatOffset) : 0.0; } - var oldMeasure = currentMeasure; - var oldBeat = currentBeat; - var oldStep = currentStep; + var oldMeasure = this.currentMeasure; + var oldBeat = this.currentBeat; + var oldStep = this.currentStep; // Set the song position we are at (for purposes of calculating note positions, etc). - Conductor.songPosition = songPosition; + this.songPosition = songPos; currentTimeChange = timeChanges[0]; - if (Conductor.songPosition > 0.0) + if (this.songPosition > 0.0) { for (i in 0...timeChanges.length) { - if (songPosition >= timeChanges[i].timeStamp) currentTimeChange = timeChanges[i]; + if (this.songPosition >= timeChanges[i].timeStamp) currentTimeChange = timeChanges[i]; - if (songPosition < timeChanges[i].timeStamp) break; + if (this.songPosition < timeChanges[i].timeStamp) break; } } @@ -286,45 +296,49 @@ class Conductor { trace('WARNING: Conductor is broken, timeChanges is empty.'); } - else if (currentTimeChange != null && Conductor.songPosition > 0.0) + else if (currentTimeChange != null && this.songPosition > 0.0) { // roundDecimal prevents representing 8 as 7.9999999 - currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6); - currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT; - currentMeasureTime = currentStepTime / stepsPerMeasure; - currentStep = Math.floor(currentStepTime); - currentBeat = Math.floor(currentBeatTime); - currentMeasure = Math.floor(currentMeasureTime); + this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6); + this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT; + this.currentMeasureTime = currentStepTime / stepsPerMeasure; + this.currentStep = Math.floor(currentStepTime); + this.currentBeat = Math.floor(currentBeatTime); + this.currentMeasure = Math.floor(currentMeasureTime); } else { // Assume a constant BPM equal to the forced value. - currentStepTime = FlxMath.roundDecimal((songPosition / stepLengthMs), 4); - currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT; - currentMeasureTime = currentStepTime / stepsPerMeasure; - currentStep = Math.floor(currentStepTime); - currentBeat = Math.floor(currentBeatTime); - currentMeasure = Math.floor(currentMeasureTime); + this.currentStepTime = FlxMath.roundDecimal((songPosition / stepLengthMs), 4); + this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT; + this.currentMeasureTime = currentStepTime / stepsPerMeasure; + this.currentStep = Math.floor(currentStepTime); + this.currentBeat = Math.floor(currentBeatTime); + this.currentMeasure = Math.floor(currentMeasureTime); } - // FlxSignals are really cool. - if (currentStep != oldStep) + // Only fire the signal if we are THE Conductor. + if (this == Conductor.instance) { - stepHit.dispatch(); - } + // FlxSignals are really cool. + if (currentStep != oldStep) + { + Conductor.stepHit.dispatch(); + } - if (currentBeat != oldBeat) - { - beatHit.dispatch(); - } + if (currentBeat != oldBeat) + { + Conductor.beatHit.dispatch(); + } - if (currentMeasure != oldMeasure) - { - measureHit.dispatch(); + if (currentMeasure != oldMeasure) + { + Conductor.measureHit.dispatch(); + } } } - public static function mapTimeChanges(songTimeChanges:Array) + public function mapTimeChanges(songTimeChanges:Array) { timeChanges = []; @@ -338,24 +352,21 @@ class Conductor // Without any custom handling, `currentStepTime` becomes non-zero at `songPosition = 0`. if (currentTimeChange.timeStamp < 0.0) currentTimeChange.timeStamp = 0.0; - if (currentTimeChange.beatTime == null) + if (currentTimeChange.timeStamp <= 0.0) { - if (currentTimeChange.timeStamp <= 0.0) - { - currentTimeChange.beatTime = 0.0; - } - else - { - // Calculate the beat time of this timestamp. - currentTimeChange.beatTime = 0.0; + currentTimeChange.beatTime = 0.0; + } + else + { + // Calculate the beat time of this timestamp. + currentTimeChange.beatTime = 0.0; - if (currentTimeChange.timeStamp > 0.0 && timeChanges.length > 0) - { - var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1]; - currentTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime - + ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC), - 4); - } + if (currentTimeChange.timeStamp > 0.0 && timeChanges.length > 0) + { + var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1]; + currentTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime + + ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC), + 4); } } @@ -368,13 +379,13 @@ class Conductor } // Update currentStepTime - Conductor.update(Conductor.songPosition); + this.update(Conductor.instance.songPosition); } /** * Given a time in milliseconds, return a time in steps. */ - public static function getTimeInSteps(ms:Float):Float + public function getTimeInSteps(ms:Float):Float { if (timeChanges.length == 0) { @@ -411,7 +422,7 @@ class Conductor /** * Given a time in steps and fractional steps, return a time in milliseconds. */ - public static function getStepTimeInMs(stepTime:Float):Float + public function getStepTimeInMs(stepTime:Float):Float { if (timeChanges.length == 0) { @@ -447,7 +458,7 @@ class Conductor /** * Given a time in beats and fractional beats, return a time in milliseconds. */ - public static function getBeatTimeInMs(beatTime:Float):Float + public function getBeatTimeInMs(beatTime:Float):Float { if (timeChanges.length == 0) { @@ -480,13 +491,20 @@ class Conductor } } + public static function watchQuick():Void + { + FlxG.watch.addQuick("songPosition", Conductor.instance.songPosition); + FlxG.watch.addQuick("bpm", Conductor.instance.bpm); + FlxG.watch.addQuick("currentMeasureTime", Conductor.instance.currentMeasureTime); + FlxG.watch.addQuick("currentBeatTime", Conductor.instance.currentBeatTime); + FlxG.watch.addQuick("currentStepTime", Conductor.instance.currentStepTime); + } + + /** + * Reset the Conductor, replacing the current instance with a fresh one. + */ public static function reset():Void { - beatHit.removeAll(); - stepHit.removeAll(); - - mapTimeChanges([]); - forceBPM(null); - update(0); + Conductor.instance = new Conductor(); } } diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 13bcd306e..02b46c88c 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -19,7 +19,7 @@ import funkin.play.PlayStatePlaylist; import openfl.display.BitmapData; import funkin.data.level.LevelRegistry; import funkin.data.notestyle.NoteStyleRegistry; -import funkin.data.event.SongEventData.SongEventParser; +import funkin.data.event.SongEventRegistry; import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.play.cutscene.dialogue.DialogueBoxDataParser; import funkin.play.cutscene.dialogue.SpeakerDataParser; @@ -197,6 +197,13 @@ class InitState extends FlxState FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK]; #end + // + // FLIXEL PLUGINS + // + funkin.util.plugins.EvacuateDebugPlugin.initialize(); + funkin.util.plugins.ReloadAssetsDebugPlugin.initialize(); + funkin.util.plugins.WatchPlugin.initialize(); + // // GAME DATA PARSING // @@ -206,7 +213,7 @@ class InitState extends FlxState SongRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries(); NoteStyleRegistry.instance.loadEntries(); - SongEventParser.loadEventCache(); + SongEventRegistry.loadEventCache(); ConversationDataParser.loadConversationCache(); DialogueBoxDataParser.loadDialogueBoxCache(); SpeakerDataParser.loadSpeakerCache(); diff --git a/source/funkin/audio/visualize/ABotVis.hx b/source/funkin/audio/visualize/ABotVis.hx index 681287808..89b004df4 100644 --- a/source/funkin/audio/visualize/ABotVis.hx +++ b/source/funkin/audio/visualize/ABotVis.hx @@ -64,7 +64,7 @@ class ABotVis extends FlxTypedSpriteGroup if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples)); else - remappedShit = Std.int(FlxMath.remapToRange(Conductor.songPosition, 0, vis.snd.length, 0, vis.numSamples)); + remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, vis.numSamples)); var fftSamples:Array = []; diff --git a/source/funkin/audio/visualize/SpectogramSprite.hx b/source/funkin/audio/visualize/SpectogramSprite.hx index 63d0fcd2e..b4e024a4c 100644 --- a/source/funkin/audio/visualize/SpectogramSprite.hx +++ b/source/funkin/audio/visualize/SpectogramSprite.hx @@ -164,7 +164,7 @@ class SpectogramSprite extends FlxTypedSpriteGroup if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, numSamples)); else - remappedShit = Std.int(FlxMath.remapToRange(Conductor.songPosition, 0, vis.snd.length, 0, numSamples)); + remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, numSamples)); var fftSamples:Array = []; var i = remappedShit; @@ -235,15 +235,15 @@ class SpectogramSprite extends FlxTypedSpriteGroup if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, numSamples)); else { - if (curTime == Conductor.songPosition) + if (curTime == Conductor.instance.songPosition) { wavOptimiz = 3; return; // already did shit, so finishes function early } - curTime = Conductor.songPosition; + curTime = Conductor.instance.songPosition; - remappedShit = Std.int(FlxMath.remapToRange(Conductor.songPosition, 0, vis.snd.length, 0, numSamples)); + remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, numSamples)); } wavOptimiz = 8; diff --git a/source/funkin/data/event/SongEventData.hx b/source/funkin/data/event/SongEventRegistry.hx similarity index 71% rename from source/funkin/data/event/SongEventData.hx rename to source/funkin/data/event/SongEventRegistry.hx index 7a167b031..dc5589813 100644 --- a/source/funkin/data/event/SongEventData.hx +++ b/source/funkin/data/event/SongEventRegistry.hx @@ -1,7 +1,7 @@ package funkin.data.event; import funkin.play.event.SongEvent; -import funkin.data.event.SongEventData.SongEventSchema; +import funkin.data.event.SongEventSchema; import funkin.data.song.SongData.SongEventData; import funkin.util.macro.ClassMacro; import funkin.play.event.ScriptedSongEvent; @@ -9,7 +9,7 @@ import funkin.play.event.ScriptedSongEvent; /** * This class statically handles the parsing of internal and scripted song event handlers. */ -class SongEventParser +class SongEventRegistry { /** * Every built-in event class must be added to this list. @@ -160,84 +160,3 @@ class SongEventParser } } } - -enum abstract SongEventFieldType(String) from String to String -{ - /** - * The STRING type will display as a text field. - */ - var STRING = "string"; - - /** - * The INTEGER type will display as a text field that only accepts numbers. - */ - var INTEGER = "integer"; - - /** - * The FLOAT type will display as a text field that only accepts numbers. - */ - var FLOAT = "float"; - - /** - * The BOOL type will display as a checkbox. - */ - var BOOL = "bool"; - - /** - * The ENUM type will display as a dropdown. - * Make sure to specify the `keys` field in the schema. - */ - var ENUM = "enum"; -} - -typedef SongEventSchemaField = -{ - /** - * The name of the property as it should be saved in the event data. - */ - name:String, - - /** - * The title of the field to display in the UI. - */ - title:String, - - /** - * The type of the field. - */ - type:SongEventFieldType, - - /** - * Used only for ENUM values. - * The key is the display name and the value is the actual value. - */ - ?keys:Map, - - /** - * Used for INTEGER and FLOAT values. - * The minimum value that can be entered. - * @default No minimum - */ - ?min:Float, - - /** - * Used for INTEGER and FLOAT values. - * The maximum value that can be entered. - * @default No maximum - */ - ?max:Float, - - /** - * Used for INTEGER and FLOAT values. - * The step value that will be used when incrementing/decrementing the value. - * @default `0.1` - */ - ?step:Float, - - /** - * An optional default value for the field. - */ - ?defaultValue:Dynamic, -} - -typedef SongEventSchema = Array; diff --git a/source/funkin/data/event/SongEventSchema.hx b/source/funkin/data/event/SongEventSchema.hx new file mode 100644 index 000000000..b5b2978d7 --- /dev/null +++ b/source/funkin/data/event/SongEventSchema.hx @@ -0,0 +1,125 @@ +package funkin.data.event; + +import funkin.play.event.SongEvent; +import funkin.data.event.SongEventSchema; +import funkin.data.song.SongData.SongEventData; +import funkin.util.macro.ClassMacro; +import funkin.play.event.ScriptedSongEvent; + +@:forward(name, tittlte, type, keys, min, max, step, defaultValue, iterator) +abstract SongEventSchema(SongEventSchemaRaw) +{ + public function new(?fields:Array) + { + this = fields; + } + + @:arrayAccess + public inline function getByName(name:String):SongEventSchemaField + { + for (field in this) + { + if (field.name == name) return field; + } + + return null; + } + + public function getFirstField():SongEventSchemaField + { + return this[0]; + } + + @:arrayAccess + public inline function get(key:Int) + { + return this[key]; + } + + @:arrayAccess + public inline function arrayWrite(k:Int, v:SongEventSchemaField):SongEventSchemaField + { + return this[k] = v; + } +} + +typedef SongEventSchemaRaw = Array; + +typedef SongEventSchemaField = +{ + /** + * The name of the property as it should be saved in the event data. + */ + name:String, + + /** + * The title of the field to display in the UI. + */ + title:String, + + /** + * The type of the field. + */ + type:SongEventFieldType, + + /** + * Used only for ENUM values. + * The key is the display name and the value is the actual value. + */ + ?keys:Map, + + /** + * Used for INTEGER and FLOAT values. + * The minimum value that can be entered. + * @default No minimum + */ + ?min:Float, + + /** + * Used for INTEGER and FLOAT values. + * The maximum value that can be entered. + * @default No maximum + */ + ?max:Float, + + /** + * Used for INTEGER and FLOAT values. + * The step value that will be used when incrementing/decrementing the value. + * @default `0.1` + */ + ?step:Float, + + /** + * An optional default value for the field. + */ + ?defaultValue:Dynamic, +} + +enum abstract SongEventFieldType(String) from String to String +{ + /** + * The STRING type will display as a text field. + */ + var STRING = "string"; + + /** + * The INTEGER type will display as a text field that only accepts numbers. + */ + var INTEGER = "integer"; + + /** + * The FLOAT type will display as a text field that only accepts numbers. + */ + var FLOAT = "float"; + + /** + * The BOOL type will display as a checkbox. + */ + var BOOL = "bool"; + + /** + * The ENUM type will display as a dropdown. + * Make sure to specify the `keys` field in the schema. + */ + var ENUM = "enum"; +} diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 600871e2f..1a726254f 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -1,7 +1,10 @@ package funkin.data.song; +import funkin.data.event.SongEventRegistry; +import funkin.data.event.SongEventSchema; import funkin.data.song.SongRegistry; import thx.semver.Version; +import funkin.util.tools.ICloneable; /** * Data containing information about a song. @@ -9,7 +12,7 @@ import thx.semver.Version; * Data which is only necessary in-game should be stored in the SongChartData. */ @:nullSafety -class SongMetadata +class SongMetadata implements ICloneable { /** * A semantic versioning string for the song data format. @@ -84,16 +87,16 @@ class SongMetadata * @param newVariation Set to a new variation ID to change the new metadata. * @return The cloned SongMetadata */ - public function clone(?newVariation:String = null):SongMetadata + public function clone():SongMetadata { - var result:SongMetadata = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation); + var result:SongMetadata = new SongMetadata(this.songName, this.artist, this.variation); result.version = this.version; result.timeFormat = this.timeFormat; result.divisions = this.divisions; - result.offsets = this.offsets; - result.timeChanges = this.timeChanges; + result.offsets = this.offsets.clone(); + result.timeChanges = this.timeChanges.deepClone(); result.looped = this.looped; - result.playData = this.playData; + result.playData = this.playData.clone(); result.generatedBy = this.generatedBy; return result; @@ -128,7 +131,7 @@ enum abstract SongTimeFormat(String) from String to String var MILLISECONDS = 'ms'; } -class SongTimeChange +class SongTimeChange implements ICloneable { public static final DEFAULT_SONGTIMECHANGE:SongTimeChange = new SongTimeChange(0, 100); @@ -149,7 +152,7 @@ class SongTimeChange */ @:optional @:alias("b") - public var beatTime:Null; + public var beatTime:Float; /** * Quarter notes per minute (float). Cannot be empty in the first element of the list, @@ -195,6 +198,11 @@ class SongTimeChange this.beatTuplets = beatTuplets == null ? DEFAULT_BEAT_TUPLETS : beatTuplets; } + public function clone():SongTimeChange + { + return new SongTimeChange(this.timeStamp, this.bpm, this.timeSignatureNum, this.timeSignatureDen, this.beatTime, this.beatTuplets); + } + /** * Produces a string representation suitable for debugging. */ @@ -209,7 +217,7 @@ class SongTimeChange * These are intended to correct for issues with the chart, or with the song's audio (for example a 10ms delay before the song starts). * This is independent of the offsets applied in the user's settings, which are applied after these offsets and intended to correct for the user's hardware. */ -class SongOffsets +class SongOffsets implements ICloneable { /** * The offset, in milliseconds, to apply to the song's instrumental relative to the chart. @@ -279,6 +287,15 @@ class SongOffsets return value; } + public function clone():SongOffsets + { + var result:SongOffsets = new SongOffsets(this.instrumental); + result.altInstrumentals = this.altInstrumentals.clone(); + result.vocals = this.vocals.clone(); + + return result; + } + /** * Produces a string representation suitable for debugging. */ @@ -292,7 +309,7 @@ class SongOffsets * Metadata for a song only used for the music. * For example, the menu music. */ -class SongMusicData +class SongMusicData implements ICloneable { /** * A semantic versioning string for the song data format. @@ -346,13 +363,13 @@ class SongMusicData this.variation = variation == null ? Constants.DEFAULT_VARIATION : variation; } - public function clone(?newVariation:String = null):SongMusicData + public function clone():SongMusicData { - var result:SongMusicData = new SongMusicData(this.songName, this.artist, newVariation == null ? this.variation : newVariation); + var result:SongMusicData = new SongMusicData(this.songName, this.artist, this.variation); result.version = this.version; result.timeFormat = this.timeFormat; result.divisions = this.divisions; - result.timeChanges = this.timeChanges; + result.timeChanges = this.timeChanges.clone(); result.looped = this.looped; result.generatedBy = this.generatedBy; @@ -368,7 +385,7 @@ class SongMusicData } } -class SongPlayData +class SongPlayData implements ICloneable { /** * The variations this song has. The associated metadata files should exist. @@ -417,6 +434,20 @@ class SongPlayData ratings = new Map(); } + public function clone():SongPlayData + { + var result:SongPlayData = new SongPlayData(); + result.songVariations = this.songVariations.clone(); + result.difficulties = this.difficulties.clone(); + result.characters = this.characters.clone(); + result.stage = this.stage; + result.noteStyle = this.noteStyle; + result.ratings = this.ratings.clone(); + result.album = this.album; + + return result; + } + /** * Produces a string representation suitable for debugging. */ @@ -430,7 +461,7 @@ class SongPlayData * Information about the characters used in this variation of the song. * Create a new variation if you want to change the characters. */ -class SongCharacterData +class SongCharacterData implements ICloneable { @:optional @:default('') @@ -460,6 +491,14 @@ class SongCharacterData this.instrumental = instrumental; } + public function clone():SongCharacterData + { + var result:SongCharacterData = new SongCharacterData(this.player, this.girlfriend, this.opponent, this.instrumental); + result.altInstrumentals = this.altInstrumentals.clone(); + + return result; + } + /** * Produces a string representation suitable for debugging. */ @@ -469,7 +508,7 @@ class SongCharacterData } } -class SongChartData +class SongChartData implements ICloneable { @:default(funkin.data.song.SongRegistry.SONG_CHART_DATA_VERSION) @:jcustomparse(funkin.data.DataParse.semverVersion) @@ -539,6 +578,24 @@ class SongChartData return writer.write(this, pretty ? ' ' : null); } + public function clone():SongChartData + { + // We have to manually perform the deep clone here because Map.deepClone() doesn't work. + var noteDataClone:Map> = new Map>(); + for (key in this.notes.keys()) + { + noteDataClone.set(key, this.notes.get(key).deepClone()); + } + var eventDataClone:Array = this.events.deepClone(); + + var result:SongChartData = new SongChartData(this.scrollSpeed.clone(), eventDataClone, noteDataClone); + result.version = this.version; + result.generatedBy = this.generatedBy; + result.variation = this.variation; + + return result; + } + /** * Produces a string representation suitable for debugging. */ @@ -548,7 +605,7 @@ class SongChartData } } -class SongEventDataRaw +class SongEventDataRaw implements ICloneable { /** * The timestamp of the event. The timestamp is in the format of the song's time format. @@ -602,14 +659,19 @@ class SongEventDataRaw { if (_stepTime != null && !force) return _stepTime; - return _stepTime = Conductor.getTimeInSteps(this.time); + return _stepTime = Conductor.instance.getTimeInSteps(this.time); + } + + public function clone():SongEventDataRaw + { + return new SongEventDataRaw(this.time, this.event, this.value); } } /** * Wrap SongEventData in an abstract so we can overload operators. */ -@:forward(time, event, value, activated, getStepTime) +@:forward(time, event, value, activated, getStepTime, clone) abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw { public function new(time:Float, event:String, value:Dynamic = null) @@ -617,6 +679,33 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR this = new SongEventDataRaw(time, event, value); } + public inline function valueAsStruct(?defaultKey:String = "key"):Dynamic + { + if (this.value == null) return {}; + if (Std.isOfType(this.value, Array)) + { + var result:haxe.DynamicAccess = {}; + result.set(defaultKey, this.value); + return cast result; + } + else if (Reflect.isObject(this.value)) + { + // We enter this case if the value is a struct. + return cast this.value; + } + else + { + var result:haxe.DynamicAccess = {}; + result.set(defaultKey, this.value); + return cast result; + } + } + + public inline function getSchema():Null + { + return SongEventRegistry.getEventSchema(this.event); + } + public inline function getDynamic(key:String):Null { return this.value == null ? null : Reflect.field(this.value, key); @@ -662,11 +751,6 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR return this.value == null ? null : cast Reflect.field(this.value, key); } - public function clone():SongEventData - { - return new SongEventData(this.time, this.event, this.value); - } - @:op(A == B) public function op_equals(other:SongEventData):Bool { @@ -712,7 +796,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR } } -class SongNoteDataRaw +class SongNoteDataRaw implements ICloneable { /** * The timestamp of the note. The timestamp is in the format of the song's time format. @@ -796,7 +880,7 @@ class SongNoteDataRaw { if (_stepTime != null && !force) return _stepTime; - return _stepTime = Conductor.getTimeInSteps(this.time); + return _stepTime = Conductor.instance.getTimeInSteps(this.time); } @:jignored @@ -812,7 +896,7 @@ class SongNoteDataRaw if (_stepLength != null && !force) return _stepLength; - return _stepLength = Conductor.getTimeInSteps(this.time + this.length) - getStepTime(); + return _stepLength = Conductor.instance.getTimeInSteps(this.time + this.length) - getStepTime(); } public function setStepLength(value:Float):Void @@ -823,11 +907,16 @@ class SongNoteDataRaw } else { - var lengthMs:Float = Conductor.getStepTimeInMs(value) - this.time; + var lengthMs:Float = Conductor.instance.getStepTimeInMs(value) - this.time; this.length = lengthMs; } _stepLength = null; } + + public function clone():SongNoteDataRaw + { + return new SongNoteDataRaw(this.time, this.data, this.length, this.kind); + } } /** diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 7716f0f02..b7ef07be5 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -8,7 +8,7 @@ import funkin.play.stage.StageData; import polymod.Polymod; import polymod.backends.PolymodAssets.PolymodAssetType; import polymod.format.ParseRules.TextFileFormat; -import funkin.data.event.SongEventData.SongEventParser; +import funkin.data.event.SongEventRegistry; import funkin.util.FileUtil; import funkin.data.level.LevelRegistry; import funkin.data.notestyle.NoteStyleRegistry; @@ -271,7 +271,7 @@ class PolymodHandler SongRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries(); NoteStyleRegistry.instance.loadEntries(); - SongEventParser.loadEventCache(); + SongEventRegistry.loadEventCache(); ConversationDataParser.loadConversationCache(); DialogueBoxDataParser.loadDialogueBoxCache(); SpeakerDataParser.loadSpeakerCache(); diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx index d23574ce2..5b7ce9fc2 100644 --- a/source/funkin/play/Countdown.hx +++ b/source/funkin/play/Countdown.hx @@ -40,7 +40,7 @@ class Countdown stopCountdown(); PlayState.instance.isInCountdown = true; - Conductor.update(PlayState.instance.startTimestamp + Conductor.beatLengthMs * -5); + Conductor.instance.update(PlayState.instance.startTimestamp + Conductor.instance.beatLengthMs * -5); // Handle onBeatHit events manually // @:privateAccess // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0)); @@ -48,7 +48,7 @@ class Countdown // The timer function gets called based on the beat of the song. countdownTimer = new FlxTimer(); - countdownTimer.start(Conductor.beatLengthMs / 1000, function(tmr:FlxTimer) { + countdownTimer.start(Conductor.instance.beatLengthMs / 1000, function(tmr:FlxTimer) { if (PlayState.instance == null) { tmr.cancel(); @@ -158,7 +158,7 @@ class Countdown { stopCountdown(); // This will trigger PlayState.startSong() - Conductor.update(0); + Conductor.instance.update(0); // PlayState.isInCountdown = false; } @@ -225,7 +225,7 @@ class Countdown countdownSprite.screenCenter(); // Fade sprite in, then out, then destroy it. - FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100, alpha: 0}, Conductor.beatLengthMs / 1000, + FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100, alpha: 0}, Conductor.instance.beatLengthMs / 1000, { ease: FlxEase.cubeInOut, onComplete: function(twn:FlxTween) { diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index dadd5a3d9..137bf3905 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -129,7 +129,7 @@ class GameOverSubState extends MusicBeatSubState gameOverMusic.stop(); // The conductor now represents the BPM of the game over music. - Conductor.update(0); + Conductor.instance.update(0); } var hasStartedAnimation:Bool = false; @@ -204,7 +204,7 @@ class GameOverSubState extends MusicBeatSubState { // Match the conductor to the music. // This enables the stepHit and beatHit events. - Conductor.update(gameOverMusic.time); + Conductor.instance.update(gameOverMusic.time); } else { diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 3dcabf953..995797dd1 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -42,7 +42,7 @@ import funkin.play.cutscene.dialogue.Conversation; import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.play.cutscene.VanillaCutscenes; import funkin.play.cutscene.VideoCutscene; -import funkin.data.event.SongEventData.SongEventParser; +import funkin.data.event.SongEventRegistry; import funkin.play.notes.NoteSprite; import funkin.play.notes.NoteDirection; import funkin.play.notes.Strumline; @@ -561,15 +561,15 @@ class PlayState extends MusicBeatSubState } // Prepare the Conductor. - Conductor.forceBPM(null); + Conductor.instance.forceBPM(null); if (currentChart.offsets != null) { - Conductor.instrumentalOffset = currentChart.offsets.getInstrumentalOffset(); + Conductor.instance.instrumentalOffset = currentChart.offsets.getInstrumentalOffset(); } - Conductor.mapTimeChanges(currentChart.timeChanges); - Conductor.update((Conductor.beatLengthMs * -5) + startTimestamp); + Conductor.instance.mapTimeChanges(currentChart.timeChanges); + Conductor.instance.update((Conductor.instance.beatLengthMs * -5) + startTimestamp); // The song is now loaded. We can continue to initialize the play state. initCameras(); @@ -734,7 +734,7 @@ class PlayState extends MusicBeatSubState // Reset music properly. - FlxG.sound.music.time = Math.max(0, startTimestamp - Conductor.instrumentalOffset); + FlxG.sound.music.time = Math.max(0, startTimestamp - Conductor.instance.instrumentalOffset); FlxG.sound.music.pause(); if (!overrideMusic) @@ -785,22 +785,22 @@ class PlayState extends MusicBeatSubState { if (isInCountdown) { - Conductor.update(Conductor.songPosition + elapsed * 1000); - if (Conductor.songPosition >= (startTimestamp)) startSong(); + Conductor.instance.update(Conductor.instance.songPosition + elapsed * 1000); + if (Conductor.instance.songPosition >= (startTimestamp)) startSong(); } } else { if (Constants.EXT_SOUND == 'mp3') { - Conductor.formatOffset = Constants.MP3_DELAY_MS; + Conductor.instance.formatOffset = Constants.MP3_DELAY_MS; } else { - Conductor.formatOffset = 0.0; + Conductor.instance.formatOffset = 0.0; } - Conductor.update(); // Normal conductor update. + Conductor.instance.update(); // Normal conductor update. } var androidPause:Bool = false; @@ -942,7 +942,7 @@ class PlayState extends MusicBeatSubState // TODO: Check that these work even when songPosition is less than 0. if (songEvents != null && songEvents.length > 0) { - var songEventsToActivate:Array = SongEventParser.queryEvents(songEvents, Conductor.songPosition); + var songEventsToActivate:Array = SongEventRegistry.queryEvents(songEvents, Conductor.instance.songPosition); if (songEventsToActivate.length > 0) { @@ -950,7 +950,7 @@ class PlayState extends MusicBeatSubState for (event in songEventsToActivate) { // If an event is trying to play, but it's over 5 seconds old, skip it. - if (event.time - Conductor.songPosition < -5000) + if (event.time - Conductor.instance.songPosition < -5000) { event.activated = true; continue; @@ -961,7 +961,7 @@ class PlayState extends MusicBeatSubState // Calling event.cancelEvent() skips the event. Neat! if (!eventEvent.eventCanceled) { - SongEventParser.handleEvent(event); + SongEventRegistry.handleEvent(event); } } } @@ -1052,7 +1052,7 @@ class PlayState extends MusicBeatSubState if (startTimer.finished) { DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true, - currentSongLengthMs - Conductor.songPosition); + currentSongLengthMs - Conductor.instance.songPosition); } else { @@ -1076,12 +1076,12 @@ class PlayState extends MusicBeatSubState { if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause) { - if (Conductor.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song + if (Conductor.instance.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC, true, currentSongLengthMs - - Conductor.songPosition); + - Conductor.instance.songPosition); else DiscordClient.changePresence(detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC); } @@ -1154,17 +1154,17 @@ class PlayState extends MusicBeatSubState if (!startingSong && FlxG.sound.music != null - && (Math.abs(FlxG.sound.music.time - (Conductor.songPosition + Conductor.instrumentalOffset)) > 200 - || Math.abs(vocals.checkSyncError(Conductor.songPosition + Conductor.instrumentalOffset)) > 200)) + && (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200 + || Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200)) { trace("VOCALS NEED RESYNC"); - if (vocals != null) trace(vocals.checkSyncError(Conductor.songPosition + Conductor.instrumentalOffset)); - trace(FlxG.sound.music.time - (Conductor.songPosition + Conductor.instrumentalOffset)); + if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)); + trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)); resyncVocals(); } - if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.currentStep)); - if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.currentStep)); + if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.instance.currentStep)); + if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.instance.currentStep)); return true; } @@ -1185,14 +1185,14 @@ class PlayState extends MusicBeatSubState } // Only zoom camera if we are zoomed by less than 35%. - if (FlxG.camera.zoom < (1.35 * defaultCameraZoom) && cameraZoomRate > 0 && Conductor.currentBeat % cameraZoomRate == 0) + if (FlxG.camera.zoom < (1.35 * defaultCameraZoom) && cameraZoomRate > 0 && Conductor.instance.currentBeat % cameraZoomRate == 0) { // Zoom camera in (1.5%) FlxG.camera.zoom += cameraZoomIntensity * defaultCameraZoom; // Hud zooms double (3%) camHUD.zoom += hudCameraZoomIntensity * defaultHUDCameraZoom; } - // trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.currentBeat} % ${cameraZoomRate} == ${Conductor.currentBeat % cameraZoomRate}}'); + // trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.instance.currentBeat} % ${cameraZoomRate} == ${Conductor.instance.currentBeat % cameraZoomRate}}'); // That combo milestones that got spoiled that one time. // Comes with NEAT visual and audio effects. @@ -1205,13 +1205,13 @@ class PlayState extends MusicBeatSubState // TODO: Re-enable combo text (how to do this without sections?). // if (currentSong != null) // { - // shouldShowComboText = (Conductor.currentBeat % 8 == 7); - // var daSection = .getSong()[Std.int(Conductor.currentBeat / 16)]; + // shouldShowComboText = (Conductor.instance.currentBeat % 8 == 7); + // var daSection = .getSong()[Std.int(Conductor.instance.currentBeat / 16)]; // shouldShowComboText = shouldShowComboText && (daSection != null && daSection.mustHitSection); // shouldShowComboText = shouldShowComboText && (Highscore.tallies.combo > 5); // - // var daNextSection = .getSong()[Std.int(Conductor.currentBeat / 16) + 1]; - // var isEndOfSong = .getSong().length < Std.int(Conductor.currentBeat / 16); + // var daNextSection = .getSong()[Std.int(Conductor.instance.currentBeat / 16) + 1]; + // var isEndOfSong = .getSong().length < Std.int(Conductor.instance.currentBeat / 16); // shouldShowComboText = shouldShowComboText && (isEndOfSong || (daNextSection != null && !daNextSection.mustHitSection)); // } @@ -1224,7 +1224,7 @@ class PlayState extends MusicBeatSubState var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation - new FlxTimer().start(((Conductor.beatLengthMs / 1000) * 1.25) - frameShit, function(tmr) { + new FlxTimer().start(((Conductor.instance.beatLengthMs / 1000) * 1.25) - frameShit, function(tmr) { animShit.forceFinish(); }); } @@ -1261,10 +1261,10 @@ class PlayState extends MusicBeatSubState if (currentStage == null) return; // TODO: Add HEY! song events to Tutorial. - if (Conductor.currentBeat % 16 == 15 + if (Conductor.instance.currentBeat % 16 == 15 && currentStage.getDad().characterId == 'gf' - && Conductor.currentBeat > 16 - && Conductor.currentBeat < 48) + && Conductor.instance.currentBeat > 16 + && Conductor.instance.currentBeat < 48) { currentStage.getBoyfriend().playAnimation('hey', true); currentStage.getDad().playAnimation('cheer', true); @@ -1575,7 +1575,7 @@ class PlayState extends MusicBeatSubState trace('Song difficulty could not be loaded.'); } - // Conductor.forceBPM(currentChart.getStartingBPM()); + // Conductor.instance.forceBPM(currentChart.getStartingBPM()); if (!overrideMusic) { @@ -1607,7 +1607,7 @@ class PlayState extends MusicBeatSubState // Reset song events. songEvents = currentChart.getEvents(); - SongEventParser.resetEvents(songEvents); + SongEventRegistry.resetEvents(songEvents); // Reset the notes on each strumline. var playerNoteData:Array = []; @@ -1706,7 +1706,7 @@ class PlayState extends MusicBeatSubState FlxG.sound.music.onComplete = endSong; // A negative instrumental offset means the song skips the first few milliseconds of the track. // This just gets added into the startTimestamp behavior so we don't need to do anything extra. - FlxG.sound.music.time = startTimestamp - Conductor.instrumentalOffset; + FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset; trace('Playing vocals...'); add(vocals); @@ -1722,7 +1722,7 @@ class PlayState extends MusicBeatSubState if (startTimestamp > 0) { - // FlxG.sound.music.time = startTimestamp - Conductor.instrumentalOffset; + // FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset; handleSkippedNotes(); } } @@ -1800,7 +1800,7 @@ class PlayState extends MusicBeatSubState var hitWindowCenter = note.strumTime; var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS; - if (Conductor.songPosition > hitWindowEnd) + if (Conductor.instance.songPosition > hitWindowEnd) { if (note.hasMissed) continue; @@ -1810,7 +1810,7 @@ class PlayState extends MusicBeatSubState if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true; } - else if (Conductor.songPosition > hitWindowCenter) + else if (Conductor.instance.songPosition > hitWindowCenter) { if (note.hasBeenHit) continue; @@ -1831,7 +1831,7 @@ class PlayState extends MusicBeatSubState opponentStrumline.playNoteHoldCover(note.holdNoteSprite); } } - else if (Conductor.songPosition > hitWindowStart) + else if (Conductor.instance.songPosition > hitWindowStart) { if (note.hasBeenHit || note.hasMissed) continue; @@ -1877,14 +1877,14 @@ class PlayState extends MusicBeatSubState var hitWindowCenter = note.strumTime; var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS; - if (Conductor.songPosition > hitWindowEnd) + if (Conductor.instance.songPosition > hitWindowEnd) { note.tooEarly = false; note.mayHit = false; note.hasMissed = true; if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true; } - else if (Conductor.songPosition > hitWindowStart) + else if (Conductor.instance.songPosition > hitWindowStart) { note.tooEarly = false; note.mayHit = true; @@ -1951,7 +1951,7 @@ class PlayState extends MusicBeatSubState if (note == null || note.hasBeenHit) continue; var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS; - if (Conductor.songPosition > hitWindowEnd) + if (Conductor.instance.songPosition > hitWindowEnd) { // We have passed this note. // Flag the note for deletion without actually penalizing the player. @@ -2115,7 +2115,7 @@ class PlayState extends MusicBeatSubState { inputSpitter.push( { - t: Std.int(Conductor.songPosition), + t: Std.int(Conductor.instance.songPosition), d: indices[i], l: 20 }); @@ -2125,7 +2125,7 @@ class PlayState extends MusicBeatSubState { inputSpitter.push( { - t: Std.int(Conductor.songPosition), + t: Std.int(Conductor.instance.songPosition), d: -1, l: 20 }); @@ -2186,7 +2186,7 @@ class PlayState extends MusicBeatSubState { inputSpitter.push( { - t: Std.int(Conductor.songPosition), + t: Std.int(Conductor.instance.songPosition), d: indices[i], l: 20 }); @@ -2275,7 +2275,7 @@ class PlayState extends MusicBeatSubState // Get the offset and compensate for input latency. // Round inward (trim remainder) for consistency. - var noteDiff:Int = Std.int(Conductor.songPosition - daNote.noteData.time - inputLatencyMs); + var noteDiff:Int = Std.int(Conductor.instance.songPosition - daNote.noteData.time - inputLatencyMs); var score = Scoring.scoreNote(noteDiff, PBOT1); var daRating = Scoring.judgeNote(noteDiff, PBOT1); @@ -2330,7 +2330,7 @@ class PlayState extends MusicBeatSubState { inputSpitter.push( { - t: Std.int(Conductor.songPosition), + t: Std.int(Conductor.instance.songPosition), d: indices[i], l: 20 }); @@ -2340,7 +2340,7 @@ class PlayState extends MusicBeatSubState { inputSpitter.push( { - t: Std.int(Conductor.songPosition), + t: Std.int(Conductor.instance.songPosition), d: -1, l: 20 }); @@ -2739,15 +2739,15 @@ class PlayState extends MusicBeatSubState { FlxG.sound.music.pause(); - var targetTimeSteps:Float = Conductor.currentStepTime + (Conductor.timeSignatureNumerator * Constants.STEPS_PER_BEAT * sections); - var targetTimeMs:Float = Conductor.getStepTimeInMs(targetTimeSteps); + var targetTimeSteps:Float = Conductor.instance.currentStepTime + (Conductor.instance.timeSignatureNumerator * Constants.STEPS_PER_BEAT * sections); + var targetTimeMs:Float = Conductor.instance.getStepTimeInMs(targetTimeSteps); FlxG.sound.music.time = targetTimeMs; handleSkippedNotes(); // regenNoteData(FlxG.sound.music.time); - Conductor.update(FlxG.sound.music.time); + Conductor.instance.update(FlxG.sound.music.time); resyncVocals(); } diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index 7ad0892f6..390864148 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -367,7 +367,7 @@ class BaseCharacter extends Bopper // This lets you add frames to the end of the sing animation to ease back into the idle! holdTimer += event.elapsed; - var singTimeSec:Float = singTimeSec * (Conductor.beatLengthMs * 0.001); // x beats, to ms. + var singTimeSec:Float = singTimeSec * (Conductor.instance.beatLengthMs * 0.001); // x beats, to ms. if (getCurrentAnimation().endsWith('miss')) singTimeSec *= 2; // makes it feel more awkward when you miss diff --git a/source/funkin/play/components/ComboMilestone.hx b/source/funkin/play/components/ComboMilestone.hx index 54d1438f1..4119e45c2 100644 --- a/source/funkin/play/components/ComboMilestone.hx +++ b/source/funkin/play/components/ComboMilestone.hx @@ -40,7 +40,7 @@ class ComboMilestone extends FlxTypedSpriteGroup { if (onScreenTime < 0.9) { - new FlxTimer().start((Conductor.beatLengthMs / 1000) * 0.25, function(tmr) { + new FlxTimer().start((Conductor.instance.beatLengthMs / 1000) * 0.25, function(tmr) { forceFinish(); }); } diff --git a/source/funkin/play/components/PopUpStuff.hx b/source/funkin/play/components/PopUpStuff.hx index 38a6ec15a..9553856a9 100644 --- a/source/funkin/play/components/PopUpStuff.hx +++ b/source/funkin/play/components/PopUpStuff.hx @@ -59,7 +59,7 @@ class PopUpStuff extends FlxTypedGroup remove(rating, true); rating.destroy(); }, - startDelay: Conductor.beatLengthMs * 0.001 + startDelay: Conductor.instance.beatLengthMs * 0.001 }); } @@ -110,7 +110,7 @@ class PopUpStuff extends FlxTypedGroup remove(comboSpr, true); comboSpr.destroy(); }, - startDelay: Conductor.beatLengthMs * 0.001 + startDelay: Conductor.instance.beatLengthMs * 0.001 }); var seperatedScore:Array = []; @@ -157,7 +157,7 @@ class PopUpStuff extends FlxTypedGroup remove(numScore, true); numScore.destroy(); }, - startDelay: Conductor.beatLengthMs * 0.002 + startDelay: Conductor.instance.beatLengthMs * 0.002 }); daLoop++; diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx index 5f63254b0..83c978ba8 100644 --- a/source/funkin/play/event/FocusCameraSongEvent.hx +++ b/source/funkin/play/event/FocusCameraSongEvent.hx @@ -5,8 +5,8 @@ import funkin.data.song.SongData; import funkin.data.song.SongData.SongEventData; // Data from the event schema import funkin.play.event.SongEvent; -import funkin.data.event.SongEventData.SongEventSchema; -import funkin.data.event.SongEventData.SongEventFieldType; +import funkin.data.event.SongEventSchema; +import funkin.data.event.SongEventSchema.SongEventFieldType; /** * This class represents a handler for a type of song event. @@ -132,7 +132,7 @@ class FocusCameraSongEvent extends SongEvent */ public override function getEventSchema():SongEventSchema { - return [ + return new SongEventSchema([ { name: "char", title: "Character", @@ -154,6 +154,6 @@ class FocusCameraSongEvent extends SongEvent step: 10.0, type: SongEventFieldType.FLOAT, } - ]; + ]); } } diff --git a/source/funkin/play/event/PlayAnimationSongEvent.hx b/source/funkin/play/event/PlayAnimationSongEvent.hx index 6bc625517..4e6669479 100644 --- a/source/funkin/play/event/PlayAnimationSongEvent.hx +++ b/source/funkin/play/event/PlayAnimationSongEvent.hx @@ -7,8 +7,8 @@ import funkin.data.song.SongData; import funkin.data.song.SongData.SongEventData; // Data from the event schema import funkin.play.event.SongEvent; -import funkin.data.event.SongEventData.SongEventSchema; -import funkin.data.event.SongEventData.SongEventFieldType; +import funkin.data.event.SongEventSchema; +import funkin.data.event.SongEventSchema.SongEventFieldType; class PlayAnimationSongEvent extends SongEvent { @@ -89,7 +89,7 @@ class PlayAnimationSongEvent extends SongEvent */ public override function getEventSchema():SongEventSchema { - return [ + return new SongEventSchema([ { name: 'target', title: 'Target', @@ -108,6 +108,6 @@ class PlayAnimationSongEvent extends SongEvent type: SongEventFieldType.BOOL, defaultValue: false } - ]; + ]); } } diff --git a/source/funkin/play/event/SetCameraBopSongEvent.hx b/source/funkin/play/event/SetCameraBopSongEvent.hx index 3cdeb9a67..d0e01346f 100644 --- a/source/funkin/play/event/SetCameraBopSongEvent.hx +++ b/source/funkin/play/event/SetCameraBopSongEvent.hx @@ -8,8 +8,8 @@ import funkin.data.song.SongData; import funkin.data.song.SongData.SongEventData; // Data from the event schema import funkin.play.event.SongEvent; -import funkin.data.event.SongEventData.SongEventSchema; -import funkin.data.event.SongEventData.SongEventFieldType; +import funkin.data.event.SongEventSchema; +import funkin.data.event.SongEventSchema.SongEventFieldType; /** * This class represents a handler for configuring camera bop intensity and rate. @@ -72,7 +72,7 @@ class SetCameraBopSongEvent extends SongEvent */ public override function getEventSchema():SongEventSchema { - return [ + return new SongEventSchema([ { name: 'intensity', title: 'Intensity', @@ -87,6 +87,6 @@ class SetCameraBopSongEvent extends SongEvent step: 1, type: SongEventFieldType.INTEGER, } - ]; + ]); } } diff --git a/source/funkin/play/event/SongEvent.hx b/source/funkin/play/event/SongEvent.hx index 36a886673..29b394c0e 100644 --- a/source/funkin/play/event/SongEvent.hx +++ b/source/funkin/play/event/SongEvent.hx @@ -1,7 +1,7 @@ package funkin.play.event; import funkin.data.song.SongData.SongEventData; -import funkin.data.event.SongEventData.SongEventSchema; +import funkin.data.event.SongEventSchema; /** * This class represents a handler for a type of song event. diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx index 1ae76039e..a35a12e1e 100644 --- a/source/funkin/play/event/ZoomCameraSongEvent.hx +++ b/source/funkin/play/event/ZoomCameraSongEvent.hx @@ -8,8 +8,8 @@ import funkin.data.song.SongData; import funkin.data.song.SongData.SongEventData; // Data from the event schema import funkin.play.event.SongEvent; -import funkin.data.event.SongEventData.SongEventFieldType; -import funkin.data.event.SongEventData.SongEventSchema; +import funkin.data.event.SongEventSchema; +import funkin.data.event.SongEventSchema.SongEventFieldType; /** * This class represents a handler for camera zoom events. @@ -79,7 +79,8 @@ class ZoomCameraSongEvent extends SongEvent return; } - FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.stepLengthMs * duration / 1000), {ease: easeFunction}); + FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.instance.stepLengthMs * duration / 1000), + {ease: easeFunction}); } } @@ -99,7 +100,7 @@ class ZoomCameraSongEvent extends SongEvent */ public override function getEventSchema():SongEventSchema { - return [ + return new SongEventSchema([ { name: 'zoom', title: 'Zoom Level', @@ -145,6 +146,6 @@ class ZoomCameraSongEvent extends SongEvent 'Elastic In/Out' => 'elasticInOut', ] } - ]; + ]); } } diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 948e9fa5b..b312494cf 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -279,7 +279,7 @@ class Strumline extends FlxSpriteGroup var vwoosh:Float = 1.0; var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0; - return Constants.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1); + return Constants.PIXELS_PER_MS * (Conductor.instance.songPosition - strumTime) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1); } function updateNotes():Void @@ -287,8 +287,8 @@ class Strumline extends FlxSpriteGroup if (noteData.length == 0) return; var songStart:Float = PlayState.instance?.startTimestamp ?? 0.0; - var hitWindowStart:Float = Conductor.songPosition - Constants.HIT_WINDOW_MS; - var renderWindowStart:Float = Conductor.songPosition + RENDER_DISTANCE_MS; + var hitWindowStart:Float = Conductor.instance.songPosition - Constants.HIT_WINDOW_MS; + var renderWindowStart:Float = Conductor.instance.songPosition + RENDER_DISTANCE_MS; for (noteIndex in nextNoteIndex...noteData.length) { @@ -335,7 +335,7 @@ class Strumline extends FlxSpriteGroup { if (holdNote == null || !holdNote.alive) continue; - if (Conductor.songPosition > holdNote.strumTime && holdNote.hitNote && !holdNote.missedNote) + if (Conductor.instance.songPosition > holdNote.strumTime && holdNote.hitNote && !holdNote.missedNote) { if (isPlayer && !isKeyHeld(holdNote.noteDirection)) { @@ -349,7 +349,7 @@ class Strumline extends FlxSpriteGroup var renderWindowEnd = holdNote.strumTime + holdNote.fullSustainLength + Constants.HIT_WINDOW_MS + RENDER_DISTANCE_MS / 8; - if (holdNote.missedNote && Conductor.songPosition >= renderWindowEnd) + if (holdNote.missedNote && Conductor.instance.songPosition >= renderWindowEnd) { // Hold note is offscreen, kill it. holdNote.visible = false; @@ -399,13 +399,13 @@ class Strumline extends FlxSpriteGroup holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) + yOffset + STRUMLINE_SIZE / 2; } } - else if (Conductor.songPosition > holdNote.strumTime && holdNote.hitNote) + else if (Conductor.instance.songPosition > holdNote.strumTime && holdNote.hitNote) { // Hold note is currently being hit, clip it off. holdConfirm(holdNote.noteDirection); holdNote.visible = true; - holdNote.sustainLength = (holdNote.strumTime + holdNote.fullSustainLength) - Conductor.songPosition; + holdNote.sustainLength = (holdNote.strumTime + holdNote.fullSustainLength) - Conductor.instance.songPosition; if (holdNote.sustainLength <= 10) { diff --git a/source/funkin/ui/MusicBeatState.hx b/source/funkin/ui/MusicBeatState.hx index 077e9e495..33333565f 100644 --- a/source/funkin/ui/MusicBeatState.hx +++ b/source/funkin/ui/MusicBeatState.hx @@ -80,25 +80,11 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler if (FlxG.keys.justPressed.F5) debug_refreshModules(); } - function handleQuickWatch():Void - { - // Display Conductor info in the watch window. - FlxG.watch.addQuick("songPosition", Conductor.songPosition); - FlxG.watch.addQuick("songPositionNoOffset", Conductor.songPosition + Conductor.instrumentalOffset); - FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0); - FlxG.watch.addQuick("bpm", Conductor.bpm); - FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime); - FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime); - FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime); - } - override function update(elapsed:Float) { super.update(elapsed); handleControls(); - handleFunctionControls(); - handleQuickWatch(); dispatchEvent(new UpdateScriptEvent(elapsed)); } @@ -139,7 +125,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler public function stepHit():Bool { - var event = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep); + var event = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep); dispatchEvent(event); @@ -150,7 +136,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler public function beatHit():Bool { - var event = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep); + var event = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep); dispatchEvent(event); diff --git a/source/funkin/ui/MusicBeatSubState.hx b/source/funkin/ui/MusicBeatSubState.hx index 9dd755b62..0fa55c234 100644 --- a/source/funkin/ui/MusicBeatSubState.hx +++ b/source/funkin/ui/MusicBeatSubState.hx @@ -65,12 +65,8 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl if (FlxG.keys.justPressed.F5) debug_refreshModules(); // Display Conductor info in the watch window. - FlxG.watch.addQuick("songPosition", Conductor.songPosition); FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0); - FlxG.watch.addQuick("bpm", Conductor.bpm); - FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime); - FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime); - FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime); + Conductor.watchQuick(); dispatchEvent(new UpdateScriptEvent(elapsed)); } @@ -99,7 +95,7 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl */ public function stepHit():Bool { - var event:ScriptEvent = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep); + var event:ScriptEvent = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep); dispatchEvent(event); @@ -115,7 +111,7 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl */ public function beatHit():Bool { - var event:ScriptEvent = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep); + var event:ScriptEvent = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep); dispatchEvent(event); diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 16f83275b..64a2f19ed 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -15,12 +15,14 @@ import flixel.group.FlxSpriteGroup; import flixel.input.keyboard.FlxKey; import flixel.math.FlxMath; import flixel.math.FlxPoint; +import flixel.graphics.FlxGraphic; import flixel.math.FlxRect; import flixel.sound.FlxSound; import flixel.system.FlxAssets.FlxSoundAsset; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.tweens.misc.VarTween; +import haxe.ui.Toolkit; import flixel.util.FlxColor; import flixel.util.FlxSort; import flixel.util.FlxTimer; @@ -81,6 +83,7 @@ import funkin.ui.debug.charting.components.ChartEditorEventSprite; import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite; import funkin.ui.debug.charting.components.ChartEditorNotePreview; import funkin.ui.debug.charting.components.ChartEditorNoteSprite; +import funkin.ui.debug.charting.components.ChartEditorMeasureTicks; import funkin.ui.debug.charting.components.ChartEditorPlaybarHead; import funkin.ui.debug.charting.components.ChartEditorSelectionSquareSprite; import funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler; @@ -150,7 +153,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Layouts public static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata'); - public static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata'); + public static final CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata'); public static final CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:String = Paths.ui('chart-editor/toolbox/playtest-properties'); public static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata'); public static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/toolbox/difficulty'); @@ -170,7 +173,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState /** * The width of the scroll area. */ - public static final PLAYHEAD_SCROLL_AREA_WIDTH:Int = 12; + public static final PLAYHEAD_SCROLL_AREA_WIDTH:Int = Std.int(GRID_SIZE); /** * The height of the playhead, in pixels. @@ -306,13 +309,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function get_songLengthInSteps():Float { - return Conductor.getTimeInSteps(songLengthInMs); + return Conductor.instance.getTimeInSteps(songLengthInMs); } function set_songLengthInSteps(value:Float):Float { - // Getting a reasonable result from setting songLengthInSteps requires that Conductor.mapBPMChanges be called first. - songLengthInMs = Conductor.getStepTimeInMs(value); + // Getting a reasonable result from setting songLengthInSteps requires that Conductor.instance.mapBPMChanges be called first. + songLengthInMs = Conductor.instance.getStepTimeInMs(value); return value; } @@ -366,17 +369,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState this.scrollPositionInPixels = value; // Move the grid sprite to the correct position. - if (gridTiledSprite != null && gridPlayheadScrollArea != null) + if (gridTiledSprite != null && measureTicks != null) { if (isViewDownscroll) { gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS); - gridPlayheadScrollArea.y = gridTiledSprite.y; + measureTicks.y = gridTiledSprite.y; } else { gridTiledSprite.y = -scrollPositionInPixels + (GRID_INITIAL_Y_POS); - gridPlayheadScrollArea.y = gridTiledSprite.y; + measureTicks.y = gridTiledSprite.y; if (audioVisGroup != null && audioVisGroup.playerVis != null) { @@ -398,6 +401,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff; // Update the note preview viewport box. setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); + // Update the measure tick display. + if (measureTicks != null) measureTicks.y = gridTiledSprite?.y ?? 0.0; return this.scrollPositionInPixels; } @@ -426,12 +431,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function get_scrollPositionInMs():Float { - return Conductor.getStepTimeInMs(scrollPositionInSteps); + return Conductor.instance.getStepTimeInMs(scrollPositionInSteps); } function set_scrollPositionInMs(value:Float):Float { - scrollPositionInSteps = Conductor.getTimeInSteps(value); + scrollPositionInSteps = Conductor.instance.getTimeInSteps(value); return value; } @@ -485,13 +490,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function get_playheadPositionInMs():Float { if (audioVisGroup != null && audioVisGroup.playerVis != null) - audioVisGroup.playerVis.realtimeStartOffset = -Conductor.getStepTimeInMs(playheadPositionInSteps); - return Conductor.getStepTimeInMs(playheadPositionInSteps); + audioVisGroup.playerVis.realtimeStartOffset = -Conductor.instance.getStepTimeInMs(playheadPositionInSteps); + return Conductor.instance.getStepTimeInMs(playheadPositionInSteps); } function set_playheadPositionInMs(value:Float):Float { - playheadPositionInSteps = Conductor.getTimeInSteps(value); + playheadPositionInSteps = Conductor.instance.getTimeInSteps(value); if (audioVisGroup != null && audioVisGroup.playerVis != null) audioVisGroup.playerVis.realtimeStartOffset = -value; return value; @@ -522,17 +527,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState /** * The note kind to use for notes being placed in the chart. Defaults to `''`. */ - var selectedNoteKind:String = ''; + var noteKindToPlace:String = ''; /** * The event type to use for events being placed in the chart. Defaults to `''`. */ - var selectedEventKind:String = 'FocusCamera'; + var eventKindToPlace:String = 'FocusCamera'; /** * The event data to use for events being placed in the chart. */ - var selectedEventData:DynamicAccess = {}; + var eventDataToPlace:DynamicAccess = {}; /** * The internal index of what note snapping value is in use. @@ -906,6 +911,70 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return Save.get().chartEditorHasBackup = value; } + /** + * A list of previous working file paths. + * Also known as the "recent files" list. + * The first element is [null] if the current working file has not been saved anywhere yet. + */ + public var previousWorkingFilePaths(default, set):Array> = [null]; + + function set_previousWorkingFilePaths(value:Array>):Array> + { + // Called only when the WHOLE LIST is overridden. + previousWorkingFilePaths = value; + applyWindowTitle(); + populateOpenRecentMenu(); + applyCanQuickSave(); + return value; + } + + /** + * The current file path which the chart editor is working with. + * If `null`, the current chart has not been saved yet. + */ + public var currentWorkingFilePath(get, set):Null; + + function get_currentWorkingFilePath():Null + { + return previousWorkingFilePaths[0]; + } + + function set_currentWorkingFilePath(value:Null):Null + { + if (value == previousWorkingFilePaths[0]) return value; + + if (previousWorkingFilePaths.contains(null)) + { + // Filter all instances of `null` from the array. + previousWorkingFilePaths = previousWorkingFilePaths.filter(function(x:Null):Bool { + return x != null; + }); + } + + if (previousWorkingFilePaths.contains(value)) + { + // Move the path to the front of the list. + previousWorkingFilePaths.remove(value); + previousWorkingFilePaths.unshift(value); + } + else + { + // Add the path to the front of the list. + previousWorkingFilePaths.unshift(value); + } + + while (previousWorkingFilePaths.length > Constants.MAX_PREVIOUS_WORKING_FILES) + { + // Remove the last path in the list. + previousWorkingFilePaths.pop(); + } + + populateOpenRecentMenu(); + applyWindowTitle(); + + return value; + } + /** * Whether the difficulty tree view in the toolbox has been modified and needs to be updated. * This happens when we add/remove difficulties. @@ -935,6 +1004,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var commandHistoryDirty:Bool = true; + /** + * If true, we are currently in the process of quitting the chart editor. + * Skip any update functions as most of them will call a crash. + */ + var criticalFailure:Bool = false; + // Input /** @@ -1680,23 +1755,28 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var notePreviewViewportBitmap:Null = null; + /**r + * The IMAGE used for the measure ticks. Updated by ChartEditorThemeHandler. + */ + var measureTickBitmap:Null = null; + /** * The tiled sprite used to display the grid. * The height is the length of the song, and scrolling is done by simply the sprite. */ var gridTiledSprite:Null = null; + /** + * The measure ticks area. Includes the numbers and the background sprite. + */ + var measureTicks:Null = null; + /** * The playhead representing the current position in the song. * Can move around on the grid independently of the view. */ var gridPlayhead:FlxSpriteGroup = new FlxSpriteGroup(); - /** - * The sprite for the scroll area under - */ - var gridPlayheadScrollArea:Null = null; - /** * A sprite used to indicate the note that will be placed on click. */ @@ -1783,70 +1863,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var params:Null; - /** - * A list of previous working file paths. - * Also known as the "recent files" list. - * The first element is [null] if the current working file has not been saved anywhere yet. - */ - public var previousWorkingFilePaths(default, set):Array> = [null]; - - function set_previousWorkingFilePaths(value:Array>):Array> - { - // Called only when the WHOLE LIST is overridden. - previousWorkingFilePaths = value; - applyWindowTitle(); - populateOpenRecentMenu(); - applyCanQuickSave(); - return value; - } - - /** - * The current file path which the chart editor is working with. - * If `null`, the current chart has not been saved yet. - */ - public var currentWorkingFilePath(get, set):Null; - - function get_currentWorkingFilePath():Null - { - return previousWorkingFilePaths[0]; - } - - function set_currentWorkingFilePath(value:Null):Null - { - if (value == previousWorkingFilePaths[0]) return value; - - if (previousWorkingFilePaths.contains(null)) - { - // Filter all instances of `null` from the array. - previousWorkingFilePaths = previousWorkingFilePaths.filter(function(x:Null):Bool { - return x != null; - }); - } - - if (previousWorkingFilePaths.contains(value)) - { - // Move the path to the front of the list. - previousWorkingFilePaths.remove(value); - previousWorkingFilePaths.unshift(value); - } - else - { - // Add the path to the front of the list. - previousWorkingFilePaths.unshift(value); - } - - while (previousWorkingFilePaths.length > Constants.MAX_PREVIOUS_WORKING_FILES) - { - // Remove the last path in the list. - previousWorkingFilePaths.pop(); - } - - populateOpenRecentMenu(); - applyWindowTitle(); - - return value; - } - public function new(?params:ChartEditorParams) { super(); @@ -1918,6 +1934,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState this.updateTheme(); buildGrid(); + buildMeasureTicks(); buildNotePreview(); buildSelectionBox(); @@ -1927,6 +1944,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Setup the onClick listeners for the UI after it's been created. setupUIListeners(); + setupContextMenu(); setupTurboKeyHandlers(); setupAutoSave(); @@ -2172,15 +2190,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState buildNoteGroup(); - gridPlayheadScrollArea = new FlxSprite(0, 0); - gridPlayheadScrollArea.makeGraphic(10, 10, PLAYHEAD_SCROLL_AREA_COLOR); // Make it 10x10px and then scale it as needed. - add(gridPlayheadScrollArea); - gridPlayheadScrollArea.setGraphicSize(PLAYHEAD_SCROLL_AREA_WIDTH, 3000); - gridPlayheadScrollArea.updateHitbox(); - gridPlayheadScrollArea.x = GRID_X_POS - PLAYHEAD_SCROLL_AREA_WIDTH; - gridPlayheadScrollArea.y = GRID_INITIAL_Y_POS; - gridPlayheadScrollArea.zIndex = 25; - // The playhead that show the current position in the song. add(gridPlayhead); gridPlayhead.zIndex = 30; @@ -2216,6 +2225,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState add(audioVisGroup); } + function buildMeasureTicks():Void + { + measureTicks = new ChartEditorMeasureTicks(this); + var measureTicksWidth = (GRID_SIZE); + measureTicks.x = gridTiledSprite.x - measureTicksWidth; + measureTicks.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; + measureTicks.zIndex = 20; + + add(measureTicks); + } + function buildNotePreview():Void { var playbarHeightWithPad = PLAYBAR_HEIGHT + 10; @@ -2301,6 +2321,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState bounds.height = MIN_HEIGHT; } + trace('Note preview viewport bounds: ' + bounds.toString()); + return bounds; } @@ -2541,13 +2563,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } else { - Conductor.currentTimeChange.bpm += 1; + Conductor.instance.currentTimeChange.bpm += 1; this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); } } playbarBPM.onRightClick = _ -> { - Conductor.currentTimeChange.bpm -= 1; + Conductor.instance.currentTimeChange.bpm -= 1; this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); } @@ -2590,14 +2612,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemUndo.onClick = _ -> undoLastCommand(); menubarItemRedo.onClick = _ -> redoLastCommand(); menubarItemCopy.onClick = function(_) { + copySelection(); }; menubarItemCut.onClick = _ -> performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection)); menubarItemPaste.onClick = _ -> { var targetMs:Float = scrollPositionInMs + playheadPositionInMs; - var targetStep:Float = Conductor.getTimeInSteps(targetMs); + var targetStep:Float = Conductor.instance.getTimeInSteps(targetMs); var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio; - var targetSnappedMs:Float = Conductor.getStepTimeInMs(targetSnappedStep); + var targetSnappedMs:Float = Conductor.instance.getStepTimeInMs(targetSnappedStep); performCommand(new PasteItemsCommand(targetSnappedMs)); }; @@ -2753,7 +2776,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemToggleToolboxDifficulty.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value); menubarItemToggleToolboxMetadata.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value); menubarItemToggleToolboxNotes.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value); - menubarItemToggleToolboxEvents.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT, event.value); + menubarItemToggleToolboxEventData.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT, event.value); menubarItemToggleToolboxPlaytestProperties.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT, event.value); menubarItemToggleToolboxPlayerPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value); menubarItemToggleToolboxOpponentPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value); @@ -2762,6 +2785,42 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // registerContextMenu(null, Paths.ui('chart-editor/context/test')); } + function setupContextMenu():Void + { + Screen.instance.registerEvent(MouseEvent.RIGHT_MOUSE_UP, function(e:MouseEvent) { + var xPos = e.screenX; + var yPos = e.screenY; + onContextMenu(xPos, yPos); + }); + } + + function onContextMenu(xPos:Float, yPos:Float) + { + trace('User right clicked to open menu at (${xPos}, ${yPos})'); + // this.openDefaultContextMenu(xPos, yPos); + } + + function copySelection():Void + { + // 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, timeOffset), + events: SongDataUtils.buildEventClipboard(currentEventSelection, timeOffset), + }); + } + /** * Initialize TurboKeyHandlers and add them to the state (so `update()` is called) * We can then probe `keyHandler.activated` to see if the key combo's action should be taken. @@ -2793,10 +2852,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState saveDataDirty = false; } + var displayAutosavePopup:Bool = false; + /** * UPDATE FUNCTIONS */ - function autoSave():Void + function autoSave(?beforePlaytest:Bool = false):Void { var needsAutoSave:Bool = saveDataDirty; @@ -2814,13 +2875,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (needsAutoSave) { this.exportAllSongData(true, null); - var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]); - this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [ - { - text: "Take Me There", - callback: openBackupsFolder, - } - ]); + if (beforePlaytest) + { + displayAutosavePopup = true; + } + else + { + displayAutosavePopup = false; + var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]); + this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [ + { + text: "Take Me There", + callback: openBackupsFolder, + } + ]); + } } #end } @@ -2888,7 +2957,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState public override function update(elapsed:Float):Void { // Override F4 behavior to include the autosave. - if (FlxG.keys.justPressed.F4) + if (FlxG.keys.justPressed.F4 && !criticalFailure) { quitChartEditor(); return; @@ -2897,6 +2966,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // dispatchEvent gets called here. super.update(elapsed); + if (criticalFailure) return; + // These ones happen even if the modal dialog is open. handleMusicPlayback(elapsed); handleNoteDisplay(); @@ -2936,9 +3007,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (metronomeVolume > 0.0 && this.subState == null && (audioInstTrack != null && audioInstTrack.isPlaying)) { - playMetronomeTick(Conductor.currentBeat % 4 == 0); + playMetronomeTick(Conductor.instance.currentBeat % Conductor.instance.beatsPerMeasure == 0); } + // Show the mouse cursor. + // Just throwing this somewhere convenient and infrequently called because sometimes Flixel's debug thing hides the cursor. + Cursor.show(); + return true; } @@ -2952,8 +3027,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (audioInstTrack != null && audioInstTrack.isPlaying) { - if (healthIconDad != null) healthIconDad.onStepHit(Conductor.currentStep); - if (healthIconBF != null) healthIconBF.onStepHit(Conductor.currentStep); + if (healthIconDad != null) healthIconDad.onStepHit(Conductor.instance.currentStep); + if (healthIconBF != null) healthIconBF.onStepHit(Conductor.instance.currentStep); } // Updating these every step keeps it more accurate. @@ -2981,12 +3056,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState audioInstTrack.update(elapsed); // If the song starts 50ms in, make sure we start the song there. - if (Conductor.instrumentalOffset < 0) + if (Conductor.instance.instrumentalOffset < 0) { - if (audioInstTrack.time < -Conductor.instrumentalOffset) + if (audioInstTrack.time < -Conductor.instance.instrumentalOffset) { - trace('Resetting instrumental time to ${- Conductor.instrumentalOffset}ms'); - audioInstTrack.time = -Conductor.instrumentalOffset; + trace('Resetting instrumental time to ${- Conductor.instance.instrumentalOffset}ms'); + audioInstTrack.time = -Conductor.instance.instrumentalOffset; } } } @@ -2997,16 +3072,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { // If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat! - var oldStepTime:Float = Conductor.currentStepTime; - var oldSongPosition:Float = Conductor.songPosition + Conductor.instrumentalOffset; - Conductor.update(audioInstTrack.time); - handleHitsounds(oldSongPosition, Conductor.songPosition + Conductor.instrumentalOffset); + var oldStepTime:Float = Conductor.instance.currentStepTime; + var oldSongPosition:Float = Conductor.instance.songPosition + Conductor.instance.instrumentalOffset; + Conductor.instance.update(audioInstTrack.time); + handleHitsounds(oldSongPosition, Conductor.instance.songPosition + Conductor.instance.instrumentalOffset); // Resync vocals. if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) { audioVocalTrackGroup.time = audioInstTrack.time; } - var diffStepTime:Float = Conductor.currentStepTime - oldStepTime; + var diffStepTime:Float = Conductor.instance.currentStepTime - oldStepTime; // Move the playhead. playheadPositionInPixels += diffStepTime * GRID_SIZE; @@ -3016,9 +3091,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState else { // Else, move the entire view. - var oldSongPosition:Float = Conductor.songPosition + Conductor.instrumentalOffset; - Conductor.update(audioInstTrack.time); - handleHitsounds(oldSongPosition, Conductor.songPosition + Conductor.instrumentalOffset); + var oldSongPosition:Float = Conductor.instance.songPosition + Conductor.instance.instrumentalOffset; + Conductor.instance.update(audioInstTrack.time); + handleHitsounds(oldSongPosition, Conductor.instance.songPosition + Conductor.instance.instrumentalOffset); // Resync vocals. if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) { @@ -3027,7 +3102,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // We need time in fractional steps here to allow the song to actually play. // Also account for a potentially offset playhead. - scrollPositionInPixels = (Conductor.currentStepTime + Conductor.instrumentalOffsetSteps) * GRID_SIZE - playheadPositionInPixels; + scrollPositionInPixels = (Conductor.instance.currentStepTime + Conductor.instance.instrumentalOffsetSteps) * GRID_SIZE - playheadPositionInPixels; // DO NOT move song to scroll position here specifically. @@ -3142,6 +3217,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Update the event sprite's position. eventSprite.updateEventPosition(renderedEvents); + // Update the sprite's graphic. TODO: Is this inefficient? + eventSprite.playAnimation(eventSprite.eventData.event); } else { @@ -3156,8 +3233,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Let's try testing only notes within a certain range of the view area. // TODO: I don't think this messes up really long sustains, does it? - var viewAreaTopMs:Float = scrollPositionInMs - (Conductor.measureLengthMs * 2); // Is 2 measures enough? - var viewAreaBottomMs:Float = scrollPositionInMs + (Conductor.measureLengthMs * 2); // Is 2 measures enough? + var viewAreaTopMs:Float = scrollPositionInMs - (Conductor.instance.measureLengthMs * 2); // Is 2 measures enough? + var viewAreaBottomMs:Float = scrollPositionInMs + (Conductor.instance.measureLengthMs * 2); // Is 2 measures enough? // Add notes that are now visible. for (noteData in currentSongChartNoteData) @@ -3444,14 +3521,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // PAGE UP = Jump up to nearest measure if (pageUpKeyHandler.activated) { - var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure; + var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var targetScrollPosition:Float = Math.floor(playheadPos / measureHeight) * measureHeight; // If we would move less than one grid, instead move to the top of the previous measure. var targetScrollAmount = Math.abs(targetScrollPosition - playheadPos); if (targetScrollAmount < GRID_SIZE) { - targetScrollPosition -= GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.beatsPerMeasure; + targetScrollPosition -= GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure; } scrollAmount = targetScrollPosition - playheadPos; @@ -3460,21 +3537,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (playbarButtonPressed == 'playbarBack') { playbarButtonPressed = ''; - scrollAmount = -GRID_SIZE * 4 * Conductor.beatsPerMeasure; + scrollAmount = -GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; shouldPause = true; } // PAGE DOWN = Jump down to nearest measure if (pageDownKeyHandler.activated) { - var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure; + var measureHeight:Float = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var targetScrollPosition:Float = Math.ceil(playheadPos / measureHeight) * measureHeight; // If we would move less than one grid, instead move to the top of the next measure. var targetScrollAmount = Math.abs(targetScrollPosition - playheadPos); if (targetScrollAmount < GRID_SIZE) { - targetScrollPosition += GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.beatsPerMeasure; + targetScrollPosition += GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.instance.beatsPerMeasure; } scrollAmount = targetScrollPosition - playheadPos; @@ -3483,7 +3560,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (playbarButtonPressed == 'playbarForward') { playbarButtonPressed = ''; - scrollAmount = GRID_SIZE * 4 * Conductor.beatsPerMeasure; + scrollAmount = GRID_SIZE * 4 * Conductor.instance.beatsPerMeasure; shouldPause = true; } @@ -3598,6 +3675,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // trace('shouldHandleCursor: $shouldHandleCursor'); + // TODO: TBH some of this should be using FlxMouseEventManager... + if (shouldHandleCursor) { // Over the course of this big conditional block, @@ -3641,7 +3720,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { scrollAnchorScreenPos = null; } - else if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea) && !isCursorOverHaxeUI) + else if (measureTicks != null && FlxG.mouse.overlaps(measureTicks) && !isCursorOverHaxeUI) { gridPlayheadScrollAreaPressed = true; } @@ -3686,10 +3765,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // The song position of the cursor, in steps. var cursorFractionalStep:Float = cursorY / GRID_SIZE; - var cursorMs:Float = Conductor.getStepTimeInMs(cursorFractionalStep); + var cursorMs:Float = Conductor.instance.getStepTimeInMs(cursorFractionalStep); // Round the cursor step to the nearest snap quant. var cursorSnappedStep:Float = Math.floor(cursorFractionalStep / noteSnapRatio) * noteSnapRatio; - var cursorSnappedMs:Float = Conductor.getStepTimeInMs(cursorSnappedStep); + var cursorSnappedMs:Float = Conductor.instance.getStepTimeInMs(cursorSnappedStep); // The direction value for the column at the cursor. var cursorGridPos:Int = Math.floor(cursorX / GRID_SIZE); @@ -3711,7 +3790,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // We released the mouse. Select the notes in the box. var cursorFractionalStepStart:Float = cursorYStart / GRID_SIZE; var cursorStepStart:Int = Math.floor(cursorFractionalStepStart); - var cursorMsStart:Float = Conductor.getStepTimeInMs(cursorStepStart); + var cursorMsStart:Float = Conductor.instance.getStepTimeInMs(cursorStepStart); var cursorColumnBase:Int = Math.floor(cursorX / GRID_SIZE); var cursorColumnBaseStart:Int = Math.floor(cursorXStart / GRID_SIZE); @@ -3947,11 +4026,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var dragDistanceMs:Float = 0; if (dragTargetNote != null && dragTargetNote.noteData != null) { - dragDistanceMs = Conductor.getStepTimeInMs(dragTargetNote.noteData.getStepTime() + dragDistanceSteps) - dragTargetNote.noteData.time; + dragDistanceMs = Conductor.instance.getStepTimeInMs(dragTargetNote.noteData.getStepTime() + dragDistanceSteps) - dragTargetNote.noteData.time; } else if (dragTargetEvent != null && dragTargetEvent.eventData != null) { - dragDistanceMs = Conductor.getStepTimeInMs(dragTargetEvent.eventData.getStepTime() + dragDistanceSteps) - dragTargetEvent.eventData.time; + dragDistanceMs = Conductor.instance.getStepTimeInMs(dragTargetEvent.eventData.getStepTime() + dragDistanceSteps) - dragTargetEvent.eventData.time; } var dragDistanceColumns:Int = dragTargetCurrentColumn; @@ -4011,7 +4090,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { stepTime = dragTargetEvent.eventData.getStepTime(); } - var dragDistanceSteps:Float = Conductor.getTimeInSteps(cursorSnappedMs).clamp(0, songLengthInSteps - (1 * noteSnapRatio)) - stepTime; + var dragDistanceSteps:Float = Conductor.instance.getTimeInSteps(cursorSnappedMs).clamp(0, songLengthInSteps - (1 * noteSnapRatio)) - stepTime; var data:Int = 0; var noteGridPos:Int = 0; if (dragTargetNote != null && dragTargetNote.noteData != null) @@ -4043,8 +4122,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Handle extending the note as you drag. var stepTime:Float = inline currentPlaceNoteData.getStepTime(); - var dragLengthSteps:Float = Conductor.getTimeInSteps(cursorSnappedMs) - stepTime; - var dragLengthMs:Float = dragLengthSteps * Conductor.stepLengthMs; + var dragLengthSteps:Float = Conductor.instance.getTimeInSteps(cursorSnappedMs) - stepTime; + var dragLengthMs:Float = dragLengthSteps * Conductor.instance.stepLengthMs; var dragLengthPixels:Float = dragLengthSteps * GRID_SIZE; if (gridGhostNote != null && gridGhostNote.noteData != null && gridGhostHoldNote != null) @@ -4181,14 +4260,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { // Create an event and place it in the chart. // TODO: Figure out configuring event data. - var newEventData:SongEventData = new SongEventData(cursorSnappedMs, selectedEventKind, selectedEventData.clone()); + var newEventData:SongEventData = new SongEventData(cursorSnappedMs, eventKindToPlace, eventDataToPlace.clone()); performCommand(new AddEventsCommand([newEventData], FlxG.keys.pressed.CONTROL)); } else { // Create a note and place it in the chart. - var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, selectedNoteKind.clone()); + var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace.clone()); performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); @@ -4226,13 +4305,52 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (highlightedNote != null && highlightedNote.noteData != null) { // TODO: Handle the case of clicking on a sustain piece. - // Remove the note. - performCommand(new RemoveNotesCommand([highlightedNote.noteData])); + if (FlxG.keys.pressed.SHIFT) + { + // Shift + Right click opens the context menu. + // If we are clicking a large selection, open the Selection context menu, otherwise open the Note context menu. + var isHighlightedNoteSelected:Bool = isNoteSelected(highlightedNote.noteData); + var useSingleNoteContextMenu:Bool = (!isHighlightedNoteSelected) + || (isHighlightedNoteSelected && currentNoteSelection.length == 1); + // Show the context menu connected to the note. + if (useSingleNoteContextMenu) + { + this.openNoteContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY, highlightedNote.noteData); + } + else + { + this.openSelectionContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY); + } + } + else + { + // Right click removes the note. + performCommand(new RemoveNotesCommand([highlightedNote.noteData])); + } } else if (highlightedEvent != null && highlightedEvent.eventData != null) { - // Remove the event. - performCommand(new RemoveEventsCommand([highlightedEvent.eventData])); + if (FlxG.keys.pressed.SHIFT) + { + // Shift + Right click opens the context menu. + // If we are clicking a large selection, open the Selection context menu, otherwise open the Event context menu. + var isHighlightedEventSelected:Bool = isEventSelected(highlightedEvent.eventData); + var useSingleEventContextMenu:Bool = (!isHighlightedEventSelected) + || (isHighlightedEventSelected && currentEventSelection.length == 1); + if (useSingleEventContextMenu) + { + this.openEventContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY, highlightedEvent.eventData); + } + else + { + this.openSelectionContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY); + } + } + else + { + // Right click removes the event. + performCommand(new RemoveEventsCommand([highlightedEvent.eventData])); + } } else { @@ -4253,11 +4371,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (gridGhostEvent == null) throw "ERROR: Tried to handle cursor, but gridGhostEvent is null! Check ChartEditorState.buildGrid()"; - var eventData:SongEventData = gridGhostEvent.eventData != null ? gridGhostEvent.eventData : new SongEventData(cursorMs, selectedEventKind, null); + var eventData:SongEventData = gridGhostEvent.eventData != null ? gridGhostEvent.eventData : new SongEventData(cursorMs, eventKindToPlace, null); - if (selectedEventKind != eventData.event) + if (eventKindToPlace != eventData.event) { - eventData.event = selectedEventKind; + eventData.event = eventKindToPlace; } eventData.time = cursorSnappedMs; @@ -4273,11 +4391,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (gridGhostNote == null) throw "ERROR: Tried to handle cursor, but gridGhostNote is null! Check ChartEditorState.buildGrid()"; - var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, selectedNoteKind); + var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, noteKindToPlace); - if (cursorColumn != noteData.data || selectedNoteKind != noteData.kind) + if (cursorColumn != noteData.data || noteKindToPlace != noteData.kind) { - noteData.kind = selectedNoteKind; + noteData.kind = noteKindToPlace; noteData.data = cursorColumn; gridGhostNote.playNoteAnimation(); } @@ -4319,7 +4437,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { targetCursorMode = Pointer; } - else if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea)) + else if (measureTicks != null && FlxG.mouse.overlaps(measureTicks)) { targetCursorMode = Pointer; } @@ -4520,7 +4638,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (playbarHeadLayout.playbarHead.value != songPosPercent) playbarHeadLayout.playbarHead.value = songPosPercent; } - var songPos:Float = Conductor.songPosition + Conductor.instrumentalOffset; + var songPos:Float = Conductor.instance.songPosition + Conductor.instance.instrumentalOffset; var songPosSeconds:String = Std.string(Math.floor((Math.abs(songPos) / 1000) % 60)).lpad('0', 2); var songPosMinutes:String = Std.string(Math.floor((Math.abs(songPos) / 1000) / 60)).lpad('0', 2); if (songPos < 0) songPosMinutes = '-' + songPosMinutes; @@ -4576,16 +4694,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels; var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio; var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep)); - var playheadPosSnappedMs:Float = playheadPosStep * Conductor.stepLengthMs * noteSnapRatio; + var playheadPosSnappedMs:Float = playheadPosStep * Conductor.instance.stepLengthMs * noteSnapRatio; // Look for notes within 1 step of the playhead. var notesAtPos:Array = SongDataUtils.getNotesInTimeRange(currentSongChartNoteData, playheadPosSnappedMs, - playheadPosSnappedMs + Conductor.stepLengthMs * noteSnapRatio); + playheadPosSnappedMs + Conductor.instance.stepLengthMs * noteSnapRatio); notesAtPos = SongDataUtils.getNotesWithData(notesAtPos, [column]); if (notesAtPos.length == 0) { - var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, selectedNoteKind); + var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace); performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); } else @@ -4699,6 +4817,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState FlxG.switchState(new MainMenuState()); resetWindowTitle(); + + criticalFailure = true; } /** @@ -4742,9 +4862,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState else { var targetMs:Float = scrollPositionInMs + playheadPositionInMs; - var targetStep:Float = Conductor.getTimeInSteps(targetMs); + var targetStep:Float = Conductor.instance.getTimeInSteps(targetMs); var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio; - var targetSnappedMs:Float = Conductor.getStepTimeInMs(targetSnappedStep); + var targetSnappedMs:Float = Conductor.instance.getStepTimeInMs(targetSnappedStep); targetSnappedMs; } performCommand(new PasteItemsCommand(targetMs)); @@ -4878,11 +4998,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState #end } - override function handleQuickWatch():Void + function handleQuickWatch():Void { - super.handleQuickWatch(); - - FlxG.watch.addQuick('musicTime', audioInstTrack?.time); + FlxG.watch.addQuick('musicTime', audioInstTrack?.time ?? 0.0); FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels); FlxG.watch.addQuick('playheadPosInPixels', playheadPositionInPixels); @@ -4909,7 +5027,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ function testSongInPlayState(minimal:Bool = false):Void { - autoSave(); + autoSave(true); stopWelcomeMusic(); @@ -5109,16 +5227,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState function onSongLengthChanged():Void { - if (gridTiledSprite != null) gridTiledSprite.height = songLengthInPixels; - if (gridPlayheadScrollArea != null) + if (gridTiledSprite != null) { - gridPlayheadScrollArea.setGraphicSize(Std.int(gridPlayheadScrollArea.width), songLengthInPixels); - gridPlayheadScrollArea.updateHitbox(); + gridTiledSprite.height = songLengthInPixels; + } + if (measureTicks != null) + { + measureTicks.setHeight(songLengthInPixels); } // Remove any notes past the end of the song. var songCutoffPointSteps:Float = songLengthInSteps - 0.1; - var songCutoffPointMs:Float = Conductor.getStepTimeInMs(songCutoffPointSteps); + var songCutoffPointMs:Float = Conductor.instance.getStepTimeInMs(songCutoffPointSteps); currentSongChartNoteData = SongDataUtils.clampSongNoteData(currentSongChartNoteData, 0.0, songCutoffPointMs); currentSongChartEventData = SongDataUtils.clampSongEventData(currentSongChartEventData, 0.0, songCutoffPointMs); @@ -5220,7 +5340,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var prevDifficulty = availableDifficulties[availableDifficulties.length - 1]; selectedDifficulty = prevDifficulty; - Conductor.mapTimeChanges(this.currentSongMetadata.timeChanges); + Conductor.instance.mapTimeChanges(this.currentSongMetadata.timeChanges); + updateTimeSignature(); refreshDifficultyTreeSelection(); this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); @@ -5282,9 +5403,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Update the songPosition in the audio tracks. if (audioInstTrack != null) { - audioInstTrack.time = scrollPositionInMs + playheadPositionInMs - Conductor.instrumentalOffset; + audioInstTrack.time = scrollPositionInMs + playheadPositionInMs - Conductor.instance.instrumentalOffset; // Update the songPosition in the Conductor. - Conductor.update(audioInstTrack.time); + Conductor.instance.update(audioInstTrack.time); if (audioVocalTrackGroup != null) audioVocalTrackGroup.time = audioInstTrack.time; } @@ -5344,6 +5465,20 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState this.persistentUpdate = true; this.persistentDraw = true; + if (displayAutosavePopup) + { + displayAutosavePopup = false; + Toolkit.callLater(() -> { + var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]); + this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [ + { + text: "Take Me There", + callback: openBackupsFolder, + } + ]); + }); + } + moveSongToScrollPosition(); fadeInWelcomeMusic(7, 10); @@ -5360,6 +5495,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = vocalTargetVolume; } + function updateTimeSignature():Void + { + // Redo the grid bitmap to be 4/4. + this.updateTheme(); + gridTiledSprite.loadGraphic(gridBitmap); + measureTicks.reloadTickBitmap(); + } + /** * HAXEUI FUNCTIONS */ @@ -5472,6 +5615,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (notePreviewViewportBoundsDirty) { setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); + notePreviewViewportBoundsDirty = false; } } @@ -5603,7 +5747,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState trace('ERROR: Instrumental track is null!'); } - this.songLengthInMs = (audioInstTrack?.length ?? 1000.0) + Conductor.instrumentalOffset; + this.songLengthInMs = (audioInstTrack?.length ?? 1000.0) + Conductor.instance.instrumentalOffset; // Many things get reset when song length changes. healthIconsDirty = true; @@ -5628,6 +5772,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState cleanupAutoSave(); + this.closeAllMenus(); + // Hide the mouse cursor on other states. Cursor.hide(); diff --git a/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx b/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx index 5264623f6..bd832fab3 100644 --- a/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx +++ b/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx @@ -34,7 +34,12 @@ class ChangeStartingBPMCommand implements ChartEditorCommand state.currentSongMetadata.timeChanges = timeChanges; - Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + state.notePreviewViewportBoundsDirty = true; + state.scrollPositionInPixels = 0; + + Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges); } public function undo(state:ChartEditorState):Void @@ -51,7 +56,12 @@ class ChangeStartingBPMCommand implements ChartEditorCommand state.currentSongMetadata.timeChanges = timeChanges; - Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); + state.noteDisplayDirty = true; + state.notePreviewDirty = true; + state.notePreviewViewportBoundsDirty = true; + state.scrollPositionInPixels = 0; + + Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges); } public function shouldAddToHistory(state:ChartEditorState):Bool diff --git a/source/funkin/ui/debug/charting/commands/MoveEventsCommand.hx b/source/funkin/ui/debug/charting/commands/MoveEventsCommand.hx index a0368f908..ed50ad33e 100644 --- a/source/funkin/ui/debug/charting/commands/MoveEventsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/MoveEventsCommand.hx @@ -33,7 +33,7 @@ class MoveEventsCommand implements ChartEditorCommand { // Clone the notes to prevent editing from affecting the history. var resultEvent = event.clone(); - resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); + resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); movedEvents.push(resultEvent); } diff --git a/source/funkin/ui/debug/charting/commands/MoveItemsCommand.hx b/source/funkin/ui/debug/charting/commands/MoveItemsCommand.hx index 4fa1e2f87..f44cb973a 100644 --- a/source/funkin/ui/debug/charting/commands/MoveItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/MoveItemsCommand.hx @@ -21,8 +21,8 @@ class MoveItemsCommand implements ChartEditorCommand public function new(notes:Array, events:Array, offset:Float, columns:Int) { // Clone the notes to prevent editing from affecting the history. - this.notes = [for (note in notes) note.clone()]; - this.events = [for (event in events) event.clone()]; + this.notes = notes.clone(); + this.events = events.clone(); this.offset = offset; this.columns = columns; this.movedNotes = []; @@ -41,7 +41,7 @@ class MoveItemsCommand implements ChartEditorCommand { // Clone the notes to prevent editing from affecting the history. var resultNote = note.clone(); - resultNote.time = (resultNote.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); + resultNote.time = (resultNote.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); resultNote.data = ChartEditorState.gridColumnToNoteData((ChartEditorState.noteDataToGridColumn(resultNote.data) + columns).clamp(0, ChartEditorState.STRUMLINE_SIZE * 2 - 1)); @@ -52,7 +52,7 @@ class MoveItemsCommand implements ChartEditorCommand { // Clone the notes to prevent editing from affecting the history. var resultEvent = event.clone(); - resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); + resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); movedEvents.push(resultEvent); } diff --git a/source/funkin/ui/debug/charting/commands/MoveNotesCommand.hx b/source/funkin/ui/debug/charting/commands/MoveNotesCommand.hx index 37ed61d72..51aeb5bbc 100644 --- a/source/funkin/ui/debug/charting/commands/MoveNotesCommand.hx +++ b/source/funkin/ui/debug/charting/commands/MoveNotesCommand.hx @@ -34,7 +34,7 @@ class MoveNotesCommand implements ChartEditorCommand { // Clone the notes to prevent editing from affecting the history. var resultNote = note.clone(); - resultNote.time = (resultNote.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); + resultNote.time = (resultNote.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio))); resultNote.data = ChartEditorState.gridColumnToNoteData((ChartEditorState.noteDataToGridColumn(resultNote.data) + columns).clamp(0, ChartEditorState.STRUMLINE_SIZE * 2 - 1)); diff --git a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx index bba6ae866..257db94b4 100644 --- a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx @@ -32,9 +32,9 @@ class PasteItemsCommand implements ChartEditorCommand return; } - var stepEndOfSong:Float = Conductor.getTimeInSteps(state.songLengthInMs); + var stepEndOfSong:Float = Conductor.instance.getTimeInSteps(state.songLengthInMs); var stepCutoff:Float = stepEndOfSong - 1.0; - var msCutoff:Float = Conductor.getStepTimeInMs(stepCutoff); + var msCutoff:Float = Conductor.instance.getStepTimeInMs(stepCutoff); addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard.notes, Std.int(targetTimestamp)); addedNotes = SongDataUtils.clampSongNoteData(addedNotes, 0.0, msCutoff); diff --git a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx index 17f45f946..d6c5beeac 100644 --- a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx @@ -33,6 +33,32 @@ class SelectItemsCommand implements ChartEditorCommand state.currentEventSelection.push(event); } + // If we just selected one or more events (and no notes), then we should make the event data toolbox display the event data for the selected event. + if (this.notes.length == 0 && this.events.length >= 1) + { + var eventSelected = this.events[0]; + + state.eventKindToPlace = eventSelected.event; + + // This code is here to parse event data that's not built as a struct for some reason. + // TODO: Clean this up or get rid of it. + var eventSchema = eventSelected.getSchema(); + var defaultKey = null; + if (eventSchema == null) + { + trace('[WARNING] Event schema not found for event ${eventSelected.event}.'); + } + else + { + defaultKey = eventSchema.getFirstField()?.name; + } + var eventData = eventSelected.valueAsStruct(defaultKey); + + state.eventDataToPlace = eventData; + + state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT); + } + state.noteDisplayDirty = true; state.notePreviewDirty = true; } diff --git a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx index f9b4fb6d7..35a00e562 100644 --- a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx +++ b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx @@ -30,6 +30,32 @@ class SetItemSelectionCommand implements ChartEditorCommand state.currentNoteSelection = notes; state.currentEventSelection = events; + // If we just selected one or more events (and no notes), then we should make the event data toolbox display the event data for the selected event. + if (this.notes.length == 0 && this.events.length >= 1) + { + var eventSelected = this.events[0]; + + state.eventKindToPlace = eventSelected.event; + + // This code is here to parse event data that's not built as a struct for some reason. + // TODO: Clean this up or get rid of it. + var eventSchema = eventSelected.getSchema(); + var defaultKey = null; + if (eventSchema == null) + { + trace('[WARNING] Event schema not found for event ${eventSelected.event}.'); + } + else + { + defaultKey = eventSchema.getFirstField()?.name; + } + var eventData = eventSelected.valueAsStruct(defaultKey); + + state.eventDataToPlace = eventData; + + state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT); + } + state.noteDisplayDirty = true; } diff --git a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx index e0070cc7b..36c6b1d2f 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx @@ -1,6 +1,6 @@ package funkin.ui.debug.charting.components; -import funkin.data.event.SongEventData.SongEventParser; +import funkin.data.event.SongEventRegistry; import flixel.graphics.frames.FlxAtlasFrames; import openfl.display.BitmapData; import openfl.utils.Assets; @@ -79,7 +79,7 @@ class ChartEditorEventSprite extends FlxSprite } // Push all the other events as frames. - for (eventName in SongEventParser.listEventIds()) + for (eventName in SongEventRegistry.listEventIds()) { var exists:Bool = Assets.exists(Paths.image('ui/chart-editor/events/$eventName')); if (!exists) continue; // No graphic for this event. @@ -105,7 +105,7 @@ class ChartEditorEventSprite extends FlxSprite function buildAnimations():Void { - var eventNames:Array = [DEFAULT_EVENT].concat(SongEventParser.listEventIds()); + var eventNames:Array = [DEFAULT_EVENT].concat(SongEventRegistry.listEventIds()); for (eventName in eventNames) { this.animation.addByPrefix(eventName, '${eventName}0', 24, false); @@ -147,8 +147,6 @@ class ChartEditorEventSprite extends FlxSprite else { this.visible = true; - // Only play the animation if the event type has changed. - // if (this.eventData == null || this.eventData.event != value.event) playAnimation(value.event); this.eventData = value; // Update the position to match the note data. diff --git a/source/funkin/ui/debug/charting/components/ChartEditorMeasureTicks.hx b/source/funkin/ui/debug/charting/components/ChartEditorMeasureTicks.hx new file mode 100644 index 000000000..1a76d1e22 --- /dev/null +++ b/source/funkin/ui/debug/charting/components/ChartEditorMeasureTicks.hx @@ -0,0 +1,71 @@ +package funkin.ui.debug.charting.components; + +import flixel.FlxSprite; +import flixel.addons.display.FlxTiledSprite; +import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; +import flixel.text.FlxText; +import flixel.util.FlxColor; + +@:access(funkin.ui.debug.charting.ChartEditorState) +class ChartEditorMeasureTicks extends FlxTypedSpriteGroup +{ + var chartEditorState:ChartEditorState; + + var tickTiledSprite:FlxTiledSprite; + var measureNumber:FlxText; + + override function set_y(value:Float):Float + { + var result = super.set_y(value); + + updateMeasureNumber(); + + return result; + } + + public function new(chartEditorState:ChartEditorState) + { + super(); + + this.chartEditorState = chartEditorState; + + tickTiledSprite = new FlxTiledSprite(chartEditorState.measureTickBitmap, chartEditorState.measureTickBitmap.width, 1000, false, true); + add(tickTiledSprite); + + measureNumber = new FlxText(0, 0, ChartEditorState.GRID_SIZE, "1"); + measureNumber.setFormat(Paths.font('vcr.ttf'), 20, FlxColor.WHITE); + measureNumber.borderStyle = FlxTextBorderStyle.OUTLINE; + measureNumber.borderColor = FlxColor.BLACK; + add(measureNumber); + } + + public function reloadTickBitmap():Void + { + tickTiledSprite.loadGraphic(chartEditorState.measureTickBitmap); + } + + /** + * At time of writing, we only have to manipulate one measure number because we can only see one measure at a time. + */ + function updateMeasureNumber() + { + if (measureNumber == null) return; + + var viewTopPosition = 0 - this.y; + var viewHeight = FlxG.height - ChartEditorState.MENU_BAR_HEIGHT - ChartEditorState.PLAYBAR_HEIGHT; + var viewBottomPosition = viewTopPosition + viewHeight; + + var measureNumberInViewport = Math.floor(viewTopPosition / ChartEditorState.GRID_SIZE / Conductor.instance.stepsPerMeasure) + 1; + var measureNumberPosition = measureNumberInViewport * ChartEditorState.GRID_SIZE * Conductor.instance.stepsPerMeasure; + + measureNumber.text = '${measureNumberInViewport + 1}'; + measureNumber.y = measureNumberPosition + this.y; + + // trace(measureNumber.text + ' at ' + measureNumber.y); + } + + public function setHeight(songLengthInPixels:Float):Void + { + tickTiledSprite.height = songLengthInPixels; + } +} diff --git a/source/funkin/ui/debug/charting/contextmenus/ChartEditorBaseContextMenu.hx b/source/funkin/ui/debug/charting/contextmenus/ChartEditorBaseContextMenu.hx new file mode 100644 index 000000000..f25f3ebb3 --- /dev/null +++ b/source/funkin/ui/debug/charting/contextmenus/ChartEditorBaseContextMenu.hx @@ -0,0 +1,19 @@ +package funkin.ui.debug.charting.contextmenus; + +import haxe.ui.containers.menus.Menu; + +@:access(funkin.ui.debug.charting.ChartEditorState) +class ChartEditorBaseContextMenu extends Menu +{ + var chartEditorState:ChartEditorState; + + public function new(chartEditorState:ChartEditorState, xPos:Float = 0, yPos:Float = 0) + { + super(); + + this.chartEditorState = chartEditorState; + + this.left = xPos; + this.top = yPos; + } +} diff --git a/source/funkin/ui/debug/charting/contextmenus/ChartEditorDefaultContextMenu.hx b/source/funkin/ui/debug/charting/contextmenus/ChartEditorDefaultContextMenu.hx new file mode 100644 index 000000000..9529cc2fd --- /dev/null +++ b/source/funkin/ui/debug/charting/contextmenus/ChartEditorDefaultContextMenu.hx @@ -0,0 +1,14 @@ +package funkin.ui.debug.charting.contextmenus; + +import haxe.ui.containers.menus.Menu; +import haxe.ui.core.Screen; + +@:access(funkin.ui.debug.charting.ChartEditorState) +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/default.xml")) +class ChartEditorDefaultContextMenu extends ChartEditorBaseContextMenu +{ + public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0) + { + super(chartEditorState2, xPos2, yPos2); + } +} diff --git a/source/funkin/ui/debug/charting/contextmenus/ChartEditorEventContextMenu.hx b/source/funkin/ui/debug/charting/contextmenus/ChartEditorEventContextMenu.hx new file mode 100644 index 000000000..a79125b21 --- /dev/null +++ b/source/funkin/ui/debug/charting/contextmenus/ChartEditorEventContextMenu.hx @@ -0,0 +1,32 @@ +package funkin.ui.debug.charting.contextmenus; + +import haxe.ui.containers.menus.Menu; +import haxe.ui.containers.menus.MenuItem; +import haxe.ui.core.Screen; +import funkin.data.song.SongData.SongEventData; +import funkin.ui.debug.charting.commands.RemoveEventsCommand; + +@:access(funkin.ui.debug.charting.ChartEditorState) +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/event.xml")) +class ChartEditorEventContextMenu extends ChartEditorBaseContextMenu +{ + var contextmenuEdit:MenuItem; + var contextmenuDelete:MenuItem; + + var data:SongEventData; + + public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongEventData) + { + super(chartEditorState2, xPos2, yPos2); + this.data = data; + + initialize(); + } + + function initialize() + { + contextmenuDelete.onClick = function(_) { + chartEditorState.performCommand(new RemoveEventsCommand([data])); + } + } +} diff --git a/source/funkin/ui/debug/charting/contextmenus/ChartEditorNoteContextMenu.hx b/source/funkin/ui/debug/charting/contextmenus/ChartEditorNoteContextMenu.hx new file mode 100644 index 000000000..4bfab27e8 --- /dev/null +++ b/source/funkin/ui/debug/charting/contextmenus/ChartEditorNoteContextMenu.hx @@ -0,0 +1,38 @@ +package funkin.ui.debug.charting.contextmenus; + +import haxe.ui.containers.menus.Menu; +import haxe.ui.containers.menus.MenuItem; +import haxe.ui.core.Screen; +import funkin.data.song.SongData.SongNoteData; +import funkin.ui.debug.charting.commands.FlipNotesCommand; +import funkin.ui.debug.charting.commands.RemoveNotesCommand; + +@:access(funkin.ui.debug.charting.ChartEditorState) +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/note.xml")) +class ChartEditorNoteContextMenu extends ChartEditorBaseContextMenu +{ + var contextmenuFlip:MenuItem; + var contextmenuDelete:MenuItem; + + var data:SongNoteData; + + public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongNoteData) + { + super(chartEditorState2, xPos2, yPos2); + this.data = data; + + initialize(); + } + + function initialize():Void + { + // NOTE: Remember to use commands here to ensure undo/redo works properly + contextmenuFlip.onClick = function(_) { + chartEditorState.performCommand(new FlipNotesCommand([data])); + } + + contextmenuDelete.onClick = function(_) { + chartEditorState.performCommand(new RemoveNotesCommand([data])); + } + } +} diff --git a/source/funkin/ui/debug/charting/contextmenus/ChartEditorSelectionContextMenu.hx b/source/funkin/ui/debug/charting/contextmenus/ChartEditorSelectionContextMenu.hx new file mode 100644 index 000000000..feed9b689 --- /dev/null +++ b/source/funkin/ui/debug/charting/contextmenus/ChartEditorSelectionContextMenu.hx @@ -0,0 +1,58 @@ +package funkin.ui.debug.charting.contextmenus; + +import haxe.ui.containers.menus.Menu; +import haxe.ui.containers.menus.MenuItem; +import haxe.ui.core.Screen; +import funkin.ui.debug.charting.commands.CutItemsCommand; +import funkin.ui.debug.charting.commands.RemoveEventsCommand; +import funkin.ui.debug.charting.commands.RemoveItemsCommand; +import funkin.ui.debug.charting.commands.RemoveNotesCommand; + +@:access(funkin.ui.debug.charting.ChartEditorState) +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/selection.xml")) +class ChartEditorSelectionContextMenu extends ChartEditorBaseContextMenu +{ + var contextmenuCut:MenuItem; + var contextmenuCopy:MenuItem; + var contextmenuPaste:MenuItem; + var contextmenuDelete:MenuItem; + var contextmenuFlip:MenuItem; + var contextmenuSelectAll:MenuItem; + var contextmenuSelectInverse:MenuItem; + var contextmenuSelectNone:MenuItem; + + public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0) + { + super(chartEditorState2, xPos2, yPos2); + + initialize(); + } + + function initialize():Void + { + contextmenuCut.onClick = (_) -> { + chartEditorState.performCommand(new CutItemsCommand(chartEditorState.currentNoteSelection, chartEditorState.currentEventSelection)); + }; + contextmenuCopy.onClick = (_) -> { + chartEditorState.copySelection(); + }; + contextmenuFlip.onClick = (_) -> { + if (chartEditorState.currentNoteSelection.length > 0 && chartEditorState.currentEventSelection.length > 0) + { + chartEditorState.performCommand(new RemoveItemsCommand(chartEditorState.currentNoteSelection, chartEditorState.currentEventSelection)); + } + else if (chartEditorState.currentNoteSelection.length > 0) + { + chartEditorState.performCommand(new RemoveNotesCommand(chartEditorState.currentNoteSelection)); + } + else if (chartEditorState.currentEventSelection.length > 0) + { + chartEditorState.performCommand(new RemoveEventsCommand(chartEditorState.currentEventSelection)); + } + else + { + // Do nothing??? + } + }; + } +} diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx index d4fcc4638..0edba7357 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx @@ -185,8 +185,8 @@ class ChartEditorAudioHandler state.audioVocalTrackGroup.addPlayerVoice(vocalTrack); state.audioVisGroup.addPlayerVis(vocalTrack); state.audioVisGroup.playerVis.x = 885; - state.audioVisGroup.playerVis.realtimeVisLenght = Conductor.getStepTimeInMs(16) * 0.00195; // The height of the visualizer, in time. - state.audioVisGroup.playerVis.daHeight = (ChartEditorState.GRID_SIZE) * 16; // The height of the visualizer, in pixels. + state.audioVisGroup.playerVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195; + state.audioVisGroup.playerVis.daHeight = (ChartEditorState.GRID_SIZE) * 16; state.audioVisGroup.playerVis.detail = 1; state.audioVisGroup.playerVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS); @@ -196,8 +196,9 @@ class ChartEditorAudioHandler state.audioVocalTrackGroup.addOpponentVoice(vocalTrack); state.audioVisGroup.addOpponentVis(vocalTrack); state.audioVisGroup.opponentVis.x = 435; - state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.getStepTimeInMs(16) * 0.00195; // The height of the visualizer, in time. - state.audioVisGroup.opponentVis.daHeight = (ChartEditorState.GRID_SIZE) * 16; // The height of the visualizer, in pixels. + + state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195; + state.audioVisGroup.opponentVis.daHeight = (ChartEditorState.GRID_SIZE) * 16; state.audioVisGroup.opponentVis.detail = 1; state.audioVisGroup.opponentVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS); diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorContextMenuHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorContextMenuHandler.hx new file mode 100644 index 000000000..b914f4149 --- /dev/null +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorContextMenuHandler.hx @@ -0,0 +1,64 @@ +package funkin.ui.debug.charting.handlers; + +import funkin.ui.debug.charting.contextmenus.ChartEditorDefaultContextMenu; +import funkin.ui.debug.charting.contextmenus.ChartEditorEventContextMenu; +import funkin.ui.debug.charting.contextmenus.ChartEditorNoteContextMenu; +import funkin.ui.debug.charting.contextmenus.ChartEditorSelectionContextMenu; +import haxe.ui.containers.menus.Menu; +import haxe.ui.core.Screen; +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongEventData; + +/** + * Handles context menus (the little menus that appear when you right click on stuff) for the new Chart Editor. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class ChartEditorContextMenuHandler +{ + static var existingMenus:Array = []; + + public static function openDefaultContextMenu(state:ChartEditorState, xPos:Float, yPos:Float) + { + displayMenu(state, new ChartEditorDefaultContextMenu(state, xPos, yPos)); + } + + public static function openSelectionContextMenu(state:ChartEditorState, xPos:Float, yPos:Float) + { + displayMenu(state, new ChartEditorSelectionContextMenu(state, xPos, yPos)); + } + + public static function openNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData) + { + displayMenu(state, new ChartEditorNoteContextMenu(state, xPos, yPos, data)); + } + + public static function openEventContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongEventData) + { + displayMenu(state, new ChartEditorEventContextMenu(state, xPos, yPos, data)); + } + + static function displayMenu(state:ChartEditorState, targetMenu:Menu) + { + // Close any existing menus + closeAllMenus(state); + + // Show the new menu + Screen.instance.addComponent(targetMenu); + existingMenus.push(targetMenu); + } + + public static function closeMenu(state:ChartEditorState, targetMenu:Menu) + { + // targetMenu.close(); + existingMenus.remove(targetMenu); + } + + public static function closeAllMenus(state:ChartEditorState) + { + for (existingMenu in existingMenus) + { + closeMenu(state, existingMenu); + } + } +} diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx index 2ede1a39f..1e1a02974 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -684,8 +684,9 @@ class ChartEditorDialogHandler state.songMetadata.set(targetVariation, newSongMetadata); - Conductor.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata. - Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); + Conductor.instance.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata. + Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges); + state.updateTimeSignature(); state.selectedVariation = Constants.DEFAULT_VARIATION; state.selectedDifficulty = state.availableDifficulties[0]; diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx index 267d2208a..9c86269e8 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx @@ -43,7 +43,8 @@ class ChartEditorImportExportHandler var variation = (metadata.variation == null || metadata.variation == '') ? Constants.DEFAULT_VARIATION : metadata.variation; // Clone to prevent modifying the original. - var metadataClone:SongMetadata = metadata.clone(variation); + var metadataClone:SongMetadata = metadata.clone(); + metadataClone.variation = variation; if (metadataClone != null) songMetadata.set(variation, metadataClone); var chartData:Null = SongRegistry.instance.parseEntryChartData(songId, metadata.variation); @@ -114,9 +115,10 @@ class ChartEditorImportExportHandler state.songMetadata = newSongMetadata; state.songChartData = newSongChartData; - Conductor.forceBPM(null); // Disable the forced BPM. - Conductor.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata. - Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); + Conductor.instance.forceBPM(null); // Disable the forced BPM. + Conductor.instance.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata. + Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges); + state.updateTimeSignature(); state.notePreviewDirty = true; state.notePreviewViewportBoundsDirty = true; @@ -415,16 +417,34 @@ class ChartEditorImportExportHandler ]); // We have to force write because the program will die before the save dialog is closed. trace('Force exporting to $targetPath...'); - FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode); - if (onSaveCb != null) onSaveCb(targetPath); + try + { + FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode); + // On success. + if (onSaveCb != null) onSaveCb(targetPath); + } + catch (e) + { + // On failure. + if (onCancelCb != null) onCancelCb(); + } } else { // Force write since we know what file the user wants to overwrite. trace('Force exporting to $targetPath...'); - FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode); - state.saveDataDirty = false; - if (onSaveCb != null) onSaveCb(targetPath); + try + { + // On success. + FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode); + state.saveDataDirty = false; + if (onSaveCb != null) onSaveCb(targetPath); + } + catch (e) + { + // On failure. + if (onCancelCb != null) onCancelCb(); + } } } else diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx index 4197ebdd3..d3aef4bfd 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx @@ -81,6 +81,7 @@ class ChartEditorThemeHandler { updateBackground(state); updateGridBitmap(state); + updateMeasureTicks(state); updateSelectionSquare(state); updateNotePreview(state); } @@ -125,7 +126,7 @@ 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 * TOTAL_COLUMN_COUNT); - var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.stepsPerMeasure); + var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.instance.stepsPerMeasure); state.gridBitmap = FlxGridOverlay.createGrid(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE, gridWidth, gridHeight, true, gridColor1, gridColor2); // Selection borders @@ -142,7 +143,7 @@ class ChartEditorThemeHandler selectionBorderColor); // Selection borders horizontally along the middle. - for (i in 1...(Conductor.stepsPerMeasure)) + for (i in 1...(Conductor.instance.stepsPerMeasure)) { state.gridBitmap.fillRect(new Rectangle(0, (ChartEditorState.GRID_SIZE * i) - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), state.gridBitmap.width, ChartEditorState.GRID_SELECTION_BORDER_WIDTH), @@ -197,9 +198,9 @@ class ChartEditorThemeHandler }; // Selection borders horizontally in the middle. - for (i in 1...(Conductor.stepsPerMeasure)) + for (i in 1...(Conductor.instance.stepsPerMeasure)) { - if ((i % Conductor.beatsPerMeasure) == 0) + if ((i % Conductor.instance.beatsPerMeasure) == 0) { state.gridBitmap.fillRect(new Rectangle(0, (ChartEditorState.GRID_SIZE * i) - (GRID_BEAT_DIVIDER_WIDTH / 2), state.gridBitmap.width, GRID_BEAT_DIVIDER_WIDTH), @@ -207,9 +208,6 @@ class ChartEditorThemeHandler } } - // Divider at top - state.gridBitmap.fillRect(new Rectangle(0, 0, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor); - // Draw vertical dividers between the strumlines. var gridStrumlineDividerColor:FlxColor = switch (state.currentTheme) @@ -233,6 +231,61 @@ class ChartEditorThemeHandler // Else, gridTiledSprite will be built later. } + static function updateMeasureTicks(state:ChartEditorState):Void + { + var measureTickWidth:Int = 6; + var beatTickWidth:Int = 4; + var stepTickWidth:Int = 2; + + // Draw the measure ticks. + var ticksWidth:Int = Std.int(ChartEditorState.GRID_SIZE); // 1 grid squares wide. + var ticksHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.instance.stepsPerMeasure); // 1 measure tall. + state.measureTickBitmap = new BitmapData(ticksWidth, ticksHeight, true); + state.measureTickBitmap.fillRect(new Rectangle(0, 0, ticksWidth, ticksHeight), GRID_BEAT_DIVIDER_COLOR_DARK); + + // Draw the measure ticks. + state.measureTickBitmap.fillRect(new Rectangle(0, 0, state.measureTickBitmap.width, measureTickWidth / 2), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + var bottomTickY:Float = state.measureTickBitmap.height - (measureTickWidth / 2); + state.measureTickBitmap.fillRect(new Rectangle(0, bottomTickY, state.measureTickBitmap.width, measureTickWidth / 2), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + + // Draw the beat ticks. + var beatTick2Y:Float = state.measureTickBitmap.height * 1 / Conductor.instance.beatsPerMeasure - (beatTickWidth / 2); + var beatTick3Y:Float = state.measureTickBitmap.height * 2 / Conductor.instance.beatsPerMeasure - (beatTickWidth / 2); + var beatTick4Y:Float = state.measureTickBitmap.height * 3 / Conductor.instance.beatsPerMeasure - (beatTickWidth / 2); + var beatTickLength:Float = state.measureTickBitmap.width * 2 / 3; + state.measureTickBitmap.fillRect(new Rectangle(0, beatTick2Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.measureTickBitmap.fillRect(new Rectangle(0, beatTick3Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.measureTickBitmap.fillRect(new Rectangle(0, beatTick4Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + + // Draw the step ticks. + // TODO: Make this a loop or something. + var stepTick2Y:Float = state.measureTickBitmap.height * 1 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); + var stepTick3Y:Float = state.measureTickBitmap.height * 2 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); + var stepTick4Y:Float = state.measureTickBitmap.height * 3 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); + var stepTick6Y:Float = state.measureTickBitmap.height * 5 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); + var stepTick7Y:Float = state.measureTickBitmap.height * 6 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); + var stepTick8Y:Float = state.measureTickBitmap.height * 7 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); + var stepTick10Y:Float = state.measureTickBitmap.height * 9 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); + var stepTick11Y:Float = state.measureTickBitmap.height * 10 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); + var stepTick12Y:Float = state.measureTickBitmap.height * 11 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); + var stepTick14Y:Float = state.measureTickBitmap.height * 13 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); + var stepTick15Y:Float = state.measureTickBitmap.height * 14 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); + var stepTick16Y:Float = state.measureTickBitmap.height * 15 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2); + var stepTickLength:Float = state.measureTickBitmap.width * 1 / 3; + state.measureTickBitmap.fillRect(new Rectangle(0, stepTick2Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.measureTickBitmap.fillRect(new Rectangle(0, stepTick3Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.measureTickBitmap.fillRect(new Rectangle(0, stepTick4Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.measureTickBitmap.fillRect(new Rectangle(0, stepTick6Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.measureTickBitmap.fillRect(new Rectangle(0, stepTick7Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.measureTickBitmap.fillRect(new Rectangle(0, stepTick8Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.measureTickBitmap.fillRect(new Rectangle(0, stepTick10Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.measureTickBitmap.fillRect(new Rectangle(0, stepTick11Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.measureTickBitmap.fillRect(new Rectangle(0, stepTick12Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.measureTickBitmap.fillRect(new Rectangle(0, stepTick14Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.measureTickBitmap.fillRect(new Rectangle(0, stepTick15Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + state.measureTickBitmap.fillRect(new Rectangle(0, stepTick16Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT); + } + static function updateSelectionSquare(state:ChartEditorState):Void { var selectionSquareBorderColor:FlxColor = switch (state.currentTheme) @@ -289,14 +342,21 @@ class ChartEditorThemeHandler ChartEditorState.GRID_SIZE - (SELECTION_SQUARE_BORDER_WIDTH * 2), ChartEditorState.GRID_SIZE - (SELECTION_SQUARE_BORDER_WIDTH * 2)), viewportFillColor); - state.notePreviewViewport = new FlxSliceSprite(state.notePreviewViewportBitmap, - new FlxRect(SELECTION_SQUARE_BORDER_WIDTH - + 1, SELECTION_SQUARE_BORDER_WIDTH - + 1, ChartEditorState.GRID_SIZE - - (2 * SELECTION_SQUARE_BORDER_WIDTH + 2), - ChartEditorState.GRID_SIZE - - (2 * SELECTION_SQUARE_BORDER_WIDTH + 2)), - 32, 32); + if (state.notePreviewViewport != null) + { + state.notePreviewViewport.loadGraphic(state.notePreviewViewportBitmap); + } + else + { + state.notePreviewViewport = new FlxSliceSprite(state.notePreviewViewportBitmap, + new FlxRect(SELECTION_SQUARE_BORDER_WIDTH + + 1, SELECTION_SQUARE_BORDER_WIDTH + + 1, + ChartEditorState.GRID_SIZE + - (2 * SELECTION_SQUARE_BORDER_WIDTH + 2), ChartEditorState.GRID_SIZE + - (2 * SELECTION_SQUARE_BORDER_WIDTH + 2)), + 32, 32); + } } public static function buildPlayheadBlock():FlxSprite diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx index 98d04887d..ce1997968 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx @@ -9,7 +9,7 @@ import haxe.ui.containers.TreeView; import haxe.ui.containers.TreeViewNode; import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.event.SongEvent; -import funkin.data.event.SongEventData; +import funkin.data.event.SongEventSchema; import funkin.data.song.SongData.SongTimeChange; import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.CharacterData; @@ -23,6 +23,7 @@ import funkin.ui.debug.charting.util.ChartEditorDropdowns; import funkin.ui.haxeui.components.CharacterPlayer; import funkin.util.FileUtil; import haxe.ui.components.Button; +import haxe.ui.data.ArrayDataSource; import haxe.ui.components.CheckBox; import haxe.ui.components.DropDown; import haxe.ui.components.HorizontalSlider; @@ -36,12 +37,12 @@ import haxe.ui.containers.dialogs.Dialog.DialogButton; import haxe.ui.containers.dialogs.Dialog.DialogEvent; import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorMetadataToolbox; +import funkin.ui.debug.charting.toolboxes.ChartEditorEventDataToolbox; import haxe.ui.containers.Frame; import haxe.ui.containers.Grid; import haxe.ui.containers.TreeView; import haxe.ui.containers.TreeViewNode; import haxe.ui.core.Component; -import haxe.ui.data.ArrayDataSource; import haxe.ui.events.UIEvent; /** @@ -79,8 +80,9 @@ class ChartEditorToolboxHandler { case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT: onShowToolboxNoteData(state, toolbox); - case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT: - onShowToolboxEventData(state, toolbox); + case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT: + // TODO: Fix this. + cast(toolbox, ChartEditorBaseToolbox).refresh(); case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT: onShowToolboxPlaytestProperties(state, toolbox); case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT: @@ -119,7 +121,7 @@ class ChartEditorToolboxHandler { case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT: onHideToolboxNoteData(state, toolbox); - case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT: + case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT: onHideToolboxEventData(state, toolbox); case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT: onHideToolboxPlaytestProperties(state, toolbox); @@ -195,7 +197,7 @@ class ChartEditorToolboxHandler { case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT: toolbox = buildToolboxNoteDataLayout(state); - case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT: + case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT: toolbox = buildToolboxEventDataLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT: toolbox = buildToolboxPlaytestPropertiesLayout(state); @@ -283,19 +285,19 @@ class ChartEditorToolboxHandler toolboxNotesCustomKindLabel.hidden = false; toolboxNotesCustomKind.hidden = false; - state.selectedNoteKind = toolboxNotesCustomKind.text; + state.noteKindToPlace = toolboxNotesCustomKind.text; } else { toolboxNotesCustomKindLabel.hidden = true; toolboxNotesCustomKind.hidden = true; - state.selectedNoteKind = event.data.id; + state.noteKindToPlace = event.data.id; } } toolboxNotesCustomKind.onChange = function(event:UIEvent) { - state.selectedNoteKind = toolboxNotesCustomKind.text; + state.noteKindToPlace = toolboxNotesCustomKind.text; } return toolbox; @@ -305,159 +307,16 @@ class ChartEditorToolboxHandler static function onHideToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - static function buildToolboxEventDataLayout(state:ChartEditorState):Null - { - var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT); - - if (toolbox == null) return null; - - // Starting position. - toolbox.x = 100; - toolbox.y = 150; - - toolbox.onDialogClosed = function(event:DialogEvent) { - state.menubarItemToggleToolboxEvents.selected = false; - } - - var toolboxEventsEventKind:Null = toolbox.findComponent('toolboxEventsEventKind', DropDown); - if (toolboxEventsEventKind == null) throw 'ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Could not find toolboxEventsEventKind component.'; - var toolboxEventsDataGrid:Null = toolbox.findComponent('toolboxEventsDataGrid', Grid); - if (toolboxEventsDataGrid == null) throw 'ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Could not find toolboxEventsDataGrid component.'; - - toolboxEventsEventKind.dataSource = new ArrayDataSource(); - - var songEvents:Array = SongEventParser.listEvents(); - - for (event in songEvents) - { - toolboxEventsEventKind.dataSource.add({text: event.getTitle(), value: event.id}); - } - - toolboxEventsEventKind.onChange = function(event:UIEvent) { - var eventType:String = event.data.value; - - trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event type changed: $eventType'); - - state.selectedEventKind = eventType; - - var schema:SongEventSchema = SongEventParser.getEventSchema(eventType); - - if (schema == null) - { - trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Unknown event kind: $eventType'); - return; - } - - buildEventDataFormFromSchema(state, toolboxEventsDataGrid, schema); - } - toolboxEventsEventKind.value = state.selectedEventKind; - - return toolbox; - } - - static function onShowToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - - static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} + static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} + static function onHideToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} + + static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} + static function onHideToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - static function buildEventDataFormFromSchema(state:ChartEditorState, target:Box, schema:SongEventSchema):Void - { - trace(schema); - // Clear the frame. - target.removeAllComponents(); - - state.selectedEventData = {}; - - for (field in schema) - { - if (field == null) continue; - - // Add a label. - var label:Label = new Label(); - label.text = field.title; - label.verticalAlign = "center"; - target.addComponent(label); - - var input:Component; - switch (field.type) - { - case INTEGER: - var numberStepper:NumberStepper = new NumberStepper(); - numberStepper.id = field.name; - numberStepper.step = field.step ?? 1.0; - numberStepper.min = field.min ?? 0.0; - numberStepper.max = field.max ?? 10.0; - if (field.defaultValue != null) numberStepper.value = field.defaultValue; - input = numberStepper; - case FLOAT: - var numberStepper:NumberStepper = new NumberStepper(); - numberStepper.id = field.name; - numberStepper.step = field.step ?? 0.1; - if (field.min != null) numberStepper.min = field.min; - if (field.max != null) numberStepper.max = field.max; - if (field.defaultValue != null) numberStepper.value = field.defaultValue; - input = numberStepper; - case BOOL: - var checkBox:CheckBox = new CheckBox(); - checkBox.id = field.name; - if (field.defaultValue != null) checkBox.selected = field.defaultValue; - input = checkBox; - case ENUM: - var dropDown:DropDown = new DropDown(); - dropDown.id = field.name; - dropDown.width = 200.0; - dropDown.dataSource = new ArrayDataSource(); - - if (field.keys == null) throw 'Field "${field.name}" is of Enum type but has no keys.'; - - // Add entries to the dropdown. - - for (optionName in field.keys.keys()) - { - var optionValue:Null = field.keys.get(optionName); - trace('$optionName : $optionValue'); - dropDown.dataSource.add({value: optionValue, text: optionName}); - } - - dropDown.value = field.defaultValue; - - input = dropDown; - case STRING: - input = new TextField(); - input.id = field.name; - if (field.defaultValue != null) input.text = field.defaultValue; - default: - // Unknown type. Display a label so we know what it is. - input = new Label(); - input.id = field.name; - input.text = field.type; - } - - target.addComponent(input); - - input.onChange = function(event:UIEvent) { - var value = event.target.value; - if (field.type == ENUM) - { - value = event.target.value.value; - } - trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${value}'); - - if (value == null) - { - state.selectedEventData.remove(event.target.id); - } - else - { - state.selectedEventData.set(event.target.id, value); - } - } - } - } - static function buildToolboxPlaytestPropertiesLayout(state:ChartEditorState):Null { // fill with playtest properties @@ -586,8 +445,6 @@ class ChartEditorToolboxHandler trace('selected node: ${treeView.selectedNode}'); } - static function onHideToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - static function buildToolboxMetadataLayout(state:ChartEditorState):Null { var toolbox:ChartEditorBaseToolbox = ChartEditorMetadataToolbox.build(state); @@ -597,7 +454,14 @@ class ChartEditorToolboxHandler return toolbox; } - static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} + static function buildToolboxEventDataLayout(state:ChartEditorState):Null + { + var toolbox:ChartEditorBaseToolbox = ChartEditorEventDataToolbox.build(state); + + if (toolbox == null) return null; + + return toolbox; + } static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Null { diff --git a/source/funkin/ui/debug/charting/import.hx b/source/funkin/ui/debug/charting/import.hx index 933eaa3a5..b0569e3bb 100644 --- a/source/funkin/ui/debug/charting/import.hx +++ b/source/funkin/ui/debug/charting/import.hx @@ -3,6 +3,7 @@ package funkin.ui.debug.charting; #if !macro // Apply handlers so they can be called as though they were functions in ChartEditorState using funkin.ui.debug.charting.handlers.ChartEditorAudioHandler; +using funkin.ui.debug.charting.handlers.ChartEditorContextMenuHandler; using funkin.ui.debug.charting.handlers.ChartEditorDialogHandler; using funkin.ui.debug.charting.handlers.ChartEditorImportExportHandler; using funkin.ui.debug.charting.handlers.ChartEditorNotificationHandler; diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx new file mode 100644 index 000000000..480873bc5 --- /dev/null +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx @@ -0,0 +1,259 @@ +package funkin.ui.debug.charting.toolboxes; + +import funkin.play.character.BaseCharacter.CharacterType; +import funkin.play.character.CharacterData; +import funkin.play.stage.StageData; +import funkin.play.event.SongEvent; +import funkin.data.event.SongEventSchema; +import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand; +import funkin.ui.debug.charting.util.ChartEditorDropdowns; +import haxe.ui.components.Button; +import haxe.ui.components.CheckBox; +import haxe.ui.components.DropDown; +import haxe.ui.components.HorizontalSlider; +import haxe.ui.components.Label; +import haxe.ui.components.NumberStepper; +import haxe.ui.components.Slider; +import haxe.ui.core.Component; +import funkin.data.event.SongEventRegistry; +import haxe.ui.components.TextField; +import haxe.ui.containers.Box; +import haxe.ui.containers.Frame; +import haxe.ui.events.UIEvent; +import haxe.ui.data.ArrayDataSource; +import haxe.ui.containers.Grid; +import haxe.ui.components.DropDown; +import haxe.ui.containers.Frame; + +/** + * The toolbox which allows modifying information like Song Title, Scroll Speed, Characters/Stages, and starting BPM. + */ +// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros. +@:access(funkin.ui.debug.charting.ChartEditorState) +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/event-data.xml")) +class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox +{ + var toolboxEventsEventKind:DropDown; + var toolboxEventsDataFrame:Frame; + var toolboxEventsDataGrid:Grid; + + var _initializing:Bool = true; + + public function new(chartEditorState2:ChartEditorState) + { + super(chartEditorState2); + + initialize(); + + this.onDialogClosed = onClose; + + this._initializing = false; + } + + function onClose(event:UIEvent) + { + chartEditorState.menubarItemToggleToolboxEventData.selected = false; + } + + function initialize():Void + { + toolboxEventsEventKind.dataSource = new ArrayDataSource(); + + var songEvents:Array = SongEventRegistry.listEvents(); + + for (event in songEvents) + { + toolboxEventsEventKind.dataSource.add({text: event.getTitle(), value: event.id}); + } + + toolboxEventsEventKind.onChange = function(event:UIEvent) { + var eventType:String = event.data.value; + + trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event type changed: $eventType'); + + // Edit the event data to place. + chartEditorState.eventKindToPlace = eventType; + + var schema:SongEventSchema = SongEventRegistry.getEventSchema(eventType); + + if (schema == null) + { + trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Unknown event kind: $eventType'); + return; + } + + buildEventDataFormFromSchema(toolboxEventsDataGrid, schema); + + if (!_initializing && chartEditorState.currentEventSelection.length > 0) + { + // Edit the event data of any selected events. + for (event in chartEditorState.currentEventSelection) + { + event.event = chartEditorState.eventKindToPlace; + event.value = chartEditorState.eventDataToPlace; + } + chartEditorState.saveDataDirty = true; + chartEditorState.noteDisplayDirty = true; + chartEditorState.notePreviewDirty = true; + } + } + toolboxEventsEventKind.value = chartEditorState.eventKindToPlace; + } + + public override function refresh():Void + { + super.refresh(); + + toolboxEventsEventKind.value = chartEditorState.eventKindToPlace; + + for (pair in chartEditorState.eventDataToPlace.keyValueIterator()) + { + var fieldId:String = pair.key; + var value:Null = pair.value; + + var field:Component = toolboxEventsDataGrid.findComponent(fieldId); + + if (field == null) + { + throw 'ChartEditorToolboxHandler.refresh() - Field "${fieldId}" does not exist in the event data form.'; + } + else + { + switch (field) + { + case Std.isOfType(_, NumberStepper) => true: + var numberStepper:NumberStepper = cast field; + numberStepper.value = value; + case Std.isOfType(_, CheckBox) => true: + var checkBox:CheckBox = cast field; + checkBox.selected = value; + case Std.isOfType(_, DropDown) => true: + var dropDown:DropDown = cast field; + dropDown.value = value; + case Std.isOfType(_, TextField) => true: + var textField:TextField = cast field; + textField.text = value; + default: + throw 'ChartEditorToolboxHandler.refresh() - Field "${fieldId}" is of unknown type "${Type.getClassName(Type.getClass(field))}".'; + } + } + } + } + + function buildEventDataFormFromSchema(target:Box, schema:SongEventSchema):Void + { + trace(schema); + // Clear the frame. + target.removeAllComponents(); + + chartEditorState.eventDataToPlace = {}; + + for (field in schema) + { + if (field == null) continue; + + // Add a label for the data field. + var label:Label = new Label(); + label.text = field.title; + label.verticalAlign = "center"; + target.addComponent(label); + + // Add an input field for the data field. + var input:Component; + switch (field.type) + { + case INTEGER: + var numberStepper:NumberStepper = new NumberStepper(); + numberStepper.id = field.name; + numberStepper.step = field.step ?? 1.0; + numberStepper.min = field.min ?? 0.0; + numberStepper.max = field.max ?? 10.0; + if (field.defaultValue != null) numberStepper.value = field.defaultValue; + input = numberStepper; + case FLOAT: + var numberStepper:NumberStepper = new NumberStepper(); + numberStepper.id = field.name; + numberStepper.step = field.step ?? 0.1; + if (field.min != null) numberStepper.min = field.min; + if (field.max != null) numberStepper.max = field.max; + if (field.defaultValue != null) numberStepper.value = field.defaultValue; + input = numberStepper; + case BOOL: + var checkBox:CheckBox = new CheckBox(); + checkBox.id = field.name; + if (field.defaultValue != null) checkBox.selected = field.defaultValue; + input = checkBox; + case ENUM: + var dropDown:DropDown = new DropDown(); + dropDown.id = field.name; + dropDown.width = 200.0; + dropDown.dataSource = new ArrayDataSource(); + + if (field.keys == null) throw 'Field "${field.name}" is of Enum type but has no keys.'; + + // Add entries to the dropdown. + + for (optionName in field.keys.keys()) + { + var optionValue:Null = field.keys.get(optionName); + trace('$optionName : $optionValue'); + dropDown.dataSource.add({value: optionValue, text: optionName}); + } + + dropDown.value = field.defaultValue; + + input = dropDown; + case STRING: + input = new TextField(); + input.id = field.name; + if (field.defaultValue != null) input.text = field.defaultValue; + default: + // Unknown type. Display a label that proclaims the type so we can debug it. + input = new Label(); + input.id = field.name; + input.text = field.type; + } + + target.addComponent(input); + + // Update the value of the event data. + input.onChange = function(event:UIEvent) { + var value = event.target.value; + if (field.type == ENUM) + { + value = event.target.value.value; + } + + trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${value}'); + + // Edit the event data to place. + if (value == null) + { + chartEditorState.eventDataToPlace.remove(event.target.id); + } + else + { + chartEditorState.eventDataToPlace.set(event.target.id, value); + } + + // Edit the event data of any existing events. + if (!_initializing && chartEditorState.currentEventSelection.length > 0) + { + for (event in chartEditorState.currentEventSelection) + { + event.event = chartEditorState.eventKindToPlace; + event.value = chartEditorState.eventDataToPlace; + } + chartEditorState.saveDataDirty = true; + chartEditorState.noteDisplayDirty = true; + chartEditorState.notePreviewDirty = true; + } + } + } + } + + public static function build(chartEditorState:ChartEditorState):ChartEditorEventDataToolbox + { + return new ChartEditorEventDataToolbox(chartEditorState); + } +} diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx index bc9384cf3..98aa02151 100644 --- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx +++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx @@ -116,13 +116,33 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox } }; + inputTimeSignature.onChange = function(event:UIEvent) { + var timeSignatureStr:String = event.data.text; + var timeSignature = timeSignatureStr.split('/'); + if (timeSignature.length != 2) return; + + var timeSignatureNum:Int = Std.parseInt(timeSignature[0]); + var timeSignatureDen:Int = Std.parseInt(timeSignature[1]); + + var previousTimeSignatureNum:Int = chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum; + var previousTimeSignatureDen:Int = chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen; + if (timeSignatureNum == previousTimeSignatureNum && timeSignatureDen == previousTimeSignatureDen) return; + + chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum = timeSignatureNum; + chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen = timeSignatureDen; + + trace('Time signature changed to ${timeSignatureNum}/${timeSignatureDen}'); + + chartEditorState.updateTimeSignature(); + }; + inputOffsetInst.onChange = function(event:UIEvent) { if (event.value == null) return; chartEditorState.currentInstrumentalOffset = event.value; - Conductor.instrumentalOffset = event.value; + Conductor.instance.instrumentalOffset = event.value; // Update song length. - chartEditorState.songLengthInMs = (chartEditorState.audioInstTrack?.length ?? 1000.0) + Conductor.instrumentalOffset; + chartEditorState.songLengthInMs = (chartEditorState.audioInstTrack?.length ?? 1000.0) + Conductor.instance.instrumentalOffset; }; inputOffsetVocal.onChange = function(event:UIEvent) { @@ -162,6 +182,8 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox public override function refresh():Void { + super.refresh(); + inputSongName.value = chartEditorState.currentSongMetadata.songName; inputSongArtist.value = chartEditorState.currentSongMetadata.artist; inputStage.value = chartEditorState.currentSongMetadata.playData.stage; @@ -172,6 +194,10 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}'; frameDifficulty.text = 'Difficulty: ${chartEditorState.selectedDifficulty.toTitleCase()}'; + var currentTimeSignature = '${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum}/${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen}'; + trace('Setting time signature to ${currentTimeSignature}'); + inputTimeSignature.value = {id: currentTimeSignature, text: currentTimeSignature}; + var stageId:String = chartEditorState.currentSongMetadata.playData.stage; var stageData:Null = StageDataParser.parseStageData(stageId); if (inputStage != null) diff --git a/source/funkin/ui/debug/latency/LatencyState.hx b/source/funkin/ui/debug/latency/LatencyState.hx index 18b0010b2..70ef97fd0 100644 --- a/source/funkin/ui/debug/latency/LatencyState.hx +++ b/source/funkin/ui/debug/latency/LatencyState.hx @@ -75,7 +75,7 @@ class LatencyState extends MusicBeatSubState // funnyStatsGraph.hi - Conductor.forceBPM(60); + Conductor.instance.forceBPM(60); noteGrp = new FlxTypedGroup(); add(noteGrp); @@ -91,14 +91,14 @@ class LatencyState extends MusicBeatSubState // // musSpec.visType = FREQUENCIES; // add(musSpec); - for (beat in 0...Math.floor(FlxG.sound.music.length / Conductor.beatLengthMs)) + for (beat in 0...Math.floor(FlxG.sound.music.length / Conductor.instance.beatLengthMs)) { - var beatTick:FlxSprite = new FlxSprite(songPosToX(beat * Conductor.beatLengthMs), FlxG.height - 15); + var beatTick:FlxSprite = new FlxSprite(songPosToX(beat * Conductor.instance.beatLengthMs), FlxG.height - 15); beatTick.makeGraphic(2, 15); beatTick.alpha = 0.3; add(beatTick); - var offsetTxt:FlxText = new FlxText(songPosToX(beat * Conductor.beatLengthMs), FlxG.height - 26, 0, "swag"); + var offsetTxt:FlxText = new FlxText(songPosToX(beat * Conductor.instance.beatLengthMs), FlxG.height - 26, 0, "swag"); offsetTxt.alpha = 0.5; diffGrp.add(offsetTxt); @@ -130,7 +130,7 @@ class LatencyState extends MusicBeatSubState for (i in 0...32) { - var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), Conductor.beatLengthMs * i); + var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), Conductor.instance.beatLengthMs * i); noteGrp.add(note); } @@ -146,9 +146,9 @@ class LatencyState extends MusicBeatSubState override function stepHit():Bool { - if (Conductor.currentStep % 4 == 2) + if (Conductor.instance.currentStep % 4 == 2) { - blocks.members[((Conductor.currentBeat % 8) + 1) % 8].alpha = 0.5; + blocks.members[((Conductor.instance.currentBeat % 8) + 1) % 8].alpha = 0.5; } return super.stepHit(); @@ -156,11 +156,11 @@ class LatencyState extends MusicBeatSubState override function beatHit():Bool { - if (Conductor.currentBeat % 8 == 0) blocks.forEach(blok -> { + if (Conductor.instance.currentBeat % 8 == 0) blocks.forEach(blok -> { blok.alpha = 0; }); - blocks.members[Conductor.currentBeat % 8].alpha = 1; + blocks.members[Conductor.instance.currentBeat % 8].alpha = 1; // block.visible = !block.visible; return super.beatHit(); @@ -192,17 +192,17 @@ class LatencyState extends MusicBeatSubState if (FlxG.keys.pressed.D) FlxG.sound.music.time += 1000 * FlxG.elapsed; - Conductor.update(swagSong.getTimeWithDiff() - Conductor.inputOffset); - // Conductor.songPosition += (Timer.stamp() * 1000) - FlxG.sound.music.prevTimestamp; + Conductor.instance.update(swagSong.getTimeWithDiff() - Conductor.instance.inputOffset); + // Conductor.instance.songPosition += (Timer.stamp() * 1000) - FlxG.sound.music.prevTimestamp; - songPosVis.x = songPosToX(Conductor.songPosition); - songVisFollowAudio.x = songPosToX(Conductor.songPosition - Conductor.instrumentalOffset); - songVisFollowVideo.x = songPosToX(Conductor.songPosition - Conductor.inputOffset); + songPosVis.x = songPosToX(Conductor.instance.songPosition); + songVisFollowAudio.x = songPosToX(Conductor.instance.songPosition - Conductor.instance.instrumentalOffset); + songVisFollowVideo.x = songPosToX(Conductor.instance.songPosition - Conductor.instance.inputOffset); - offsetText.text = "INST Offset: " + Conductor.instrumentalOffset + "ms"; - offsetText.text += "\nINPUT Offset: " + Conductor.inputOffset + "ms"; - offsetText.text += "\ncurrentStep: " + Conductor.currentStep; - offsetText.text += "\ncurrentBeat: " + Conductor.currentBeat; + offsetText.text = "INST Offset: " + Conductor.instance.instrumentalOffset + "ms"; + offsetText.text += "\nINPUT Offset: " + Conductor.instance.inputOffset + "ms"; + offsetText.text += "\ncurrentStep: " + Conductor.instance.currentStep; + offsetText.text += "\ncurrentBeat: " + Conductor.instance.currentBeat; var avgOffsetInput:Float = 0; @@ -221,24 +221,24 @@ class LatencyState extends MusicBeatSubState { if (FlxG.keys.justPressed.RIGHT) { - Conductor.instrumentalOffset += 1.0 * multiply; + Conductor.instance.instrumentalOffset += 1.0 * multiply; } if (FlxG.keys.justPressed.LEFT) { - Conductor.instrumentalOffset -= 1.0 * multiply; + Conductor.instance.instrumentalOffset -= 1.0 * multiply; } } else { if (FlxG.keys.justPressed.RIGHT) { - Conductor.inputOffset += 1.0 * multiply; + Conductor.instance.inputOffset += 1.0 * multiply; } if (FlxG.keys.justPressed.LEFT) { - Conductor.inputOffset -= 1.0 * multiply; + Conductor.instance.inputOffset -= 1.0 * multiply; } } @@ -250,7 +250,7 @@ class LatencyState extends MusicBeatSubState }*/ noteGrp.forEach(function(daNote:NoteSprite) { - daNote.y = (strumLine.y - ((Conductor.songPosition - Conductor.instrumentalOffset) - daNote.noteData.time) * 0.45); + daNote.y = (strumLine.y - ((Conductor.instance.songPosition - Conductor.instance.instrumentalOffset) - daNote.noteData.time) * 0.45); daNote.x = strumLine.x + 30; if (daNote.y < strumLine.y) daNote.alpha = 0.5; @@ -258,7 +258,7 @@ class LatencyState extends MusicBeatSubState if (daNote.y < 0 - daNote.height) { daNote.alpha = 1; - // daNote.data.strumTime += Conductor.beatLengthMs * 8; + // daNote.data.strumTime += Conductor.instance.beatLengthMs * 8; } }); @@ -267,14 +267,14 @@ class LatencyState extends MusicBeatSubState function generateBeatStuff() { - Conductor.update(swagSong.getTimeWithDiff()); + Conductor.instance.update(swagSong.getTimeWithDiff()); - var closestBeat:Int = Math.round(Conductor.songPosition / Conductor.beatLengthMs) % diffGrp.members.length; - var getDiff:Float = Conductor.songPosition - (closestBeat * Conductor.beatLengthMs); - getDiff -= Conductor.inputOffset; + var closestBeat:Int = Math.round(Conductor.instance.songPosition / Conductor.instance.beatLengthMs) % diffGrp.members.length; + var getDiff:Float = Conductor.instance.songPosition - (closestBeat * Conductor.instance.beatLengthMs); + getDiff -= Conductor.instance.inputOffset; // lil fix for end of song - if (closestBeat == 0 && getDiff >= Conductor.beatLengthMs * 2) getDiff -= FlxG.sound.music.length; + if (closestBeat == 0 && getDiff >= Conductor.instance.beatLengthMs * 2) getDiff -= FlxG.sound.music.length; trace("\tDISTANCE TO CLOSEST BEAT: " + getDiff + "ms"); trace("\tCLOSEST BEAT: " + closestBeat); diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 6e4cdacaf..d6dd536f7 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -238,7 +238,7 @@ class StoryMenuState extends MusicBeatState var freakyMenuMetadata:Null = SongRegistry.instance.parseMusicData('freakyMenu'); if (freakyMenuMetadata != null) { - Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges); + Conductor.instance.mapTimeChanges(freakyMenuMetadata.timeChanges); } FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0); @@ -317,7 +317,7 @@ class StoryMenuState extends MusicBeatState override function update(elapsed:Float) { - Conductor.update(); + Conductor.instance.update(); highScoreLerp = Std.int(MathUtil.coolLerp(highScoreLerp, highScore, 0.5)); diff --git a/source/funkin/ui/title/TitleState.hx b/source/funkin/ui/title/TitleState.hx index 7671bb336..bc44af073 100644 --- a/source/funkin/ui/title/TitleState.hx +++ b/source/funkin/ui/title/TitleState.hx @@ -221,7 +221,7 @@ class TitleState extends MusicBeatState var freakyMenuMetadata:Null = SongRegistry.instance.parseMusicData('freakyMenu'); if (freakyMenuMetadata != null) { - Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges); + Conductor.instance.mapTimeChanges(freakyMenuMetadata.timeChanges); } FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0); FlxG.sound.music.fadeIn(4, 0, 0.7); @@ -256,7 +256,7 @@ class TitleState extends MusicBeatState if (FlxG.keys.pressed.DOWN) FlxG.sound.music.pitch -= 0.5 * elapsed; #end - Conductor.update(); + Conductor.instance.update(); /* if (FlxG.onMobile) { @@ -280,7 +280,7 @@ class TitleState extends MusicBeatState FlxTween.tween(FlxG.stage.window, {y: FlxG.stage.window.y + 100}, 0.7, {ease: FlxEase.quadInOut, type: PINGPONG}); } - if (FlxG.sound.music != null) Conductor.update(FlxG.sound.music.time); + if (FlxG.sound.music != null) Conductor.instance.update(FlxG.sound.music.time); if (FlxG.keys.justPressed.F) FlxG.fullscreen = !FlxG.fullscreen; // do controls.PAUSE | controls.ACCEPT instead? @@ -390,7 +390,7 @@ class TitleState extends MusicBeatState var spec:SpectogramSprite = new SpectogramSprite(FlxG.sound.music); add(spec); - Conductor.forceBPM(190); + Conductor.instance.forceBPM(190); FlxG.camera.flash(FlxColor.WHITE, 1); FlxG.sound.play(Paths.sound('confirmMenu'), 0.7); } @@ -442,13 +442,13 @@ class TitleState extends MusicBeatState if (!skippedIntro) { - // FlxG.log.add(Conductor.currentBeat); + // FlxG.log.add(Conductor.instance.currentBeat); // if the user is draggin the window some beats will // be missed so this is just to compensate - if (Conductor.currentBeat > lastBeat) + if (Conductor.instance.currentBeat > lastBeat) { // TODO: Why does it perform ALL the previous steps each beat? - for (i in lastBeat...Conductor.currentBeat) + for (i in lastBeat...Conductor.instance.currentBeat) { switch (i + 1) { @@ -483,11 +483,11 @@ class TitleState extends MusicBeatState } } } - lastBeat = Conductor.currentBeat; + lastBeat = Conductor.instance.currentBeat; } if (skippedIntro) { - if (cheatActive && Conductor.currentBeat % 2 == 0) swagShader.update(0.125); + if (cheatActive && Conductor.instance.currentBeat % 2 == 0) swagShader.update(0.125); if (logoBl != null && logoBl.animation != null) logoBl.animation.play('bump', true); diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 123267a49..197fa28e8 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -70,7 +70,7 @@ class Constants public static final URL_KICKSTARTER:String = 'https://www.kickstarter.com/projects/funkin/friday-night-funkin-the-full-ass-game/'; /** - * GIT REPO DATA + * REPOSITORY DATA */ // ============================== @@ -86,6 +86,11 @@ class Constants public static final GIT_HASH:String = funkin.util.macro.GitCommit.getGitCommitHash(); #end + /** + * The current library versions, as provided by hmm. + */ + public static final LIBRARY_VERSIONS:Array = funkin.util.macro.HaxelibVersions.getLibraryVersions(); + /** * COLORS */ diff --git a/source/funkin/util/logging/CrashHandler.hx b/source/funkin/util/logging/CrashHandler.hx index a21732048..e32ba2c42 100644 --- a/source/funkin/util/logging/CrashHandler.hx +++ b/source/funkin/util/logging/CrashHandler.hx @@ -123,6 +123,17 @@ class CrashHandler fullContents += '=====================\n'; + fullContents += 'Haxelibs: \n'; + + for (lib in Constants.LIBRARY_VERSIONS) + { + fullContents += '- ${lib}\n'; + } + + fullContents += '\n'; + + fullContents += '=====================\n'; + fullContents += '\n'; fullContents += message; diff --git a/source/funkin/util/macro/HaxelibVersions.hx b/source/funkin/util/macro/HaxelibVersions.hx new file mode 100644 index 000000000..f0317c397 --- /dev/null +++ b/source/funkin/util/macro/HaxelibVersions.hx @@ -0,0 +1,67 @@ +package funkin.util.macro; + +import haxe.io.Path; + +class HaxelibVersions +{ + public static macro function getLibraryVersions():haxe.macro.Expr.ExprOf> + { + #if !display + return macro $v{formatHmmData(readHmmData())}; + #else + // `#if display` is used for code completion. In this case returning an + // empty string is good enough; We don't want to call functions on every hint. + var commitHash:String = ""; + return macro $v{commitHashSplice}; + #end + } + + #if (debug && macro) + static function readHmmData():hmm.HmmConfig + { + return hmm.HmmConfig.HmmConfigs.readHmmJsonOrThrow(); + } + + static function formatHmmData(hmmData:hmm.HmmConfig):Array + { + var result:Array = []; + + for (library in hmmData.dependencies) + { + switch (library) + { + case Haxelib(name, version): + result.push('${name} haxelib(${o(version)})'); + case Git(name, url, ref, dir): + result.push('${name} git(${url}/${o(dir, '')}:${o(ref)})'); + case Mercurial(name, url, ref, dir): + result.push('${name} mercurial(${url}/${o(dir, '')}:${o(ref)})'); + case Dev(name, path): + result.push('${name} dev(${path})'); + } + } + + return result; + } + + static function o(option:haxe.ds.Option, defaultValue:String = 'None'):String + { + switch (option) + { + case Some(value): + return value; + case None: + return defaultValue; + } + } + + static function readLibraryCurrentVersion(libraryName:String):String + { + var path = Path.join([Path.addTrailingSlash(Sys.getCwd()), '.haxelib', libraryName, '.current']); + // This is compile time so we should always have Sys available. + var result = sys.io.File.getContent(path); + + return result; + } + #end +} diff --git a/source/funkin/util/plugins/EvacuateDebugPlugin.hx b/source/funkin/util/plugins/EvacuateDebugPlugin.hx new file mode 100644 index 000000000..1803c25ba --- /dev/null +++ b/source/funkin/util/plugins/EvacuateDebugPlugin.hx @@ -0,0 +1,35 @@ +package funkin.util.plugins; + +import flixel.FlxBasic; + +/** + * A plugin which adds functionality to press `F4` to immediately transition to the main menu. + * This is useful for debugging or if you get softlocked or something. + */ +class EvacuateDebugPlugin extends FlxBasic +{ + public function new() + { + super(); + } + + public static function initialize():Void + { + FlxG.plugins.addPlugin(new EvacuateDebugPlugin()); + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (FlxG.keys.justPressed.F4) + { + FlxG.switchState(new funkin.ui.mainmenu.MainMenuState()); + } + } + + public override function destroy():Void + { + super.destroy(); + } +} diff --git a/source/funkin/util/plugins/README.md b/source/funkin/util/plugins/README.md new file mode 100644 index 000000000..fe87d36e5 --- /dev/null +++ b/source/funkin/util/plugins/README.md @@ -0,0 +1,5 @@ +# funkin.util.plugins + +Flixel plugins are objects with `update()` functions that are called from every state. + +See: https://github.com/HaxeFlixel/flixel/blob/dev/flixel/system/frontEnds/PluginFrontEnd.hx diff --git a/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx b/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx new file mode 100644 index 000000000..a43317cce --- /dev/null +++ b/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx @@ -0,0 +1,38 @@ +package funkin.util.plugins; + +import flixel.FlxBasic; + +/** + * A plugin which adds functionality to press `F5` to reload all game assets, then reload the current state. + * This is useful for hot reloading assets during development. + */ +class ReloadAssetsDebugPlugin extends FlxBasic +{ + public function new() + { + super(); + } + + public static function initialize():Void + { + FlxG.plugins.addPlugin(new ReloadAssetsDebugPlugin()); + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (FlxG.keys.justPressed.F5) + { + funkin.modding.PolymodHandler.forceReloadAssets(); + + // Create a new instance of the current state, so old data is cleared. + FlxG.resetState(); + } + } + + public override function destroy():Void + { + super.destroy(); + } +} diff --git a/source/funkin/util/plugins/WatchPlugin.hx b/source/funkin/util/plugins/WatchPlugin.hx new file mode 100644 index 000000000..17b2dd129 --- /dev/null +++ b/source/funkin/util/plugins/WatchPlugin.hx @@ -0,0 +1,38 @@ +package funkin.util.plugins; + +import flixel.FlxBasic; + +/** + * A plugin which adds functionality to display several universally important values + * in the Flixel variable watch window. + */ +class WatchPlugin extends FlxBasic +{ + public function new() + { + super(); + } + + public static function initialize():Void + { + FlxG.plugins.addPlugin(new WatchPlugin()); + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + FlxG.watch.addQuick("songPosition", Conductor.instance.songPosition); + FlxG.watch.addQuick("songPositionNoOffset", Conductor.instance.songPosition + Conductor.instance.instrumentalOffset); + FlxG.watch.addQuick("musicTime", FlxG.sound?.music?.time ?? 0.0); + FlxG.watch.addQuick("bpm", Conductor.instance.bpm); + FlxG.watch.addQuick("currentMeasureTime", Conductor.instance.currentMeasureTime); + FlxG.watch.addQuick("currentBeatTime", Conductor.instance.currentBeatTime); + FlxG.watch.addQuick("currentStepTime", Conductor.instance.currentStepTime); + } + + public override function destroy():Void + { + super.destroy(); + } +} diff --git a/source/funkin/util/tools/ArrayTools.hx b/source/funkin/util/tools/ArrayTools.hx index 925bb67a5..0209cfc19 100644 --- a/source/funkin/util/tools/ArrayTools.hx +++ b/source/funkin/util/tools/ArrayTools.hx @@ -77,6 +77,22 @@ class ArrayTools array.pop(); } + /** + * Create a new array with all elements of the given array, to prevent modifying the original. + */ + public static function clone(array:Array):Array + { + return [for (element in array) element]; + } + + /** + * Create a new array with clones of all elements of the given array, to prevent modifying the original. + */ + public static function deepClone>(array:Array):Array + { + return [for (element in array) element.clone()]; + } + /** * Return true only if both arrays contain the same elements (possibly in a different order). * @param a The first array to compare. diff --git a/source/funkin/util/tools/ICloneable.hx b/source/funkin/util/tools/ICloneable.hx new file mode 100644 index 000000000..33f19f167 --- /dev/null +++ b/source/funkin/util/tools/ICloneable.hx @@ -0,0 +1,10 @@ +package funkin.util.tools; + +/** + * Implement this on a class to enable `Array.deepClone()` to work on it. + * NOTE: T should be the type of the class that implements this interface. + */ +interface ICloneable +{ + public function clone():T; +} diff --git a/source/funkin/util/tools/MapTools.hx b/source/funkin/util/tools/MapTools.hx index 739c5efdb..1399fb791 100644 --- a/source/funkin/util/tools/MapTools.hx +++ b/source/funkin/util/tools/MapTools.hx @@ -25,6 +25,33 @@ class MapTools return [for (i in map.iterator()) i]; } + /** + * Create a new array with all elements of the given array, to prevent modifying the original. + */ + public static function clone(map:Map):Map + { + return map.copy(); + } + + /** + * Create a new array with clones of all elements of the given array, to prevent modifying the original. + */ + public static function deepClone>(map:Map):Map + { + // TODO: This function does NOT work. + throw "Not implemented"; + + /* + var newMap:Map = []; + // Replace each value with a clone of itself. + for (key in newMap.keys()) + { + newMap.set(key, newMap.get(key).clone()); + } + return newMap; + */ + } + /** * Return a list of keys from the map (as an array, rather than an iterator). * TODO: Rename this? diff --git a/tests/unit/source/funkin/ConductorTest.hx b/tests/unit/source/funkin/ConductorTest.hx index c65f3f297..a0cfedbab 100644 --- a/tests/unit/source/funkin/ConductorTest.hx +++ b/tests/unit/source/funkin/ConductorTest.hx @@ -31,23 +31,23 @@ class ConductorTest extends FunkinTest { // NOTE: Expected value comes first. - Assert.areEqual([], Conductor.timeChanges); - Assert.areEqual(null, Conductor.currentTimeChange); + Assert.areEqual([], Conductor.instance.timeChanges); + Assert.areEqual(null, Conductor.instance.currentTimeChange); - Assert.areEqual(0, Conductor.songPosition); - Assert.areEqual(Constants.DEFAULT_BPM, Conductor.bpm); - Assert.areEqual(null, Conductor.bpmOverride); + Assert.areEqual(0, Conductor.instance.songPosition); + Assert.areEqual(Constants.DEFAULT_BPM, Conductor.instance.bpm); + Assert.areEqual(null, Conductor.instance.bpmOverride); - Assert.areEqual(600, Conductor.beatLengthMs); + Assert.areEqual(600, Conductor.instance.beatLengthMs); - Assert.areEqual(4, Conductor.timeSignatureNumerator); - Assert.areEqual(4, Conductor.timeSignatureDenominator); + Assert.areEqual(4, Conductor.instance.timeSignatureNumerator); + Assert.areEqual(4, Conductor.instance.timeSignatureDenominator); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(0, Conductor.currentStep); - Assert.areEqual(0.0, Conductor.currentStepTime); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(0, Conductor.instance.currentStep); + Assert.areEqual(0.0, Conductor.instance.currentStepTime); - Assert.areEqual(150, Conductor.stepLengthMs); + Assert.areEqual(150, Conductor.instance.stepLengthMs); } /** @@ -60,23 +60,23 @@ class ConductorTest extends FunkinTest var currentConductorState:Null = conductorState; Assert.isNotNull(currentConductorState); - Assert.areEqual(0, Conductor.songPosition); + Assert.areEqual(0, Conductor.instance.songPosition); step(); // 1 var BPM_100_STEP_TIME = 1 / 9; - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.songPosition); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(0, Conductor.currentStep); - FunkinAssert.areNear(1 / 9, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.instance.songPosition); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(0, Conductor.instance.currentStep); + FunkinAssert.areNear(1 / 9, Conductor.instance.currentStepTime); step(7); // 8 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 8, Conductor.songPosition); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(0, Conductor.currentStep); - FunkinAssert.areNear(8 / 9, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 8, Conductor.instance.songPosition); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(0, Conductor.instance.currentStep); + FunkinAssert.areNear(8 / 9, Conductor.instance.currentStepTime); Assert.areEqual(0, currentConductorState.beatsHit); Assert.areEqual(0, currentConductorState.stepsHit); @@ -88,10 +88,10 @@ class ConductorTest extends FunkinTest currentConductorState.beatsHit = 0; currentConductorState.stepsHit = 0; - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 9, Conductor.songPosition); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(1, Conductor.currentStep); - FunkinAssert.areNear(1.0, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 9, Conductor.instance.songPosition); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(1, Conductor.instance.currentStep); + FunkinAssert.areNear(1.0, Conductor.instance.currentStepTime); step(35 - 9); // 35 @@ -100,10 +100,10 @@ class ConductorTest extends FunkinTest currentConductorState.beatsHit = 0; currentConductorState.stepsHit = 0; - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 35, Conductor.songPosition); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(3, Conductor.currentStep); - FunkinAssert.areNear(3.0 + 8 / 9, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 35, Conductor.instance.songPosition); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(3, Conductor.instance.currentStep); + FunkinAssert.areNear(3.0 + 8 / 9, Conductor.instance.currentStepTime); step(); // 36 @@ -112,83 +112,83 @@ class ConductorTest extends FunkinTest currentConductorState.beatsHit = 0; currentConductorState.stepsHit = 0; - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 36, Conductor.songPosition); - Assert.areEqual(1, Conductor.currentBeat); - Assert.areEqual(4, Conductor.currentStep); - FunkinAssert.areNear(4.0, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 36, Conductor.instance.songPosition); + Assert.areEqual(1, Conductor.instance.currentBeat); + Assert.areEqual(4, Conductor.instance.currentStep); + FunkinAssert.areNear(4.0, Conductor.instance.currentStepTime); step(50 - 36); // 50 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 50, Conductor.songPosition); - Assert.areEqual(1, Conductor.currentBeat); - Assert.areEqual(5, Conductor.currentStep); - FunkinAssert.areNear(5.555555, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 50, Conductor.instance.songPosition); + Assert.areEqual(1, Conductor.instance.currentBeat); + Assert.areEqual(5, Conductor.instance.currentStep); + FunkinAssert.areNear(5.555555, Conductor.instance.currentStepTime); step(49); // 99 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 99, Conductor.songPosition); - Assert.areEqual(2, Conductor.currentBeat); - Assert.areEqual(11, Conductor.currentStep); - FunkinAssert.areNear(11.0, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 99, Conductor.instance.songPosition); + Assert.areEqual(2, Conductor.instance.currentBeat); + Assert.areEqual(11, Conductor.instance.currentStep); + FunkinAssert.areNear(11.0, Conductor.instance.currentStepTime); step(1); // 100 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 100, Conductor.songPosition); - Assert.areEqual(2, Conductor.currentBeat); - Assert.areEqual(11, Conductor.currentStep); - FunkinAssert.areNear(11.111111, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 100, Conductor.instance.songPosition); + Assert.areEqual(2, Conductor.instance.currentBeat); + Assert.areEqual(11, Conductor.instance.currentStep); + FunkinAssert.areNear(11.111111, Conductor.instance.currentStepTime); } @Test function testUpdateForcedBPM():Void { - Conductor.forceBPM(60); + Conductor.instance.forceBPM(60); - Assert.areEqual(0, Conductor.songPosition); + Assert.areEqual(0, Conductor.instance.songPosition); // 60 beats per minute = 1 beat per second // 1 beat per second = 1/60 beats per frame = 4/60 steps per frame step(); // Advances time 1/60 of 1 second. - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.songPosition); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(0, Conductor.currentStep); - FunkinAssert.areNear(4 / 60, Conductor.currentStepTime); // 1/60 of 1 beat = 4/60 of 1 step + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.instance.songPosition); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(0, Conductor.instance.currentStep); + FunkinAssert.areNear(4 / 60, Conductor.instance.currentStepTime); // 1/60 of 1 beat = 4/60 of 1 step step(14 - 1); // 14 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 14, Conductor.songPosition); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(0, Conductor.currentStep); - FunkinAssert.areNear(1.0 - 4 / 60, Conductor.currentStepTime); // 1/60 of 1 beat = 4/60 of 1 step + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 14, Conductor.instance.songPosition); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(0, Conductor.instance.currentStep); + FunkinAssert.areNear(1.0 - 4 / 60, Conductor.instance.currentStepTime); // 1/60 of 1 beat = 4/60 of 1 step step(); // 15 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 15, Conductor.songPosition); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(1, Conductor.currentStep); - FunkinAssert.areNear(1.0, Conductor.currentStepTime); // 1/60 of 1 beat = 4/60 of 1 step + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 15, Conductor.instance.songPosition); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(1, Conductor.instance.currentStep); + FunkinAssert.areNear(1.0, Conductor.instance.currentStepTime); // 1/60 of 1 beat = 4/60 of 1 step step(45 - 1); // 59 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.songPosition); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(3, Conductor.currentStep); - FunkinAssert.areNear(4.0 - 4 / 60, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.instance.songPosition); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(3, Conductor.instance.currentStep); + FunkinAssert.areNear(4.0 - 4 / 60, Conductor.instance.currentStepTime); step(); // 60 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.songPosition); - Assert.areEqual(1, Conductor.currentBeat); - Assert.areEqual(4, Conductor.currentStep); - FunkinAssert.areNear(4.0, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.instance.songPosition); + Assert.areEqual(1, Conductor.instance.currentBeat); + Assert.areEqual(4, Conductor.instance.currentStep); + FunkinAssert.areNear(4.0, Conductor.instance.currentStepTime); step(); // 61 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.songPosition); - Assert.areEqual(1, Conductor.currentBeat); - Assert.areEqual(4, Conductor.currentStep); - FunkinAssert.areNear(4.0 + 4 / 60, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.instance.songPosition); + Assert.areEqual(1, Conductor.instance.currentBeat); + Assert.areEqual(4, Conductor.instance.currentStep); + FunkinAssert.areNear(4.0 + 4 / 60, Conductor.instance.currentStepTime); } @Test @@ -196,50 +196,50 @@ class ConductorTest extends FunkinTest { // Start the song with a BPM of 120. var songTimeChanges:Array = [new SongTimeChange(0, 120)]; - Conductor.mapTimeChanges(songTimeChanges); + Conductor.instance.mapTimeChanges(songTimeChanges); // All should be at 0. - FunkinAssert.areNear(0, Conductor.songPosition); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(0, Conductor.currentStep); - FunkinAssert.areNear(0.0, Conductor.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step + FunkinAssert.areNear(0, Conductor.instance.songPosition); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(0, Conductor.instance.currentStep); + FunkinAssert.areNear(0.0, Conductor.instance.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step // 120 beats per minute = 2 beat per second // 2 beat per second = 2/60 beats per frame = 16/120 steps per frame step(); // Advances time 1/60 of 1 second. - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.songPosition); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(0, Conductor.currentStep); - FunkinAssert.areNear(16 / 120, Conductor.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.instance.songPosition); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(0, Conductor.instance.currentStep); + FunkinAssert.areNear(16 / 120, Conductor.instance.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step step(15 - 1); // 15 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 15, Conductor.songPosition); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(2, Conductor.currentStep); - FunkinAssert.areNear(2.0, Conductor.currentStepTime); // 2/60 of 1 beat = 8/60 of 1 step + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 15, Conductor.instance.songPosition); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(2, Conductor.instance.currentStep); + FunkinAssert.areNear(2.0, Conductor.instance.currentStepTime); // 2/60 of 1 beat = 8/60 of 1 step step(45 - 1); // 59 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.songPosition); - Assert.areEqual(1, Conductor.currentBeat); - Assert.areEqual(7, Conductor.currentStep); - FunkinAssert.areNear(7.0 + 104 / 120, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.instance.songPosition); + Assert.areEqual(1, Conductor.instance.currentBeat); + Assert.areEqual(7, Conductor.instance.currentStep); + FunkinAssert.areNear(7.0 + 104 / 120, Conductor.instance.currentStepTime); step(); // 60 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.songPosition); - Assert.areEqual(2, Conductor.currentBeat); - Assert.areEqual(8, Conductor.currentStep); - FunkinAssert.areNear(8.0, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.instance.songPosition); + Assert.areEqual(2, Conductor.instance.currentBeat); + Assert.areEqual(8, Conductor.instance.currentStep); + FunkinAssert.areNear(8.0, Conductor.instance.currentStepTime); step(); // 61 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.songPosition); - Assert.areEqual(2, Conductor.currentBeat); - Assert.areEqual(8, Conductor.currentStep); - FunkinAssert.areNear(8.0 + 8 / 60, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.instance.songPosition); + Assert.areEqual(2, Conductor.instance.currentBeat); + Assert.areEqual(8, Conductor.instance.currentStep); + FunkinAssert.areNear(8.0 + 8 / 60, Conductor.instance.currentStepTime); } @Test @@ -247,57 +247,57 @@ class ConductorTest extends FunkinTest { // Start the song with a BPM of 120. var songTimeChanges:Array = [new SongTimeChange(0, 120), new SongTimeChange(3000, 90)]; - Conductor.mapTimeChanges(songTimeChanges); + Conductor.instance.mapTimeChanges(songTimeChanges); // All should be at 0. - FunkinAssert.areNear(0, Conductor.songPosition); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(0, Conductor.currentStep); - FunkinAssert.areNear(0.0, Conductor.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step + FunkinAssert.areNear(0, Conductor.instance.songPosition); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(0, Conductor.instance.currentStep); + FunkinAssert.areNear(0.0, Conductor.instance.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step // 120 beats per minute = 2 beat per second // 2 beat per second = 2/60 beats per frame = 16/120 steps per frame step(); // Advances time 1/60 of 1 second. - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.songPosition); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(0, Conductor.currentStep); - FunkinAssert.areNear(16 / 120, Conductor.currentStepTime); // 4/120 of 1 beat = 16/120 of 1 step + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.instance.songPosition); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(0, Conductor.instance.currentStep); + FunkinAssert.areNear(16 / 120, Conductor.instance.currentStepTime); // 4/120 of 1 beat = 16/120 of 1 step step(60 - 1 - 1); // 59 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.songPosition); - Assert.areEqual(1, Conductor.currentBeat); - Assert.areEqual(7, Conductor.currentStep); - FunkinAssert.areNear(7.0 + 104 / 120, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.instance.songPosition); + Assert.areEqual(1, Conductor.instance.currentBeat); + Assert.areEqual(7, Conductor.instance.currentStep); + FunkinAssert.areNear(7.0 + 104 / 120, Conductor.instance.currentStepTime); step(); // 60 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.songPosition); - Assert.areEqual(2, Conductor.currentBeat); - Assert.areEqual(8, Conductor.currentStep); - FunkinAssert.areNear(8.0, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.instance.songPosition); + Assert.areEqual(2, Conductor.instance.currentBeat); + Assert.areEqual(8, Conductor.instance.currentStep); + FunkinAssert.areNear(8.0, Conductor.instance.currentStepTime); step(); // 61 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.songPosition); - Assert.areEqual(2, Conductor.currentBeat); - Assert.areEqual(8, Conductor.currentStep); - FunkinAssert.areNear(8.0 + 8 / 60, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.instance.songPosition); + Assert.areEqual(2, Conductor.instance.currentBeat); + Assert.areEqual(8, Conductor.instance.currentStep); + FunkinAssert.areNear(8.0 + 8 / 60, Conductor.instance.currentStepTime); step(179 - 61); // 179 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 179, Conductor.songPosition); - Assert.areEqual(5, Conductor.currentBeat); - Assert.areEqual(23, Conductor.currentStep); - FunkinAssert.areNear(23.0 + 52 / 60, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 179, Conductor.instance.songPosition); + Assert.areEqual(5, Conductor.instance.currentBeat); + Assert.areEqual(23, Conductor.instance.currentStep); + FunkinAssert.areNear(23.0 + 52 / 60, Conductor.instance.currentStepTime); step(); // 180 (3 seconds) - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 180, Conductor.songPosition); - Assert.areEqual(6, Conductor.currentBeat); - Assert.areEqual(24, Conductor.currentStep); - FunkinAssert.areNear(24.0, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 180, Conductor.instance.songPosition); + Assert.areEqual(6, Conductor.instance.currentBeat); + Assert.areEqual(24, Conductor.instance.currentStep); + FunkinAssert.areNear(24.0, Conductor.instance.currentStepTime); step(); // 181 (3 + 1/60 seconds) // BPM has switched to 90! @@ -305,24 +305,24 @@ class ConductorTest extends FunkinTest // 1.5 beat per second = 1.5/60 beats per frame = 3/120 beats per frame // = 12/120 steps per frame - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 181, Conductor.songPosition); - Assert.areEqual(6, Conductor.currentBeat); - Assert.areEqual(24, Conductor.currentStep); - FunkinAssert.areNear(24.0 + 12 / 120, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 181, Conductor.instance.songPosition); + Assert.areEqual(6, Conductor.instance.currentBeat); + Assert.areEqual(24, Conductor.instance.currentStep); + FunkinAssert.areNear(24.0 + 12 / 120, Conductor.instance.currentStepTime); step(59); // 240 (4 seconds) - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 240, Conductor.songPosition); - Assert.areEqual(7, Conductor.currentBeat); - Assert.areEqual(30, Conductor.currentStep); - FunkinAssert.areNear(30.0, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 240, Conductor.instance.songPosition); + Assert.areEqual(7, Conductor.instance.currentBeat); + Assert.areEqual(30, Conductor.instance.currentStep); + FunkinAssert.areNear(30.0, Conductor.instance.currentStepTime); step(); // 241 (4 + 1/60 seconds) - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 241, Conductor.songPosition); - Assert.areEqual(7, Conductor.currentBeat); - Assert.areEqual(30, Conductor.currentStep); - FunkinAssert.areNear(30.0 + 12 / 120, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 241, Conductor.instance.songPosition); + Assert.areEqual(7, Conductor.instance.currentBeat); + Assert.areEqual(30, Conductor.instance.currentStep); + FunkinAssert.areNear(30.0 + 12 / 120, Conductor.instance.currentStepTime); } @Test @@ -334,63 +334,63 @@ class ConductorTest extends FunkinTest new SongTimeChange(3000, 90), new SongTimeChange(6000, 180) ]; - Conductor.mapTimeChanges(songTimeChanges); + Conductor.instance.mapTimeChanges(songTimeChanges); // Verify time changes. - Assert.areEqual(3, Conductor.timeChanges.length); - FunkinAssert.areNear(0, Conductor.timeChanges[0].beatTime); - FunkinAssert.areNear(6, Conductor.timeChanges[1].beatTime); - FunkinAssert.areNear(10.5, Conductor.timeChanges[2].beatTime); + Assert.areEqual(3, Conductor.instance.timeChanges.length); + FunkinAssert.areNear(0, Conductor.instance.timeChanges[0].beatTime); + FunkinAssert.areNear(6, Conductor.instance.timeChanges[1].beatTime); + FunkinAssert.areNear(10.5, Conductor.instance.timeChanges[2].beatTime); // All should be at 0. - FunkinAssert.areNear(0, Conductor.songPosition); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(0, Conductor.currentStep); - FunkinAssert.areNear(0.0, Conductor.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step + FunkinAssert.areNear(0, Conductor.instance.songPosition); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(0, Conductor.instance.currentStep); + FunkinAssert.areNear(0.0, Conductor.instance.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step // 120 beats per minute = 2 beat per second // 2 beat per second = 2/60 beats per frame = 16/120 steps per frame step(); // Advances time 1/60 of 1 second. - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.songPosition); - Assert.areEqual(0, Conductor.currentBeat); - Assert.areEqual(0, Conductor.currentStep); - FunkinAssert.areNear(16 / 120, Conductor.currentStepTime); // 4/120 of 1 beat = 16/120 of 1 step + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.instance.songPosition); + Assert.areEqual(0, Conductor.instance.currentBeat); + Assert.areEqual(0, Conductor.instance.currentStep); + FunkinAssert.areNear(16 / 120, Conductor.instance.currentStepTime); // 4/120 of 1 beat = 16/120 of 1 step step(60 - 1 - 1); // 59 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.songPosition); - Assert.areEqual(1, Conductor.currentBeat); - Assert.areEqual(7, Conductor.currentStep); - FunkinAssert.areNear(7 + 104 / 120, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.instance.songPosition); + Assert.areEqual(1, Conductor.instance.currentBeat); + Assert.areEqual(7, Conductor.instance.currentStep); + FunkinAssert.areNear(7 + 104 / 120, Conductor.instance.currentStepTime); step(); // 60 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.songPosition); - Assert.areEqual(2, Conductor.currentBeat); - Assert.areEqual(8, Conductor.currentStep); - FunkinAssert.areNear(8.0, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.instance.songPosition); + Assert.areEqual(2, Conductor.instance.currentBeat); + Assert.areEqual(8, Conductor.instance.currentStep); + FunkinAssert.areNear(8.0, Conductor.instance.currentStepTime); step(); // 61 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.songPosition); - Assert.areEqual(2, Conductor.currentBeat); - Assert.areEqual(8, Conductor.currentStep); - FunkinAssert.areNear(8.0 + 8 / 60, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.instance.songPosition); + Assert.areEqual(2, Conductor.instance.currentBeat); + Assert.areEqual(8, Conductor.instance.currentStep); + FunkinAssert.areNear(8.0 + 8 / 60, Conductor.instance.currentStepTime); step(179 - 61); // 179 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 179, Conductor.songPosition); - Assert.areEqual(5, Conductor.currentBeat); - Assert.areEqual(23, Conductor.currentStep); - FunkinAssert.areNear(23.0 + 52 / 60, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 179, Conductor.instance.songPosition); + Assert.areEqual(5, Conductor.instance.currentBeat); + Assert.areEqual(23, Conductor.instance.currentStep); + FunkinAssert.areNear(23.0 + 52 / 60, Conductor.instance.currentStepTime); step(); // 180 (3 seconds) - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 180, Conductor.songPosition); - Assert.areEqual(6, Conductor.currentBeat); - Assert.areEqual(24, Conductor.currentStep); // 23.999 => 24 - FunkinAssert.areNear(24.0, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 180, Conductor.instance.songPosition); + Assert.areEqual(6, Conductor.instance.currentBeat); + Assert.areEqual(24, Conductor.instance.currentStep); // 23.999 => 24 + FunkinAssert.areNear(24.0, Conductor.instance.currentStepTime); step(); // 181 (3 + 1/60 seconds) // BPM has switched to 90! @@ -398,45 +398,45 @@ class ConductorTest extends FunkinTest // 1.5 beat per second = 1.5/60 beats per frame = 3/120 beats per frame // = 12/120 steps per frame - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 181, Conductor.songPosition); - Assert.areEqual(6, Conductor.currentBeat); - Assert.areEqual(24, Conductor.currentStep); - FunkinAssert.areNear(24.0 + 12 / 120, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 181, Conductor.instance.songPosition); + Assert.areEqual(6, Conductor.instance.currentBeat); + Assert.areEqual(24, Conductor.instance.currentStep); + FunkinAssert.areNear(24.0 + 12 / 120, Conductor.instance.currentStepTime); step(60 - 1 - 1); // 240 (4 seconds) - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 239, Conductor.songPosition); - Assert.areEqual(7, Conductor.currentBeat); - Assert.areEqual(29, Conductor.currentStep); - FunkinAssert.areNear(29.0 + 108 / 120, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 239, Conductor.instance.songPosition); + Assert.areEqual(7, Conductor.instance.currentBeat); + Assert.areEqual(29, Conductor.instance.currentStep); + FunkinAssert.areNear(29.0 + 108 / 120, Conductor.instance.currentStepTime); step(); // 240 (4 seconds) - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 240, Conductor.songPosition); - Assert.areEqual(7, Conductor.currentBeat); - Assert.areEqual(30, Conductor.currentStep); - FunkinAssert.areNear(30.0, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 240, Conductor.instance.songPosition); + Assert.areEqual(7, Conductor.instance.currentBeat); + Assert.areEqual(30, Conductor.instance.currentStep); + FunkinAssert.areNear(30.0, Conductor.instance.currentStepTime); step(); // 241 (4 + 1/60 seconds) - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 241, Conductor.songPosition); - Assert.areEqual(7, Conductor.currentBeat); - Assert.areEqual(30, Conductor.currentStep); - FunkinAssert.areNear(30.0 + 12 / 120, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 241, Conductor.instance.songPosition); + Assert.areEqual(7, Conductor.instance.currentBeat); + Assert.areEqual(30, Conductor.instance.currentStep); + FunkinAssert.areNear(30.0 + 12 / 120, Conductor.instance.currentStepTime); step(359 - 241); // 359 (5 + 59/60 seconds) - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 359, Conductor.songPosition); - Assert.areEqual(10, Conductor.currentBeat); - Assert.areEqual(41, Conductor.currentStep); - FunkinAssert.areNear(41 + 108 / 120, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 359, Conductor.instance.songPosition); + Assert.areEqual(10, Conductor.instance.currentBeat); + Assert.areEqual(41, Conductor.instance.currentStep); + FunkinAssert.areNear(41 + 108 / 120, Conductor.instance.currentStepTime); step(); // 360 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 360, Conductor.songPosition); - Assert.areEqual(10, Conductor.currentBeat); - Assert.areEqual(42, Conductor.currentStep); // 41.999 - FunkinAssert.areNear(42.0, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 360, Conductor.instance.songPosition); + Assert.areEqual(10, Conductor.instance.currentBeat); + Assert.areEqual(42, Conductor.instance.currentStep); // 41.999 + FunkinAssert.areNear(42.0, Conductor.instance.currentStepTime); step(); // 361 // BPM has switched to 180! @@ -444,24 +444,24 @@ class ConductorTest extends FunkinTest // 3 beat per second = 3/60 beats per frame // = 12/60 steps per frame - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 361, Conductor.songPosition); - Assert.areEqual(10, Conductor.currentBeat); - Assert.areEqual(42, Conductor.currentStep); - FunkinAssert.areNear(42.0 + 12 / 60, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 361, Conductor.instance.songPosition); + Assert.areEqual(10, Conductor.instance.currentBeat); + Assert.areEqual(42, Conductor.instance.currentStep); + FunkinAssert.areNear(42.0 + 12 / 60, Conductor.instance.currentStepTime); step(); // 362 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 362, Conductor.songPosition); - Assert.areEqual(10, Conductor.currentBeat); - Assert.areEqual(42, Conductor.currentStep); - FunkinAssert.areNear(42.0 + 24 / 60, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 362, Conductor.instance.songPosition); + Assert.areEqual(10, Conductor.instance.currentBeat); + Assert.areEqual(42, Conductor.instance.currentStep); + FunkinAssert.areNear(42.0 + 24 / 60, Conductor.instance.currentStepTime); step(3); // 365 - FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 365, Conductor.songPosition); - Assert.areEqual(10, Conductor.currentBeat); - Assert.areEqual(43, Conductor.currentStep); // 42.999 => 42 - FunkinAssert.areNear(43.0, Conductor.currentStepTime); + FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 365, Conductor.instance.songPosition); + Assert.areEqual(10, Conductor.instance.currentBeat); + Assert.areEqual(43, Conductor.instance.currentStep); // 42.999 => 42 + FunkinAssert.areNear(43.0, Conductor.instance.currentStepTime); } } @@ -504,6 +504,6 @@ class ConductorState extends FlxState super.update(elapsed); // On each step, increment the Conductor as though the song was playing. - Conductor.update(Conductor.songPosition + elapsed * Constants.MS_PER_SEC); + Conductor.instance.update(Conductor.instance.songPosition + elapsed * Constants.MS_PER_SEC); } }