diff --git a/Project.xml b/Project.xml index a83db1677..ccf6c83a3 100644 --- a/Project.xml +++ b/Project.xml @@ -200,6 +200,12 @@ <postbuild haxe="source/Prebuild.hx"/> --> <postbuild haxe="source/Postbuild.hx"/> --> + <!-- Enable this on platforms which do not support dropping files onto the window. --> + <set name="FILE_DROP_UNSUPPORTED" if="mac" /> + <section unless="FILE_DROP_UNSUPPORTED"> + <set name="FILE_DROP_SUPPORTED" /> + </section> + <!-- Options for Polymod --> <section if="polymod"> <!-- Turns on additional debug logging. --> diff --git a/hmm.json b/hmm.json index e2670420a..f06b295e4 100644 --- a/hmm.json +++ b/hmm.json @@ -49,8 +49,8 @@ "name": "haxeui-core", "type": "git", "dir": null, - "ref": "f5daafe93bdfa957538f199294a54e0476c805b7", - "url": "https://github.com/haxeui/haxeui-core/" + "ref": "e92d5cfac847943fac84696b103670d55c2c774f", + "url": "https://github.com/haxeui/haxeui-core" }, { "name": "haxeui-flixel", @@ -137,7 +137,7 @@ "name": "openfl", "type": "git", "dir": null, - "ref": "ef43deb2c68d8a4bcd73abfbd77324fc8220d0c1", + "ref": "d33d489a137ff8fdece4994cf1302f0b6334ed08", "url": "https://github.com/EliteMasterEric/openfl" }, { diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index 9bd668b69..b0ad6c221 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -47,7 +47,7 @@ class Conductor /** * Beats per minute of the current song at the current time. */ - public static var bpm(get, null):Float; + public static var bpm(get, never):Float; static function get_bpm():Float { @@ -67,7 +67,7 @@ class Conductor /** * Duration of a measure in milliseconds. Calculated based on bpm. */ - public static var measureLengthMs(get, null):Float; + public static var measureLengthMs(get, never):Float; static function get_measureLengthMs():Float { @@ -77,7 +77,7 @@ class Conductor /** * Duration of a beat (quarter note) in milliseconds. Calculated based on bpm. */ - public static var beatLengthMs(get, null):Float; + public static var beatLengthMs(get, never):Float; static function get_beatLengthMs():Float { @@ -88,14 +88,14 @@ class Conductor /** * Duration of a step (sixtennth note) in milliseconds. Calculated based on bpm. */ - public static var stepLengthMs(get, null):Float; + public static var stepLengthMs(get, never):Float; static function get_stepLengthMs():Float { return beatLengthMs / timeSignatureNumerator; } - public static var timeSignatureNumerator(get, null):Int; + public static var timeSignatureNumerator(get, never):Int; static function get_timeSignatureNumerator():Int { @@ -104,7 +104,7 @@ class Conductor return currentTimeChange.timeSignatureNum; } - public static var timeSignatureDenominator(get, null):Int; + public static var timeSignatureDenominator(get, never):Int; static function get_timeSignatureDenominator():Int { @@ -151,7 +151,7 @@ class Conductor public static var audioOffset:Float = 0; public static var offset:Float = 0; - public static var beatsPerMeasure(get, null):Float; + public static var beatsPerMeasure(get, never):Float; static function get_beatsPerMeasure():Float { @@ -159,7 +159,7 @@ class Conductor return stepsPerMeasure / Constants.STEPS_PER_BEAT; } - public static var stepsPerMeasure(get, null):Int; + public static var stepsPerMeasure(get, never):Int; static function get_stepsPerMeasure():Int { diff --git a/source/funkin/input/Cursor.hx b/source/funkin/input/Cursor.hx index 37e819469..edd9e70f3 100644 --- a/source/funkin/input/Cursor.hx +++ b/source/funkin/input/Cursor.hx @@ -4,9 +4,34 @@ import openfl.utils.Assets; import lime.app.Future; import openfl.display.BitmapData; +@:nullSafety class Cursor { - public static var cursorMode(default, set):CursorMode; + /** + * The current cursor mode. + * Set this value to change the cursor graphic. + */ + public static var cursorMode(default, set):Null<CursorMode> = null; + + /** + * Show the cursor. + */ + public static inline function show():Void + { + FlxG.mouse.visible = true; + // Reset the cursor mode. + Cursor.cursorMode = Default; + } + + /** + * Hide the cursor. + */ + public static inline function hide():Void + { + FlxG.mouse.visible = false; + // Reset the cursor mode. + Cursor.cursorMode = null; + } static final CURSOR_DEFAULT_PARAMS:CursorParams = { @@ -15,7 +40,7 @@ class Cursor offsetX: 0, offsetY: 0, }; - static var assetCursorDefault:BitmapData = null; + static var assetCursorDefault:Null<BitmapData> = null; static final CURSOR_CROSS_PARAMS:CursorParams = { @@ -24,7 +49,7 @@ class Cursor offsetX: 0, offsetY: 0, }; - static var assetCursorCross:BitmapData = null; + static var assetCursorCross:Null<BitmapData> = null; static final CURSOR_ERASER_PARAMS:CursorParams = { @@ -33,16 +58,16 @@ class Cursor offsetX: 0, offsetY: 0, }; - static var assetCursorEraser:BitmapData = null; + static var assetCursorEraser:Null<BitmapData> = null; static final CURSOR_GRABBING_PARAMS:CursorParams = { graphic: "assets/images/cursor/cursor-grabbing.png", scale: 1.0, - offsetX: 32, + offsetX: -8, offsetY: 0, }; - static var assetCursorGrabbing:BitmapData = null; + static var assetCursorGrabbing:Null<BitmapData> = null; static final CURSOR_HOURGLASS_PARAMS:CursorParams = { @@ -51,25 +76,34 @@ class Cursor offsetX: 0, offsetY: 0, }; - static var assetCursorHourglass:BitmapData = null; + static var assetCursorHourglass:Null<BitmapData> = null; static final CURSOR_POINTER_PARAMS:CursorParams = { graphic: "assets/images/cursor/cursor-pointer.png", scale: 1.0, - offsetX: 8, + offsetX: -8, offsetY: 0, }; - static var assetCursorPointer:BitmapData = null; + static var assetCursorPointer:Null<BitmapData> = null; static final CURSOR_TEXT_PARAMS:CursorParams = { graphic: "assets/images/cursor/cursor-text.png", - scale: 1.0, + scale: 0.2, offsetX: 0, offsetY: 0, }; - static var assetCursorText:BitmapData = null; + static var assetCursorText:Null<BitmapData> = null; + + static final CURSOR_TEXT_VERTICAL_PARAMS:CursorParams = + { + graphic: "assets/images/cursor/cursor-text-vertical.png", + scale: 0.2, + offsetX: 0, + offsetY: 0, + }; + static var assetCursorTextVertical:Null<BitmapData> = null; static final CURSOR_ZOOM_IN_PARAMS:CursorParams = { @@ -78,7 +112,7 @@ class Cursor offsetX: 0, offsetY: 0, }; - static var assetCursorZoomIn:BitmapData = null; + static var assetCursorZoomIn:Null<BitmapData> = null; static final CURSOR_ZOOM_OUT_PARAMS:CursorParams = { @@ -87,11 +121,36 @@ class Cursor offsetX: 0, offsetY: 0, }; - static var assetCursorZoomOut:BitmapData = null; + static var assetCursorZoomOut:Null<BitmapData> = null; - static function set_cursorMode(value:CursorMode):CursorMode + static final CURSOR_CROSSHAIR_PARAMS:CursorParams = + { + graphic: "assets/images/cursor/cursor-crosshair.png", + scale: 1.0, + offsetX: -16, + offsetY: -16, + }; + static var assetCursorCrosshair:Null<BitmapData> = null; + + static final CURSOR_CELL_PARAMS:CursorParams = + { + graphic: "assets/images/cursor/cursor-cell.png", + scale: 1.0, + offsetX: -16, + offsetY: -16, + }; + static var assetCursorCell:Null<BitmapData> = null; + + // DESIRED CURSOR: Resize NS (vertical) + // DESIRED CURSOR: Resize EW (horizontal) + // DESIRED CURSOR: Resize NESW (diagonal) + // DESIRED CURSOR: Resize NWSE (diagonal) + // DESIRED CURSOR: Help (Cursor with question mark) + // DESIRED CURSOR: Menu (Cursor with menu icon) + + static function set_cursorMode(value:Null<CursorMode>):Null<CursorMode> { - if (cursorMode != value) + if (value != null && cursorMode != value) { cursorMode = value; setCursorGraphic(cursorMode); @@ -99,16 +158,9 @@ class Cursor return cursorMode; } - public static inline function show():Void - { - FlxG.mouse.visible = true; - } - - public static inline function hide():Void - { - FlxG.mouse.visible = false; - } - + /** + * Synchronous. + */ static function setCursorGraphic(?value:CursorMode = null):Void { if (value == null) @@ -117,6 +169,156 @@ class Cursor return; } + switch (value) + { + case Default: + if (assetCursorDefault == null) + { + var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_DEFAULT_PARAMS.graphic); + assetCursorDefault = bitmapData; + applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS); + } + else + { + applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS); + } + + case Cross: + if (assetCursorCross == null) + { + var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_CROSS_PARAMS.graphic); + assetCursorCross = bitmapData; + applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS); + } + else + { + applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS); + } + + case Eraser: + if (assetCursorEraser == null) + { + var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_ERASER_PARAMS.graphic); + assetCursorEraser = bitmapData; + applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS); + } + else + { + applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS); + } + + case Grabbing: + if (assetCursorGrabbing == null) + { + var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_GRABBING_PARAMS.graphic); + assetCursorGrabbing = bitmapData; + applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS); + } + else + { + applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS); + } + + case Hourglass: + if (assetCursorHourglass == null) + { + var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_HOURGLASS_PARAMS.graphic); + assetCursorHourglass = bitmapData; + applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS); + } + else + { + applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS); + } + + case Pointer: + if (assetCursorPointer == null) + { + var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_POINTER_PARAMS.graphic); + assetCursorPointer = bitmapData; + applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS); + } + else + { + applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS); + } + + case Text: + if (assetCursorText == null) + { + var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_TEXT_PARAMS.graphic); + assetCursorText = bitmapData; + applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS); + } + else + { + applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS); + } + + case ZoomIn: + if (assetCursorZoomIn == null) + { + var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_ZOOM_IN_PARAMS.graphic); + assetCursorZoomIn = bitmapData; + applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS); + } + else + { + applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS); + } + + case ZoomOut: + if (assetCursorZoomOut == null) + { + var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_ZOOM_OUT_PARAMS.graphic); + assetCursorZoomOut = bitmapData; + applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS); + } + else + { + applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS); + } + + case Crosshair: + if (assetCursorCrosshair == null) + { + var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_CROSSHAIR_PARAMS.graphic); + assetCursorCrosshair = bitmapData; + applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS); + } + else + { + applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS); + } + + case Cell: + if (assetCursorCell == null) + { + var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_CELL_PARAMS.graphic); + assetCursorCell = bitmapData; + applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS); + } + else + { + applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS); + } + + default: + setCursorGraphic(null); + } + } + + /** + * Asynchronous. + */ + static function loadCursorGraphic(?value:CursorMode = null):Void + { + if (value == null) + { + FlxG.mouse.unload(); + return; + } + switch (value) { case Default: @@ -127,6 +329,7 @@ class Cursor assetCursorDefault = bitmapData; applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS); }); + future.onError(onCursorError.bind(Default)); } else { @@ -141,6 +344,7 @@ class Cursor assetCursorCross = bitmapData; applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS); }); + future.onError(onCursorError.bind(Cross)); } else { @@ -155,6 +359,7 @@ class Cursor assetCursorEraser = bitmapData; applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS); }); + future.onError(onCursorError.bind(Eraser)); } else { @@ -169,6 +374,7 @@ class Cursor assetCursorGrabbing = bitmapData; applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS); }); + future.onError(onCursorError.bind(Grabbing)); } else { @@ -183,6 +389,7 @@ class Cursor assetCursorHourglass = bitmapData; applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS); }); + future.onError(onCursorError.bind(Hourglass)); } else { @@ -197,6 +404,7 @@ class Cursor assetCursorPointer = bitmapData; applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS); }); + future.onError(onCursorError.bind(Pointer)); } else { @@ -211,6 +419,7 @@ class Cursor assetCursorText = bitmapData; applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS); }); + future.onError(onCursorError.bind(Text)); } else { @@ -225,6 +434,7 @@ class Cursor assetCursorZoomIn = bitmapData; applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS); }); + future.onError(onCursorError.bind(ZoomIn)); } else { @@ -239,14 +449,45 @@ class Cursor assetCursorZoomOut = bitmapData; applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS); }); + future.onError(onCursorError.bind(ZoomOut)); } else { applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS); } + case Crosshair: + if (assetCursorCrosshair == null) + { + var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_CROSSHAIR_PARAMS.graphic); + future.onComplete(function(bitmapData:BitmapData) { + assetCursorCrosshair = bitmapData; + applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS); + }); + future.onError(onCursorError.bind(Crosshair)); + } + else + { + applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS); + } + + case Cell: + if (assetCursorCell == null) + { + var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_CELL_PARAMS.graphic); + future.onComplete(function(bitmapData:BitmapData) { + assetCursorCell = bitmapData; + applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS); + }); + future.onError(onCursorError.bind(Cell)); + } + else + { + applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS); + } + default: - setCursorGraphic(null); + loadCursorGraphic(null); } } @@ -254,6 +495,11 @@ class Cursor { FlxG.mouse.load(graphic, params.scale, params.offsetX, params.offsetY); } + + static function onCursorError(cursorMode:CursorMode, error:String):Void + { + trace("Failed to load cursor graphic for cursor mode " + cursorMode + ": " + error); + } } // https://developer.mozilla.org/en-US/docs/Web/CSS/cursor @@ -268,6 +514,8 @@ enum CursorMode Text; ZoomIn; ZoomOut; + Crosshair; + Cell; } /** diff --git a/source/funkin/input/TurboKeyHandler.hx b/source/funkin/input/TurboKeyHandler.hx index 3719ff7cc..099d373b4 100644 --- a/source/funkin/input/TurboKeyHandler.hx +++ b/source/funkin/input/TurboKeyHandler.hx @@ -26,7 +26,7 @@ class TurboKeyHandler extends FlxBasic /** * Whether all of the keys for this handler are pressed. */ - public var allPressed(get, null):Bool; + public var allPressed(get, never):Bool; /** * Whether all of the keys for this handler are activated, diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index ed82d6e99..068f32f97 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -251,14 +251,14 @@ class PlayState extends MusicBeatSubState var overrideMusic:Bool = false; - public var isSubState(get, null):Bool; + public var isSubState(get, never):Bool; function get_isSubState():Bool { return this._parentState != null; } - public var isChartingMode(get, null):Bool; + public var isChartingMode(get, never):Bool; function get_isChartingMode():Bool { @@ -427,7 +427,7 @@ class PlayState extends MusicBeatSubState * Data for the current difficulty for the current song. * Includes chart data, scroll speed, and other information. */ - public var currentChart(get, null):SongDifficulty; + public var currentChart(get, never):SongDifficulty; function get_currentChart():SongDifficulty { @@ -439,7 +439,7 @@ class PlayState extends MusicBeatSubState * The internal ID of the currently active Stage. * Used to retrieve the data required to build the `currentStage`. */ - public var currentStageId(get, null):String; + public var currentStageId(get, never):String; function get_currentStageId():String { diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index c7b58c393..30b549fd3 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -66,7 +66,7 @@ class BaseCharacter extends Bopper * The offset between the corner of the sprite and the origin of the sprite (at the character's feet). * cornerPosition = stageData - characterOrigin */ - public var characterOrigin(get, null):FlxPoint; + public var characterOrigin(get, never):FlxPoint; function get_characterOrigin():FlxPoint { @@ -103,7 +103,7 @@ class BaseCharacter extends Bopper /** * The absolute position of the character's feet, at the bottom-center of the sprite. */ - public var feetPosition(get, null):FlxPoint; + public var feetPosition(get, never):FlxPoint; function get_feetPosition():FlxPoint { @@ -264,7 +264,7 @@ class BaseCharacter extends Bopper /** * The per-character camera offset. */ - var characterCameraOffsets(get, null):Array<Float>; + var characterCameraOffsets(get, never):Array<Float>; function get_characterCameraOffsets():Array<Float> { diff --git a/source/funkin/play/cutscene/dialogue/Conversation.hx b/source/funkin/play/cutscene/dialogue/Conversation.hx index a6851f0f9..2b7db381c 100644 --- a/source/funkin/play/cutscene/dialogue/Conversation.hx +++ b/source/funkin/play/cutscene/dialogue/Conversation.hx @@ -50,7 +50,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass */ var currentDialogueEntry:Int = 0; - var currentDialogueEntryCount(get, null):Int; + var currentDialogueEntryCount(get, never):Int; function get_currentDialogueEntryCount():Int { @@ -62,14 +62,14 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass * **/ var currentDialogueLine:Int = 0; - var currentDialogueLineCount(get, null):Int; + var currentDialogueLineCount(get, never):Int; function get_currentDialogueLineCount():Int { return currentDialogueEntryData.text.length; } - var currentDialogueEntryData(get, null):DialogueEntryData; + var currentDialogueEntryData(get, never):DialogueEntryData; function get_currentDialogueEntryData():DialogueEntryData { @@ -79,7 +79,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass return conversationData.dialogue[currentDialogueEntry]; } - var currentDialogueLineString(get, null):String; + var currentDialogueLineString(get, never):String; function get_currentDialogueLineString():String { diff --git a/source/funkin/play/cutscene/dialogue/DialogueBox.hx b/source/funkin/play/cutscene/dialogue/DialogueBox.hx index bfc0e9233..cdac3c233 100644 --- a/source/funkin/play/cutscene/dialogue/DialogueBox.hx +++ b/source/funkin/play/cutscene/dialogue/DialogueBox.hx @@ -13,7 +13,7 @@ import flixel.util.FlxColor; class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass { public final dialogueBoxId:String; - public var dialogueBoxName(get, null):String; + public var dialogueBoxName(get, never):String; function get_dialogueBoxName():String { diff --git a/source/funkin/play/cutscene/dialogue/Speaker.hx b/source/funkin/play/cutscene/dialogue/Speaker.hx index 1fb341009..d7ed004f1 100644 --- a/source/funkin/play/cutscene/dialogue/Speaker.hx +++ b/source/funkin/play/cutscene/dialogue/Speaker.hx @@ -8,7 +8,7 @@ import funkin.modding.IScriptedClass.IDialogueScriptedClass; /** * The character sprite which displays during dialogue. - * + * * Most conversations have two speakers, with one being flipped. */ class Speaker extends FlxSprite implements IDialogueScriptedClass @@ -26,7 +26,7 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass /** * A readable name for this speaker. */ - public var speakerName(get, null):String; + public var speakerName(get, never):String; function get_speakerName():String { @@ -129,7 +129,7 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass /** * Set the sprite scale to the appropriate value. - * @param scale + * @param scale */ public function setScale(scale:Null<Float>):Void { @@ -184,7 +184,7 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass /** * Ensure that a given animation exists before playing it. * Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play. - * @param name + * @param name */ function correctAnimationName(name:String):String { diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 8847636bd..2b21e6b7e 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -31,7 +31,7 @@ class Strumline extends FlxSpriteGroup static final KEY_COUNT:Int = 4; static final NOTE_SPLASH_CAP:Int = 6; - static var RENDER_DISTANCE_MS(get, null):Float; + static var RENDER_DISTANCE_MS(get, never):Float; static function get_RENDER_DISTANCE_MS():Float { diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx index 72d22191b..4bcbe0528 100644 --- a/source/funkin/play/notes/SustainTrail.hx +++ b/source/funkin/play/notes/SustainTrail.hx @@ -31,7 +31,7 @@ class SustainTrail extends FlxSprite public var noteDirection:NoteDirection = 0; public var sustainLength(default, set):Float = 0; // millis public var fullSustainLength:Float = 0; - public var noteData:SongNoteData; + public var noteData:Null<SongNoteData>; public var cover:NoteHoldCover = null; diff --git a/source/funkin/play/song/SongMigrator.hx b/source/funkin/play/song/SongMigrator.hx index bb8718bb7..f33d9bbe9 100644 --- a/source/funkin/play/song/SongMigrator.hx +++ b/source/funkin/play/song/SongMigrator.hx @@ -179,7 +179,7 @@ class SongMigrator songMetadata.playData.playableChars = {}; try { - Reflect.setField(songMetadata.playData.playableChars, songData.song.player1, new SongPlayableChar('', songData.song.player2)); + songMetadata.playData.playableChars.set(songData.song.player1, new SongPlayableChar('', songData.song.player2)); } catch (e) { diff --git a/source/funkin/play/song/SongValidator.hx b/source/funkin/play/song/SongValidator.hx index 16ea88664..11cc758b9 100644 --- a/source/funkin/play/song/SongValidator.hx +++ b/source/funkin/play/song/SongValidator.hx @@ -20,7 +20,7 @@ class SongValidator public static final DEFAULT_STAGE:String = "mainStage"; public static final DEFAULT_SCROLLSPEED:Float = 1.0; - public static var DEFAULT_GENERATEDBY(get, null):String; + public static var DEFAULT_GENERATEDBY(get, never):String; static function get_DEFAULT_GENERATEDBY():String { diff --git a/source/funkin/ui/debug/charting/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/ChartEditorCommand.hx index fd179c481..f0ecb573b 100644 --- a/source/funkin/ui/debug/charting/ChartEditorCommand.hx +++ b/source/funkin/ui/debug/charting/ChartEditorCommand.hx @@ -35,6 +35,7 @@ interface ChartEditorCommand public function toString():String; } +@:nullSafety class AddNotesCommand implements ChartEditorCommand { var notes:Array<SongNoteData>; @@ -98,6 +99,7 @@ class AddNotesCommand implements ChartEditorCommand } } +@:nullSafety class RemoveNotesCommand implements ChartEditorCommand { var notes:Array<SongNoteData>; @@ -153,6 +155,7 @@ class RemoveNotesCommand implements ChartEditorCommand /** * Appends one or more items to the selection. */ +@:nullSafety class SelectItemsCommand implements ChartEditorCommand { var notes:Array<SongNoteData>; @@ -220,6 +223,7 @@ class SelectItemsCommand implements ChartEditorCommand } } +@:nullSafety class AddEventsCommand implements ChartEditorCommand { var events:Array<SongEventData>; @@ -278,6 +282,7 @@ class AddEventsCommand implements ChartEditorCommand } } +@:nullSafety class RemoveEventsCommand implements ChartEditorCommand { var events:Array<SongEventData>; @@ -327,6 +332,7 @@ class RemoveEventsCommand implements ChartEditorCommand } } +@:nullSafety class RemoveItemsCommand implements ChartEditorCommand { var notes:Array<SongNoteData>; @@ -385,6 +391,7 @@ class RemoveItemsCommand implements ChartEditorCommand } } +@:nullSafety class SwitchDifficultyCommand implements ChartEditorCommand { var prevDifficulty:String; @@ -424,6 +431,7 @@ class SwitchDifficultyCommand implements ChartEditorCommand } } +@:nullSafety class DeselectItemsCommand implements ChartEditorCommand { var notes:Array<SongNoteData>; @@ -478,6 +486,7 @@ class DeselectItemsCommand implements ChartEditorCommand * Sets the selection rather than appends it. * Deselects any notes that are not in the new selection. */ +@:nullSafety class SetItemSelectionCommand implements ChartEditorCommand { var notes:Array<SongNoteData>; @@ -518,6 +527,7 @@ class SetItemSelectionCommand implements ChartEditorCommand } } +@:nullSafety class SelectAllItemsCommand implements ChartEditorCommand { var previousNoteSelection:Array<SongNoteData>; @@ -553,6 +563,7 @@ class SelectAllItemsCommand implements ChartEditorCommand } } +@:nullSafety class InvertSelectedItemsCommand implements ChartEditorCommand { var previousNoteSelection:Array<SongNoteData>; @@ -587,6 +598,7 @@ class InvertSelectedItemsCommand implements ChartEditorCommand } } +@:nullSafety class DeselectAllItemsCommand implements ChartEditorCommand { var previousNoteSelection:Array<SongNoteData>; @@ -622,6 +634,7 @@ class DeselectAllItemsCommand implements ChartEditorCommand } } +@:nullSafety class CutItemsCommand implements ChartEditorCommand { var notes:Array<SongNoteData>; @@ -679,14 +692,16 @@ class CutItemsCommand implements ChartEditorCommand } } +@:nullSafety class FlipNotesCommand implements ChartEditorCommand { - var notes:Array<SongNoteData>; - var flippedNotes:Array<SongNoteData>; + var notes:Array<SongNoteData> = []; + var flippedNotes:Array<SongNoteData> = []; public function new(notes:Array<SongNoteData>) { this.notes = notes; + this.flippedNotes = SongDataUtils.flipNotes(notes); } public function execute(state:ChartEditorState):Void @@ -695,7 +710,6 @@ class FlipNotesCommand implements ChartEditorCommand state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); // Add the flipped notes. - flippedNotes = SongDataUtils.flipNotes(notes); state.currentSongChartNoteData = state.currentSongChartNoteData.concat(flippedNotes); state.currentNoteSelection = flippedNotes; @@ -729,12 +743,13 @@ class FlipNotesCommand implements ChartEditorCommand } } +@:nullSafety class PasteItemsCommand implements ChartEditorCommand { var targetTimestamp:Float; // Notes we added with this command, for undo. - var addedNotes:Array<SongNoteData>; - var addedEvents:Array<SongEventData>; + var addedNotes:Array<SongNoteData> = []; + var addedEvents:Array<SongEventData> = []; public function new(targetTimestamp:Float) { @@ -787,6 +802,7 @@ class PasteItemsCommand implements ChartEditorCommand } } +@:nullSafety class ExtendNoteLengthCommand implements ChartEditorCommand { var note:SongNoteData; diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index e5b2d332c..59bee0d74 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -6,6 +6,7 @@ import funkin.util.SerializerUtil; import funkin.play.song.SongData.SongChartData; import funkin.play.song.SongData.SongMetadata; import flixel.util.FlxTimer; +import funkin.ui.haxeui.components.FunkinLink; import funkin.util.SortUtil; import funkin.input.Cursor; import funkin.play.character.BaseCharacter; @@ -40,6 +41,7 @@ using Lambda; /** * Handles dialogs for the new Chart Editor. */ +@:nullSafety class ChartEditorDialogHandler { static final CHART_EDITOR_DIALOG_ABOUT_LAYOUT:String = Paths.ui('chart-editor/dialogs/about'); @@ -59,7 +61,7 @@ class ChartEditorDialogHandler * @param state The current chart editor state. * @return The dialog that was opened. */ - public static inline function openAboutDialog(state:ChartEditorState):Dialog + public static inline function openAboutDialog(state:ChartEditorState):Null<Dialog> { return openDialog(state, CHART_EDITOR_DIALOG_ABOUT_LAYOUT, true, true); } @@ -70,12 +72,14 @@ class ChartEditorDialogHandler * @param closable Whether the dialog can be closed by the user. * @return The dialog that was opened. */ - public static function openWelcomeDialog(state:ChartEditorState, closable:Bool = true):Dialog + public static function openWelcomeDialog(state:ChartEditorState, closable:Bool = true):Null<Dialog> { - var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable); + var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable); + if (dialog == null) throw 'Could not locate Welcome dialog'; // Add handlers to the "Create From Song" section. - var linkCreateBasic:Link = dialog.findComponent('splashCreateFromSongBasic', Link); + var linkCreateBasic:Null<Link> = dialog.findComponent('splashCreateFromSongBasic', Link); + if (linkCreateBasic == null) throw 'Could not locate splashCreateFromSongBasic link in Welcome dialog'; linkCreateBasic.onClick = function(_event) { // Hide the welcome dialog dialog.hideDialog(DialogButton.CANCEL); @@ -86,7 +90,8 @@ class ChartEditorDialogHandler openCreateSongWizard(state, false); } - var linkImportChartLegacy:Link = dialog.findComponent('splashImportChartLegacy', Link); + var linkImportChartLegacy:Null<Link> = dialog.findComponent('splashImportChartLegacy', Link); + if (linkImportChartLegacy == null) throw 'Could not locate splashImportChartLegacy link in Welcome dialog'; linkImportChartLegacy.onClick = function(_event) { // Hide the welcome dialog dialog.hideDialog(DialogButton.CANCEL); @@ -95,7 +100,8 @@ class ChartEditorDialogHandler openImportChartWizard(state, 'legacy', false); }; - var buttonBrowse:Button = dialog.findComponent('splashBrowse', Button); + var buttonBrowse:Null<Button> = dialog.findComponent('splashBrowse', Button); + if (buttonBrowse == null) throw 'Could not locate splashBrowse button in Welcome dialog'; buttonBrowse.onClick = function(_event) { // Hide the welcome dialog dialog.hideDialog(DialogButton.CANCEL); @@ -104,25 +110,32 @@ class ChartEditorDialogHandler openBrowseWizard(state, false); } - var splashTemplateContainer:VBox = dialog.findComponent('splashTemplateContainer', VBox); + var splashTemplateContainer:Null<VBox> = dialog.findComponent('splashTemplateContainer', VBox); + if (splashTemplateContainer == null) throw 'Could not locate splashTemplateContainer in Welcome dialog'; var songList:Array<String> = SongDataParser.listSongIds(); songList.sort(SortUtil.alphabetically); for (targetSongId in songList) { - var songData:Song = SongDataParser.fetchSong(targetSongId); + var songData:Null<Song> = SongDataParser.fetchSong(targetSongId); if (songData == null) continue; - var songName:Null<String> = songData.getDifficulty('normal') ?.songName; - if (songName == null) songName = songData.getDifficulty() ?.songName; + var diffNormal:Null<SongDifficulty> = songData.getDifficulty('normal'); + var songName:Null<String> = diffNormal?.songName; + if (songName == null) + { + var diffDefault:Null<SongDifficulty> = songData.getDifficulty(); + songName = diffDefault?.songName; + } if (songName == null) { trace('[WARN] Could not fetch song name for ${targetSongId}'); + continue; } - var linkTemplateSong:Link = new Link(); + var linkTemplateSong:Link = new FunkinLink(); linkTemplateSong.text = songName; linkTemplateSong.onClick = function(_event) { dialog.hideDialog(DialogButton.CANCEL); @@ -184,7 +197,8 @@ class ChartEditorDialogHandler { // Open the "Open Chart" wizard // Step 1. Open Chart - var openChartDialog:Dialog = openImportChartDialog(state, format); + var openChartDialog:Null<Dialog> = openImportChartDialog(state, format); + if (openChartDialog == null) throw 'Could not locate Import Chart dialog'; openChartDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) @@ -260,15 +274,18 @@ class ChartEditorDialogHandler @:haxe.warning("-WVarInit") // Hide the warning about the onDropFile handler. public static function openUploadInstDialog(state:ChartEditorState, closable:Bool = true):Dialog { - var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT, true, closable); + var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT, true, closable); + if (dialog == null) throw 'Could not locate Upload Instrumental dialog'; - var buttonCancel:Button = dialog.findComponent('dialogCancel', Button); + var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); + if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Instrumental dialog'; buttonCancel.onClick = function(_event) { dialog.hideDialog(DialogButton.CANCEL); } - var instrumentalBox:Box = dialog.findComponent('instrumentalBox', Box); + var instrumentalBox:Null<Box> = dialog.findComponent('instrumentalBox', Box); + if (instrumentalBox == null) throw 'Could not locate instrumentalBox in Upload Instrumental dialog'; instrumentalBox.onMouseOver = function(_event) { instrumentalBox.swapClass('upload-bg', 'upload-bg-hover'); @@ -285,11 +302,12 @@ class ChartEditorDialogHandler instrumentalBox.onClick = function(_event) { Dialogs.openBinaryFile('Open Instrumental', [ {label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile:SelectedFileInfo) { - if (selectedFile != null) + if (selectedFile != null && selectedFile.bytes != null) { if (state.loadInstrumentalFromBytes(selectedFile.bytes)) { trace('Selected file: ' + selectedFile.fullPath); + #if !mac NotificationManager.instance.addNotification( { title: 'Success', @@ -297,6 +315,7 @@ class ChartEditorDialogHandler type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); + #end dialog.hideDialog(DialogButton.APPLY); removeDropHandler(onDropFile); @@ -305,6 +324,7 @@ class ChartEditorDialogHandler { trace('Failed to load instrumental (${selectedFile.fullPath})'); + #if !mac NotificationManager.instance.addNotification( { title: 'Failure', @@ -312,6 +332,7 @@ class ChartEditorDialogHandler type: NotificationType.Error, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); + #end } } }); @@ -323,6 +344,7 @@ class ChartEditorDialogHandler if (state.loadInstrumentalFromPath(path)) { // Tell the user the load was successful. + #if !mac NotificationManager.instance.addNotification( { title: 'Success', @@ -330,13 +352,14 @@ class ChartEditorDialogHandler type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); + #end dialog.hideDialog(DialogButton.APPLY); removeDropHandler(onDropFile); } else { - var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext)) + var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext ?? '')) { 'File format (${path.ext}) not supported for instrumental track (${path.file}.${path.ext})'; } @@ -346,6 +369,7 @@ class ChartEditorDialogHandler } // Tell the user the load was successful. + #if !mac NotificationManager.instance.addNotification( { title: 'Failure', @@ -353,6 +377,7 @@ class ChartEditorDialogHandler type: NotificationType.Error, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); + #end } }; @@ -367,6 +392,15 @@ class ChartEditorDialogHandler handler:(String->Void) }> = []; + /** + * Add a callback for when a file is dropped on a component. + * + * On OS X you can’t drop on the application window, but rather only the app icon + * (either in the dock while running or the icon on the hard drive) so this must be disabled + * and UI updated appropriately. + * @param component + * @param handler + */ static function addDropHandler(component:Component, handler:String->Void):Void { #if desktop @@ -420,15 +454,17 @@ class ChartEditorDialogHandler @:haxe.warning("-WVarInit") public static function openSongMetadataDialog(state:ChartEditorState):Dialog { - var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT, true, false); - - var buttonCancel:Button = dialog.findComponent('dialogCancel', Button); + var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT, true, false); + if (dialog == null) throw 'Could not locate Song Metadata dialog'; + var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); + if (buttonCancel == null) throw 'Could not locate dialogCancel button in Song Metadata dialog'; buttonCancel.onClick = function(_event) { dialog.hideDialog(DialogButton.CANCEL); } - var dialogSongName:TextField = dialog.findComponent('dialogSongName', TextField); + var dialogSongName:Null<TextField> = dialog.findComponent('dialogSongName', TextField); + if (dialogSongName == null) throw 'Could not locate dialogSongName TextField in Song Metadata dialog'; dialogSongName.onChange = function(event:UIEvent) { var valid:Bool = event.target.text != null && event.target.text != ''; @@ -439,12 +475,13 @@ class ChartEditorDialogHandler } else { - state.currentSongMetadata.songName = null; + state.currentSongMetadata.songName = ""; } }; - state.currentSongMetadata.songName = null; + state.currentSongMetadata.songName = ""; - var dialogSongArtist:TextField = dialog.findComponent('dialogSongArtist', TextField); + var dialogSongArtist:Null<TextField> = dialog.findComponent('dialogSongArtist', TextField); + if (dialogSongArtist == null) throw 'Could not locate dialogSongArtist TextField in Song Metadata dialog'; dialogSongArtist.onChange = function(event:UIEvent) { var valid:Bool = event.target.text != null && event.target.text != ''; @@ -455,26 +492,29 @@ class ChartEditorDialogHandler } else { - state.currentSongMetadata.artist = null; + state.currentSongMetadata.artist = ""; } }; - state.currentSongMetadata.artist = null; + state.currentSongMetadata.artist = ""; - var dialogStage:DropDown = dialog.findComponent('dialogStage', DropDown); + var dialogStage:Null<DropDown> = dialog.findComponent('dialogStage', DropDown); + if (dialogStage == null) throw 'Could not locate dialogStage DropDown in Song Metadata dialog'; dialogStage.onChange = function(event:UIEvent) { if (event.data == null && event.data.id == null) return; state.currentSongMetadata.playData.stage = event.data.id; }; - state.currentSongMetadata.playData.stage = null; + state.currentSongMetadata.playData.stage = 'mainStage'; - var dialogNoteSkin:DropDown = dialog.findComponent('dialogNoteSkin', DropDown); + var dialogNoteSkin:Null<DropDown> = dialog.findComponent('dialogNoteSkin', DropDown); + if (dialogNoteSkin == null) throw 'Could not locate dialogNoteSkin DropDown in Song Metadata dialog'; dialogNoteSkin.onChange = function(event:UIEvent) { if (event.data.id == null) return; state.currentSongMetadata.playData.noteSkin = event.data.id; }; - state.currentSongMetadata.playData.noteSkin = null; + state.currentSongMetadata.playData.noteSkin = 'funkin'; - var dialogBPM:NumberStepper = dialog.findComponent('dialogBPM', NumberStepper); + var dialogBPM:Null<NumberStepper> = dialog.findComponent('dialogBPM', NumberStepper); + if (dialogBPM == null) throw 'Could not locate dialogBPM NumberStepper in Song Metadata dialog'; dialogBPM.onChange = function(event:UIEvent) { if (event.value == null || event.value <= 0) return; @@ -493,8 +533,10 @@ class ChartEditorDialogHandler state.currentSongMetadata.timeChanges = timeChanges; }; - var dialogCharGrid:PropertyGrid = dialog.findComponent('dialogCharGrid', PropertyGrid); - var dialogCharAdd:Button = dialog.findComponent('dialogCharAdd', Button); + var dialogCharGrid:Null<PropertyGrid> = dialog.findComponent('dialogCharGrid', PropertyGrid); + if (dialogCharGrid == null) throw 'Could not locate dialogCharGrid PropertyGrid in Song Metadata dialog'; + var dialogCharAdd:Null<Button> = dialog.findComponent('dialogCharAdd', Button); + if (dialogCharAdd == null) throw 'Could not locate dialogCharAdd Button in Song Metadata dialog'; dialogCharAdd.onClick = function(event:UIEvent) { var charGroup:PropertyGroup; charGroup = buildCharGroup(state, null, () -> dialogCharGrid.removeComponent(charGroup)); @@ -504,15 +546,16 @@ class ChartEditorDialogHandler // Empty the character list. state.currentSongMetadata.playData.playableChars = {}; // Add at least one character group with no Remove button. - dialogCharGrid.addComponent(buildCharGroup(state, 'bf', null)); + dialogCharGrid.addComponent(buildCharGroup(state, 'bf')); - var dialogContinue:Button = dialog.findComponent('dialogContinue', Button); + var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button); + if (dialogContinue == null) throw 'Could not locate dialogContinue button in Song Metadata dialog'; dialogContinue.onClick = (_event) -> dialog.hideDialog(DialogButton.APPLY); return dialog; } - static function buildCharGroup(state:ChartEditorState, key:String = null, removeFunc:Void->Void):PropertyGroup + static function buildCharGroup(state:ChartEditorState, key:String = '', removeFunc:Void->Void = null):PropertyGroup { var groupKey:String = key; @@ -537,32 +580,36 @@ class ChartEditorDialogHandler var removeGroup:Void->Void = function() { state.currentSongMetadata.playData.playableChars.remove(groupKey); - removeFunc(); + if (removeFunc != null) removeFunc(); } var charData:SongPlayableChar = getCharData(); var charGroup:PropertyGroup = cast state.buildComponent(CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT); - var charGroupPlayer:DropDown = charGroup.findComponent('charGroupPlayer', DropDown); + var charGroupPlayer:Null<DropDown> = charGroup.findComponent('charGroupPlayer', DropDown); + if (charGroupPlayer == null) throw 'Could not locate charGroupPlayer DropDown in Song Metadata dialog'; charGroupPlayer.onChange = function(event:UIEvent) { charGroup.text = event.data.text; moveCharGroup(event.data.id); }; - var charGroupOpponent:DropDown = charGroup.findComponent('charGroupOpponent', DropDown); + var charGroupOpponent:Null<DropDown> = charGroup.findComponent('charGroupOpponent', DropDown); + if (charGroupOpponent == null) throw 'Could not locate charGroupOpponent DropDown in Song Metadata dialog'; charGroupOpponent.onChange = function(event:UIEvent) { charData.opponent = event.data.id; }; charGroupOpponent.value = getCharData().opponent; - var charGroupGirlfriend:DropDown = charGroup.findComponent('charGroupGirlfriend', DropDown); + var charGroupGirlfriend:Null<DropDown> = charGroup.findComponent('charGroupGirlfriend', DropDown); + if (charGroupGirlfriend == null) throw 'Could not locate charGroupGirlfriend DropDown in Song Metadata dialog'; charGroupGirlfriend.onChange = function(event:UIEvent) { charData.girlfriend = event.data.id; }; charGroupGirlfriend.value = getCharData().girlfriend; - var charGroupRemove:Button = charGroup.findComponent('charGroupRemove', Button); + var charGroupRemove:Null<Button> = charGroup.findComponent('charGroupRemove', Button); + if (charGroupRemove == null) throw 'Could not locate charGroupRemove Button in Song Metadata dialog'; charGroupRemove.onClick = function(event:UIEvent) { removeGroup(); }; @@ -584,21 +631,26 @@ class ChartEditorDialogHandler for (charKey in state.currentSongMetadata.playData.playableChars.keys()) { - var charData:SongPlayableChar = state.currentSongMetadata.playData.playableChars.get(charKey); + var charData:Null<SongPlayableChar> = state.currentSongMetadata.playData.playableChars.get(charKey); + if (charData == null) continue; charIdsForVocals.push(charKey); if (charData.opponent != null) charIdsForVocals.push(charData.opponent); } - var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT, true, closable); + var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT, true, closable); + if (dialog == null) throw 'Could not locate Upload Vocals dialog'; - var dialogContainer:Component = dialog.findComponent('vocalContainer'); + var dialogContainer:Null<Component> = dialog.findComponent('vocalContainer'); + if (dialogContainer == null) throw 'Could not locate vocalContainer in Upload Vocals dialog'; - var buttonCancel:Button = dialog.findComponent('dialogCancel', Button); + var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); + if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Vocals dialog'; buttonCancel.onClick = function(_event) { dialog.hideDialog(DialogButton.CANCEL); } - var dialogNoVocals:Button = dialog.findComponent('dialogNoVocals', Button); + var dialogNoVocals:Null<Button> = dialog.findComponent('dialogNoVocals', Button); + if (dialogNoVocals == null) throw 'Could not locate dialogNoVocals button in Upload Vocals dialog'; dialogNoVocals.onClick = function(_event) { // Dismiss dialog.hideDialog(DialogButton.APPLY); @@ -607,13 +659,18 @@ class ChartEditorDialogHandler for (charKey in charIdsForVocals) { trace('Adding vocal upload for character ${charKey}'); - var charMetadata:CharacterData = CharacterDataParser.fetchCharacterData(charKey); + var charMetadata:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charKey); var charName:String = charMetadata != null ? charMetadata.name : charKey; var vocalsEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT); - var vocalsEntryLabel:Label = vocalsEntry.findComponent('vocalsEntryLabel', Label); + var vocalsEntryLabel:Null<Label> = vocalsEntry.findComponent('vocalsEntryLabel', Label); + if (vocalsEntryLabel == null) throw 'Could not locate vocalsEntryLabel in Upload Vocals dialog'; + #if FILE_DROP_SUPPORTED vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.'; + #else + vocalsEntryLabel.text = 'Click to browse for vocals for $charName.'; + #end var onDropFile:String->Void = function(pathStr:String) { trace('Selected file: $pathStr'); @@ -622,6 +679,7 @@ class ChartEditorDialogHandler if (state.loadVocalsFromPath(path, charKey)) { // Tell the user the load was successful. + #if !mac NotificationManager.instance.addNotification( { title: 'Success', @@ -629,13 +687,18 @@ class ChartEditorDialogHandler type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); + vocalsEntryLabel.text = 'Vocals for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}'; + #else + vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${path.file}.${path.ext}'; + #end + dialogNoVocals.hidden = true; removeDropHandler(onDropFile); } else { - var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext)) + var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext ?? '')) { 'File format (${path.ext}) not supported for vocal track (${path.file}.${path.ext})'; } @@ -645,6 +708,7 @@ class ChartEditorDialogHandler } // Vocals failed to load. + #if !mac NotificationManager.instance.addNotification( { title: 'Failure', @@ -652,18 +716,27 @@ class ChartEditorDialogHandler type: NotificationType.Error, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); + #end + #if FILE_DROP_SUPPORTED vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.'; + #else + vocalsEntryLabel.text = 'Click to browse for vocals for $charName.'; + #end } }; vocalsEntry.onClick = function(_event) { Dialogs.openBinaryFile('Open $charName Vocals', [ {label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) { - if (selectedFile != null) + if (selectedFile != null && selectedFile.bytes != null) { trace('Selected file: ' + selectedFile.name); + #if FILE_DROP_SUPPORTED + vocalsEntryLabel.text = 'Vocals for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}'; + #else vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}'; + #end state.loadVocalsFromBytes(selectedFile.bytes, charKey); dialogNoVocals.hidden = true; removeDropHandler(onDropFile); @@ -672,11 +745,14 @@ class ChartEditorDialogHandler } // onDropFile + #if FILE_DROP_SUPPORTED addDropHandler(vocalsEntry, onDropFile); + #end dialogContainer.addComponent(vocalsEntry); } - var dialogContinue:Button = dialog.findComponent('dialogContinue', Button); + var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button); + if (dialogContinue == null) throw 'Could not locate dialogContinue button in Upload Vocals dialog'; dialogContinue.onClick = function(_event) { // Dismiss dialog.hideDialog(DialogButton.APPLY); @@ -694,20 +770,25 @@ class ChartEditorDialogHandler @:haxe.warning('-WVarInit') public static function openChartDialog(state:ChartEditorState, closable:Bool = true):Dialog { - var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_OPEN_CHART_LAYOUT, true, closable); + var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_OPEN_CHART_LAYOUT, true, closable); + if (dialog == null) throw 'Could not locate Open Chart dialog'; - var buttonCancel:Button = dialog.findComponent('dialogCancel', Button); + var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); + if (buttonCancel == null) throw 'Could not locate dialogCancel button in Open Chart dialog'; buttonCancel.onClick = function(_event) { dialog.hideDialog(DialogButton.CANCEL); } - var chartContainerA:Component = dialog.findComponent('chartContainerA'); - var chartContainerB:Component = dialog.findComponent('chartContainerB'); + var chartContainerA:Null<Component> = dialog.findComponent('chartContainerA'); + if (chartContainerA == null) throw 'Could not locate chartContainerA in Open Chart dialog'; + var chartContainerB:Null<Component> = dialog.findComponent('chartContainerB'); + if (chartContainerB == null) throw 'Could not locate chartContainerB in Open Chart dialog'; var songMetadata:Map<String, SongMetadata> = []; var songChartData:Map<String, SongChartData> = []; - var buttonContinue:Button = dialog.findComponent('dialogContinue', Button); + var buttonContinue:Null<Button> = dialog.findComponent('dialogContinue', Button); + if (buttonContinue == null) throw 'Could not locate dialogContinue button in Open Chart dialog'; buttonContinue.onClick = function(_event) { state.loadSong(songMetadata, songChartData); @@ -728,8 +809,13 @@ class ChartEditorDialogHandler // Build an entry for -chart.json. var songDefaultChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); - var songDefaultChartDataEntryLabel:Label = songDefaultChartDataEntry.findComponent('chartEntryLabel', Label); + var songDefaultChartDataEntryLabel:Null<Label> = songDefaultChartDataEntry.findComponent('chartEntryLabel', Label); + if (songDefaultChartDataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog'; + #if FILE_DROP_SUPPORTED songDefaultChartDataEntryLabel.text = 'Drag and drop <song>-chart.json file, or click to browse.'; + #else + songDefaultChartDataEntryLabel.text = 'Click to browse for <song>-chart.json file.'; + #end songDefaultChartDataEntry.onClick = onClickChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel); addDropHandler(songDefaultChartDataEntry, onDropFileChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel)); @@ -739,20 +825,50 @@ class ChartEditorDialogHandler { // Build entries for -metadata-<variation>.json. var songVariationMetadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); - var songVariationMetadataEntryLabel:Label = songVariationMetadataEntry.findComponent('chartEntryLabel', Label); + var songVariationMetadataEntryLabel:Null<Label> = songVariationMetadataEntry.findComponent('chartEntryLabel', Label); + if (songVariationMetadataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog'; + #if FILE_DROP_SUPPORTED songVariationMetadataEntryLabel.text = 'Drag and drop <song>-metadata-${variation}.json file, or click to browse.'; + #else + songVariationMetadataEntryLabel.text = 'Click to browse for <song>-metadata-${variation}.json file.'; + #end + songVariationMetadataEntry.onMouseOver = function(_event) { + songVariationMetadataEntry.swapClass('upload-bg', 'upload-bg-hover'); + Cursor.cursorMode = Pointer; + } + songVariationMetadataEntry.onMouseOut = function(_event) { + songVariationMetadataEntry.swapClass('upload-bg-hover', 'upload-bg'); + Cursor.cursorMode = Default; + } songVariationMetadataEntry.onClick = onClickMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel); + #if FILE_DROP_SUPPORTED addDropHandler(songVariationMetadataEntry, onDropFileMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel)); + #end chartContainerB.addComponent(songVariationMetadataEntry); // Build entries for -chart-<variation>.json. var songVariationChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); - var songVariationChartDataEntryLabel:Label = songVariationChartDataEntry.findComponent('chartEntryLabel', Label); + var songVariationChartDataEntryLabel:Null<Label> = songVariationChartDataEntry.findComponent('chartEntryLabel', Label); + if (songVariationChartDataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog'; + #if FILE_DROP_SUPPORTED songVariationChartDataEntryLabel.text = 'Drag and drop <song>-chart-${variation}.json file, or click to browse.'; + #else + songVariationChartDataEntryLabel.text = 'Click to browse for <song>-chart-${variation}.json file.'; + #end + songVariationChartDataEntry.onMouseOver = function(_event) { + songVariationChartDataEntry.swapClass('upload-bg', 'upload-bg-hover'); + Cursor.cursorMode = Pointer; + } + songVariationChartDataEntry.onMouseOut = function(_event) { + songVariationChartDataEntry.swapClass('upload-bg-hover', 'upload-bg'); + Cursor.cursorMode = Default; + } songVariationChartDataEntry.onClick = onClickChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel); + #if FILE_DROP_SUPPORTED addDropHandler(songVariationChartDataEntry, onDropFileChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel)); + #end chartContainerB.addComponent(songVariationChartDataEntry); } } @@ -768,6 +884,7 @@ class ChartEditorDialogHandler if (songMetadataVariation == null) { // Tell the user the load was not successful. + #if !mac NotificationManager.instance.addNotification( { title: 'Failure', @@ -775,12 +892,14 @@ class ChartEditorDialogHandler type: NotificationType.Error, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); + #end return; } songMetadata.set(variation, songMetadataVariation); // Tell the user the load was successful. + #if !mac NotificationManager.instance.addNotification( { title: 'Success', @@ -788,8 +907,13 @@ class ChartEditorDialogHandler type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); + #end + #if FILE_DROP_SUPPORTED label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}'; + #else + label.text = 'Metadata file (click to browse)\n${path.file}.${path.ext}'; + #end if (variation == Constants.DEFAULT_VARIATION) constructVariationEntries(songMetadataVariation.playData.songVariations); }; @@ -797,7 +921,7 @@ class ChartEditorDialogHandler onClickMetadataVariation = function(variation:String, label:Label, _event:UIEvent) { Dialogs.openBinaryFile('Open Chart ($variation) Metadata', [ {label: 'JSON File (.json)', extension: 'json'}], function(selectedFile) { - if (selectedFile != null) + if (selectedFile != null && selectedFile.bytes != null) { trace('Selected file: ' + selectedFile.name); @@ -809,6 +933,7 @@ class ChartEditorDialogHandler songMetadata.set(variation, songMetadataVariation); // Tell the user the load was successful. + #if !mac NotificationManager.instance.addNotification( { title: 'Success', @@ -816,8 +941,13 @@ class ChartEditorDialogHandler type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); + #end + #if FILE_DROP_SUPPORTED label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}'; + #else + label.text = 'Metadata file (click to browse)\n${selectedFile.name}'; + #end if (variation == Constants.DEFAULT_VARIATION) constructVariationEntries(songMetadataVariation.playData.songVariations); } @@ -838,6 +968,7 @@ class ChartEditorDialogHandler state.noteDisplayDirty = true; // Tell the user the load was successful. + #if !mac NotificationManager.instance.addNotification( { title: 'Success', @@ -845,14 +976,19 @@ class ChartEditorDialogHandler type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); + #end + #if FILE_DROP_SUPPORTED label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}'; + #else + label.text = 'Chart data file (click to browse)\n${path.file}.${path.ext}'; + #end }; onClickChartDataVariation = function(variation:String, label:Label, _event:UIEvent) { Dialogs.openBinaryFile('Open Chart ($variation) Metadata', [ {label: 'JSON File (.json)', extension: 'json'}], function(selectedFile) { - if (selectedFile != null) + if (selectedFile != null && selectedFile.bytes != null) { trace('Selected file: ' + selectedFile.name); @@ -866,6 +1002,7 @@ class ChartEditorDialogHandler state.noteDisplayDirty = true; // Tell the user the load was successful. + #if !mac NotificationManager.instance.addNotification( { title: 'Success', @@ -873,18 +1010,37 @@ class ChartEditorDialogHandler type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); + #end + #if FILE_DROP_SUPPORTED label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}'; + #else + label.text = 'Chart data file (click to browse)\n${selectedFile.name}'; + #end } }); } var metadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); - var metadataEntryLabel:Label = metadataEntry.findComponent('chartEntryLabel', Label); + var metadataEntryLabel:Null<Label> = metadataEntry.findComponent('chartEntryLabel', Label); + if (metadataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog'; + + #if FILE_DROP_SUPPORTED metadataEntryLabel.text = 'Drag and drop <song>-metadata.json file, or click to browse.'; + #else + metadataEntryLabel.text = 'Click to browse for <song>-metadata.json file.'; + #end metadataEntry.onClick = onClickMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel); addDropHandler(metadataEntry, onDropFileMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel)); + metadataEntry.onMouseOver = function(_event) { + metadataEntry.swapClass('upload-bg', 'upload-bg-hover'); + Cursor.cursorMode = Pointer; + } + metadataEntry.onMouseOut = function(_event) { + metadataEntry.swapClass('upload-bg-hover', 'upload-bg'); + Cursor.cursorMode = Default; + } chartContainerA.addComponent(metadataEntry); @@ -898,9 +1054,10 @@ class ChartEditorDialogHandler * @param closable * @return Dialog */ - public static function openImportChartDialog(state:ChartEditorState, format:String, closable:Bool = true):Dialog + public static function openImportChartDialog(state:ChartEditorState, format:String, closable:Bool = true):Null<Dialog> { - var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT, true, closable); + var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT, true, closable); + if (dialog == null) return null; var prettyFormat:String = switch (format) { @@ -916,19 +1073,20 @@ class ChartEditorDialogHandler dialog.title = 'Import Chart - ${prettyFormat}'; - var buttonCancel:Button = dialog.findComponent('dialogCancel', Button); + var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); + if (buttonCancel == null) throw 'Could not locate dialogCancel button in Import Chart dialog'; buttonCancel.onClick = function(_event) { dialog.hideDialog(DialogButton.CANCEL); } - var importBox:Box = dialog.findComponent('importBox', Box); + var importBox:Null<Box> = dialog.findComponent('importBox', Box); + if (importBox == null) throw 'Could not locate importBox in Import Chart dialog'; importBox.onMouseOver = function(_event) { importBox.swapClass('upload-bg', 'upload-bg-hover'); Cursor.cursorMode = Pointer; } - importBox.onMouseOut = function(_event) { importBox.swapClass('upload-bg-hover', 'upload-bg'); Cursor.cursorMode = Default; @@ -937,8 +1095,8 @@ class ChartEditorDialogHandler var onDropFile:String->Void; importBox.onClick = function(_event) { - Dialogs.openBinaryFile('Import Chart - ${prettyFormat}', [fileFilter], function(selectedFile:SelectedFileInfo) { - if (selectedFile != null) + Dialogs.openBinaryFile('Import Chart - ${prettyFormat}', fileFilter != null ? [fileFilter] : [], function(selectedFile:SelectedFileInfo) { + if (selectedFile != null && selectedFile.bytes != null) { trace('Selected file: ' + selectedFile.fullPath); var selectedFileJson:Dynamic = SerializerUtil.fromJSONBytes(selectedFile.bytes); @@ -948,6 +1106,7 @@ class ChartEditorDialogHandler state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]); dialog.hideDialog(DialogButton.APPLY); + #if !mac NotificationManager.instance.addNotification( { title: 'Success', @@ -955,6 +1114,7 @@ class ChartEditorDialogHandler type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); + #end } }); } @@ -968,6 +1128,7 @@ class ChartEditorDialogHandler state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]); dialog.hideDialog(DialogButton.APPLY); + #if !mac NotificationManager.instance.addNotification( { title: 'Success', @@ -975,6 +1136,7 @@ class ChartEditorDialogHandler type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); + #end }; addDropHandler(importBox, onDropFile); @@ -988,7 +1150,7 @@ class ChartEditorDialogHandler * @param state The current chart editor state. * @return The dialog that was opened. */ - public static inline function openUserGuideDialog(state:ChartEditorState):Dialog + public static inline function openUserGuideDialog(state:ChartEditorState):Null<Dialog> { return openDialog(state, CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT, true, true); } @@ -998,9 +1160,9 @@ class ChartEditorDialogHandler * @param modal Makes the background uninteractable while the dialog is open. * @param closable Hides the close button on the dialog, preventing it from being closed unless the user interacts with the dialog. */ - static function openDialog(state:ChartEditorState, key:String, modal:Bool = true, closable:Bool = true):Dialog + static function openDialog(state:ChartEditorState, key:String, modal:Bool = true, closable:Bool = true):Null<Dialog> { - var dialog:Dialog = cast state.buildComponent(key); + var dialog:Null<Dialog> = cast state.buildComponent(key); if (dialog == null) return null; dialog.destroyOnClose = true; diff --git a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx index 2524f014c..4ee6eda9f 100644 --- a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx +++ b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx @@ -16,6 +16,7 @@ import funkin.play.song.SongData.SongEventData; * A event sprite that can be used to display a song event in a chart. * Designed to be used and reused efficiently. Has no gameplay functionality. */ +@:nullSafety class ChartEditorEventSprite extends FlxSprite { public static final DEFAULT_EVENT = 'Default'; @@ -45,16 +46,18 @@ class ChartEditorEventSprite extends FlxSprite refresh(); } + static var eventFrames:Null<FlxFramesCollection> = null; + /** * Build a set of animations to allow displaying different types of chart events. * @param force `true` to force rebuilding the frames. */ static function buildFrames(force:Bool = false):FlxFramesCollection { - static var eventFrames:FlxFramesCollection = null; - if (eventFrames != null && !force) return eventFrames; - eventFrames = new FlxAtlasFrames(null); + + initEmptyEventFrames(); + if (eventFrames == null) throw 'Failed to initialize empty event frames.'; // Push the default event as a frame. var defaultFrames:FlxAtlasFrames = Paths.getSparrowAtlas('ui/chart-editor/events/$DEFAULT_EVENT'); @@ -83,6 +86,12 @@ class ChartEditorEventSprite extends FlxSprite return eventFrames; } + @:nullSafety(Off) + static function initEmptyEventFrames():Void + { + eventFrames = new FlxAtlasFrames(null); + } + function buildAnimations():Void { var eventNames:Array<String> = [DEFAULT_EVENT].concat(SongEventParser.listEventIds()); @@ -133,6 +142,8 @@ class ChartEditorEventSprite extends FlxSprite public function updateEventPosition(?origin:FlxObject) { + if (this.eventData == null) return; + this.x = (ChartEditorState.STRUMLINE_SIZE * 2 + 1 - 1) * ChartEditorState.GRID_SIZE; if (this.eventData.stepTime >= 0) this.y = this.eventData.stepTime * ChartEditorState.GRID_SIZE; diff --git a/source/funkin/ui/debug/charting/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/ChartEditorHoldNoteSprite.hx index 5805874f6..ebf65c001 100644 --- a/source/funkin/ui/debug/charting/ChartEditorHoldNoteSprite.hx +++ b/source/funkin/ui/debug/charting/ChartEditorHoldNoteSprite.hx @@ -14,6 +14,7 @@ import funkin.play.song.SongData.SongNoteData; * A hold note sprite that can be used to display a note in a chart. * Designed to be used and reused efficiently. Has no gameplay functionality. */ +@:nullSafety class ChartEditorHoldNoteSprite extends SustainTrail { /** @@ -110,8 +111,10 @@ class ChartEditorHoldNoteSprite extends SustainTrail return !aboveViewArea && !belowViewArea; } - public function updateHoldNotePosition(?origin:FlxObject) + public function updateHoldNotePosition(?origin:FlxObject):Void { + if (this.noteData == null) return; + var cursorColumn:Int = this.noteData.data; if (cursorColumn < 0) cursorColumn = 0; @@ -139,8 +142,9 @@ class ChartEditorHoldNoteSprite extends SustainTrail { // noteData.stepTime is a calculated value which accounts for BPM changes var stepTime:Float = this.noteData.stepTime; - var roundedStepTime:Float = Math.floor(stepTime + 0.01); // Add epsilon to fix rounding issues - this.y = roundedStepTime * ChartEditorState.GRID_SIZE; + // Add epsilon to fix rounding issues? + // var roundedStepTime:Float = Math.floor((stepTime + 0.01) / noteSnapRatio) * noteSnapRatio; + this.y = stepTime * ChartEditorState.GRID_SIZE; } this.x += ChartEditorState.GRID_SIZE / 2; diff --git a/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx b/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx index 69655bfe5..be45676f2 100644 --- a/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx +++ b/source/funkin/ui/debug/charting/ChartEditorNotePreview.hx @@ -10,6 +10,7 @@ import flixel.util.FlxSpriteUtil; /** * Handles the note scrollbar preview in the chart editor. */ +@:nullSafety class ChartEditorNotePreview extends FlxSprite { // diff --git a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx index 1c440f6ed..10e0f9045 100644 --- a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx +++ b/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx @@ -3,6 +3,8 @@ package funkin.ui.debug.charting; import flixel.FlxObject; import flixel.FlxSprite; import flixel.graphics.frames.FlxFramesCollection; +import flixel.graphics.frames.FlxAtlasFrames; +import flixel.graphics.frames.FlxFrame; import flixel.graphics.frames.FlxTileFrames; import flixel.math.FlxPoint; import funkin.play.song.SongData.SongNoteData; @@ -11,6 +13,7 @@ import funkin.play.song.SongData.SongNoteData; * A note sprite that can be used to display a note in a chart. * Designed to be used and reused efficiently. Has no gameplay functionality. */ +@:nullSafety class ChartEditorNoteSprite extends FlxSprite { /** @@ -32,7 +35,7 @@ class ChartEditorNoteSprite extends FlxSprite /** * The name of the note style currently in use. */ - public var noteStyle(get, null):String; + public var noteStyle(get, never):String; public function new(parent:ChartEditorState) { @@ -45,6 +48,8 @@ class ChartEditorNoteSprite extends FlxSprite initFrameCollection(); } + if (noteFrameCollection == null) throw 'ERROR: Could not initialize note sprite animations.'; + this.frames = noteFrameCollection; // Initialize all the animations, not just the one we're going to use immediately, @@ -77,18 +82,19 @@ class ChartEditorNoteSprite extends FlxSprite */ static function initFrameCollection():Void { - noteFrameCollection = new FlxFramesCollection(null, ATLAS, null); + buildEmptyFrameCollection(); + if (noteFrameCollection == null) return; // TODO: Automatically iterate over the list of note skins. // Normal notes - var frameCollectionNormal = Paths.getSparrowAtlas('NOTE_assets'); + var frameCollectionNormal:FlxAtlasFrames = Paths.getSparrowAtlas('NOTE_assets'); for (frame in frameCollectionNormal.frames) { noteFrameCollection.pushFrame(frame); } - var frameCollectionNormal2 = Paths.getSparrowAtlas('NoteHoldNormal'); + var frameCollectionNormal2:FlxAtlasFrames = Paths.getSparrowAtlas('NoteHoldNormal'); for (frame in frameCollectionNormal2.frames) { @@ -101,13 +107,20 @@ class ChartEditorNoteSprite extends FlxSprite var frameCollectionPixel = FlxTileFrames.fromGraphic(graphicPixel, new FlxPoint(17, 17)); for (i in 0...frameCollectionPixel.frames.length) { - var frame = frameCollectionPixel.frames[i]; + var frame:Null<FlxFrame> = frameCollectionPixel.frames[i]; + if (frame == null) continue; frame.name = 'pixel' + i; noteFrameCollection.pushFrame(frame); } } + @:nullSafety(Off) + static function buildEmptyFrameCollection():Void + { + noteFrameCollection = new FlxFramesCollection(null, ATLAS, null); + } + function set_noteData(value:Null<SongNoteData>):Null<SongNoteData> { this.noteData = value; @@ -130,7 +143,7 @@ class ChartEditorNoteSprite extends FlxSprite return this.noteData; } - public function updateNotePosition(?origin:FlxObject) + public function updateNotePosition(?origin:FlxObject):Void { if (this.noteData == null) return; @@ -160,9 +173,7 @@ class ChartEditorNoteSprite extends FlxSprite if (this.noteData.stepTime >= 0) { // noteData.stepTime is a calculated value which accounts for BPM changes - var stepTime:Float = this.noteData.stepTime; - var roundedStepTime:Float = Math.floor(stepTime + 0.01); // Add epsilon to fix rounding issues - this.y = roundedStepTime * ChartEditorState.GRID_SIZE; + this.y = this.noteData.stepTime * ChartEditorState.GRID_SIZE; } if (origin != null) @@ -180,6 +191,8 @@ class ChartEditorNoteSprite extends FlxSprite public function playNoteAnimation():Void { + if (this.noteData == null) return; + // Decide whether to display a note or a sustain. var baseAnimationName:String = 'tap'; diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 870b6953e..c0cb473e2 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -179,12 +179,24 @@ class ChartEditorState extends HaxeUIState */ static final SNAP_QUANTS:Array<Int> = [4, 8, 12, 16, 20, 24, 32, 48, 64, 96, 192]; + static final BASE_QUANT:Int = 16; + /** * INSTANCE DATA */ // ============================== - var noteSnapQuantIndex:Int = 3; + public var currentZoomLevel:Float = 1.0; + /** + * The internal index of what note snapping value is in use. + * Increment to make placement more preceise and decrement to make placement less precise. + */ + var noteSnapQuantIndex:Int = 3; // default is 16 + + /** + * The current note snapping value. + * For example, `32` when snapping to 32nd notes. + */ public var noteSnapQuant(get, never):Int; function get_noteSnapQuant():Int @@ -192,6 +204,17 @@ class ChartEditorState extends HaxeUIState return SNAP_QUANTS[noteSnapQuantIndex]; } + /** + * The ratio of the current note snapping value to the default. + * For example, `32` becomes `0.5` when snapping to 16th notes. + */ + public var noteSnapRatio(get, never):Float; + + function get_noteSnapRatio():Float + { + return BASE_QUANT / noteSnapQuant; + } + /** * scrollPosition is the current position in the song, in pixels. * One pixel is 1/40 of 1 step, and 1/160 of 1 beat. @@ -1098,6 +1121,11 @@ class ChartEditorState extends HaxeUIState */ var gridGhostNote:Null<ChartEditorNoteSprite> = null; + /** + * A sprite used to indicate the note that will be placed on click. + */ + var gridGhostHoldNote:Null<ChartEditorHoldNoteSprite> = null; + /** * A sprite used to indicate the event that will be placed on click. */ @@ -1193,6 +1221,9 @@ class ChartEditorState extends HaxeUIState // Set the z-index of the HaxeUI. this.component.zIndex = 100; + // Show the mouse cursor. + Cursor.show(); + fixCamera(); // Get rid of any music from the previous state. @@ -1271,6 +1302,13 @@ class ChartEditorState extends HaxeUIState add(gridGhostNote); gridGhostNote.zIndex = 11; + gridGhostHoldNote = new ChartEditorHoldNoteSprite(this); + gridGhostHoldNote.alpha = 0.6; + gridGhostHoldNote.noteData = new SongNoteData(0, 0, 0, ""); + gridGhostHoldNote.visible = false; + add(gridGhostHoldNote); + gridGhostHoldNote.zIndex = 11; + gridGhostEvent = new ChartEditorEventSprite(this); gridGhostEvent.alpha = 0.6; gridGhostEvent.eventData = new SongEventData(-1, '', {}); @@ -1280,9 +1318,13 @@ class ChartEditorState extends HaxeUIState buildNoteGroup(); - gridPlayheadScrollArea = new FlxSprite(gridTiledSprite.x - PLAYHEAD_SCROLL_AREA_WIDTH, - MENU_BAR_HEIGHT).makeGraphic(PLAYHEAD_SCROLL_AREA_WIDTH, FlxG.height - MENU_BAR_HEIGHT, PLAYHEAD_SCROLL_AREA_COLOR); + 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 = gridTiledSprite.x - PLAYHEAD_SCROLL_AREA_WIDTH; + gridPlayheadScrollArea.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; gridPlayheadScrollArea.zIndex = 25; // The playhead that show the current position in the song. @@ -1766,6 +1808,7 @@ class ChartEditorState extends HaxeUIState // These ones only happen if the modal dialog is not open. handleScrollKeybinds(); + // handleZoom(); handleSnap(); handleCursor(); @@ -1842,14 +1885,21 @@ class ChartEditorState extends HaxeUIState **/ function handleScrollKeybinds():Void { - // Don't scroll when the cursor is over the UI. - if (isCursorOverHaxeUI) return; + // Don't scroll when the cursor is over the UI, unless a playbar button (the << >> ones) is pressed. + if (isCursorOverHaxeUI && playbarButtonPressed == null) return; var scrollAmount:Float = 0; // Amount to scroll the grid. var playheadAmount:Float = 0; // Amount to scroll the playhead relative to the grid. var shouldPause:Bool = false; // Whether to pause the song when scrolling. var shouldEase:Bool = false; // Whether to ease the scroll. + // Mouse Wheel = Scroll + if (FlxG.mouse.wheel != 0 && !FlxG.keys.pressed.CONTROL) + { + scrollAmount = -10 * FlxG.mouse.wheel; + shouldPause = true; + } + // Up Arrow = Scroll Up if (upKeyHandler.activated && currentLiveInputStyle != LiveInputStyle.WASD) { @@ -1867,13 +1917,15 @@ class ChartEditorState extends HaxeUIState if (pageUpKeyHandler.activated) { var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure; - var targetScrollPosition:Float = Math.floor(scrollPositionInPixels / measureHeight) * measureHeight; + 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. - if (Math.abs(targetScrollPosition - scrollPositionInPixels) < GRID_SIZE) + var targetScrollAmount = Math.abs(targetScrollPosition - playheadPos); + if (targetScrollAmount < GRID_SIZE) { - targetScrollPosition -= GRID_SIZE * 4 * Conductor.beatsPerMeasure; + targetScrollPosition -= GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.beatsPerMeasure; } - scrollAmount = targetScrollPosition - scrollPositionInPixels; + scrollAmount = targetScrollPosition - playheadPos; shouldPause = true; } @@ -1888,13 +1940,15 @@ class ChartEditorState extends HaxeUIState if (pageDownKeyHandler.activated) { var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure; - var targetScrollPosition:Float = Math.ceil(scrollPositionInPixels / measureHeight) * measureHeight; + 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. - if (Math.abs(targetScrollPosition - scrollPositionInPixels) < GRID_SIZE) + var targetScrollAmount = Math.abs(targetScrollPosition - playheadPos); + if (targetScrollAmount < GRID_SIZE) { - targetScrollPosition += GRID_SIZE * 4 * Conductor.beatsPerMeasure; + targetScrollPosition += GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.beatsPerMeasure; } - scrollAmount = targetScrollPosition - scrollPositionInPixels; + scrollAmount = targetScrollPosition - playheadPos; shouldPause = true; } @@ -1905,13 +1959,6 @@ class ChartEditorState extends HaxeUIState shouldPause = true; } - // Mouse Wheel = Scroll - if (FlxG.mouse.wheel != 0 && !FlxG.keys.pressed.CONTROL) - { - scrollAmount = -10 * FlxG.mouse.wheel; - shouldPause = true; - } - // Middle Mouse + Drag = Scroll but move the playhead the same amount. if (FlxG.mouse.pressedMiddle) { @@ -2020,6 +2067,11 @@ class ChartEditorState extends HaxeUIState if (shouldHandleCursor) { + // Over the course of this big conditional block, + // we determine what the cursor should look like, + // and fall back to the default cursor if none of the conditions are met. + var targetCursorMode:Null<CursorMode> = null; + if (gridTiledSprite == null) throw "ERROR: Tried to handle cursor, but gridTiledSprite is null! Check ChartEditorState.buildGrid()"; var overlapsGrid:Bool = FlxG.mouse.overlaps(gridTiledSprite); @@ -2029,9 +2081,9 @@ class ChartEditorState extends HaxeUIState var cursorY:Float = FlxG.mouse.screenY - gridTiledSprite.y; var overlapsSelectionBorder:Bool = overlapsGrid - && (cursorX % 40) < (GRID_SELECTION_BORDER_WIDTH / 2) + && ((cursorX % 40) < (GRID_SELECTION_BORDER_WIDTH / 2) || (cursorX % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2)) - || (cursorY % 40) < (GRID_SELECTION_BORDER_WIDTH / 2) || (cursorY % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2)); + || (cursorY % 40) < (GRID_SELECTION_BORDER_WIDTH / 2) || (cursorY % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2))); if (FlxG.mouse.justPressed) { @@ -2047,6 +2099,8 @@ class ChartEditorState extends HaxeUIState else if (!overlapsGrid || overlapsSelectionBorder) { selectionBoxStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); + // Drawing selection box. + targetCursorMode = Crosshair; } else { @@ -2057,23 +2111,6 @@ class ChartEditorState extends HaxeUIState } } - if (gridPlayheadScrollAreaPressed) - { - Cursor.cursorMode = Grabbing; - } - else if (notePreviewScrollAreaStartPos != null) - { - Cursor.cursorMode = Pointer; - } - else if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea)) - { - Cursor.cursorMode = Pointer; - } - else - { - Cursor.cursorMode = Default; - } - if (gridPlayheadScrollAreaPressed && FlxG.mouse.released) { gridPlayheadScrollAreaPressed = false; @@ -2090,11 +2127,18 @@ class ChartEditorState extends HaxeUIState // Move the playhead to the cursor position. this.playheadPositionInPixels = FlxG.mouse.screenY - MENU_BAR_HEIGHT - GRID_TOP_PAD; moveSongToScrollPosition(); + + // Cursor should be a grabby hand. + if (targetCursorMode == null) targetCursorMode = Grabbing; } // The song position of the cursor, in steps. - var cursorFractionalStep:Float = cursorY / GRID_SIZE / (16 / noteSnapQuant); + var cursorFractionalStep:Float = cursorY / GRID_SIZE; var cursorMs:Float = Conductor.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); + // The direction value for the column at the cursor. var cursorColumn:Int = Math.floor(cursorX / GRID_SIZE); if (cursorColumn < 0) cursorColumn = 0; @@ -2217,6 +2261,8 @@ class ChartEditorState extends HaxeUIState } else { + // Clicking and dragging. + // Scroll the screen if the mouse is above or below the grid. if (FlxG.mouse.screenY < MENU_BAR_HEIGHT) { @@ -2242,6 +2288,8 @@ class ChartEditorState extends HaxeUIState selectionRect.width = Math.abs(FlxG.mouse.screenX - selectionBoxStartPos.x); selectionRect.height = Math.abs(FlxG.mouse.screenY - selectionBoxStartPos.y); setSelectionBoxBounds(selectionRect); + + targetCursorMode = Crosshair; } } else if (FlxG.mouse.justReleased) @@ -2333,7 +2381,9 @@ class ChartEditorState extends HaxeUIState } else if (notePreviewScrollAreaStartPos != null) { - trace('Updating current song time while clicking and holding...'); + // Player is clicking and holding on note preview to scrub around. + targetCursorMode = Grabbing; + var clickedPosInPixels:Float = FlxMath.remapToRange(FlxG.mouse.screenY, (notePreview?.y ?? 0.0), (notePreview?.y ?? 0.0) + (notePreview?.height ?? 0.0), 0, songLengthInPixels); @@ -2344,15 +2394,17 @@ class ChartEditorState extends HaxeUIState { // Handle extending the note as you drag. - // TODO: This should be beat snapped? - var dragLengthSteps:Float = Conductor.getTimeInSteps(cursorMs) - currentPlaceNoteData.stepTime; + var dragLengthSteps:Float = Conductor.getTimeInSteps(cursorSnappedMs) - currentPlaceNoteData.stepTime; + var dragLengthMs:Float = dragLengthSteps * Conductor.stepLengthMs; + var dragLengthPixels:Float = dragLengthSteps * GRID_SIZE; - // Without this, the newly placed note feels too short compared to the user's input. - var INCREMENT:Float = 1.0; - // TODO: Make this not busted with BPM changes - var dragLengthMs:Float = Math.floor(dragLengthSteps + INCREMENT) * Conductor.stepLengthMs; + gridGhostHoldNote.visible = true; + gridGhostHoldNote.noteData = gridGhostNote.noteData; + gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection(); - // TODO: Add and update some sort of preview? + gridGhostHoldNote.setHeightDirectly(dragLengthPixels); + + gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes); if (FlxG.mouse.justReleased) { @@ -2439,14 +2491,14 @@ class ChartEditorState extends HaxeUIState { // Create an event and place it in the chart. // TODO: Figure out configuring event data. - var newEventData:SongEventData = new SongEventData(cursorMs, selectedEventKind, selectedEventData); + var newEventData:SongEventData = new SongEventData(cursorSnappedMs, selectedEventKind, selectedEventData); performCommand(new AddEventsCommand([newEventData], FlxG.keys.pressed.CONTROL)); } else { // Create a note and place it in the chart. - var newNoteData:SongNoteData = new SongNoteData(cursorMs, cursorColumn, 0, selectedNoteKind); + var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, selectedNoteKind); performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL)); @@ -2501,13 +2553,12 @@ class ChartEditorState extends HaxeUIState // Handle grid cursor. if (overlapsGrid && !overlapsSelectionBorder && !gridPlayheadScrollAreaPressed) { - Cursor.cursorMode = Pointer; - // Indicate that we can place a note here. if (cursorColumn == eventColumn) { if (gridGhostNote != null) gridGhostNote.visible = false; + gridGhostHoldNote.visible = false; if (gridGhostEvent == null) throw "ERROR: Tried to handle cursor, but gridGhostEvent is null! Check ChartEditorState.buildGrid()"; @@ -2517,11 +2568,13 @@ class ChartEditorState extends HaxeUIState { eventData.event = selectedEventKind; } - eventData.time = cursorMs; + eventData.time = cursorSnappedMs; gridGhostEvent.visible = true; gridGhostEvent.eventData = eventData; gridGhostEvent.updateEventPosition(renderedEvents); + + targetCursorMode = Cell; } else { @@ -2537,35 +2590,63 @@ class ChartEditorState extends HaxeUIState noteData.data = cursorColumn; gridGhostNote.playNoteAnimation(); } - noteData.time = cursorMs; + noteData.time = cursorSnappedMs; gridGhostNote.visible = true; gridGhostNote.noteData = noteData; gridGhostNote.updateNotePosition(renderedNotes); - } - // gridCursor.visible = true; - // // X and Y are the cursor position relative to the grid, snapped to the top left of the grid square. - // gridCursor.x = Math.floor(cursorX / GRID_SIZE) * GRID_SIZE + gridTiledSprite.x + (GRID_SELECTION_BORDER_WIDTH / 2); - // gridCursor.y = cursorStep * GRID_SIZE + gridTiledSprite.y + (GRID_SELECTION_BORDER_WIDTH / 2); + targetCursorMode = Cell; + } } else { if (gridGhostNote != null) gridGhostNote.visible = false; + if (gridGhostHoldNote != null) gridGhostHoldNote.visible = false; if (gridGhostEvent != null) gridGhostEvent.visible = false; - Cursor.cursorMode = Default; } } + + if (targetCursorMode == null) + { + if (FlxG.mouse.pressed) + { + if (overlapsSelectionBorder) + { + targetCursorMode = Crosshair; + } + } + else + { + if (FlxG.mouse.overlaps(notePreview)) + { + targetCursorMode = Pointer; + } + else if (FlxG.mouse.overlaps(gridPlayheadScrollArea)) + { + targetCursorMode = Pointer; + } + else if (overlapsSelectionBorder) + { + targetCursorMode = Crosshair; + } + else if (overlapsGrid) + { + targetCursorMode = Cell; + } + } + } + + // Actually set the cursor mode to the one we specified earlier. + Cursor.cursorMode = targetCursorMode ?? Default; } else { if (gridGhostNote != null) gridGhostNote.visible = false; + if (gridGhostHoldNote != null) gridGhostHoldNote.visible = false; if (gridGhostEvent != null) gridGhostEvent.visible = false; - } - if (isCursorOverHaxeUIButton && Cursor.cursorMode == Default) - { - Cursor.cursorMode = Pointer; + // Do not set Cursor.cursorMode here, because it will be set by the HaxeUI. } } @@ -2617,7 +2698,7 @@ class ChartEditorState extends HaxeUIState var displayedHoldNoteData:Array<SongNoteData> = []; for (holdNoteSprite in renderedHoldNotes.members) { - if (holdNoteSprite == null || !holdNoteSprite.exists || !holdNoteSprite.visible) continue; + if (holdNoteSprite == null || holdNoteSprite.noteData == null || !holdNoteSprite.exists || !holdNoteSprite.visible) continue; if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD)) { @@ -2693,7 +2774,8 @@ class ChartEditorState extends HaxeUIState // The note sprite handles animation playback and positioning. noteSprite.noteData = noteData; - // Setting note data resets position relative to the grid so we fix that. + // Setting note data resets the position relative to the group! + // If we don't update the note position AFTER setting the note data, the note will be rendered offscreen at y=5000. noteSprite.updateNotePosition(renderedNotes); // Add hold notes that are now visible (and not already displayed). @@ -2811,10 +2893,10 @@ class ChartEditorState extends HaxeUIState } // Sort the notes DESCENDING. This keeps the sustain behind the associated note. - renderedNotes.sort(FlxSort.byY, FlxSort.DESCENDING); + renderedNotes.sort(FlxSort.byY, FlxSort.DESCENDING); // TODO: .group.insertionSort() // Sort the events DESCENDING. This keeps the sustain behind the associated note. - renderedEvents.sort(FlxSort.byY, FlxSort.DESCENDING); + renderedEvents.sort(FlxSort.byY, FlxSort.DESCENDING); // TODO: .group.insertionSort() } // Add a debug value which displays the current size of the note pool. @@ -2874,6 +2956,18 @@ class ChartEditorState extends HaxeUIState */ function handleFileKeybinds():Void { + // CTRL + N = New Chart + if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.N) + { + ChartEditorDialogHandler.openWelcomeDialog(this, true); + } + + // CTRL + O = Open Chart + if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.O) + { + ChartEditorDialogHandler.openBrowseWizard(this, true); + } + // CTRL + Q = Quit to Menu if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.Q) { @@ -3112,7 +3206,8 @@ class ChartEditorState extends HaxeUIState difficultySelectDirty = false; // Manage the Select Difficulty tree view. - var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); + var difficultyToolbox:Null<CollapsibleDialog> = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); + if (difficultyToolbox == null) return; var treeView:Null<TreeView> = difficultyToolbox.findComponent('difficultyToolboxTree'); if (treeView == null) return; @@ -3158,7 +3253,7 @@ class ChartEditorState extends HaxeUIState if (treeView == null) { // Manage the Select Difficulty tree view. - var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); + var difficultyToolbox:Null<CollapsibleDialog> = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); if (difficultyToolbox == null) return; treeView = difficultyToolbox.findComponent('difficultyToolboxTree'); @@ -3172,7 +3267,7 @@ class ChartEditorState extends HaxeUIState function handlePlayerPreviewToolbox():Void { // Manage the Select Difficulty tree view. - var charPreviewToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); + var charPreviewToolbox:Null<CollapsibleDialog> = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); if (charPreviewToolbox == null) return; var charPlayer:Null<CharacterPlayer> = charPreviewToolbox.findComponent('charPlayer'); @@ -3207,7 +3302,7 @@ class ChartEditorState extends HaxeUIState function handleOpponentPreviewToolbox():Void { // Manage the Select Difficulty tree view. - var charPreviewToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); + var charPreviewToolbox:Null<CollapsibleDialog> = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); if (charPreviewToolbox == null) return; var charPlayer:Null<CharacterPlayer> = charPreviewToolbox.findComponent('charPlayer'); @@ -3279,7 +3374,7 @@ class ChartEditorState extends HaxeUIState { if (treeView == null) { - var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); + var difficultyToolbox:Null<CollapsibleDialog> = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); if (difficultyToolbox == null) return null; treeView = difficultyToolbox.findComponent('difficultyToolboxTree'); @@ -3342,7 +3437,8 @@ class ChartEditorState extends HaxeUIState */ function refreshSongMetadataToolbox():Void { - var toolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); + var toolbox:Null<CollapsibleDialog> = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); + if (toolbox == null) return; var inputSongName:Null<TextField> = toolbox.findComponent('inputSongName', TextField); if (inputSongName != null) inputSongName.value = currentSongMetadata.songName; @@ -3670,25 +3766,29 @@ class ChartEditorState extends HaxeUIState this.scrollPositionInPixels = value; // Move the grid sprite to the correct position. - if (isViewDownscroll) + if (gridTiledSprite != null) { - if (gridTiledSprite != null) gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD); - } - else - { - if (gridTiledSprite != null) gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD); + if (isViewDownscroll) + { + gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD); + gridPlayheadScrollArea.y = gridTiledSprite.y; + } + else + { + gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD); + gridPlayheadScrollArea.y = gridTiledSprite.y; + } } + // Move the rendered notes to the correct position. renderedNotes.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); renderedHoldNotes.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); renderedEvents.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); renderedSelectionSquares.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0); - // Offset the selection box start position, if we are dragging. if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff; // Update the note preview viewport box. setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); - return this.scrollPositionInPixels; } @@ -3847,6 +3947,11 @@ class ChartEditorState extends HaxeUIState songLengthInMs = audioInstTrack.length; if (gridTiledSprite != null) gridTiledSprite.height = songLengthInPixels; + if (gridPlayheadScrollArea != null) + { + gridPlayheadScrollArea.setGraphicSize(Std.int(gridPlayheadScrollArea.width), songLengthInPixels); + gridPlayheadScrollArea.updateHitbox(); + } buildSpectrogram(audioInstTrack); } @@ -3988,6 +4093,7 @@ class ChartEditorState extends HaxeUIState } } + #if !mac NotificationManager.instance.addNotification( { title: 'Success', @@ -3995,6 +4101,7 @@ class ChartEditorState extends HaxeUIState type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); + #end } /** @@ -4131,10 +4238,12 @@ class ChartEditorState extends HaxeUIState function sortChartData():Void { + // TODO: .insertionSort() currentSongChartNoteData.sort(function(a:SongNoteData, b:SongNoteData):Int { return FlxSort.byValues(FlxSort.ASCENDING, a.time, b.time); }); + // TODO: .insertionSort() currentSongChartEventData.sort(function(a:SongEventData, b:SongEventData):Int { return FlxSort.byValues(FlxSort.ASCENDING, a.time, b.time); }); @@ -4182,6 +4291,9 @@ class ChartEditorState extends HaxeUIState cleanupAutoSave(); + // Hide the mouse cursor on other states. + Cursor.hide(); + @:privateAccess ChartEditorNoteSprite.noteFrameCollection = null; } diff --git a/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx index 17906dac2..8a9bb8b03 100644 --- a/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx @@ -20,6 +20,7 @@ enum ChartEditorTheme /** * Static functions which handle building themed UI elements for a provided ChartEditorState. */ +@:nullSafety class ChartEditorThemeHandler { // TODO: There's probably a better system of organization for these colors. @@ -50,6 +51,11 @@ class ChartEditorThemeHandler static final GRID_MEASURE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4; static final GRID_MEASURE_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH; + // Horizontal divider between beats. + static final GRID_BEAT_DIVIDER_COLOR_LIGHT:FlxColor = 0xFFC1C1C1; + static final GRID_BEAT_DIVIDER_COLOR_DARK:FlxColor = 0xFF848484; + static final GRID_BEAT_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH; + // Border on the square highlighting selected notes. static final SELECTION_SQUARE_BORDER_COLOR_LIGHT:FlxColor = 0xFF339933; static final SELECTION_SQUARE_BORDER_COLOR_DARK:FlxColor = 0xFF339933; @@ -92,6 +98,7 @@ class ChartEditorThemeHandler */ static function updateBackground(state:ChartEditorState):Void { + if (state.menuBG == null) return; state.menuBG.color = switch (state.currentTheme) { case ChartEditorTheme.Light: BACKGROUND_COLOR_LIGHT; @@ -141,7 +148,7 @@ class ChartEditorThemeHandler ChartEditorState.GRID_SELECTION_BORDER_WIDTH), selectionBorderColor); - // Selection borders in the middle. + // Selection borders horizontally along the middle. for (i in 1...(Conductor.stepsPerMeasure)) { state.gridBitmap.fillRect(new Rectangle(0, (ChartEditorState.GRID_SIZE * i) - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), @@ -159,7 +166,7 @@ class ChartEditorThemeHandler state.gridBitmap.height), selectionBorderColor); - // Selection borders across the middle. + // Selection borders vertically along the middle. for (i in 1...TOTAL_COLUMN_COUNT) { state.gridBitmap.fillRect(new Rectangle((ChartEditorState.GRID_SIZE * i) - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), 0, @@ -172,7 +179,7 @@ class ChartEditorThemeHandler ChartEditorState.GRID_SELECTION_BORDER_WIDTH, state.gridBitmap.height), selectionBorderColor); - // Draw dividers between the measures. + // Draw horizontal dividers between the measures. var gridMeasureDividerColor:FlxColor = switch (state.currentTheme) { @@ -187,7 +194,30 @@ class ChartEditorThemeHandler var dividerLineBY:Float = state.gridBitmap.height - (GRID_MEASURE_DIVIDER_WIDTH / 2); state.gridBitmap.fillRect(new Rectangle(0, dividerLineBY, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor); - // Draw dividers between the strumlines. + // Draw horizontal dividers between the beats. + + var gridBeatDividerColor:FlxColor = switch (state.currentTheme) + { + case Light: GRID_BEAT_DIVIDER_COLOR_LIGHT; + case Dark: GRID_BEAT_DIVIDER_COLOR_DARK; + default: GRID_BEAT_DIVIDER_COLOR_LIGHT; + }; + + // Selection borders horizontally in the middle. + for (i in 1...(Conductor.stepsPerMeasure)) + { + if ((i % Conductor.beatsPerMeasure) == 0) + { + state.gridBitmap.fillRect(new Rectangle(0, (ChartEditorState.GRID_SIZE * i) - (GRID_BEAT_DIVIDER_WIDTH / 2), state.gridBitmap.width, + GRID_BEAT_DIVIDER_WIDTH), + gridBeatDividerColor); + } + } + + // 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) { diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx index 152615568..f67a69112 100644 --- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx @@ -39,6 +39,7 @@ enum ChartEditorToolMode /** * Static functions which handle building themed UI elements for a provided ChartEditorState. */ +@:nullSafety @:allow(funkin.ui.debug.charting.ChartEditorState) class ChartEditorToolboxHandler { @@ -56,7 +57,7 @@ class ChartEditorToolboxHandler public static function showToolbox(state:ChartEditorState, id:String):Void { - var toolbox:CollapsibleDialog = state.activeToolboxes.get(id); + var toolbox:Null<CollapsibleDialog> = state.activeToolboxes.get(id); if (toolbox == null) toolbox = initToolbox(state, id); @@ -95,7 +96,7 @@ class ChartEditorToolboxHandler public static function hideToolbox(state:ChartEditorState, id:String):Void { - var toolbox:CollapsibleDialog = state.activeToolboxes.get(id); + var toolbox:Null<CollapsibleDialog> = state.activeToolboxes.get(id); if (toolbox == null) toolbox = initToolbox(state, id); @@ -134,7 +135,7 @@ class ChartEditorToolboxHandler public static function minimizeToolbox(state:ChartEditorState, id:String):Void { - var toolbox:CollapsibleDialog = state.activeToolboxes.get(id); + var toolbox:Null<CollapsibleDialog> = state.activeToolboxes.get(id); if (toolbox == null) return; @@ -143,16 +144,16 @@ class ChartEditorToolboxHandler public static function maximizeToolbox(state:ChartEditorState, id:String):Void { - var toolbox:CollapsibleDialog = state.activeToolboxes.get(id); + var toolbox:Null<CollapsibleDialog> = state.activeToolboxes.get(id); if (toolbox == null) return; toolbox.minimized = false; } - public static function initToolbox(state:ChartEditorState, id:String):CollapsibleDialog + public static function initToolbox(state:ChartEditorState, id:String):Null<CollapsibleDialog> { - var toolbox:CollapsibleDialog = null; + var toolbox:Null<CollapsibleDialog> = null; switch (id) { case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT: @@ -193,9 +194,9 @@ class ChartEditorToolboxHandler * @param id The asset ID of the toolbox layout. * @return The toolbox. */ - public static function getToolbox(state:ChartEditorState, id:String):CollapsibleDialog + public static function getToolbox(state:ChartEditorState, id:String):Null<CollapsibleDialog> { - var toolbox:CollapsibleDialog = state.activeToolboxes.get(id); + var toolbox:Null<CollapsibleDialog> = state.activeToolboxes.get(id); // Initialize the toolbox without showing it. if (toolbox == null) toolbox = initToolbox(state, id); @@ -205,7 +206,7 @@ class ChartEditorToolboxHandler return toolbox; } - static function buildToolboxToolsLayout(state:ChartEditorState):CollapsibleDialog + static function buildToolboxToolsLayout(state:ChartEditorState):Null<CollapsibleDialog> { var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT); @@ -219,7 +220,8 @@ class ChartEditorToolboxHandler state.setUICheckboxSelected('menubarItemToggleToolboxTools', false); } - var toolsGroup:Group = toolbox.findComponent('toolboxToolsGroup', Group); + var toolsGroup:Null<Group> = toolbox.findComponent('toolboxToolsGroup', Group); + if (toolsGroup == null) throw 'ChartEditorToolboxHandler.buildToolboxToolsLayout() - Could not find toolboxToolsGroup component.'; if (toolsGroup == null) return null; @@ -242,7 +244,7 @@ class ChartEditorToolboxHandler static function onHideToolboxTools(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - static function buildToolboxNoteDataLayout(state:ChartEditorState):CollapsibleDialog + static function buildToolboxNoteDataLayout(state:ChartEditorState):Null<CollapsibleDialog> { var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT); @@ -256,9 +258,13 @@ class ChartEditorToolboxHandler state.setUICheckboxSelected('menubarItemToggleToolboxNotes', false); } - var toolboxNotesNoteKind:DropDown = toolbox.findComponent('toolboxNotesNoteKind', DropDown); - var toolboxNotesCustomKindLabel:Label = toolbox.findComponent('toolboxNotesCustomKindLabel', Label); - var toolboxNotesCustomKind:TextField = toolbox.findComponent('toolboxNotesCustomKind', TextField); + var toolboxNotesNoteKind:Null<DropDown> = toolbox.findComponent('toolboxNotesNoteKind', DropDown); + if (toolboxNotesNoteKind == null) throw 'ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Could not find toolboxNotesNoteKind component.'; + var toolboxNotesCustomKindLabel:Null<Label> = toolbox.findComponent('toolboxNotesCustomKindLabel', Label); + if (toolboxNotesCustomKindLabel == null) + throw 'ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Could not find toolboxNotesCustomKindLabel component.'; + var toolboxNotesCustomKind:Null<TextField> = toolbox.findComponent('toolboxNotesCustomKind', TextField); + if (toolboxNotesCustomKind == null) throw 'ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Could not find toolboxNotesCustomKind component.'; toolboxNotesNoteKind.onChange = function(event:UIEvent) { var isCustom:Bool = (event.data.id == '~CUSTOM~'); @@ -290,7 +296,7 @@ class ChartEditorToolboxHandler static function onHideToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - static function buildToolboxEventDataLayout(state:ChartEditorState):CollapsibleDialog + static function buildToolboxEventDataLayout(state:ChartEditorState):Null<CollapsibleDialog> { var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT); @@ -304,8 +310,10 @@ class ChartEditorToolboxHandler state.setUICheckboxSelected('menubarItemToggleToolboxEvents', false); } - var toolboxEventsEventKind:DropDown = toolbox.findComponent('toolboxEventsEventKind', DropDown); - var toolboxEventsDataGrid:Grid = toolbox.findComponent('toolboxEventsDataGrid', Grid); + var toolboxEventsEventKind:Null<DropDown> = toolbox.findComponent('toolboxEventsEventKind', DropDown); + if (toolboxEventsEventKind == null) throw 'ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Could not find toolboxEventsEventKind component.'; + var toolboxEventsDataGrid:Null<Grid> = toolbox.findComponent('toolboxEventsDataGrid', Grid); + if (toolboxEventsDataGrid == null) throw 'ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Could not find toolboxEventsDataGrid component.'; toolboxEventsEventKind.dataSource = new ArrayDataSource(); @@ -349,6 +357,8 @@ class ChartEditorToolboxHandler for (field in schema) { + if (field == null) continue; + // Add a label. var label:Label = new Label(); label.text = field.title; @@ -360,33 +370,36 @@ class ChartEditorToolboxHandler case INTEGER: var numberStepper:NumberStepper = new NumberStepper(); numberStepper.id = field.name; - numberStepper.step = field.step == null ? 1.0 : field.step; - numberStepper.min = field.min; - numberStepper.max = field.max; - numberStepper.value = field.defaultValue; + 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 == null ? 0.1 : field.step; - numberStepper.min = field.min; - numberStepper.max = field.max; - numberStepper.value = field.defaultValue; + numberStepper.step = field.step ?? 0.1; + numberStepper.min = field.min ?? 0.0; + numberStepper.max = field.max ?? 1.0; + if (field.defaultValue != null) numberStepper.value = field.defaultValue; input = numberStepper; case BOOL: var checkBox:CheckBox = new CheckBox(); checkBox.id = field.name; - checkBox.selected = field.defaultValue; + if (field.defaultValue != null) checkBox.selected = field.defaultValue; input = checkBox; case ENUM: var dropDown:DropDown = new DropDown(); dropDown.id = field.name; 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:String = field.keys.get(optionName); + var optionValue:Null<String> = field.keys.get(optionName); trace('$optionName : $optionValue'); dropDown.dataSource.add({value: optionValue, text: optionName}); } @@ -397,7 +410,7 @@ class ChartEditorToolboxHandler case STRING: input = new TextField(); input.id = field.name; - input.text = field.defaultValue; + if (field.defaultValue != null) input.text = field.defaultValue; default: // Unknown type. Display a label so we know what it is. input = new Label(); @@ -417,7 +430,7 @@ class ChartEditorToolboxHandler } } - static function buildToolboxDifficultyLayout(state:ChartEditorState):CollapsibleDialog + static function buildToolboxDifficultyLayout(state:ChartEditorState):Null<CollapsibleDialog> { var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); @@ -431,11 +444,20 @@ class ChartEditorToolboxHandler state.setUICheckboxSelected('menubarItemToggleToolboxDifficulty', false); } - var difficultyToolboxSaveMetadata:Button = toolbox.findComponent('difficultyToolboxSaveMetadata', Button); - var difficultyToolboxSaveChart:Button = toolbox.findComponent('difficultyToolboxSaveChart', Button); - var difficultyToolboxSaveAll:Button = toolbox.findComponent('difficultyToolboxSaveAll', Button); - var difficultyToolboxLoadMetadata:Button = toolbox.findComponent('difficultyToolboxLoadMetadata', Button); - var difficultyToolboxLoadChart:Button = toolbox.findComponent('difficultyToolboxLoadChart', Button); + var difficultyToolboxSaveMetadata:Null<Button> = toolbox.findComponent('difficultyToolboxSaveMetadata', Button); + if (difficultyToolboxSaveMetadata == null) + throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveMetadata component.'; + var difficultyToolboxSaveChart:Null<Button> = toolbox.findComponent('difficultyToolboxSaveChart', Button); + if (difficultyToolboxSaveChart == null) + throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveChart component.'; + var difficultyToolboxSaveAll:Null<Button> = toolbox.findComponent('difficultyToolboxSaveAll', Button); + if (difficultyToolboxSaveAll == null) throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveAll component.'; + var difficultyToolboxLoadMetadata:Null<Button> = toolbox.findComponent('difficultyToolboxLoadMetadata', Button); + if (difficultyToolboxLoadMetadata == null) + throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadMetadata component.'; + var difficultyToolboxLoadChart:Null<Button> = toolbox.findComponent('difficultyToolboxLoadChart', Button); + if (difficultyToolboxLoadChart == null) + throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadChart component.'; difficultyToolboxSaveMetadata.onClick = function(event:UIEvent) { SongSerializer.exportSongMetadata(state.currentSongMetadata, state.currentSongId); @@ -472,16 +494,18 @@ class ChartEditorToolboxHandler static function onShowToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void { // Update the selected difficulty when reopening the toolbox. - var treeView:TreeView = toolbox.findComponent('difficultyToolboxTree'); + var treeView:Null<TreeView> = toolbox.findComponent('difficultyToolboxTree'); if (treeView == null) return; - treeView.selectedNode = state.getCurrentTreeDifficultyNode(treeView); + var current = state.getCurrentTreeDifficultyNode(treeView); + if (current == null) return; + treeView.selectedNode = current; trace('selected node: ${treeView.selectedNode}'); } static function onHideToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - static function buildToolboxMetadataLayout(state:ChartEditorState):CollapsibleDialog + static function buildToolboxMetadataLayout(state:ChartEditorState):Null<CollapsibleDialog> { var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); @@ -495,7 +519,8 @@ class ChartEditorToolboxHandler state.setUICheckboxSelected('menubarItemToggleToolboxMetadata', false); } - var inputSongName:TextField = toolbox.findComponent('inputSongName', TextField); + var inputSongName:Null<TextField> = toolbox.findComponent('inputSongName', TextField); + if (inputSongName == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputSongName component.'; inputSongName.onChange = function(event:UIEvent) { var valid:Bool = event.target.text != null && event.target.text != ''; @@ -506,12 +531,13 @@ class ChartEditorToolboxHandler } else { - state.currentSongMetadata.songName = null; + state.currentSongMetadata.songName = ''; } }; inputSongName.value = state.currentSongMetadata.songName; - var inputSongArtist:TextField = toolbox.findComponent('inputSongArtist', TextField); + var inputSongArtist:Null<TextField> = toolbox.findComponent('inputSongArtist', TextField); + if (inputSongArtist == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputSongArtist component.'; inputSongArtist.onChange = function(event:UIEvent) { var valid:Bool = event.target.text != null && event.target.text != ''; @@ -522,12 +548,13 @@ class ChartEditorToolboxHandler } else { - state.currentSongMetadata.artist = null; + state.currentSongMetadata.artist = ''; } }; inputSongArtist.value = state.currentSongMetadata.artist; - var inputStage:DropDown = toolbox.findComponent('inputStage', DropDown); + var inputStage:Null<DropDown> = toolbox.findComponent('inputStage', DropDown); + if (inputStage == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputStage component.'; inputStage.onChange = function(event:UIEvent) { var valid:Bool = event.data != null && event.data.id != null; @@ -538,14 +565,16 @@ class ChartEditorToolboxHandler }; inputStage.value = state.currentSongMetadata.playData.stage; - var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown); + var inputNoteSkin:Null<DropDown> = toolbox.findComponent('inputNoteSkin', DropDown); + if (inputNoteSkin == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputNoteSkin component.'; inputNoteSkin.onChange = function(event:UIEvent) { if ((event?.data?.id ?? null) == null) return; state.currentSongMetadata.playData.noteSkin = event.data.id; }; inputNoteSkin.value = state.currentSongMetadata.playData.noteSkin; - var inputBPM:NumberStepper = toolbox.findComponent('inputBPM', NumberStepper); + var inputBPM:Null<NumberStepper> = toolbox.findComponent('inputBPM', NumberStepper); + if (inputBPM == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputBPM component.'; inputBPM.onChange = function(event:UIEvent) { if (event.value == null || event.value <= 0) return; @@ -565,9 +594,11 @@ class ChartEditorToolboxHandler }; inputBPM.value = state.currentSongMetadata.timeChanges[0].bpm; - var labelScrollSpeed:Label = toolbox.findComponent('labelScrollSpeed', Label); + var labelScrollSpeed:Null<Label> = toolbox.findComponent('labelScrollSpeed', Label); + if (labelScrollSpeed == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find labelScrollSpeed component.'; - var inputScrollSpeed:Slider = toolbox.findComponent('inputScrollSpeed', Slider); + var inputScrollSpeed:Null<Slider> = toolbox.findComponent('inputScrollSpeed', Slider); + if (inputScrollSpeed == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputScrollSpeed component.'; inputScrollSpeed.onChange = function(event:UIEvent) { var valid:Bool = event.target.value != null && event.target.value > 0; @@ -585,10 +616,12 @@ class ChartEditorToolboxHandler inputScrollSpeed.value = state.currentSongChartScrollSpeed; labelScrollSpeed.text = 'Scroll Speed: ${state.currentSongChartScrollSpeed}x'; - var frameVariation:Frame = toolbox.findComponent('frameVariation', Frame); + var frameVariation:Null<Frame> = toolbox.findComponent('frameVariation', Frame); + if (frameVariation == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find frameVariation component.'; frameVariation.text = 'Variation: ${state.selectedVariation.toTitleCase()}'; - var frameDifficulty:Frame = toolbox.findComponent('frameDifficulty', Frame); + var frameDifficulty:Null<Frame> = toolbox.findComponent('frameDifficulty', Frame); + if (frameDifficulty == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find frameDifficulty component.'; frameDifficulty.text = 'Difficulty: ${state.selectedDifficulty.toTitleCase()}'; return toolbox; @@ -601,7 +634,7 @@ class ChartEditorToolboxHandler static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - static function buildToolboxCharactersLayout(state:ChartEditorState):CollapsibleDialog + static function buildToolboxCharactersLayout(state:ChartEditorState):Null<CollapsibleDialog> { var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT); @@ -622,7 +655,7 @@ class ChartEditorToolboxHandler static function onHideToolboxCharacters(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):CollapsibleDialog + static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Null<CollapsibleDialog> { var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); @@ -636,7 +669,8 @@ class ChartEditorToolboxHandler state.setUICheckboxSelected('menubarItemToggleToolboxPlayerPreview', false); } - var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer'); + var charPlayer:Null<CharacterPlayer> = toolbox.findComponent('charPlayer'); + if (charPlayer == null) throw 'ChartEditorToolboxHandler.buildToolboxPlayerPreviewLayout() - Could not find charPlayer component.'; // TODO: We need to implement character swapping in ChartEditorState. charPlayer.loadCharacter('bf'); charPlayer.characterType = CharacterType.BF; @@ -650,7 +684,7 @@ class ChartEditorToolboxHandler static function onHideToolboxPlayerPreview(state:ChartEditorState, toolbox:CollapsibleDialog):Void {} - static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):CollapsibleDialog + static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):Null<CollapsibleDialog> { var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); @@ -664,7 +698,8 @@ class ChartEditorToolboxHandler state.setUICheckboxSelected('menubarItemToggleToolboxOpponentPreview', false); } - var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer'); + var charPlayer:Null<CharacterPlayer> = toolbox.findComponent('charPlayer'); + if (charPlayer == null) throw 'ChartEditorToolboxHandler.buildToolboxOpponentPreviewLayout() - Could not find charPlayer component.'; // TODO: We need to implement character swapping in ChartEditorState. charPlayer.loadCharacter('dad'); charPlayer.characterType = CharacterType.DAD; diff --git a/source/funkin/ui/haxeui/components/CharacterPlayer.hx b/source/funkin/ui/haxeui/components/CharacterPlayer.hx index c638e8a72..66b94bfa2 100644 --- a/source/funkin/ui/haxeui/components/CharacterPlayer.hx +++ b/source/funkin/ui/haxeui/components/CharacterPlayer.hx @@ -30,7 +30,7 @@ typedef AnimationInfo = @:composite(Layout) class CharacterPlayer extends Box { - var character:BaseCharacter; + var character:Null<BaseCharacter>; public function new(defaultToBf:Bool = true) { @@ -47,7 +47,7 @@ class CharacterPlayer extends Box function get_charId():String { - return character.characterId; + return character?.characterId ?? ''; } function set_charId(value:String):String @@ -56,11 +56,11 @@ class CharacterPlayer extends Box return value; } - public var charName(get, null):String; + public var charName(get, never):String; function get_charName():String { - return character.characterName; + return character?.characterName ?? "Unknown"; } // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... is it smart to "collect and redispatch"? Not sure @@ -86,7 +86,11 @@ class CharacterPlayer extends Box // Prevent script issues by fetching with debug=true. var newCharacter:BaseCharacter = CharacterDataParser.fetchCharacter(id, true); - if (newCharacter == null) return; // Fail if character doesn't exist. + if (newCharacter == null) + { + character = null; + return; // Fail if character doesn't exist. + } // Assign character. character = newCharacter; diff --git a/source/funkin/ui/haxeui/components/FunkinButton.hx b/source/funkin/ui/haxeui/components/FunkinButton.hx new file mode 100644 index 000000000..45987b9ec --- /dev/null +++ b/source/funkin/ui/haxeui/components/FunkinButton.hx @@ -0,0 +1,30 @@ +package funkin.ui.haxeui.components; + +import funkin.input.Cursor; +import haxe.ui.events.MouseEvent; +import haxe.ui.components.Button; + +/** + * A HaxeUI button which: + * - Changes the current cursor when hovered over. + */ +class FunkinButton extends Button +{ + public function new() + { + super(); + + this.onMouseOver = handleMouseOver; + this.onMouseOut = handleMouseOut; + } + + private function handleMouseOver(event:MouseEvent) + { + Cursor.cursorMode = Pointer; + } + + private function handleMouseOut(event:MouseEvent) + { + Cursor.cursorMode = Default; + } +} diff --git a/source/funkin/ui/haxeui/components/FunkinHorizontalSlider.hx b/source/funkin/ui/haxeui/components/FunkinHorizontalSlider.hx new file mode 100644 index 000000000..baf42aada --- /dev/null +++ b/source/funkin/ui/haxeui/components/FunkinHorizontalSlider.hx @@ -0,0 +1,30 @@ +package funkin.ui.haxeui.components; + +import haxe.ui.components.HorizontalSlider; +import funkin.input.Cursor; +import haxe.ui.events.MouseEvent; + +/** + * A HaxeUI horizontal slider which: + * - Changes the current cursor when hovered over. + */ +class FunkinHorizontalSlider extends HorizontalSlider +{ + public function new() + { + super(); + + this.onMouseOver = handleMouseOver; + this.onMouseOut = handleMouseOut; + } + + private function handleMouseOver(event:MouseEvent) + { + Cursor.cursorMode = Pointer; + } + + private function handleMouseOut(event:MouseEvent) + { + Cursor.cursorMode = Default; + } +} diff --git a/source/funkin/ui/haxeui/components/FunkinLink.hx b/source/funkin/ui/haxeui/components/FunkinLink.hx new file mode 100644 index 000000000..74eb6e7c4 --- /dev/null +++ b/source/funkin/ui/haxeui/components/FunkinLink.hx @@ -0,0 +1,30 @@ +package funkin.ui.haxeui.components; + +import funkin.input.Cursor; +import haxe.ui.events.MouseEvent; +import haxe.ui.components.Link; + +/** + * A HaxeUI link which: + * - Changes the current cursor when hovered over. + */ +class FunkinLink extends Link +{ + public function new() + { + super(); + + this.onMouseOver = handleMouseOver; + this.onMouseOut = handleMouseOut; + } + + private function handleMouseOver(event:MouseEvent) + { + Cursor.cursorMode = Pointer; + } + + private function handleMouseOut(event:MouseEvent) + { + Cursor.cursorMode = Default; + } +} diff --git a/source/funkin/ui/haxeui/components/FunkinMenuBar.hx b/source/funkin/ui/haxeui/components/FunkinMenuBar.hx new file mode 100644 index 000000000..393372d74 --- /dev/null +++ b/source/funkin/ui/haxeui/components/FunkinMenuBar.hx @@ -0,0 +1,32 @@ +package funkin.ui.haxeui.components; + +import funkin.input.Cursor; +import haxe.ui.events.MouseEvent; +import haxe.ui.containers.menus.MenuBar; +import haxe.ui.core.CompositeBuilder; + +/** + * A HaxeUI menu bar which: + * - Changes the current cursor when each button is hovered over. + */ +class FunkinMenuBar extends MenuBar +{ + public function new() + { + super(); + + registerListeners(); + } + + private function registerListeners():Void {} + + private function handleMouseOver(event:MouseEvent) + { + Cursor.cursorMode = Pointer; + } + + private function handleMouseOut(event:MouseEvent) + { + Cursor.cursorMode = Default; + } +} diff --git a/source/funkin/ui/haxeui/components/FunkinMenuCheckBox.hx b/source/funkin/ui/haxeui/components/FunkinMenuCheckBox.hx new file mode 100644 index 000000000..263277c6f --- /dev/null +++ b/source/funkin/ui/haxeui/components/FunkinMenuCheckBox.hx @@ -0,0 +1,30 @@ +package funkin.ui.haxeui.components; + +import funkin.input.Cursor; +import haxe.ui.events.MouseEvent; +import haxe.ui.containers.menus.MenuCheckBox; + +/** + * A HaxeUI menu checkbox which: + * - Changes the current cursor when hovered over. + */ +class FunkinMenuCheckBox extends MenuCheckBox +{ + public function new() + { + super(); + + this.onMouseOver = handleMouseOver; + this.onMouseOut = handleMouseOut; + } + + private function handleMouseOver(event:MouseEvent) + { + Cursor.cursorMode = Pointer; + } + + private function handleMouseOut(event:MouseEvent) + { + Cursor.cursorMode = Default; + } +} diff --git a/source/funkin/ui/haxeui/components/FunkinMenuItem.hx b/source/funkin/ui/haxeui/components/FunkinMenuItem.hx new file mode 100644 index 000000000..2eb7db729 --- /dev/null +++ b/source/funkin/ui/haxeui/components/FunkinMenuItem.hx @@ -0,0 +1,30 @@ +package funkin.ui.haxeui.components; + +import funkin.input.Cursor; +import haxe.ui.events.MouseEvent; +import haxe.ui.containers.menus.MenuItem; + +/** + * A HaxeUI menu item which: + * - Changes the current cursor when hovered over. + */ +class FunkinMenuItem extends MenuItem +{ + public function new() + { + super(); + + this.onMouseOver = handleMouseOver; + this.onMouseOut = handleMouseOut; + } + + private function handleMouseOver(event:MouseEvent) + { + Cursor.cursorMode = Pointer; + } + + private function handleMouseOut(event:MouseEvent) + { + Cursor.cursorMode = Default; + } +} diff --git a/source/funkin/ui/haxeui/components/FunkinMenuOptionBox.hx b/source/funkin/ui/haxeui/components/FunkinMenuOptionBox.hx new file mode 100644 index 000000000..d9985eede --- /dev/null +++ b/source/funkin/ui/haxeui/components/FunkinMenuOptionBox.hx @@ -0,0 +1,30 @@ +package funkin.ui.haxeui.components; + +import haxe.ui.containers.menus.MenuOptionBox; +import funkin.input.Cursor; +import haxe.ui.events.MouseEvent; + +/** + * A HaxeUI menu option box which: + * - Changes the current cursor when hovered over. + */ +class FunkinMenuOptionBox extends MenuOptionBox +{ + public function new() + { + super(); + + this.onMouseOver = handleMouseOver; + this.onMouseOut = handleMouseOut; + } + + private function handleMouseOver(event:MouseEvent) + { + Cursor.cursorMode = Pointer; + } + + private function handleMouseOut(event:MouseEvent) + { + Cursor.cursorMode = Default; + } +} diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx index 1e5d60bf3..764606bf3 100644 --- a/source/funkin/ui/story/Level.hx +++ b/source/funkin/ui/story/Level.hx @@ -98,19 +98,40 @@ class Level implements IRegistryEntry<LevelData> return true; } + /** + * Build a sprite for the background of the level. + * Can be overriden by ScriptedLevel. Not used if `isBackgroundSimple` returns true. + */ public function buildBackground():FlxSprite { - if (_data.background.startsWith('#')) - { - // Color specified - var color:FlxColor = FlxColor.fromString(_data.background); - return new FlxSprite().makeGraphic(FlxG.width, 400, color); - } - else + if (!_data.background.startsWith('#')) { // Image specified return new FlxSprite().loadGraphic(Paths.image(_data.background)); } + + // Color specified + var result:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, 400, FlxColor.WHITE); + result.color = getBackgroundColor(); + return result; + } + + /** + * Returns true if the background is a solid color. + * If you have a ScriptedLevel with a fancy background, you may want to override this to false. + */ + public function isBackgroundSimple():Bool + { + return _data.background.startsWith('#'); + } + + /** + * Returns true if the background is a solid color. + * If you have a ScriptedLevel with a fancy background, you may want to override this to false. + */ + public function getBackgroundColor():FlxColor + { + return FlxColor.fromString(_data.background); } public function getDifficulties():Array<String> diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 8276777ab..34dd49e22 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -135,10 +135,15 @@ class StoryMenuState extends MusicBeatState this.bgColor = FlxColor.BLACK; levelTitles = new FlxTypedGroup<LevelTitle>(); + levelTitles.zIndex = 15; add(levelTitles); updateBackground(); + var black:FlxSprite = new FlxSprite(levelBackground.x, 0).makeGraphic(FlxG.width, Std.int(400 + levelBackground.y), FlxColor.BLACK); + black.zIndex = levelBackground.zIndex - 1; + add(black); + levelProps = new FlxTypedGroup<LevelProp>(); levelProps.zIndex = 1000; add(levelProps); @@ -153,17 +158,20 @@ class StoryMenuState extends MusicBeatState scoreText = new FlxText(10, 10, 0, 'HIGH SCORE: 42069420'); scoreText.setFormat("VCR OSD Mono", 32); + scoreText.zIndex = 1000; add(scoreText); modeText = new FlxText(10, 10, 0, 'Base Game Levels [TAB to switch]'); modeText.setFormat("VCR OSD Mono", 32); modeText.screenCenter(X); modeText.visible = hasModdedLevels(); + modeText.zIndex = 1000; add(modeText); levelTitleText = new FlxText(FlxG.width * 0.7, 10, 0, 'LEVEL 1'); levelTitleText.setFormat("VCR OSD Mono", 32, FlxColor.WHITE, RIGHT); levelTitleText.alpha = 0.7; + levelTitleText.zIndex = 1000; add(levelTitleText); buildLevelTitles(); @@ -384,6 +392,7 @@ class StoryMenuState extends MusicBeatState if (currentIndex < 0) currentIndex = levelList.length - 1; if (currentIndex >= levelList.length) currentIndex = 0; + var previousLevelId:String = currentLevelId; currentLevelId = levelList[currentIndex]; updateData(); @@ -399,18 +408,14 @@ class StoryMenuState extends MusicBeatState currentLevelTitle = item; item.alpha = 1.0; } - else if (index > currentIndex) - { - item.alpha = 0.6; - } else { - item.alpha = 0.0; + item.alpha = 0.6; } } updateText(); - updateBackground(); + updateBackground(previousLevelId); updateProps(); refresh(); } @@ -533,32 +538,66 @@ class StoryMenuState extends MusicBeatState }); } - function updateBackground():Void + function updateBackground(?previousLevelId:String = ''):Void { - if (levelBackground != null) + if (levelBackground == null || previousLevelId == '') { - var oldBackground:FlxSprite = levelBackground; - - FlxTween.tween(oldBackground, {alpha: 0.0}, 0.6, - { - ease: FlxEase.linear, - onComplete: function(_) { - remove(oldBackground); - } - }); + // Build a new background and display it immediately. + levelBackground = currentLevel.buildBackground(); + levelBackground.x = 0; + levelBackground.y = 56; + levelBackground.zIndex = 100; + levelBackground.alpha = 1.0; // Not hidden. + add(levelBackground); } + else + { + var previousLevel = LevelRegistry.instance.fetchEntry(previousLevelId); - levelBackground = currentLevel.buildBackground(); - levelBackground.x = 0; - levelBackground.y = 56; - levelBackground.alpha = 0.0; - levelBackground.zIndex = 100; - add(levelBackground); - - FlxTween.tween(levelBackground, {alpha: 1.0}, 0.6, + if (currentLevel.isBackgroundSimple() && previousLevel.isBackgroundSimple()) { - ease: FlxEase.linear - }); + var previousColor:FlxColor = previousLevel.getBackgroundColor(); + var currentColor:FlxColor = currentLevel.getBackgroundColor(); + if (previousColor != currentColor) + { + // Both the previous and current level were simple backgrounds. + // Fade between colors directly, rather than fading one background out and another in. + FlxTween.color(levelBackground, 0.4, previousColor, currentColor); + } + else + { + // Do no fade at all if the colors aren't different. + } + } + else + { + // Either the previous or current level has a complex background. + // We need to fade the old background out and the new one in. + + // Reference the old background and fade it out. + var oldBackground:FlxSprite = levelBackground; + FlxTween.tween(oldBackground, {alpha: 0.0}, 0.6, + { + ease: FlxEase.linear, + onComplete: function(_) { + remove(oldBackground); + } + }); + + // Build a new background and fade it in. + levelBackground = currentLevel.buildBackground(); + levelBackground.x = 0; + levelBackground.y = 56; + levelBackground.alpha = 0.0; // Hidden to start. + levelBackground.zIndex = 100; + add(levelBackground); + + FlxTween.tween(levelBackground, {alpha: 1.0}, 0.6, + { + ease: FlxEase.linear + }); + } + } } function updateProps():Void diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 71cfe7b8e..b454ca429 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -20,7 +20,7 @@ class Constants * The current version number of the game. * Modify this in the `project.xml` file. */ - public static var VERSION(get, null):String; + public static var VERSION(get, never):String; /** * A suffix to add to the game version. diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx index 21c2920d9..3a6f4e330 100644 --- a/source/funkin/util/FileUtil.hx +++ b/source/funkin/util/FileUtil.hx @@ -240,6 +240,10 @@ class FileUtil onSaveAll(paths); } + trace('Browsing for directory to save individual files to...'); + #if mac + defaultPath = null; + #end browseForDirectory(null, onSelectDir, onCancel, defaultPath, 'Choose directory to save all files to...'); return true;