diff --git a/Project.xml b/Project.xml index 8dbb43618..a83db1677 100644 --- a/Project.xml +++ b/Project.xml @@ -20,6 +20,7 @@ <!--Mobile-specific--> <window if="mobile" orientation="landscape" fullscreen="true" width="0" height="0" resizable="false" /> <!-- _____________________________ Path Settings ____________________________ --> + <set name="BUILD_DIR" value="export/debug" if="debug" /> <set name="BUILD_DIR" value="export/release" unless="debug" /> <set name="BUILD_DIR" value="export/32bit" if="32bit" /> @@ -96,8 +97,9 @@ <haxelib name="lime" /> <!-- Game engine backend --> <haxelib name="openfl" /> <!-- Game engine backend --> <haxelib name="flixel" /> <!-- Game engine --> + <haxedev set="webgl" /> - <!--In case you want to use the addons package--> + <haxelib name="flixel-addons" /> <!-- Additional utilities for Flixel --> <haxelib name="hscript" /> <!-- Scripting --> <haxelib name="flixel-ui" /> <!-- UI framework (deprecate this? --> @@ -150,8 +152,11 @@ <haxeflag name="--macro" value="include('flixel', true, [ 'flixel.addons.editors.spine.*', 'flixel.addons.nape.*', 'flixel.system.macros.*' ])" /> <!-- Necessary to provide stack traces for HScript. --> <haxedef name="hscriptPos" /> + <haxedef name="safeMode"/> <haxedef name="HXCPP_CHECK_POINTER" /> <haxedef name="HXCPP_STACK_LINE" /> + <haxedef name="HXCPP_STACK_TRACE" /> + <haxedef name="openfl-enable-handle-error" /> <!-- This macro allows addition of new functionality to existing Flixel. --> <haxeflag name="--macro" value="addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')" /> <!--Place custom nodes like icons here (higher priority to override the HaxeFlixel icon)--> @@ -159,7 +164,6 @@ <icon path="art/icon32.png" size="32" /> <icon path="art/icon64.png" size="64" /> <icon path="art/iconOG.png" /> - <!-- <haxedef name="SKIP_TO_PLAYSTATE" if="debug" /> --> <haxedef name="CAN_OPEN_LINKS" unless="switch" /> <haxedef name="CAN_CHEAT" if="switch debug" /> <haxedef name="haxeui_no_mouse_reset" /> @@ -172,7 +176,6 @@ <!-- Difficulty, only used for week or song, defaults to 1 --> <!-- <haxedef name="dif" value="2" if="debug"/> --> </section> - <!-- <haxedef name="CLEAR_INPUT_SAVE"/> --> <section if="newgrounds"> <!-- Enables Ng.core.verbose --> <!-- <haxedef name="NG_VERBOSE" /> --> @@ -182,18 +185,21 @@ <!-- <haxedef name="NG_FORCE_EXPIRED_SESSION" if="debug" /> --> </section> + <!-- Uncomment this to wipe your input settings. --> + <!-- <haxedef name="CLEAR_INPUT_SAVE"/> --> + <section if="debug" unless="NO_REDIRECT_ASSETS_FOLDER || html5"> <!-- Use the parent assets folder rather than the exported one No more will we accidentally undo our changes! - TODO: Add a thing to disable this on builds meant for itch.io. --> <haxedef name="REDIRECT_ASSETS_FOLDER" /> </section> - <!-- <prebuild haxe="trace('prebuilding');"/> --> - <!-- <postbuild haxe="art/Postbuild.hx"/> --> - <!-- <config:ios allow-provisioning-updates="true" team-id="" /> --> + <!-- Run a script before and after building. --> + <postbuild haxe="source/Prebuild.hx"/> --> + <postbuild haxe="source/Postbuild.hx"/> --> + <!-- Options for Polymod --> <section if="polymod"> <!-- Turns on additional debug logging. --> @@ -213,12 +219,4 @@ <!-- Determines the file in the mod folder used for the icon. --> <haxedef name="POLYMOD_MOD_ICON_FILE" value="_polymod_icon.png" /> </section> - <section if="TOOLS"> - <!-- Compiles tool for old song conversion shit --> - <!-- Assumes you use it on windows/desktop!!!! --> - <postbuild command="haxe -main art/SongConverter.hx --cs export/songShit" /> - <assets path="export/songShit/bin/SongConverter.exe" rename="SongConverter.exe" /> - <!-- <postbuild command='ren export/songShit/bin export/songShit/tools '/> --> - <!-- <postbuild command='move export/songShit/tools export/release/windows/bin'/> --> - </section> </project> diff --git a/hmm.json b/hmm.json index 150a4f242..a3226281b 100644 --- a/hmm.json +++ b/hmm.json @@ -47,7 +47,7 @@ "name": "haxeui-core", "type": "git", "dir": null, - "ref": "3590c94858fc6dbcf9b4d522cd644ad571269677", + "ref": "f5daafe93bdfa957538f199294a54e0476c805b7", "url": "https://github.com/haxeui/haxeui-core/" }, { @@ -128,7 +128,7 @@ "name": "openfl", "type": "git", "dir": null, - "ref": "d33d489a137ff8fdece4994cf1302f0b6334ed08", + "ref": "1591a6c5f1f72e65d711f7e17e8055df41424d94", "url": "https://github.com/EliteMasterEric/openfl" }, { diff --git a/source/Main.hx b/source/Main.hx index 1d7b73bb8..72209cd30 100644 --- a/source/Main.hx +++ b/source/Main.hx @@ -2,12 +2,13 @@ package; import flixel.FlxGame; import flixel.FlxState; +import funkin.util.logging.CrashHandler; import funkin.MemoryCounter; import haxe.ui.Toolkit; -import openfl.Lib; import openfl.display.FPS; import openfl.display.Sprite; import openfl.events.Event; +import openfl.Lib; import openfl.media.Video; import openfl.net.NetStream; @@ -77,10 +78,18 @@ class Main extends Sprite * -Eric */ + CrashHandler.initialize(); + + CrashHandler.queryStatus(); + initHaxeUI(); addChild(new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen)); + #if hxcpp_debug_server + trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.'); + #end + #if debug fpsCounter = new FPS(10, 3, 0xFFFFFF); addChild(fpsCounter); diff --git a/source/Postbuild.hx b/source/Postbuild.hx new file mode 100644 index 000000000..d48b670a4 --- /dev/null +++ b/source/Postbuild.hx @@ -0,0 +1,11 @@ +package source; // Yeah, I know... + +class Postbuild +{ + static function main() + { + trace('Postbuild'); + + // TODO: Maybe put a 'Build took X seconds' message here? + } +} diff --git a/source/Prebuild.hx b/source/Prebuild.hx new file mode 100644 index 000000000..63782fc56 --- /dev/null +++ b/source/Prebuild.hx @@ -0,0 +1,9 @@ +package source; // Yeah, I know... + +class Prebuild +{ + static function main() + { + trace('Prebuild'); + } +} diff --git a/source/funkin/Alphabet.hx b/source/funkin/Alphabet.hx index 670496727..45e9a2aee 100644 --- a/source/funkin/Alphabet.hx +++ b/source/funkin/Alphabet.hx @@ -38,7 +38,7 @@ class Alphabet extends FlxSpriteGroup var isBold:Bool = false; - public function new(x:Float = 0.0, y:Float = 0.0, text:String = "", ?bold:Bool = false, typed:Bool = false) + public function new(x:Float = 0.0, y:Float = 0.0, text:String = "", bold:Bool = false, typed:Bool = false) { super(x, y); diff --git a/source/funkin/CoolUtil.hx b/source/funkin/CoolUtil.hx index 93fa937da..d07bb4e22 100644 --- a/source/funkin/CoolUtil.hx +++ b/source/funkin/CoolUtil.hx @@ -12,7 +12,6 @@ import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import funkin.play.PlayState; import funkin.shaderslmfao.ScreenWipeShader; -import haxe.Json; import haxe.format.JsonParser; import lime.math.Rectangle; import lime.utils.Assets; @@ -120,31 +119,6 @@ class CoolUtil FlxG.camera.setFilters([new ShaderFilter(screenWipeShit)]); } - /** - * Just saves the json with some default values hehe - * @param json - * @return String - */ - public static inline function jsonStringify(data:Dynamic):String - { - return Json.stringify(data, null, "\t"); - } - - /** - * Hashlink json encoding fix for some wacky bullshit - * https://github.com/HaxeFoundation/haxe/issues/6930#issuecomment-384570392 - */ - public static function coolJSON(fileData:String) - { - var cont = fileData; - function is(n:Int, what:Int) - return cont.charCodeAt(n) == what; - return JsonParser.parse(cont.substr(if (is(0, 65279)) /// looks like a HL target, skipping only first character here: - 1 else if (is(0, 239) && is(1, 187) && is(2, 191)) /// it seems to be Neko or PHP, start from position 3: - 3 else /// all other targets, that prepare the UTF string correctly - 0)); - } - /* * frame dependant lerp kinda lol */ diff --git a/source/funkin/Discord.hx b/source/funkin/Discord.hx index 4fb6e9dcf..d2cf12535 100644 --- a/source/funkin/Discord.hx +++ b/source/funkin/Discord.hx @@ -64,7 +64,7 @@ class DiscordClient trace("Discord Client initialized"); } - public static function changePresence(details:String, state:Null<String>, ?smallImageKey:String, ?hasStartTimestamp:Bool, ?endTimestamp:Float) + public static function changePresence(details:String, ?state:String, ?smallImageKey:String, ?hasStartTimestamp:Bool, ?endTimestamp:Float) { var startTimestamp:Float = if (hasStartTimestamp) Date.now().getTime() else 0; diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx index dd87e7d36..c31e8c77b 100644 --- a/source/funkin/FreeplayState.hx +++ b/source/funkin/FreeplayState.hx @@ -464,7 +464,7 @@ class FreeplayState extends MusicBeatSubState }); } - public function generateSongList(?filterStuff:SongFilter, ?force:Bool = false) + public function generateSongList(?filterStuff:SongFilter, force:Bool = false) { curSelected = 0; @@ -1045,7 +1045,7 @@ class FreeplaySongData public var songCharacter:String = ""; public var isFav:Bool = false; - public function new(song:String, levelId:String, songCharacter:String, ?isFav:Bool = false) + public function new(song:String, levelId:String, songCharacter:String, isFav:Bool = false) { this.songName = song; this.levelId = levelId; diff --git a/source/funkin/PauseSubState.hx b/source/funkin/PauseSubState.hx index 9133a8fab..791a4bb9a 100644 --- a/source/funkin/PauseSubState.hx +++ b/source/funkin/PauseSubState.hx @@ -41,7 +41,7 @@ class PauseSubState extends MusicBeatSubState var isChartingMode:Bool; - public function new(?isChartingMode:Bool = false) + public function new(isChartingMode:Bool = false) { super(); diff --git a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx index 74b348142..ae7a5708c 100644 --- a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx +++ b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx @@ -82,7 +82,7 @@ class FlxAtlasSprite extends FlxAnimate * @param restart Whether to restart the animation if it is already playing. * @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing */ - public function playAnimation(id:String, ?restart:Bool = false, ?ignoreOther:Bool = false):Void + public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false):Void { // Skip if not allowed to play animations. if ((!canPlayOtherAnims && !ignoreOther)) return; diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index b53937361..15ed0421e 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -255,7 +255,7 @@ class GameOverSubState extends MusicBeatSubState * Starts the death music at the appropriate volume. * @param startingVolume */ - function startDeathMusic(?startingVolume:Float = 1, ?force:Bool = false):Void + function startDeathMusic(?startingVolume:Float = 1, force:Bool = false):Void { var musicPath = Paths.music('gameOver' + musicSuffix); if (isEnding) diff --git a/source/funkin/play/character/AnimateAtlasCharacter.hx b/source/funkin/play/character/AnimateAtlasCharacter.hx index 4f4b3f8f7..3523ec994 100644 --- a/source/funkin/play/character/AnimateAtlasCharacter.hx +++ b/source/funkin/play/character/AnimateAtlasCharacter.hx @@ -81,7 +81,7 @@ class AnimateAtlasCharacter extends BaseCharacter super.onCreate(event); } - public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false, ?reverse:Bool = false):Void + public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reverse:Bool = false):Void { if ((!canPlayOtherAnims && !ignoreOther)) return; diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index 72f968538..c7b58c393 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -570,7 +570,7 @@ class BaseCharacter extends Bopper * @param miss If true, play the miss animation instead of the sing animation. * @param suffix A suffix to append to the animation name, like `alt`. */ - public function playSingAnimation(dir:NoteDirection, ?miss:Bool = false, ?suffix:String = ''):Void + public function playSingAnimation(dir:NoteDirection, miss:Bool = false, ?suffix:String = ''):Void { var anim:String = 'sing${dir.nameUpper}${miss ? 'miss' : ''}${suffix != '' ? '-${suffix}' : ''}'; @@ -578,7 +578,7 @@ class BaseCharacter extends Bopper playAnimation(anim, true); } - public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false, ?reversed:Bool = false):Void + public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reversed:Bool = false):Void { FlxG.watch.addQuick('playAnim(${characterName})', name); // trace('playAnim(${characterName}): ${name}'); diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx index 710eb884b..f1b316b7f 100644 --- a/source/funkin/play/character/CharacterData.hx +++ b/source/funkin/play/character/CharacterData.hx @@ -190,7 +190,7 @@ class CharacterDataParser * @param charId The character ID to fetch. * @return The character instance, or null if the character was not found. */ - public static function fetchCharacter(charId:String, ?debug:Bool = false):Null<BaseCharacter> + public static function fetchCharacter(charId:String, debug:Bool = false):Null<BaseCharacter> { if (charId == null || charId == '' || !characterCache.exists(charId)) { diff --git a/source/funkin/play/character/MultiSparrowCharacter.hx b/source/funkin/play/character/MultiSparrowCharacter.hx index 34d89362f..968f613ff 100644 --- a/source/funkin/play/character/MultiSparrowCharacter.hx +++ b/source/funkin/play/character/MultiSparrowCharacter.hx @@ -181,7 +181,7 @@ class MultiSparrowCharacter extends BaseCharacter trace('[MULTISPARROWCHAR] Successfully loaded ${animNames.length} animations for ${characterId}'); } - public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false, ?reverse:Bool = false):Void + public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reverse:Bool = false):Void { // Make sure we ignore other animations if we're currently playing a forced one, // unless we're forcing a new animation. diff --git a/source/funkin/play/cutscene/dialogue/ConversationData.hx b/source/funkin/play/cutscene/dialogue/ConversationData.hx index d2e3b74cf..749f1b7a1 100644 --- a/source/funkin/play/cutscene/dialogue/ConversationData.hx +++ b/source/funkin/play/cutscene/dialogue/ConversationData.hx @@ -208,7 +208,7 @@ class OutroData public var type:OutroType; public var data:Dynamic; - public function new(typeStr:Null<String>, data:Dynamic) + public function new(?typeStr:String, data:Dynamic) { this.type = typeStr ?? OutroType.NONE; this.data = data; diff --git a/source/funkin/play/cutscene/dialogue/DialogueBox.hx b/source/funkin/play/cutscene/dialogue/DialogueBox.hx index 52564010a..bfc0e9233 100644 --- a/source/funkin/play/cutscene/dialogue/DialogueBox.hx +++ b/source/funkin/play/cutscene/dialogue/DialogueBox.hx @@ -172,7 +172,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass /** * Set the sprite scale to the appropriate value. - * @param scale + * @param scale */ public function setScale(scale:Null<Float>):Void { @@ -218,7 +218,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass * @param name The name of the current animation. * @param frameNumber The number of the current frame. * @param frameIndex The index of the current frame. - * + * * For example, if an animation was defined as having the indexes [3, 0, 1, 2], * then the first callback would have frameNumber = 0 and frameIndex = 3. */ @@ -253,7 +253,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass * @param restart Whether to restart the animation if it is already playing. * @param reversed If true, play the animation backwards, from the last frame to the first. */ - public function playAnimation(name:String, restart:Bool = false, ?reversed:Bool = false):Void + public function playAnimation(name:String, restart:Bool = false, reversed:Bool = false):Void { var correctName:String = correctAnimationName(name); if (correctName == null) return; @@ -266,7 +266,7 @@ class DialogueBox extends FlxSpriteGroup 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/cutscene/dialogue/DialogueBoxData.hx b/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx index 537a27129..801a01dd7 100644 --- a/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx +++ b/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx @@ -93,7 +93,7 @@ class DialogueBoxTextData public var shadowColor:Null<String>; public var shadowWidth:Null<Int>; - public function new(offsets:Null<Array<Float>>, width:Null<Int>, size:Null<Int>, color:String, shadowColor:Null<String>, shadowWidth:Null<Int>) + public function new(offsets:Null<Array<Float>>, ?width:Int, ?size:Int, color:String, ?shadowColor:String, shadowWidth:Null<Int>) { this.offsets = offsets ?? [0, 0]; this.width = width ?? 300; diff --git a/source/funkin/play/cutscene/dialogue/SpeakerData.hx b/source/funkin/play/cutscene/dialogue/SpeakerData.hx index a0f9a3300..88883ead8 100644 --- a/source/funkin/play/cutscene/dialogue/SpeakerData.hx +++ b/source/funkin/play/cutscene/dialogue/SpeakerData.hx @@ -19,8 +19,8 @@ class SpeakerData public var scale:Float; public var animations:Array<AnimationData>; - public function new(version:String, name:String, assetPath:String, animations:Array<AnimationData>, ?offsets:Array<Float>, ?flipX:Bool = false, - ?isPixel:Bool = false, ?scale:Float = 1.0) + public function new(version:String, name:String, assetPath:String, animations:Array<AnimationData>, ?offsets:Array<Float>, flipX:Bool = false, + isPixel:Bool = false, ?scale:Float = 1.0) { this.version = version; this.name = name; diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index b343bee86..8847636bd 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -271,7 +271,7 @@ class Strumline extends FlxSpriteGroup * @param strumTime * @return Float */ - static function calculateNoteYPos(strumTime:Float, ?vwoosh:Bool = true):Float + static function calculateNoteYPos(strumTime:Float, vwoosh:Bool = true):Float { // Make the note move faster visually as it moves offscreen. var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0; diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx index fd45342a0..97871b657 100644 --- a/source/funkin/play/notes/notestyle/NoteStyle.hx +++ b/source/funkin/play/notes/notestyle/NoteStyle.hx @@ -113,7 +113,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData> return noteFrames; } - function getNoteAssetPath(?raw:Bool = false):String + function getNoteAssetPath(raw:Bool = false):String { if (raw) { @@ -161,7 +161,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData> return (result == null) ? fallback.fetchNoteAnimationData(dir) : result; } - public function getHoldNoteAssetPath(?raw:Bool = false):String + public function getHoldNoteAssetPath(raw:Bool = false):String { if (raw) { @@ -209,7 +209,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData> target.antialiasing = !_data.assets.noteStrumline.isPixel; } - function getStrumlineAssetPath(?raw:Bool = false):String + function getStrumlineAssetPath(raw:Bool = false):String { if (raw) { diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index ec89d8706..63610950f 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -72,7 +72,7 @@ class Song implements IPlayStateScriptedClass @:allow(funkin.play.song.Song) public static function buildRaw(songId:String, metadata:Array<SongMetadata>, variations:Array<String>, charts:Map<String, SongChartData>, - ?validScore:Bool = false):Song + validScore:Bool = false):Song { var result:Song = new Song(songId, true); @@ -150,7 +150,7 @@ class Song implements IPlayStateScriptedClass /** * Parse and cache the chart for all difficulties of this song. */ - public function cacheCharts(?force:Bool = false):Void + public function cacheCharts(force:Bool = false):Void { if (force) { diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx index 938ee0708..bf574c399 100644 --- a/source/funkin/play/song/SongData.hx +++ b/source/funkin/play/song/SongData.hx @@ -920,7 +920,7 @@ typedef RawSongTimeChange = */ abstract SongTimeChange(RawSongTimeChange) from RawSongTimeChange { - public function new(timeStamp:Float, beatTime:Null<Float>, bpm:Float, timeSignatureNum:Int = 4, timeSignatureDen:Int = 4, beatTuplets:Array<Int>) + public function new(timeStamp:Float, ?beatTime:Float, bpm:Float, timeSignatureNum:Int = 4, timeSignatureDen:Int = 4, beatTuplets:Array<Int>) { this = { diff --git a/source/funkin/play/song/SongDataUtils.hx b/source/funkin/play/song/SongDataUtils.hx index 750d5f54b..a7cbd1b6c 100644 --- a/source/funkin/play/song/SongDataUtils.hx +++ b/source/funkin/play/song/SongDataUtils.hx @@ -123,7 +123,7 @@ class SongDataUtils /** * Sort an array of notes by strum time. */ - public static function sortNotes(notes:Array<SongNoteData>, ?desc:Bool = false):Array<SongNoteData> + public static function sortNotes(notes:Array<SongNoteData>, desc:Bool = false):Array<SongNoteData> { // TODO: Modifies the array in place. Is this okay? notes.sort(function(a:SongNoteData, b:SongNoteData):Int { @@ -135,7 +135,7 @@ class SongDataUtils /** * Sort an array of events by strum time. */ - public static function sortEvents(events:Array<SongEventData>, ?desc:Bool = false):Array<SongEventData> + public static function sortEvents(events:Array<SongEventData>, desc:Bool = false):Array<SongEventData> { // TODO: Modifies the array in place. Is this okay? events.sort(function(a:SongEventData, b:SongEventData):Int { diff --git a/source/funkin/play/song/SongValidator.hx b/source/funkin/play/song/SongValidator.hx index d91dda1d9..16ea88664 100644 --- a/source/funkin/play/song/SongValidator.hx +++ b/source/funkin/play/song/SongValidator.hx @@ -63,9 +63,15 @@ class SongValidator } input.timeChanges = validateTimeChanges(input.timeChanges, songId); + if (input.timeChanges == null) + { + trace('[SONGDATA] Song ${songId} is missing a timeChanges field. '); + return null; + } + input.playData = validatePlayData(input.playData, songId); - input.variation = ''; + if (input.variation == null) input.variation = ''; return input; } @@ -79,6 +85,12 @@ class SongValidator */ public static function validatePlayData(input:SongPlayData, songId:String = 'unknown'):SongPlayData { + if (input == null) + { + trace('[SONGDATA] Could not parse metadata.playData for song ${songId}'); + return null; + } + return input; } @@ -91,6 +103,12 @@ class SongValidator */ public static function validateTimeChange(input:SongTimeChange, songId:String = 'unknown'):SongTimeChange { + if (input == null) + { + trace('[SONGDATA] Could not parse metadata.timeChange for song ${songId}'); + return null; + } + return input; } @@ -101,8 +119,8 @@ class SongValidator { if (input == null) { - trace('[SONGDATA] Song ${songId} is missing a timeChanges field. '); - return []; + trace('[SONGDATA] Could not parse metadata.timeChange for song ${songId}'); + return null; } input = input.map((timeChange) -> validateTimeChange(timeChange, songId)); diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index a144026f5..187b5ec32 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -268,7 +268,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass * @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing * @param reversed If true, play the animation backwards, from the last frame to the first. */ - public function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false, ?reversed:Bool = false):Void + public function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reversed:Bool = false):Void { if (!canPlayOtherAnims && !ignoreOther) return; diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index f4f380a0b..1ac9b0b67 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -450,7 +450,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass * @param pop If true, the character will be removed from the stage as well. * @return The Boyfriend character. */ - public function getBoyfriend(?pop:Bool = false):BaseCharacter + public function getBoyfriend(pop:Bool = false):BaseCharacter { if (pop) { @@ -473,7 +473,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass * @param pop If true, the character will be removed from the stage as well. * @return The player/Boyfriend character. */ - public function getPlayer(?pop:Bool = false):BaseCharacter + public function getPlayer(pop:Bool = false):BaseCharacter { return getBoyfriend(pop); } @@ -483,7 +483,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass * @param pop If true, the character will be removed from the stage as well. * @return The Girlfriend character. */ - public function getGirlfriend(?pop:Bool = false):BaseCharacter + public function getGirlfriend(pop:Bool = false):BaseCharacter { if (pop) { @@ -506,7 +506,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass * @param pop If true, the character will be removed from the stage as well. * @return The Dad character. */ - public function getDad(?pop:Bool = false):BaseCharacter + public function getDad(pop:Bool = false):BaseCharacter { if (pop) { @@ -529,7 +529,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass * @param pop If true, the character will be removed from the stage as well. * @return The opponent character. */ - public function getOpponent(?pop:Bool = false):BaseCharacter + public function getOpponent(pop:Bool = false):BaseCharacter { return getDad(pop); } diff --git a/source/funkin/play/stage/StageData.hx b/source/funkin/play/stage/StageData.hx index 867c6e1a5..c14e05aaf 100644 --- a/source/funkin/play/stage/StageData.hx +++ b/source/funkin/play/stage/StageData.hx @@ -503,7 +503,7 @@ typedef StageDataCharacter = * Again, just like CSS. * @default 0 */ - zIndex:Null<Int>, + ?zIndex:Int, /** * The position to render the character at. diff --git a/source/funkin/ui/debug/charting/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/ChartEditorCommand.hx index bf4d710dd..fd179c481 100644 --- a/source/funkin/ui/debug/charting/ChartEditorCommand.hx +++ b/source/funkin/ui/debug/charting/ChartEditorCommand.hx @@ -225,7 +225,7 @@ class AddEventsCommand implements ChartEditorCommand var events:Array<SongEventData>; var appendToSelection:Bool; - public function new(events:Array<SongEventData>, ?appendToSelection:Bool = false) + public function new(events:Array<SongEventData>, appendToSelection:Bool = false) { this.events = events; this.appendToSelection = appendToSelection; diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index df5a25b62..63dc8bd92 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -258,7 +258,7 @@ class ChartEditorDialogHandler * @return The dialog that was opened. */ @:haxe.warning("-WVarInit") - public static function openUploadInstDialog(state:ChartEditorState, ?closable:Bool = true):Dialog + public static function openUploadInstDialog(state:ChartEditorState, closable:Bool = true):Dialog { var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT, true, closable); @@ -578,7 +578,7 @@ class ChartEditorDialogHandler * @param closable Whether the dialog can be closed by the user. * @return The dialog that was opened. */ - public static function openUploadVocalsDialog(state:ChartEditorState, ?closable:Bool = true):Dialog + public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog { var charIdsForVocals:Array<String> = []; @@ -692,7 +692,7 @@ class ChartEditorDialogHandler * @return The dialog that was opened. */ @:haxe.warning('-WVarInit') - public static function openChartDialog(state:ChartEditorState, ?closable:Bool = true):Dialog + public static function openChartDialog(state:ChartEditorState, closable:Bool = true):Dialog { var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_OPEN_CHART_LAYOUT, true, closable); @@ -765,6 +765,19 @@ class ChartEditorDialogHandler var songMetadataVariation:SongMetadata = SongMigrator.migrateSongMetadata(songMetadataJson, 'import'); songMetadataVariation = SongValidator.validateSongMetadata(songMetadataVariation, 'import'); + if (songMetadataVariation == null) + { + // Tell the user the load was not successful. + NotificationManager.instance.addNotification( + { + title: 'Failure', + body: 'Could not load metadata file (${path.file}.${path.ext})', + type: NotificationType.Error, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + return; + } + songMetadata.set(variation, songMetadataVariation); // Tell the user the load was successful. @@ -879,7 +892,7 @@ 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):Dialog { var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT, true, closable); diff --git a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx index 2cd9ab2fe..2524f014c 100644 --- a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx +++ b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx @@ -26,12 +26,12 @@ class ChartEditorEventSprite extends FlxSprite * The note data that this sprite represents. * You can set this to null to kill the sprite and flag it for recycling. */ - public var eventData(default, set):SongEventData; + public var eventData(default, set):Null<SongEventData> = null; /** * The image used for all song events. Cached for performance. */ - static var eventSpriteBasic:BitmapData; + static var eventSpriteBasic:Null<BitmapData> = null; public function new(parent:ChartEditorState) { @@ -49,7 +49,7 @@ class ChartEditorEventSprite extends FlxSprite * 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 function buildFrames(force:Bool = false):FlxFramesCollection { static var eventFrames:FlxFramesCollection = null; @@ -112,7 +112,7 @@ class ChartEditorEventSprite extends FlxSprite this.updateHitbox(); } - function set_eventData(value:SongEventData):SongEventData + function set_eventData(value:Null<SongEventData>):Null<SongEventData> { this.eventData = value; diff --git a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx index 14ffa3a76..0adbf1a20 100644 --- a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx +++ b/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx @@ -27,7 +27,7 @@ class ChartEditorNoteSprite extends FlxSprite * The note data that this sprite represents. * You can set this to null to kill the sprite and flag it for recycling. */ - public var noteData(default, set):SongNoteData; + public var noteData(default, set):Null<SongNoteData>; /** * The name of the note style currently in use. @@ -70,7 +70,7 @@ class ChartEditorNoteSprite extends FlxSprite this.animation.addByPrefix('tapRightPixel', 'pixel7'); } - static var noteFrameCollection:FlxFramesCollection = null; + static var noteFrameCollection:Null<FlxFramesCollection> = null; /** * We load all the note frames once, then reuse them. @@ -108,7 +108,7 @@ class ChartEditorNoteSprite extends FlxSprite } } - function set_noteData(value:SongNoteData):SongNoteData + function set_noteData(value:Null<SongNoteData>):Null<SongNoteData> { this.noteData = value; diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index aa6e70714..83c052050 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1,5 +1,6 @@ package funkin.ui.debug.charting; +import flixel.system.FlxAssets.FlxSoundAsset; import flixel.math.FlxMath; import haxe.ui.components.TextField; import haxe.ui.components.DropDown; @@ -85,6 +86,7 @@ using Lambda; */ // Give other classes access to private instance fields +@:nullSafety @:allow(funkin.ui.debug.charting.ChartEditorCommand) @:allow(funkin.ui.debug.charting.ChartEditorDialogHandler) @:allow(funkin.ui.debug.charting.ChartEditorThemeHandler) @@ -238,7 +240,7 @@ class ChartEditorState extends HaxeUIState * 40 means the playhead is 1 grid length below the base position. * -40 means the playhead is 1 grid length above the base position. */ - var playheadPositionInPixels(default, set):Float; + var playheadPositionInPixels(default, set):Float = 0.0; function set_playheadPositionInPixels(value:Float):Float { @@ -258,28 +260,40 @@ class ChartEditorState extends HaxeUIState * playheadPosition, converted to steps. * NOT dependant on BPM, because the size of a grid square does not change with BPM. */ - var playheadPositionInSteps(get, null):Float; + var playheadPositionInSteps(get, set):Float; function get_playheadPositionInSteps():Float { return playheadPositionInPixels / GRID_SIZE; } + function set_playheadPositionInSteps(value:Float):Float + { + playheadPositionInPixels = value * GRID_SIZE; + return value; + } + /** * playheadPosition, converted to milliseconds. * DEPENDANT on BPM, because the duration of a grid square changes with BPM. */ - var playheadPositionInMs(get, null):Float; + var playheadPositionInMs(get, set):Float; function get_playheadPositionInMs():Float { return Conductor.getStepTimeInMs(playheadPositionInSteps); } + function set_playheadPositionInMs(value:Float):Float + { + playheadPositionInSteps = Conductor.getTimeInSteps(value); + return value; + } + /** * songLength, in milliseconds. */ - @:isVar var songLengthInMs(get, set):Float; + @:isVar var songLengthInMs(get, set):Float = 0; function get_songLengthInMs():Float { @@ -337,7 +351,7 @@ class ChartEditorState extends HaxeUIState * Dictates the appearance of many UI elements. * Currently hardcoded to just Light and Dark. */ - var currentTheme(default, set):ChartEditorTheme = null; + var currentTheme(default, set):ChartEditorTheme = ChartEditorTheme.Light; function set_currentTheme(value:ChartEditorTheme):ChartEditorTheme { @@ -350,9 +364,10 @@ class ChartEditorState extends HaxeUIState /** * Whether a skip button has been pressed on the playbar, and which one. + * `null` if no button has been pressed. * This will be used to update the scrollPosition (in the same function that handles the scroll wheel), then cleared. */ - var playbarButtonPressed:String = null; + var playbarButtonPressed:Null<String> = null; /** * Whether the head of the playbar is currently being dragged with the mouse by the user. @@ -392,13 +407,15 @@ class ChartEditorState extends HaxeUIState /** * The character sprite in the Player Preview window. + * `null` until accessed. */ - var currentPlayerCharacterPlayer:CharacterPlayer = null; + var currentPlayerCharacterPlayer:Null<CharacterPlayer> = null; /** * The character sprite in the Opponent Preview window. + * `null` until accessed. */ - var currentOpponentCharacterPlayer:CharacterPlayer = null; + var currentOpponentCharacterPlayer:Null<CharacterPlayer> = null; /** * The currently selected live input style. @@ -431,7 +448,7 @@ class ChartEditorState extends HaxeUIState /** * Whether hitsounds are enabled for at least one character. */ - var hitsoundsEnabled(get, null):Bool; + var hitsoundsEnabled(get, never):Bool; function get_hitsoundsEnabled():Bool { @@ -452,14 +469,14 @@ class ChartEditorState extends HaxeUIState * Whether the user's mouse cursor is hovering over a SOLID component of the HaxeUI. * If so, ignore mouse events underneath. */ - var isCursorOverHaxeUI(get, null):Bool; + var isCursorOverHaxeUI(get, never):Bool; function get_isCursorOverHaxeUI():Bool { return Screen.instance.hasSolidComponentUnderPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); } - var isCursorOverHaxeUIButton(get, null):Bool; + var isCursorOverHaxeUIButton(get, never):Bool; function get_isCursorOverHaxeUIButton():Bool { @@ -575,10 +592,13 @@ class ChartEditorState extends HaxeUIState } else { - // Stop the auto-save timer. - autoSaveTimer.cancel(); - autoSaveTimer.destroy(); - autoSaveTimer = null; + if (autoSaveTimer != null) + { + // Stop the auto-save timer. + autoSaveTimer.cancel(); + autoSaveTimer.destroy(); + autoSaveTimer = null; + } } return saveDataDirty = value; @@ -587,7 +607,7 @@ class ChartEditorState extends HaxeUIState /** * A timer used to auto-save the chart after a period of inactivity. */ - var autoSaveTimer:FlxTimer; + var autoSaveTimer:Null<FlxTimer> = null; /** * Whether the difficulty tree view in the toolbox has been modified and needs to be updated. @@ -672,9 +692,10 @@ class ChartEditorState extends HaxeUIState /** * The position where the user clicked to start a selection. + * `null` if the user isn't currently selecting anything. * The selection box extends from this point to the current mouse position. */ - var selectionBoxStartPos:FlxPoint = null; + var selectionBoxStartPos:Null<FlxPoint> = null; /** * Whether the user's last mouse click was on the playhead scroll area. @@ -685,13 +706,14 @@ class ChartEditorState extends HaxeUIState * Where the user's last mouse click was on the note preview scroll area. * `null` if the user isn't clicking on the note preview. */ - var notePreviewScrollAreaStartPos:FlxPoint = null; + var notePreviewScrollAreaStartPos:Null<FlxPoint> = null; /** * The SongNoteData which is currently being placed. + * `null` if the user isn't currently placing a note. * As the user drags, we will update this note's sustain length. */ - var currentPlaceNoteData:SongNoteData = null; + var currentPlaceNoteData:Null<SongNoteData> = null; /** * The Dialog components representing the currently available tool windows. @@ -706,18 +728,21 @@ class ChartEditorState extends HaxeUIState /** * The audio track for the instrumental. + * `null` until an instrumental track is loaded. */ - var audioInstTrack:FlxSound; + var audioInstTrack:Null<FlxSound> = null; /** * The raw byte data for the instrumental audio track. + * `null` until an instrumental track is loaded. */ - var audioInstTrackData:Bytes = null; + var audioInstTrackData:Null<Bytes> = null; /** * The audio track for the vocals. + * `null` until vocal track(s) are loaded. */ - var audioVocalTrackGroup:VoicesGroup; + var audioVocalTrackGroup:Null<VoicesGroup> = null; /** * A map of the audio tracks for each character's vocals. @@ -738,12 +763,12 @@ class ChartEditorState extends HaxeUIState * - Keys are the variation IDs. At least one (`default`) must exist. * - Values are the relevant metadata, ready to be serialized to JSON. */ - var songMetadata:Map<String, SongMetadata>; + var songMetadata:Map<String, SongMetadata> = []; /** * Retrieves the list of variations for the current song. */ - var availableVariations(get, null):Array<String>; + var availableVariations(get, never):Array<String>; function get_availableVariations():Array<String> { @@ -756,21 +781,28 @@ class ChartEditorState extends HaxeUIState * Retrieves the list of difficulties for the current variation of the current song. * ONLY CONTAINS DIFFICULTIES FOR THE CURRENT VARIATION so if on the default variation, erect/nightmare won't be included. */ - var availableDifficulties(get, null):Array<String>; + var availableDifficulties(get, never):Array<String>; function get_availableDifficulties():Array<String> { - return songMetadata.get(selectedVariation).playData.difficulties; + var m:Null<SongMetadata> = songMetadata.get(selectedVariation); + return m?.playData?.difficulties ?? []; } /** * Retrieves the list of difficulties for ALL variations of the current song. */ - var allDifficulties(get, null):Array<String>; + var allDifficulties(get, never):Array<String>; function get_allDifficulties():Array<String> { - var result:Array<Array<String>> = [for (x in availableVariations) songMetadata.get(x).playData.difficulties]; + var result:Array<Array<String>> = [ + for (x in availableVariations) + { + var m:Null<SongMetadata> = songMetadata.get(x); + m?.playData?.difficulties ?? []; + } + ]; return result.flatten(); } @@ -779,7 +811,7 @@ class ChartEditorState extends HaxeUIState * - Keys are the variation IDs. At least one (`default`) must exist. * - Values are the relevant chart data, ready to be serialized to JSON. */ - var songChartData:Map<String, SongChartData>; + var songChartData:Map<String, SongChartData> = []; /** * Convenience property to get the chart data for the current variation. @@ -788,7 +820,7 @@ class ChartEditorState extends HaxeUIState function get_currentSongMetadata():SongMetadata { - var result:SongMetadata = songMetadata.get(selectedVariation); + var result:Null<SongMetadata> = songMetadata.get(selectedVariation); if (result == null) { result = new SongMetadata('Dad Battle', 'Kawai Sprite', selectedVariation); @@ -810,7 +842,7 @@ class ChartEditorState extends HaxeUIState function get_currentSongChartData():SongChartData { - var result:SongChartData = songChartData.get(selectedVariation); + var result:Null<SongChartData> = songChartData.get(selectedVariation); if (result == null) { result = new SongChartData(1.0, [], []); @@ -944,7 +976,7 @@ class ChartEditorState extends HaxeUIState return currentSongMetadata.songName = value; } - var currentSongId(get, null):String; + var currentSongId(get, never):String; function get_currentSongId():String { @@ -968,7 +1000,7 @@ class ChartEditorState extends HaxeUIState return currentSongMetadata.artist = value; } - var currentSongPlayableCharacters(get, null):Array<String>; + var currentSongPlayableCharacters(get, never):Array<String>; function get_currentSongPlayableCharacters():Array<String> { @@ -1025,7 +1057,7 @@ class ChartEditorState extends HaxeUIState * SIGNALS */ // ============================== - // public var onDifficultyChange(default, null):FlxTypedSignal<ChartEditorState->Void> = new FlxTypedSignal<ChartEditorState->Void>(); + // public var onDifficultyChange(default, never):FlxTypedSignal<ChartEditorState->Void> = new FlxTypedSignal<ChartEditorState->Void>(); /** * RENDER OBJECTS */ @@ -1034,7 +1066,7 @@ class ChartEditorState extends HaxeUIState /** * The IMAGE used for the grid. Updated by ChartEditorThemeHandler. */ - var gridBitmap:BitmapData; + var gridBitmap:Null<BitmapData> = null; /** * The IMAGE used for the selection squares. Updated by ChartEditorThemeHandler. @@ -1042,100 +1074,114 @@ class ChartEditorState extends HaxeUIState * 1. A sprite is given this bitmap and placed over selected notes. * 2. The image is split and used for a 9-slice sprite for the selection box. */ - var selectionSquareBitmap:BitmapData = null; + var selectionSquareBitmap:Null<BitmapData> = null; /** * The IMAGE used for the note preview bitmap. Updated by ChartEditorThemeHandler. * The image is split and used for a 9-slice sprite for the box over the note preview. */ - var notePreviewViewportBitmap:BitmapData = null; + var notePreviewViewportBitmap:Null<BitmapData> = null; /** * The tiled sprite used to display the grid. * The height is the length of the song, and scrolling is done by simply the sprite. */ - var gridTiledSprite:FlxSprite; + var gridTiledSprite:Null<FlxSprite> = null; /** * The playhead representing the current position in the song. * Can move around on the grid independently of the view. */ - var gridPlayhead:FlxSpriteGroup; + var gridPlayhead:FlxSpriteGroup = new FlxSpriteGroup(); - var gridPlayheadScrollArea:FlxSprite; + var gridPlayheadScrollArea:Null<FlxSprite> = null; /** * A sprite used to indicate the note that will be placed on click. */ - var gridGhostNote:ChartEditorNoteSprite; + var gridGhostNote:Null<ChartEditorNoteSprite> = null; /** * A sprite used to indicate the event that will be placed on click. */ - var gridGhostEvent:ChartEditorEventSprite; + var gridGhostEvent:Null<ChartEditorEventSprite> = null; /** * The waveform which (optionally) displays over the grid, underneath the notes and playhead. */ - var gridSpectrogram:PolygonSpectogram; + var gridSpectrogram:Null<PolygonSpectogram> = null; /** * The sprite used to display the note preview area. * We move this up and down to scroll the preview. */ - var notePreview:ChartEditorNotePreview; + var notePreview:Null<ChartEditorNotePreview> = null; /** * The rectangular sprite used for representing the current viewport on the note preview. * We move this up and down and resize it to represent the visible area. */ - var notePreviewViewport:FlxSliceSprite; + var notePreviewViewport:Null<FlxSliceSprite> = null; /** * The rectangular sprite used for rendering the selection box. * Uses a 9-slice to stretch the selection box to the correct size without warping. */ - var selectionBoxSprite:FlxSliceSprite; + var selectionBoxSprite:Null<FlxSliceSprite> = null; /** * The opponent's health icon. */ - var healthIconDad:HealthIcon; + var healthIconDad:Null<HealthIcon> = null; /** * The player's health icon. */ - var healthIconBF:HealthIcon; + var healthIconBF:Null<HealthIcon> = null; /** * The purple background sprite. */ - var menuBG:FlxSprite; + var menuBG:Null<FlxSprite> = null; + + /** + * The layout containing the playbar head slider. + */ + var playbarHeadLayout:Null<Component> = null; + + /** + * The playbar head slider. + */ + var playbarHead:Null<Slider> = null; + + /** + * The current process that is lerping the scroll position. + * Used to cancel the previous lerp if the user scrolls again. + */ + var currentScrollEase:Null<VarTween>; /** * The sprite group containing the note graphics. * Only displays a subset of the data from `currentSongChartNoteData`, * and kills notes that are off-screen to be recycled later. */ - var renderedNotes:FlxTypedSpriteGroup<ChartEditorNoteSprite>; + var renderedNotes:FlxTypedSpriteGroup<ChartEditorNoteSprite> = new FlxTypedSpriteGroup<ChartEditorNoteSprite>(); /** * The sprite group containing the hold note graphics. * Only displays a subset of the data from `currentSongChartNoteData`, * and kills notes that are off-screen to be recycled later. */ - var renderedHoldNotes:FlxTypedSpriteGroup<ChartEditorHoldNoteSprite>; + var renderedHoldNotes:FlxTypedSpriteGroup<ChartEditorHoldNoteSprite> = new FlxTypedSpriteGroup<ChartEditorHoldNoteSprite>(); /** * The sprite group containing the song events. * Only displays a subset of the data from `currentSongChartEventData`, * and kills events that are off-screen to be recycled later. */ - var renderedEvents:FlxTypedSpriteGroup<ChartEditorEventSprite>; + var renderedEvents:FlxTypedSpriteGroup<ChartEditorEventSprite> = new FlxTypedSpriteGroup<ChartEditorEventSprite>(); - var renderedSelectionSquares:FlxTypedSpriteGroup<FlxSprite>; - - var playbarHead:Slider; + var renderedSelectionSquares:FlxTypedSpriteGroup<FlxSprite> = new FlxTypedSpriteGroup<FlxSprite>(); public function new() { @@ -1159,7 +1205,7 @@ class ChartEditorState extends HaxeUIState buildBackground(); - currentTheme = ChartEditorTheme.Light; + ChartEditorThemeHandler.updateTheme(this); buildGrid(); // buildSpectrogram(audioInstTrack); @@ -1213,6 +1259,8 @@ class ChartEditorState extends HaxeUIState */ function buildGrid():Void { + if (gridBitmap == null) throw 'ERROR: Tried to build grid, but gridBitmap is null! Check ChartEditorThemeHandler.updateTheme().'; + gridTiledSprite = new FlxTiledSprite(gridBitmap, gridBitmap.width, 1000, false, true); gridTiledSprite.x = FlxG.width / 2 - GRID_SIZE * STRUMLINE_SIZE; // Center the grid. gridTiledSprite.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; // Push down to account for the menu bar. @@ -1241,7 +1289,6 @@ class ChartEditorState extends HaxeUIState gridPlayheadScrollArea.zIndex = 25; // The playhead that show the current position in the song. - gridPlayhead = new FlxSpriteGroup(); add(gridPlayhead); gridPlayhead.zIndex = 30; @@ -1279,6 +1326,8 @@ class ChartEditorState extends HaxeUIState function buildSelectionBox():Void { + if (selectionBoxSprite == null) throw 'ERROR: Tried to build selection box, but selectionBoxSprite is null! Check ChartEditorThemeHandler.updateTheme().'; + selectionBoxSprite.scrollFactor.set(0, 0); add(selectionBoxSprite); selectionBoxSprite.zIndex = 30; @@ -1288,6 +1337,9 @@ class ChartEditorState extends HaxeUIState function setSelectionBoxBounds(bounds:FlxRect = null):Void { + if (selectionBoxSprite == null) + throw 'ERROR: Tried to set selection box bounds, but selectionBoxSprite is null! Check ChartEditorThemeHandler.updateTheme().'; + if (bounds == null) { selectionBoxSprite.visible = false; @@ -1312,6 +1364,8 @@ class ChartEditorState extends HaxeUIState notePreview.y = MENU_BAR_HEIGHT + GRID_TOP_PAD; add(notePreview); + if (notePreviewViewport == null) throw 'ERROR: Tried to build note preview, but notePreviewViewport is null! Check ChartEditorThemeHandler.updateTheme().'; + notePreviewViewport.scrollFactor.set(0, 0); add(notePreviewViewport); notePreviewViewport.zIndex = 30; @@ -1323,6 +1377,9 @@ class ChartEditorState extends HaxeUIState { var bounds:FlxRect = new FlxRect(); + // Return 0, 0, 0, 0 if the note preview doesn't exist for some reason. + if (notePreview == null) return bounds; + // Horizontal position and width are constant. bounds.x = notePreview.x; bounds.width = notePreview.width; @@ -1356,6 +1413,9 @@ class ChartEditorState extends HaxeUIState function setNotePreviewViewportBounds(bounds:FlxRect = null):Void { + if (notePreviewViewport == null) + throw 'ERROR: Tried to set note preview viewport bounds, but notePreviewViewport is null! Check ChartEditorThemeHandler.updateTheme().'; + if (bounds == null) { notePreviewViewport.visible = false; @@ -1387,32 +1447,31 @@ class ChartEditorState extends HaxeUIState */ function buildNoteGroup():Void { - renderedHoldNotes = new FlxTypedSpriteGroup<ChartEditorHoldNoteSprite>(); + if (gridTiledSprite == null) throw 'ERROR: Tried to build note groups, but gridTiledSprite is null! Check ChartEditorState.buildGrid().'; + renderedHoldNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y); add(renderedHoldNotes); renderedHoldNotes.zIndex = 24; - renderedNotes = new FlxTypedSpriteGroup<ChartEditorNoteSprite>(); renderedNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y); add(renderedNotes); renderedNotes.zIndex = 25; - renderedEvents = new FlxTypedSpriteGroup<ChartEditorEventSprite>(); renderedEvents.setPosition(gridTiledSprite.x, gridTiledSprite.y); add(renderedEvents); renderedNotes.zIndex = 25; - renderedSelectionSquares = new FlxTypedSpriteGroup<FlxSprite>(); renderedSelectionSquares.setPosition(gridTiledSprite.x, gridTiledSprite.y); add(renderedSelectionSquares); renderedNotes.zIndex = 26; } - var playbarHeadLayout:Component; - function buildAdditionalUI():Void { playbarHeadLayout = buildComponent(CHART_EDITOR_PLAYBARHEAD_LAYOUT); + + if (playbarHeadLayout == null) throw 'ERROR: Failed to construct playbarHeadLayout! Check "${CHART_EDITOR_PLAYBARHEAD_LAYOUT}".'; + playbarHeadLayout.zIndex = 110; playbarHeadLayout.width = FlxG.width - 8; @@ -1421,6 +1480,7 @@ class ChartEditorState extends HaxeUIState playbarHeadLayout.y = FlxG.height - 48 - 8; playbarHead = playbarHeadLayout.findComponent('playbarHead', Slider); + if (playbarHead == null) throw 'ERROR: Failed to fetch playbarHead from playbarHeadLayout! Check "${CHART_EDITOR_PLAYBARHEAD_LAYOUT}".'; playbarHead.allowFocus = false; playbarHead.width = FlxG.width; playbarHead.height = 10; @@ -1445,7 +1505,7 @@ class ChartEditorState extends HaxeUIState playbarHeadDragging = false; // Set the song position to where the playhead was moved to. - scrollPositionInPixels = songLengthInPixels * (playbarHead.value / 100); + scrollPositionInPixels = songLengthInPixels * (playbarHead?.value ?? 0 / 100); // Update the conductor and audio tracks to match. moveSongToScrollPosition(); @@ -1584,31 +1644,40 @@ class ChartEditorState extends HaxeUIState addUIChangeListener('menubarItemOpponentHitsounds', event -> hitsoundsEnabledOpponent = event.value); setUICheckboxSelected('menubarItemOpponentHitsounds', hitsoundsEnabledOpponent); - var instVolumeLabel:Label = findComponent('menubarLabelVolumeInstrumental', Label); - addUIChangeListener('menubarItemVolumeInstrumental', function(event:UIEvent) { - var volume:Float = event.value / 100.0; - if (audioInstTrack != null) audioInstTrack.volume = volume; - instVolumeLabel.text = 'Instrumental - ${Std.int(event.value)}%'; - }); + var instVolumeLabel:Null<Label> = findComponent('menubarLabelVolumeInstrumental', Label); + if (instVolumeLabel != null) + { + addUIChangeListener('menubarItemVolumeInstrumental', function(event:UIEvent) { + var volume:Float = event?.value ?? 0 / 100.0; + if (audioInstTrack != null) audioInstTrack.volume = volume; + instVolumeLabel.text = 'Instrumental - ${Std.int(event.value)}%'; + }); + } - var vocalsVolumeLabel:Label = findComponent('menubarLabelVolumeVocals', Label); - addUIChangeListener('menubarItemVolumeVocals', function(event:UIEvent) { - var volume:Float = event.value / 100.0; - if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume; - vocalsVolumeLabel.text = 'Vocals - ${Std.int(event.value)}%'; - }); + var vocalsVolumeLabel:Null<Label> = findComponent('menubarLabelVolumeVocals', Label); + if (vocalsVolumeLabel != null) + { + addUIChangeListener('menubarItemVolumeVocals', function(event:UIEvent) { + var volume:Float = event?.value ?? 0 / 100.0; + if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume; + vocalsVolumeLabel.text = 'Vocals - ${Std.int(event.value)}%'; + }); + } - var playbackSpeedLabel:Label = findComponent('menubarLabelPlaybackSpeed', Label); - addUIChangeListener('menubarItemPlaybackSpeed', function(event:UIEvent) { - var pitch:Float = event.value * 2.0 / 100.0; - pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25. - #if FLX_PITCH - if (audioInstTrack != null) audioInstTrack.pitch = pitch; - if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch; - #end - var pitchDisplay:Float = Std.int(pitch * 100) / 100; // Round to 2 decimal places. - playbackSpeedLabel.text = 'Playback Speed - ${pitchDisplay}x'; - }); + var playbackSpeedLabel:Null<Label> = findComponent('menubarLabelPlaybackSpeed', Label); + if (playbackSpeedLabel != null) + { + addUIChangeListener('menubarItemPlaybackSpeed', function(event:UIEvent) { + var pitch:Float = event.value * 2.0 / 100.0; + pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25. + #if FLX_PITCH + if (audioInstTrack != null) audioInstTrack.pitch = pitch; + if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch; + #end + var pitchDisplay:Float = Std.int(pitch * 100) / 100; // Round to 2 decimal places. + playbackSpeedLabel.text = 'Playback Speed - ${pitchDisplay}x'; + }); + } addUIChangeListener('menubarItemToggleToolboxTools', event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT, event.value)); @@ -1725,12 +1794,14 @@ class ChartEditorState extends HaxeUIState #end // Right align the BF health icon. - - // Base X position to the right of the grid. - var baseHealthIconXPos:Float = gridTiledSprite.x + GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + 15; - // Will be 0 when not bopping. When bopping, will increase to push the icon left. - var healthIconOffset:Float = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5); - healthIconBF.x = baseHealthIconXPos - healthIconOffset; + if (healthIconBF != null) + { + // Base X position to the right of the grid. + var baseHealthIconXPos:Float = gridTiledSprite?.x ?? 0.0 + GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + 15; + // Will be 0 when not bopping. When bopping, will increase to push the icon left. + var healthIconOffset:Float = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5); + healthIconBF.x = baseHealthIconXPos - healthIconOffset; + } } /** @@ -1759,8 +1830,8 @@ class ChartEditorState extends HaxeUIState if (audioInstTrack != null && audioInstTrack.playing) { - healthIconDad.onStepHit(Conductor.currentStep); - healthIconBF.onStepHit(Conductor.currentStep); + if (healthIconDad != null) healthIconDad.onStepHit(Conductor.currentStep); + if (healthIconBF != null) healthIconBF.onStepHit(Conductor.currentStep); } // Updating these every step keeps it more accurate. @@ -1976,6 +2047,8 @@ class ChartEditorState extends HaxeUIState if (shouldHandleCursor) { + if (gridTiledSprite == null) throw "ERROR: Tried to handle cursor, but gridTiledSprite is null! Check ChartEditorState.buildGrid()"; + var overlapsGrid:Bool = FlxG.mouse.overlaps(gridTiledSprite); // Cursor position relative to the grid. @@ -1989,11 +2062,11 @@ class ChartEditorState extends HaxeUIState if (FlxG.mouse.justPressed) { - if (FlxG.mouse.overlaps(gridPlayheadScrollArea)) + if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea)) { gridPlayheadScrollAreaPressed = true; } - else if (FlxG.mouse.overlaps(notePreview)) + else if (notePreview != null && FlxG.mouse.overlaps(notePreview)) { // Clicked note preview notePreviewScrollAreaStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); @@ -2019,7 +2092,7 @@ class ChartEditorState extends HaxeUIState { Cursor.cursorMode = Pointer; } - else if (FlxG.mouse.overlaps(gridPlayheadScrollArea)) + else if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea)) { Cursor.cursorMode = Pointer; } @@ -2180,10 +2253,10 @@ class ChartEditorState extends HaxeUIState trace('Scroll up: ' + diff); moveSongToScrollPosition(); } - else if (FlxG.mouse.screenY > playbarHeadLayout.y) + else if (FlxG.mouse.screenY > (playbarHeadLayout?.y ?? 0.0)) { // Scroll down. - var diff:Float = FlxG.mouse.screenY - playbarHeadLayout.y; + var diff:Float = FlxG.mouse.screenY - (playbarHeadLayout?.y ?? 0.0); scrollPositionInPixels += diff * 0.5; // Too fast! trace('Scroll down: ' + diff); moveSongToScrollPosition(); @@ -2209,11 +2282,11 @@ class ChartEditorState extends HaxeUIState // We clicked on the grid without moving the mouse. // Find the first note that is at the cursor position. - var highlightedNote:ChartEditorNoteSprite = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool { + var highlightedNote:Null<ChartEditorNoteSprite> = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool { // If note.alive is false, the note is dead and awaiting recycling. return note.alive && FlxG.mouse.overlaps(note); }); - var highlightedEvent:ChartEditorEventSprite = null; + var highlightedEvent:Null<ChartEditorEventSprite> = null; if (highlightedNote == null) { highlightedEvent = renderedEvents.members.find(function(event:ChartEditorEventSprite):Bool { @@ -2223,7 +2296,7 @@ class ChartEditorState extends HaxeUIState if (FlxG.keys.pressed.CONTROL) { - if (highlightedNote != null) + if (highlightedNote != null && highlightedNote.noteData != null) { // TODO: Handle the case of clicking on a sustain piece. // Control click to select/deselect an individual note. @@ -2236,7 +2309,7 @@ class ChartEditorState extends HaxeUIState performCommand(new SelectItemsCommand([highlightedNote.noteData], [])); } } - else if (highlightedEvent != null) + else if (highlightedEvent != null && highlightedEvent.eventData != null) { // Control click to select/deselect an individual note. if (isEventSelected(highlightedEvent.eventData)) @@ -2255,12 +2328,12 @@ class ChartEditorState extends HaxeUIState } else { - if (highlightedNote != null) + if (highlightedNote != null && highlightedNote.noteData != null) { // Click a note to select it. performCommand(new SetItemSelectionCommand([highlightedNote.noteData], [], currentNoteSelection, currentEventSelection)); } - else if (highlightedEvent != null) + else if (highlightedEvent != null && highlightedEvent.eventData != null) { // Click an event to select it. performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData], currentNoteSelection, currentEventSelection)); @@ -2288,7 +2361,8 @@ class ChartEditorState extends HaxeUIState else if (notePreviewScrollAreaStartPos != null) { trace('Updating current song time while clicking and holding...'); - var clickedPosInPixels:Float = FlxMath.remapToRange(FlxG.mouse.screenY, notePreview.y, notePreview.y + notePreview.height, 0, songLengthInPixels); + var clickedPosInPixels:Float = FlxMath.remapToRange(FlxG.mouse.screenY, (notePreview?.y ?? 0.0), + (notePreview?.y ?? 0.0) + (notePreview?.height ?? 0.0), 0, songLengthInPixels); scrollPositionInPixels = clickedPosInPixels; moveSongToScrollPosition(); @@ -2329,11 +2403,11 @@ class ChartEditorState extends HaxeUIState // We clicked on the grid without moving the mouse. // Find the first note that is at the cursor position. - var highlightedNote:ChartEditorNoteSprite = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool { + var highlightedNote:Null<ChartEditorNoteSprite> = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool { // If note.alive is false, the note is dead and awaiting recycling. return note.alive && FlxG.mouse.overlaps(note); }); - var highlightedEvent:ChartEditorEventSprite = null; + var highlightedEvent:Null<ChartEditorEventSprite> = null; if (highlightedNote == null) { highlightedEvent = renderedEvents.members.find(function(event:ChartEditorEventSprite):Bool { @@ -2345,7 +2419,7 @@ class ChartEditorState extends HaxeUIState if (FlxG.keys.pressed.CONTROL) { // Control click to select/deselect an individual note. - if (highlightedNote != null) + if (highlightedNote != null && highlightedNote.noteData != null) { if (isNoteSelected(highlightedNote.noteData)) { @@ -2356,7 +2430,7 @@ class ChartEditorState extends HaxeUIState performCommand(new SelectItemsCommand([highlightedNote.noteData], [])); } } - else if (highlightedEvent != null) + else if (highlightedEvent != null && highlightedEvent.eventData != null) { if (isEventSelected(highlightedEvent.eventData)) { @@ -2374,12 +2448,12 @@ class ChartEditorState extends HaxeUIState } else { - if (highlightedNote != null) + if (highlightedNote != null && highlightedNote.noteData != null) { // Click a note to select it. performCommand(new SetItemSelectionCommand([highlightedNote.noteData], [], currentNoteSelection, currentEventSelection)); } - else if (highlightedEvent != null) + else if (highlightedEvent != null && highlightedEvent.eventData != null) { // Click an event to select it. performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData], currentNoteSelection, currentEventSelection)); @@ -2421,11 +2495,11 @@ class ChartEditorState extends HaxeUIState // We right clicked on the grid. // Find the first note that is at the cursor position. - var highlightedNote:ChartEditorNoteSprite = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool { + var highlightedNote:Null<ChartEditorNoteSprite> = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool { // If note.alive is false, the note is dead and awaiting recycling. return note.alive && FlxG.mouse.overlaps(note); }); - var highlightedEvent:ChartEditorEventSprite = null; + var highlightedEvent:Null<ChartEditorEventSprite> = null; if (highlightedNote == null) { highlightedEvent = renderedEvents.members.find(function(event:ChartEditorEventSprite):Bool { @@ -2434,13 +2508,13 @@ class ChartEditorState extends HaxeUIState }); } - if (highlightedNote != null) + if (highlightedNote != null && highlightedNote.noteData != null) { // TODO: Handle the case of clicking on a sustain piece. // Remove the note. performCommand(new RemoveNotesCommand([highlightedNote.noteData])); } - else if (highlightedEvent != null) + else if (highlightedEvent != null && highlightedEvent.eventData != null) { // Remove the event. performCommand(new RemoveEventsCommand([highlightedEvent.eventData])); @@ -2460,30 +2534,41 @@ class ChartEditorState extends HaxeUIState if (cursorColumn == eventColumn) { - gridGhostEvent.visible = true; - gridGhostNote.visible = false; + if (gridGhostNote != null) gridGhostNote.visible = false; - if (selectedEventKind != gridGhostEvent.eventData.event) + if (gridGhostEvent == null) throw "ERROR: Tried to handle cursor, but gridGhostEvent is null! Check ChartEditorState.buildGrid()"; + + var eventData:SongEventData = gridGhostEvent.eventData != null ? gridGhostEvent.eventData : new SongEventData(cursorMs, selectedEventKind, null); + + if (selectedEventKind != eventData.event) { - gridGhostEvent.eventData.event = selectedEventKind; + eventData.event = selectedEventKind; } + eventData.time = cursorMs; - gridGhostEvent.eventData.time = cursorMs; + gridGhostEvent.visible = true; + gridGhostEvent.eventData = eventData; gridGhostEvent.updateEventPosition(renderedEvents); } else { - gridGhostEvent.visible = false; - gridGhostNote.visible = true; + if (gridGhostEvent != null) gridGhostEvent.visible = false; - if (cursorColumn != gridGhostNote.noteData.data || selectedNoteKind != gridGhostNote.noteData.kind) + if (gridGhostNote == null) throw "ERROR: Tried to handle cursor, but gridGhostNote is null! Check ChartEditorState.buildGrid()"; + + var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, + selectedNoteKind); + + if (cursorColumn != noteData.data || selectedNoteKind != noteData.kind) { - gridGhostNote.noteData.kind = selectedNoteKind; - gridGhostNote.noteData.data = cursorColumn; + noteData.kind = selectedNoteKind; + noteData.data = cursorColumn; gridGhostNote.playNoteAnimation(); } + noteData.time = cursorMs; - gridGhostNote.noteData.time = cursorMs; + gridGhostNote.visible = true; + gridGhostNote.noteData = noteData; gridGhostNote.updateNotePosition(renderedNotes); } @@ -2494,16 +2579,16 @@ class ChartEditorState extends HaxeUIState } else { - gridGhostNote.visible = false; - gridGhostEvent.visible = false; + if (gridGhostNote != null) gridGhostNote.visible = false; + if (gridGhostEvent != null) gridGhostEvent.visible = false; Cursor.cursorMode = Default; } } } else { - gridGhostNote.visible = false; - gridGhostEvent.visible = false; + if (gridGhostNote != null) gridGhostNote.visible = false; + if (gridGhostEvent != null) gridGhostEvent.visible = false; } if (isCursorOverHaxeUIButton && Cursor.cursorMode == Default) @@ -2533,7 +2618,7 @@ class ChartEditorState extends HaxeUIState var displayedNoteData:Array<SongNoteData> = []; for (noteSprite in renderedNotes.members) { - if (noteSprite == null || !noteSprite.exists || !noteSprite.visible) continue; + if (noteSprite == null || noteSprite.noteData == null || !noteSprite.exists || !noteSprite.visible) continue; if (!noteSprite.isNoteVisible(viewAreaBottomPixels, viewAreaTopPixels)) { @@ -2590,7 +2675,7 @@ class ChartEditorState extends HaxeUIState var displayedEventData:Array<SongEventData> = []; for (eventSprite in renderedEvents.members) { - if (eventSprite == null || !eventSprite.exists || !eventSprite.visible) continue; + if (eventSprite == null || eventSprite.eventData == null || !eventSprite.exists || !eventSprite.visible) continue; if (!eventSprite.isEventVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD)) { @@ -2618,7 +2703,7 @@ class ChartEditorState extends HaxeUIState for (noteData in currentSongChartNoteData) { // Remember if we are already displaying this note. - if (displayedNoteData.indexOf(noteData) != -1) + if (noteData == null || displayedNoteData.indexOf(noteData) != -1) { continue; } @@ -2633,14 +2718,14 @@ class ChartEditorState extends HaxeUIState trace('Creating new Note... (${renderedNotes.members.length})'); noteSprite.parentState = this; - // The note sprite handles animation playback and positioning. - noteSprite.noteData = noteData; - // Setting note data resets position relative to the grid so we fix that. noteSprite.updateNotePosition(renderedNotes); + // The note sprite handles animation playback and positioning. + noteSprite.noteData = noteData; + // Add hold notes that are now visible (and not already displayed). - if (noteSprite.noteData.length > 0 && displayedHoldNoteData.indexOf(noteData) == -1) + if (noteSprite.noteData != null && noteSprite.noteData.length > 0 && displayedHoldNoteData.indexOf(noteSprite.noteData) == -1) { var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(() -> new ChartEditorHoldNoteSprite(this)); trace('Creating new HoldNote... (${renderedHoldNotes.members.length})'); @@ -2688,7 +2773,7 @@ class ChartEditorState extends HaxeUIState for (noteData in currentSongChartNoteData) { // Is the note a hold note? - if (noteData.length <= 0) continue; + if (noteData == null || noteData.length <= 0) continue; // Is the hold note rendered already? if (displayedHoldNoteData.indexOf(noteData) != -1) continue; @@ -2769,6 +2854,9 @@ class ChartEditorState extends HaxeUIState function buildSelectionSquare():FlxSprite { + if (selectionSquareBitmap == null) + throw "ERROR: Tried to build selection square, but selectionSquareBitmap is null! Check ChartEditorThemeHandler.updateSelectionSquare()"; + return new FlxSprite().loadGraphic(selectionSquareBitmap); } @@ -2777,6 +2865,9 @@ class ChartEditorState extends HaxeUIState */ function handlePlaybar():Void { + if (playbarHeadLayout == null) throw "ERROR: Tried to handle playbar, but playbarHeadLayout is null!"; + if (playbarHead == null) throw "ERROR: Tried to handle playbar, but playbarHeadLayout is null!"; + // Make sure the playbar is never nudged out of the correct spot. playbarHeadLayout.x = 4; playbarHeadLayout.y = FlxG.height - 48 - 8; @@ -3038,9 +3129,8 @@ class ChartEditorState extends HaxeUIState // Manage the Select Difficulty tree view. var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); - if (difficultyToolbox == null) return; - var treeView:TreeView = difficultyToolbox.findComponent('difficultyToolboxTree'); + var treeView:Null<TreeView> = difficultyToolbox.findComponent('difficultyToolboxTree'); if (treeView == null) return; // Clear the tree view so we can rebuild it. @@ -3050,12 +3140,10 @@ class ChartEditorState extends HaxeUIState var treeSong:TreeViewNode = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName'}); treeSong.expanded = true; - var variations = Reflect.copy(availableVariations); - variations.sort(SortUtil.alphabetically); - - for (curVariation in variations) + for (curVariation in availableVariations) { - var variationMetadata:SongMetadata = songMetadata.get(curVariation); + var variationMetadata:Null<SongMetadata> = songMetadata.get(curVariation); + if (variationMetadata == null) continue; var treeVariation:TreeViewNode = treeSong.addNode( { @@ -3093,8 +3181,8 @@ class ChartEditorState extends HaxeUIState if (treeView == null) return; } - trace(treeView); - treeView.selectedNode = getCurrentTreeDifficultyNode(treeView); + var currentTreeDifficultyNode = getCurrentTreeDifficultyNode(treeView); + if (currentTreeDifficultyNode != null) treeView.selectedNode = currentTreeDifficultyNode; } function handlePlayerPreviewToolbox():Void @@ -3103,7 +3191,7 @@ class ChartEditorState extends HaxeUIState var charPreviewToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); if (charPreviewToolbox == null) return; - var charPlayer:CharacterPlayer = charPreviewToolbox.findComponent('charPlayer'); + var charPlayer:Null<CharacterPlayer> = charPreviewToolbox.findComponent('charPlayer'); if (charPlayer == null) return; currentPlayerCharacterPlayer = charPlayer; @@ -3114,7 +3202,7 @@ class ChartEditorState extends HaxeUIState if (currentSongCharacterPlayer != charPlayer.charId) { - healthIconBF.characterId = currentSongCharacterPlayer; + if (healthIconBF != null) healthIconBF.characterId = currentSongCharacterPlayer; charPlayer.loadCharacter(currentSongCharacterPlayer); charPlayer.characterType = CharacterType.BF; @@ -3138,7 +3226,7 @@ class ChartEditorState extends HaxeUIState var charPreviewToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); if (charPreviewToolbox == null) return; - var charPlayer:CharacterPlayer = charPreviewToolbox.findComponent('charPlayer'); + var charPlayer:Null<CharacterPlayer> = charPreviewToolbox.findComponent('charPlayer'); if (charPlayer == null) return; currentOpponentCharacterPlayer = charPlayer; @@ -3149,7 +3237,7 @@ class ChartEditorState extends HaxeUIState if (currentSongCharacterOpponent != charPlayer.charId) { - healthIconDad.characterId = currentSongCharacterOpponent; + if (healthIconDad != null) healthIconDad.characterId = currentSongCharacterOpponent; charPlayer.loadCharacter(currentSongCharacterOpponent); charPlayer.characterType = CharacterType.DAD; @@ -3203,7 +3291,7 @@ class ChartEditorState extends HaxeUIState } } - function getCurrentTreeDifficultyNode(?treeView:TreeView = null):TreeViewNode + function getCurrentTreeDifficultyNode(?treeView:TreeView = null):Null<TreeViewNode> { if (treeView == null) { @@ -3235,7 +3323,8 @@ class ChartEditorState extends HaxeUIState { trace('No target node!'); // Reset the user's selection. - treeView.selectedNode = getCurrentTreeDifficultyNode(treeView); + var currentTreeDifficultyNode = getCurrentTreeDifficultyNode(treeView); + if (currentTreeDifficultyNode != null) treeView.selectedNode = currentTreeDifficultyNode; return; } @@ -3258,7 +3347,8 @@ class ChartEditorState extends HaxeUIState default: // Reset the user's selection. trace('Selected wrong node type, resetting selection.'); - treeView.selectedNode = getCurrentTreeDifficultyNode(treeView); + var currentTreeDifficultyNode = getCurrentTreeDifficultyNode(treeView); + if (currentTreeDifficultyNode != null) treeView.selectedNode = currentTreeDifficultyNode; refreshSongMetadataToolbox(); } } @@ -3270,31 +3360,31 @@ class ChartEditorState extends HaxeUIState { var toolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); - var inputSongName:TextField = toolbox.findComponent('inputSongName', TextField); - inputSongName.value = currentSongMetadata.songName; + var inputSongName:Null<TextField> = toolbox.findComponent('inputSongName', TextField); + if (inputSongName != null) inputSongName.value = currentSongMetadata.songName; - var inputSongArtist:TextField = toolbox.findComponent('inputSongArtist', TextField); - inputSongArtist.value = currentSongMetadata.artist; + var inputSongArtist:Null<TextField> = toolbox.findComponent('inputSongArtist', TextField); + if (inputSongArtist != null) inputSongArtist.value = currentSongMetadata.artist; - var inputStage:DropDown = toolbox.findComponent('inputStage', DropDown); - inputStage.value = currentSongMetadata.playData.stage; + var inputStage:Null<DropDown> = toolbox.findComponent('inputStage', DropDown); + if (inputStage != null) inputStage.value = currentSongMetadata.playData.stage; - var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown); - inputNoteSkin.value = currentSongMetadata.playData.noteSkin; + var inputNoteSkin:Null<DropDown> = toolbox.findComponent('inputNoteSkin', DropDown); + if (inputNoteSkin != null) inputNoteSkin.value = currentSongMetadata.playData.noteSkin; - var inputBPM:NumberStepper = toolbox.findComponent('inputBPM', NumberStepper); - inputBPM.value = currentSongMetadata.timeChanges[0].bpm; + var inputBPM:Null<NumberStepper> = toolbox.findComponent('inputBPM', NumberStepper); + if (inputBPM != null) inputBPM.value = currentSongMetadata.timeChanges[0].bpm; - var labelScrollSpeed:Label = toolbox.findComponent('labelScrollSpeed', Label); - labelScrollSpeed.text = 'Scroll Speed: ${currentSongChartScrollSpeed}x'; + var labelScrollSpeed:Null<Label> = toolbox.findComponent('labelScrollSpeed', Label); + if (labelScrollSpeed != null) labelScrollSpeed.text = 'Scroll Speed: ${currentSongChartScrollSpeed}x'; - var inputScrollSpeed:Slider = toolbox.findComponent('inputScrollSpeed', Slider); - inputScrollSpeed.value = currentSongChartScrollSpeed; + var inputScrollSpeed:Null<Slider> = toolbox.findComponent('inputScrollSpeed', Slider); + if (inputScrollSpeed != null) inputScrollSpeed.value = currentSongChartScrollSpeed; - var frameVariation:Frame = toolbox.findComponent('frameVariation', Frame); - frameVariation.text = 'Variation: ${selectedVariation.toTitleCase()}'; - var frameDifficulty:Frame = toolbox.findComponent('frameDifficulty', Frame); - frameDifficulty.text = 'Difficulty: ${selectedDifficulty.toTitleCase()}'; + var frameVariation:Null<Frame> = toolbox.findComponent('frameVariation', Frame); + if (frameVariation != null) frameVariation.text = 'Variation: ${selectedVariation.toTitleCase()}'; + var frameDifficulty:Null<Frame> = toolbox.findComponent('frameDifficulty', Frame); + if (frameDifficulty != null) frameDifficulty.text = 'Difficulty: ${selectedDifficulty.toTitleCase()}'; } function addDifficulty(variation:String):Void {} @@ -3322,7 +3412,7 @@ class ChartEditorState extends HaxeUIState */ function handleNotePreview():Void { - if (notePreviewDirty) + if (notePreviewDirty && notePreview != null) { notePreviewDirty = false; @@ -3349,7 +3439,7 @@ class ChartEditorState extends HaxeUIState commandHistoryDirty = false; // Update the Undo and Redo buttons. - var undoButton:MenuItem = findComponent('menubarItemUndo', MenuItem); + var undoButton:Null<MenuItem> = findComponent('menubarItemUndo', MenuItem); if (undoButton != null) { @@ -3371,7 +3461,7 @@ class ChartEditorState extends HaxeUIState trace('undoButton is null'); } - var redoButton:MenuItem = findComponent('menubarItemRedo', MenuItem); + var redoButton:Null<MenuItem> = findComponent('menubarItemRedo', MenuItem); if (redoButton != null) { @@ -3499,8 +3589,11 @@ class ChartEditorState extends HaxeUIState function startAudioPlayback():Void { - if (audioInstTrack != null) audioInstTrack.play(false, audioInstTrack.time); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(false, audioInstTrack.time); + if (audioInstTrack != null) + { + audioInstTrack.play(false, audioInstTrack.time); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(false, audioInstTrack.time); + } setComponentText('playbarPlay', '||'); } @@ -3595,17 +3688,17 @@ class ChartEditorState extends HaxeUIState // Move the grid sprite to the correct position. if (isViewDownscroll) { - gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD); + if (gridTiledSprite != null) gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD); } else { - gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD); + if (gridTiledSprite != null) gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD); } // Move the rendered notes to the correct position. - renderedNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y); - renderedHoldNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y); - renderedEvents.setPosition(gridTiledSprite.x, gridTiledSprite.y); - renderedSelectionSquares.setPosition(gridTiledSprite.x, gridTiledSprite.y); + 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; @@ -3618,7 +3711,7 @@ class ChartEditorState extends HaxeUIState /** * Transitions to the Play State to test the song */ - public function testSongInPlayState(?minimal:Bool = false):Void + public function testSongInPlayState(minimal:Bool = false):Void { var startTimestamp:Float = 0; if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs; @@ -3665,8 +3758,8 @@ class ChartEditorState extends HaxeUIState }); // Override music. - FlxG.sound.music = audioInstTrack; - targetState.vocals = audioVocalTrackGroup; + if (audioInstTrack != null) FlxG.sound.music = audioInstTrack; + if (audioVocalTrackGroup != null) targetState.vocals = audioVocalTrackGroup; openSubState(targetState); } @@ -3697,7 +3790,7 @@ class ChartEditorState extends HaxeUIState { #if sys // Validate file extension. - if (!SUPPORTED_MUSIC_FORMATS.contains(path.ext)) + if (path.ext != null && !SUPPORTED_MUSIC_FORMATS.contains(path.ext)) { return false; } @@ -3759,17 +3852,20 @@ class ChartEditorState extends HaxeUIState public function postLoadInstrumental():Void { - // Prevent the time from skipping back to 0 when the song ends. - audioInstTrack.onComplete = function() { - if (audioInstTrack != null) audioInstTrack.pause(); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); - }; + if (audioInstTrack != null) + { + // Prevent the time from skipping back to 0 when the song ends. + audioInstTrack.onComplete = function() { + if (audioInstTrack != null) audioInstTrack.pause(); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); + }; - songLengthInMs = audioInstTrack.length; + songLengthInMs = audioInstTrack.length; - gridTiledSprite.height = songLengthInPixels; + if (gridTiledSprite != null) gridTiledSprite.height = songLengthInPixels; - buildSpectrogram(audioInstTrack); + buildSpectrogram(audioInstTrack); + } scrollPositionInPixels = 0; playheadPositionInPixels = 0; @@ -3798,7 +3894,7 @@ class ChartEditorState extends HaxeUIState */ public function clearVocals():Void { - audioVocalTrackGroup.clear(); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.clear(); } /** @@ -3816,13 +3912,13 @@ class ChartEditorState extends HaxeUIState switch (charType) { case CharacterType.BF: - audioVocalTrackGroup.addPlayerVoice(vocalTrack); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.addPlayerVoice(vocalTrack); audioVocalTrackData.set(currentSongCharacterPlayer, Assets.getBytes(path)); case CharacterType.DAD: - audioVocalTrackGroup.addOpponentVoice(vocalTrack); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.addOpponentVoice(vocalTrack); audioVocalTrackData.set(currentSongCharacterOpponent, Assets.getBytes(path)); default: - audioVocalTrackGroup.add(vocalTrack); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.add(vocalTrack); audioVocalTrackData.set('default', Assets.getBytes(path)); } @@ -3834,12 +3930,12 @@ class ChartEditorState extends HaxeUIState /** * Loads a vocal track from audio byte data. */ - public function loadVocalsFromBytes(bytes:haxe.io.Bytes, charKey:String = null):Bool + public function loadVocalsFromBytes(bytes:haxe.io.Bytes, charKey:String = ''):Bool { var openflSound:openfl.media.Sound = new openfl.media.Sound(); openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length); var vocalTrack:FlxSound = FlxG.sound.load(openflSound, 1.0, false); - audioVocalTrackGroup.add(vocalTrack); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.add(vocalTrack); audioVocalTrackData.set(charKey, bytes); return true; } @@ -3849,12 +3945,9 @@ class ChartEditorState extends HaxeUIState */ public function loadSongAsTemplate(songId:String):Void { - var song:Song = SongDataParser.fetchSong(songId); + var song:Null<Song> = SongDataParser.fetchSong(songId); - if (song == null) - { - return; - } + if (song == null) return; // Load the song metadata. var rawSongMetadata:Array<SongMetadata> = song.getRawMetadata(); @@ -3863,8 +3956,13 @@ class ChartEditorState extends HaxeUIState for (metadata in rawSongMetadata) { + if (metadata == null) continue; var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation; - songMetadata.set(variation, Reflect.copy(metadata)); + + // Clone to prevent modifying the original. + var metadataClone = Reflect.copy(metadata); + if (metadataClone != null) songMetadata.set(variation, metadataClone); + songChartData.set(variation, SongDataParser.parseSongChartData(songId, metadata.variation)); } @@ -3888,7 +3986,8 @@ class ChartEditorState extends HaxeUIState loadInstrumentalFromAsset(Paths.inst(songId)); - var voiceList:Array<String> = song.getDifficulty(selectedDifficulty).buildVoiceList(currentSongCharacterPlayer); + var diff:Null<SongDifficulty> = song.getDifficulty(selectedDifficulty); + var voiceList:Array<String> = diff != null ? diff.buildVoiceList(currentSongCharacterPlayer) : []; if (voiceList.length == 2) { loadVocalsFromAsset(voiceList[0], BF); @@ -3959,8 +4058,6 @@ class ChartEditorState extends HaxeUIState noteDisplayDirty = true; } - var currentScrollEase:VarTween; - function easeSongToScrollPosition(targetScrollPosition:Float):Void { if (currentScrollEase != null) cancelScrollEase(currentScrollEase); @@ -3998,7 +4095,7 @@ class ChartEditorState extends HaxeUIState * @param command The command to perform. * @param purgeRedoStack If true, the redo stack will be cleared. */ - function performCommand(command:ChartEditorCommand, ?purgeRedoStack:Bool = true):Void + function performCommand(command:ChartEditorCommand, purgeRedoStack:Bool = true):Void { command.execute(this); undoHistory.push(command); @@ -4022,13 +4119,12 @@ class ChartEditorState extends HaxeUIState */ function undoLastCommand():Void { - if (undoHistory.length == 0) + var command:Null<ChartEditorCommand> = undoHistory.pop(); + if (command == null) { trace('No actions to undo.'); return; } - - var command:ChartEditorCommand = undoHistory.pop(); undoCommand(command); } @@ -4037,13 +4133,12 @@ class ChartEditorState extends HaxeUIState */ function redoLastCommand():Void { - if (redoHistory.length == 0) + var command:Null<ChartEditorCommand> = redoHistory.pop(); + if (command == null) { trace('No actions to redo.'); return; } - - var command:ChartEditorCommand = redoHistory.pop(); performCommand(command, false); } @@ -4058,19 +4153,19 @@ class ChartEditorState extends HaxeUIState }); } - function playMetronomeTick(?high:Bool = false):Void + function playMetronomeTick(high:Bool = false):Void { playSound(Paths.sound('pianoStuff/piano-${high ? '001' : '008'}')); } - function isNoteSelected(note:SongNoteData):Bool + function isNoteSelected(note:Null<SongNoteData>):Bool { - return currentNoteSelection.indexOf(note) != -1; + return note != null && currentNoteSelection.indexOf(note) != -1; } - function isEventSelected(event:SongEventData):Bool + function isEventSelected(event:Null<SongEventData>):Bool { - return currentEventSelection.indexOf(event) != -1; + return event != null && currentEventSelection.indexOf(event) != -1; } /** @@ -4079,8 +4174,16 @@ class ChartEditorState extends HaxeUIState */ function playSound(path:String):Void { - var snd:FlxSound = FlxG.sound.list.recycle(FlxSound); - snd.loadEmbedded(FlxG.sound.cache(path)); + var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound(); + + var asset:Null<FlxSoundAsset> = FlxG.sound.cache(path); + if (asset == null) + { + trace('WARN: Failed to play sound $path, asset not found.'); + return; + } + + snd.loadEmbedded(asset); snd.autoDestroy = true; FlxG.sound.list.add(snd); snd.play(); @@ -4108,7 +4211,7 @@ class ChartEditorState extends HaxeUIState * @param force Whether to force the export without prompting the user for a file location. * @param tmp If true, save to the temporary directory instead of the local `backup` directory. */ - public function exportAllSongData(?force:Bool = false, ?tmp:Bool = false):Void + public function exportAllSongData(force:Bool = false, tmp:Bool = false):Void { var zipEntries:Array<haxe.zip.Entry> = []; @@ -4122,24 +4225,27 @@ class ChartEditorState extends HaxeUIState if (variationId == '') { - var variationMetadata:SongMetadata = songMetadata.get(variation); - zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata.json', SerializerUtil.toJSON(variationMetadata))); - var variationChart:SongChartData = songChartData.get(variation); - zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart.json', SerializerUtil.toJSON(variationChart))); + var variationMetadata:Null<SongMetadata> = songMetadata.get(variation); + if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata.json', SerializerUtil.toJSON(variationMetadata))); + var variationChart:Null<SongChartData> = songChartData.get(variation); + if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart.json', SerializerUtil.toJSON(variationChart))); } else { - var variationMetadata:SongMetadata = songMetadata.get(variation); - zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata-$variationId.json', SerializerUtil.toJSON(variationMetadata))); - var variationChart:SongChartData = songChartData.get(variation); - zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart-$variationId.json', SerializerUtil.toJSON(variationChart))); + var variationMetadata:Null<SongMetadata> = songMetadata.get(variation); + if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-metadata-$variationId.json', + SerializerUtil.toJSON(variationMetadata))); + var variationChart:Null<SongChartData> = songChartData.get(variation); + if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('$currentSongId-chart-$variationId.json', SerializerUtil.toJSON(variationChart))); } } - zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', audioInstTrackData)); + if (audioInstTrackData != null) zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', audioInstTrackData)); for (charId in audioVocalTrackData.keys()) { - zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-$charId.ogg', audioVocalTrackData.get(charId))); + var entryData = audioVocalTrackData.get(charId); + if (entryData == null) continue; + zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-$charId.ogg', entryData)); } trace('Exporting ${zipEntries.length} files to ZIP...'); diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx index a6e230f6e..152615568 100644 --- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx @@ -168,9 +168,9 @@ class ChartEditorToolboxHandler case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT: toolbox = buildToolboxCharactersLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT: - toolbox = null; // buildToolboxPlayerPreviewLayout(state); + toolbox = buildToolboxPlayerPreviewLayout(state); case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT: - toolbox = null; // buildToolboxOpponentPreviewLayout(state); + toolbox = buildToolboxOpponentPreviewLayout(state); default: // This happens if you try to load an unknown layout. trace('ChartEditorToolboxHandler.initToolbox() - Unknown toolbox ID: $id'); @@ -200,6 +200,8 @@ class ChartEditorToolboxHandler // Initialize the toolbox without showing it. if (toolbox == null) toolbox = initToolbox(state, id); + if (toolbox == null) throw 'ChartEditorToolboxHandler.getToolbox() - Could not retrieve or build toolbox: $id'; + return toolbox; } diff --git a/source/funkin/ui/haxeui/components/CharacterPlayer.hx b/source/funkin/ui/haxeui/components/CharacterPlayer.hx index 0e6981535..c638e8a72 100644 --- a/source/funkin/ui/haxeui/components/CharacterPlayer.hx +++ b/source/funkin/ui/haxeui/components/CharacterPlayer.hx @@ -32,7 +32,7 @@ class CharacterPlayer extends Box { var character:BaseCharacter; - public function new(?defaultToBf:Bool = true) + public function new(defaultToBf:Bool = true) { super(); _overrideSkipTransformChildren = false; diff --git a/source/funkin/ui/stageBuildShit/StageEditorCommand.hx b/source/funkin/ui/stageBuildShit/StageEditorCommand.hx index 3248d16d8..d61281e07 100644 --- a/source/funkin/ui/stageBuildShit/StageEditorCommand.hx +++ b/source/funkin/ui/stageBuildShit/StageEditorCommand.hx @@ -21,7 +21,7 @@ class MovePropCommand implements StageEditorCommand var yDiff:Float; var realMove:Bool; // if needs a move! - public function new(xDiff:Float = 0, yDiff:Float = 0, ?realMove:Bool = true) + public function new(xDiff:Float = 0, yDiff:Float = 0, realMove:Bool = true) { this.xDiff = xDiff; this.yDiff = yDiff; diff --git a/source/funkin/ui/stageBuildShit/StageOffsetSubState.hx b/source/funkin/ui/stageBuildShit/StageOffsetSubState.hx index dff0cca6b..a6aa6fa68 100644 --- a/source/funkin/ui/stageBuildShit/StageOffsetSubState.hx +++ b/source/funkin/ui/stageBuildShit/StageOffsetSubState.hx @@ -3,17 +3,18 @@ package funkin.ui.stageBuildShit; import flixel.FlxSprite; import flixel.input.mouse.FlxMouseEvent; import flixel.math.FlxPoint; -import funkin.play.PlayState; import funkin.play.character.BaseCharacter; +import funkin.play.PlayState; import funkin.play.stage.StageData; import funkin.play.stage.StageProp; import funkin.shaderslmfao.StrokeShader; import funkin.ui.haxeui.HaxeUISubState; import funkin.ui.stageBuildShit.StageEditorCommand; -import haxe.ui.RuntimeComponentBuilder; +import funkin.util.SerializerUtil; import haxe.ui.containers.ListView; import haxe.ui.core.Component; import haxe.ui.events.UIEvent; +import haxe.ui.RuntimeComponentBuilder; import openfl.events.Event; import openfl.events.IOErrorEvent; import openfl.net.FileReference; @@ -376,6 +377,6 @@ class StageOffsetSubState extends HaxeUISubState stageLol.characters.gf.position[0] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.x); stageLol.characters.gf.position[1] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.y); - return CoolUtil.jsonStringify(stageLol); + return SerializerUtil.toJSON(stageLol); } } diff --git a/source/funkin/util/ClipboardUtil.hx b/source/funkin/util/ClipboardUtil.hx index 292cb111f..2796bcad6 100644 --- a/source/funkin/util/ClipboardUtil.hx +++ b/source/funkin/util/ClipboardUtil.hx @@ -14,7 +14,7 @@ class ClipboardUtil * @param once If true, the callback will only execute once and then be deleted. * @param priority Set the priority at which the callback will be executed. Higher values execute first. */ - public static function addListener(callback:Void->Void, ?once:Bool = false, ?priority:Int = 0):Void + public static function addListener(callback:Void->Void, once:Bool = false, ?priority:Int = 0):Void { lime.system.Clipboard.onUpdate.add(callback, once, priority); } diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx index 3494e620b..21c2920d9 100644 --- a/source/funkin/util/FileUtil.hx +++ b/source/funkin/util/FileUtil.hx @@ -209,7 +209,7 @@ class FileUtil * @return Whether the file dialog was opened successfully. */ public static function saveMultipleFiles(resources:Array<Entry>, ?onSaveAll:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String, - ?force:Bool = false):Bool + force:Bool = false):Bool { #if desktop // Prompt the user for a directory, then write all of the files to there. @@ -257,7 +257,7 @@ class FileUtil * Takes an array of file entries and prompts the user to save them as a ZIP file. */ public static function saveFilesAsZIP(resources:Array<Entry>, ?onSave:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String, - ?force:Bool = false):Bool + force:Bool = false):Bool { // Create a ZIP file. var zipBytes:Bytes = createZIPFromEntries(resources); @@ -278,7 +278,7 @@ class FileUtil * Use `saveFilesAsZIP` instead. * @param force Whether to force overwrite an existing file. */ - public static function saveFilesAsZIPToPath(resources:Array<Entry>, path:String, ?force:Bool = false):Bool + public static function saveFilesAsZIPToPath(resources:Array<Entry>, path:String, force:Bool = false):Bool { #if desktop // Create a ZIP file. diff --git a/source/funkin/util/SerializerUtil.hx b/source/funkin/util/SerializerUtil.hx index 0075e83c0..26563efce 100644 --- a/source/funkin/util/SerializerUtil.hx +++ b/source/funkin/util/SerializerUtil.hx @@ -21,7 +21,7 @@ class SerializerUtil /** * Convert a Haxe object to a JSON string. */ - public static function toJSON(input:Dynamic, ?pretty:Bool = true):String + public static function toJSON(input:Dynamic, pretty:Bool = true):String { return Json.stringify(input, replacer, pretty ? INDENT_CHAR : null); } diff --git a/source/funkin/util/logging/CrashHandler.hx b/source/funkin/util/logging/CrashHandler.hx new file mode 100644 index 000000000..a295f9bf1 --- /dev/null +++ b/source/funkin/util/logging/CrashHandler.hx @@ -0,0 +1,143 @@ +package funkin.util.logging; + +import openfl.Lib; +import openfl.events.UncaughtErrorEvent; + +/** + * A custom crash handler that writes to a log file and displays a message box. + */ +@:nullSafety +class CrashHandler +{ + static final LOG_FOLDER = 'logs'; + + /** + * Initializes + */ + public static function initialize():Void + { + trace('[LOG] Enabling standard uncaught error handler...'); + Lib.current.loaderInfo.uncaughtErrorEvents.addEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, onUncaughtError); + + #if cpp + trace('[LOG] Enabling C++ critical error handler...'); + untyped __global__.__hxcpp_set_critical_error_handler(onCriticalError); + #end + } + + /** + * Called when an uncaught error occurs. + * This handles most thrown errors, and is sufficient to handle everything alone on HTML5. + * @param error Information on the error that was thrown. + */ + static function onUncaughtError(error:UncaughtErrorEvent):Void + { + try + { + #if sys + logError(error); + #end + + displayError(error); + } + catch (e:Dynamic) + { + trace('Error while handling crash: ' + e); + } + } + + static function onCriticalError(message:String):Void + { + try + { + #if sys + logErrorMessage(message, true); + #end + + displayErrorMessage(message); + } + catch (e:Dynamic) + { + trace('Error while handling crash: $e'); + + trace('Message: $message'); + } + } + + static function displayError(error:UncaughtErrorEvent):Void + { + displayErrorMessage(generateErrorMessage(error)); + } + + static function displayErrorMessage(message:String):Void + { + lime.app.Application.current.window.alert(message, "Fatal Uncaught Exception"); + } + + #if sys + static function logError(error:UncaughtErrorEvent):Void + { + logErrorMessage(generateErrorMessage(error)); + } + + static function logErrorMessage(message:String, critical:Bool = false):Void + { + FileUtil.createDirIfNotExists(LOG_FOLDER); + + sys.io.File.saveContent('$LOG_FOLDER/crash${critical ? '-critical' : ''}-${DateUtil.generateTimestamp()}.log', message); + } + #end + + static function generateErrorMessage(error:UncaughtErrorEvent):String + { + var errorMessage:String = ""; + var callStack:Array<haxe.CallStack.StackItem> = haxe.CallStack.exceptionStack(true); + + errorMessage += '${error.error}\n'; + + for (stackItem in callStack) + { + switch (stackItem) + { + case FilePos(innerStackItem, file, line, column): + errorMessage += ' in ${file}#${line}'; + if (column != null) errorMessage += ':${column}'; + case CFunction: + errorMessage += '[Function] '; + case Module(m): + errorMessage += '[Module(${m})] '; + case Method(classname, method): + errorMessage += '[Function(${classname}.${method})] '; + case LocalFunction(v): + errorMessage += '[LocalFunction(${v})] '; + } + errorMessage += '\n'; + } + + return errorMessage; + } + + public static function queryStatus():Void + { + @:privateAccess + var currentStatus = Lib.current.stage.__uncaughtErrorEvents.__enabled; + trace('ERROR HANDLER STATUS: ' + currentStatus); + + #if openfl_enable_handle_error + trace('Define: openfl_enable_handle_error is enabled'); + #else + trace('Define: openfl_enable_handle_error is disabled'); + #end + + #if openfl_disable_handle_error + trace('Define: openfl_disable_handle_error is enabled'); + #else + trace('Define: openfl_disable_handle_error is disabled'); + #end + } + + public static function induceBasicCrash():Void + { + throw "This is an example of an uncaught exception."; + } +} diff --git a/source/funkin/util/macro/ClassMacro.hx b/source/funkin/util/macro/ClassMacro.hx index 43b437dda..59d96e2a8 100644 --- a/source/funkin/util/macro/ClassMacro.hx +++ b/source/funkin/util/macro/ClassMacro.hx @@ -22,7 +22,7 @@ class ClassMacro * @param includeSubPackages Whether to include classes located in sub-packages of the target package. * @return A list of classes matching the specified criteria. */ - public static macro function listClassesInPackage(targetPackage:String, ?includeSubPackages:Bool = true):ExprOf<Iterable<Class<Dynamic>>> + public static macro function listClassesInPackage(targetPackage:String, includeSubPackages:Bool = true):ExprOf<Iterable<Class<Dynamic>>> { if (!onGenerateCallbackRegistered) {