package funkin.ui.debug.charting.handlers; import flixel.util.FlxTimer; import funkin.data.song.importer.FNFLegacyData; import funkin.data.song.importer.FNFLegacyImporter; import funkin.data.song.SongData.SongCharacterData; import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongMetadata; import funkin.data.song.SongData.SongTimeChange; import funkin.data.song.SongRegistry; import funkin.input.Cursor; import funkin.play.character.BaseCharacter; import funkin.play.character.CharacterData; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.song.Song; import funkin.play.stage.StageData; import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget; import funkin.ui.debug.charting.dialogs.ChartEditorAboutDialog; import funkin.ui.debug.charting.dialogs.ChartEditorUploadChartDialog; import funkin.ui.debug.charting.dialogs.ChartEditorWelcomeDialog; import funkin.ui.debug.charting.util.ChartEditorDropdowns; import funkin.util.Constants; import funkin.util.FileUtil; import funkin.util.SerializerUtil; import funkin.util.SortUtil; import funkin.util.VersionUtil; import haxe.io.Path; import haxe.ui.components.Button; import haxe.ui.components.DropDown; import haxe.ui.components.Label; import haxe.ui.components.Link; import haxe.ui.components.NumberStepper; import haxe.ui.components.Slider; import haxe.ui.components.TextField; import haxe.ui.containers.Box; import haxe.ui.containers.dialogs.Dialog; import haxe.ui.containers.dialogs.Dialog.DialogButton; import haxe.ui.containers.dialogs.Dialogs; import haxe.ui.containers.Form; import haxe.ui.containers.VBox; import haxe.ui.core.Component; import haxe.ui.events.UIEvent; import haxe.ui.notifications.NotificationManager; import haxe.ui.notifications.NotificationType; import haxe.ui.RuntimeComponentBuilder; import thx.semver.Version; using Lambda; /** * Handles dialogs for the new Chart Editor. */ @:nullSafety @:access(funkin.ui.debug.charting.ChartEditorState) class ChartEditorDialogHandler { // Paths to HaxeUI layout files for each dialog. static final CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-chart'); static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst'); static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata'); static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals'); static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals-entry'); static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts'); static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts-entry'); static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart'); static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT:String = Paths.ui('chart-editor/dialogs/user-guide'); static final CHART_EDITOR_DIALOG_ADD_VARIATION_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-variation'); static final CHART_EDITOR_DIALOG_ADD_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-difficulty'); /** * Builds and opens a dialog giving brief credits for the chart editor. * @param state The current chart editor state. * @return The dialog that was opened. */ public static function openAboutDialog(state:ChartEditorState):Null { var dialog = ChartEditorAboutDialog.build(state); dialog.zIndex = 1000; state.isHaxeUIDialogOpen = true; return dialog; } /** * Builds and opens a dialog letting the user create a new chart, open a recent chart, or load from a template. * @param state The current chart editor state. * @param closable Whether the dialog can be closed by the user. * @return The dialog that was opened. */ public static function openWelcomeDialog(state:ChartEditorState, closable:Bool = true):Null { var dialog = ChartEditorWelcomeDialog.build(state, closable); dialog.zIndex = 1000; state.isHaxeUIDialogOpen = true; state.fadeInWelcomeMusic(); return dialog; } /** * Builds and opens a dialog letting the user browse for a chart file to open. * @param state The current chart editor state. * @param closable Whether the dialog can be closed by the user. * @return The dialog that was opened. */ public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null { var dialog = ChartEditorUploadChartDialog.build(state, closable); dialog.zIndex = 1000; state.isHaxeUIDialogOpen = true; return dialog; } /** * Open the wizard for opening an existing chart from individual files. * @param state * @param closable */ public static function openBrowseWizard(state:ChartEditorState, closable:Bool):Void { // Open the "Open Chart" wizard // Step 1. Open Chart var openChartDialog:Dialog = openChartDialog(state); openChartDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { // Step 2. Upload instrumental var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); uploadInstDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { // Step 3. Upload Vocals // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog uploadVocalsDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; state.currentWorkingFilePath = null; // Built from parts, so no .fnfc to save to. state.switchToCurrentInstrumental(); state.postLoadInstrumental(); } } else { // User cancelled the wizard! Back to the welcome dialog. state.openWelcomeDialog(closable); } }; } else { // User cancelled the wizard! Back to the welcome dialog. state.openWelcomeDialog(closable); } }; } public static function openImportChartWizard(state:ChartEditorState, format:String, closable:Bool):Void { // Open the "Open Chart" wizard // Step 1. Open Chart var openChartDialog:Null = openImportChartDialog(state, format); if (openChartDialog == null) throw 'Could not locate Import Chart dialog'; openChartDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { // Step 2. Upload instrumental var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); uploadInstDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { // Step 3. Upload Vocals // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog uploadVocalsDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; state.currentWorkingFilePath = null; // New file, so no path. state.switchToCurrentInstrumental(); state.postLoadInstrumental(); } } else { // User cancelled the wizard! Back to the welcome dialog. state.openWelcomeDialog(closable); } }; } else { // User cancelled the wizard! Back to the welcome dialog. state.openWelcomeDialog(closable); } }; } public static function openCreateSongWizardBasicOnly(state:ChartEditorState, closable:Bool):Void { // Step 1. Song Metadata var songMetadataDialog:Dialog = openSongMetadataDialog(state, false, Constants.DEFAULT_VARIATION); songMetadataDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { // Step 2. Upload Instrumental var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); uploadInstDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { // Step 3. Upload Vocals // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog uploadVocalsDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; state.currentWorkingFilePath = null; // New file, so no path. state.switchToCurrentInstrumental(); state.postLoadInstrumental(); } } else { // User cancelled the wizard at Step 2! Back to the welcome dialog. state.openWelcomeDialog(closable); } }; } else { // User cancelled the wizard at Step 1! Back to the welcome dialog. state.openWelcomeDialog(closable); } }; } public static function openCreateSongWizardErectOnly(state:ChartEditorState, closable:Bool):Void { // Step 1. Song Metadata var songMetadataDialog:Dialog = openSongMetadataDialog(state, true, Constants.DEFAULT_VARIATION); songMetadataDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { // Step 2. Upload Instrumental var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); uploadInstDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { // Step 3. Upload Vocals // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog uploadVocalsDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; state.currentWorkingFilePath = null; // New file, so no path. state.switchToCurrentInstrumental(); state.postLoadInstrumental(); } } else { // User cancelled the wizard at Step 2! Back to the welcome dialog. state.openWelcomeDialog(closable); } }; } else { // User cancelled the wizard at Step 1! Back to the welcome dialog. state.openWelcomeDialog(closable); } }; } public static function openCreateSongWizardBasicErect(state:ChartEditorState, closable:Bool):Void { // Step 1. Song Metadata var songMetadataDialog:Dialog = openSongMetadataDialog(state, false, Constants.DEFAULT_VARIATION); songMetadataDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { // Step 2. Upload Instrumental var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); uploadInstDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { // Step 3. Upload Vocals // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog uploadVocalsDialog.onDialogClosed = function(_event) { state.switchToCurrentInstrumental(); // Step 4. Song Metadata (Erect) var songMetadataDialogErect:Dialog = openSongMetadataDialog(state, true, 'erect'); songMetadataDialogErect.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { // Switch to the Erect variation so uploading the instrumental applies properly. state.selectedVariation = 'erect'; // Step 5. Upload Instrumental (Erect) var uploadInstDialogErect:Dialog = openUploadInstDialog(state, closable); uploadInstDialogErect.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { // Step 6. Upload Vocals (Erect) // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. var uploadVocalsDialogErect:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog uploadVocalsDialogErect.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; state.currentWorkingFilePath = null; // New file, so no path. state.switchToCurrentInstrumental(); state.postLoadInstrumental(); } } else { // User cancelled the wizard at Step 5! Back to the welcome dialog. state.openWelcomeDialog(closable); } }; } else { // User cancelled the wizard at Step 4! Back to the welcome dialog. state.openWelcomeDialog(closable); } } } } else { // User cancelled the wizard at Step 2! Back to the welcome dialog. state.openWelcomeDialog(closable); } }; } else { // User cancelled the wizard at Step 1! Back to the welcome dialog. state.openWelcomeDialog(closable); } }; } /** * Builds and opens a dialog where the user uploads an instrumental for the current song. * @param state The current chart editor state. * @param closable Whether the dialog can be closed by the user. * @return The dialog that was opened. */ @:haxe.warning("-WVarInit") // Hide the warning about the onDropFile handler. public static function openUploadInstDialog(state:ChartEditorState, closable:Bool = true):Dialog { var dialog:Null = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT, true, closable); if (dialog == null) throw 'Could not locate Upload Instrumental dialog'; var buttonCancel:Null