1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-03-24 02:49:33 +00:00

Merge branch 'rewrite/master' into bugfix/senpai-naughty

This commit is contained in:
Cameron Taylor 2023-11-15 19:52:14 -05:00
commit ad345fd834
9 changed files with 1049 additions and 343 deletions

2
assets

@ -1 +1 @@
Subproject commit 482ef76582208a484a2f6450ce5ad9e278db08f8 Subproject commit 3b05b0fdd8e3b2cd09b9e4e415c186bae8e3b7d3

View file

@ -674,6 +674,22 @@ class SongNoteDataRaw
this.kind = kind; 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 @:jignored
var _stepTime:Null<Float> = null; var _stepTime:Null<Float> = null;
@ -730,22 +746,6 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
this = new SongNoteDataRaw(time, data, length, kind); 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 public static function buildDirectionName(data:Int, strumlineSize:Int = 4):String
{ {
switch (data % strumlineSize) switch (data % strumlineSize)

View file

@ -5,12 +5,17 @@ import funkin.save.migrator.SaveDataMigrator;
import thx.semver.Version; import thx.semver.Version;
import funkin.input.Controls.Device; import funkin.input.Controls.Device;
import funkin.save.migrator.RawSaveData_v1_0_0; import funkin.save.migrator.RawSaveData_v1_0_0;
import funkin.save.migrator.SaveDataMigrator;
import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle;
import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme;
import thx.semver.Version;
@:nullSafety @:nullSafety
@:forward(volume, mute) @:forward(volume, mute)
abstract Save(RawSaveData) 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"; 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. // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility.
@ -94,6 +99,18 @@ abstract Save(RawSaveData)
optionsChartEditor: optionsChartEditor:
{ {
// Reasonable defaults. // Reasonable defaults.
previousFiles: [],
noteQuant: 3,
chartEditorLiveInputStyle: ChartEditorLiveInputStyle.None,
theme: ChartEditorTheme.Light,
playtestStartTime: false,
downscroll: false,
metronomeEnabled: true,
hitsoundsEnabledPlayer: true,
hitsoundsEnabledOpponent: true,
instVolume: 1.0,
voicesVolume: 1.0,
playbackSpeed: 1.0,
}, },
}; };
} }
@ -124,7 +141,9 @@ abstract Save(RawSaveData)
function set_ngSessionId(value:Null<String>):Null<String> 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>; public var enabledModIds(get, set):Array<String>;
@ -136,7 +155,213 @@ abstract Save(RawSaveData)
function set_enabledModIds(value:Array<String>):Array<String> 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):ChartEditorLiveInputStyle;
function get_chartEditorLiveInputStyle():ChartEditorLiveInputStyle
{
if (this.optionsChartEditor.chartEditorLiveInputStyle == null) this.optionsChartEditor.chartEditorLiveInputStyle = ChartEditorLiveInputStyle.None;
return this.optionsChartEditor.chartEditorLiveInputStyle;
}
function set_chartEditorLiveInputStyle(value:ChartEditorLiveInputStyle):ChartEditorLiveInputStyle
{
// Set and apply.
this.optionsChartEditor.chartEditorLiveInputStyle = value;
flush();
return this.optionsChartEditor.chartEditorLiveInputStyle;
}
public var chartEditorDownscroll(get, set):Bool;
function get_chartEditorDownscroll():Bool
{
if (this.optionsChartEditor.downscroll == null) this.optionsChartEditor.downscroll = false;
return this.optionsChartEditor.downscroll;
}
function set_chartEditorDownscroll(value:Bool):Bool
{
// Set and apply.
this.optionsChartEditor.downscroll = value;
flush();
return this.optionsChartEditor.downscroll;
}
public var chartEditorPlaytestStartTime(get, set):Bool;
function get_chartEditorPlaytestStartTime():Bool
{
if (this.optionsChartEditor.playtestStartTime == null) this.optionsChartEditor.playtestStartTime = false;
return this.optionsChartEditor.playtestStartTime;
}
function set_chartEditorPlaytestStartTime(value:Bool):Bool
{
// Set and apply.
this.optionsChartEditor.playtestStartTime = value;
flush();
return this.optionsChartEditor.playtestStartTime;
}
public var chartEditorTheme(get, set):ChartEditorTheme;
function get_chartEditorTheme():ChartEditorTheme
{
if (this.optionsChartEditor.theme == null) this.optionsChartEditor.theme = ChartEditorTheme.Light;
return this.optionsChartEditor.theme;
}
function set_chartEditorTheme(value:ChartEditorTheme):ChartEditorTheme
{
// Set and apply.
this.optionsChartEditor.theme = value;
flush();
return this.optionsChartEditor.theme;
}
public var chartEditorMetronomeEnabled(get, set):Bool;
function get_chartEditorMetronomeEnabled():Bool
{
if (this.optionsChartEditor.metronomeEnabled == null) this.optionsChartEditor.metronomeEnabled = true;
return this.optionsChartEditor.metronomeEnabled;
}
function set_chartEditorMetronomeEnabled(value:Bool):Bool
{
// Set and apply.
this.optionsChartEditor.metronomeEnabled = value;
flush();
return this.optionsChartEditor.metronomeEnabled;
}
public var chartEditorHitsoundsEnabledPlayer(get, set):Bool;
function get_chartEditorHitsoundsEnabledPlayer():Bool
{
if (this.optionsChartEditor.hitsoundsEnabledPlayer == null) this.optionsChartEditor.hitsoundsEnabledPlayer = true;
return this.optionsChartEditor.hitsoundsEnabledPlayer;
}
function set_chartEditorHitsoundsEnabledPlayer(value:Bool):Bool
{
// Set and apply.
this.optionsChartEditor.hitsoundsEnabledPlayer = value;
flush();
return this.optionsChartEditor.hitsoundsEnabledPlayer;
}
public var chartEditorHitsoundsEnabledOpponent(get, set):Bool;
function get_chartEditorHitsoundsEnabledOpponent():Bool
{
if (this.optionsChartEditor.hitsoundsEnabledOpponent == null) this.optionsChartEditor.hitsoundsEnabledOpponent = true;
return this.optionsChartEditor.hitsoundsEnabledOpponent;
}
function set_chartEditorHitsoundsEnabledOpponent(value:Bool):Bool
{
// Set and apply.
this.optionsChartEditor.hitsoundsEnabledOpponent = value;
flush();
return this.optionsChartEditor.hitsoundsEnabledOpponent;
}
public var chartEditorInstVolume(get, set):Float;
function get_chartEditorInstVolume():Float
{
if (this.optionsChartEditor.instVolume == null) this.optionsChartEditor.instVolume = 1.0;
return this.optionsChartEditor.instVolume;
}
function set_chartEditorInstVolume(value:Float):Float
{
// Set and apply.
this.optionsChartEditor.instVolume = value;
flush();
return this.optionsChartEditor.instVolume;
}
public var chartEditorVoicesVolume(get, set):Float;
function get_chartEditorVoicesVolume():Float
{
if (this.optionsChartEditor.voicesVolume == null) this.optionsChartEditor.voicesVolume = 1.0;
return this.optionsChartEditor.voicesVolume;
}
function set_chartEditorVoicesVolume(value:Float):Float
{
// Set and apply.
this.optionsChartEditor.voicesVolume = value;
flush();
return this.optionsChartEditor.voicesVolume;
}
public var chartEditorPlaybackSpeed(get, set):Float;
function get_chartEditorPlaybackSpeed():Float
{
if (this.optionsChartEditor.playbackSpeed == null) this.optionsChartEditor.playbackSpeed = 1.0;
return this.optionsChartEditor.playbackSpeed;
}
function set_chartEditorPlaybackSpeed(value:Float):Float
{
// Set and apply.
this.optionsChartEditor.playbackSpeed = value;
flush();
return this.optionsChartEditor.playbackSpeed;
} }
/** /**
@ -699,4 +924,77 @@ typedef SaveControlsData =
/** /**
* An anonymous structure containing all the user's options and preferences, specific to the Chart Editor. * 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 `ChartEditorLiveInputStyle.None`
*/
var ?chartEditorLiveInputStyle:ChartEditorLiveInputStyle;
/**
* Theme in the Chart Editor.
* @default `ChartEditorTheme.Light`
*/
var ?theme:ChartEditorTheme;
/**
* Downscroll in the Chart Editor.
* @default `false`
*/
var ?downscroll:Bool;
/**
* Metronome sounds in the Chart Editor.
* @default `true`
*/
var ?metronomeEnabled:Bool;
/**
* If true, playtest songs from the current position in the Chart Editor.
* @default `false`
*/
var ?playtestStartTime:Bool;
/**
* Player note hit sounds in the Chart Editor.
* @default `true`
*/
var ?hitsoundsEnabledPlayer:Bool;
/**
* Opponent note hit sounds in the Chart Editor.
* @default `true`
*/
var ?hitsoundsEnabledOpponent:Bool;
/**
* Instrumental volume in the Chart Editor.
* @default `1.0`
*/
var ?instVolume:Float;
/**
* Voices volume in the Chart Editor.
* @default `1.0`
*/
var ?voicesVolume:Float;
/**
* Playback speed in the Chart Editor.
* @default `1.0`
*/
var ?playbackSpeed:Float;
};

File diff suppressed because it is too large Load diff

View file

@ -217,6 +217,18 @@ class ChartEditorAudioHandler
snd.play(); snd.play();
} }
public static function wipeInstrumentalData(state:ChartEditorState):Void
{
state.audioInstTrackData.clear();
stopExistingInstrumental(state);
}
public static function wipeVocalData(state:ChartEditorState):Void
{
state.audioVocalTrackData.clear();
stopExistingVocals(state);
}
/** /**
* Create a list of ZIP file entries from the current loaded instrumental tracks in the chart eidtor. * Create a list of ZIP file entries from the current loaded instrumental tracks in the chart eidtor.
* @param state The chart editor state. * @param state The chart editor state.
@ -226,18 +238,27 @@ class ChartEditorAudioHandler
{ {
var zipEntries = []; var zipEntries = [];
for (key in state.audioInstTrackData.keys()) var instTrackIds = state.audioInstTrackData.keys().array();
for (key in instTrackIds)
{ {
if (key == 'default') if (key == 'default')
{ {
var data:Null<Bytes> = state.audioInstTrackData.get('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)); zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', data));
} }
else else
{ {
var data:Null<Bytes> = state.audioInstTrackData.get(key); 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)); zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst-${key}.ogg', data));
} }
} }
@ -254,10 +275,15 @@ class ChartEditorAudioHandler
{ {
var zipEntries = []; var zipEntries = [];
var vocalTrackIds = state.audioVocalTrackData.keys().array();
for (key in state.audioVocalTrackData.keys()) for (key in state.audioVocalTrackData.keys())
{ {
var data:Null<Bytes> = state.audioVocalTrackData.get(key); 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)); zipEntries.push(FileUtil.makeZIPEntryFromBytes('Voices-${key}.ogg', data));
} }

View file

@ -85,11 +85,73 @@ class ChartEditorDialogHandler
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable); var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable);
if (dialog == null) throw 'Could not locate Welcome dialog'; if (dialog == null) throw 'Could not locate Welcome dialog';
state.isHaxeUIDialogOpen = true;
dialog.onDialogClosed = function(_event) { dialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
// Called when the Welcome dialog is closed while it is closable. // Called when the Welcome dialog is closed while it is closable.
state.stopWelcomeMusic(); 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)
{
if (chartPath == null) continue;
var linkRecentChart:Link = new Link();
linkRecentChart.text = chartPath;
linkRecentChart.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
state.stopWelcomeMusic();
// Load chart from file
var result:Null<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: Constants.NOTIFICATION_DISMISS_TIME
});
#end
}
else
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load chart (${chartPath.toString()})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
}
}
if (!FileUtil.doesFileExist(chartPath))
{
trace('Previously loaded chart file (${chartPath}) does not exist, disabling link...');
linkRecentChart.disabled = true;
}
splashRecentContainer.addComponent(linkRecentChart);
}
#else
var splashRecentContainer:Null<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" // Create New Song "Easy/Normal/Hard"
var linkCreateBasic:Null<Link> = dialog.findComponent('splashCreateFromSongBasic', Link); var linkCreateBasic:Null<Link> = dialog.findComponent('splashCreateFromSongBasic', Link);
if (linkCreateBasic == null) throw 'Could not locate splashCreateFromSongBasic link in Welcome dialog'; if (linkCreateBasic == null) throw 'Could not locate splashCreateFromSongBasic link in Welcome dialog';
@ -181,6 +243,7 @@ class ChartEditorDialogHandler
if (dialog == null) throw 'Could not locate Upload Chart dialog'; if (dialog == null) throw 'Could not locate Upload Chart dialog';
dialog.onDialogClosed = function(_event) { dialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY) if (_event.button == DialogButton.APPLY)
{ {
// Simply let the dialog close. // Simply let the dialog close.
@ -195,6 +258,7 @@ class ChartEditorDialogHandler
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Chart dialog'; if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Chart dialog';
state.isHaxeUIDialogOpen = true;
buttonCancel.onClick = function(_event) { buttonCancel.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL); dialog.hideDialog(DialogButton.CANCEL);
} }
@ -221,7 +285,8 @@ class ChartEditorDialogHandler
{ {
try try
{ {
if (ChartEditorImportExportHandler.loadFromFNFC(state, selectedFile.bytes)) var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFC(state, selectedFile.bytes);
if (result != null)
{ {
#if !mac #if !mac
NotificationManager.instance.addNotification( NotificationManager.instance.addNotification(
@ -260,21 +325,33 @@ class ChartEditorDialogHandler
try try
{ {
if (ChartEditorImportExportHandler.loadFromFNFCPath(state, path.toString())) var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(state, path.toString());
if (result != null)
{ {
#if !mac #if !mac
NotificationManager.instance.addNotification( NotificationManager.instance.addNotification(
{ {
title: 'Success', title: 'Success',
body: 'Loaded chart (${path.toString()})', body: result.length == 0 ? 'Loaded chart (${path.toString()})' : 'Loaded chart (${path.toString()})\n${result.join("\n")}',
type: NotificationType.Success, type: result.length == 0 ? NotificationType.Success : NotificationType.Warning,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME expiryMs: Constants.NOTIFICATION_DISMISS_TIME
}); });
#end #end
dialog.hideDialog(DialogButton.APPLY); dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile); removeDropHandler(onDropFile);
} }
else
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load chart (${path.toString()})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
}
} }
catch (err) catch (err)
{ {
@ -320,6 +397,8 @@ class ChartEditorDialogHandler
var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
uploadVocalsDialog.onDialogClosed = function(_event) { uploadVocalsDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false; state.isHaxeUIDialogOpen = false;
state.currentWorkingFilePath = null; // Built from parts, so no .fnfc to save to.
state.switchToCurrentInstrumental();
state.postLoadInstrumental(); state.postLoadInstrumental();
} }
} }
@ -359,6 +438,8 @@ class ChartEditorDialogHandler
var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
uploadVocalsDialog.onDialogClosed = function(_event) { uploadVocalsDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false; state.isHaxeUIDialogOpen = false;
state.currentWorkingFilePath = null; // New file, so no path.
state.switchToCurrentInstrumental();
state.postLoadInstrumental(); state.postLoadInstrumental();
} }
} }
@ -396,6 +477,7 @@ class ChartEditorDialogHandler
var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
uploadVocalsDialog.onDialogClosed = function(_event) { uploadVocalsDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false; state.isHaxeUIDialogOpen = false;
state.currentWorkingFilePath = null; // New file, so no path.
state.switchToCurrentInstrumental(); state.switchToCurrentInstrumental();
state.postLoadInstrumental(); state.postLoadInstrumental();
} }
@ -454,6 +536,7 @@ class ChartEditorDialogHandler
var uploadVocalsDialogErect:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog var uploadVocalsDialogErect:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
uploadVocalsDialogErect.onDialogClosed = function(_event) { uploadVocalsDialogErect.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false; state.isHaxeUIDialogOpen = false;
state.currentWorkingFilePath = null; // New file, so no path.
state.switchToCurrentInstrumental(); state.switchToCurrentInstrumental();
state.postLoadInstrumental(); state.postLoadInstrumental();
} }
@ -630,7 +713,9 @@ class ChartEditorDialogHandler
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Song Metadata dialog'; if (buttonCancel == null) throw 'Could not locate dialogCancel button in Song Metadata dialog';
state.isHaxeUIDialogOpen = true;
buttonCancel.onClick = function(_event) { buttonCancel.onClick = function(_event) {
state.isHaxeUIDialogOpen = false;
dialog.hideDialog(DialogButton.CANCEL); dialog.hideDialog(DialogButton.CANCEL);
} }
@ -745,6 +830,7 @@ class ChartEditorDialogHandler
var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button); var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button);
if (dialogContinue == null) throw 'Could not locate dialogContinue button in Song Metadata dialog'; if (dialogContinue == null) throw 'Could not locate dialogContinue button in Song Metadata dialog';
dialogContinue.onClick = (_event) -> { dialogContinue.onClick = (_event) -> {
state.songMetadata.clear();
state.songMetadata.set(targetVariation, newSongMetadata); state.songMetadata.set(targetVariation, newSongMetadata);
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
@ -1352,7 +1438,9 @@ class ChartEditorDialogHandler
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Import Chart dialog'; if (buttonCancel == null) throw 'Could not locate dialogCancel button in Import Chart dialog';
state.isHaxeUIDialogOpen = true;
buttonCancel.onClick = function(_event) { buttonCancel.onClick = function(_event) {
state.isHaxeUIDialogOpen = false;
dialog.hideDialog(DialogButton.CANCEL); dialog.hideDialog(DialogButton.CANCEL);
} }
@ -1513,7 +1601,9 @@ class ChartEditorDialogHandler
// If all validators succeeded, this callback is called. // If all validators succeeded, this callback is called.
state.isHaxeUIDialogOpen = true;
variationForm.onSubmit = function(_event) { variationForm.onSubmit = function(_event) {
state.isHaxeUIDialogOpen = false;
trace('Add Variation dialog submitted, validation succeeded!'); trace('Add Variation dialog submitted, validation succeeded!');
var dialogVariationName:Null<TextField> = dialog.findComponent('dialogVariationName', TextField); var dialogVariationName:Null<TextField> = dialog.findComponent('dialogVariationName', TextField);

View file

@ -7,7 +7,7 @@ import haxe.io.Path;
import funkin.util.SerializerUtil; import funkin.util.SerializerUtil;
import haxe.ui.notifications.NotificationManager; import haxe.ui.notifications.NotificationManager;
import funkin.util.FileUtil; import funkin.util.FileUtil;
import funkin.util.FileUtil; import funkin.util.FileUtil.FileWriteMode;
import haxe.io.Bytes; import haxe.io.Bytes;
import funkin.play.song.Song; import funkin.play.song.Song;
import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongChartData;
@ -53,6 +53,8 @@ class ChartEditorImportExportHandler
state.sortChartData(); state.sortChartData();
ChartEditorAudioHandler.wipeInstrumentalData(state);
ChartEditorAudioHandler.wipeVocalData(state);
state.stopExistingVocals(); state.stopExistingVocals();
var variations:Array<String> = state.availableVariations; var variations:Array<String> = state.availableVariations;
@ -91,7 +93,10 @@ class ChartEditorImportExportHandler
} }
} }
state.isHaxeUIDialogOpen = false;
state.currentWorkingFilePath = null; // New file, so no path.
state.switchToCurrentInstrumental(); state.switchToCurrentInstrumental();
state.postLoadInstrumental();
state.refreshMetadataToolbox(); state.refreshMetadataToolbox();
@ -138,31 +143,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); var bytes:Null<Bytes> = FileUtil.readBytesFromPath(path);
if (bytes == null) return false; if (bytes == null) return null;
trace('Loaded ${bytes.length} bytes from $path'); trace('Loaded ${bytes.length} bytes from $path');
var result:Bool = loadFromFNFC(state, bytes); var result:Null<Array<String>> = loadFromFNFC(state, bytes);
if (result) if (result != null)
{ {
state.currentWorkingFilePath = path; state.currentWorkingFilePath = path;
state.saveDataDirty = false; // Just loaded file!
} }
return result; 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 state
* @param bytes * @param bytes
* @param instId * @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 songMetadatas:Map<String, SongMetadata> = [];
var songChartDatas:Map<String, SongChartData> = []; var songChartDatas:Map<String, SongChartData> = [];
@ -231,8 +245,8 @@ class ChartEditorImportExportHandler
songChartDatas.set(variation, variChartData); songChartDatas.set(variation, variChartData);
} }
ChartEditorAudioHandler.stopExistingInstrumental(state); ChartEditorAudioHandler.wipeInstrumentalData(state);
ChartEditorAudioHandler.stopExistingVocals(state); ChartEditorAudioHandler.wipeVocalData(state);
// Load instrumentals // Load instrumentals
for (variation in [Constants.DEFAULT_VARIATION].concat(variationList)) for (variation in [Constants.DEFAULT_VARIATION].concat(variationList))
@ -264,12 +278,14 @@ class ChartEditorImportExportHandler
{ {
if (!ChartEditorAudioHandler.loadVocalsFromBytes(state, playerVocalsFileBytes, playerCharId, instId)) 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 else
{ {
throw 'Could not find vocals ($playerVocalsFileName).'; warnings.push('Could not find vocals ($playerVocalsFileName).');
// throw 'Could not find vocals ($playerVocalsFileName).';
} }
if (opponentCharId != null) if (opponentCharId != null)
@ -280,12 +296,14 @@ class ChartEditorImportExportHandler
{ {
if (!ChartEditorAudioHandler.loadVocalsFromBytes(state, opponentVocalsFileBytes, opponentCharId, instId)) 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 else
{ {
throw 'Could not load vocals ($opponentCharId).'; warnings.push('Could not find vocals ($opponentVocalsFileName).');
// throw 'Could not find vocals ($opponentVocalsFileName).';
} }
} }
} }
@ -297,7 +315,7 @@ class ChartEditorImportExportHandler
state.switchToCurrentInstrumental(); state.switchToCurrentInstrumental();
return true; return warnings;
} }
/** /**
@ -345,8 +363,10 @@ class ChartEditorImportExportHandler
if (force) if (force)
{ {
var targetMode:FileWriteMode = Force;
if (targetPath == null) if (targetPath == null)
{ {
targetMode = Skip;
targetPath = Path.join([ targetPath = Path.join([
'./backups/', './backups/',
'chart-editor-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}' 'chart-editor-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}'
@ -355,13 +375,24 @@ class ChartEditorImportExportHandler
// We have to force write because the program will die before the save dialog is closed. // We have to force write because the program will die before the save dialog is closed.
trace('Force exporting to $targetPath...'); trace('Force exporting to $targetPath...');
FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath); FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode);
state.saveDataDirty = false;
} }
else else
{ {
// Prompt and save. // Prompt and save.
var onSave:Array<String>->Void = function(paths:Array<String>) { var onSave:Array<String>->Void = function(paths:Array<String>) {
trace('Successfully exported files.'); 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();
}
}; };
var onCancel:Void->Void = function() { var onCancel:Void->Void = function() {
@ -372,6 +403,7 @@ class ChartEditorImportExportHandler
try try
{ {
FileUtil.saveChartAsFNFC(zipEntries, onSave, onCancel, '${state.currentSongId}.${Constants.EXT_CHART}'); FileUtil.saveChartAsFNFC(zipEntries, onSave, onCancel, '${state.currentSongId}.${Constants.EXT_CHART}');
state.saveDataDirty = false;
} }
catch (e) {} catch (e) {}
} }

View file

@ -402,8 +402,12 @@ class Constants
public static final GHOST_TAPPING:Bool = false; public static final GHOST_TAPPING:Bool = false;
/** /**
* The separator between an asset library and the asset path. * 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.
*/ */
public static final LIBRARY_SEPARATOR:String = ':'; public static final LIBRARY_SEPARATOR:String = ':';

View file

@ -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. * 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? * @param typeFilter TODO What does this do?
@ -183,7 +183,7 @@ class FileUtil
var filter:String = convertTypeFilter(typeFilter); var filter:String = convertTypeFilter(typeFilter);
var fileDialog:FileDialog = new FileDialog(); 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); if (onCancel != null) fileDialog.onCancel.add(onCancel);
fileDialog.save(data, filter, defaultFileName, dialogTitle); fileDialog.save(data, filter, defaultFileName, dialogTitle);
@ -268,7 +268,8 @@ class FileUtil
var zipBytes:Bytes = createZIPFromEntries(resources); var zipBytes:Bytes = createZIPFromEntries(resources);
var onSave:String->Void = function(path:String) { 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. // Prompt the user to save the ZIP file.
@ -287,7 +288,8 @@ class FileUtil
var zipBytes:Bytes = createZIPFromEntries(resources); var zipBytes:Bytes = createZIPFromEntries(resources);
var onSave:String->Void = function(path:String) { 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. // Prompt the user to save the ZIP file.
@ -302,14 +304,14 @@ class FileUtil
* Use `saveFilesAsZIP` instead. * Use `saveFilesAsZIP` instead.
* @param force Whether to force overwrite an existing file. * @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 #if desktop
// Create a ZIP file. // Create a ZIP file.
var zipBytes:Bytes = createZIPFromEntries(resources); var zipBytes:Bytes = createZIPFromEntries(resources);
// Write the ZIP. // Write the ZIP.
writeBytesToPath(path, zipBytes, force ? Force : Skip); writeBytesToPath(path, zipBytes, mode);
return true; return true;
#else #else
@ -344,13 +346,22 @@ class FileUtil
public static function readBytesFromPath(path:String):Bytes public static function readBytesFromPath(path:String):Bytes
{ {
#if sys #if sys
if (!sys.FileSystem.exists(path)) return null; if (!doesFileExist(path)) return null;
return sys.io.File.getBytes(path); return sys.io.File.getBytes(path);
#else #else
return null; return null;
#end #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. * Browse for a file to read and execute a callback once we have a file reference.
* Works great on HTML5 or desktop. * Works great on HTML5 or desktop.
@ -434,18 +445,20 @@ class FileUtil
case Force: case Force:
sys.io.File.saveContent(path, data); sys.io.File.saveContent(path, data);
case Skip: case Skip:
if (!sys.FileSystem.exists(path)) if (!doesFileExist(path))
{ {
sys.io.File.saveContent(path, data); sys.io.File.saveContent(path, data);
} }
else else
{ {
throw 'File already exists: $path'; // Do nothing.
// throw 'File already exists: $path';
} }
case Ask: case Ask:
if (sys.FileSystem.exists(path)) if (doesFileExist(path))
{ {
// TODO: We don't have the technology to use native popups yet. // TODO: We don't have the technology to use native popups yet.
throw 'File already exists: $path';
} }
else else
{ {
@ -475,18 +488,20 @@ class FileUtil
case Force: case Force:
sys.io.File.saveBytes(path, data); sys.io.File.saveBytes(path, data);
case Skip: case Skip:
if (!sys.FileSystem.exists(path)) if (!doesFileExist(path))
{ {
sys.io.File.saveBytes(path, data); sys.io.File.saveBytes(path, data);
} }
else else
{ {
throw 'File already exists: $path'; // Do nothing.
// throw 'File already exists: $path';
} }
case Ask: case Ask:
if (sys.FileSystem.exists(path)) if (doesFileExist(path))
{ {
// TODO: We don't have the technology to use native popups yet. // TODO: We don't have the technology to use native popups yet.
throw 'File already exists: $path';
} }
else else
{ {
@ -523,7 +538,7 @@ class FileUtil
public static function createDirIfNotExists(dir:String):Void public static function createDirIfNotExists(dir:String):Void
{ {
#if sys #if sys
if (!sys.FileSystem.exists(dir)) if (!doesFileExist(dir))
{ {
sys.FileSystem.createDirectory(dir); sys.FileSystem.createDirectory(dir);
} }