From 8664aed4cc11b2059521039151b96fdc646dbc3e Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Sun, 22 Oct 2023 15:43:39 -0400 Subject: [PATCH 1/8] Implement "Open Recent" menu --- assets | 2 +- source/funkin/save/Save.hx | 308 +++++++++++++++++- .../charting/ChartEditorDialogHandler.hx | 48 +++ .../ui/debug/charting/ChartEditorState.hx | 205 +++++++++--- source/funkin/util/Constants.hx | 5 + source/funkin/util/FileUtil.hx | 21 +- 6 files changed, 527 insertions(+), 62 deletions(-) diff --git a/assets b/assets index 118b62295..c1cea2051 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 118b622953171aaf127cb160538e21bc468620e2 +Subproject commit c1cea20513dfa93e3e74a0db98498b2fd8da50fc diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 54b66605c..dcf7f9f0d 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -1,16 +1,19 @@ package funkin.save; import flixel.util.FlxSave; -import funkin.save.migrator.SaveDataMigrator; -import thx.semver.Version; import funkin.Controls.Device; import funkin.save.migrator.RawSaveData_v1_0_0; +import funkin.save.migrator.SaveDataMigrator; +import funkin.ui.debug.charting.ChartEditorState.LiveInputStyle; +import funkin.ui.debug.charting.ChartEditorThemeHandler.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 +97,18 @@ abstract Save(RawSaveData) optionsChartEditor: { // Reasonable defaults. + previousFiles: [], + noteQuant: 3, + liveInputStyle: LiveInputStyle.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 +139,9 @@ abstract Save(RawSaveData) function set_ngSessionId(value:Null<String>):Null<String> { - return this.api.newgrounds.sessionId = value; + this.api.newgrounds.sessionId = value; + flush(); + return this.api.newgrounds.sessionId; } public var enabledModIds(get, set):Array<String>; @@ -136,7 +153,213 @@ abstract Save(RawSaveData) function set_enabledModIds(value:Array<String>):Array<String> { - return this.mods.enabledMods = value; + this.mods.enabledMods = value; + flush(); + return this.mods.enabledMods; + } + + public var chartEditorPreviousFiles(get, set):Array<String>; + + function get_chartEditorPreviousFiles():Array<String> + { + if (this.optionsChartEditor.previousFiles == null) this.optionsChartEditor.previousFiles = []; + + return this.optionsChartEditor.previousFiles; + } + + function set_chartEditorPreviousFiles(value:Array<String>):Array<String> + { + // 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):LiveInputStyle; + + function get_chartEditorLiveInputStyle():LiveInputStyle + { + if (this.optionsChartEditor.liveInputStyle == null) this.optionsChartEditor.liveInputStyle = LiveInputStyle.None; + + return this.optionsChartEditor.liveInputStyle; + } + + function set_chartEditorLiveInputStyle(value:LiveInputStyle):LiveInputStyle + { + // Set and apply. + this.optionsChartEditor.liveInputStyle = value; + flush(); + return this.optionsChartEditor.liveInputStyle; + } + + 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 +922,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<String>; + + /** + * Note snapping level in the Chart Editor. + * @default `3` + */ + var ?noteQuant:Int; + + /** + * Live input style in the Chart Editor. + * @default `LiveInputStyle.None` + */ + var ?liveInputStyle:LiveInputStyle; + + /** + * 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/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index dd5ddb06c..dd874577a 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -84,11 +84,47 @@ class ChartEditorDialogHandler var dialog:Null<Dialog> = 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<VBox> = dialog.findComponent('splashRecentContainer', VBox); + if (splashRecentContainer == null) throw 'Could not locate splashRecentContainer in Welcome dialog'; + + for (chartPath in state.previousWorkingFilePaths) + { + var linkRecentChart:Link = new FunkinLink(); + linkRecentChart.text = chartPath; + linkRecentChart.onClick = function(_event) { + dialog.hideDialog(DialogButton.CANCEL); + state.stopWelcomeMusic(); + + // Load chart from file + ChartEditorImportExportHandler.loadFromFNFCPath(state, chartPath); + } + + 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<VBox> = 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<Link> = dialog.findComponent('splashCreateFromSongBasic', Link); if (linkCreateBasic == null) throw 'Could not locate splashCreateFromSongBasic link in Welcome dialog'; @@ -180,6 +216,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. @@ -194,6 +231,7 @@ class ChartEditorDialogHandler var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Chart dialog'; + state.isHaxeUIDialogOpen = true; buttonCancel.onClick = function(_event) { dialog.hideDialog(DialogButton.CANCEL); } @@ -232,6 +270,10 @@ class ChartEditorDialogHandler }); #end + trace(selectedFile.name); + trace(selectedFile.text); + trace(selectedFile.isBinary); + trace(selectedFile.fullPath); if (selectedFile.fullPath != null) state.currentWorkingFilePath = selectedFile.fullPath; dialog.hideDialog(DialogButton.APPLY); removeDropHandler(onDropFile); @@ -689,7 +731,9 @@ class ChartEditorDialogHandler var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); if (buttonCancel == null) throw 'Could not locate dialogCancel button in Song Metadata dialog'; + state.isHaxeUIDialogOpen = true; buttonCancel.onClick = function(_event) { + state.isHaxeUIDialogOpen = false; dialog.hideDialog(DialogButton.CANCEL); } @@ -1411,7 +1455,9 @@ class ChartEditorDialogHandler var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); if (buttonCancel == null) throw 'Could not locate dialogCancel button in Import Chart dialog'; + state.isHaxeUIDialogOpen = true; buttonCancel.onClick = function(_event) { + state.isHaxeUIDialogOpen = false; dialog.hideDialog(DialogButton.CANCEL); } @@ -1596,7 +1642,9 @@ class ChartEditorDialogHandler // If all validators succeeded, this callback is called. + state.isHaxeUIDialogOpen = true; variationForm.onSubmit = function(_event) { + state.isHaxeUIDialogOpen = false; trace('Add Variation dialog submitted, validation succeeded!'); var dialogVariationName:Null<TextField> = dialog.findComponent('dialogVariationName', TextField); diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index d392c2c06..831afe738 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1,25 +1,18 @@ package funkin.ui.debug.charting; -import funkin.play.stage.StageData; -import funkin.play.character.CharacterData.CharacterDataParser; -import funkin.play.character.CharacterData; -import flixel.system.FlxAssets.FlxSoundAsset; -import flixel.math.FlxMath; -import haxe.ui.components.TextField; -import haxe.ui.components.DropDown; -import haxe.ui.components.NumberStepper; -import haxe.ui.containers.Frame; import flixel.addons.display.FlxSliceSprite; import flixel.addons.display.FlxTiledSprite; +import flixel.addons.transition.FlxTransitionableState; import flixel.FlxCamera; import flixel.FlxSprite; import flixel.FlxSubState; import flixel.group.FlxSpriteGroup; -import flixel.addons.transition.FlxTransitionableState; import flixel.input.keyboard.FlxKey; +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; @@ -29,28 +22,31 @@ import flixel.util.FlxTimer; import funkin.audio.visualize.PolygonSpectogram; import funkin.audio.VoicesGroup; import funkin.data.notestyle.NoteStyleRegistry; -import funkin.data.notestyle.NoteStyleRegistry; +import funkin.data.song.SongData.SongCharacterData; +import funkin.data.song.SongData.SongChartData; +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongData.SongMetadata; +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongDataUtils; +import funkin.data.song.SongRegistry; import funkin.input.Cursor; 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.HealthIcon; import funkin.play.notes.NoteSprite; import funkin.play.notes.Strumline; import funkin.play.PlayState; import funkin.play.song.Song; -import funkin.data.song.SongData.SongChartData; -import funkin.data.song.SongRegistry; -import funkin.data.song.SongData.SongEventData; -import funkin.data.song.SongData.SongMetadata; -import funkin.data.song.SongData.SongNoteData; -import funkin.data.song.SongData.SongCharacterData; -import funkin.data.song.SongDataUtils; -import funkin.ui.debug.charting.ChartEditorCommand; +import funkin.play.stage.StageData; +import funkin.save.Save; 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.components.FunkinMenuItem; import funkin.ui.haxeui.HaxeUIState; import funkin.util.Constants; import funkin.util.DateUtil; @@ -61,10 +57,14 @@ 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; import haxe.ui.components.Slider; +import haxe.ui.components.TextField; import haxe.ui.containers.dialogs.CollapsibleDialog; -import haxe.ui.containers.menus.MenuItem; +import haxe.ui.containers.Frame; +import haxe.ui.containers.menus.Menu; import haxe.ui.containers.TreeView; import haxe.ui.containers.TreeViewNode; import haxe.ui.core.Component; @@ -594,27 +594,6 @@ class ChartEditorState extends HaxeUIState return selectedCharacter; } - /** - * Whether the user is currently in Pattern Mode. - * This overrides the chart editor's normal behavior. - */ - var isInPatternMode(default, set):Bool = false; - - function set_isInPatternMode(value:Bool):Bool - { - isInPatternMode = value; - - // Make sure view is updated when we change modes. - noteDisplayDirty = true; - notePreviewDirty = true; - notePreviewViewportBoundsDirty = true; - this.scrollPositionInPixels = 0; - - return isInPatternMode; - } - - var currentPattern:String = ''; - /** * Whether the note display render group has been modified and needs to be updated. * This happens when we scroll or add/remove notes, and need to update what notes are displayed and where. @@ -1183,6 +1162,11 @@ class ChartEditorState extends HaxeUIState */ var playbarHeadLayout:Null<Component> = null; + /** + * The submenu in the menubar containing recently opened files. + */ + var menubarOpenRecent:Null<Menu> = null; + /** * The playbar head slider. */ @@ -1237,10 +1221,50 @@ class ChartEditorState extends HaxeUIState */ var params:Null<ChartEditorParams>; + /** + * A list of previous working file paths. + * Also known as the "recent files" list. + */ + public var previousWorkingFilePaths:Array<String> = []; + /** * The current file path which the chart editor is working with. */ - public var currentWorkingFilePath:Null<String>; + public var currentWorkingFilePath(get, set):Null<String>; + + function get_currentWorkingFilePath():Null<String> + { + return previousWorkingFilePaths[0]; + } + + function set_currentWorkingFilePath(value:Null<String>):Null<String> + { + if (value == null) return null; + + if (value == previousWorkingFilePaths[0]) return value; + + 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 oldest path. + previousWorkingFilePaths.pop(); + } + + populateOpenRecentMenu(); + + return value; + } public function new(?params:ChartEditorParams) { @@ -1260,6 +1284,8 @@ class ChartEditorState extends HaxeUIState // Show the mouse cursor. Cursor.show(); + loadPreferences(); + fixCamera(); // Get rid of any music from the previous state. @@ -1280,6 +1306,7 @@ class ChartEditorState extends HaxeUIState buildSelectionBox(); buildAdditionalUI(); + populateOpenRecentMenu(); // Setup the onClick listeners for the UI after it's been created. setupUIListeners(); @@ -1322,8 +1349,80 @@ class ChartEditorState extends HaxeUIState { this.welcomeMusic.loadEmbedded(Paths.music('chartEditorLoop/chartEditorLoop')); this.welcomeMusic.looped = true; - // this.welcomeMusic.play(); - // fadeInWelcomeMusic(); + } + + public function loadPreferences():Void + { + var save:Save = Save.get(); + + previousWorkingFilePaths = 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(); + + save.chartEditorPreviousFiles = previousWorkingFilePaths; + 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.clear(); + + for (chartPath in previousWorkingFilePaths) + { + var menuItemRecentChart:FunkinMenuItem = new FunkinMenuItem(); + menuItemRecentChart.text = chartPath; + menuItemRecentChart.onClick = function(_event) { + stopWelcomeMusic(); + + // Load chart from file + ChartEditorImportExportHandler.loadFromFNFCPath(this, chartPath); + } + + 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 } public function fadeInWelcomeMusic():Void @@ -1540,7 +1639,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) { @@ -1646,9 +1748,11 @@ class ChartEditorState extends HaxeUIState add(playbarHeadLayout); + menubarOpenRecent = findComponent('menubarOpenRecent', Menu); + if (menubarOpenRecent == null) throw "Could not find menubarOpenRecent!"; + // Setup notifications. @:privateAccess - // NotificationManager.GUTTER_SIZE = 56; NotificationManager.GUTTER_SIZE = 20; } @@ -1886,10 +1990,13 @@ class ChartEditorState extends HaxeUIState { saveDataDirty = false; - // Auto-save the chart. + // 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); @@ -3835,7 +3942,7 @@ class ChartEditorState extends HaxeUIState commandHistoryDirty = false; // Update the Undo and Redo buttons. - var undoButton:Null<MenuItem> = findComponent('menubarItemUndo', MenuItem); + var undoButton:Null<FunkinMenuItem> = findComponent('menubarItemUndo', FunkinMenuItem); if (undoButton != null) { @@ -3857,7 +3964,7 @@ class ChartEditorState extends HaxeUIState trace('undoButton is null'); } - var redoButton:Null<MenuItem> = findComponent('menubarItemRedo', MenuItem); + var redoButton:Null<FunkinMenuItem> = findComponent('menubarItemRedo', FunkinMenuItem); if (redoButton != null) { diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index edd95f946..d37505b4c 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -391,6 +391,11 @@ class Constants */ public static final GHOST_TAPPING:Bool = false; + /** + * The maximum number of previous file paths for the Chart Editor to remember. + */ + public static final MAX_PREVIOUS_WORKING_FILES:Int = 10; + /** * The separator between an asset library and the asset path. */ diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx index 72c9c43f1..5fae983d4 100644 --- a/source/funkin/util/FileUtil.hx +++ b/source/funkin/util/FileUtil.hx @@ -344,13 +344,22 @@ class FileUtil public static function readBytesFromPath(path:String):Bytes { #if sys - if (!sys.FileSystem.exists(path)) return null; + if (!doesFileExist(path)) return null; return sys.io.File.getBytes(path); #else return null; #end } + public static function doesFileExist(path:String):Bool + { + #if sys + return sys.FileSystem.exists(path); + #else + return false; + #end + } + /** * Browse for a file to read and execute a callback once we have a file reference. * Works great on HTML5 or desktop. @@ -434,7 +443,7 @@ class FileUtil case Force: sys.io.File.saveContent(path, data); case Skip: - if (!sys.FileSystem.exists(path)) + if (!doesFileExist(path)) { sys.io.File.saveContent(path, data); } @@ -443,7 +452,7 @@ class FileUtil throw 'File already exists: $path'; } case Ask: - if (sys.FileSystem.exists(path)) + if (doesFileExist(path)) { // TODO: We don't have the technology to use native popups yet. } @@ -475,7 +484,7 @@ class FileUtil case Force: sys.io.File.saveBytes(path, data); case Skip: - if (!sys.FileSystem.exists(path)) + if (!doesFileExist(path)) { sys.io.File.saveBytes(path, data); } @@ -484,7 +493,7 @@ class FileUtil throw 'File already exists: $path'; } case Ask: - if (sys.FileSystem.exists(path)) + if (doesFileExist(path)) { // TODO: We don't have the technology to use native popups yet. } @@ -523,7 +532,7 @@ class FileUtil public static function createDirIfNotExists(dir:String):Void { #if sys - if (!sys.FileSystem.exists(dir)) + if (!doesFileExist(dir)) { sys.FileSystem.createDirectory(dir); } From a9e253a53078aa9d22dc6dce2f43dc920349770a Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Mon, 23 Oct 2023 12:22:29 -0400 Subject: [PATCH 2/8] Save handling (with window title!) --- .../charting/ChartEditorDialogHandler.hx | 8 +-- .../ChartEditorImportExportHandler.hx | 10 ++- .../ui/debug/charting/ChartEditorState.hx | 66 +++++++++++++++++-- 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index dd874577a..bfb860c92 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -97,6 +97,8 @@ class ChartEditorDialogHandler for (chartPath in state.previousWorkingFilePaths) { + if (chartPath == null) continue; + var linkRecentChart:Link = new FunkinLink(); linkRecentChart.text = chartPath; linkRecentChart.onClick = function(_event) { @@ -270,10 +272,6 @@ class ChartEditorDialogHandler }); #end - trace(selectedFile.name); - trace(selectedFile.text); - trace(selectedFile.isBinary); - trace(selectedFile.fullPath); if (selectedFile.fullPath != null) state.currentWorkingFilePath = selectedFile.fullPath; dialog.hideDialog(DialogButton.APPLY); removeDropHandler(onDropFile); @@ -437,6 +435,7 @@ class ChartEditorDialogHandler var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog uploadVocalsDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; + state.currentWorkingFilePath = null; // New file, so no path. state.switchToCurrentInstrumental(); state.postLoadInstrumental(); } @@ -495,6 +494,7 @@ class ChartEditorDialogHandler var uploadVocalsDialogErect:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog uploadVocalsDialogErect.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; + state.currentWorkingFilePath = null; // New file, so no path. state.switchToCurrentInstrumental(); state.postLoadInstrumental(); } diff --git a/source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx index c5cbdd5de..6e9022457 100644 --- a/source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx @@ -361,7 +361,15 @@ class ChartEditorImportExportHandler { // Prompt and save. var onSave:Array<String>->Void = function(paths:Array<String>) { - trace('Successfully exported files.'); + if (paths.length != 1) + { + trace('[WARN] Could not get save path.'); + } + else + { + state.currentWorkingFilePath = paths[0]; + state.applyWindowTitle(); + } }; var onCancel:Void->Void = function() { diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 831afe738..fb91e763d 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1167,6 +1167,11 @@ class ChartEditorState extends HaxeUIState */ var menubarOpenRecent:Null<Menu> = null; + /** + * The item in the menubar to save the currently opened chart. + */ + var menubarItemSave:Null<FunkinMenuItem> = null; + /** * The playbar head slider. */ @@ -1225,10 +1230,21 @@ class ChartEditorState extends HaxeUIState * A list of previous working file paths. * Also known as the "recent files" list. */ - public var previousWorkingFilePaths:Array<String> = []; + public var previousWorkingFilePaths(default, set):Array<String> = []; + + function set_previousWorkingFilePaths(value:Array<String>):Array<String> + { + // 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<String>; @@ -1239,10 +1255,14 @@ class ChartEditorState extends HaxeUIState function set_currentWorkingFilePath(value:Null<String>):Null<String> { - if (value == null) return null; - if (value == previousWorkingFilePaths[0]) return value; + if (previousWorkingFilePaths.contains(null)) + { + // Filter all instances of `null` from the array. + previousWorkingFilePaths = previousWorkingFilePaths.filter((x) -> x != null); + } + if (previousWorkingFilePaths.contains(value)) { // Move the path to the front of the list. @@ -1257,11 +1277,12 @@ class ChartEditorState extends HaxeUIState while (previousWorkingFilePaths.length > Constants.MAX_PREVIOUS_WORKING_FILES) { - // Remove the oldest path. + // Remove the last path in the list. previousWorkingFilePaths.pop(); } populateOpenRecentMenu(); + applyWindowTitle(); return value; } @@ -1395,10 +1416,12 @@ class ChartEditorState extends HaxeUIState if (menubarOpenRecent == null) return; #if sys - menubarOpenRecent.clear(); + menubarOpenRecent.removeAllComponents(); for (chartPath in previousWorkingFilePaths) { + if (chartPath == null) continue; + var menuItemRecentChart:FunkinMenuItem = new FunkinMenuItem(); menuItemRecentChart.text = chartPath; menuItemRecentChart.onClick = function(_event) { @@ -1751,6 +1774,9 @@ class ChartEditorState extends HaxeUIState menubarOpenRecent = findComponent('menubarOpenRecent', Menu); if (menubarOpenRecent == null) throw "Could not find menubarOpenRecent!"; + menubarItemSave = findComponent('menubarItemSave', FunkinMenuItem); + if (menubarItemSave == null) throw "Could not find menubarItemSave!"; + // Setup notifications. @:privateAccess NotificationManager.GUTTER_SIZE = 20; @@ -1783,6 +1809,16 @@ class ChartEditorState extends HaxeUIState 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)); @@ -4495,6 +4531,26 @@ class ChartEditorState extends HaxeUIState { NotificationManager.instance.clearNotifications(); } + + function applyCanQuickSave():Void + { + if (currentWorkingFilePath == null) {} + else {} + } + + function applyWindowTitle():Void + { + var inner:String = (currentSongMetadata.songName != null) ? currentSongMetadata.songName : 'Untitled'; + if (currentWorkingFilePath == null) + { + inner += '*'; + } + else + { + inner += ' (${currentWorkingFilePath})'; + } + WindowUtil.setWindowTitle('FNF Chart Editor - ${inner}'); + } } enum LiveInputStyle From 818ae6ddd9b6191df17d513c310fcc5fcb33dfcd Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Mon, 23 Oct 2023 12:23:06 -0400 Subject: [PATCH 3/8] Update assets --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index c1cea2051..fd745fcb1 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit c1cea20513dfa93e3e74a0db98498b2fd8da50fc +Subproject commit fd745fcb16c6a0de73449ae833ce1d92f022d9d6 From 33a1b81737cf55aee328b5839eab99ac186182b3 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Tue, 24 Oct 2023 15:50:02 -0400 Subject: [PATCH 4/8] Fixes for quicksave --- assets | 2 +- .../debug/charting/ChartEditorAudioHandler.hx | 34 ++++- .../charting/ChartEditorDialogHandler.hx | 54 ++++++- .../ChartEditorImportExportHandler.hx | 57 +++++--- .../ui/debug/charting/ChartEditorState.hx | 132 ++++++++++++++---- source/funkin/util/FileUtil.hx | 22 +-- 6 files changed, 238 insertions(+), 63 deletions(-) diff --git a/assets b/assets index fd745fcb1..8e8aeb064 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit fd745fcb16c6a0de73449ae833ce1d92f022d9d6 +Subproject commit 8e8aeb06472ca294c569818cbefb1bb3dfce7854 diff --git a/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx index 6f390e604..1ceeadd5f 100644 --- a/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx @@ -218,6 +218,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); + } + /** * Convert byte data into a playable sound. * @@ -238,18 +250,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<Bytes> = 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<Bytes> = 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)); } } @@ -261,10 +282,15 @@ class ChartEditorAudioHandler { var zipEntries = []; + var vocalTrackIds = state.audioVocalTrackData.keys().array(); for (key in state.audioVocalTrackData.keys()) { var data:Null<Bytes> = 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/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index bfb860c92..0dd916ba1 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -106,7 +106,31 @@ class ChartEditorDialogHandler state.stopWelcomeMusic(); // Load chart from file - ChartEditorImportExportHandler.loadFromFNFCPath(state, chartPath); + var result:Null<Array<String>> = 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: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + #end + } + else + { + #if !mac + NotificationManager.instance.addNotification( + { + title: 'Failure', + body: 'Failed to load chart (${chartPath.toString()})', + type: NotificationType.Error, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + #end + } } if (!FileUtil.doesFileExist(chartPath)) @@ -260,7 +284,8 @@ class ChartEditorDialogHandler { try { - if (ChartEditorImportExportHandler.loadFromFNFC(state, selectedFile.bytes)) + var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFC(state, selectedFile.bytes); + if (result != null) { #if !mac NotificationManager.instance.addNotification( @@ -299,21 +324,33 @@ class ChartEditorDialogHandler try { - if (ChartEditorImportExportHandler.loadFromFNFCPath(state, path.toString())) + var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(state, path.toString()); + if (result != null) { #if !mac NotificationManager.instance.addNotification( { title: 'Success', - body: 'Loaded chart (${path.toString()})', - type: NotificationType.Success, + body: result.length == 0 ? 'Loaded chart (${path.toString()})' : 'Loaded chart (${path.toString()})\n${result.join("\n")}', + type: result.length == 0 ? NotificationType.Success : NotificationType.Warning, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); #end - dialog.hideDialog(DialogButton.APPLY); removeDropHandler(onDropFile); } + else + { + #if !mac + NotificationManager.instance.addNotification( + { + title: 'Failure', + body: 'Failed to load chart (${path.toString()})', + type: NotificationType.Error, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + #end + } } catch (err) { @@ -359,6 +396,8 @@ class ChartEditorDialogHandler var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog uploadVocalsDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; + state.currentWorkingFilePath = null; // Built from parts, so no .fnfc to save to. + state.switchToCurrentInstrumental(); state.postLoadInstrumental(); } } @@ -398,6 +437,8 @@ class ChartEditorDialogHandler var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog uploadVocalsDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; + state.currentWorkingFilePath = null; // New file, so no path. + state.switchToCurrentInstrumental(); state.postLoadInstrumental(); } } @@ -848,6 +889,7 @@ class ChartEditorDialogHandler var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button); if (dialogContinue == null) throw 'Could not locate dialogContinue button in Song Metadata dialog'; dialogContinue.onClick = (_event) -> { + state.songMetadata.clear(); state.songMetadata.set(targetVariation, newSongMetadata); Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); diff --git a/source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx index 6e9022457..26dde114d 100644 --- a/source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx @@ -7,7 +7,7 @@ import haxe.io.Path; import funkin.util.SerializerUtil; import haxe.ui.notifications.NotificationManager; import funkin.util.FileUtil; -import funkin.util.FileUtil; +import funkin.util.FileUtil.FileWriteMode; import haxe.io.Bytes; import funkin.play.song.Song; import funkin.data.song.SongData.SongChartData; @@ -53,7 +53,8 @@ class ChartEditorImportExportHandler state.sortChartData(); - state.clearVocals(); + ChartEditorAudioHandler.wipeInstrumentalData(state); + ChartEditorAudioHandler.wipeVocalData(state); var variations:Array<String> = state.availableVariations; for (variation in variations) @@ -91,7 +92,10 @@ class ChartEditorImportExportHandler } } + state.isHaxeUIDialogOpen = false; + state.currentWorkingFilePath = null; // New file, so no path. state.switchToCurrentInstrumental(); + state.postLoadInstrumental(); state.refreshMetadataToolbox(); @@ -138,31 +142,40 @@ class ChartEditorImportExportHandler } } - public static function loadFromFNFCPath(state:ChartEditorState, path:String):Bool + /** + * Load a chart's metadata, chart data, and audio from an FNFC file path. + * @param state + * @param path + * @return `null` on failure, `[]` on success, `[warnings]` on success with warnings. + */ + public static function loadFromFNFCPath(state:ChartEditorState, path:String):Null<Array<String>> { var bytes:Null<Bytes> = FileUtil.readBytesFromPath(path); - if (bytes == null) return false; + if (bytes == null) return null; trace('Loaded ${bytes.length} bytes from $path'); - var result:Bool = loadFromFNFC(state, bytes); - if (result) + var result:Null<Array<String>> = loadFromFNFC(state, bytes); + if (result != null) { state.currentWorkingFilePath = path; + state.saveDataDirty = false; // Just loaded file! } return result; } /** - * Load a chart's metadata, chart data, and audio from an FNFC archive.. + * Load a chart's metadata, chart data, and audio from an FNFC archive. * @param state * @param bytes * @param instId - * @return Bool + * @return `null` on failure, `[]` on success, `[warnings]` on success with warnings. */ - public static function loadFromFNFC(state:ChartEditorState, bytes:Bytes):Bool + public static function loadFromFNFC(state:ChartEditorState, bytes:Bytes):Null<Array<String>> { + var warnings:Array<String> = []; + var songMetadatas:Map<String, SongMetadata> = []; var songChartDatas:Map<String, SongChartData> = []; @@ -231,8 +244,8 @@ class ChartEditorImportExportHandler songChartDatas.set(variation, variChartData); } - ChartEditorAudioHandler.stopExistingInstrumental(state); - ChartEditorAudioHandler.stopExistingVocals(state); + ChartEditorAudioHandler.wipeInstrumentalData(state); + ChartEditorAudioHandler.wipeVocalData(state); // Load instrumentals for (variation in [Constants.DEFAULT_VARIATION].concat(variationList)) @@ -264,12 +277,14 @@ class ChartEditorImportExportHandler { if (!ChartEditorAudioHandler.loadVocalsFromBytes(state, playerVocalsFileBytes, playerCharId, instId)) { - throw 'Could not load vocals ($playerCharId).'; + warnings.push('Could not parse vocals ($playerCharId).'); + // throw 'Could not parse vocals ($playerCharId).'; } } else { - throw 'Could not find vocals ($playerVocalsFileName).'; + warnings.push('Could not find vocals ($playerVocalsFileName).'); + // throw 'Could not find vocals ($playerVocalsFileName).'; } if (opponentCharId != null) @@ -280,12 +295,14 @@ class ChartEditorImportExportHandler { if (!ChartEditorAudioHandler.loadVocalsFromBytes(state, opponentVocalsFileBytes, opponentCharId, instId)) { - throw 'Could not load vocals ($opponentCharId).'; + warnings.push('Could not parse vocals ($opponentCharId).'); + // throw 'Could not parse vocals ($opponentCharId).'; } } else { - throw 'Could not load vocals ($playerCharId-$instId).'; + warnings.push('Could not find vocals ($opponentVocalsFileName).'); + // throw 'Could not find vocals ($opponentVocalsFileName).'; } } } @@ -297,7 +314,7 @@ class ChartEditorImportExportHandler state.switchToCurrentInstrumental(); - return true; + return warnings; } /** @@ -345,8 +362,10 @@ class ChartEditorImportExportHandler if (force) { + var targetMode:FileWriteMode = Force; if (targetPath == null) { + targetMode = Skip; targetPath = Path.join([ './backups/', 'chart-editor-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}' @@ -355,7 +374,8 @@ class ChartEditorImportExportHandler // We have to force write because the program will die before the save dialog is closed. trace('Force exporting to $targetPath...'); - FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath); + FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode); + state.saveDataDirty = false; } else { @@ -364,9 +384,11 @@ class ChartEditorImportExportHandler if (paths.length != 1) { trace('[WARN] Could not get save path.'); + state.applyWindowTitle(); } else { + trace('Saved to "${paths[0]}"'); state.currentWorkingFilePath = paths[0]; state.applyWindowTitle(); } @@ -380,6 +402,7 @@ class ChartEditorImportExportHandler try { FileUtil.saveChartAsFNFC(zipEntries, onSave, onCancel, '${state.currentSongId}.${Constants.EXT_CHART}'); + state.saveDataDirty = false; } catch (e) {} } diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index fb91e763d..7babd6038 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -644,7 +644,9 @@ class ChartEditorState extends HaxeUIState } } - return saveDataDirty = value; + saveDataDirty = value; + applyWindowTitle(); + return saveDataDirty; } /** @@ -882,7 +884,7 @@ class ChartEditorState extends HaxeUIState var result:Null<SongMetadata> = 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; @@ -1170,7 +1172,7 @@ class ChartEditorState extends HaxeUIState /** * The item in the menubar to save the currently opened chart. */ - var menubarItemSave:Null<FunkinMenuItem> = null; + var menubarItemSaveChart:Null<FunkinMenuItem> = null; /** * The playbar head slider. @@ -1229,10 +1231,11 @@ class ChartEditorState extends HaxeUIState /** * 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 previousWorkingFilePaths(default, set):Array<String> = []; + public var previousWorkingFilePaths(default, set):Array<Null<String>> = [null]; - function set_previousWorkingFilePaths(value:Array<String>):Array<String> + function set_previousWorkingFilePaths(value:Array<Null<String>>):Array<Null<String>> { // Called only when the WHOLE LIST is overridden. previousWorkingFilePaths = value; @@ -1260,7 +1263,9 @@ class ChartEditorState extends HaxeUIState if (previousWorkingFilePaths.contains(null)) { // Filter all instances of `null` from the array. - previousWorkingFilePaths = previousWorkingFilePaths.filter((x) -> x != null); + previousWorkingFilePaths = previousWorkingFilePaths.filter(function(x:Null<String>):Bool { + return x != null; + }); } if (previousWorkingFilePaths.contains(value)) @@ -1340,22 +1345,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<Array<String>> = 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: ChartEditorState.NOTIFICATION_DISMISS_TIME }); #end } else { + #if !mac + NotificationManager.instance.addNotification( + { + title: 'Failure', + body: 'Failed to load chart (${params.fnfcTargetPath})', + type: NotificationType.Error, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + #end + // Song failed to load, open the Welcome dialog so we aren't in a broken state. ChartEditorDialogHandler.openWelcomeDialog(this, false); } @@ -1376,7 +1390,14 @@ class ChartEditorState extends HaxeUIState { var save:Save = Save.get(); - previousWorkingFilePaths = save.chartEditorPreviousFiles; + if (previousWorkingFilePaths[0] == null) + { + previousWorkingFilePaths = [null].concat(save.chartEditorPreviousFiles); + } + else + { + previousWorkingFilePaths = [currentWorkingFilePath].concat(save.chartEditorPreviousFiles); + } noteSnapQuantIndex = save.chartEditorNoteQuant; currentLiveInputStyle = save.chartEditorLiveInputStyle; isViewDownscroll = save.chartEditorDownscroll; @@ -1396,7 +1417,12 @@ class ChartEditorState extends HaxeUIState { var save:Save = Save.get(); - save.chartEditorPreviousFiles = previousWorkingFilePaths; + // Can't use filter() because of null safety checking! + var filteredWorkingFilePaths:Array<String> = []; + for (chartPath in previousWorkingFilePaths) + if (chartPath != null) filteredWorkingFilePaths.push(chartPath); + + save.chartEditorPreviousFiles = filteredWorkingFilePaths; save.chartEditorNoteQuant = noteSnapQuantIndex; save.chartEditorLiveInputStyle = currentLiveInputStyle; save.chartEditorDownscroll = isViewDownscroll; @@ -1428,7 +1454,31 @@ class ChartEditorState extends HaxeUIState stopWelcomeMusic(); // Load chart from file - ChartEditorImportExportHandler.loadFromFNFCPath(this, chartPath); + var result:Null<Array<String>> = 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: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + #end + } + else + { + #if !mac + NotificationManager.instance.addNotification( + { + title: 'Failure', + body: 'Failed to load chart (${chartPath.toString()})', + type: NotificationType.Error, + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME + }); + #end + } } if (!FileUtil.doesFileExist(chartPath)) @@ -1774,8 +1824,8 @@ class ChartEditorState extends HaxeUIState menubarOpenRecent = findComponent('menubarOpenRecent', Menu); if (menubarOpenRecent == null) throw "Could not find menubarOpenRecent!"; - menubarItemSave = findComponent('menubarItemSave', FunkinMenuItem); - if (menubarItemSave == null) throw "Could not find menubarItemSave!"; + menubarItemSaveChart = findComponent('menubarItemSaveChart', FunkinMenuItem); + if (menubarItemSaveChart == null) throw "Could not find menubarItemSaveChart!"; // Setup notifications. @:privateAccess @@ -3340,12 +3390,24 @@ class ChartEditorState extends HaxeUIState ChartEditorDialogHandler.openBrowseFNFC(this, 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) { ChartEditorImportExportHandler.exportAllSongData(this, false); } - // CTRL + Q = Quit to Menu if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.Q) { @@ -3361,6 +3423,8 @@ class ChartEditorState extends HaxeUIState // TODO: PR Flixel to make onComplete nullable. if (audioInstTrack != null) audioInstTrack.onComplete = null; FlxG.switchState(new MainMenuState()); + + resetWindowTitle(); } /** @@ -4534,22 +4598,36 @@ class ChartEditorState extends HaxeUIState function applyCanQuickSave():Void { - if (currentWorkingFilePath == null) {} - else {} + if (menubarItemSaveChart == null) return; + + if (currentWorkingFilePath == null) + { + menubarItemSaveChart.disabled = true; + } + else + { + menubarItemSaveChart.disabled = false; + } } function applyWindowTitle():Void { - var inner:String = (currentSongMetadata.songName != null) ? currentSongMetadata.songName : 'Untitled'; - if (currentWorkingFilePath == null) + var inner:String = 'New Chart'; + var cwfp:Null<String> = currentWorkingFilePath; + if (cwfp != null) + { + inner = cwfp; + } + if (currentWorkingFilePath == null || saveDataDirty) { inner += '*'; } - else - { - inner += ' (${currentWorkingFilePath})'; - } - WindowUtil.setWindowTitle('FNF Chart Editor - ${inner}'); + WindowUtil.setWindowTitle('Friday Night Funkin\' Chart Editor - ${inner}'); + } + + function resetWindowTitle():Void + { + WindowUtil.setWindowTitle('Friday Night Funkin\''); } } diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx index 5fae983d4..8c41cc363 100644 --- a/source/funkin/util/FileUtil.hx +++ b/source/funkin/util/FileUtil.hx @@ -101,7 +101,7 @@ class FileUtil } /** - * Browses for a file location to save to, then calls `onSelect(path)` when a path chosen. + * Browses for a file location to save to, then calls `onSave(path)` when a path chosen. * Note that on HTML5 you can't do much with this, you should call `saveFile(resource:haxe.io.Bytes)` instead. * * @param typeFilter TODO What does this do? @@ -183,7 +183,7 @@ class FileUtil var filter:String = convertTypeFilter(typeFilter); var fileDialog:FileDialog = new FileDialog(); - if (onSave != null) fileDialog.onSelect.add(onSave); + if (onSave != null) fileDialog.onSave.add(onSave); if (onCancel != null) fileDialog.onCancel.add(onCancel); fileDialog.save(data, filter, defaultFileName, dialogTitle); @@ -268,7 +268,8 @@ class FileUtil var zipBytes:Bytes = createZIPFromEntries(resources); var onSave:String->Void = function(path:String) { - onSave([path]); + trace('Saved ${resources.length} files to ZIP at "$path".'); + if (onSave != null) onSave([path]); }; // Prompt the user to save the ZIP file. @@ -287,7 +288,8 @@ class FileUtil var zipBytes:Bytes = createZIPFromEntries(resources); var onSave:String->Void = function(path:String) { - onSave([path]); + trace('Saved FNF file to "$path"'); + if (onSave != null) onSave([path]); }; // Prompt the user to save the ZIP file. @@ -302,14 +304,14 @@ class FileUtil * Use `saveFilesAsZIP` instead. * @param force Whether to force overwrite an existing file. */ - public static function saveFilesAsZIPToPath(resources:Array<Entry>, path:String, force:Bool = false):Bool + public static function saveFilesAsZIPToPath(resources:Array<Entry>, path:String, mode:FileWriteMode = Skip):Bool { #if desktop // Create a ZIP file. var zipBytes:Bytes = createZIPFromEntries(resources); // Write the ZIP. - writeBytesToPath(path, zipBytes, force ? Force : Skip); + writeBytesToPath(path, zipBytes, mode); return true; #else @@ -449,12 +451,14 @@ class FileUtil } else { - throw 'File already exists: $path'; + // Do nothing. + // throw 'File already exists: $path'; } case Ask: if (doesFileExist(path)) { // TODO: We don't have the technology to use native popups yet. + throw 'File already exists: $path'; } else { @@ -490,12 +494,14 @@ class FileUtil } else { - throw 'File already exists: $path'; + // Do nothing. + // throw 'File already exists: $path'; } case Ask: if (doesFileExist(path)) { // TODO: We don't have the technology to use native popups yet. + throw 'File already exists: $path'; } else { From 3e1146aadf90cc84ddd38bb3f02da8133b6f9862 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Tue, 7 Nov 2023 18:32:00 -0500 Subject: [PATCH 5/8] Fix bug where getDirection didn't work in scripts. --- assets | 2 +- source/funkin/data/song/SongData.hx | 32 ++++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/assets b/assets index e634c8f50..5d5a860af 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit e634c8f50c34845097283e0f411e1f89409e1498 +Subproject commit 5d5a860af517ef0cf2aa39c537eb228c7ae803d0 diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 783f52a64..29ca28036 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -658,6 +658,22 @@ class SongNoteDataRaw this.kind = kind; } + /** + * The direction of the note, if applicable. + * Strips the strumline index from the data. + * + * 0 = left, 1 = down, 2 = up, 3 = right + */ + public inline function getDirection(strumlineSize:Int = 4):Int + { + return this.data % strumlineSize; + } + + public function getDirectionName(strumlineSize:Int = 4):String + { + return SongNoteData.buildDirectionName(this.data, strumlineSize); + } + @:jignored var _stepTime:Null<Float> = null; @@ -714,22 +730,6 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw this = new SongNoteDataRaw(time, data, length, kind); } - /** - * The direction of the note, if applicable. - * Strips the strumline index from the data. - * - * 0 = left, 1 = down, 2 = up, 3 = right - */ - public inline function getDirection(strumlineSize:Int = 4):Int - { - return this.data % strumlineSize; - } - - public function getDirectionName(strumlineSize:Int = 4):String - { - return SongNoteData.buildDirectionName(this.data, strumlineSize); - } - public static function buildDirectionName(data:Int, strumlineSize:Int = 4):String { switch (data % strumlineSize) From 2c9b8a115fde80f9ad75494164b791fa5fe55f1e Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Wed, 15 Nov 2023 00:49:23 -0500 Subject: [PATCH 6/8] Update assets commit --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index 5d5a860af..fb0a5fb09 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 5d5a860af517ef0cf2aa39c537eb228c7ae803d0 +Subproject commit fb0a5fb09fb966e95433d9cfb45826a89a1c5870 From 398b2e386e840c07c9522eb922b060253953dca3 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Wed, 15 Nov 2023 11:30:38 -0500 Subject: [PATCH 7/8] Remove wonky Spectrogram code --- .../ui/debug/charting/ChartEditorState.hx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index aa5372327..67c4e1fc4 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1237,11 +1237,6 @@ class ChartEditorState extends HaxeUIState */ var gridGhostEvent:Null<ChartEditorEventSprite> = null; - /** - * The waveform which (optionally) displays over the grid, underneath the notes and playhead. - */ - var gridSpectrogram:Null<PolygonSpectogram> = null; - /** * The sprite used to display the note preview area. * We move this up and down to scroll the preview. @@ -1480,7 +1475,6 @@ class ChartEditorState extends HaxeUIState this.updateTheme(); buildGrid(); - // buildSpectrogram(audioInstTrack); buildNotePreview(); buildSelectionBox(); @@ -1881,16 +1875,6 @@ class ChartEditorState extends HaxeUIState } } - function buildSpectrogram(target:FlxSound):Void - { - gridSpectrogram = new PolygonSpectogram(FlxG.sound.music, FlxColor.RED, FlxG.height / 2, Math.floor(FlxG.height / 2)); - gridSpectrogram.x += 170; - gridSpectrogram.scrollFactor.set(); - gridSpectrogram.waveAmplitude = 50; - gridSpectrogram.visType = UPDATED; - add(gridSpectrogram); - } - /** * Builds the group that will hold all the notes. */ @@ -4857,8 +4841,6 @@ class ChartEditorState extends HaxeUIState gridPlayheadScrollArea.setGraphicSize(Std.int(gridPlayheadScrollArea.width), songLengthInPixels); gridPlayheadScrollArea.updateHitbox(); } - - buildSpectrogram(audioInstTrack); } else { From add4036911a63f14a61494cd292d6c5b47514725 Mon Sep 17 00:00:00 2001 From: Cameron Taylor <cameron.taylor.ninja@gmail.com> Date: Wed, 15 Nov 2023 19:51:20 -0500 Subject: [PATCH 8/8] assets update --- assets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets b/assets index fb0a5fb09..3b05b0fdd 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit fb0a5fb09fb966e95433d9cfb45826a89a1c5870 +Subproject commit 3b05b0fdd8e3b2cd09b9e4e415c186bae8e3b7d3