diff --git a/source/funkin/LatencyState.hx b/source/funkin/LatencyState.hx index 4a8ed2d2e..7385ca640 100644 --- a/source/funkin/LatencyState.hx +++ b/source/funkin/LatencyState.hx @@ -191,7 +191,7 @@ class LatencyState extends MusicBeatSubState if (FlxG.keys.pressed.D) FlxG.sound.music.time += 1000 * FlxG.elapsed; - Conductor.songPosition = swagSong.getTimeWithDiff() - Conductor.offset; + Conductor.update(swagSong.getTimeWithDiff() - Conductor.offset); // Conductor.songPosition += (Timer.stamp() * 1000) - FlxG.sound.music.prevTimestamp; songPosVis.x = songPosToX(Conductor.songPosition); diff --git a/source/funkin/MusicBeatSubState.hx b/source/funkin/MusicBeatSubState.hx index 1958c6074..31d1bd14c 100644 --- a/source/funkin/MusicBeatSubState.hx +++ b/source/funkin/MusicBeatSubState.hx @@ -61,6 +61,15 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl // This can now be used in EVERY STATE YAY! if (FlxG.keys.justPressed.F5) debug_refreshModules(); + + // Display Conductor info in the watch window. + FlxG.watch.addQuick("songPosition", Conductor.songPosition); + FlxG.watch.addQuick("bpm", Conductor.bpm); + FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime); + FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime); + FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime); + + dispatchEvent(new UpdateScriptEvent(elapsed)); } function debug_refreshModules() diff --git a/source/funkin/TitleState.hx b/source/funkin/TitleState.hx index 8ba5121fa..47cc33a38 100644 --- a/source/funkin/TitleState.hx +++ b/source/funkin/TitleState.hx @@ -256,7 +256,7 @@ class TitleState extends MusicBeatState FlxTween.tween(FlxG.stage.window, {y: FlxG.stage.window.y + 100}, 0.7, {ease: FlxEase.quadInOut, type: PINGPONG}); } - if (FlxG.sound.music != null) Conductor.songPosition = FlxG.sound.music.time; + if (FlxG.sound.music != null) Conductor.update(FlxG.sound.music.time); if (FlxG.keys.justPressed.F) FlxG.fullscreen = !FlxG.fullscreen; // do controls.PAUSE | controls.ACCEPT instead? diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx index 51d72693e..48e98e84f 100644 --- a/source/funkin/play/Countdown.hx +++ b/source/funkin/play/Countdown.hx @@ -37,7 +37,7 @@ class Countdown stopCountdown(); PlayState.instance.isInCountdown = true; - Conductor.songPosition = Conductor.beatLengthMs * -5; + Conductor.update(PlayState.instance.startTimestamp + Conductor.beatLengthMs * -5); // Handle onBeatHit events manually @:privateAccess PlayState.instance.dispatchEvent(new SongTimeScriptEvent(ScriptEvent.SONG_BEAT_HIT, 0, 0)); @@ -46,6 +46,12 @@ class Countdown countdownTimer = new FlxTimer(); countdownTimer.start(Conductor.beatLengthMs / 1000, function(tmr:FlxTimer) { + if (PlayState.instance == null) + { + tmr.cancel(); + return; + } + countdownStep = decrement(countdownStep); // Handle onBeatHit events manually @@ -146,7 +152,7 @@ class Countdown { stopCountdown(); // This will trigger PlayState.startSong() - Conductor.songPosition = 0; + Conductor.update(0); // PlayState.isInCountdown = false; } diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index f38dabea4..161da5191 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -117,7 +117,7 @@ class GameOverSubState extends MusicBeatSubState gameOverMusic.stop(); // The conductor now represents the BPM of the game over music. - Conductor.songPosition = 0; + Conductor.update(0); } var hasStartedAnimation:Bool = false; @@ -183,7 +183,7 @@ class GameOverSubState extends MusicBeatSubState { // Match the conductor to the music. // This enables the stepHit and beatHit events. - Conductor.songPosition = gameOverMusic.time; + Conductor.update(gameOverMusic.time); } else { diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index cf12db06b..ae57f3cd5 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -88,6 +88,10 @@ typedef PlayStateParams = * @default `false` */ ?minimalMode:Bool, + /** + * If specified, the game will jump to the specified timestamp after the countdown ends. + */ + ?startTimestamp:Float, } /** @@ -236,6 +240,8 @@ class PlayState extends MusicBeatSubState */ public var disableKeys:Bool = false; + public var startTimestamp:Float = 0.0; + public var isSubState(get, null):Bool; function get_isSubState():Bool @@ -471,6 +477,7 @@ class PlayState extends MusicBeatSubState if (params.targetCharacter != null) currentPlayerId = params.targetCharacter; isPracticeMode = params.practiceMode ?? false; isMinimalMode = params.minimalMode ?? false; + startTimestamp = params.startTimestamp ?? 0.0; // Don't do anything else here! Wait until create() when we attach to the camera. } @@ -560,7 +567,7 @@ class PlayState extends MusicBeatSubState // Prepare the Conductor. Conductor.mapTimeChanges(currentChart.timeChanges); - Conductor.update(-5000); + Conductor.update((Conductor.beatLengthMs * -5) + startTimestamp); // The song is now loaded. We can continue to initialize the play state. initCameras(); @@ -669,7 +676,7 @@ class PlayState extends MusicBeatSubState FlxG.sound.music.pause(); vocals.pause(); - FlxG.sound.music.time = 0; + FlxG.sound.music.time = (startTimestamp); vocals.time = 0; FlxG.sound.music.volume = 1; @@ -700,8 +707,8 @@ class PlayState extends MusicBeatSubState { if (isInCountdown) { - Conductor.songPosition += elapsed * 1000; - if (Conductor.songPosition >= 0) startSong(); + Conductor.update(Conductor.songPosition + elapsed * 1000); + if (Conductor.songPosition >= startTimestamp) startSong(); } } else @@ -1067,7 +1074,8 @@ class PlayState extends MusicBeatSubState // super.stepHit() returns false if a module cancelled the event. if (!super.stepHit()) return false; - if (FlxG.sound.music != null + if (!startingSong + && FlxG.sound.music != null && (Math.abs(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)) > 200 || Math.abs(vocals.checkSyncError(Conductor.songPosition - Conductor.offset)) > 200)) { @@ -1515,7 +1523,7 @@ class PlayState extends MusicBeatSubState /** * Read note data from the chart and generate the notes. */ - function regenNoteData():Void + function regenNoteData(startTime:Float = 0):Void { Highscore.tallies.combo = 0; Highscore.tallies = new Tallies(); @@ -1531,6 +1539,8 @@ class PlayState extends MusicBeatSubState for (songNote in currentChart.notes) { var strumTime:Float = songNote.time; + if (strumTime < startTime) continue; // Skip notes that are before the start time. + var noteData:Int = songNote.getDirection(); var playerNote:Bool = true; @@ -1617,14 +1627,22 @@ class PlayState extends MusicBeatSubState } FlxG.sound.music.onComplete = endSong; + FlxG.sound.music.play(false, startTimestamp); trace('Playing vocals...'); add(vocals); vocals.play(); + resyncVocals(); #if discord_rpc // Updating Discord Rich Presence (with Time Left) DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true, currentSongLengthMs); #end + + if (startTimestamp > 0) + { + FlxG.sound.music.time = startTimestamp; + handleSkippedNotes(); + } } /** @@ -1640,7 +1658,7 @@ class PlayState extends MusicBeatSubState Conductor.update(); vocals.time = FlxG.sound.music.time; - vocals.play(); + vocals.play(false, FlxG.sound.music.time); } /** @@ -1836,6 +1854,23 @@ class PlayState extends MusicBeatSubState */ var inputSpitter:Array = []; + function handleSkippedNotes():Void + { + for (note in playerStrumline.notes.members) + { + var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS; + var hitWindowCenter = note.strumTime; + var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS; + + if (Conductor.songPosition > hitWindowEnd) + { + // We have passed this note. + // Flag the note for deletion without actually penalizing the player. + note.handledMiss = true; + } + } + } + /** * PreciseInputEvents are put into a queue between update() calls, * and then processed here. @@ -2064,11 +2099,10 @@ class PlayState extends MusicBeatSubState if (event.eventCanceled) return; health -= Constants.HEALTH_MISS_PENALTY; + songScore -= 10; if (!isPracticeMode) { - songScore -= 10; - // messy copy paste rn lol var pressArray:Array = [ controls.NOTE_LEFT_P, @@ -2139,11 +2173,10 @@ class PlayState extends MusicBeatSubState if (event.eventCanceled) return; health += event.healthChange; + songScore += event.scoreChange; if (!isPracticeMode) { - songScore += event.scoreChange; - var pressArray:Array = [ controls.NOTE_LEFT_P, controls.NOTE_DOWN_P, @@ -2282,11 +2315,10 @@ class PlayState extends MusicBeatSubState playerStrumline.playNoteSplash(daNote.noteData.getDirection()); } - // Only add the score if you're not on practice mode + songScore += score; + if (!isPracticeMode) { - songScore += score; - // TODO: Input splitter uses old input system, make it pull from the precise input queue directly. var pressArray:Array = [ controls.NOTE_LEFT_P, @@ -2643,30 +2675,16 @@ class PlayState extends MusicBeatSubState { FlxG.sound.music.pause(); - FlxG.sound.music.time += sections * Conductor.measureLengthMs; + var targetTimeSteps:Float = Conductor.currentStepTime + (Conductor.timeSignatureNumerator * Constants.STEPS_PER_BEAT * sections); + var targetTimeMs:Float = Conductor.getStepTimeInMs(targetTimeSteps); + + FlxG.sound.music.time = targetTimeMs; + + handleSkippedNotes(); + // regenNoteData(FlxG.sound.music.time); Conductor.update(FlxG.sound.music.time); - /** - * - // TODO: Redo this for the new conductor. - var daBPM:Float = Conductor.bpm; - var daPos:Float = 0; - for (i in 0...(Std.int(Conductor.currentStep / 16 + sec))) - { - var section = .getSong()[i]; - if (section == null) continue; - if (section.changeBPM) - { - daBPM = .getSong()[i].bpm; - } - daPos += 4 * (1000 * 60 / daBPM); - } - Conductor.songPosition = FlxG.sound.music.time = daPos; - Conductor.songPosition += Conductor.offset; - - */ - resyncVocals(); } #end diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 454ec13e1..40bd8656a 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -220,6 +220,7 @@ class Strumline extends FlxSpriteGroup { if (noteData.length == 0) return; + var songStart:Float = PlayState.instance.startTimestamp ?? 0.0; var renderWindowStart:Float = Conductor.songPosition + RENDER_DISTANCE_MS; for (noteIndex in nextNoteIndex...noteData.length) @@ -227,6 +228,7 @@ class Strumline extends FlxSpriteGroup var note:Null = noteData[noteIndex]; if (note == null) continue; + if (note.time < songStart) continue; if (note.time > renderWindowStart) break; var noteSprite = buildNoteSprite(note); diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index a3fcd0f22..8cf496637 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -400,6 +400,11 @@ class ChartEditorState extends HaxeUIState return isViewDownscroll; } + /** + * If true, playtesting a chart will skip to the current playhead position. + */ + var playtestStartTime:Bool = false; + /** * Whether hitsounds are enabled for at least one character. */ @@ -1305,6 +1310,9 @@ class ChartEditorState extends HaxeUIState addUIChangeListener('menubarItemDownscroll', event -> isViewDownscroll = event.value); setUICheckboxSelected('menubarItemDownscroll', isViewDownscroll); + addUIChangeListener('menubarItemPlaytestStartTime', event -> playtestStartTime = event.value); + setUICheckboxSelected('menubarItemPlaytestStartTime', playtestStartTime); + addUIChangeListener('menuBarItemThemeLight', function(event:UIEvent) { if (event.target.value) currentTheme = ChartEditorTheme.Light; }); @@ -3049,6 +3057,9 @@ class ChartEditorState extends HaxeUIState */ public function testSongInPlayState(?minimal:Bool = false):Void { + var startTimestamp:Float = 0; + if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs; + var targetSong:Song = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, false); subStateClosed.add(fixCamera); @@ -3061,6 +3072,7 @@ class ChartEditorState extends HaxeUIState // targetCharacter: targetCharacter, practiceMode: true, minimalMode: minimal, + startTimestamp: startTimestamp, })); }