diff --git a/hmm.json b/hmm.json index 57c9378aa..0f06acaa7 100644 --- a/hmm.json +++ b/hmm.json @@ -49,7 +49,7 @@ "name": "haxeui-core", "type": "git", "dir": null, - "ref": "91ed8d7867c52af5ea2a9513204057d69ab33c8e", + "ref": "5d4ac180f85b39e72624f4b8d17925d91ebe4278", "url": "https://github.com/haxeui/haxeui-core" }, { diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index b8ded63da..f333b4072 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -45,6 +45,8 @@ class Conductor */ public static var songPosition(default, null):Float = 0; + public static var songPositionNoOffset(default, null):Float = 0; + /** * Beats per minute of the current song at the current time. */ @@ -144,13 +146,27 @@ class Conductor */ public static var currentStepTime(default, null):Float; + public static var currentStepTimeNoOffset(default, null):Float; + public static var beatHit(default, null):FlxSignal = new FlxSignal(); public static var stepHit(default, null):FlxSignal = new FlxSignal(); public static var lastSongPos:Float; - public static var visualOffset:Float = 0; - public static var audioOffset:Float = 0; - public static var offset:Float = 0; + + /** + * An offset tied to the current chart file to compensate for a delay in the instrumental. + */ + public static var instrumentalOffset:Float = 0; + + /** + * An offset tied to the file format of the audio file being played. + */ + public static var formatOffset:Float = 0; + + /** + * An offset set by the user to compensate for input lag. + */ + public static var inputOffset:Float = 0; public static var beatsPerMeasure(get, never):Float; @@ -200,15 +216,24 @@ class Conductor * @param songPosition The current position in the song in milliseconds. * Leave blank to use the FlxG.sound.music position. */ - public static function update(songPosition:Float = null) + public static function update(?songPosition:Float) { - if (songPosition == null) songPosition = (FlxG.sound.music != null) ? FlxG.sound.music.time + Conductor.offset : 0.0; + if (songPosition == null) + { + // Take into account instrumental and file format song offsets. + songPosition = (FlxG.sound.music != null) ? (FlxG.sound.music.time + instrumentalOffset + formatOffset) : 0.0; + } var oldBeat = currentBeat; var oldStep = currentStep; + // Set the song position we are at (for purposes of calculating note positions, etc). Conductor.songPosition = songPosition; + // Exclude the offsets we just included earlier. + // This is the "true" song position that the audio should be using. + Conductor.songPositionNoOffset = Conductor.songPosition - instrumentalOffset - formatOffset; + currentTimeChange = timeChanges[0]; for (i in 0...timeChanges.length) { @@ -230,6 +255,9 @@ class Conductor currentStep = Math.floor(currentStepTime); currentBeat = Math.floor(currentBeatTime); currentMeasure = Math.floor(currentMeasureTime); + + currentStepTimeNoOffset = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + + (songPositionNoOffset - currentTimeChange.timeStamp) / stepLengthMs, 6); } else { @@ -240,6 +268,8 @@ class Conductor currentStep = Math.floor(currentStepTime); currentBeat = Math.floor(currentBeatTime); currentMeasure = Math.floor(currentMeasureTime); + + currentStepTimeNoOffset = FlxMath.roundDecimal((songPositionNoOffset / stepLengthMs), 4); } // FlxSignals are really cool. diff --git a/source/funkin/audio/VoicesGroup.hx b/source/funkin/audio/VoicesGroup.hx index 6d61e6481..8c95eb3eb 100644 --- a/source/funkin/audio/VoicesGroup.hx +++ b/source/funkin/audio/VoicesGroup.hx @@ -11,12 +11,22 @@ class VoicesGroup extends SoundGroup /** * Control the volume of only the sounds in the player group. */ - public var playerVolume(default, set):Float; + public var playerVolume(default, set):Float = 1.0; /** * Control the volume of only the sounds in the opponent group. */ - public var opponentVolume(default, set):Float; + public var opponentVolume(default, set):Float = 1.0; + + /** + * Set the time offset for the player's vocal track. + */ + public var playerVoicesOffset(default, set):Float = 0.0; + + /** + * Set the time offset for the opponent's vocal track. + */ + public var opponentVoicesOffset(default, set):Float = 0.0; public function new() { @@ -42,6 +52,57 @@ class VoicesGroup extends SoundGroup return playerVolume = volume; } + override function set_time(time:Float):Float + { + forEachAlive(function(snd) { + // account for different offsets per sound? + snd.time = time; + }); + + playerVoices.forEachAlive(function(voice:FlxSound) { + voice.time -= playerVoicesOffset; + }); + opponentVoices.forEachAlive(function(voice:FlxSound) { + voice.time -= opponentVoicesOffset; + }); + + return time; + } + + function set_playerVoicesOffset(offset:Float):Float + { + playerVoices.forEachAlive(function(voice:FlxSound) { + voice.time += playerVoicesOffset; + voice.time -= offset; + }); + return playerVoicesOffset = offset; + } + + function set_opponentVoicesOffset(offset:Float):Float + { + opponentVoices.forEachAlive(function(voice:FlxSound) { + voice.time += opponentVoicesOffset; + voice.time -= offset; + }); + return opponentVoicesOffset = offset; + } + + public override function update(elapsed:Float):Void + { + forEachAlive(function(snd) { + if (snd.time < 0) + { + // Sync the time without calling update(). + // time gets reset if it's negative. + snd.time += elapsed * 1000; + } + else + { + snd.update(elapsed); + } + }); + } + /** * Add a voice to the opponent group. */ diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 663dcc9bd..c0fd96855 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -34,6 +34,12 @@ class SongMetadata @:default(false) public var looped:Bool; + /** + * Instrumental and vocal offsets. Optional, defaults to 0. + */ + @:optional + public var offsets:SongOffsets; + /** * Data relating to the song's gameplay. */ @@ -59,6 +65,7 @@ class SongMetadata this.artist = artist; this.timeFormat = 'ms'; this.divisions = null; + this.offsets = new SongOffsets(); this.timeChanges = [new SongTimeChange(0, 100)]; this.looped = false; this.playData = new SongPlayData(); @@ -196,6 +203,90 @@ class SongTimeChange } } +/** + * Offsets to apply to the song's instrumental and vocals, relative to the chart. + * These are intended to correct for issues with the chart, or with the song's audio (for example a 10ms delay before the song starts). + * This is independent of the offsets applied in the user's settings, which are applied after these offsets and intended to correct for the user's hardware. + */ +class SongOffsets +{ + /** + * The offset, in milliseconds, to apply to the song's instrumental relative to the chart. + * For example, setting this to `-10.0` will start the instrumental 10ms earlier than the chart. + * + * Setting this to `-5000.0` means the chart start 5 seconds into the song. + * Setting this to `5000.0` means there will be 5 seconds of silence before the song starts. + */ + @:optional + @:default(0) + public var instrumental:Float; + + /** + * Apply different offsets to different alternate instrumentals. + */ + @:optional + @:default([]) + public var altInstrumentals:Map; + + /** + * The offset, in milliseconds, to apply to the song's vocals, relative to the chart. + * These are applied ON TOP OF the instrumental offset. + */ + @:optional + @:default([]) + public var vocals:Map; + + public function new(instrumental:Float = 0.0, ?altInstrumentals:Map, ?vocals:Map) + { + this.instrumental = instrumental; + this.altInstrumentals = altInstrumentals == null ? new Map() : altInstrumentals; + this.vocals = vocals == null ? new Map() : vocals; + } + + public function getInstrumentalOffset(?instrumental:String):Float + { + if (instrumental == null || instrumental == '') return this.instrumental; + + if (!this.altInstrumentals.exists(instrumental)) return this.instrumental; + + return this.altInstrumentals.get(instrumental); + } + + public function setInstrumentalOffset(value:Float, ?instrumental:String):Float + { + if (instrumental == null || instrumental == '') + { + this.instrumental = value; + } + else + { + this.altInstrumentals.set(instrumental, value); + } + return value; + } + + public function getVocalOffset(charId:String):Float + { + if (!this.vocals.exists(charId)) return 0.0; + + return this.vocals.get(charId); + } + + public function setVocalOffset(charId:String, value:Float):Float + { + this.vocals.set(charId, value); + return value; + } + + /** + * Produces a string representation suitable for debugging. + */ + public function toString():String + { + return 'SongOffsets(${this.instrumental}ms, ${this.altInstrumentals}, ${this.vocals})'; + } +} + /** * Metadata for a song only used for the music. * For example, the menu music. @@ -309,6 +400,7 @@ class SongPlayData * The difficulty ratings for this song as displayed in Freeplay. * Key is a difficulty ID or `default`. */ + @:optional @:default(['default' => 1]) public var ratings:Map; diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx index 9a9f758b1..4ae4b1426 100644 --- a/source/funkin/data/song/SongDataUtils.hx +++ b/source/funkin/data/song/SongDataUtils.hx @@ -32,10 +32,7 @@ class SongDataUtils return new SongNoteData(time, data, length, kind); }; - trace(notes); - trace(notes[0]); var result = [for (i in 0...notes.length) offsetNote(notes[i])]; - trace(result); return result; } @@ -54,6 +51,36 @@ class SongDataUtils }); } + /** + * Given an array of SongNoteData objects, return a new array of SongNoteData objects + * which excludes any notes whose timestamps are outside of the given range. + * @param notes The notes to modify. + * @param startTime The start of the range in milliseconds. + * @param endTime The end of the range in milliseconds. + * @return The filtered array of notes. + */ + public static function clampSongNoteData(notes:Array, startTime:Float, endTime:Float):Array + { + return notes.filter(function(note:SongNoteData):Bool { + return note.time >= startTime && note.time <= endTime; + }); + } + + /** + * Given an array of SongEventData objects, return a new array of SongEventData objects + * which excludes any events whose timestamps are outside of the given range. + * @param events The events to modify. + * @param startTime The start of the range in milliseconds. + * @param endTime The end of the range in milliseconds. + * @return The filtered array of events. + */ + public static function clampSongEventData(events:Array, startTime:Float, endTime:Float):Array + { + return events.filter(function(event:SongEventData):Bool { + return event.time >= startTime && event.time <= endTime; + }); + } + /** * Return a new array without a certain subset of notes from an array of SongNoteData objects. * Does not mutate the original array. diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index 8e0f4577d..850654eb7 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -19,7 +19,7 @@ class SongRegistry extends BaseRegistry * Handle breaking changes by incrementing this value * and adding migration to the `migrateStageData()` function. */ - public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.0"; + public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.1"; public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.2.x"; diff --git a/source/funkin/data/song/migrator/SongData_v2_1_0.hx b/source/funkin/data/song/migrator/SongData_v2_1_0.hx index 192c440e0..1a61184ed 100644 --- a/source/funkin/data/song/migrator/SongData_v2_1_0.hx +++ b/source/funkin/data/song/migrator/SongData_v2_1_0.hx @@ -16,6 +16,8 @@ class SongMetadata_v2_1_0 */ public var playData:SongPlayData_v2_1_0; + // In metadata `v2.2.1`, `SongOffsets` was added. + // var offsets:SongOffsets; // ========== // UNMODIFIED VALUES // ========== diff --git a/source/funkin/input/Cursor.hx b/source/funkin/input/Cursor.hx index c609c9e30..b4bf43808 100644 --- a/source/funkin/input/Cursor.hx +++ b/source/funkin/input/Cursor.hx @@ -142,12 +142,14 @@ class Cursor }; static var assetCursorCell:Null = null; - // DESIRED CURSOR: Resize NS (vertical) - // DESIRED CURSOR: Resize EW (horizontal) - // DESIRED CURSOR: Resize NESW (diagonal) - // DESIRED CURSOR: Resize NWSE (diagonal) - // DESIRED CURSOR: Help (Cursor with question mark) - // DESIRED CURSOR: Menu (Cursor with menu icon) + public static final CURSOR_SCROLL_PARAMS:CursorParams = + { + graphic: "assets/images/cursor/cursor-scroll.png", + scale: 0.2, + offsetX: -15, + offsetY: -15, + }; + static var assetCursorScroll:Null = null; static function set_cursorMode(value:Null):Null { @@ -304,6 +306,18 @@ class Cursor applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS); } + case Scroll: + if (assetCursorScroll == null) + { + var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_SCROLL_PARAMS.graphic); + assetCursorScroll = bitmapData; + applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS); + } + else + { + applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS); + } + default: setCursorGraphic(null); } @@ -487,6 +501,21 @@ class Cursor applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS); } + case Scroll: + if (assetCursorScroll == null) + { + var future:Future = Assets.loadBitmapData(CURSOR_SCROLL_PARAMS.graphic); + future.onComplete(function(bitmapData:BitmapData) { + assetCursorScroll = bitmapData; + applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS); + }); + future.onError(onCursorError.bind(Scroll)); + } + else + { + applyCursorParams(assetCursorScroll, CURSOR_SCROLL_PARAMS); + } + default: loadCursorGraphic(null); } @@ -517,6 +546,7 @@ class Cursor registerHaxeUICursor('zoom-out', CURSOR_ZOOM_OUT_PARAMS); registerHaxeUICursor('crosshair', CURSOR_CROSSHAIR_PARAMS); registerHaxeUICursor('cell', CURSOR_CELL_PARAMS); + registerHaxeUICursor('scroll', CURSOR_SCROLL_PARAMS); } public static function registerHaxeUICursor(id:String, params:CursorParams):Void @@ -539,6 +569,7 @@ enum CursorMode ZoomOut; Crosshair; Cell; + Scroll; } /** diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 7d20f4697..a26addbe6 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -179,6 +179,13 @@ class PlayState extends MusicBeatSubState */ public var songScore:Int = 0; + /** + * Start at this point in the song once the countdown is done. + * For example, if `startTimestamp` is `30000`, the song will start at the 30 second mark. + * Used for chart playtesting or practice. + */ + public var startTimestamp:Float = 0.0; + /** * An empty FlxObject contained in the scene. * The current gameplay camera will always follow this object. Tween its position to move the camera smoothly. @@ -254,10 +261,6 @@ class PlayState extends MusicBeatSubState */ public var disableKeys:Bool = false; - public var startTimestamp:Float = 0.0; - - var overrideMusic:Bool = false; - public var isSubState(get, never):Bool; function get_isSubState():Bool @@ -317,6 +320,18 @@ class PlayState extends MusicBeatSubState */ var skipHeldTimer:Float = 0; + /** + * Whether the PlayState was started with instrumentals and vocals already provided. + * Used by the chart editor to prevent replacing the music. + */ + var overrideMusic:Bool = false; + + /** + * After the song starts, the song offset may dictate we wait before the instrumental starts. + * This variable represents that delay, and is subtracted from until it reaches 0, before calling `music.play()` + */ + var songStartDelay:Float = 0.0; + /** * Forcibly disables all update logic while the game moves back to the Menu state. * This is used only when a critical error occurs and the game absolutely cannot continue. @@ -468,6 +483,8 @@ class PlayState extends MusicBeatSubState var generatedMusic:Bool = false; var perfectMode:Bool = false; + static final BACKGROUND_COLOR:FlxColor = FlxColor.MAGENTA; + /** * Instantiate a new PlayState. * @param params The parameters used to initialize the PlayState. @@ -551,6 +568,7 @@ class PlayState extends MusicBeatSubState // Prepare the Conductor. Conductor.forceBPM(null); + Conductor.instrumentalOffset = currentChart.offsets.getInstrumentalOffset(); Conductor.mapTimeChanges(currentChart.timeChanges); Conductor.update((Conductor.beatLengthMs * -5) + startTimestamp); @@ -631,6 +649,24 @@ class PlayState extends MusicBeatSubState initialized = true; } + public override function draw():Void + { + // if (FlxG.renderBlit) + // { + // camGame.fill(BACKGROUND_COLOR); + // } + // else if (FlxG.renderTile) + // { + // FlxG.log.warn("PlayState background not displayed properly on tile renderer!"); + // } + // else + // { + // FlxG.log.warn("PlayState background not displayed properly, unknown renderer!"); + // } + + super.draw(); + } + function assertChartExists():Bool { // Returns null if the song failed to load or doesn't have the selected difficulty. @@ -699,8 +735,8 @@ class PlayState extends MusicBeatSubState // Reset music properly. + FlxG.sound.music.time = Math.max(0, startTimestamp - Conductor.instrumentalOffset); FlxG.sound.music.pause(); - FlxG.sound.music.time = (startTimestamp); if (!overrideMusic) { @@ -751,17 +787,40 @@ class PlayState extends MusicBeatSubState if (isInCountdown) { Conductor.update(Conductor.songPosition + elapsed * 1000); - if (Conductor.songPosition >= startTimestamp) startSong(); + if (Conductor.songPosition >= (startTimestamp)) startSong(); } } else { - // DO NOT FORGET TO REMOVE THE HARDCODE! WHEN I MAKE BETTER OFFSET SYSTEM! + if (Constants.EXT_SOUND == 'mp3') + { + Conductor.formatOffset = Constants.MP3_DELAY_MS; + } + else + { + Conductor.formatOffset = 0.0; + } - // :nerd: um ackshually it's not 13 it's 11.97278911564 - if (Constants.EXT_SOUND == 'mp3') Conductor.offset = Constants.MP3_DELAY_MS; - - Conductor.update(); + if (songStartDelay > 0) + { + // Code to handle the song not starting yet (positive instrumental offset in metadata). + // Wait for offset to elapse before actually hitting play on the instrumental. + songStartDelay -= elapsed * 1000; + if (songStartDelay <= 0) + { + FlxG.sound.music.play(); + Conductor.update(); // Normal conductor update. + } + else + { + // Make beat events still happen. + Conductor.update(Conductor.songPosition + elapsed * 1000); + } + } + else + { + Conductor.update(); // Normal conductor update. + } if (!isGamePaused) { @@ -1137,12 +1196,12 @@ class PlayState extends MusicBeatSubState 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)) + && (Math.abs(FlxG.sound.music.time - (Conductor.songPositionNoOffset)) > 200 + || Math.abs(vocals.checkSyncError(Conductor.songPositionNoOffset)) > 200)) { trace("VOCALS NEED RESYNC"); - if (vocals != null) trace(vocals.checkSyncError(Conductor.songPosition - Conductor.offset)); - trace(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)); + if (vocals != null) trace(vocals.checkSyncError(Conductor.songPositionNoOffset)); + trace(FlxG.sound.music.time - (Conductor.songPositionNoOffset)); resyncVocals(); } @@ -1258,6 +1317,7 @@ class PlayState extends MusicBeatSubState function initCameras():Void { camGame = new SwagCamera(); + 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(); @@ -1684,12 +1744,28 @@ class PlayState extends MusicBeatSubState } FlxG.sound.music.onComplete = endSong; - FlxG.sound.music.play(); - FlxG.sound.music.time = startTimestamp; + // A negative instrumental offset means the song skips the first few milliseconds of the track. + // This just gets added into the startTimestamp behavior so we don't need to do anything extra. + FlxG.sound.music.time = startTimestamp - Conductor.instrumentalOffset; + trace('Playing vocals...'); add(vocals); - vocals.play(); - resyncVocals(); + + if (FlxG.sound.music.time < 0) + { + // A positive instrumentalOffset means Conductor.songPosition will still be negative after the countdown elapses. + trace('POSITIVE OFFSET: Waiting to start song...'); + songStartDelay = Math.abs(FlxG.sound.music.time); + FlxG.sound.music.time = 0; + FlxG.sound.music.pause(); + vocals.pause(); + } + else + { + FlxG.sound.music.play(); + vocals.play(); + resyncVocals(); + } #if discord_rpc // Updating Discord Rich Presence (with Time Left) @@ -1698,7 +1774,7 @@ class PlayState extends MusicBeatSubState if (startTimestamp > 0) { - FlxG.sound.music.time = startTimestamp; + FlxG.sound.music.time = startTimestamp - Conductor.instrumentalOffset; handleSkippedNotes(); } } @@ -1710,13 +1786,13 @@ class PlayState extends MusicBeatSubState { if (_exiting || vocals == null) return; - // Skip this if the music is paused (GameOver, Pause menu, etc.) + // Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.) if (!FlxG.sound.music.playing) return; + if (songStartDelay > 0) return; vocals.pause(); FlxG.sound.music.play(); - Conductor.update(); vocals.time = FlxG.sound.music.time; vocals.play(false, FlxG.sound.music.time); diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 3d7903724..1cba42f60 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -1,21 +1,22 @@ package funkin.play.song; -import funkin.util.SortUtil; import flixel.sound.FlxSound; -import openfl.utils.Assets; -import funkin.modding.events.ScriptEvent; -import funkin.modding.IScriptedClass; import funkin.audio.VoicesGroup; -import funkin.data.song.SongRegistry; +import funkin.data.IRegistryEntry; +import funkin.data.song.SongData.SongCharacterData; import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongEventData; -import funkin.data.song.SongData.SongNoteData; -import funkin.data.song.SongRegistry; import funkin.data.song.SongData.SongMetadata; -import funkin.data.song.SongData.SongCharacterData; +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongOffsets; import funkin.data.song.SongData.SongTimeChange; import funkin.data.song.SongData.SongTimeFormat; -import funkin.data.IRegistryEntry; +import funkin.data.song.SongRegistry; +import funkin.data.song.SongRegistry; +import funkin.modding.events.ScriptEvent; +import funkin.modding.IScriptedClass; +import funkin.util.SortUtil; +import openfl.utils.Assets; /** * This is a data structure managing information about the current song. @@ -172,6 +173,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = null; public var looped:Bool = false; + public var offsets:SongOffsets = new SongOffsets(); public var generatedBy:String = SongRegistry.DEFAULT_GENERATEDBY; public var timeChanges:Array = []; @@ -542,6 +545,9 @@ class SongDifficulty } } + result.playerVoicesOffset = offsets.getVocalOffset(characters.player); + result.opponentVoicesOffset = offsets.getVocalOffset(characters.opponent); + return result; } } diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index db1f2b69a..810d0fd93 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -105,7 +105,7 @@ abstract Save(RawSaveData) theme: ChartEditorTheme.Light, playtestStartTime: false, downscroll: false, - metronomeEnabled: true, + metronomeVolume: 1.0, hitsoundsEnabledPlayer: true, hitsoundsEnabledOpponent: true, instVolume: 1.0, @@ -279,21 +279,38 @@ abstract Save(RawSaveData) return this.optionsChartEditor.theme; } - public var chartEditorMetronomeEnabled(get, set):Bool; + public var chartEditorMetronomeVolume(get, set):Float; - function get_chartEditorMetronomeEnabled():Bool + function get_chartEditorMetronomeVolume():Float { - if (this.optionsChartEditor.metronomeEnabled == null) this.optionsChartEditor.metronomeEnabled = true; + if (this.optionsChartEditor.metronomeVolume == null) this.optionsChartEditor.metronomeVolume = 1.0; - return this.optionsChartEditor.metronomeEnabled; + return this.optionsChartEditor.metronomeVolume; } - function set_chartEditorMetronomeEnabled(value:Bool):Bool + function set_chartEditorMetronomeVolume(value:Float):Float { // Set and apply. - this.optionsChartEditor.metronomeEnabled = value; + this.optionsChartEditor.metronomeVolume = value; flush(); - return this.optionsChartEditor.metronomeEnabled; + return this.optionsChartEditor.metronomeVolume; + } + + public var chartEditorHitsoundVolume(get, set):Float; + + function get_chartEditorHitsoundVolume():Float + { + if (this.optionsChartEditor.hitsoundVolume == null) this.optionsChartEditor.hitsoundVolume = 1.0; + + return this.optionsChartEditor.hitsoundVolume; + } + + function set_chartEditorHitsoundVolume(value:Float):Float + { + // Set and apply. + this.optionsChartEditor.hitsoundVolume = value; + flush(); + return this.optionsChartEditor.hitsoundVolume; } public var chartEditorHitsoundsEnabledPlayer(get, set):Bool; @@ -981,10 +998,16 @@ typedef SaveDataChartEditorOptions = var ?downscroll:Bool; /** - * Metronome sounds in the Chart Editor. - * @default `true` + * Metronome volume in the Chart Editor. + * @default `1.0` */ - var ?metronomeEnabled:Bool; + var ?metronomeVolume:Float; + + /** + * Hitsound volume in the Chart Editor. + * @default `1.0` + */ + var ?hitsoundVolume:Float; /** * If true, playtest songs from the current position in the Chart Editor. diff --git a/source/funkin/ui/MusicBeatState.hx b/source/funkin/ui/MusicBeatState.hx index 4e0e19d5e..7bdf8689c 100644 --- a/source/funkin/ui/MusicBeatState.hx +++ b/source/funkin/ui/MusicBeatState.hx @@ -84,6 +84,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler { // Display Conductor info in the watch window. FlxG.watch.addQuick("songPosition", Conductor.songPosition); + FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0); FlxG.watch.addQuick("bpm", Conductor.bpm); FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime); FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime); diff --git a/source/funkin/ui/MusicBeatSubState.hx b/source/funkin/ui/MusicBeatSubState.hx index 64df6ee71..9dd755b62 100644 --- a/source/funkin/ui/MusicBeatSubState.hx +++ b/source/funkin/ui/MusicBeatSubState.hx @@ -66,6 +66,7 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl // Display Conductor info in the watch window. FlxG.watch.addQuick("songPosition", Conductor.songPosition); + FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0); FlxG.watch.addQuick("bpm", Conductor.bpm); FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime); FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime); diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 28dbf54f3..3b1f82768 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -92,6 +92,7 @@ import haxe.ui.backend.flixel.UIRuntimeState; import haxe.ui.backend.flixel.UIState; import haxe.ui.components.DropDown; import haxe.ui.components.Label; +import haxe.ui.components.Button; import haxe.ui.components.NumberStepper; import haxe.ui.components.Slider; import haxe.ui.components.TextField; @@ -100,6 +101,7 @@ import haxe.ui.containers.Frame; import haxe.ui.containers.menus.Menu; import haxe.ui.containers.menus.MenuBar; import haxe.ui.containers.menus.MenuItem; +import haxe.ui.containers.menus.MenuCheckBox; import haxe.ui.containers.TreeView; import haxe.ui.containers.TreeViewNode; import haxe.ui.core.Component; @@ -615,9 +617,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Audio /** - * Whether to play a metronome sound while the playhead is moving. + * Whether to play a metronome sound while the playhead is moving, and what volume. */ - var isMetronomeEnabled:Bool = true; + var metronomeVolume:Float = 1.0; + + /** + * The volume to play hitsounds at. + */ + var hitsoundVolume:Float = 1.0; /** * Whether hitsounds are enabled for the player. @@ -665,6 +672,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var currentScrollEase:Null; + /** + * The position where the user middle clicked to place a scroll anchor. + * Scroll each frame with speed based on the distance between the mouse and the scroll anchor. + * `null` if no scroll anchor is present. + */ + var scrollAnchorScreenPos:Null = null; + // Note Placement /** @@ -1249,98 +1263,257 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var playbarHeadLayout:Null = null; // NOTE: All the components below are automatically assigned via HaxeUI macros. + /** * The menubar at the top of the screen. */ - // var menubar:MenuBar; + var menubar:MenuBar; + /** * The `File -> New Chart` menu item. */ - // var menubarItemNewChart:MenuItem; + var menubarItemNewChart:MenuItem; + /** * The `File -> Open Chart` menu item. */ - // var menubarItemOpenChart:MenuItem; + var menubarItemOpenChart:MenuItem; + /** * The `File -> Open Recent` menu. */ - // var menubarOpenRecent:Menu; + var menubarOpenRecent:Menu; + /** * The `File -> Save Chart` menu item. */ - // var menubarItemSaveChart:MenuItem; + var menubarItemSaveChart:MenuItem; + /** * The `File -> Save Chart As` menu item. */ - // var menubarItemSaveChartAs:MenuItem; + var menubarItemSaveChartAs:MenuItem; + /** * The `File -> Preferences` menu item. */ - // var menubarItemPreferences:MenuItem; + var menubarItemPreferences:MenuItem; + /** * The `File -> Exit` menu item. */ - // var menubarItemExit:MenuItem; + var menubarItemExit:MenuItem; + /** * The `Edit -> Undo` menu item. */ - // var menubarItemUndo:MenuItem; + var menubarItemUndo:MenuItem; + /** * The `Edit -> Redo` menu item. */ - // var menubarItemRedo:MenuItem; + var menubarItemRedo:MenuItem; + /** * The `Edit -> Cut` menu item. */ - // var menubarItemCut:MenuItem; + var menubarItemCut:MenuItem; + /** * The `Edit -> Copy` menu item. */ - // var menubarItemCopy:MenuItem; + var menubarItemCopy:MenuItem; + /** * The `Edit -> Paste` menu item. */ - // var menubarItemPaste:MenuItem; + var menubarItemPaste:MenuItem; + /** * The `Edit -> Paste Unsnapped` menu item. */ - // var menubarItemPasteUnsnapped:MenuItem; + var menubarItemPasteUnsnapped:MenuItem; + /** * The `Edit -> Delete` menu item. */ - // var menubarItemDelete:MenuItem; + var menubarItemDelete:MenuItem; + + /** + * The `Edit -> Flip Notes` menu item. + */ + var menubarItemFlipNotes:MenuItem; + + /** + * The `Edit -> Select All` menu item. + */ + var menubarItemSelectAll:MenuItem; + + /** + * The `Edit -> Select Inverse` menu item. + */ + var menubarItemSelectInverse:MenuItem; + + /** + * The `Edit -> Select None` menu item. + */ + var menubarItemSelectNone:MenuItem; + + /** + * The `Edit -> Select Region` menu item. + */ + var menubarItemSelectRegion:MenuItem; + + /** + * The `Edit -> Select Before Cursor` menu item. + */ + var menubarItemSelectBeforeCursor:MenuItem; + + /** + * The `Edit -> Select After Cursor` menu item. + */ + var menubarItemSelectAfterCursor:MenuItem; + + /** + * The `Edit -> Decrease Note Snap Precision` menu item. + */ + var menuBarItemNoteSnapDecrease:MenuItem; + + /** + * The `Edit -> Decrease Note Snap Precision` menu item. + */ + var menuBarItemNoteSnapIncrease:MenuItem; + + /** + * The `View -> Downscroll` menu item. + */ + var menubarItemDownscroll:MenuCheckBox; + + /** + * The `View -> Increase Difficulty` menu item. + */ + var menubarItemDifficultyUp:MenuItem; + + /** + * The `View -> Decrease Difficulty` menu item. + */ + var menubarItemDifficultyDown:MenuItem; + + /** + * The `Audio -> Play/Pause` menu item. + */ + var menubarItemPlayPause:MenuItem; + + /** + * The `Audio -> Load Instrumental` menu item. + */ + var menubarItemLoadInstrumental:MenuItem; + + /** + * The `Audio -> Load Vocals` menu item. + */ + var menubarItemLoadVocals:MenuItem; + + /** + * The `Audio -> Metronome Volume` label. + */ + var menubarLabelVolumeMetronome:Label; + + /** + * The `Audio -> Metronome Volume` slider. + */ + var menubarItemVolumeMetronome:Slider; + + /** + * The `Audio -> Enable Player Hitsounds` menu checkbox. + */ + var menubarItemPlayerHitsounds:MenuCheckBox; + + /** + * The `Audio -> Enable Opponent Hitsounds` menu checkbox. + */ + var menubarItemOpponentHitsounds:MenuCheckBox; + + /** + * The `Audio -> Hitsound Volume` label. + */ + var menubarLabelVolumeHitsounds:Label; + + /** + * The `Audio -> Hitsound Volume` slider. + */ + var menubarItemVolumeHitsounds:Slider; + + /** + * The `Audio -> Instrumental Volume` label. + */ + var menubarLabelVolumeInstrumental:Label; + + /** + * The `Audio -> Instrumental Volume` slider. + */ + var menubarItemVolumeInstrumental:Slider; + + /** + * The `Audio -> Vocal Volume` label. + */ + var menubarLabelVolumeVocals:Label; + + /** + * The `Audio -> Vocal Volume` slider. + */ + var menubarItemVolumeVocals:Slider; + + /** + * The `Audio -> Playback Speed` label. + */ + var menubarLabelPlaybackSpeed:Label; + + /** + * The `Audio -> Playback Speed` slider. + */ + var menubarItemPlaybackSpeed:Slider; + /** * The label by the playbar telling the song position. */ - // var playbarSongPos:Label; + var playbarSongPos:Label; + /** * The label by the playbar telling the song time remaining. */ - // var playbarSongRemaining:Label; + var playbarSongRemaining:Label; + /** * The label by the playbar telling the note snap. */ - // var playbarNoteSnap:Label; + var playbarNoteSnap:Label; + /** * The button by the playbar to jump to the start of the song. */ - // var playbarStart:Button; + var playbarStart:Button; + /** * The button by the playbar to jump backwards in the song. */ - // var playbarBack:Button; + var playbarBack:Button; + /** * The button by the playbar to play or pause the song. */ - // var playbarPlay:Button; + var playbarPlay:Button; + /** * The button by the playbar to jump forwards in the song. */ - // var playbarForward:Button; + var playbarForward:Button; + /** * The button by the playbar to jump to the end of the song. */ - // var playbarEnd:Button; + var playbarEnd:Button; + /** * RENDER OBJECTS */ @@ -1682,7 +1855,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState isViewDownscroll = save.chartEditorDownscroll; playtestStartTime = save.chartEditorPlaytestStartTime; currentTheme = save.chartEditorTheme; - isMetronomeEnabled = save.chartEditorMetronomeEnabled; + metronomeVolume = save.chartEditorMetronomeVolume; + hitsoundVolume = save.chartEditorHitsoundVolume; hitsoundsEnabledPlayer = save.chartEditorHitsoundsEnabledPlayer; hitsoundsEnabledOpponent = save.chartEditorHitsoundsEnabledOpponent; @@ -1710,7 +1884,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState save.chartEditorDownscroll = isViewDownscroll; save.chartEditorPlaytestStartTime = playtestStartTime; save.chartEditorTheme = currentTheme; - save.chartEditorMetronomeEnabled = isMetronomeEnabled; + save.chartEditorMetronomeVolume = metronomeVolume; + save.chartEditorHitsoundVolume = hitsoundVolume; save.chartEditorHitsoundsEnabledPlayer = hitsoundsEnabledPlayer; save.chartEditorHitsoundsEnabledOpponent = hitsoundsEnabledOpponent; @@ -2282,8 +2457,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemLoadInstrumental.onClick = _ -> this.openUploadInstDialog(true); menubarItemLoadVocals.onClick = _ -> this.openUploadVocalsDialog(true); - menubarItemMetronomeEnabled.onChange = event -> isMetronomeEnabled = event.value; - menubarItemMetronomeEnabled.selected = isMetronomeEnabled; + menubarItemVolumeMetronome.onChange = event -> { + var volume:Float = (event?.value ?? 0) / 100.0; + metronomeVolume = volume; + menubarLabelVolumeMetronome.text = 'Metronome - ${Std.int(event.value)}%'; + }; + menubarItemVolumeMetronome.value = Std.int(metronomeVolume * 100); menubarItemPlayerHitsounds.onChange = event -> hitsoundsEnabledPlayer = event.value; menubarItemPlayerHitsounds.selected = hitsoundsEnabledPlayer; @@ -2291,6 +2470,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemOpponentHitsounds.onChange = event -> hitsoundsEnabledOpponent = event.value; menubarItemOpponentHitsounds.selected = hitsoundsEnabledOpponent; + menubarItemVolumeHitsound.onChange = event -> { + var volume:Float = (event?.value ?? 0) / 100.0; + hitsoundVolume = volume; + menubarLabelVolumeHitsound.text = 'Hitsound - ${Std.int(event.value)}%'; + }; + menubarItemVolumeHitsound.value = Std.int(hitsoundVolume * 100); + menubarItemVolumeInstrumental.onChange = event -> { var volume:Float = (event?.value ?? 0) / 100.0; if (audioInstTrack != null) audioInstTrack.volume = volume; @@ -2499,7 +2685,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // dispatchEvent gets called here. if (!super.beatHit()) return false; - if (isMetronomeEnabled && this.subState == null && (audioInstTrack != null && audioInstTrack.playing)) + if (metronomeVolume > 0.0 && this.subState == null && (audioInstTrack != null && audioInstTrack.playing)) { playMetronomeTick(Conductor.currentBeat % 4 == 0); } @@ -2540,7 +2726,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { if (audioInstTrack != null && audioInstTrack.playing) { - if (FlxG.mouse.pressedMiddle) + if (FlxG.keys.pressed.ALT) { // If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat! @@ -2940,6 +3126,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var shouldPause:Bool = false; // Whether to pause the song when scrolling. var shouldEase:Bool = false; // Whether to ease the scroll. + // Handle scroll anchor + if (scrollAnchorScreenPos != null) + { + var currentScreenPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); + var distance = currentScreenPos - scrollAnchorScreenPos; + + var verticalDistance = distance.y; + + // How much scrolling should be done based on the distance of the cursor from the anchor. + final ANCHOR_SCROLL_SPEED = 0.2; + + scrollAmount = ANCHOR_SCROLL_SPEED * verticalDistance; + shouldPause = true; + } + // Mouse Wheel = Scroll if (FlxG.mouse.wheel != 0 && !FlxG.keys.pressed.CONTROL) { @@ -3019,18 +3220,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState shouldPause = true; } - // Middle Mouse + Drag = Scroll but move the playhead the same amount. - if (FlxG.mouse.pressedMiddle) - { - if (FlxG.mouse.deltaY != 0) - { - // Scroll down by the amount dragged. - scrollAmount += -FlxG.mouse.deltaY; - // Move the playhead by the same amount in the other direction so it is stationary. - playheadAmount += FlxG.mouse.deltaY; - } - } - // SHIFT + Scroll = Scroll Fast if (FlxG.keys.pressed.SHIFT) { @@ -3042,7 +3231,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState scrollAmount /= 10; } - // ALT = Move playhead instead. + // Alt + Drag = Scroll but move the playhead the same amount. if (FlxG.keys.pressed.ALT) { playheadAmount = scrollAmount; @@ -3164,9 +3353,26 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var overlapsSelection:Bool = FlxG.mouse.overlaps(renderedSelectionSquares); + if (FlxG.mouse.justPressedMiddle) + { + if (scrollAnchorScreenPos == null) + { + scrollAnchorScreenPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); + selectionBoxStartPos = null; + } + else + { + scrollAnchorScreenPos = null; + } + } + if (FlxG.mouse.justPressed) { - if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea)) + if (scrollAnchorScreenPos != null) + { + scrollAnchorScreenPos = null; + } + else if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea)) { gridPlayheadScrollAreaPressed = true; } @@ -3175,7 +3381,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Clicked note preview notePreviewScrollAreaStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); } - else if (!overlapsGrid || overlapsSelectionBorder) + else if (!isCursorOverHaxeUI && (!overlapsGrid || overlapsSelectionBorder)) { selectionBoxStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY); // Drawing selection box. @@ -3458,6 +3664,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState scrollPositionInPixels = clickedPosInPixels; moveSongToScrollPosition(); } + else if (scrollAnchorScreenPos != null) + { + // Cursor should be a scroll anchor. + targetCursorMode = Scroll; + } else if (dragTargetNote != null || dragTargetEvent != null) { if (FlxG.mouse.justReleased) @@ -4371,7 +4582,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var startTimestamp:Float = 0; if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs; - var targetSong:Song = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, false); + var targetSong:Song; + try + { + targetSong = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, false); + } + catch (e) + { + this.error("Could Not Playtest", 'Got an error trying to playtest the song.\n${e}'); + return; + } // TODO: Rework asset system so we can remove this. switch (currentSongStage) @@ -4415,6 +4635,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Override music. if (audioInstTrack != null) FlxG.sound.music = audioInstTrack; if (audioVocalTrackGroup != null) targetState.vocals = audioVocalTrackGroup; + + this.persistentUpdate = false; + this.persistentDraw = false; stopWelcomeMusic(); openSubState(targetState); } @@ -4530,7 +4753,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ function playMetronomeTick(high:Bool = false):Void { - this.playSound(Paths.sound('chartingSounds/metronome${high ? '1' : '2'}')); + this.playSound(Paths.sound('chartingSounds/metronome${high ? '1' : '2'}'), metronomeVolume); } function switchToCurrentInstrumental():Void @@ -4629,6 +4852,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var prevDifficulty = availableDifficulties[availableDifficulties.length - 1]; selectedDifficulty = prevDifficulty; + Conductor.mapTimeChanges(this.currentSongMetadata.timeChanges); + refreshDifficultyTreeSelection(); refreshMetadataToolbox(); } @@ -4747,6 +4972,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState @:nullSafety(Off) function resetConductorAfterTest(_:FlxSubState = null):Void { + this.persistentUpdate = true; + this.persistentDraw = true; + moveSongToScrollPosition(); // Reapply the volume. @@ -5026,9 +5254,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState switch (noteData.getStrumlineIndex()) { case 0: // Player - if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer')); + if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'), hitsoundVolume); case 1: // Opponent - if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent')); + if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'), hitsoundVolume); } } } diff --git a/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx b/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx new file mode 100644 index 000000000..3c45c1168 --- /dev/null +++ b/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx @@ -0,0 +1,61 @@ +package funkin.ui.debug.charting.commands; + +import funkin.data.song.SongData.SongTimeChange; + +/** + * A command which changes the starting BPM of the song. + */ +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class ChangeStartingBPMCommand implements ChartEditorCommand +{ + var targetBPM:Float; + + var previousBPM:Float = 100; + + public function new(targetBPM:Float) + { + this.targetBPM = targetBPM; + } + + public function execute(state:ChartEditorState):Void + { + var timeChanges:Array = state.currentSongMetadata.timeChanges; + if (timeChanges == null || timeChanges.length == 0) + { + previousBPM = 100; + timeChanges = [new SongTimeChange(0, targetBPM)]; + } + else + { + previousBPM = timeChanges[0].bpm; + timeChanges[0].bpm = targetBPM; + } + + state.currentSongMetadata.timeChanges = timeChanges; + + Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); + } + + public function undo(state:ChartEditorState):Void + { + var timeChanges:Array = state.currentSongMetadata.timeChanges; + if (timeChanges == null || timeChanges.length == 0) + { + timeChanges = [new SongTimeChange(0, previousBPM)]; + } + else + { + timeChanges[0].bpm = previousBPM; + } + + state.currentSongMetadata.timeChanges = timeChanges; + + Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); + } + + public function toString():String + { + return 'Change Starting BPM to ${targetBPM}'; + } +} diff --git a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx index 1857b44db..75382da41 100644 --- a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx @@ -32,10 +32,14 @@ class PasteItemsCommand implements ChartEditorCommand return; } - trace(currentClipboard.notes); + var stepEndOfSong:Float = Conductor.getTimeInSteps(state.songLengthInMs); + var stepCutoff:Float = stepEndOfSong - 1.0; + var msCutoff:Float = Conductor.getStepTimeInMs(stepCutoff); addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard.notes, Std.int(targetTimestamp)); + addedNotes = SongDataUtils.clampSongNoteData(addedNotes, 0.0, msCutoff); addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp)); + addedEvents = SongDataUtils.clampSongEventData(addedEvents, 0.0, msCutoff); state.currentSongChartNoteData = state.currentSongChartNoteData.concat(addedNotes); state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents); diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx index 74f9983f1..044ac176f 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx @@ -211,7 +211,7 @@ class ChartEditorAudioHandler * Automatically cleans up after itself and recycles previous FlxSound instances if available, for performance. * @param path The path to the sound effect. Use `Paths` to build this. */ - public static function playSound(_state:ChartEditorState, path:String):Void + public static function playSound(_state:ChartEditorState, path:String, volume:Float = 1.0):Void { var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound(); var asset:Null = FlxG.sound.cache(path); @@ -223,6 +223,7 @@ class ChartEditorAudioHandler snd.loadEmbedded(asset); snd.autoDestroy = true; FlxG.sound.list.add(snd); + snd.volume = volume; snd.play(); } diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx index 79937ce6f..0e7ba374c 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -110,12 +110,12 @@ class ChartEditorDialogHandler { var dialog:Null = openDialog(state, CHART_EDITOR_DIALOG_BACKUP_AVAILABLE_LAYOUT, true, true); if (dialog == null) throw 'Could not locate Backup Available dialog'; - dialog.onDialogClosed = function(_event) { + dialog.onDialogClosed = function(event) { state.isHaxeUIDialogOpen = false; - if (_event.button == DialogButton.APPLY) + if (event.button == DialogButton.APPLY) { // User loaded the backup! Close the welcome dialog behind this. - if (welcomeDialog != null) welcomeDialog.hideDialog(DialogButton.CANCEL); + if (welcomeDialog != null) welcomeDialog.hideDialog(DialogButton.APPLY); } else { @@ -137,22 +137,22 @@ class ChartEditorDialogHandler var buttonCancel:Null