From 3c218ec01cfd96930dfa2f5717ec835ce6391656 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Sat, 22 Jul 2023 20:16:43 -0400 Subject: [PATCH] Done with BPM change fixes, currently working on rendering efficiency --- source/funkin/Conductor.hx | 25 +-- source/funkin/FreeplayState.hx | 2 +- source/funkin/InitState.hx | 2 +- source/funkin/MainMenuState.hx | 2 +- source/funkin/TitleState.hx | 2 +- source/funkin/play/PlayState.hx | 10 +- source/funkin/play/notes/Strumline.hx | 8 +- source/funkin/play/notes/SustainTrail.hx | 17 +- source/funkin/play/song/SongData.hx | 14 ++ .../debug/charting/ChartEditorEventSprite.hx | 39 +++- .../charting/ChartEditorHoldNoteSprite.hx | 143 +++++++++++++ .../debug/charting/ChartEditorNoteSprite.hx | 29 ++- .../ui/debug/charting/ChartEditorState.hx | 199 ++++++++++-------- .../ui/stageBuildShit/StageBuilderState.hx | 2 +- source/funkin/util/Constants.hx | 134 ++++++------ 15 files changed, 425 insertions(+), 203 deletions(-) create mode 100644 source/funkin/ui/debug/charting/ChartEditorHoldNoteSprite.hx diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index c1d841623..3c6470373 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -3,7 +3,6 @@ package funkin; import funkin.util.Constants; import flixel.util.FlxSignal; import flixel.math.FlxMath; -import funkin.SongLoad.SwagSong; import funkin.play.song.Song.SongDifficulty; import funkin.play.song.SongData.SongTimeChange; @@ -13,12 +12,6 @@ import funkin.play.song.SongData.SongTimeChange; */ class Conductor { - public static final PIXELS_PER_MS:Float = 0.45; - public static final HIT_WINDOW_MS:Float = 160; - public static final SECONDS_PER_MINUTE:Float = 60; - public static final MILLIS_PER_SECOND:Float = 1000; - public static final STEPS_PER_BEAT:Int = 4; - // onBeatHit is called every quarter note // onStepHit is called every sixteenth note // 4/4 = 4 beats per measure = 16 steps per measure @@ -82,18 +75,18 @@ class Conductor } /** - * Duration of a beat in milliseconds. Calculated based on bpm. + * Duration of a beat (quarter note) in milliseconds. Calculated based on bpm. */ public static var beatLengthMs(get, null):Float; static function get_beatLengthMs():Float { // Tied directly to BPM. - return ((SECONDS_PER_MINUTE / bpm) * MILLIS_PER_SECOND); + return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC); } /** - * Duration of a step (quarter) in milliseconds. Calculated based on bpm. + * Duration of a step (sixtennth note) in milliseconds. Calculated based on bpm. */ public static var stepLengthMs(get, null):Float; @@ -280,7 +273,8 @@ class Conductor { var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1]; currentTimeChange.beatTime = prevTimeChange.beatTime - + ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC); + + ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC) + + 0.01; } } } @@ -323,7 +317,8 @@ class Conductor } } - var resultFractionalStep:Float = (ms - lastTimeChange.timeStamp) / stepLengthMs; + var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator; + var resultFractionalStep:Float = (ms - lastTimeChange.timeStamp) / lastStepLengthMs; resultStep += resultFractionalStep; // Math.floor(); return resultStep; @@ -359,7 +354,8 @@ class Conductor } } - resultMs += (stepTime - lastTimeChange.beatTime * 4) * stepLengthMs; + var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator; + resultMs += (stepTime - lastTimeChange.beatTime * 4) * lastStepLengthMs; return resultMs; } @@ -394,7 +390,8 @@ class Conductor } } - resultMs += (beatTime - lastTimeChange.beatTime) * stepLengthMs * Constants.STEPS_PER_BEAT; + var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator; + resultMs += (beatTime - lastTimeChange.beatTime) * lastStepLengthMs * Constants.STEPS_PER_BEAT; return resultMs; } diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx index 1c226dbb5..608898a5f 100644 --- a/source/funkin/FreeplayState.hx +++ b/source/funkin/FreeplayState.hx @@ -127,7 +127,7 @@ class FreeplayState extends MusicBeatSubState if (FlxG.sound.music != null) { - if (!FlxG.sound.music.playing) FlxG.sound.playMusic(Paths.music('freakyMenu')); + if (!FlxG.sound.music.playing) FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu')); } // if (StoryMenuState.weekUnlocked[2] || isDebug) diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index ce863bd0b..eeffebdb1 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -261,7 +261,7 @@ class InitState extends FlxTransitionableState */ function startGameNormally():Void { - FlxG.sound.cache(Paths.music('freakyMenu')); + FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu')); FlxG.switchState(new TitleState()); } diff --git a/source/funkin/MainMenuState.hx b/source/funkin/MainMenuState.hx index 020a121c0..2c251635c 100644 --- a/source/funkin/MainMenuState.hx +++ b/source/funkin/MainMenuState.hx @@ -56,7 +56,7 @@ class MainMenuState extends MusicBeatState if (!FlxG.sound.music.playing) { - FlxG.sound.playMusic(Paths.music('freakyMenu')); + FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu')); } persistentUpdate = persistentDraw = true; diff --git a/source/funkin/TitleState.hx b/source/funkin/TitleState.hx index a19d09473..8ba5121fa 100644 --- a/source/funkin/TitleState.hx +++ b/source/funkin/TitleState.hx @@ -49,7 +49,7 @@ class TitleState extends MusicBeatState swagShader = new ColorSwap(); curWacky = FlxG.random.getObject(getIntroTextShit()); - FlxG.sound.cache(Paths.music('freakyMenu')); + FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu')); // DEBUG BULLSHIT diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index ae8d9ae86..c0705bd96 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1637,9 +1637,9 @@ class PlayState extends MusicBeatState { if (note == null) continue; - var hitWindowStart = note.strumTime - Conductor.HIT_WINDOW_MS; + var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS; var hitWindowCenter = note.strumTime; - var hitWindowEnd = note.strumTime + Conductor.HIT_WINDOW_MS; + var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS; if (Conductor.songPosition > hitWindowEnd) { @@ -1714,9 +1714,9 @@ class PlayState extends MusicBeatState { if (note == null || note.hasBeenHit) continue; - var hitWindowStart = note.strumTime - Conductor.HIT_WINDOW_MS; + var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS; var hitWindowCenter = note.strumTime; - var hitWindowEnd = note.strumTime + Conductor.HIT_WINDOW_MS; + var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS; if (Conductor.songPosition > hitWindowEnd) { @@ -2367,7 +2367,7 @@ class PlayState extends MusicBeatState if (targetSongId == null) { - FlxG.sound.playMusic(Paths.music('freakyMenu')); + FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu')); transIn = FlxTransitionableState.defaultTransIn; transOut = FlxTransitionableState.defaultTransOut; diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 4fdf5afe3..454ec13e1 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -213,7 +213,7 @@ class Strumline extends FlxSpriteGroup var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0; var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0; - return Conductor.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (PreferencesMenu.getPref('downscroll') ? 1 : -1); + return Constants.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (PreferencesMenu.getPref('downscroll') ? 1 : -1); } function updateNotes():Void @@ -273,7 +273,7 @@ class Strumline extends FlxSpriteGroup } } - var renderWindowEnd = holdNote.strumTime + holdNote.fullSustainLength + Conductor.HIT_WINDOW_MS + RENDER_DISTANCE_MS / 8; + var renderWindowEnd = holdNote.strumTime + holdNote.fullSustainLength + Constants.HIT_WINDOW_MS + RENDER_DISTANCE_MS / 8; if (holdNote.missedNote && Conductor.songPosition >= renderWindowEnd) { @@ -308,7 +308,7 @@ class Strumline extends FlxSpriteGroup // Hold note was dropped before completing, keep it in its clipped state. holdNote.visible = true; - var yOffset:Float = (holdNote.fullSustainLength - holdNote.sustainLength) * Conductor.PIXELS_PER_MS; + var yOffset:Float = (holdNote.fullSustainLength - holdNote.sustainLength) * Constants.PIXELS_PER_MS; trace('yOffset: ' + yOffset); trace('holdNote.fullSustainLength: ' + holdNote.fullSustainLength); @@ -678,7 +678,7 @@ class Strumline extends FlxSpriteGroup { // The note sprite pool is full and all note splashes are active. // We have to create a new note. - result = new SustainTrail(0, 100, noteStyle.getHoldNoteAssetPath(), noteStyle); + result = new SustainTrail(0, 100, noteStyle); this.holdNotes.add(result); } diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx index d9f1aab6e..fdd613667 100644 --- a/source/funkin/play/notes/SustainTrail.hx +++ b/source/funkin/play/notes/SustainTrail.hx @@ -88,9 +88,9 @@ class SustainTrail extends FlxSprite * @param SustainLength Length in milliseconds. * @param fileName */ - public function new(noteDirection:NoteDirection, sustainLength:Float, fileName:String, noteStyle:NoteStyle) + public function new(noteDirection:NoteDirection, sustainLength:Float, noteStyle:NoteStyle) { - super(0, 0, fileName); + super(0, 0, noteStyle.getHoldNoteAssetPath()); antialiasing = true; @@ -111,7 +111,7 @@ class SustainTrail extends FlxSprite // CALCULATE SIZE width = graphic.width / 8 * zoom; // amount of notes * 2 - height = sustainHeight(sustainLength, PlayState.instance.currentChart.scrollSpeed); + height = sustainHeight(sustainLength, getScrollSpeed()); // instead of scrollSpeed, PlayState.SONG.speed flipY = PreferencesMenu.getPref('downscroll'); @@ -123,6 +123,13 @@ class SustainTrail extends FlxSprite updateClipping(); indices = new DrawData<Int>(12, true, TRIANGLE_VERTEX_INDICES); + + this.active = true; // This NEEDS to be true for the note to be drawn! + } + + function getScrollSpeed():Float + { + return PlayState?.instance?.currentChart?.scrollSpeed ?? 1.0; } /** @@ -139,7 +146,7 @@ class SustainTrail extends FlxSprite { if (s < 0) s = 0; - height = sustainHeight(s, PlayState.instance.currentChart.scrollSpeed); + height = sustainHeight(s, getScrollSpeed()); updateColorTransform(); updateClipping(); return sustainLength = s; @@ -152,7 +159,7 @@ class SustainTrail extends FlxSprite */ public function updateClipping(songTime:Float = 0):Void { - var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), PlayState.instance.currentChart.scrollSpeed), 0, height); + var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, height); if (clipHeight == 0) { visible = false; diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx index 5398f6c4b..c2a701ce9 100644 --- a/source/funkin/play/song/SongData.hx +++ b/source/funkin/play/song/SongData.hx @@ -386,6 +386,9 @@ abstract SongNoteData(RawSongNoteData) }; } + /** + * The timestamp of the note, in milliseconds. + */ public var time(get, set):Float; public function get_time():Float @@ -398,6 +401,9 @@ abstract SongNoteData(RawSongNoteData) return this.t = value; } + /** + * The timestamp of the note, in steps. + */ public var stepTime(get, never):Float; public function get_stepTime():Float @@ -470,6 +476,10 @@ abstract SongNoteData(RawSongNoteData) return getStrumlineIndex(strumlineSize) == 0; } + /** + * If this is a hold note, this is the length of the hold note in milliseconds. + * @default 0 (not a hold note) + */ public var length(get, set):Float; function get_length():Float @@ -482,6 +492,10 @@ abstract SongNoteData(RawSongNoteData) return this.l = value; } + /** + * If this is a hold note, this is the length of the hold note in steps. + * @default 0 (not a hold note) + */ public var stepLength(get, set):Float; function get_stepLength():Float diff --git a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx index f671e98e1..0abee3715 100644 --- a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx +++ b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx @@ -67,8 +67,12 @@ class ChartEditorEventSprite extends FlxSprite // Push all the other events as frames. for (eventName in SongEventParser.listEventIds()) { + var exists:Bool = Assets.exists(Paths.image('ui/chart-editor/events/$eventName')); + if (!exists) continue; // No graphic for this event. + var frames:FlxAtlasFrames = Paths.getSparrowAtlas('ui/chart-editor/events/$eventName'); - if (frames == null) continue; // No graphic for this event. + if (frames == null) continue; // Could not load graphic for this event. + frames.parent.persist = true; for (frame in frames.frames) { @@ -140,19 +144,34 @@ class ChartEditorEventSprite extends FlxSprite } /** - * Return whether this note (or its parent) is currently visible. + * Return whether this event is currently visible. */ - public function isEventVisible(viewAreaBottom:Float, viewAreaTop:Float):Bool + public function isNoteVisible(viewAreaBottom:Float, viewAreaTop:Float):Bool { - var outsideViewArea = (this.y + this.height < viewAreaTop || this.y > viewAreaBottom); + // True if the note is above the view area. + var aboveViewArea = (this.y + this.height < viewAreaTop); - if (!outsideViewArea) - { - return true; - } + // True if the note is below the view area. + var belowViewArea = (this.y > viewAreaBottom); - // TODO: Check if this note's parent or child is visible. + return !aboveViewArea && !belowViewArea; + } - return false; + /** + * Return whether an event, if placed in the scene, would be visible. + */ + public static function wouldNoteBeVisible(viewAreaBottom:Float, viewAreaTop:Float, eventData:SongEventData, ?origin:FlxObject):Bool + { + var noteHeight:Float = ChartEditorState.GRID_SIZE; + var notePosY:Float = eventData.stepTime * ChartEditorState.GRID_SIZE; + if (origin != null) notePosY += origin.y; + + // True if the note is above the view area. + var aboveViewArea = (notePosY + noteHeight < viewAreaTop); + + // True if the note is below the view area. + var belowViewArea = (notePosY > viewAreaBottom); + + return !aboveViewArea && !belowViewArea; } } diff --git a/source/funkin/ui/debug/charting/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/ChartEditorHoldNoteSprite.hx new file mode 100644 index 000000000..38cdaffeb --- /dev/null +++ b/source/funkin/ui/debug/charting/ChartEditorHoldNoteSprite.hx @@ -0,0 +1,143 @@ +package funkin.ui.debug.charting; + +import funkin.play.notes.Strumline; +import funkin.data.notestyle.NoteStyleRegistry; +import flixel.FlxObject; +import flixel.FlxSprite; +import flixel.graphics.frames.FlxFramesCollection; +import flixel.graphics.frames.FlxTileFrames; +import flixel.math.FlxPoint; +import funkin.play.notes.SustainTrail; +import funkin.play.song.SongData.SongNoteData; + +/** + * A hold note sprite that can be used to display a note in a chart. + * Designed to be used and reused efficiently. Has no gameplay functionality. + */ +class ChartEditorHoldNoteSprite extends SustainTrail +{ + /** + * The ChartEditorState this note belongs to. + */ + public var parentState:ChartEditorState; + + public function new(parent:ChartEditorState) + { + var noteStyle = NoteStyleRegistry.instance.fetchDefault(); + + super(0, 100, noteStyle); + + this.parentState = parent; + + zoom = 1.0; + zoom *= noteStyle.fetchHoldNoteScale(); + zoom *= 0.7; + zoom *= ChartEditorState.GRID_SIZE / Strumline.STRUMLINE_SIZE; + + setup(); + } + + /** + * Set the height directly, to a value in pixels. + * @param h The desired height in pixels. + */ + public function setHeightDirectly(h:Float) + { + sustainLength = h / (getScrollSpeed() * Constants.PIXELS_PER_MS); + fullSustainLength = sustainLength; + } + + function setup():Void + { + strumTime = 999999999; + missedNote = false; + hitNote = false; + visible = true; + alpha = 1.0; + width = graphic.width / 8 * zoom; // amount of notes * 2 + } + + public override function revive():Void + { + super.revive(); + + setup(); + } + + /** + * Return whether this note is currently visible. + */ + public function isHoldNoteVisible(viewAreaBottom:Float, viewAreaTop:Float):Bool + { + // True if the note is above the view area. + var aboveViewArea = (this.y + this.height < viewAreaTop); + + // True if the note is below the view area. + var belowViewArea = (this.y > viewAreaBottom); + + return !aboveViewArea && !belowViewArea; + } + + /** + * Return whether a hold note, if placed in the scene, would be visible. + */ + public static function wouldHoldNoteBeVisible(viewAreaBottom:Float, viewAreaTop:Float, noteData:SongNoteData, ?origin:FlxObject):Bool + { + var noteHeight:Float = noteData.stepLength * ChartEditorState.GRID_SIZE; + var notePosY:Float = noteData.stepTime * ChartEditorState.GRID_SIZE; + if (origin != null) notePosY += origin.y; + + // True if the note is above the view area. + var aboveViewArea = (notePosY + noteHeight < viewAreaTop); + + // True if the note is below the view area. + var belowViewArea = (notePosY > viewAreaBottom); + + return !aboveViewArea && !belowViewArea; + } + + public function updateHoldNotePosition(?origin:FlxObject) + { + var cursorColumn:Int = this.noteData.data; + + if (cursorColumn < 0) cursorColumn = 0; + if (cursorColumn >= (ChartEditorState.STRUMLINE_SIZE * 2 + 1)) + { + cursorColumn = (ChartEditorState.STRUMLINE_SIZE * 2 + 1); + } + else + { + // Invert player and opponent columns. + if (cursorColumn >= ChartEditorState.STRUMLINE_SIZE) + { + cursorColumn -= ChartEditorState.STRUMLINE_SIZE; + } + else + { + cursorColumn += ChartEditorState.STRUMLINE_SIZE; + } + } + + this.x = cursorColumn * ChartEditorState.GRID_SIZE; + + // Notes far in the song will start far down, but the group they belong to will have a high negative offset. + if (this.noteData.stepTime >= 0) + { + // noteData.stepTime is a calculated value which accounts for BPM changes + var stepTime:Float = this.noteData.stepTime; + var roundedStepTime:Float = Math.floor(stepTime + 0.01); // Add epsilon to fix rounding issues + this.y = roundedStepTime * ChartEditorState.GRID_SIZE; + } + + this.x += ChartEditorState.GRID_SIZE / 2; + this.x -= this.width / 2; + + this.y += ChartEditorState.GRID_SIZE / 2; + + if (origin != null) + { + this.x += origin.x; + this.y += origin.y; + } + } +} diff --git a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx index aa0b97270..14ffa3a76 100644 --- a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx +++ b/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx @@ -204,15 +204,30 @@ class ChartEditorNoteSprite extends FlxSprite */ public function isNoteVisible(viewAreaBottom:Float, viewAreaTop:Float):Bool { - var outsideViewArea = (this.y + this.height < viewAreaTop || this.y > viewAreaBottom); + // True if the note is above the view area. + var aboveViewArea = (this.y + this.height < viewAreaTop); - if (!outsideViewArea) - { - return true; - } + // True if the note is below the view area. + var belowViewArea = (this.y > viewAreaBottom); - // TODO: Check if this note's parent or child is visible. + return !aboveViewArea && !belowViewArea; + } - return false; + /** + * Return whether a note, if placed in the scene, would be visible. + */ + public static function wouldNoteBeVisible(viewAreaBottom:Float, viewAreaTop:Float, noteData:SongNoteData, ?origin:FlxObject):Bool + { + var noteHeight:Float = ChartEditorState.GRID_SIZE; + var notePosY:Float = noteData.stepTime * ChartEditorState.GRID_SIZE; + if (origin != null) notePosY += origin.y; + + // True if the note is above the view area. + var aboveViewArea = (notePosY + noteHeight < viewAreaTop); + + // True if the note is below the view area. + var belowViewArea = (notePosY > viewAreaBottom); + + return !aboveViewArea && !belowViewArea; } } diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 41dbb0e0f..cda2b09df 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1,19 +1,10 @@ package funkin.ui.debug.charting; -import funkin.graphics.rendering.SustainTrail; -import funkin.util.SortUtil; -import funkin.data.notestyle.NoteStyleRegistry; -import funkin.ui.debug.charting.ChartEditorCommand; -import flixel.input.keyboard.FlxKey; -import funkin.input.TurboKeyHandler; -import haxe.ui.notifications.NotificationType; -import haxe.ui.notifications.NotificationManager; -import haxe.DynamicAccess; -import haxe.io.Path; import flixel.addons.display.FlxSliceSprite; import flixel.addons.display.FlxTiledSprite; import flixel.FlxSprite; import flixel.group.FlxSpriteGroup; +import flixel.input.keyboard.FlxKey; import flixel.math.FlxPoint; import flixel.math.FlxRect; import flixel.sound.FlxSound; @@ -22,10 +13,13 @@ import flixel.util.FlxSort; import flixel.util.FlxTimer; import funkin.audio.visualize.PolygonSpectogram; import funkin.audio.VoicesGroup; +import funkin.data.notestyle.NoteStyleRegistry; import funkin.input.Cursor; +import funkin.input.TurboKeyHandler; import funkin.modding.events.ScriptEvent; import funkin.play.HealthIcon; import funkin.play.notes.NoteSprite; +import funkin.play.notes.Strumline; import funkin.play.song.Song; import funkin.play.song.SongData.SongChartData; import funkin.play.song.SongData.SongDataParser; @@ -33,13 +27,18 @@ import funkin.play.song.SongData.SongEventData; import funkin.play.song.SongData.SongMetadata; import funkin.play.song.SongData.SongNoteData; import funkin.play.song.SongDataUtils; +import funkin.ui.debug.charting.ChartEditorCommand; import funkin.ui.debug.charting.ChartEditorThemeHandler.ChartEditorTheme; import funkin.ui.debug.charting.ChartEditorToolboxHandler.ChartEditorToolMode; import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.HaxeUIState; -import funkin.util.FileUtil; import funkin.util.DateUtil; +import funkin.util.FileUtil; import funkin.util.SerializerUtil; +import funkin.util.SortUtil; +import funkin.util.WindowUtil; +import haxe.DynamicAccess; +import haxe.io.Path; import haxe.ui.components.Label; import haxe.ui.components.Slider; import haxe.ui.containers.dialogs.Dialog; @@ -50,7 +49,8 @@ import haxe.ui.core.Component; import haxe.ui.core.Screen; import haxe.ui.events.DragEvent; import haxe.ui.events.UIEvent; -import funkin.util.WindowUtil; +import haxe.ui.notifications.NotificationManager; +import haxe.ui.notifications.NotificationType; import openfl.display.BitmapData; import openfl.geom.Rectangle; @@ -112,7 +112,12 @@ class ChartEditorState extends HaxeUIState /** * The height of the menu bar in the layout. */ - static final MENU_BAR_HEIGHT = 32; + static final MENU_BAR_HEIGHT:Int = 32; + + /** + * The height of the playbar in the layout. + */ + static final PLAYBAR_HEIGHT:Int = 48; /** * Duration to wait before autosaving the chart. @@ -946,7 +951,12 @@ class ChartEditorState extends HaxeUIState */ var renderedNotes:FlxTypedSpriteGroup<ChartEditorNoteSprite>; - var renderedHoldNotes:FlxTypedSpriteGroup<SustainTrail>; + /** + * 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>; /** * The sprite group containing the song events. @@ -1032,7 +1042,7 @@ class ChartEditorState extends HaxeUIState gridGhostNote = new ChartEditorNoteSprite(this); gridGhostNote.alpha = 0.6; - gridGhostNote.noteData = new SongNoteData(-1, -1, 0, ""); + gridGhostNote.noteData = new SongNoteData(0, 0, 0, ""); gridGhostNote.visible = false; add(gridGhostNote); @@ -1127,6 +1137,10 @@ class ChartEditorState extends HaxeUIState */ function buildNoteGroup():Void { + renderedHoldNotes = new FlxTypedSpriteGroup<ChartEditorHoldNoteSprite>(); + renderedHoldNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y); + add(renderedHoldNotes); + renderedNotes = new FlxTypedSpriteGroup<ChartEditorNoteSprite>(); renderedNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y); add(renderedNotes); @@ -1704,12 +1718,9 @@ class ChartEditorState extends HaxeUIState moveSongToScrollPosition(); } - // Cursor position snapped to the grid. - // The song position of the cursor, in steps. var cursorFractionalStep:Float = cursorY / GRID_SIZE / (16 / noteSnapQuant); - var cursorStep:Int = Std.int(Math.floor(cursorFractionalStep)); - var cursorMs:Float = Conductor.getStepTimeInMs(cursorStep); + var cursorMs:Float = Conductor.getStepTimeInMs(cursorFractionalStep); // The direction value for the column at the cursor. var cursorColumn:Int = Math.floor(cursorX / GRID_SIZE); if (cursorColumn < 0) cursorColumn = 0; @@ -2145,10 +2156,10 @@ class ChartEditorState extends HaxeUIState // Update for whether downscroll is enabled. renderedNotes.flipX = (isViewDownscroll); - // Calculate the view bounds. - var viewAreaTop:Float = this.scrollPositionInPixels - GRID_TOP_PAD; - var viewHeight:Float = (FlxG.height - MENU_BAR_HEIGHT); - var viewAreaBottom:Float = this.scrollPositionInPixels + viewHeight; + // Calculate the top and bottom of the view area. + var viewAreaTopPixels:Float = MENU_BAR_HEIGHT; + var visibleGridHeightPixels:Float = FlxG.height - MENU_BAR_HEIGHT - PLAYBAR_HEIGHT; // The area underneath the menu bar and playbar is not visible. + var viewAreaBottomPixels:Float = viewAreaTopPixels + visibleGridHeightPixels; // Remove notes that are no longer visible and list the ones that are. var displayedNoteData:Array<SongNoteData> = []; @@ -2156,7 +2167,7 @@ class ChartEditorState extends HaxeUIState { if (noteSprite == null || !noteSprite.exists || !noteSprite.visible) continue; - if (!noteSprite.isNoteVisible(viewAreaBottom, viewAreaTop)) + if (!noteSprite.isNoteVisible(viewAreaBottomPixels, viewAreaTopPixels)) { // This sprite is off-screen. // Kill the note sprite and recycle it. @@ -2168,18 +2179,6 @@ class ChartEditorState extends HaxeUIState // Kill the note sprite and recycle it. noteSprite.noteData = null; } - // else if (noteSprite.noteData.length > 0 && (noteSprite.parentNoteSprite == null && noteSprite.childNoteSprite == null)) - // { - // // Note was extended. - // // Kill the note sprite and recycle it. - // noteSprite.noteData = null; - // } - // else if (noteSprite.noteData.length == 0 && (noteSprite.parentNoteSprite != null || noteSprite.childNoteSprite != null)) - // { - // // Note was shortened. - // // Kill the note sprite and recycle it. - // noteSprite.noteData = null; - // } else { // Note is already displayed and should remain displayed. @@ -2190,13 +2189,42 @@ class ChartEditorState extends HaxeUIState } } + var displayedHoldNoteData:Array<SongNoteData> = []; + for (holdNoteSprite in renderedHoldNotes.members) + { + if (holdNoteSprite == null || !holdNoteSprite.exists || !holdNoteSprite.visible) continue; + + if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD)) + { + holdNoteSprite.kill(); + } + else if (currentSongChartNoteData.indexOf(holdNoteSprite.noteData) == -1 || holdNoteSprite.noteData.length == 0) + { + // This hold note was deleted. + // Kill the hold note sprite and recycle it. + holdNoteSprite.kill(); + } + else if (displayedHoldNoteData.indexOf(holdNoteSprite.noteData) != -1) + { + // This hold note is a duplicate. + // Kill the hold note sprite and recycle it. + holdNoteSprite.kill(); + } + else + { + displayedHoldNoteData.push(holdNoteSprite.noteData); + // Update the event sprite's position. + holdNoteSprite.updateHoldNotePosition(renderedNotes); + } + } + // Remove events that are no longer visible and list the ones that are. var displayedEventData:Array<SongEventData> = []; for (eventSprite in renderedEvents.members) { if (eventSprite == null || !eventSprite.exists || !eventSprite.visible) continue; - if (!eventSprite.isEventVisible(viewAreaBottom, viewAreaTop)) + if (!eventSprite.isEventVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD)) { // This sprite is off-screen. // Kill the event sprite and recycle it. @@ -2227,63 +2255,36 @@ class ChartEditorState extends HaxeUIState continue; } - // Get the position the note should be at. - var noteTimePixels:Float = noteData.stepTime * GRID_SIZE; - - // Make sure the note appears when scrolling up. - var modifiedViewAreaTop = viewAreaTop - GRID_SIZE; - - if (noteTimePixels < modifiedViewAreaTop || noteTimePixels > viewAreaBottom) continue; - - // Else, this note is visible and we need to render it! + if (!ChartEditorNoteSprite.wouldNoteBeVisible(viewAreaBottomPixels, viewAreaTopPixels, noteData, + renderedNotes)) continue; // Else, this note is visible and we need to render it! // Get a note sprite from the pool. // If we can reuse a deleted note, do so. // If a new note is needed, call buildNoteSprite. var noteSprite:ChartEditorNoteSprite = renderedNotes.recycle(() -> new ChartEditorNoteSprite(this)); + 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.x += renderedNotes.x; - noteSprite.y += renderedNotes.y; + noteSprite.updateNotePosition(renderedNotes); - // TODO: Replace this with SustainTrail. - if (noteSprite.noteData.length > 0) + // Add hold notes that are now visible (and not already displayed). + if (noteSprite.noteData.length > 0 && displayedHoldNoteData.indexOf(noteData) == -1) { - var holdNoteSprite:SustainTrail = renderedHoldNotes.recycle(() -> new SustainTrail(this)); + var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(() -> new ChartEditorHoldNoteSprite(this)); + trace('Creating new HoldNote... (${renderedHoldNotes.members.length})'); var noteLengthPixels:Float = noteSprite.noteData.stepLength * GRID_SIZE; - // If the note is a hold, we need to make sure it's long enough. - // var noteLengthSteps:Float = ; - // var lastNoteSprite:ChartEditorNoteSprite = noteSprite; - // - // while (noteLengthSteps > 0) - // { - // if (noteLengthSteps <= 1.0) - // { - // // Last note in the hold. - // // TODO: We may need to make it shorter and clip it visually. - // } - // - // var nextNoteSprite:ChartEditorNoteSprite = renderedNotes.recycle(ChartEditorNoteSprite); - // nextNoteSprite.parentState = this; - // nextNoteSprite.parentNoteSprite = lastNoteSprite; - // lastNoteSprite.childNoteSprite = nextNoteSprite; - // - // lastNoteSprite = nextNoteSprite; - // - // noteLengthSteps -= 1; - // } - // - // // Make sure the last note sprite shows the end cap properly. - // lastNoteSprite.childNoteSprite = null; + holdNoteSprite.noteData = noteSprite.noteData; + holdNoteSprite.noteDirection = noteSprite.noteData.getDirection(); - // var noteLengthPixels:Float = (noteLengthMs / Conductor.stepLengthMs + 1) * GRID_SIZE; - // add(new FlxSprite(noteSprite.x, noteSprite.y - renderedNotes.y + noteLengthPixels).makeGraphic(40, 2, 0xFFFF0000)); + holdNoteSprite.setHeightDirectly(noteLengthPixels); + + holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); } } @@ -2296,13 +2297,7 @@ class ChartEditorState extends HaxeUIState continue; } - // Get the position the event should be at. - var eventTimePixels:Float = eventData.stepTime * GRID_SIZE; - - // Make sure the event appears when scrolling up. - var modifiedViewAreaTop = viewAreaTop - GRID_SIZE; - - if (eventTimePixels < modifiedViewAreaTop || eventTimePixels > viewAreaBottom) continue; + if (!ChartEditorEventSprite.wouldEventBeVisible(viewAreaBottomPixels, viewAreaTopPixels, eventData, renderedNotes)) continue; // Else, this event is visible and we need to render it! @@ -2311,6 +2306,7 @@ class ChartEditorState extends HaxeUIState // If a new event is needed, call buildEventSprite. var eventSprite:ChartEditorEventSprite = renderedEvents.recycle(() -> new ChartEditorEventSprite(this), false, true); eventSprite.parentState = this; + trace('Creating new Event... (${renderedEvents.members.length})'); // The event sprite handles animation playback and positioning. eventSprite.eventData = eventData; @@ -2320,6 +2316,34 @@ class ChartEditorState extends HaxeUIState eventSprite.y += renderedEvents.y; } + // Add hold notes that have been made visible (but not their parents) + for (noteData in currentSongChartNoteData) + { + // Is the note a hold note? + if (noteData.length <= 0) continue; + + // Is the hold note rendered already? + if (displayedHoldNoteData.indexOf(noteData) != -1) continue; + + // Is the hold note offscreen? + if (!ChartEditorHoldNoteSprite.wouldHoldNoteBeVisible(viewAreaBottomPixels, viewAreaTopPixels, noteData, renderedHoldNotes)) continue; + + // Hold note should be rendered. + var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(() -> new ChartEditorHoldNoteSprite(this)); + trace('Creating new HoldNote... (${renderedHoldNotes.members.length})'); + + var noteLengthPixels:Float = noteData.stepLength * GRID_SIZE; + + holdNoteSprite.noteData = noteData; + holdNoteSprite.noteDirection = noteData.getDirection(); + + holdNoteSprite.setHeightDirectly(noteLengthPixels); + + holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); + + displayedHoldNoteData.push(noteData); + } + // Destroy all existing selection squares. for (member in renderedSelectionSquares.members) { @@ -2958,6 +2982,7 @@ class ChartEditorState extends HaxeUIState } // 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); if (gridSpectrogram != null) @@ -2974,6 +2999,7 @@ class ChartEditorState extends HaxeUIState * Loads an instrumental from an absolute file path, replacing the current instrumental. * * @param path The absolute path to the audio file. + * * @return Success or failure. */ public function loadInstrumentalFromPath(path:Path):Bool @@ -3114,7 +3140,7 @@ class ChartEditorState extends HaxeUIState for (metadata in rawSongMetadata) { var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation; - this.songMetadata.set(variation, metadata); + this.songMetadata.set(variation, Reflect.copy(metadata)); } this.songChartData = new Map<String, SongChartData>(); @@ -3154,7 +3180,8 @@ class ChartEditorState extends HaxeUIState function moveSongToScrollPosition():Void { // Update the songPosition in the Conductor. - Conductor.update(scrollPositionInMs); + var targetPos = scrollPositionInMs; + Conductor.update(targetPos); // Update the songPosition in the audio tracks. if (audioInstTrack != null) audioInstTrack.time = scrollPositionInMs + playheadPositionInMs; diff --git a/source/funkin/ui/stageBuildShit/StageBuilderState.hx b/source/funkin/ui/stageBuildShit/StageBuilderState.hx index de874e5ff..31a73ff8f 100644 --- a/source/funkin/ui/stageBuildShit/StageBuilderState.hx +++ b/source/funkin/ui/stageBuildShit/StageBuilderState.hx @@ -66,7 +66,7 @@ class StageBuilderState extends MusicBeatState // snd.addEventListener(SampleDataEvent.SAMPLE_DATA, sineShit); // snd.__buffer. - // snd = Assets.getSound(Paths.music('freakyMenu')); + // snd = Assets.getSound(Paths.music('freakyMenu/freakyMenu')); // for (thing in snd.load) // thing = Std.int(thing / 2); // snd.play(); diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index d5174e74f..a0063741b 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -122,6 +122,69 @@ class Constants */ public static final DEFAULT_VARIATION:String = 'default'; + /** + * The default intensity for camera zooms. + */ + public static final DEFAULT_ZOOM_INTENSITY:Float = 0.015; + + /** + * The default rate for camera zooms (in beats per zoom). + */ + public static final DEFAULT_ZOOM_RATE:Int = 4; + + /** + * The default BPM for charts, so things don't break if none is specified. + */ + public static final DEFAULT_BPM:Int = 100; + + /** + * Default numerator for the time signature. + */ + public static final DEFAULT_TIME_SIGNATURE_NUM:Int = 4; + + /** + * Default denominator for the time signature. + */ + public static final DEFAULT_TIME_SIGNATURE_DEN:Int = 4; + + /** + * TIMING + */ + // ============================== + + /** + * A magic number used when calculating scroll speed and note distances. + */ + public static final PIXELS_PER_MS:Float = 0.45; + + /** + * The maximum interval within which a note can be hit, in milliseconds. + */ + public static final HIT_WINDOW_MS:Float = 160; + + /** + * Constant for the number of seconds in a minute. + */ + public static final SECS_PER_MIN:Float = 60; + + /** + * Constant for the number of milliseconds in a second. + */ + public static final MS_PER_SEC:Float = 1000; + + /** + * The number of steps in one beat. + * + * Each beat represents ONE quarter note, so one step is one sixteenth note! + */ + public static final STEPS_PER_BEAT:Int = 4; + + /** + * All MP3 decoders introduce a playback delay of `528` samples, + * which at 44,100 Hz (samples per second) is ~12 ms. + */ + public static final MP3_DELAY_MS:Float = 528 / 44100 * Constants.MS_PER_SEC; + /** * HEALTH VALUES */ @@ -205,65 +268,12 @@ class Constants * OTHER */ // ============================== + + /** + * The separator between an asset library and the asset path. + */ public static final LIBRARY_SEPARATOR:String = ':'; - /** - * The number of seconds in a minute. - */ - public static final SECS_PER_MIN:Int = 60; - - /** - * The number of milliseconds in a second. - */ - public static final MS_PER_SEC:Int = 1000; - - /** - * The number of microseconds in a millisecond. - */ - public static final US_PER_MS:Int = 1000; - - /** - * The number of microseconds in a second. - */ - public static final US_PER_SEC:Int = US_PER_MS * MS_PER_SEC; - - /** - * The number of nanoseconds in a microsecond. - */ - public static final NS_PER_US:Int = 1000; - - /** - * The number of nanoseconds in a millisecond. - */ - public static final NS_PER_MS:Int = NS_PER_US * US_PER_MS; - - /** - * The number of nanoseconds in a second. - */ - public static final NS_PER_SEC:Int = NS_PER_US * US_PER_MS * MS_PER_SEC; - - /** - * All MP3 decoders introduce a playback delay of `528` samples, - * which at 44,100 Hz (samples per second) is ~12 ms. - */ - public static final MP3_DELAY_MS:Float = 528 / 44100 * MS_PER_SEC; - - /** - * The default BPM of the conductor. - */ - public static final DEFAULT_BPM:Float = 100.0; - - public static final DEFAULT_TIME_SIGNATURE_NUM:Int = 4; - - public static final DEFAULT_TIME_SIGNATURE_DEN:Int = 4; - - public static final STEPS_PER_BEAT:Int = 4; - - /** - * OTHER - */ - // ============================== - /** * The scale factor to use when increasing the size of pixel art graphics. */ @@ -276,14 +286,4 @@ class Constants public static final STRUMLINE_X_OFFSET:Float = 48; public static final STRUMLINE_Y_OFFSET:Float = 24; - - /** - * The default intensity for camera zooms. - */ - public static final DEFAULT_ZOOM_INTENSITY:Float = 0.015; - - /** - * The default rate for camera zooms (in beats per zoom). - */ - public static final DEFAULT_ZOOM_RATE:Int = 4; }