diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index ad30e99f1..f5fc04569 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -162,7 +162,7 @@ class GameOverSubState extends MusicBeatSubState if ((parentPlayState?.isMinimalMode ?? true)) {} else { - boyfriend = parentPlayState?.currentStage.getBoyfriend(true); + boyfriend = parentPlayState?.currentStage?.getBoyfriend(true); if (boyfriend != null) { boyfriend.canPlayOtherAnims = true; @@ -193,10 +193,9 @@ class GameOverSubState extends MusicBeatSubState }); } - @:nullSafety(Off) function setCameraTarget():Void { - if ((parentPlayState?.isMinimalMode ?? true) || boyfriend == null) return; + if (parentPlayState == null || parentPlayState.isMinimalMode || boyfriend == null) return; // Assign a camera follow point to the boyfriend's position. cameraFollowPoint = new FlxObject(parentPlayState.cameraFollowPoint.x, parentPlayState.cameraFollowPoint.y, 1, 1); @@ -207,6 +206,7 @@ class GameOverSubState extends MusicBeatSubState cameraFollowPoint.y += offsets[1]; add(cameraFollowPoint); + @:nullSafety(Off) FlxG.camera.target = null; FlxG.camera.follow(cameraFollowPoint, LOCKON, Constants.DEFAULT_CAMERA_FOLLOW_RATE / 2); targetCameraZoom = (parentPlayState?.currentStage?.camZoom ?? 1.0) * boyfriend.getDeathCameraZoom(); @@ -394,7 +394,7 @@ class GameOverSubState extends MusicBeatSubState // Readd Boyfriend to the stage. boyfriend.isDead = false; remove(boyfriend); - parentPlayState?.currentStage.addCharacter(boyfriend, BF); + parentPlayState?.currentStage?.addCharacter(boyfriend, BF); } // Snap reset the camera which may have changed because of the player character data. @@ -564,11 +564,11 @@ class GameOverSubState extends MusicBeatSubState PlayStatePlaylist.reset(); } - var stickerPackId:Null = parentPlayState?.currentChart.stickerPack; + var stickerPackId:Null = parentPlayState?.currentChart?.stickerPack; if (stickerPackId == null) { - var playerCharacterId:Null = PlayerRegistry.instance.getCharacterOwnerId(parentPlayState?.currentChart.characters.player); + var playerCharacterId:Null = PlayerRegistry.instance.getCharacterOwnerId(parentPlayState?.currentChart?.characters.player); var playerCharacter:Null = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? Constants.DEFAULT_CHARACTER); if (playerCharacter != null) diff --git a/source/funkin/play/GitarooPause.hx b/source/funkin/play/GitarooPause.hx index 8a2d7f3f9..9df525629 100644 --- a/source/funkin/play/GitarooPause.hx +++ b/source/funkin/play/GitarooPause.hx @@ -18,9 +18,9 @@ class GitarooPause extends MusicBeatState var replaySelect:Bool = false; - var previousParams:PlayStateParams; + var previousParams:Null; - public function new(previousParams:PlayStateParams):Void + public function new(?previousParams:PlayStateParams):Void { super(); diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index a1f222703..d7679d354 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -146,6 +146,7 @@ typedef PlayStateParams = * The gameplay state, where all the rhythm gaming happens. * SubState so it can be loaded as a child of the chart editor. */ +@:nullSafety class PlayState extends MusicBeatSubState { /** @@ -157,13 +158,13 @@ class PlayState extends MusicBeatSubState * The currently active PlayState. * There should be only one PlayState in existance at a time, we can use a singleton. */ - public static var instance:PlayState = null; + public static var instance:Null; /** * This sucks. We need this because FlxG.resetState(); assumes the constructor has no arguments. * @see https://github.com/HaxeFlixel/flixel/issues/2541 */ - static var lastParams:PlayStateParams = null; + static var lastParams:Null; /** * PUBLIC INSTANCE VARIABLES @@ -173,7 +174,7 @@ class PlayState extends MusicBeatSubState /** * The currently selected stage. */ - public var currentSong:Song = null; + public var currentSong:Song; /** * The currently selected difficulty. @@ -194,7 +195,7 @@ class PlayState extends MusicBeatSubState /** * The currently active Stage. This is the object containing all the props. */ - public var currentStage:Stage = null; + public var currentStage:Null = null; /** * Gets set to true when the PlayState needs to reset (player opted to restart or died). @@ -250,12 +251,12 @@ class PlayState extends MusicBeatSubState * An FlxTween that tweens the camera to the follow point. * Only used when tweening the camera manually, rather than tweening via follow. */ - public var cameraFollowTween:FlxTween; + public var cameraFollowTween:Null; /** * An FlxTween that zooms the camera to the desired amount. */ - public var cameraZoomTween:FlxTween; + public var cameraZoomTween:Null; /** * An FlxTween that changes the additive speed to the desired amount. @@ -266,7 +267,7 @@ class PlayState extends MusicBeatSubState * The camera follow point from the last stage. * Used to persist the position of the `cameraFollowPosition` between levels. */ - public var previousCameraFollowPoint:FlxPoint = null; + public var previousCameraFollowPoint:Null; /** * The current camera zoom level without any modifiers applied. @@ -380,7 +381,7 @@ class PlayState extends MusicBeatSubState /** * The current dialogue. */ - public var currentConversation:Conversation; + public var currentConversation:Null; /** * Key press inputs which have been received but not yet processed. @@ -399,6 +400,11 @@ class PlayState extends MusicBeatSubState */ var justUnpaused:Bool = false; + /** + * The current note style used by the song. + */ + var noteStyle:NoteStyle; + /** * PRIVATE INSTANCE VARIABLES * Private instance variables should be used for information that must be reset or dereferenced @@ -408,7 +414,7 @@ class PlayState extends MusicBeatSubState * The Array containing the upcoming song events. * The `update()` function regularly shifts these out to trigger events. */ - var songEvents:Array; + var songEvents:Array = []; /** * If true, the player is allowed to pause the game. @@ -467,7 +473,7 @@ class PlayState extends MusicBeatSubState /** * A group of audio tracks, used to play the song's vocals. */ - public var vocals:VoicesGroup; + public var vocals:Null; #if FEATURE_DISCORD_RPC // Discord RPC variables @@ -498,12 +504,12 @@ class PlayState extends MusicBeatSubState /** * The health icon representing the player. */ - public var iconP1:HealthIcon; + public var iconP1:Null; /** * The health icon representing the opponent. */ - public var iconP2:HealthIcon; + public var iconP2:Null; /** * The sprite group containing active player's strumline notes. @@ -594,9 +600,9 @@ class PlayState extends MusicBeatSubState * Data for the current difficulty for the current song. * Includes chart data, scroll speed, and other information. */ - public var currentChart(get, never):SongDifficulty; + public var currentChart(get, never):Null; - function get_currentChart():SongDifficulty + function get_currentChart():Null { if (currentSong == null || currentDifficulty == null) return null; return currentSong.getDifficulty(currentDifficulty, currentVariation); @@ -610,8 +616,8 @@ class PlayState extends MusicBeatSubState function get_currentStageId():String { - if (currentChart == null || currentChart.stage == null || currentChart.stage == '') return Constants.DEFAULT_STAGE; - return currentChart.stage; + var stage:String = currentChart?.stage ?? ''; + return stage == '' ? Constants.DEFAULT_STAGE : stage; } /** @@ -621,7 +627,7 @@ class PlayState extends MusicBeatSubState function get_currentSongLengthMs():Float { - return FlxG?.sound?.music?.length; + return FlxG.sound.music?.length ?? 0; } /** @@ -642,27 +648,20 @@ class PlayState extends MusicBeatSubState * @param params The parameters used to initialize the PlayState. * Includes information about what song to play and more. */ - public function new(params:PlayStateParams) + public function new(?params:PlayStateParams) { super(); // Validate parameters. - if (params == null && lastParams == null) - { - throw 'PlayState constructor called with no available parameters.'; - } - else if (params == null) - { - trace('WARNING: PlayState constructor called with no parameters. Reusing previous parameters.'); - params = lastParams; - } - else - { - lastParams = params; - } + var params:PlayStateParams = params ?? + { + trace('WARNING: PlayState constructor called with no parameters. Reusing previous parameters.'); + lastParams ?? throw 'PlayState constructor called with no available parameters.'; + } + lastParams = params; // Apply parameters. - currentSong = params.targetSong; + currentSong = params.targetSong ?? throw "targetSong should not be null"; if (params.targetDifficulty != null) currentDifficulty = params.targetDifficulty; previousDifficulty = currentDifficulty; if (params.targetVariation != null) currentVariation = params.targetVariation; @@ -675,26 +674,7 @@ class PlayState extends MusicBeatSubState overrideMusic = params.overrideMusic ?? false; previousCameraFollowPoint = params.cameraFollowPoint; - // Don't do anything else here! Wait until create() when we attach to the camera. - } - - /** - * Called when the PlayState is switched to. - */ - public override function create():Void - { - if (instance != null) - { - // TODO: Do something in this case? IDK. - trace('WARNING: PlayState instance already exists. This should not happen.'); - } - instance = this; - #if !mobile - // TODO: Figure out how to do the flair for charting mode!! I can't figure it out for the love of god. -Zack - if (!isChartingMode) FlxG.autoPause = false; - #end - - if (!assertChartExists()) return; + // Basic object initialization // TODO: Add something to toggle this on! if (false) @@ -712,6 +692,58 @@ class PlayState extends MusicBeatSubState cameraFollowPoint = new FlxObject(0, 0); } + // Cameras + camGame = new FunkinCamera('playStateCamGame'); + camHUD = new FlxCamera(); + camCutscene = new FlxCamera(); + camCutouts = new FlxCamera(); + + var currentChart = currentSong.getDifficulty(currentDifficulty, currentVariation); + var noteStyleId:Null = currentChart?.noteStyle; + var nulNoteStyle:Null = NoteStyleRegistry.instance.fetchEntry(noteStyleId ?? Constants.DEFAULT_NOTE_STYLE); + if (nulNoteStyle == null) throw "Failed to retrieve both note style and default note style. This shouldn't happen!"; + noteStyle = nulNoteStyle; + + // Strumlines + playerStrumline = new Strumline(noteStyle, !isBotPlayMode, currentChart?.scrollSpeed); + opponentStrumline = new Strumline(noteStyle, false, currentChart?.scrollSpeed); + + // Healthbar + healthBarBG = FunkinSprite.create(0, 0, 'healthBar'); + healthBar = new FlxBar(0, 0, RIGHT_TO_LEFT, Std.int(healthBarBG.width - 8), Std.int(healthBarBG.height - 8), null, 0, 2); + scoreText = new FlxText(0, 0, 0, '', 20); + + // Combo & Pop Up + comboPopUps = new PopUpStuff(noteStyle); + + // Pause sprites + #if mobile + pauseButton = FunkinSprite.createSparrow(0, 0, "pauseButton"); + pauseCircle = FunkinSprite.create(0, 0, 'pauseCircle'); + #end + + // Don't do anything else here! Wait until create() when we attach to the camera. + } + + /** + * Called when the PlayState is switched to. + */ + @:nullSafety(Off) + public override function create():Void + { + if (instance != null) + { + // TODO: Do something in this case? IDK. + trace('WARNING: PlayState instance already exists. This should not happen.'); + } + instance = this; + #if !mobile + // TODO: Figure out how to do the flair for charting mode!! I can't figure it out for the love of god. -Zack + if (!isChartingMode) FlxG.autoPause = false; + #end + + if (!assertChartExists()) return; + #if mobile // Force allowScreenTimeout to be disabled lime.system.System.allowScreenTimeout = false; @@ -723,11 +755,11 @@ class PlayState extends MusicBeatSubState this.persistentDraw = true; // Stop any pre-existing music. - if (!overrideMusic && FlxG.sound.music != null) FlxG.sound.music.stop(); - - // Prepare the current song's instrumental and vocals to be played. - if (!overrideMusic && currentChart != null) + if (!overrideMusic) { + if (FlxG.sound.music != null) FlxG.sound.music.stop(); + + // Prepare the current song's instrumental and vocals to be played. currentChart.cacheInst(currentInstrumental); currentChart.cacheVocals(); } @@ -767,13 +799,16 @@ class PlayState extends MusicBeatSubState { // Initialize the hitbox for mobile controls addHitbox(false); - hitbox.isPixel = currentChart.noteStyle == "pixel"; - - if (Preferences.controlsScheme == FunkinHitboxControlSchemes.Arrows) + if (hitbox != null) { - for (direction in Strumline.DIRECTIONS) + hitbox.isPixel = currentChart.noteStyle == "pixel"; + + if (Preferences.controlsScheme == FunkinHitboxControlSchemes.Arrows) { - hitbox.getFirstHintByDirection(direction).follow(playerStrumline.getByDirection(direction)); + for (direction in Strumline.DIRECTIONS) + { + hitbox.getFirstHintByDirection(direction).follow(playerStrumline.getByDirection(direction)); + } } } } @@ -806,7 +841,7 @@ class PlayState extends MusicBeatSubState startingSong = true; // TODO: We hardcoded the transition into Winter Horrorland. Do this with a ScriptedSong instead. - if ((currentSong?.id ?? '').toLowerCase() == 'winter-horrorland') + if ((currentSong.id ?? '').toLowerCase() == 'winter-horrorland') { // VanillaCutscenes will call startCountdown later. VanillaCutscenes.playHorrorStartCutscene(); @@ -821,31 +856,7 @@ class PlayState extends MusicBeatSubState // Create the pause button. #if mobile - pauseButton = FunkinSprite.createSparrow(0, 0, "pauseButton"); - pauseButton.animation.addByIndices('idle', 'back', [0], "", 24, false); - pauseButton.animation.addByIndices('hold', 'back', [5], "", 24, false); - pauseButton.animation.addByIndices('confirm', 'back', [ - 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 - ], "", 24, false); - pauseButton.scale.set(0.8, 0.8); - pauseButton.updateHitbox(); - pauseButton.animation.play("idle"); - pauseButton.setPosition((FlxG.width - pauseButton.width) - 35, 35); - pauseButton.cameras = [camControls]; - - pauseCircle = FunkinSprite.create(0, 0, 'pauseCircle'); - pauseCircle.scale.set(0.84, 0.8); - pauseCircle.updateHitbox(); - pauseCircle.cameras = [camControls]; - pauseCircle.x = ((pauseButton.x + (pauseButton.width / 2)) - (pauseCircle.width / 2)); - pauseCircle.y = ((pauseButton.y + (pauseButton.height / 2)) - (pauseCircle.height / 2)); - pauseCircle.alpha = 0.1; - - add(pauseCircle); - add(pauseButton); - hitbox?.forEachAlive(function(hint:FunkinHint) { - hint.deadZones.push(pauseButton); - }); + initPauseSprites(); #end // Do this last to prevent beatHit from being called before create() is done. @@ -880,7 +891,7 @@ class PlayState extends MusicBeatSubState function assertChartExists():Bool { // Returns null if the song failed to load or doesn't have the selected difficulty. - if (currentSong == null || currentChart == null || currentChart.notes == null) + if (currentSong == null || currentChart == null || currentChart?.notes == null) { // We have encountered a critical error. Prevent Flixel from trying to run any gameplay logic. criticalFailure = true; @@ -899,7 +910,7 @@ class PlayState extends MusicBeatSubState { message = 'There was a critical error retrieving data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.'; } - else if (currentChart.notes == null) + else if (currentChart?.notes == null) { message = 'There was a critical error retrieving note data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.'; } @@ -914,7 +925,7 @@ class PlayState extends MusicBeatSubState } else { - this.remove(currentStage); + if (currentStage != null) this.remove(currentStage); FlxG.switchState(() -> new MainMenuState()); } return false; @@ -963,26 +974,31 @@ class PlayState extends MusicBeatSubState FlxG.sound.music.pitch = playbackRate; } - if (!overrideMusic) + if (!overrideMusic && vocals != null) { // Stop the vocals if they already exist. - if (vocals != null) vocals.stop(); - vocals = currentChart.buildVocals(currentInstrumental); + vocals.stop(); + vocals = currentChart?.buildVocals(currentInstrumental); - if (vocals.members.length == 0) + if (vocals?.members?.length == 0) { trace('WARNING: No vocals found for this song.'); } } - vocals.pause(); - vocals.time = startTimestamp - Conductor.instance.instrumentalOffset; if (FlxG.sound.music != null) FlxG.sound.music.volume = 1; - vocals.volume = 1; - vocals.playerVolume = 1; - vocals.opponentVolume = 1; - if (currentStage != null) currentStage.resetStage(); + if (vocals != null) + { + vocals.pause(); + vocals.time = startTimestamp - Conductor.instance.instrumentalOffset; + + vocals.volume = 1; + vocals.playerVolume = 1; + vocals.opponentVolume = 1; + } + + currentStage?.resetStage(); if (!fromDeathState) { @@ -1141,7 +1157,7 @@ class PlayState extends MusicBeatSubState if (health <= Constants.HEALTH_MIN && !isPracticeMode && !isPlayerDying) { - vocals.pause(); + vocals?.pause(); if (FlxG.sound.music != null) FlxG.sound.music.pause(); @@ -1226,7 +1242,7 @@ class PlayState extends MusicBeatSubState #end } - function pause(?mode:PauseMode = Standard):Void + function pause(mode:PauseMode = Standard):Void { if (!mayPauseGame || justUnpaused || isGamePaused || isPlayerDying) return; @@ -1234,7 +1250,7 @@ class PlayState extends MusicBeatSubState { case Conversation: preparePauseUI(); - openPauseSubState(Conversation, FullScreenScaleMode.hasFakeCutouts ? camCutouts : camCutscene, () -> currentConversation.pauseMusic()); + openPauseSubState(Conversation, FullScreenScaleMode.hasFakeCutouts ? camCutouts : camCutscene, () -> currentConversation?.pauseMusic()); case Cutscene: preparePauseUI(); @@ -1256,7 +1272,7 @@ class PlayState extends MusicBeatSubState if (!isSubState && event.gitaroo) { - this.remove(currentStage); + if (currentStage != null) this.remove(currentStage); FlxG.switchState(() -> new GitarooPause(lastParams)); } else @@ -1324,8 +1340,8 @@ class PlayState extends MusicBeatSubState if (!isMinimalMode) { - if (iconP1 != null) iconP1.updatePosition(); - if (iconP2 != null) iconP2.updatePosition(); + iconP1?.updatePosition(); + iconP2?.updatePosition(); } // Transition to the game over substate. @@ -1343,7 +1359,7 @@ class PlayState extends MusicBeatSubState { // Query and activate song events. // TODO: Check that these work appropriately even when songPosition is less than 0, to play events during countdown. - if (songEvents != null && songEvents.length > 0) + if (songEvents.length > 0) { var songEventsToActivate:Array = SongEventRegistry.queryEvents(songEvents, Conductor.instance.songPosition); @@ -1658,7 +1674,11 @@ class PlayState extends MusicBeatSubState instance = this; funkin.modding.PolymodHandler.forceReloadAssets(); - lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id); + if (lastParams == null) + { + throw "No lastParams to refer to"; + } + lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id) ?? throw "Could not load current song from ID. This shouldn't happen!"; LoadingState.loadPlayState(lastParams); } @@ -1671,8 +1691,8 @@ class PlayState extends MusicBeatSubState if (isGamePaused) return false; - if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.instance.currentStep)); - if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.instance.currentStep)); + iconP1?.onStepHit(Std.int(Conductor.instance.currentStep)); + iconP2?.onStepHit(Std.int(Conductor.instance.currentStep)); // Try to call hold note haptics each step hit. Works if atleast one note status is NoteStatus.isHoldNotePressed. playerStrumline.noteVibrations.tryHoldNoteVibration(); @@ -1700,17 +1720,17 @@ class PlayState extends MusicBeatSubState var correctSync:Float = Math.min(FlxG.sound.music.length, Math.max(0, Conductor.instance.songPosition - Conductor.instance.combinedOffset)); var playerVoicesError:Float = 0; var opponentVoicesError:Float = 0; - if (vocals != null && vocals.playing) { + @:nullSafety(Off) @:privateAccess // todo: maybe make the groups public :thinking: { - vocals.playerVoices.forEachAlive(function(voice:FunkinSound) { + vocals.playerVoices?.forEachAlive(function(voice:FunkinSound) { var currentRawVoiceTime:Float = voice.time + vocals.playerVoicesOffset; if (Math.abs(currentRawVoiceTime - correctSync) > Math.abs(playerVoicesError)) playerVoicesError = currentRawVoiceTime - correctSync; }); - vocals.opponentVoices.forEachAlive(function(voice:FunkinSound) { + vocals.opponentVoices?.forEachAlive(function(voice:FunkinSound) { var currentRawVoiceTime:Float = voice.time + vocals.opponentVoicesOffset; if (Math.abs(currentRawVoiceTime - correctSync) > Math.abs(opponentVoicesError)) opponentVoicesError = currentRawVoiceTime - correctSync; }); @@ -1781,13 +1801,11 @@ class PlayState extends MusicBeatSubState */ function initCameras():Void { - camGame = new FunkinCamera('playStateCamGame'); camGame.bgColor = BACKGROUND_COLOR; // Show a pink background behind the stage. - camHUD = new FlxCamera(); camHUD.bgColor.alpha = 0; // Show the game scene behind the camera. - camCutscene = new FlxCamera(); camCutscene.bgColor.alpha = 0; // Show the game scene behind the camera. - camCutouts = new FlxCamera((FlxG.width - FlxG.initialWidth) / 2, (FlxG.height - FlxG.initialHeight) / 2, FlxG.initialWidth, FlxG.initialHeight); + camCutouts.setPosition((FlxG.width - FlxG.initialWidth) / 2, (FlxG.height - FlxG.initialHeight) / 2); + camCutouts.setSize(FlxG.initialWidth, FlxG.initialHeight); camCutouts.bgColor.alpha = 0; // Show the game scene behind the camera. FlxG.cameras.reset(camGame); @@ -1815,21 +1833,24 @@ class PlayState extends MusicBeatSubState && !ControlsHandler.usingExternalInputDevice) healthBarYPos = FlxG.height * 0.1; #end - healthBarBG = FunkinSprite.create(0, healthBarYPos, 'healthBar'); + healthBarBG.y = healthBarYPos; healthBarBG.screenCenter(X); healthBarBG.scrollFactor.set(0, 0); healthBarBG.zIndex = 800; add(healthBarBG); - healthBar = new FlxBar(healthBarBG.x + 4, healthBarBG.y + 4, RIGHT_TO_LEFT, Std.int(healthBarBG.width - 8), Std.int(healthBarBG.height - 8), this, - 'healthLerp', 0, 2); + healthBar.x = healthBarBG.x + 4; + healthBar.y = healthBarBG.y + 4; + healthBar.parent = this; + healthBar.parentVariable = 'healthLerp'; healthBar.scrollFactor.set(); healthBar.createFilledBar(Constants.COLOR_HEALTH_BAR_RED, Constants.COLOR_HEALTH_BAR_GREEN); healthBar.zIndex = 801; add(healthBar); // The score text below the health bar. - scoreText = new FlxText(healthBarBG.x + healthBarBG.width - 190, healthBarBG.y + 30, 0, '', 20); + scoreText.x = healthBarBG.x + healthBarBG.width - 190; + scoreText.y = healthBarBG.y + 30; scoreText.setFormat(Paths.font('vcr.ttf'), 16, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); scoreText.scrollFactor.set(); scoreText.zIndex = 802; @@ -1897,7 +1918,7 @@ class PlayState extends MusicBeatSubState public function resetCameraZoom():Void { - if (PlayState.instance.isMinimalMode) return; + if (isMinimalMode) return; // Apply camera zoom level from stage data. currentCameraZoom = stageZoom; FlxG.camera.zoom = currentCameraZoom; @@ -1913,15 +1934,21 @@ class PlayState extends MusicBeatSubState { if (currentSong == null || currentChart == null) { - trace('Song difficulty could not be loaded.'); + throw 'Song difficulty could not be loaded.'; } - var currentCharacterData:SongCharacterData = currentChart.characters; // Switch the variation we are playing on by manipulating targetVariation. + var currentCharacterData:Null = currentChart?.characters; + if (currentCharacterData == null) + { + trace('Cannot retrieve character data'); + return; + } + // Switch the variation we are playing on by manipulating targetVariation. // // GIRLFRIEND // - var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.girlfriend); + var girlfriend:Null = CharacterDataParser.fetchCharacter(currentCharacterData.girlfriend); if (girlfriend != null) { @@ -1939,7 +1966,7 @@ class PlayState extends MusicBeatSubState // // DAD // - var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.opponent); + var dad:Null = CharacterDataParser.fetchCharacter(currentCharacterData.opponent); if (dad != null) { @@ -1954,7 +1981,7 @@ class PlayState extends MusicBeatSubState iconP2.cameras = [camHUD]; #if FEATURE_DISCORD_RPC - discordRPCAlbum = 'album-${currentChart.album}'; + discordRPCAlbum = 'album-${currentChart?.album}'; discordRPCIcon = 'icon-${currentCharacterData.opponent}'; #end } @@ -1962,7 +1989,7 @@ class PlayState extends MusicBeatSubState // // BOYFRIEND // - var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.player); + var boyfriend:Null = CharacterDataParser.fetchCharacter(currentCharacterData.player); if (boyfriend != null) { @@ -2023,13 +2050,7 @@ class PlayState extends MusicBeatSubState */ function initStrumlines():Void { - var noteStyleId:String = currentChart.noteStyle; - var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId); - if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault(); - - playerStrumline = new Strumline(noteStyle, !isBotPlayMode); playerStrumline.onNoteIncoming.add(onStrumlineNoteIncoming); - opponentStrumline = new Strumline(noteStyle, false); opponentStrumline.onNoteIncoming.add(onStrumlineNoteIncoming); add(playerStrumline); add(opponentStrumline); @@ -2074,6 +2095,7 @@ class PlayState extends MusicBeatSubState playerStrumline.strumlineScale.set(playerStrumlineScale, playerStrumlineScale); playerStrumline.setNoteSpacing(playerNoteSpacing); + @:nullSafety(Off) // Who thought it'd be a good idea to make the iterator nullable? for (strum in playerStrumline) { strum.width *= 2; @@ -2082,7 +2104,7 @@ class PlayState extends MusicBeatSubState playerStrumline.x = (FlxG.width - playerStrumline.width) / 2 + Constants.STRUMLINE_X_OFFSET; playerStrumline.y = (FlxG.height - playerStrumline.height) * 0.95 - Constants.STRUMLINE_Y_OFFSET; - if (currentChart.noteStyle != "pixel") + if (currentChart?.noteStyle != "pixel") { #if android playerStrumline.y += 10; #end } @@ -2093,6 +2115,35 @@ class PlayState extends MusicBeatSubState opponentStrumline.y = Constants.STRUMLINE_Y_OFFSET * 0.3; opponentStrumline.x -= 30; } + + function initPauseSprites() + { + pauseButton.animation.addByIndices('idle', 'back', [0], "", 24, false); + pauseButton.animation.addByIndices('hold', 'back', [5], "", 24, false); + pauseButton.animation.addByIndices('confirm', 'back', [ + 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 + ], "", 24, false); + pauseButton.scale.set(0.8, 0.8); + pauseButton.updateHitbox(); + pauseButton.animation.play("idle"); + pauseButton.setPosition((FlxG.width - pauseButton.width) - 35, 35); + @:nullSafety(Off) // AAAAAAA why did camControls have to be nullable AAAAAAAAAA + pauseButton.cameras = [camControls]; + + pauseCircle.scale.set(0.84, 0.8); + pauseCircle.updateHitbox(); + @:nullSafety(Off) + pauseCircle.cameras = [camControls]; + pauseCircle.x = ((pauseButton.x + (pauseButton.width / 2)) - (pauseCircle.width / 2)); + pauseCircle.y = ((pauseButton.y + (pauseButton.height / 2)) - (pauseCircle.height / 2)); + pauseCircle.alpha = 0.1; + + add(pauseCircle); + add(pauseButton); + hitbox?.forEachAlive(function(hint:FunkinHint) { + hint.deadZones.push(pauseButton); + }); + } #end /** @@ -2100,11 +2151,7 @@ class PlayState extends MusicBeatSubState */ function initPopups():Void { - var noteStyleId:String = currentChart.noteStyle; - var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId); - if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault(); // Initialize the judgements and combo meter. - comboPopUps = new PopUpStuff(noteStyle); comboPopUps.zIndex = 900; add(comboPopUps); comboPopUps.cameras = [camHUD]; @@ -2170,8 +2217,12 @@ class PlayState extends MusicBeatSubState function buildDiscordRPCState():String { - var discordRPCDifficulty = PlayState.instance.currentDifficulty.replace('-', ' ').toTitleCase(); - return '${currentChart.songName} [${discordRPCDifficulty}]'; + if (currentChart == null) + { + trace("WARNING: Difficulty data for RPC is null."); + } + var discordRPCDifficulty = PlayState.instance?.currentDifficulty?.replace('-', ' ')?.toTitleCase() ?? '???'; + return '${currentChart?.songName ?? '???'} [${discordRPCDifficulty}]'; } function initPreciseInputs():Void @@ -2188,7 +2239,7 @@ class PlayState extends MusicBeatSubState { if (currentChart == null) { - trace('Song difficulty could not be loaded.'); + throw 'Song difficulty could not be loaded.'; } // Conductor.instance.forceBPM(currentChart.getStartingBPM()); @@ -2196,10 +2247,10 @@ class PlayState extends MusicBeatSubState if (!overrideMusic) { // Stop the vocals if they already exist. - if (vocals != null) vocals.stop(); - vocals = currentChart.buildVocals(currentInstrumental); + vocals?.stop(); + vocals = currentChart?.buildVocals(currentInstrumental); - if (vocals.members.length == 0) + if (vocals?.members?.length == 0) { trace('WARNING: No vocals found for this song.'); } @@ -2218,9 +2269,16 @@ class PlayState extends MusicBeatSubState */ function regenNoteData(startTime:Float = 0):Void { + if (currentChart == null) + { + trace('Cannot regenerate note data for null chart'); + return; + } + Highscore.tallies.combo = 0; Highscore.tallies = new Tallies(); + @:nullSafety(Off) var event:SongLoadScriptEvent = new SongLoadScriptEvent(currentChart.song.id, currentChart.difficulty, currentChart.notes.copy(), currentChart.getEvents()); dispatchEvent(event); @@ -2243,7 +2301,7 @@ class PlayState extends MusicBeatSubState var scoreable = true; if (songNote.kind != null) { - var noteKind:NoteKind = NoteKindManager.getNoteKind(songNote.kind); + var noteKind:Null = NoteKindManager.getNoteKind(songNote.kind ?? ''); if (noteKind != null) scoreable = noteKind.scoreable; } @@ -2346,7 +2404,7 @@ class PlayState extends MusicBeatSubState if (!overrideMusic && !isGamePaused && currentChart != null) { - currentChart.playInst(1.0, currentInstrumental, false); + currentChart?.playInst(1.0, currentInstrumental, false); } if (FlxG.sound.music == null) @@ -2367,19 +2425,23 @@ class PlayState extends MusicBeatSubState FlxG.sound.music.volume = 1.0; if (FlxG.sound.music.fadeTween != null) FlxG.sound.music.fadeTween.cancel(); - trace('Playing vocals...'); - add(vocals); + if (vocals != null) + { + trace('Playing vocals...'); + add(vocals); - vocals.time = startTimestamp - Conductor.instance.instrumentalOffset; - vocals.pitch = playbackRate; - vocals.volume = 1.0; + vocals.time = startTimestamp - Conductor.instance.instrumentalOffset; + vocals.pitch = playbackRate; + vocals.volume = 1.0; - // trace('STARTING SONG AT:'); - // trace('${FlxG.sound.music.time}'); - // trace('${vocals.time}'); + // trace('STARTING SONG AT:'); + // trace('${FlxG.sound.music.time}'); + // trace('${vocals.time}'); + + vocals.play(); + } FlxG.sound.music.play(); - vocals.play(); #if FEATURE_DISCORD_RPC // Updating Discord Rich Presence (with Time Left) @@ -2493,7 +2555,7 @@ class PlayState extends MusicBeatSubState */ function processNotes(elapsed:Float):Void { - if (playerStrumline?.notes?.members == null || opponentStrumline?.notes?.members == null) return; + if (playerStrumline.notes?.members == null || opponentStrumline.notes?.members == null) return; // Process notes on the opponent's side. for (note in opponentStrumline.notes.members) @@ -2522,7 +2584,7 @@ class PlayState extends MusicBeatSubState // Process hold notes on the opponent's side. for (holdNote in opponentStrumline.holdNotes.members) { - if (holdNote == null || !holdNote.alive) continue; + if (holdNote == null || !holdNote.alive || holdNote.noteData == null) continue; // While the hold note is being hit, and there is length on the hold note... if (holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0) @@ -2541,7 +2603,7 @@ class PlayState extends MusicBeatSubState // We dropped a hold note. // Play miss animation, but don't penalize. - currentStage.getOpponent().playSingAnimation(holdNote.noteData.getDirection(), true); + if (currentStage != null) currentStage.getOpponent().playSingAnimation(holdNote.noteData.getDirection(), true); } } @@ -2654,7 +2716,7 @@ class PlayState extends MusicBeatSubState applyScore(event.score, '', event.healthChange, event.isComboBreak); // Play the miss sound. - vocals.playerVolume = 0; + if (vocals != null) vocals.playerVolume = 0; FunkinSound.playOnce(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.5, 0.6)); } else @@ -2712,7 +2774,8 @@ class PlayState extends MusicBeatSubState while (inputPressQueue.length > 0) { - var input:PreciseInputEvent = inputPressQueue.shift(); + var input:Null = inputPressQueue.shift(); + if (input == null) continue; playerStrumline.pressKey(input.noteDirection); @@ -2764,7 +2827,8 @@ class PlayState extends MusicBeatSubState while (inputReleaseQueue.length > 0) { - var input:PreciseInputEvent = inputReleaseQueue.shift(); + var input:Null = inputReleaseQueue.shift(); + if (input == null) continue; // Play the strumline animation. playerStrumline.playStatic(input.noteDirection); @@ -2817,8 +2881,7 @@ class PlayState extends MusicBeatSubState // Send the note hit event. var event:HitNoteScriptEvent = new HitNoteScriptEvent(note, healthChange, score, daRating, isComboBreak, - note.scoreable ? Highscore.tallies.combo + 1 : Highscore.tallies.combo, noteDiff, - daRating == 'sick'); + note.scoreable ? Highscore.tallies.combo + 1 : Highscore.tallies.combo, noteDiff, daRating == 'sick'); dispatchEvent(event); // Calling event.cancelEvent() skips all the other logic! Neat! @@ -2827,7 +2890,7 @@ class PlayState extends MusicBeatSubState playerStrumline.hitNote(note, !event.isComboBreak); if (event.doesNotesplash) playerStrumline.playNoteSplash(note.noteData.getDirection()); if (note.isHoldNote && note.holdNoteSprite != null) playerStrumline.playNoteHoldCover(note.holdNoteSprite); - vocals.playerVolume = 1; + if (vocals != null) vocals.playerVolume = 1; // Display the combo meter and add the calculation to the score. if (note.scoreable) @@ -2862,13 +2925,13 @@ class PlayState extends MusicBeatSubState if (pressArray[i]) indices.push(i); } } - vocals.playerVolume = 0; + if (vocals != null) vocals.playerVolume = 0; applyScore(Scoring.getMissScore(), 'miss', healthChange, true); if (playSound) { - vocals.playerVolume = 0; + if (vocals != null) vocals.playerVolume = 0; FunkinSound.playOnce(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.5, 0.6)); } } @@ -2914,7 +2977,7 @@ class PlayState extends MusicBeatSubState if (event.playSound) { - vocals.playerVolume = 0; + if (vocals != null) vocals.playerVolume = 0; FunkinSound.playOnce(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2)); } } @@ -2949,7 +3012,7 @@ class PlayState extends MusicBeatSubState } else { - this.remove(currentStage); + if (currentStage != null) this.remove(currentStage); FlxG.switchState(() -> new ChartEditorState( { targetSongId: currentSong.id, @@ -3057,7 +3120,7 @@ class PlayState extends MusicBeatSubState comboPopUps.displayRating(daRating); if (combo >= 10) comboPopUps.displayCombo(combo); - vocals.playerVolume = 1; + if (vocals != null) vocals.playerVolume = 1; } /** @@ -3120,7 +3183,7 @@ class PlayState extends MusicBeatSubState public function endSong(rightGoddamnNow:Bool = false):Void { if (FlxG.sound.music != null) FlxG.sound.music.volume = 0; - vocals.volume = 0; + if (vocals != null) vocals.volume = 0; mayPauseGame = false; isSongEnd = true; @@ -3201,7 +3264,7 @@ class PlayState extends MusicBeatSubState if (Date.now().getDay() == 5) Medals.award(FridayNight); // Determine the score rank for this song we just finished. - var scoreRank:ScoringRank = Scoring.calculateRank( + var scoreRank:Null = Scoring.calculateRank( { score: songScore, tallies: @@ -3227,7 +3290,7 @@ class PlayState extends MusicBeatSubState if (currentVariation == 'pico' && !PlayStatePlaylist.isStoryMode) Medals.award(FreeplayPicoMix); if (currentVariation == 'pico' && currentSong.id == 'stress') Medals.award(FreeplayStressPico); - Events.logEarnRank(scoreRank.toString()); + if (scoreRank != null) Events.logEarnRank(scoreRank.toString()); } #end @@ -3243,7 +3306,7 @@ class PlayState extends MusicBeatSubState // Pop the next song ID from the list. // Returns null if the list is empty. - var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift(); + var targetSongId:Null = PlayStatePlaylist.playlistSongIds.shift(); if (targetSongId == null) { @@ -3267,20 +3330,23 @@ class PlayState extends MusicBeatSubState }, }; - #if FEATURE_NEWGROUNDS - // Award a medal for beating a Story level. - Medals.awardStoryLevel(PlayStatePlaylist.campaignId); - - // Submit the score for the Story level to Newgrounds. - Leaderboards.submitLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, PlayStatePlaylist.campaignScore); - - Events.logCompleteLevel(PlayStatePlaylist.campaignId); - #end - - if (Save.instance.isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data)) + if (PlayStatePlaylist.campaignId != null) { - Save.instance.setLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data); - isNewHighscore = true; + #if FEATURE_NEWGROUNDS + // Award a medal for beating a Story level. + Medals.awardStoryLevel(PlayStatePlaylist.campaignId); + + // Submit the score for the Story level to Newgrounds. + Leaderboards.submitLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, PlayStatePlaylist.campaignScore); + + Events.logCompleteLevel(PlayStatePlaylist.campaignId); + #end + + if (Save.instance.isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data)) + { + Save.instance.setLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data); + isNewHighscore = true; + } } } @@ -3309,8 +3375,8 @@ class PlayState extends MusicBeatSubState FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransOut = true; - if (FlxG.sound.music != null) FlxG.sound.music.stop(); - vocals.stop(); + FlxG.sound.music?.stop(); + vocals?.stop(); // TODO: Softcode this cutscene. if (currentSong.id == 'eggnog') @@ -3324,13 +3390,13 @@ class PlayState extends MusicBeatSubState FunkinSound.playOnce(Paths.sound('Lights_Shut_off'), function() { // no camFollow so it centers on horror tree - var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId); + var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId) ?? throw 'Could not find a song with the ID $targetSongId'; var targetVariation:String = currentVariation; if (!targetSong.hasDifficulty(PlayStatePlaylist.campaignDifficulty, currentVariation)) { targetVariation = targetSong.getFirstValidVariation(PlayStatePlaylist.campaignDifficulty) ?? Constants.DEFAULT_VARIATION; } - this.remove(currentStage); + if (currentStage != null) this.remove(currentStage); LoadingState.loadPlayState( { targetSong: targetSong, @@ -3342,13 +3408,13 @@ class PlayState extends MusicBeatSubState } else { - var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId); + var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId) ?? throw 'Could not find a song with ID $targetSongId'; var targetVariation:String = currentVariation; if (!targetSong.hasDifficulty(PlayStatePlaylist.campaignDifficulty, currentVariation)) { targetVariation = targetSong.getFirstValidVariation(PlayStatePlaylist.campaignDifficulty) ?? Constants.DEFAULT_VARIATION; } - this.remove(currentStage); + if (currentStage != null) this.remove(currentStage); LoadingState.loadPlayState( { targetSong: targetSong, @@ -3470,20 +3536,24 @@ class PlayState extends MusicBeatSubState // If the opponent is GF, zoom in on the opponent. // Else, if there is no GF, zoom in on BF. // Else, zoom in on GF. - var targetDad:Bool = currentStage.getDad() != null && currentStage.getDad().characterId == 'gf'; - var targetBF:Bool = currentStage.getGirlfriend() == null && !targetDad; + var boyfriend:Null = currentStage?.getBoyfriend(); + var girlfriend:Null = currentStage?.getGirlfriend(); + var dad:Null = currentStage?.getDad(); - if (targetBF) + var targetDad:Bool = dad?.characterId == 'gf'; + var targetBF:Bool = girlfriend == null && !targetDad; + + if (targetBF && boyfriend != null) { - FlxG.camera.follow(currentStage.getBoyfriend(), null, 0.05); + FlxG.camera.follow(boyfriend, null, 0.05); } - else if (targetDad) + else if (targetDad && dad != null) { - FlxG.camera.follow(currentStage.getDad(), null, 0.05); + FlxG.camera.follow(dad, null, 0.05); } - else + else if (girlfriend != null) { - FlxG.camera.follow(currentStage.getGirlfriend(), null, 0.05); + FlxG.camera.follow(girlfriend, null, 0.05); } // TODO: Make target offset configurable. @@ -3505,15 +3575,15 @@ class PlayState extends MusicBeatSubState new FlxTimer().start(0.8, function(_) { if (targetBF) { - currentStage.getBoyfriend().animation.play('hey'); + boyfriend?.animation.play('hey'); } else if (targetDad) { - currentStage.getDad().animation.play('cheer'); + dad?.animation.play('cheer'); } else { - currentStage.getGirlfriend().animation.play('cheer'); + girlfriend?.animation.play('cheer'); } // Zoom over to the Results screen. @@ -3532,12 +3602,17 @@ class PlayState extends MusicBeatSubState */ function moveToResultsScreen(isNewHighscore:Bool, ?prevScoreData:SaveScoreData):Void { + var currentChart:SongDifficulty = currentChart ?? + { + trace('ERROR: Cannot move to results screen with a null chart.'); + return; + } + persistentUpdate = false; - vocals.stop(); + vocals?.stop(); camHUD.alpha = 1; var talliesToUse:Tallies = PlayStatePlaylist.isStoryMode ? Highscore.talliesLevel : Highscore.tallies; - var res:ResultState = new ResultState( { storyMode: PlayStatePlaylist.isStoryMode, @@ -3583,7 +3658,7 @@ class PlayState extends MusicBeatSubState /** * Resets the camera's zoom level and focus point. */ - public function resetCamera(?resetZoom:Bool = true, ?cancelTweens:Bool = true, ?snap:Bool = true):Void + public function resetCamera(resetZoom:Bool = true, cancelTweens:Bool = true, snap:Bool = true):Void { // Cancel camera tweens if any are active. if (cancelTweens) @@ -3606,7 +3681,7 @@ class PlayState extends MusicBeatSubState /** * Sets the camera follow point's position and tweens the camera there. */ - public function tweenCameraToPosition(?x:Float, ?y:Float, ?duration:Float, ?ease:NullFloat>):Void + public function tweenCameraToPosition(x:Float = 0, y:Float = 0, duration:Float = 0, ?ease:NullFloat>):Void { cameraFollowPoint.setPosition(x, y); tweenCameraToFollowPoint(duration, ease); @@ -3615,7 +3690,7 @@ class PlayState extends MusicBeatSubState /** * Disables camera following and tweens the camera to the follow point manually. */ - public function tweenCameraToFollowPoint(?duration:Float, ?ease:NullFloat>):Void + public function tweenCameraToFollowPoint(duration:Float = 0, ?ease:NullFloat>):Void { // Cancel the current tween if it's active. cancelCameraFollowTween(); @@ -3628,6 +3703,7 @@ class PlayState extends MusicBeatSubState else { // Disable camera following for the duration of the tween. + @:nullSafety(Off) FlxG.camera.target = null; // Follow tween! Caching it so we can cancel/pause it later if needed. @@ -3653,7 +3729,7 @@ class PlayState extends MusicBeatSubState /** * Tweens the camera zoom to the desired amount. */ - public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?direct:Bool, ?ease:NullFloat>):Void + public function tweenCameraZoom(zoom:Float = 1, duration:Float = 0, direct:Bool = false, ?ease:NullFloat>):Void { // Cancel the current tween if it's active. cancelCameraZoomTween(); @@ -3674,7 +3750,7 @@ class PlayState extends MusicBeatSubState } } - public function cancelCameraZoomTween() + public function cancelCameraZoomTween():Void { if (cameraZoomTween != null) { @@ -3714,7 +3790,7 @@ class PlayState extends MusicBeatSubState for (i in strumlines) { - var value:Float = speed; + var value:Float = speed ?? 0; var strum:Strumline = Reflect.getProperty(this, i); if (duration == 0) diff --git a/source/funkin/play/components/HealthIcon.hx b/source/funkin/play/components/HealthIcon.hx index 459869bd9..2d0b4cc78 100644 --- a/source/funkin/play/components/HealthIcon.hx +++ b/source/funkin/play/components/HealthIcon.hx @@ -151,10 +151,12 @@ class HealthIcon extends FunkinSprite */ public function toggleOldIcon():Void { + final playState:Null = PlayState.instance; + if (playState == null || playState.currentStage == null) return; if (characterId == 'bf-old') { - isPixel = PlayState.instance.currentStage.getBoyfriend().isPixel; - PlayState.instance.currentStage.getBoyfriend().initHealthIcon(false); + isPixel = playState.currentStage.getBoyfriend()?.isPixel ?? false; + playState.currentStage.getBoyfriend()?.initHealthIcon(false); } else { diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 95f1ab4c2..f59321a5c 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -97,9 +97,9 @@ class Strumline extends FlxSpriteGroup /** * Reset the scroll speed to the current chart's scroll speed. */ - public function resetScrollSpeed():Void + public function resetScrollSpeed(?newScrollSpeed:Float):Void { - scrollSpeed = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0; + scrollSpeed = newScrollSpeed ?? PlayState.instance?.currentChart?.scrollSpeed ?? 1.0; } var _conductorInUse:Null; @@ -184,7 +184,7 @@ class Strumline extends FlxSpriteGroup static final BACKGROUND_PAD:Int = 16; - public function new(noteStyle:NoteStyle, isPlayer:Bool) + public function new(noteStyle:NoteStyle, isPlayer:Bool, ?scrollSpeed:Float) { super(); @@ -243,7 +243,7 @@ class Strumline extends FlxSpriteGroup this.refresh(); this.onNoteIncoming = new FlxTypedSignalVoid>(); - resetScrollSpeed(); + resetScrollSpeed(scrollSpeed); for (i in 0...KEY_COUNT) { diff --git a/source/funkin/ui/transition/LoadingState.hx b/source/funkin/ui/transition/LoadingState.hx index fd8579a42..be0d17240 100644 --- a/source/funkin/ui/transition/LoadingState.hx +++ b/source/funkin/ui/transition/LoadingState.hx @@ -12,6 +12,7 @@ import funkin.graphics.shaders.ScreenWipeShader; import funkin.play.PlayState; import funkin.play.PlayStatePlaylist; import funkin.play.song.Song.SongDifficulty; +import funkin.play.stage.Stage; import haxe.io.Path; import lime.app.Future; import lime.app.Promise; @@ -21,6 +22,7 @@ import lime.utils.Assets as LimeAssets; import openfl.filters.ShaderFilter; import openfl.utils.Assets as OpenFLAssets; +@:nullSafety class LoadingState extends MusicBeatSubState { inline static var MIN_TIME = 1.0; @@ -30,18 +32,21 @@ class LoadingState extends MusicBeatSubState var target:NextState; var playParams:Null; var stopMusic:Bool = false; - var callbacks:MultiCallback; + var callbacks:Null; var danceLeft:Bool = false; var loadBar:FlxSprite; var funkay:FlxSprite; - function new(target:NextState, stopMusic:Bool, playParams:Null = null) + function new(target:NextState, stopMusic:Bool, ?playParams:PlayStateParams) { super(); this.target = target; this.playParams = playParams; this.stopMusic = stopMusic; + + this.loadBar = new FunkinSprite(0, FlxG.height - 20).makeSolidColor(0, 10, 0xFFff16d2); + this.funkay = FunkinSprite.create('funkay'); } override function create():Void @@ -49,14 +54,12 @@ class LoadingState extends MusicBeatSubState var bg:FunkinSprite = new FunkinSprite().makeSolidColor(FlxG.width, FlxG.height, 0xFFcaff4d); add(bg); - funkay = FunkinSprite.create('funkay'); funkay.setGraphicSize(0, FlxG.height); funkay.updateHitbox(); add(funkay); funkay.scrollFactor.set(); funkay.screenCenter(); - loadBar = new FunkinSprite(0, FlxG.height - 20).makeSolidColor(0, 10, 0xFFff16d2); add(loadBar); initSongsManifest().onComplete(function(lib) { @@ -66,15 +69,21 @@ class LoadingState extends MusicBeatSubState if (playParams != null) { // Load and cache the song's charts. - if (playParams.targetSong != null) + if (playParams.targetSong == null) { - playParams.targetSong.cacheCharts(true); + throw 'Invalid parameter: Target song should not be null'; } + playParams.targetSong.cacheCharts(true); + // Preload the song for the play state. var difficulty:String = playParams.targetDifficulty ?? Constants.DEFAULT_DIFFICULTY; var variation:String = playParams.targetVariation ?? Constants.DEFAULT_VARIATION; - var targetChart:SongDifficulty = playParams.targetSong?.getDifficulty(difficulty, variation); + var targetChart:Null = playParams.targetSong.getDifficulty(difficulty, variation); + if (targetChart == null) + { + throw 'Couldn\'t retrieve chart data for song "${playParams.targetSong.songName}" on difficulty "$difficulty" and variation "$variation"'; + } var instPath:String = targetChart.getInstPath(playParams.targetInstrumental); var voicesPaths:Array = targetChart.buildVoiceList(); @@ -105,9 +114,9 @@ class LoadingState extends MusicBeatSubState // library.types.set(symbolPath, SOUND); // @:privateAccess // library.pathGroups.set(symbolPath, [library.__cacheBreak(symbolPath)]); - var callback = callbacks.add('song:' + path); + var callback = callbacks?.add('song:' + path); Assets.loadSound(path).onComplete(function(_) { - callback(); + if (callback != null) callback(); }); } } @@ -120,9 +129,9 @@ class LoadingState extends MusicBeatSubState @:privateAccess if (!LimeAssets.libraryPaths.exists(library)) throw 'Missing library: ' + library; - var callback = callbacks.add('library:' + library); + var callback = callbacks?.add('library:' + library); Assets.loadLibrary(library).onComplete(function(_) { - callback(); + if (callback != null) callback(); }); } } @@ -177,6 +186,7 @@ class LoadingState extends MusicBeatSubState function onLoad():Void { // Stop the instrumental. + @:nullSafety(Off) if (stopMusic && FlxG.sound.music != null) { FlxG.sound.music.destroy(); @@ -197,7 +207,7 @@ class LoadingState extends MusicBeatSubState static function getSongPath():String { - return Paths.inst(PlayState.instance.currentSong.id); + return Paths.inst(PlayState.instance?.currentSong.id ?? throw 'Cannot retrieve song path'); } static var stageDirectory:String = "shared"; @@ -211,10 +221,10 @@ class LoadingState extends MusicBeatSubState */ public static function loadPlayState(params:PlayStateParams, shouldStopMusic = false, asSubState = false, ?onConstruct:PlayState->Void):Void { - var daChart:Null = params.targetSong.getDifficulty(params.targetDifficulty ?? Constants.DEFAULT_DIFFICULTY, + var daChart:Null = params.targetSong?.getDifficulty(params.targetDifficulty ?? Constants.DEFAULT_DIFFICULTY, params.targetVariation ?? Constants.DEFAULT_VARIATION); - var daStage = funkin.data.stage.StageRegistry.instance.fetchEntry(daChart?.stage ?? Constants.DEFAULT_STAGE); + var daStage:Null = funkin.data.stage.StageRegistry.instance.fetchEntry(daChart?.stage ?? Constants.DEFAULT_STAGE); stageDirectory = daStage?._data?.directory ?? "shared"; Paths.setCurrentLevel(stageDirectory); @@ -249,6 +259,7 @@ class LoadingState extends MusicBeatSubState } #else // All assets preloaded, switch directly to play state (defualt on other targets). + @:nullSafety(Off) if (shouldStopMusic && FlxG.sound.music != null) { FlxG.sound.music.destroy(); @@ -257,7 +268,7 @@ class LoadingState extends MusicBeatSubState // Load and cache the song's charts. // Don't do this if we already provided the music and charts. - if (params?.targetSong != null && !params.overrideMusic) + if (!(params.overrideMusic ?? false)) { params.targetSong.cacheCharts(true); } @@ -272,7 +283,8 @@ class LoadingState extends MusicBeatSubState var songDifficulty = params.targetSong.getDifficulty(params.targetDifficulty, params.targetVariation); if (songDifficulty != null) { - var noteStyle = NoteStyleRegistry.instance.fetchEntry(songDifficulty.noteStyle); + var noteStyle = NoteStyleRegistry.instance.fetchEntry(songDifficulty.noteStyle ?? ''); + if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault(); FunkinMemory.cacheNoteStyle(noteStyle); } @@ -439,7 +451,7 @@ class LoadingState extends MusicBeatSubState var libraryPaths = LimeAssets.libraryPaths; if (libraryPaths.exists(id)) { - path = libraryPaths[id]; + path = libraryPaths[id] ?? path; rootPath = Path.directory(path); } else @@ -490,17 +502,18 @@ class LoadingState extends MusicBeatSubState } } +@:nullSafety class MultiCallback { public var callback:Void->Void; - public var logId:String = null; + public var logId:Null; public var length(default, null) = 0; public var numRemaining(default, null) = 0; var unfired = new MapVoid>(); var fired = new Array(); - public function new(callback:Void->Void, logId:String = null) + public function new(callback:Void->Void, ?logId:String) { this.callback = callback; this.logId = logId; @@ -511,8 +524,7 @@ class MultiCallback id = '$length:$id'; length++; numRemaining++; - var func:Void->Void = null; - func = function() { + var func:Void->Void = function() { if (unfired.exists(id)) { unfired.remove(id);