1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2024-11-25 16:24:40 +00:00

Autosave and save-on-quit for chart editor

This commit is contained in:
EliteMasterEric 2023-01-02 17:40:53 -05:00
parent b3b7fb49c2
commit 6b3c15348b
35 changed files with 356 additions and 131 deletions

View file

@ -135,14 +135,6 @@
<haxelib name="hxcpp-debug-server" if="desktop debug" />
<!--
With these options enabled, console popup and beep no longer occur.
You can still see the log messages by opening the console (F2).
Be sure to remove these during cleanup and bugfix testing!
-->
<haxedef name="FLX_NO_ERROR_SOUND" />
<haxedef name="FLX_NO_ERROR_CONSOLE" />
<!--Disable the Flixel core focus lost screen-->
<haxedef name="FLX_NO_FOCUS_LOST_SCREEN" />

View file

@ -5,8 +5,6 @@ import flixel.group.FlxSpriteGroup;
import flixel.math.FlxMath;
import flixel.util.FlxTimer;
using StringTools;
/**
* Loosley based on FlxTypeText lolol
*/

View file

@ -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<String> = ['EASY', "NORMAL", "HARD"];

View file

@ -4,8 +4,6 @@ import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.math.FlxPoint;
using StringTools;
class CutsceneCharacter extends FlxTypedGroup<FlxSprite>
{
public var coolPos:FlxPoint = FlxPoint.get();

View file

@ -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;

View file

@ -1,9 +1,6 @@
package funkin;
import Sys.sleep;
using StringTools;
#if discord_rpc
import discord_rpc.DiscordRpc;
#end

View file

@ -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<SongMetadata> = [];

View file

@ -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();

View file

@ -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";

View file

@ -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

View file

@ -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
/**

View file

@ -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();

View file

@ -6,8 +6,6 @@ import funkin.play.PlayState;
import haxe.Json;
import lime.utils.Assets;
using StringTools;
typedef SwagSong =
{
var song:String;

View file

@ -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

View file

@ -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

View file

@ -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.
*/

View file

@ -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.
*/

View file

@ -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

View file

@ -3,8 +3,6 @@ package funkin.freeplayStuff;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
using StringTools;
class FreeplayScore extends FlxTypedSpriteGroup<ScoreNum>
{
public var scoreShit(default, set):Int = 0;

View file

@ -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);
}
/**

View file

@ -10,8 +10,6 @@ import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
import flixel.util.FlxTimer;
using StringTools;
class Countdown
{
/**

View file

@ -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.

View file

@ -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

View file

@ -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.
*

View file

@ -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, "}"))
{

View file

@ -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;
}
}

View file

@ -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.
*/

View file

@ -6,8 +6,6 @@ import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.tweens.FlxTween;
import funkin.play.PlayState;
using StringTools;
class PopUpStuff extends FlxTypedGroup<FlxSprite>
{
override public function new()

View file

@ -33,7 +33,6 @@ import openfl.net.URLLoader;
import openfl.net.URLRequest;
import openfl.utils.ByteArray;
using StringTools;
using flixel.util.FlxSpriteUtil;
#if web

View file

@ -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;

View file

@ -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<String>->Void = (paths:Array<String>) ->
{
@ -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');
}
}

View file

@ -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)}';
}
}

View file

@ -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<Entry>, ?onSaveAll:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String, ?force:Bool = false):Bool
public static function saveMultipleFiles(resources:Array<Entry>, ?onSaveAll:Array<String>->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<Entry>, ?onSave:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String, ?force:Bool = false):Bool {
public static function saveFilesAsZIP(resources:Array<Entry>, ?onSave:Array<String>->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<Entry>, 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<String> = ['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<Entry>):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<FileFilter>):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;
}

View file

@ -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:FlxTypedSignal<Int->Void> = new FlxTypedSignal<Int->Void>();
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);
});
}
}

View file

@ -1,7 +1,5 @@
package funkin.util.assets;
using StringTools;
class DataAssets
{
static function buildDataPath(path:String):String