From 6b3c15348b76380aef6a9948b4c8e2594d551df3 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Mon, 2 Jan 2023 17:40:53 -0500 Subject: [PATCH] Autosave and save-on-quit for chart editor --- Project.xml | 8 - source/funkin/Alphabet.hx | 2 - source/funkin/CoolUtil.hx | 2 - source/funkin/CutsceneCharacter.hx | 2 - source/funkin/DialogueBox.hx | 2 - source/funkin/Discord.hx | 3 - source/funkin/FreeplayState.hx | 2 - source/funkin/InitState.hx | 13 +- source/funkin/LoadingState.hx | 9 +- source/funkin/MainMenuState.hx | 3 - source/funkin/NGio.hx | 2 - source/funkin/Note.hx | 2 - source/funkin/SongLoad.hx | 2 - source/funkin/StoryMenuState.hx | 3 - source/funkin/TitleState.hx | 2 - source/funkin/api/newgrounds/NGUnsafe.hx | 2 - source/funkin/api/newgrounds/NGUtil.hx | 2 - source/funkin/charting/ChartingState.hx | 1 - source/funkin/freeplayStuff/FreeplayScore.hx | 2 - source/funkin/modding/PolymodHandler.hx | 8 +- source/funkin/play/Countdown.hx | 2 - source/funkin/play/GameOverSubstate.hx | 2 - source/funkin/play/PlayState.hx | 3 - source/funkin/play/character/BaseCharacter.hx | 2 - source/funkin/play/character/CharacterData.hx | 4 +- source/funkin/play/song/SongData.hx | 7 +- source/funkin/play/stage/StageData.hx | 2 - source/funkin/ui/PopUpStuff.hx | 2 - .../ui/animDebugShit/DebugBoundingState.hx | 1 - .../ui/debug/charting/ChartEditorCommand.hx | 14 ++ .../ui/debug/charting/ChartEditorState.hx | 120 +++++++++- source/funkin/util/DateUtil.hx | 11 + source/funkin/util/FileUtil.hx | 223 ++++++++++++++---- source/funkin/util/WindowUtil.hx | 20 +- source/funkin/util/assets/DataAssets.hx | 2 - 35 files changed, 356 insertions(+), 131 deletions(-) create mode 100644 source/funkin/util/DateUtil.hx diff --git a/Project.xml b/Project.xml index 73b384ee0..d7ee924f5 100644 --- a/Project.xml +++ b/Project.xml @@ -135,14 +135,6 @@ - - - - diff --git a/source/funkin/Alphabet.hx b/source/funkin/Alphabet.hx index cba2025a6..85861720a 100644 --- a/source/funkin/Alphabet.hx +++ b/source/funkin/Alphabet.hx @@ -5,8 +5,6 @@ import flixel.group.FlxSpriteGroup; import flixel.math.FlxMath; import flixel.util.FlxTimer; -using StringTools; - /** * Loosley based on FlxTypeText lolol */ diff --git a/source/funkin/CoolUtil.hx b/source/funkin/CoolUtil.hx index a35364e5f..fbe703803 100644 --- a/source/funkin/CoolUtil.hx +++ b/source/funkin/CoolUtil.hx @@ -18,8 +18,6 @@ import lime.math.Rectangle; import lime.utils.Assets; import openfl.filters.ShaderFilter; -using StringTools; - class CoolUtil { public static var difficultyArray:Array = ['EASY', "NORMAL", "HARD"]; diff --git a/source/funkin/CutsceneCharacter.hx b/source/funkin/CutsceneCharacter.hx index cf2caa870..37fd190af 100644 --- a/source/funkin/CutsceneCharacter.hx +++ b/source/funkin/CutsceneCharacter.hx @@ -4,8 +4,6 @@ import flixel.FlxSprite; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.math.FlxPoint; -using StringTools; - class CutsceneCharacter extends FlxTypedGroup { public var coolPos:FlxPoint = FlxPoint.get(); diff --git a/source/funkin/DialogueBox.hx b/source/funkin/DialogueBox.hx index 599077563..663b7fdba 100644 --- a/source/funkin/DialogueBox.hx +++ b/source/funkin/DialogueBox.hx @@ -11,8 +11,6 @@ import flixel.util.FlxColor; import flixel.util.FlxTimer; import funkin.play.PlayState; -using StringTools; - class DialogueBox extends FlxSpriteGroup { var box:FlxSprite; diff --git a/source/funkin/Discord.hx b/source/funkin/Discord.hx index 34fce44ed..3278a148d 100644 --- a/source/funkin/Discord.hx +++ b/source/funkin/Discord.hx @@ -1,9 +1,6 @@ package funkin; import Sys.sleep; - -using StringTools; - #if discord_rpc import discord_rpc.DiscordRpc; #end diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx index ecb8267bc..af3a7fa70 100644 --- a/source/funkin/FreeplayState.hx +++ b/source/funkin/FreeplayState.hx @@ -34,8 +34,6 @@ import funkin.shaderslmfao.StrokeShader; import lime.app.Future; import lime.utils.Assets; -using StringTools; - class FreeplayState extends MusicBeatSubstate { var songs:Array = []; diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index ab610aef6..7f192c170 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -1,5 +1,6 @@ package funkin; +import flixel.system.debug.log.LogStyle; import flixel.addons.transition.FlxTransitionSprite.GraphicTransTileDiamond; import flixel.addons.transition.FlxTransitionableState; import flixel.addons.transition.TransitionData; @@ -15,10 +16,8 @@ import funkin.play.song.SongData.SongDataParser; import funkin.play.stage.StageData; import funkin.ui.PreferencesMenu; import funkin.util.macro.MacroUtil; +import funkin.util.WindowUtil; import openfl.display.BitmapData; - -using StringTools; - #if colyseus import io.colyseus.Client; import io.colyseus.Room; @@ -88,8 +87,16 @@ class InitState extends FlxTransitionableState if (FlxG.save.data.mute != null) FlxG.sound.muted = FlxG.save.data.mute; + // Make errors and warnings less annoying. + LogStyle.ERROR.openConsole = false; + LogStyle.ERROR.errorSound = null; + LogStyle.WARNING.openConsole = false; + LogStyle.WARNING.errorSound = null; + // FlxG.save.close(); // FlxG.sound.loadSavedPrefs(); + WindowUtil.initWindowEvents(); + PreferencesMenu.initPrefs(); PlayerSettings.init(); Highscore.load(); diff --git a/source/funkin/LoadingState.hx b/source/funkin/LoadingState.hx index 4b4f38016..688ea96ea 100644 --- a/source/funkin/LoadingState.hx +++ b/source/funkin/LoadingState.hx @@ -188,10 +188,13 @@ class LoadingState extends MusicBeatState { Paths.setCurrentLevel('tutorial'); } - else if (PlayState.storyWeek == 8) { + else if (PlayState.storyWeek == 8) + { // TODO: Refactor this code. Paths.setCurrentLevel("weekend1"); - } else { + } + else + { Paths.setCurrentLevel("week" + PlayState.storyWeek); } #if NO_PRELOAD_ALL @@ -251,7 +254,7 @@ class LoadingState extends MusicBeatState } else { - if (StringTools.endsWith(path, ".bundle")) + if (path.endsWith(".bundle")) { rootPath = path; path += "/library.json"; diff --git a/source/funkin/MainMenuState.hx b/source/funkin/MainMenuState.hx index 715793012..d2ca2657e 100644 --- a/source/funkin/MainMenuState.hx +++ b/source/funkin/MainMenuState.hx @@ -28,9 +28,6 @@ import funkin.util.Constants; import funkin.util.WindowUtil; import lime.app.Application; import openfl.filters.ShaderFilter; - -using StringTools; - #if discord_rpc import Discord.DiscordClient; #end diff --git a/source/funkin/NGio.hx b/source/funkin/NGio.hx index b0b429f6c..fdab9507d 100644 --- a/source/funkin/NGio.hx +++ b/source/funkin/NGio.hx @@ -15,8 +15,6 @@ import io.newgrounds.objects.events.Result.GetCurrentVersionResult; import io.newgrounds.objects.events.Result.GetVersionResult; import lime.app.Application; import openfl.display.Stage; - -using StringTools; #end /** diff --git a/source/funkin/Note.hx b/source/funkin/Note.hx index 916e3bf8e..144614757 100644 --- a/source/funkin/Note.hx +++ b/source/funkin/Note.hx @@ -10,8 +10,6 @@ import funkin.shaderslmfao.ColorSwap; import funkin.ui.PreferencesMenu; import funkin.util.Constants; -using StringTools; - class Note extends FlxSprite { public var data = new NoteData(); diff --git a/source/funkin/SongLoad.hx b/source/funkin/SongLoad.hx index b4cbc3c26..fc786977f 100644 --- a/source/funkin/SongLoad.hx +++ b/source/funkin/SongLoad.hx @@ -6,8 +6,6 @@ import funkin.play.PlayState; import haxe.Json; import lime.utils.Assets; -using StringTools; - typedef SwagSong = { var song:String; diff --git a/source/funkin/StoryMenuState.hx b/source/funkin/StoryMenuState.hx index 55260b1e3..7da968f4f 100644 --- a/source/funkin/StoryMenuState.hx +++ b/source/funkin/StoryMenuState.hx @@ -15,9 +15,6 @@ import funkin.play.PlayState; import funkin.play.song.SongData.SongDataParser; import lime.net.curl.CURLCode; import openfl.Assets; - -using StringTools; - #if discord_rpc import Discord.DiscordClient; #end diff --git a/source/funkin/TitleState.hx b/source/funkin/TitleState.hx index b4b521052..9d0863ddf 100644 --- a/source/funkin/TitleState.hx +++ b/source/funkin/TitleState.hx @@ -22,8 +22,6 @@ import openfl.events.NetStatusEvent; import openfl.media.Video; import openfl.net.NetStream; -using StringTools; - #if desktop #end class TitleState extends MusicBeatState diff --git a/source/funkin/api/newgrounds/NGUnsafe.hx b/source/funkin/api/newgrounds/NGUnsafe.hx index 2995988a9..bb2305a55 100644 --- a/source/funkin/api/newgrounds/NGUnsafe.hx +++ b/source/funkin/api/newgrounds/NGUnsafe.hx @@ -17,8 +17,6 @@ import io.newgrounds.objects.events.Result.GetCurrentVersionResult; import io.newgrounds.objects.events.Result.GetVersionResult; #end -using StringTools; - /** * Contains any script functions which should be BLOCKED from use by modded scripts. */ diff --git a/source/funkin/api/newgrounds/NGUtil.hx b/source/funkin/api/newgrounds/NGUtil.hx index 8ec06b27f..ef4081050 100644 --- a/source/funkin/api/newgrounds/NGUtil.hx +++ b/source/funkin/api/newgrounds/NGUtil.hx @@ -17,8 +17,6 @@ import io.newgrounds.objects.events.Result.GetCurrentVersionResult; import io.newgrounds.objects.events.Result.GetVersionResult; #end -using StringTools; - /** * Contains any script functions which should be ALLOWD for use by modded scripts. */ diff --git a/source/funkin/charting/ChartingState.hx b/source/funkin/charting/ChartingState.hx index 1b78c1d6a..f6913d4ae 100644 --- a/source/funkin/charting/ChartingState.hx +++ b/source/funkin/charting/ChartingState.hx @@ -34,7 +34,6 @@ import openfl.events.IOErrorEvent; import openfl.net.FileReference; using Lambda; -using StringTools; using flixel.util.FlxSpriteUtil; // add in "compiler save" that saves the JSON directly to the debug json using File.write() stuff on windows / sys class ChartingState extends MusicBeatState diff --git a/source/funkin/freeplayStuff/FreeplayScore.hx b/source/funkin/freeplayStuff/FreeplayScore.hx index cef363cd0..8dcfdb8f9 100644 --- a/source/funkin/freeplayStuff/FreeplayScore.hx +++ b/source/funkin/freeplayStuff/FreeplayScore.hx @@ -3,8 +3,6 @@ package funkin.freeplayStuff; import flixel.FlxSprite; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; -using StringTools; - class FreeplayScore extends FlxTypedSpriteGroup { public var scoreShit(default, set):Int = 0; diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 3f7a23041..9ec5a968f 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -7,6 +7,7 @@ import funkin.play.stage.StageData; import polymod.Polymod; import polymod.backends.PolymodAssets.PolymodAssetType; import polymod.format.ParseRules.TextFileFormat; +import funkin.util.FileUtil; class PolymodHandler { @@ -25,12 +26,7 @@ class PolymodHandler public static function createModRoot() { - #if sys - if (!sys.FileSystem.exists(MOD_FOLDER)) - { - sys.FileSystem.createDirectory(MOD_FOLDER); - } - #end + FileUtil.createDirIfNotExists(MOD_FOLDER); } /** diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx index 235db587e..5d722bbef 100644 --- a/source/funkin/play/Countdown.hx +++ b/source/funkin/play/Countdown.hx @@ -10,8 +10,6 @@ import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent.CountdownScriptEvent; import flixel.util.FlxTimer; -using StringTools; - class Countdown { /** diff --git a/source/funkin/play/GameOverSubstate.hx b/source/funkin/play/GameOverSubstate.hx index 48a65df0a..0cf5f7379 100644 --- a/source/funkin/play/GameOverSubstate.hx +++ b/source/funkin/play/GameOverSubstate.hx @@ -11,8 +11,6 @@ import funkin.play.PlayState; import funkin.play.character.BaseCharacter; import funkin.ui.PreferencesMenu; -using StringTools; - /** * A substate which renders over the PlayState when the player dies. * Displays the player death animation, plays the music, and handles restarting the song. diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index ee8a9d3c3..4aa8fa2c7 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -41,9 +41,6 @@ import funkin.ui.stageBuildShit.StageOffsetSubstate; import funkin.util.Constants; import funkin.util.SortUtil; import lime.ui.Haptic; - -using StringTools; - #if discord_rpc import Discord.DiscordClient; #end diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index eb42e506d..bb71fb993 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -6,8 +6,6 @@ import funkin.noteStuff.NoteBasic.NoteDir; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.stage.Bopper; -using StringTools; - /** * A Character is a stage prop which bops to the music as well as controlled by the strumlines. * diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx index 23a800109..2eaf4f944 100644 --- a/source/funkin/play/character/CharacterData.hx +++ b/source/funkin/play/character/CharacterData.hx @@ -16,8 +16,6 @@ import funkin.util.assets.DataAssets; import haxe.Json; import openfl.utils.Assets; -using StringTools; - class CharacterDataParser { /** @@ -258,7 +256,7 @@ class CharacterDataParser static function loadCharacterFile(charPath:String):String { var charFilePath:String = Paths.json('characters/${charPath}'); - var rawJson = StringTools.trim(Assets.getText(charFilePath)); + var rawJson = Assets.getText(charFilePath).trim(); while (!StringTools.endsWith(rawJson, "}")) { diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx index b647d7394..775e78c11 100644 --- a/source/funkin/play/song/SongData.hx +++ b/source/funkin/play/song/SongData.hx @@ -8,8 +8,6 @@ import haxe.Json; import openfl.utils.Assets; import thx.semver.Version; -using StringTools; - /** * Contains utilities for loading and parsing stage data. */ @@ -267,7 +265,8 @@ abstract SongMetadata(RawSongMetadata) }; } - public function clone(?newVariation:String = null):SongMetadata { + public function clone(?newVariation:String = null):SongMetadata + { var result = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation); result.version = this.version; result.timeFormat = this.timeFormat; @@ -276,7 +275,7 @@ abstract SongMetadata(RawSongMetadata) result.loop = this.loop; result.playData = this.playData; result.generatedBy = this.generatedBy; - + return result; } } diff --git a/source/funkin/play/stage/StageData.hx b/source/funkin/play/stage/StageData.hx index 0b5a97ce0..380efc2fa 100644 --- a/source/funkin/play/stage/StageData.hx +++ b/source/funkin/play/stage/StageData.hx @@ -8,8 +8,6 @@ import funkin.util.assets.DataAssets; import haxe.Json; import openfl.Assets; -using StringTools; - /** * Contains utilities for loading and parsing stage data. */ diff --git a/source/funkin/ui/PopUpStuff.hx b/source/funkin/ui/PopUpStuff.hx index 0542ff14c..1d9d09c7b 100644 --- a/source/funkin/ui/PopUpStuff.hx +++ b/source/funkin/ui/PopUpStuff.hx @@ -6,8 +6,6 @@ import flixel.group.FlxGroup.FlxTypedGroup; import flixel.tweens.FlxTween; import funkin.play.PlayState; -using StringTools; - class PopUpStuff extends FlxTypedGroup { override public function new() diff --git a/source/funkin/ui/animDebugShit/DebugBoundingState.hx b/source/funkin/ui/animDebugShit/DebugBoundingState.hx index cf28d23da..af84ed117 100644 --- a/source/funkin/ui/animDebugShit/DebugBoundingState.hx +++ b/source/funkin/ui/animDebugShit/DebugBoundingState.hx @@ -33,7 +33,6 @@ import openfl.net.URLLoader; import openfl.net.URLRequest; import openfl.utils.ByteArray; -using StringTools; using flixel.util.FlxSpriteUtil; #if web diff --git a/source/funkin/ui/debug/charting/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/ChartEditorCommand.hx index 39cae449b..64fab3866 100644 --- a/source/funkin/ui/debug/charting/ChartEditorCommand.hx +++ b/source/funkin/ui/debug/charting/ChartEditorCommand.hx @@ -64,6 +64,7 @@ class AddNotesCommand implements ChartEditorCommand state.playSound(Paths.sound('funnyNoise/funnyNoise-08')); + state.saveDataDirty = true; state.noteDisplayDirty = true; state.notePreviewDirty = true; @@ -76,6 +77,7 @@ class AddNotesCommand implements ChartEditorCommand state.currentSelection = []; state.playSound(Paths.sound('funnyNoise/funnyNoise-01')); + state.saveDataDirty = true; state.noteDisplayDirty = true; state.notePreviewDirty = true; @@ -109,6 +111,7 @@ class RemoveNotesCommand implements ChartEditorCommand state.currentSelection = []; state.playSound(Paths.sound('funnyNoise/funnyNoise-01')); + state.saveDataDirty = true; state.noteDisplayDirty = true; state.notePreviewDirty = true; @@ -124,6 +127,7 @@ class RemoveNotesCommand implements ChartEditorCommand state.currentSelection = notes; state.playSound(Paths.sound('funnyNoise/funnyNoise-08')); + state.saveDataDirty = true; state.noteDisplayDirty = true; state.notePreviewDirty = true; @@ -409,6 +413,7 @@ class CutNotesCommand implements ChartEditorCommand // Delete the notes. state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentSelection = []; + state.saveDataDirty = true; state.noteDisplayDirty = true; state.notePreviewDirty = true; state.sortChartData(); @@ -419,6 +424,7 @@ class CutNotesCommand implements ChartEditorCommand state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes); state.currentSelection = notes; + state.saveDataDirty = true; state.noteDisplayDirty = true; state.notePreviewDirty = true; @@ -453,6 +459,7 @@ class FlipNotesCommand implements ChartEditorCommand state.currentSelection = flippedNotes; + state.saveDataDirty = true; state.noteDisplayDirty = true; state.notePreviewDirty = true; state.sortChartData(); @@ -465,6 +472,7 @@ class FlipNotesCommand implements ChartEditorCommand state.currentSelection = notes; + state.saveDataDirty = true; state.noteDisplayDirty = true; state.notePreviewDirty = true; @@ -498,6 +506,7 @@ class PasteNotesCommand implements ChartEditorCommand state.currentSongChartNoteData = state.currentSongChartNoteData.concat(addedNotes); state.currentSelection = addedNotes.copy(); + state.saveDataDirty = true; state.noteDisplayDirty = true; state.notePreviewDirty = true; @@ -509,6 +518,7 @@ class PasteNotesCommand implements ChartEditorCommand state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes); state.currentSelection = []; + state.saveDataDirty = true; state.noteDisplayDirty = true; state.notePreviewDirty = true; @@ -539,6 +549,7 @@ class AddEventsCommand implements ChartEditorCommand // TODO: Allow selecting events. // state.currentSelection = events; + state.saveDataDirty = true; state.noteDisplayDirty = true; state.notePreviewDirty = true; @@ -551,6 +562,7 @@ class AddEventsCommand implements ChartEditorCommand state.currentSelection = []; + state.saveDataDirty = true; state.noteDisplayDirty = true; state.notePreviewDirty = true; @@ -581,6 +593,7 @@ class ExtendNoteLengthCommand implements ChartEditorCommand { note.length = newLength; + state.saveDataDirty = true; state.noteDisplayDirty = true; state.notePreviewDirty = true; @@ -591,6 +604,7 @@ class ExtendNoteLengthCommand implements ChartEditorCommand { note.length = oldLength; + state.saveDataDirty = true; state.noteDisplayDirty = true; state.notePreviewDirty = true; diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 39d1b34c3..9acaf59f0 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1,5 +1,6 @@ package funkin.ui.debug.charting; +import haxe.io.Path; import flixel.addons.display.FlxSliceSprite; import flixel.addons.display.FlxTiledSprite; import flixel.FlxSprite; @@ -31,6 +32,7 @@ import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.HaxeUIState; import funkin.util.Constants; import funkin.util.FileUtil; +import funkin.util.DateUtil; import funkin.util.SerializerUtil; import haxe.ui.components.Button; import haxe.ui.components.CheckBox; @@ -49,11 +51,11 @@ import haxe.ui.events.DragEvent; import haxe.ui.events.MouseEvent; import haxe.ui.events.UIEvent; import lime.media.AudioBuffer; +import funkin.util.WindowUtil; import openfl.display.BitmapData; import openfl.geom.Rectangle; using Lambda; -using StringTools; /** * A state dedicated to allowing the user to create and edit song charts. @@ -98,6 +100,11 @@ class ChartEditorState extends HaxeUIState // The height of the menu bar in the layout. static final MENU_BAR_HEIGHT = 32; + /** + * Duration to wait before autosaving the chart. + */ + static final AUTOSAVE_TIMER_DELAY:Float = 60.0 * 5.0; + // The amount of padding between the menu bar and the chart grid when fully scrolled up. static final GRID_TOP_PAD:Int = 8; @@ -435,6 +442,38 @@ class ChartEditorState extends HaxeUIState */ var notePreviewDirty:Bool = true; + /** + * Whether the chart has been modified since it was last saved. + * Used to determine whether to auto-save, etc. + */ + var saveDataDirty(default, set):Bool = false; + + function set_saveDataDirty(value:Bool):Bool + { + if (value == saveDataDirty) + return value; + + if (value) + { + // Start the auto-save timer. + autoSaveTimer = new FlxTimer().start(AUTOSAVE_TIMER_DELAY, (_) -> autoSave()); + } + else + { + // Stop the auto-save timer. + autoSaveTimer.cancel(); + autoSaveTimer.destroy(); + autoSaveTimer = null; + } + + return saveDataDirty = value; + } + + /** + * A timer used to auto-save the chart after a period of inactivity. + */ + var autoSaveTimer:FlxTimer; + /** * Whether the difficulty tree view in the toolbox has been modified and needs to be updated. * This happens when we add/remove difficulties. @@ -847,6 +886,8 @@ class ChartEditorState extends HaxeUIState // Setup the onClick listeners for the UI after it's been created. setupUIListeners(); + setupAutoSave(); + // TODO: We should be loading the music later when the user requests it. // loadDefaultMusic(); @@ -1240,6 +1281,48 @@ class ChartEditorState extends HaxeUIState registerContextMenu(null, Paths.ui('chart-editor/context/test')); } + /** + * Setup timers and listerners to handle auto-save. + */ + function setupAutoSave() + { + WindowUtil.windowExit.add(onWindowClose); + saveDataDirty = false; + } + + /** + * Called after 5 minutes without saving. + */ + function autoSave() + { + saveDataDirty = false; + + // Auto-save the chart. + + #if html5 + // Auto-save to local storage. + #else + // Auto-save to temp file. + exportAllSongData(true, true); + #end + } + + function onWindowClose(exitCode:Int) + { + trace('Window exited with exit code: $exitCode'); + trace('Should save chart? $saveDataDirty'); + + if (saveDataDirty) + { + exportAllSongData(true); + } + } + + function cleanupAutoSave() + { + WindowUtil.windowExit.remove(onWindowClose); + } + public override function update(elapsed:Float) { // dispatchEvent gets called here. @@ -1268,25 +1351,20 @@ class ChartEditorState extends HaxeUIState handleHelpKeybinds(); // DEBUG + #if debug if (FlxG.keys.justPressed.F) { - showNotification('Hi there :)'); - } + // This breaks the layout don't use it. + // showNotification('Hi there :)'); - if (FlxG.keys.justPressed.Q) - { - ChartEditorDialogHandler.openWelcomeDialog(this, true); - } - - if (FlxG.keys.justPressed.W) - { - difficultySelectDirty = true; + autoSave(); } if (FlxG.keys.justPressed.E) { currentSongMetadata.timeChanges[0].timeSignatureNum = (currentSongMetadata.timeChanges[0].timeSignatureNum == 4 ? 3 : 4); } + #end // Right align the BF health icon. @@ -2959,6 +3037,8 @@ class ChartEditorState extends HaxeUIState { super.destroy(); + cleanupAutoSave(); + @:privateAccess ChartEditorNoteSprite.noteFrameCollection = null; } @@ -2998,7 +3078,11 @@ class ChartEditorState extends HaxeUIState notifBar.hide(); } - public function exportAllSongData():Void + /** + * @param force Whether to force the export without prompting the user for a file location. + * @param tmp If true, save to the temporary directory instead of the local `backup` directory. + */ + public function exportAllSongData(?force:Bool = false, ?tmp:Bool = false):Void { var zipEntries = []; @@ -3030,6 +3114,16 @@ class ChartEditorState extends HaxeUIState trace('Exporting ${zipEntries.length} files to ZIP...'); + if (force) + { + var targetPath:String = tmp ? Path.join([FileUtil.getTempDir(), 'chart-editor-exit-${DateUtil.generateTimestamp()}.zip']) : Path.join(['./backups/', 'chart-editor-exit-${DateUtil.generateTimestamp()}.zip']); + + // 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); + return; + } + // Prompt and save. var onSave:Array->Void = (paths:Array) -> { @@ -3041,6 +3135,6 @@ class ChartEditorState extends HaxeUIState trace('Export cancelled.'); }; - FileUtil.saveFilesAsZIP(zipEntries, onSave, onCancel, '$currentSongId-chart.zip'); + FileUtil.saveMultipleFiles(zipEntries, onSave, onCancel, '$currentSongId-chart.zip'); } } diff --git a/source/funkin/util/DateUtil.hx b/source/funkin/util/DateUtil.hx new file mode 100644 index 000000000..32db7804d --- /dev/null +++ b/source/funkin/util/DateUtil.hx @@ -0,0 +1,11 @@ +package funkin.util; + +class DateUtil +{ + public static function generateTimestamp():String + { + var date = Date.now(); + return + '${date.getFullYear()}-${Std.string(date.getMonth() + 1).lpad('0', 2)}-${Std.string(date.getDate()).lpad('0', 2)}-${Std.string(date.getHours()).lpad('0', 2)}-${Std.string(date.getMinutes()).lpad('0', 2)}-${Std.string(date.getSeconds()).lpad('0', 2)}'; + } +} diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx index 4685ec1d9..e3dafc2cb 100644 --- a/source/funkin/util/FileUtil.hx +++ b/source/funkin/util/FileUtil.hx @@ -1,5 +1,6 @@ package funkin.util; +import cpp.abi.Abi; import haxe.zip.Entry; import lime.utils.Bytes; import lime.ui.FileDialog; @@ -222,7 +223,8 @@ class FileUtil * @param typeFilter TODO What does this do? * @return Whether the file dialog was opened successfully. */ - public static function saveMultipleFiles(resources:Array, ?onSaveAll:Array->Void, ?onCancel:Void->Void, ?defaultPath:String, ?force:Bool = false):Bool + public static function saveMultipleFiles(resources:Array, ?onSaveAll:Array->Void, ?onCancel:Void->Void, ?defaultPath:String, + ?force:Bool = false):Bool { #if desktop // Prompt the user for a directory, then write all of the files to there. @@ -232,17 +234,23 @@ class FileUtil for (resource in resources) { var filePath = haxe.io.Path.join([targetPath, resource.fileName]); - try { - if (resource.data == null) { + try + { + if (resource.data == null) + { trace('WARNING: File $filePath has no data or content. Skipping.'); continue; - } else { - writeBytesToPath(filePath, resource.data, force); } - } catch (e:Dynamic) { - trace('Failed to write file (probably already exists): $filePath' + filePath); - continue; - } + else + { + writeBytesToPath(filePath, resource.data, force ? Force : Skip); + } + } + catch (e:Dynamic) + { + trace('Failed to write file (probably already exists): $filePath' + filePath); + continue; + } paths.push(filePath); } onSaveAll(paths); @@ -264,7 +272,9 @@ class FileUtil /** * Takes an array of file entries and prompts the user to save them as a ZIP file. */ - public static function saveFilesAsZIP(resources:Array, ?onSave:Array->Void, ?onCancel:Void->Void, ?defaultPath:String, ?force:Bool = false):Bool { + public static function saveFilesAsZIP(resources:Array, ?onSave:Array->Void, ?onCancel:Void->Void, ?defaultPath:String, + ?force:Bool = false):Bool + { // Create a ZIP file. var zipBytes = createZIPFromEntries(resources); @@ -280,46 +290,162 @@ class FileUtil } /** - * Write string file contents directly to a given path. - * Only works on desktop. + * Takes an array of file entries and forcibly writes a ZIP to the given path. + * Only works on desktop, because HTML5 doesn't allow you to write files to arbitrary paths. + * Use `saveFilesAsZIP` instead. + * @param force Whether to force overwrite an existing file. */ - public static function writeStringToPath(path:String, data:String, force:Bool = false) + public static function saveFilesAsZIPToPath(resources:Array, path:String, ?force:Bool = false):Bool { - if (force || !sys.FileSystem.exists(path)) + #if desktop + // Create a ZIP file. + var zipBytes = createZIPFromEntries(resources); + + // Write the ZIP. + writeBytesToPath(path, zipBytes, force ? Force : Skip); + + return true; + #else + return false; + #end + } + + /** + * Write string file contents directly to a given path. + * Only works on desktop. + * + * @param mode Whether to Force, Skip, or Ask to overwrite an existing file. + */ + public static function writeStringToPath(path:String, data:String, mode:FileWriteMode = Skip) + { + #if sys + createDirIfNotExists(Path.directory(path)); + + switch (mode) { - sys.io.File.saveContent(path, data); - } - else - { - throw 'File already exists: $path'; + case Force: + sys.io.File.saveContent(path, data); + case Skip: + if (!sys.FileSystem.exists(path)) + { + sys.io.File.saveContent(path, data); + } + else + { + throw 'File already exists: $path'; + } + case Ask: + if (sys.FileSystem.exists(path)) + { + // TODO: We don't have the technology to use native popups yet. + } + else + { + sys.io.File.saveContent(path, data); + } } + #else + throw 'Direct file writing by path not supported on this platform.'; + #end } /** * Write byte file contents directly to a given path. - * Only works on desktop. + * Only works on desktop. + * + * @param mode Whether to Force, Skip, or Ask to overwrite an existing file. */ - public static function writeBytesToPath(path:String, data:Bytes, force:Bool = false) + public static function writeBytesToPath(path:String, data:Bytes, mode:FileWriteMode = Skip) { - if (force || !sys.FileSystem.exists(path)) + #if sys + createDirIfNotExists(Path.directory(path)); + + switch (mode) { - sys.io.File.saveBytes(path, data); - } - else - { - throw 'File already exists: $path'; + case Force: + sys.io.File.saveBytes(path, data); + case Skip: + if (!sys.FileSystem.exists(path)) + { + sys.io.File.saveBytes(path, data); + } + else + { + throw 'File already exists: $path'; + } + case Ask: + if (sys.FileSystem.exists(path)) + { + // TODO: We don't have the technology to use native popups yet. + } + else + { + sys.io.File.saveBytes(path, data); + } } + #else + throw 'Direct file writing by path not supported on this platform.'; + #end } /** * Write string file contents directly to the end of a file at the given path. - * Only works on desktop. + * Only works on desktop. */ public static function appendStringToPath(path:String, data:String) { sys.io.File.append(path, false).writeString(data); } + /** + * Create a directory if it doesn't already exist. + * Only works on desktop. + */ + public static function createDirIfNotExists(dir:String) + { + #if sys + if (!sys.FileSystem.exists(dir)) + { + sys.FileSystem.createDirectory(dir); + } + #end + } + + static var tempDir:String = null; + static final TEMP_ENV_VARS:Array = ['TEMP', 'TMPDIR', 'TEMPDIR', 'TMP']; + + /** + * Get the path to a temporary directory we can use for writing files. + * Only works on desktop. + */ + public static function getTempDir():String + { + if (tempDir != null) + return tempDir; + + #if sys + #if windows + var path:String = null; + + for (envName in TEMP_ENV_VARS) + { + path = Sys.getEnv(envName); + + if (path == "") + path = null; + if (path != null) + break; + } + + return tempDir = Path.join([path, 'funkin/']); + #else + return tempDir = '/tmp/funkin/'; + #end + #else + return null; + #end + } + /** * Create a Bytes object containing a ZIP file, containing the provided entries. * @@ -329,7 +455,7 @@ class FileUtil public static function createZIPFromEntries(entries:Array):Bytes { var o = new haxe.io.BytesOutput(); - + var zipWriter = new haxe.zip.Writer(o); zipWriter.write(entries.list()); @@ -348,24 +474,23 @@ class FileUtil var data = haxe.io.Bytes.ofString(content, UTF8); return { - fileName : name, - fileSize : data.length, - - data : data, - dataSize : data.length, + fileName: name, + fileSize: data.length, - compressed : false, - - fileTime : Date.now(), - crc32 : null, - extraFields : null, + data: data, + dataSize: data.length, + + compressed: false, + + fileTime: Date.now(), + crc32: null, + extraFields: null, }; } static function convertTypeFilter(typeFilter:Array):String { var filter = null; - if (typeFilter != null) { var filters = []; @@ -379,3 +504,21 @@ class FileUtil return filter; } } + +enum FileWriteMode +{ + /** + * Forcibly overwrite the file if it already exists. + */ + Force; + + /** + * Ask the user if they want to overwrite the file if it already exists. + */ + Ask; + + /** + * Skip the file if it already exists. + */ + Skip; +} diff --git a/source/funkin/util/WindowUtil.hx b/source/funkin/util/WindowUtil.hx index fe27ed252..bf80e688f 100644 --- a/source/funkin/util/WindowUtil.hx +++ b/source/funkin/util/WindowUtil.hx @@ -1,5 +1,7 @@ package funkin.util; +import flixel.util.FlxSignal.FlxTypedSignal; + class WindowUtil { public static function openURL(targetUrl:String) @@ -12,7 +14,23 @@ class WindowUtil FlxG.openURL(targetUrl); #end #else - trace('Cannot open') + trace('Cannot open'); #end } + + /** + * Dispatched when the game window is closed. + */ + public static final windowExit:FlxTypedSignalVoid> = new FlxTypedSignalVoid>(); + + public static function initWindowEvents() + { + // onUpdate is called every frame just before rendering. + + // onExit is called when the game window is closed. + openfl.Lib.current.stage.application.onExit.add(function(exitCode:Int) + { + windowExit.dispatch(exitCode); + }); + } } diff --git a/source/funkin/util/assets/DataAssets.hx b/source/funkin/util/assets/DataAssets.hx index ee0699810..3e95e92c3 100644 --- a/source/funkin/util/assets/DataAssets.hx +++ b/source/funkin/util/assets/DataAssets.hx @@ -1,7 +1,5 @@ package funkin.util.assets; -using StringTools; - class DataAssets { static function buildDataPath(path:String):String