diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 674c96fb6..efb3ee623 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -5,12 +5,17 @@ import funkin.save.migrator.SaveDataMigrator; import thx.semver.Version; import funkin.input.Controls.Device; import funkin.save.migrator.RawSaveData_v1_0_0; +import funkin.save.migrator.SaveDataMigrator; +import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle; +import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme; +import thx.semver.Version; @:nullSafety @:forward(volume, mute) abstract Save(RawSaveData) { - public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.0"; + // Version 2.0.1 adds attributes to `optionsChartEditor`, that should return default values if they are null. + public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.1"; public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility. @@ -94,6 +99,18 @@ abstract Save(RawSaveData) optionsChartEditor: { // Reasonable defaults. + previousFiles: [], + noteQuant: 3, + chartEditorLiveInputStyle: ChartEditorLiveInputStyle.None, + theme: ChartEditorTheme.Light, + playtestStartTime: false, + downscroll: false, + metronomeEnabled: true, + hitsoundsEnabledPlayer: true, + hitsoundsEnabledOpponent: true, + instVolume: 1.0, + voicesVolume: 1.0, + playbackSpeed: 1.0, }, }; } @@ -124,7 +141,9 @@ abstract Save(RawSaveData) function set_ngSessionId(value:Null):Null { - return this.api.newgrounds.sessionId = value; + this.api.newgrounds.sessionId = value; + flush(); + return this.api.newgrounds.sessionId; } public var enabledModIds(get, set):Array; @@ -136,7 +155,213 @@ abstract Save(RawSaveData) function set_enabledModIds(value:Array):Array { - return this.mods.enabledMods = value; + this.mods.enabledMods = value; + flush(); + return this.mods.enabledMods; + } + + public var chartEditorPreviousFiles(get, set):Array; + + function get_chartEditorPreviousFiles():Array + { + if (this.optionsChartEditor.previousFiles == null) this.optionsChartEditor.previousFiles = []; + + return this.optionsChartEditor.previousFiles; + } + + function set_chartEditorPreviousFiles(value:Array):Array + { + // Set and apply. + this.optionsChartEditor.previousFiles = value; + flush(); + return this.optionsChartEditor.previousFiles; + } + + public var chartEditorNoteQuant(get, set):Int; + + function get_chartEditorNoteQuant():Int + { + if (this.optionsChartEditor.noteQuant == null) this.optionsChartEditor.noteQuant = 3; + + return this.optionsChartEditor.noteQuant; + } + + function set_chartEditorNoteQuant(value:Int):Int + { + // Set and apply. + this.optionsChartEditor.noteQuant = value; + flush(); + return this.optionsChartEditor.noteQuant; + } + + public var chartEditorLiveInputStyle(get, set):ChartEditorLiveInputStyle; + + function get_chartEditorLiveInputStyle():ChartEditorLiveInputStyle + { + if (this.optionsChartEditor.chartEditorLiveInputStyle == null) this.optionsChartEditor.chartEditorLiveInputStyle = ChartEditorLiveInputStyle.None; + + return this.optionsChartEditor.chartEditorLiveInputStyle; + } + + function set_chartEditorLiveInputStyle(value:ChartEditorLiveInputStyle):ChartEditorLiveInputStyle + { + // Set and apply. + this.optionsChartEditor.chartEditorLiveInputStyle = value; + flush(); + return this.optionsChartEditor.chartEditorLiveInputStyle; + } + + public var chartEditorDownscroll(get, set):Bool; + + function get_chartEditorDownscroll():Bool + { + if (this.optionsChartEditor.downscroll == null) this.optionsChartEditor.downscroll = false; + + return this.optionsChartEditor.downscroll; + } + + function set_chartEditorDownscroll(value:Bool):Bool + { + // Set and apply. + this.optionsChartEditor.downscroll = value; + flush(); + return this.optionsChartEditor.downscroll; + } + + public var chartEditorPlaytestStartTime(get, set):Bool; + + function get_chartEditorPlaytestStartTime():Bool + { + if (this.optionsChartEditor.playtestStartTime == null) this.optionsChartEditor.playtestStartTime = false; + + return this.optionsChartEditor.playtestStartTime; + } + + function set_chartEditorPlaytestStartTime(value:Bool):Bool + { + // Set and apply. + this.optionsChartEditor.playtestStartTime = value; + flush(); + return this.optionsChartEditor.playtestStartTime; + } + + public var chartEditorTheme(get, set):ChartEditorTheme; + + function get_chartEditorTheme():ChartEditorTheme + { + if (this.optionsChartEditor.theme == null) this.optionsChartEditor.theme = ChartEditorTheme.Light; + + return this.optionsChartEditor.theme; + } + + function set_chartEditorTheme(value:ChartEditorTheme):ChartEditorTheme + { + // Set and apply. + this.optionsChartEditor.theme = value; + flush(); + return this.optionsChartEditor.theme; + } + + public var chartEditorMetronomeEnabled(get, set):Bool; + + function get_chartEditorMetronomeEnabled():Bool + { + if (this.optionsChartEditor.metronomeEnabled == null) this.optionsChartEditor.metronomeEnabled = true; + + return this.optionsChartEditor.metronomeEnabled; + } + + function set_chartEditorMetronomeEnabled(value:Bool):Bool + { + // Set and apply. + this.optionsChartEditor.metronomeEnabled = value; + flush(); + return this.optionsChartEditor.metronomeEnabled; + } + + public var chartEditorHitsoundsEnabledPlayer(get, set):Bool; + + function get_chartEditorHitsoundsEnabledPlayer():Bool + { + if (this.optionsChartEditor.hitsoundsEnabledPlayer == null) this.optionsChartEditor.hitsoundsEnabledPlayer = true; + + return this.optionsChartEditor.hitsoundsEnabledPlayer; + } + + function set_chartEditorHitsoundsEnabledPlayer(value:Bool):Bool + { + // Set and apply. + this.optionsChartEditor.hitsoundsEnabledPlayer = value; + flush(); + return this.optionsChartEditor.hitsoundsEnabledPlayer; + } + + public var chartEditorHitsoundsEnabledOpponent(get, set):Bool; + + function get_chartEditorHitsoundsEnabledOpponent():Bool + { + if (this.optionsChartEditor.hitsoundsEnabledOpponent == null) this.optionsChartEditor.hitsoundsEnabledOpponent = true; + + return this.optionsChartEditor.hitsoundsEnabledOpponent; + } + + function set_chartEditorHitsoundsEnabledOpponent(value:Bool):Bool + { + // Set and apply. + this.optionsChartEditor.hitsoundsEnabledOpponent = value; + flush(); + return this.optionsChartEditor.hitsoundsEnabledOpponent; + } + + public var chartEditorInstVolume(get, set):Float; + + function get_chartEditorInstVolume():Float + { + if (this.optionsChartEditor.instVolume == null) this.optionsChartEditor.instVolume = 1.0; + + return this.optionsChartEditor.instVolume; + } + + function set_chartEditorInstVolume(value:Float):Float + { + // Set and apply. + this.optionsChartEditor.instVolume = value; + flush(); + return this.optionsChartEditor.instVolume; + } + + public var chartEditorVoicesVolume(get, set):Float; + + function get_chartEditorVoicesVolume():Float + { + if (this.optionsChartEditor.voicesVolume == null) this.optionsChartEditor.voicesVolume = 1.0; + + return this.optionsChartEditor.voicesVolume; + } + + function set_chartEditorVoicesVolume(value:Float):Float + { + // Set and apply. + this.optionsChartEditor.voicesVolume = value; + flush(); + return this.optionsChartEditor.voicesVolume; + } + + public var chartEditorPlaybackSpeed(get, set):Float; + + function get_chartEditorPlaybackSpeed():Float + { + if (this.optionsChartEditor.playbackSpeed == null) this.optionsChartEditor.playbackSpeed = 1.0; + + return this.optionsChartEditor.playbackSpeed; + } + + function set_chartEditorPlaybackSpeed(value:Float):Float + { + // Set and apply. + this.optionsChartEditor.playbackSpeed = value; + flush(); + return this.optionsChartEditor.playbackSpeed; } /** @@ -699,4 +924,77 @@ typedef SaveControlsData = /** * An anonymous structure containing all the user's options and preferences, specific to the Chart Editor. */ -typedef SaveDataChartEditorOptions = {}; +typedef SaveDataChartEditorOptions = +{ + /** + * Previous files opened in the Chart Editor. + * @default `[]` + */ + var ?previousFiles:Array; + + /** + * Note snapping level in the Chart Editor. + * @default `3` + */ + var ?noteQuant:Int; + + /** + * Live input style in the Chart Editor. + * @default `ChartEditorLiveInputStyle.None` + */ + var ?chartEditorLiveInputStyle:ChartEditorLiveInputStyle; + + /** + * Theme in the Chart Editor. + * @default `ChartEditorTheme.Light` + */ + var ?theme:ChartEditorTheme; + + /** + * Downscroll in the Chart Editor. + * @default `false` + */ + var ?downscroll:Bool; + + /** + * Metronome sounds in the Chart Editor. + * @default `true` + */ + var ?metronomeEnabled:Bool; + + /** + * If true, playtest songs from the current position in the Chart Editor. + * @default `false` + */ + var ?playtestStartTime:Bool; + + /** + * Player note hit sounds in the Chart Editor. + * @default `true` + */ + var ?hitsoundsEnabledPlayer:Bool; + + /** + * Opponent note hit sounds in the Chart Editor. + * @default `true` + */ + var ?hitsoundsEnabledOpponent:Bool; + + /** + * Instrumental volume in the Chart Editor. + * @default `1.0` + */ + var ?instVolume:Float; + + /** + * Voices volume in the Chart Editor. + * @default `1.0` + */ + var ?voicesVolume:Float; + + /** + * Playback speed in the Chart Editor. + * @default `1.0` + */ + var ?playbackSpeed:Float; +}; diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 382bab592..aa5372327 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -12,6 +12,7 @@ import flixel.math.FlxMath; import flixel.math.FlxPoint; import flixel.math.FlxRect; import flixel.sound.FlxSound; +import flixel.system.FlxAssets.FlxSoundAsset; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.tweens.misc.VarTween; @@ -32,6 +33,7 @@ import funkin.input.TurboKeyHandler; import funkin.modding.events.ScriptEvent; import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.CharacterData; +import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.components.HealthIcon; import funkin.play.notes.NoteSprite; import funkin.play.PlayState; @@ -46,6 +48,7 @@ import funkin.data.song.SongDataUtils; import funkin.ui.debug.charting.commands.ChartEditorCommand; import funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler; import funkin.play.stage.StageData; +import funkin.save.Save; import funkin.ui.debug.charting.commands.AddEventsCommand; import funkin.ui.debug.charting.commands.AddNotesCommand; import funkin.ui.debug.charting.commands.ChartEditorCommand; @@ -77,6 +80,7 @@ import funkin.util.SortUtil; import funkin.util.WindowUtil; import haxe.DynamicAccess; import haxe.io.Bytes; +import haxe.io.Path; import haxe.ui.components.DropDown; import haxe.ui.components.Label; import haxe.ui.components.NumberStepper; @@ -84,6 +88,7 @@ import haxe.ui.components.Slider; import haxe.ui.components.TextField; import haxe.ui.containers.dialogs.CollapsibleDialog; import haxe.ui.containers.Frame; +import haxe.ui.containers.menus.Menu; import haxe.ui.containers.menus.MenuItem; import haxe.ui.containers.TreeView; import haxe.ui.containers.TreeViewNode; @@ -95,6 +100,7 @@ import haxe.ui.focus.FocusManager; import haxe.ui.notifications.NotificationManager; import haxe.ui.notifications.NotificationType; import openfl.display.BitmapData; +import funkin.util.FileUtil; using Lambda; @@ -750,7 +756,9 @@ class ChartEditorState extends HaxeUIState } } - return saveDataDirty = value; + saveDataDirty = value; + applyWindowTitle(); + return saveDataDirty; } /** @@ -932,7 +940,7 @@ class ChartEditorState extends HaxeUIState var result:Null = songMetadata.get(selectedVariation); if (result == null) { - result = new SongMetadata('Dad Battle', 'Kawai Sprite', selectedVariation); + result = new SongMetadata('DadBattle', 'Kawai Sprite', selectedVariation); songMetadata.set(selectedVariation, result); } return result; @@ -1272,6 +1280,16 @@ class ChartEditorState extends HaxeUIState */ var playbarHeadLayout:Null = null; + /** + * The submenu in the menubar containing recently opened files. + */ + var menubarOpenRecent:Null = null; + + /** + * The item in the menubar to save the currently opened chart. + */ + var menubarItemSaveChart:Null = null; + /** * The playbar head slider. */ @@ -1326,9 +1344,68 @@ class ChartEditorState extends HaxeUIState var params:Null; /** - * The current file path which the chart editor is working with. + * A list of previous working file paths. + * Also known as the "recent files" list. + * The first element is [null] if the current working file has not been saved anywhere yet. */ - public var currentWorkingFilePath:Null; + public var previousWorkingFilePaths(default, set):Array> = [null]; + + function set_previousWorkingFilePaths(value:Array>):Array> + { + // Called only when the WHOLE LIST is overridden. + previousWorkingFilePaths = value; + applyWindowTitle(); + populateOpenRecentMenu(); + applyCanQuickSave(); + return value; + } + + /** + * The current file path which the chart editor is working with. + * If `null`, the current chart has not been saved yet. + */ + public var currentWorkingFilePath(get, set):Null; + + function get_currentWorkingFilePath():Null + { + return previousWorkingFilePaths[0]; + } + + function set_currentWorkingFilePath(value:Null):Null + { + if (value == previousWorkingFilePaths[0]) return value; + + if (previousWorkingFilePaths.contains(null)) + { + // Filter all instances of `null` from the array. + previousWorkingFilePaths = previousWorkingFilePaths.filter(function(x:Null):Bool { + return x != null; + }); + } + + if (previousWorkingFilePaths.contains(value)) + { + // Move the path to the front of the list. + previousWorkingFilePaths.remove(value); + previousWorkingFilePaths.unshift(value); + } + else + { + // Add the path to the front of the list. + previousWorkingFilePaths.unshift(value); + } + + while (previousWorkingFilePaths.length > Constants.MAX_PREVIOUS_WORKING_FILES) + { + // Remove the last path in the list. + previousWorkingFilePaths.pop(); + } + + populateOpenRecentMenu(); + applyWindowTitle(); + + return value; + } public function new(?params:ChartEditorParams) { @@ -1386,6 +1463,8 @@ class ChartEditorState extends HaxeUIState // Show the mouse cursor. Cursor.show(); + loadPreferences(); + fixCamera(); // Get rid of any music from the previous state. @@ -1406,6 +1485,7 @@ class ChartEditorState extends HaxeUIState buildSelectionBox(); buildAdditionalUI(); + populateOpenRecentMenu(); ChartEditorShortcutHandler.applyPlatformShortcutText(this); // Setup the onClick listeners for the UI after it's been created. @@ -1419,22 +1499,31 @@ class ChartEditorState extends HaxeUIState if (params != null && params.fnfcTargetPath != null) { // Chart editor was opened from the command line. Open the FNFC file now! - if (ChartEditorImportExportHandler.loadFromFNFCPath(this, params.fnfcTargetPath)) + var result:Null> = ChartEditorImportExportHandler.loadFromFNFCPath(this, params.fnfcTargetPath); + if (result != null) { - // Don't open the welcome dialog! - #if !mac NotificationManager.instance.addNotification( { title: 'Success', - body: 'Loaded chart (${params.fnfcTargetPath})', - type: NotificationType.Success, + body: result.length == 0 ? 'Loaded chart (${params.fnfcTargetPath})' : 'Loaded chart (${params.fnfcTargetPath})\n${result.join("\n")}', + type: result.length == 0 ? NotificationType.Success : NotificationType.Warning, expiryMs: Constants.NOTIFICATION_DISMISS_TIME }); #end } else { + #if !mac + NotificationManager.instance.addNotification( + { + title: 'Failure', + body: 'Failed to load chart (${params.fnfcTargetPath})', + type: NotificationType.Error, + expiryMs: Constants.NOTIFICATION_DISMISS_TIME + }); + #end + // Song failed to load, open the Welcome dialog so we aren't in a broken state. ChartEditorDialogHandler.openWelcomeDialog(this, false); } @@ -1445,25 +1534,122 @@ class ChartEditorState extends HaxeUIState } } - override function destroy():Void - { - super.destroy(); - - cleanupAutoSave(); - - // Hide the mouse cursor on other states. - Cursor.hide(); - - @:privateAccess - ChartEditorNoteSprite.noteFrameCollection = null; - } - function setupWelcomeMusic() { this.welcomeMusic.loadEmbedded(Paths.music('chartEditorLoop/chartEditorLoop')); this.welcomeMusic.looped = true; - // this.welcomeMusic.play(); - // fadeInWelcomeMusic(); + } + + public function loadPreferences():Void + { + var save:Save = Save.get(); + + if (previousWorkingFilePaths[0] == null) + { + previousWorkingFilePaths = [null].concat(save.chartEditorPreviousFiles); + } + else + { + previousWorkingFilePaths = [currentWorkingFilePath].concat(save.chartEditorPreviousFiles); + } + noteSnapQuantIndex = save.chartEditorNoteQuant; + currentLiveInputStyle = save.chartEditorLiveInputStyle; + isViewDownscroll = save.chartEditorDownscroll; + playtestStartTime = save.chartEditorPlaytestStartTime; + currentTheme = save.chartEditorTheme; + isMetronomeEnabled = save.chartEditorMetronomeEnabled; + hitsoundsEnabledPlayer = save.chartEditorHitsoundsEnabledPlayer; + hitsoundsEnabledOpponent = save.chartEditorHitsoundsEnabledOpponent; + + // audioInstTrack.volume = save.chartEditorInstVolume; + // audioInstTrack.pitch = save.chartEditorPlaybackSpeed; + // audioVocalTrackGroup.volume = save.chartEditorVoicesVolume; + // audioVocalTrackGroup.pitch = save.chartEditorPlaybackSpeed; + } + + public function writePreferences():Void + { + var save:Save = Save.get(); + + // Can't use filter() because of null safety checking! + var filteredWorkingFilePaths:Array = []; + for (chartPath in previousWorkingFilePaths) + if (chartPath != null) filteredWorkingFilePaths.push(chartPath); + + save.chartEditorPreviousFiles = filteredWorkingFilePaths; + save.chartEditorNoteQuant = noteSnapQuantIndex; + save.chartEditorLiveInputStyle = currentLiveInputStyle; + save.chartEditorDownscroll = isViewDownscroll; + save.chartEditorPlaytestStartTime = playtestStartTime; + save.chartEditorTheme = currentTheme; + save.chartEditorMetronomeEnabled = isMetronomeEnabled; + save.chartEditorHitsoundsEnabledPlayer = hitsoundsEnabledPlayer; + save.chartEditorHitsoundsEnabledOpponent = hitsoundsEnabledOpponent; + + // save.chartEditorInstVolume = audioInstTrack.volume; + // save.chartEditorVoicesVolume = audioVocalTrackGroup.volume; + // save.chartEditorPlaybackSpeed = audioInstTrack.pitch; + } + + public function populateOpenRecentMenu():Void + { + if (menubarOpenRecent == null) return; + + #if sys + menubarOpenRecent.removeAllComponents(); + + for (chartPath in previousWorkingFilePaths) + { + if (chartPath == null) continue; + + var menuItemRecentChart:MenuItem = new MenuItem(); + menuItemRecentChart.text = chartPath; + menuItemRecentChart.onClick = function(_event) { + stopWelcomeMusic(); + + // Load chart from file + var result:Null> = ChartEditorImportExportHandler.loadFromFNFCPath(this, chartPath); + if (result != null) + { + #if !mac + NotificationManager.instance.addNotification( + { + title: 'Success', + body: result.length == 0 ? 'Loaded chart (${chartPath.toString()})' : 'Loaded chart (${chartPath.toString()})\n${result.join("\n")}', + type: result.length == 0 ? NotificationType.Success : NotificationType.Warning, + expiryMs: Constants.NOTIFICATION_DISMISS_TIME + }); + #end + } + else + { + #if !mac + NotificationManager.instance.addNotification( + { + title: 'Failure', + body: 'Failed to load chart (${chartPath.toString()})', + type: NotificationType.Error, + expiryMs: Constants.NOTIFICATION_DISMISS_TIME + }); + #end + } + } + + if (!FileUtil.doesFileExist(chartPath)) + { + trace('Previously loaded chart file (${chartPath}) does not exist, disabling link...'); + menuItemRecentChart.disabled = true; + } + else + { + menuItemRecentChart.disabled = false; + } + + menubarOpenRecent.addComponent(menuItemRecentChart); + } + #else + menubarOpenRecent.hide(); + #end } function fadeInWelcomeMusic():Void @@ -1674,7 +1860,10 @@ class ChartEditorState extends HaxeUIState function setNotePreviewViewportBounds(bounds:FlxRect = null):Void { if (notePreviewViewport == null) - throw 'ERROR: Tried to set note preview viewport bounds, but notePreviewViewport is null! Check ChartEditorThemeHandler.updateTheme().'; + { + trace('[WARN] Tried to set note preview viewport bounds, but notePreviewViewport is null!'); + return; + } if (bounds == null) { @@ -1780,9 +1969,14 @@ class ChartEditorState extends HaxeUIState add(playbarHeadLayout); + menubarOpenRecent = findComponent('menubarOpenRecent', Menu); + if (menubarOpenRecent == null) throw "Could not find menubarOpenRecent!"; + + menubarItemSaveChart = findComponent('menubarItemSaveChart', MenuItem); + if (menubarItemSaveChart == null) throw "Could not find menubarItemSaveChart!"; + // Setup notifications. @:privateAccess - // NotificationManager.GUTTER_SIZE = 56; NotificationManager.GUTTER_SIZE = 20; } @@ -1811,11 +2005,21 @@ class ChartEditorState extends HaxeUIState // Add functionality to the menu items. - addUIClickListener('menubarItemNewChart', _ -> this.openWelcomeDialog(true)); - addUIClickListener('menubarItemOpenChart', _ -> this.openBrowseFNFC(true)); - addUIClickListener('menubarItemSaveChartAs', _ -> this.exportAllSongData()); - addUIClickListener('menubarItemLoadInst', _ -> this.openUploadInstDialog(true)); - addUIClickListener('menubarItemImportChart', _ -> this.openImportChartDialog('legacy', true)); + addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true)); + addUIClickListener('menubarItemOpenChart', _ -> ChartEditorDialogHandler.openBrowseFNFC(this, true)); + addUIClickListener('menubarItemSaveChart', _ -> { + if (currentWorkingFilePath != null) + { + ChartEditorImportExportHandler.exportAllSongData(this, true, currentWorkingFilePath); + } + else + { + ChartEditorImportExportHandler.exportAllSongData(this, false); + } + }); + addUIClickListener('menubarItemSaveChartAs', _ -> ChartEditorImportExportHandler.exportAllSongData(this)); + addUIClickListener('menubarItemLoadInst', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true)); + addUIClickListener('menubarItemImportChart', _ -> ChartEditorDialogHandler.openImportChartDialog(this, 'legacy', true)); addUIClickListener('menubarItemExit', _ -> quitChartEditor()); addUIClickListener('menubarItemUndo', _ -> undoLastCommand()); @@ -2012,6 +2216,39 @@ class ChartEditorState extends HaxeUIState /** * UPDATE FUNCTIONS */ + function autoSave():Void + { + saveDataDirty = false; + + // Auto-save preferences. + writePreferences(); + + // Auto-save the chart. + #if html5 + // Auto-save to local storage. + // TODO: Implement this. + #else + // Auto-save to temp file. + ChartEditorImportExportHandler.exportAllSongData(this, true); + #end + } + + function onWindowClose(exitCode:Int):Void + { + trace('Window exited with exit code: $exitCode'); + trace('Should save chart? $saveDataDirty'); + + if (saveDataDirty) + { + ChartEditorImportExportHandler.exportAllSongData(this, true); + } + } + + function cleanupAutoSave():Void + { + WindowUtil.windowExit.remove(onWindowClose); + } + public override function update(elapsed:Float):Void { // Override F4 behavior to include the autosave. @@ -2152,49 +2389,6 @@ class ChartEditorState extends HaxeUIState } } - /** - * Handle the playback of hitsounds. - */ - function handleHitsounds(oldSongPosition:Float, newSongPosition:Float):Void - { - if (!hitsoundsEnabled) return; - - // Assume notes are sorted by time. - for (noteData in currentSongChartNoteData) - { - // Check for notes between the old and new song positions. - - if (noteData.time < oldSongPosition) // Note is in the past. - continue; - - if (noteData.time > newSongPosition) // Note is in the future. - return; // Assume all notes are also in the future. - - // Note was just hit. - - // Character preview. - - // NoteScriptEvent takes a sprite, ehe. Need to rework that. - var tempNote:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault()); - tempNote.noteData = noteData; - tempNote.scrollFactor.set(0, 0); - var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, tempNote, 1, true); - dispatchEvent(event); - - // Calling event.cancelEvent() skips all the other logic! Neat! - if (event.eventCanceled) continue; - - // Hitsounds. - switch (noteData.getStrumlineIndex()) - { - case 0: // Player - if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer')); - case 1: // Opponent - if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent')); - } - } - } - /** * Handle using `renderedNotes` to display notes from `currentSongChartNoteData`. */ @@ -3463,63 +3657,6 @@ class ChartEditorState extends HaxeUIState } } - /** - * Handles passive behavior of the menu bar, such as updating labels or enabled/disabled status. - * Does not handle onClick ACTIONS of the menubar. - */ - function handleMenubar():Void - { - if (commandHistoryDirty) - { - commandHistoryDirty = false; - - // Update the Undo and Redo buttons. - var undoButton:Null = findComponent('menubarItemUndo', MenuItem); - - if (undoButton != null) - { - if (undoHistory.length == 0) - { - // Disable the Undo button. - undoButton.disabled = true; - undoButton.text = 'Undo'; - } - else - { - // Change the label to the last command. - undoButton.disabled = false; - undoButton.text = 'Undo ${undoHistory[undoHistory.length - 1].toString()}'; - } - } - else - { - trace('undoButton is null'); - } - - var redoButton:Null = findComponent('menubarItemRedo', MenuItem); - - if (redoButton != null) - { - if (redoHistory.length == 0) - { - // Disable the Redo button. - redoButton.disabled = true; - redoButton.text = 'Redo'; - } - else - { - // Change the label to the last command. - redoButton.disabled = false; - redoButton.text = 'Redo ${redoHistory[redoHistory.length - 1].toString()}'; - } - } - else - { - trace('redoButton is null'); - } - } - } - function handleToolboxes():Void { handleDifficultyToolbox(); @@ -3743,32 +3880,6 @@ class ChartEditorState extends HaxeUIState } } - /** - * Handles the note preview/scroll area on the right side. - * Notes are rendered here as small bars. - * This function also handles: - * - Moving the viewport preview box around based on its current position. - * - Scrolling the note preview area down if the note preview is taller than the screen, - * and the viewport nears the end of the visible area. - */ - function handleNotePreview():Void - { - if (notePreviewDirty && notePreview != null) - { - notePreviewDirty = false; - - // TODO: Only update the notes that have changed. - notePreview.erase(); - notePreview.addNotes(currentSongChartNoteData, Std.int(songLengthInMs)); - notePreview.addEvents(currentSongChartEventData, Std.int(songLengthInMs)); - } - - if (notePreviewViewportBoundsDirty) - { - setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); - } - } - /** * Handle aligning the health icons next to the grid. */ @@ -3825,12 +3936,24 @@ class ChartEditorState extends HaxeUIState this.openBrowseFNFC(true); } - // CTRL + SHIFT + S = Save As + if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.S) + { + if (currentWorkingFilePath == null || FlxG.keys.pressed.SHIFT) + { + // CTRL + SHIFT + S = Save As + ChartEditorImportExportHandler.exportAllSongData(this, false); + } + else + { + // CTRL + S = Save Chart + ChartEditorImportExportHandler.exportAllSongData(this, true, currentWorkingFilePath); + } + } + if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.S) { this.exportAllSongData(false); } - // CTRL + Q = Quit to Menu if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.Q) { @@ -3846,6 +3969,8 @@ class ChartEditorState extends HaxeUIState // TODO: PR Flixel to make onComplete nullable. if (audioInstTrack != null) audioInstTrack.onComplete = null; FlxG.switchState(new MainMenuState()); + + resetWindowTitle(); } /** @@ -4119,48 +4244,6 @@ class ChartEditorState extends HaxeUIState performCommand(command, false); } - /** - * SAVE, AUTOSAVE, QUIT FUNCTIONS - */ - // ==================== - - /** - * Called after 5 minutes without saving. - */ - function autoSave():Void - { - saveDataDirty = false; - - // Auto-save the chart. - - #if html5 - // Auto-save to local storage. - #else - // Auto-save to temp file. - this.exportAllSongData(true); - #end - } - - /** - * Called when the window is closed while we are in the chart editor. - * @param exitCode The exit code of the window. - */ - function onWindowClose(exitCode:Int):Void - { - trace('Window exited with exit code: $exitCode'); - trace('Should save chart? $saveDataDirty'); - - if (saveDataDirty) - { - this.exportAllSongData(true); - } - } - - function cleanupAutoSave():Void - { - WindowUtil.windowExit.remove(onWindowClose); - } - /** * GRAPHICS FUNCTIONS */ @@ -4208,28 +4291,6 @@ class ChartEditorState extends HaxeUIState setComponentText('playbarPlay', '||'); } - function stopAudioPlayback():Void - { - if (audioInstTrack != null) audioInstTrack.pause(); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); - - setComponentText('playbarPlay', '>'); - } - - function toggleAudioPlayback():Void - { - if (audioInstTrack == null) return; - - if (audioInstTrack.playing) - { - stopAudioPlayback(); - } - else - { - startAudioPlayback(); - } - } - /** * Play the metronome tick sound. * @param high Whether to play the full beat sound rather than the quarter beat sound. @@ -4245,42 +4306,6 @@ class ChartEditorState extends HaxeUIState this.switchToInstrumental(currentInstrumentalId, currentSongMetadata.playData.characters.player, currentSongMetadata.playData.characters.opponent); } - function postLoadInstrumental():Void - { - if (audioInstTrack != null) - { - // Prevent the time from skipping back to 0 when the song ends. - audioInstTrack.onComplete = function() { - if (audioInstTrack != null) audioInstTrack.pause(); - if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); - }; - - songLengthInMs = audioInstTrack.length; - - if (gridTiledSprite != null) gridTiledSprite.height = songLengthInPixels; - if (gridPlayheadScrollArea != null) - { - gridPlayheadScrollArea.setGraphicSize(Std.int(gridPlayheadScrollArea.width), songLengthInPixels); - gridPlayheadScrollArea.updateHitbox(); - } - - buildSpectrogram(audioInstTrack); - } - else - { - trace('[WARN] Instrumental track was null!'); - } - - // Pretty much everything is going to need to be reset. - scrollPositionInPixels = 0; - playheadPositionInPixels = 0; - notePreviewDirty = true; - notePreviewViewportBoundsDirty = true; - noteDisplayDirty = true; - healthIconsDirty = true; - moveSongToScrollPosition(); - } - /** * CHART DATA FUNCTIONS */ @@ -4299,11 +4324,6 @@ class ChartEditorState extends HaxeUIState }); } - function isNoteSelected(note:Null):Bool - { - return note != null && currentNoteSelection.indexOf(note) != -1; - } - function isEventSelected(event:Null):Bool { return event != null && currentEventSelection.indexOf(event) != -1; @@ -4679,11 +4699,250 @@ class ChartEditorState extends HaxeUIState /** * Dismiss any existing HaxeUI notifications, if there are any. */ - public static function dismissNotifications():Void + function handleNotePreview():Void + { + if (notePreviewDirty && notePreview != null) + { + notePreviewDirty = false; + + // TODO: Only update the notes that have changed. + notePreview.erase(); + notePreview.addNotes(currentSongChartNoteData, Std.int(songLengthInMs)); + notePreview.addEvents(currentSongChartEventData, Std.int(songLengthInMs)); + } + + if (notePreviewViewportBoundsDirty) + { + setNotePreviewViewportBounds(calculateNotePreviewViewportBounds()); + } + } + + /** + * Handles passive behavior of the menu bar, such as updating labels or enabled/disabled status. + * Does not handle onClick ACTIONS of the menubar. + */ + function handleMenubar():Void + { + if (commandHistoryDirty) + { + commandHistoryDirty = false; + + // Update the Undo and Redo buttons. + var undoButton:Null = findComponent('menubarItemUndo', MenuItem); + + if (undoButton != null) + { + if (undoHistory.length == 0) + { + // Disable the Undo button. + undoButton.disabled = true; + undoButton.text = 'Undo'; + } + else + { + // Change the label to the last command. + undoButton.disabled = false; + undoButton.text = 'Undo ${undoHistory[undoHistory.length - 1].toString()}'; + } + } + else + { + trace('undoButton is null'); + } + + var redoButton:Null = findComponent('menubarItemRedo', MenuItem); + + if (redoButton != null) + { + if (redoHistory.length == 0) + { + // Disable the Redo button. + redoButton.disabled = true; + redoButton.text = 'Redo'; + } + else + { + // Change the label to the last command. + redoButton.disabled = false; + redoButton.text = 'Redo ${redoHistory[redoHistory.length - 1].toString()}'; + } + } + else + { + trace('redoButton is null'); + } + } + } + + /** + * Handle the playback of hitsounds. + */ + function handleHitsounds(oldSongPosition:Float, newSongPosition:Float):Void + { + if (!hitsoundsEnabled) return; + + // Assume notes are sorted by time. + for (noteData in currentSongChartNoteData) + { + // Check for notes between the old and new song positions. + + if (noteData.time < oldSongPosition) // Note is in the past. + continue; + + if (noteData.time > newSongPosition) // Note is in the future. + return; // Assume all notes are also in the future. + + // Note was just hit. + + // Character preview. + + // NoteScriptEvent takes a sprite, ehe. Need to rework that. + var tempNote:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault()); + tempNote.noteData = noteData; + tempNote.scrollFactor.set(0, 0); + var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, tempNote, 1, true); + dispatchEvent(event); + + // Calling event.cancelEvent() skips all the other logic! Neat! + if (event.eventCanceled) continue; + + // Hitsounds. + switch (noteData.getStrumlineIndex()) + { + case 0: // Player + if (hitsoundsEnabledPlayer) ChartEditorAudioHandler.playSound(this, Paths.sound('chartingSounds/hitNotePlayer')); + case 1: // Opponent + if (hitsoundsEnabledOpponent) ChartEditorAudioHandler.playSound(this, Paths.sound('chartingSounds/hitNoteOpponent')); + } + } + } + + function stopAudioPlayback():Void + { + if (audioInstTrack != null) audioInstTrack.pause(); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); + + setComponentText('playbarPlay', '>'); + } + + function toggleAudioPlayback():Void + { + if (audioInstTrack == null) return; + + if (audioInstTrack.playing) + { + stopAudioPlayback(); + } + else + { + startAudioPlayback(); + } + } + + public function postLoadInstrumental():Void + { + if (audioInstTrack != null) + { + // Prevent the time from skipping back to 0 when the song ends. + audioInstTrack.onComplete = function() { + if (audioInstTrack != null) audioInstTrack.pause(); + if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); + }; + + songLengthInMs = audioInstTrack.length; + + if (gridTiledSprite != null) gridTiledSprite.height = songLengthInPixels; + if (gridPlayheadScrollArea != null) + { + gridPlayheadScrollArea.setGraphicSize(Std.int(gridPlayheadScrollArea.width), songLengthInPixels); + gridPlayheadScrollArea.updateHitbox(); + } + + buildSpectrogram(audioInstTrack); + } + else + { + trace('[WARN] Instrumental track was null!'); + } + + // Pretty much everything is going to need to be reset. + scrollPositionInPixels = 0; + playheadPositionInPixels = 0; + notePreviewDirty = true; + notePreviewViewportBoundsDirty = true; + noteDisplayDirty = true; + healthIconsDirty = true; + moveSongToScrollPosition(); + } + + /** + * Clear the voices group. + */ + public function clearVocals():Void + { + if (audioVocalTrackGroup != null) audioVocalTrackGroup.clear(); + } + + function isNoteSelected(note:Null):Bool + { + return note != null && currentNoteSelection.indexOf(note) != -1; + } + + override function destroy():Void + { + super.destroy(); + + cleanupAutoSave(); + + // Hide the mouse cursor on other states. + Cursor.hide(); + + @:privateAccess + ChartEditorNoteSprite.noteFrameCollection = null; + } + + /** + * Dismiss any existing notifications, if there are any. + */ + function dismissNotifications():Void { NotificationManager.instance.clearNotifications(); } + function applyCanQuickSave():Void + { + if (menubarItemSaveChart == null) return; + + if (currentWorkingFilePath == null) + { + menubarItemSaveChart.disabled = true; + } + else + { + menubarItemSaveChart.disabled = false; + } + } + + function applyWindowTitle():Void + { + var inner:String = 'New Chart'; + var cwfp:Null = currentWorkingFilePath; + if (cwfp != null) + { + inner = cwfp; + } + if (currentWorkingFilePath == null || saveDataDirty) + { + inner += '*'; + } + WindowUtil.setWindowTitle('Friday Night Funkin\' Chart Editor - ${inner}'); + } + + function resetWindowTitle():Void + { + WindowUtil.setWindowTitle('Friday Night Funkin\''); + } + /** * Convert a note data value into a chart editor grid column number. */ diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx index 5ea125eb4..f82a123a4 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx @@ -217,6 +217,18 @@ class ChartEditorAudioHandler snd.play(); } + public static function wipeInstrumentalData(state:ChartEditorState):Void + { + state.audioInstTrackData.clear(); + stopExistingInstrumental(state); + } + + public static function wipeVocalData(state:ChartEditorState):Void + { + state.audioVocalTrackData.clear(); + stopExistingVocals(state); + } + /** * Create a list of ZIP file entries from the current loaded instrumental tracks in the chart eidtor. * @param state The chart editor state. @@ -226,18 +238,27 @@ class ChartEditorAudioHandler { var zipEntries = []; - for (key in state.audioInstTrackData.keys()) + var instTrackIds = state.audioInstTrackData.keys().array(); + for (key in instTrackIds) { if (key == 'default') { var data:Null = state.audioInstTrackData.get('default'); - if (data == null) continue; + if (data == null) + { + trace('[WARN] Failed to access inst track ($key)'); + continue; + } zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', data)); } else { var data:Null = state.audioInstTrackData.get(key); - if (data == null) continue; + if (data == null) + { + trace('[WARN] Failed to access inst track ($key)'); + continue; + } zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst-${key}.ogg', data)); } } @@ -254,10 +275,15 @@ class ChartEditorAudioHandler { var zipEntries = []; + var vocalTrackIds = state.audioVocalTrackData.keys().array(); for (key in state.audioVocalTrackData.keys()) { var data:Null = state.audioVocalTrackData.get(key); - if (data == null) continue; + if (data == null) + { + trace('[WARN] Failed to access vocal track ($key)'); + continue; + } zipEntries.push(FileUtil.makeZIPEntryFromBytes('Voices-${key}.ogg', data)); } diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx index f5cbccff6..a048bdbbe 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -85,11 +85,73 @@ class ChartEditorDialogHandler var dialog:Null = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable); if (dialog == null) throw 'Could not locate Welcome dialog'; + state.isHaxeUIDialogOpen = true; dialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; // Called when the Welcome dialog is closed while it is closable. state.stopWelcomeMusic(); } + #if sys + var splashRecentContainer:Null = dialog.findComponent('splashRecentContainer', VBox); + if (splashRecentContainer == null) throw 'Could not locate splashRecentContainer in Welcome dialog'; + + for (chartPath in state.previousWorkingFilePaths) + { + if (chartPath == null) continue; + + var linkRecentChart:Link = new Link(); + linkRecentChart.text = chartPath; + linkRecentChart.onClick = function(_event) { + dialog.hideDialog(DialogButton.CANCEL); + state.stopWelcomeMusic(); + + // Load chart from file + var result:Null> = ChartEditorImportExportHandler.loadFromFNFCPath(state, chartPath); + if (result != null) + { + #if !mac + NotificationManager.instance.addNotification( + { + title: 'Success', + body: result.length == 0 ? 'Loaded chart (${chartPath.toString()})' : 'Loaded chart (${chartPath.toString()})\n${result.join("\n")}', + type: result.length == 0 ? NotificationType.Success : NotificationType.Warning, + expiryMs: Constants.NOTIFICATION_DISMISS_TIME + }); + #end + } + else + { + #if !mac + NotificationManager.instance.addNotification( + { + title: 'Failure', + body: 'Failed to load chart (${chartPath.toString()})', + type: NotificationType.Error, + expiryMs: Constants.NOTIFICATION_DISMISS_TIME + }); + #end + } + } + + if (!FileUtil.doesFileExist(chartPath)) + { + trace('Previously loaded chart file (${chartPath}) does not exist, disabling link...'); + linkRecentChart.disabled = true; + } + + splashRecentContainer.addComponent(linkRecentChart); + } + #else + var splashRecentContainer:Null = dialog.findComponent('splashRecentContainer', VBox); + if (splashRecentContainer == null) throw 'Could not locate splashRecentContainer in Welcome dialog'; + + var webLoadLabel:Label = new Label(); + webLoadLabel.text = 'Click the button below to load a chart file (.fnfc) from your computer.'; + + splashRecentContainer.add(webLoadLabel); + #end + // Create New Song "Easy/Normal/Hard" var linkCreateBasic:Null = dialog.findComponent('splashCreateFromSongBasic', Link); if (linkCreateBasic == null) throw 'Could not locate splashCreateFromSongBasic link in Welcome dialog'; @@ -181,6 +243,7 @@ class ChartEditorDialogHandler if (dialog == null) throw 'Could not locate Upload Chart dialog'; dialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { // Simply let the dialog close. @@ -195,6 +258,7 @@ class ChartEditorDialogHandler var buttonCancel:Null