mirror of
				https://github.com/ninjamuffin99/Funkin.git
				synced 2025-10-31 00:14:58 +00:00 
			
		
		
		
	Merge branch 'rewrite/master' into bugfix/senpai-naughty
This commit is contained in:
		
						commit
						ad345fd834
					
				
							
								
								
									
										2
									
								
								assets
									
									
									
									
									
								
							
							
								
								
								
								
								
								
							
						
						
									
										2
									
								
								assets
									
									
									
									
									
								
							|  | @ -1 +1 @@ | |||
| Subproject commit 482ef76582208a484a2f6450ce5ad9e278db08f8 | ||||
| Subproject commit 3b05b0fdd8e3b2cd09b9e4e415c186bae8e3b7d3 | ||||
|  | @ -674,6 +674,22 @@ class SongNoteDataRaw | |||
|     this.kind = kind; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * The direction of the note, if applicable. | ||||
|    * Strips the strumline index from the data. | ||||
|    * | ||||
|    * 0 = left, 1 = down, 2 = up, 3 = right | ||||
|    */ | ||||
|   public inline function getDirection(strumlineSize:Int = 4):Int | ||||
|   { | ||||
|     return this.data % strumlineSize; | ||||
|   } | ||||
| 
 | ||||
|   public function getDirectionName(strumlineSize:Int = 4):String | ||||
|   { | ||||
|     return SongNoteData.buildDirectionName(this.data, strumlineSize); | ||||
|   } | ||||
| 
 | ||||
|   @:jignored | ||||
|   var _stepTime:Null<Float> = null; | ||||
| 
 | ||||
|  | @ -730,22 +746,6 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw | |||
|     this = new SongNoteDataRaw(time, data, length, kind); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * The direction of the note, if applicable. | ||||
|    * Strips the strumline index from the data. | ||||
|    * | ||||
|    * 0 = left, 1 = down, 2 = up, 3 = right | ||||
|    */ | ||||
|   public inline function getDirection(strumlineSize:Int = 4):Int | ||||
|   { | ||||
|     return this.data % strumlineSize; | ||||
|   } | ||||
| 
 | ||||
|   public function getDirectionName(strumlineSize:Int = 4):String | ||||
|   { | ||||
|     return SongNoteData.buildDirectionName(this.data, strumlineSize); | ||||
|   } | ||||
| 
 | ||||
|   public static function buildDirectionName(data:Int, strumlineSize:Int = 4):String | ||||
|   { | ||||
|     switch (data % strumlineSize) | ||||
|  |  | |||
|  | @ -5,12 +5,17 @@ import funkin.save.migrator.SaveDataMigrator; | |||
| import thx.semver.Version; | ||||
| import funkin.input.Controls.Device; | ||||
| 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 | ||||
| @:forward(volume, mute) | ||||
| abstract Save(RawSaveData) | ||||
| { | ||||
|   public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.0"; | ||||
|   // Version 2.0.1 adds attributes to `optionsChartEditor`, that should return default values if they are null. | ||||
|   public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.1"; | ||||
|   public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; | ||||
| 
 | ||||
|   // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility. | ||||
|  | @ -94,6 +99,18 @@ abstract Save(RawSaveData) | |||
|         optionsChartEditor: | ||||
|           { | ||||
|             // 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> | ||||
|   { | ||||
|     return this.api.newgrounds.sessionId = value; | ||||
|     this.api.newgrounds.sessionId = value; | ||||
|     flush(); | ||||
|     return this.api.newgrounds.sessionId; | ||||
|   } | ||||
| 
 | ||||
|   public var enabledModIds(get, set):Array<String>; | ||||
|  | @ -136,7 +155,213 @@ abstract Save(RawSaveData) | |||
| 
 | ||||
|   function set_enabledModIds(value:Array<String>):Array<String> | ||||
|   { | ||||
|     return this.mods.enabledMods = value; | ||||
|     this.mods.enabledMods = value; | ||||
|     flush(); | ||||
|     return this.mods.enabledMods; | ||||
|   } | ||||
| 
 | ||||
|   public var chartEditorPreviousFiles(get, set):Array<String>; | ||||
| 
 | ||||
|   function get_chartEditorPreviousFiles():Array<String> | ||||
|   { | ||||
|     if (this.optionsChartEditor.previousFiles == null) this.optionsChartEditor.previousFiles = []; | ||||
| 
 | ||||
|     return this.optionsChartEditor.previousFiles; | ||||
|   } | ||||
| 
 | ||||
|   function set_chartEditorPreviousFiles(value:Array<String>):Array<String> | ||||
|   { | ||||
|     // Set and apply. | ||||
|     this.optionsChartEditor.previousFiles = value; | ||||
|     flush(); | ||||
|     return this.optionsChartEditor.previousFiles; | ||||
|   } | ||||
| 
 | ||||
|   public var chartEditorNoteQuant(get, set):Int; | ||||
| 
 | ||||
|   function get_chartEditorNoteQuant():Int | ||||
|   { | ||||
|     if (this.optionsChartEditor.noteQuant == null) this.optionsChartEditor.noteQuant = 3; | ||||
| 
 | ||||
|     return this.optionsChartEditor.noteQuant; | ||||
|   } | ||||
| 
 | ||||
|   function set_chartEditorNoteQuant(value:Int):Int | ||||
|   { | ||||
|     // Set and apply. | ||||
|     this.optionsChartEditor.noteQuant = value; | ||||
|     flush(); | ||||
|     return this.optionsChartEditor.noteQuant; | ||||
|   } | ||||
| 
 | ||||
|   public var chartEditorLiveInputStyle(get, set):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. | ||||
|  */ | ||||
| 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
											
										
									
								
							|  | @ -217,6 +217,18 @@ class ChartEditorAudioHandler | |||
|     snd.play(); | ||||
|   } | ||||
| 
 | ||||
|   public static function wipeInstrumentalData(state:ChartEditorState):Void | ||||
|   { | ||||
|     state.audioInstTrackData.clear(); | ||||
|     stopExistingInstrumental(state); | ||||
|   } | ||||
| 
 | ||||
|   public static function wipeVocalData(state:ChartEditorState):Void | ||||
|   { | ||||
|     state.audioVocalTrackData.clear(); | ||||
|     stopExistingVocals(state); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Create a list of ZIP file entries from the current loaded instrumental tracks in the chart eidtor. | ||||
|    * @param state The chart editor state. | ||||
|  | @ -226,18 +238,27 @@ class ChartEditorAudioHandler | |||
|   { | ||||
|     var zipEntries = []; | ||||
| 
 | ||||
|     for (key in state.audioInstTrackData.keys()) | ||||
|     var instTrackIds = state.audioInstTrackData.keys().array(); | ||||
|     for (key in instTrackIds) | ||||
|     { | ||||
|       if (key == 'default') | ||||
|       { | ||||
|         var data:Null<Bytes> = state.audioInstTrackData.get('default'); | ||||
|         if (data == null) continue; | ||||
|         if (data == null) | ||||
|         { | ||||
|           trace('[WARN] Failed to access inst track ($key)'); | ||||
|           continue; | ||||
|         } | ||||
|         zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', data)); | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|         var data:Null<Bytes> = state.audioInstTrackData.get(key); | ||||
|         if (data == null) continue; | ||||
|         if (data == null) | ||||
|         { | ||||
|           trace('[WARN] Failed to access inst track ($key)'); | ||||
|           continue; | ||||
|         } | ||||
|         zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst-${key}.ogg', data)); | ||||
|       } | ||||
|     } | ||||
|  | @ -254,10 +275,15 @@ class ChartEditorAudioHandler | |||
|   { | ||||
|     var zipEntries = []; | ||||
| 
 | ||||
|     var vocalTrackIds = state.audioVocalTrackData.keys().array(); | ||||
|     for (key in state.audioVocalTrackData.keys()) | ||||
|     { | ||||
|       var data:Null<Bytes> = state.audioVocalTrackData.get(key); | ||||
|       if (data == null) continue; | ||||
|       if (data == null) | ||||
|       { | ||||
|         trace('[WARN] Failed to access vocal track ($key)'); | ||||
|         continue; | ||||
|       } | ||||
|       zipEntries.push(FileUtil.makeZIPEntryFromBytes('Voices-${key}.ogg', data)); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -85,11 +85,73 @@ class ChartEditorDialogHandler | |||
|     var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable); | ||||
|     if (dialog == null) throw 'Could not locate Welcome dialog'; | ||||
| 
 | ||||
|     state.isHaxeUIDialogOpen = true; | ||||
|     dialog.onDialogClosed = function(_event) { | ||||
|       state.isHaxeUIDialogOpen = false; | ||||
|       // Called when the Welcome dialog is closed while it is closable. | ||||
|       state.stopWelcomeMusic(); | ||||
|     } | ||||
| 
 | ||||
|     #if sys | ||||
|     var splashRecentContainer:Null<VBox> = dialog.findComponent('splashRecentContainer', VBox); | ||||
|     if (splashRecentContainer == null) throw 'Could not locate splashRecentContainer in Welcome dialog'; | ||||
| 
 | ||||
|     for (chartPath in state.previousWorkingFilePaths) | ||||
|     { | ||||
|       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" | ||||
|     var linkCreateBasic:Null<Link> = dialog.findComponent('splashCreateFromSongBasic', Link); | ||||
|     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'; | ||||
| 
 | ||||
|     dialog.onDialogClosed = function(_event) { | ||||
|       state.isHaxeUIDialogOpen = false; | ||||
|       if (_event.button == DialogButton.APPLY) | ||||
|       { | ||||
|         // Simply let the dialog close. | ||||
|  | @ -195,6 +258,7 @@ class ChartEditorDialogHandler | |||
|     var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); | ||||
|     if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Chart dialog'; | ||||
| 
 | ||||
|     state.isHaxeUIDialogOpen = true; | ||||
|     buttonCancel.onClick = function(_event) { | ||||
|       dialog.hideDialog(DialogButton.CANCEL); | ||||
|     } | ||||
|  | @ -221,7 +285,8 @@ class ChartEditorDialogHandler | |||
|           { | ||||
|             try | ||||
|             { | ||||
|               if (ChartEditorImportExportHandler.loadFromFNFC(state, selectedFile.bytes)) | ||||
|               var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFC(state, selectedFile.bytes); | ||||
|               if (result != null) | ||||
|               { | ||||
|                 #if !mac | ||||
|                 NotificationManager.instance.addNotification( | ||||
|  | @ -260,21 +325,33 @@ class ChartEditorDialogHandler | |||
| 
 | ||||
|       try | ||||
|       { | ||||
|         if (ChartEditorImportExportHandler.loadFromFNFCPath(state, path.toString())) | ||||
|         var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(state, path.toString()); | ||||
|         if (result != null) | ||||
|         { | ||||
|           #if !mac | ||||
|           NotificationManager.instance.addNotification( | ||||
|             { | ||||
|               title: 'Success', | ||||
|               body: 'Loaded chart (${path.toString()})', | ||||
|               type: NotificationType.Success, | ||||
|               body: result.length == 0 ? 'Loaded chart (${path.toString()})' : 'Loaded chart (${path.toString()})\n${result.join("\n")}', | ||||
|               type: result.length == 0 ? NotificationType.Success : NotificationType.Warning, | ||||
|               expiryMs: Constants.NOTIFICATION_DISMISS_TIME | ||||
|             }); | ||||
|           #end | ||||
| 
 | ||||
|           dialog.hideDialog(DialogButton.APPLY); | ||||
|           removeDropHandler(onDropFile); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|           #if !mac | ||||
|           NotificationManager.instance.addNotification( | ||||
|             { | ||||
|               title: 'Failure', | ||||
|               body: 'Failed to load chart (${path.toString()})', | ||||
|               type: NotificationType.Error, | ||||
|               expiryMs: Constants.NOTIFICATION_DISMISS_TIME | ||||
|             }); | ||||
|           #end | ||||
|         } | ||||
|       } | ||||
|       catch (err) | ||||
|       { | ||||
|  | @ -320,6 +397,8 @@ class ChartEditorDialogHandler | |||
|             var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog | ||||
|             uploadVocalsDialog.onDialogClosed = function(_event) { | ||||
|               state.isHaxeUIDialogOpen = false; | ||||
|               state.currentWorkingFilePath = null; // Built from parts, so no .fnfc to save to. | ||||
|               state.switchToCurrentInstrumental(); | ||||
|               state.postLoadInstrumental(); | ||||
|             } | ||||
|           } | ||||
|  | @ -359,6 +438,8 @@ class ChartEditorDialogHandler | |||
|             var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog | ||||
|             uploadVocalsDialog.onDialogClosed = function(_event) { | ||||
|               state.isHaxeUIDialogOpen = false; | ||||
|               state.currentWorkingFilePath = null; // New file, so no path. | ||||
|               state.switchToCurrentInstrumental(); | ||||
|               state.postLoadInstrumental(); | ||||
|             } | ||||
|           } | ||||
|  | @ -396,6 +477,7 @@ class ChartEditorDialogHandler | |||
|             var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog | ||||
|             uploadVocalsDialog.onDialogClosed = function(_event) { | ||||
|               state.isHaxeUIDialogOpen = false; | ||||
|               state.currentWorkingFilePath = null; // New file, so no path. | ||||
|               state.switchToCurrentInstrumental(); | ||||
|               state.postLoadInstrumental(); | ||||
|             } | ||||
|  | @ -454,6 +536,7 @@ class ChartEditorDialogHandler | |||
|                       var uploadVocalsDialogErect:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog | ||||
|                       uploadVocalsDialogErect.onDialogClosed = function(_event) { | ||||
|                         state.isHaxeUIDialogOpen = false; | ||||
|                         state.currentWorkingFilePath = null; // New file, so no path. | ||||
|                         state.switchToCurrentInstrumental(); | ||||
|                         state.postLoadInstrumental(); | ||||
|                       } | ||||
|  | @ -630,7 +713,9 @@ class ChartEditorDialogHandler | |||
| 
 | ||||
|     var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); | ||||
|     if (buttonCancel == null) throw 'Could not locate dialogCancel button in Song Metadata dialog'; | ||||
|     state.isHaxeUIDialogOpen = true; | ||||
|     buttonCancel.onClick = function(_event) { | ||||
|       state.isHaxeUIDialogOpen = false; | ||||
|       dialog.hideDialog(DialogButton.CANCEL); | ||||
|     } | ||||
| 
 | ||||
|  | @ -745,6 +830,7 @@ class ChartEditorDialogHandler | |||
|     var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button); | ||||
|     if (dialogContinue == null) throw 'Could not locate dialogContinue button in Song Metadata dialog'; | ||||
|     dialogContinue.onClick = (_event) -> { | ||||
|       state.songMetadata.clear(); | ||||
|       state.songMetadata.set(targetVariation, newSongMetadata); | ||||
| 
 | ||||
|       Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); | ||||
|  | @ -1352,7 +1438,9 @@ class ChartEditorDialogHandler | |||
|     var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); | ||||
|     if (buttonCancel == null) throw 'Could not locate dialogCancel button in Import Chart dialog'; | ||||
| 
 | ||||
|     state.isHaxeUIDialogOpen = true; | ||||
|     buttonCancel.onClick = function(_event) { | ||||
|       state.isHaxeUIDialogOpen = false; | ||||
|       dialog.hideDialog(DialogButton.CANCEL); | ||||
|     } | ||||
| 
 | ||||
|  | @ -1513,7 +1601,9 @@ class ChartEditorDialogHandler | |||
| 
 | ||||
|     // If all validators succeeded, this callback is called. | ||||
| 
 | ||||
|     state.isHaxeUIDialogOpen = true; | ||||
|     variationForm.onSubmit = function(_event) { | ||||
|       state.isHaxeUIDialogOpen = false; | ||||
|       trace('Add Variation dialog submitted, validation succeeded!'); | ||||
| 
 | ||||
|       var dialogVariationName:Null<TextField> = dialog.findComponent('dialogVariationName', TextField); | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ import haxe.io.Path; | |||
| import funkin.util.SerializerUtil; | ||||
| import haxe.ui.notifications.NotificationManager; | ||||
| import funkin.util.FileUtil; | ||||
| import funkin.util.FileUtil; | ||||
| import funkin.util.FileUtil.FileWriteMode; | ||||
| import haxe.io.Bytes; | ||||
| import funkin.play.song.Song; | ||||
| import funkin.data.song.SongData.SongChartData; | ||||
|  | @ -53,6 +53,8 @@ class ChartEditorImportExportHandler | |||
| 
 | ||||
|     state.sortChartData(); | ||||
| 
 | ||||
|     ChartEditorAudioHandler.wipeInstrumentalData(state); | ||||
|     ChartEditorAudioHandler.wipeVocalData(state); | ||||
|     state.stopExistingVocals(); | ||||
| 
 | ||||
|     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.postLoadInstrumental(); | ||||
| 
 | ||||
|     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); | ||||
|     if (bytes == null) return false; | ||||
|     if (bytes == null) return null; | ||||
| 
 | ||||
|     trace('Loaded ${bytes.length} bytes from $path'); | ||||
| 
 | ||||
|     var result:Bool = loadFromFNFC(state, bytes); | ||||
|     if (result) | ||||
|     var result:Null<Array<String>> = loadFromFNFC(state, bytes); | ||||
|     if (result != null) | ||||
|     { | ||||
|       state.currentWorkingFilePath = path; | ||||
|       state.saveDataDirty = false; // Just loaded file! | ||||
|     } | ||||
| 
 | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Load a chart's metadata, chart data, and audio from an FNFC archive.. | ||||
|    * Load a chart's metadata, chart data, and audio from an FNFC archive. | ||||
|    * @param state | ||||
|    * @param bytes | ||||
|    * @param instId | ||||
|    * @return Bool | ||||
|    * @return `null` on failure, `[]` on success, `[warnings]` on success with warnings. | ||||
|    */ | ||||
|   public static function loadFromFNFC(state:ChartEditorState, bytes:Bytes):Bool | ||||
|   public static function loadFromFNFC(state:ChartEditorState, bytes:Bytes):Null<Array<String>> | ||||
|   { | ||||
|     var warnings:Array<String> = []; | ||||
| 
 | ||||
|     var songMetadatas:Map<String, SongMetadata> = []; | ||||
|     var songChartDatas:Map<String, SongChartData> = []; | ||||
| 
 | ||||
|  | @ -231,8 +245,8 @@ class ChartEditorImportExportHandler | |||
|       songChartDatas.set(variation, variChartData); | ||||
|     } | ||||
| 
 | ||||
|     ChartEditorAudioHandler.stopExistingInstrumental(state); | ||||
|     ChartEditorAudioHandler.stopExistingVocals(state); | ||||
|     ChartEditorAudioHandler.wipeInstrumentalData(state); | ||||
|     ChartEditorAudioHandler.wipeVocalData(state); | ||||
| 
 | ||||
|     // Load instrumentals | ||||
|     for (variation in [Constants.DEFAULT_VARIATION].concat(variationList)) | ||||
|  | @ -264,12 +278,14 @@ class ChartEditorImportExportHandler | |||
|       { | ||||
|         if (!ChartEditorAudioHandler.loadVocalsFromBytes(state, playerVocalsFileBytes, playerCharId, instId)) | ||||
|         { | ||||
|           throw 'Could not load vocals ($playerCharId).'; | ||||
|           warnings.push('Could not parse vocals ($playerCharId).'); | ||||
|           // throw 'Could not parse vocals ($playerCharId).'; | ||||
|         } | ||||
|       } | ||||
|       else | ||||
|       { | ||||
|         throw 'Could not find vocals ($playerVocalsFileName).'; | ||||
|         warnings.push('Could not find vocals ($playerVocalsFileName).'); | ||||
|         // throw 'Could not find vocals ($playerVocalsFileName).'; | ||||
|       } | ||||
| 
 | ||||
|       if (opponentCharId != null) | ||||
|  | @ -280,12 +296,14 @@ class ChartEditorImportExportHandler | |||
|         { | ||||
|           if (!ChartEditorAudioHandler.loadVocalsFromBytes(state, opponentVocalsFileBytes, opponentCharId, instId)) | ||||
|           { | ||||
|             throw 'Could not load vocals ($opponentCharId).'; | ||||
|             warnings.push('Could not parse vocals ($opponentCharId).'); | ||||
|             // throw 'Could not parse vocals ($opponentCharId).'; | ||||
|           } | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|           throw 'Could not load vocals ($opponentCharId).'; | ||||
|           warnings.push('Could not find vocals ($opponentVocalsFileName).'); | ||||
|           // throw 'Could not find vocals ($opponentVocalsFileName).'; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | @ -297,7 +315,7 @@ class ChartEditorImportExportHandler | |||
| 
 | ||||
|     state.switchToCurrentInstrumental(); | ||||
| 
 | ||||
|     return true; | ||||
|     return warnings; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | @ -345,8 +363,10 @@ class ChartEditorImportExportHandler | |||
| 
 | ||||
|     if (force) | ||||
|     { | ||||
|       var targetMode:FileWriteMode = Force; | ||||
|       if (targetPath == null) | ||||
|       { | ||||
|         targetMode = Skip; | ||||
|         targetPath = Path.join([ | ||||
|           './backups/', | ||||
|           'chart-editor-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}' | ||||
|  | @ -355,13 +375,24 @@ class ChartEditorImportExportHandler | |||
| 
 | ||||
|       // We have to force write because the program will die before the save dialog is closed. | ||||
|       trace('Force exporting to $targetPath...'); | ||||
|       FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath); | ||||
|       FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode); | ||||
|       state.saveDataDirty = false; | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|       // Prompt and save. | ||||
|       var onSave:Array<String>->Void = function(paths:Array<String>) { | ||||
|         trace('Successfully exported files.'); | ||||
|         if (paths.length != 1) | ||||
|         { | ||||
|           trace('[WARN] Could not get save path.'); | ||||
|           state.applyWindowTitle(); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|           trace('Saved to "${paths[0]}"'); | ||||
|           state.currentWorkingFilePath = paths[0]; | ||||
|           state.applyWindowTitle(); | ||||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       var onCancel:Void->Void = function() { | ||||
|  | @ -372,6 +403,7 @@ class ChartEditorImportExportHandler | |||
|       try | ||||
|       { | ||||
|         FileUtil.saveChartAsFNFC(zipEntries, onSave, onCancel, '${state.currentSongId}.${Constants.EXT_CHART}'); | ||||
|         state.saveDataDirty = false; | ||||
|       } | ||||
|       catch (e) {} | ||||
|     } | ||||
|  |  | |||
|  | @ -402,8 +402,12 @@ class Constants | |||
|   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 = ':'; | ||||
| 
 | ||||
|  |  | |||
|  | @ -101,7 +101,7 @@ class FileUtil | |||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Browses for a file location to save to, then calls `onSelect(path)` when a path chosen. | ||||
|    * Browses for a file location to save to, then calls `onSave(path)` when a path chosen. | ||||
|    * Note that on HTML5 you can't do much with this, you should call `saveFile(resource:haxe.io.Bytes)` instead. | ||||
|    * | ||||
|    * @param typeFilter TODO What does this do? | ||||
|  | @ -183,7 +183,7 @@ class FileUtil | |||
|     var filter:String = convertTypeFilter(typeFilter); | ||||
| 
 | ||||
|     var fileDialog:FileDialog = new FileDialog(); | ||||
|     if (onSave != null) fileDialog.onSelect.add(onSave); | ||||
|     if (onSave != null) fileDialog.onSave.add(onSave); | ||||
|     if (onCancel != null) fileDialog.onCancel.add(onCancel); | ||||
| 
 | ||||
|     fileDialog.save(data, filter, defaultFileName, dialogTitle); | ||||
|  | @ -268,7 +268,8 @@ class FileUtil | |||
|     var zipBytes:Bytes = createZIPFromEntries(resources); | ||||
| 
 | ||||
|     var onSave:String->Void = function(path:String) { | ||||
|       onSave([path]); | ||||
|       trace('Saved ${resources.length} files to ZIP at "$path".'); | ||||
|       if (onSave != null) onSave([path]); | ||||
|     }; | ||||
| 
 | ||||
|     // Prompt the user to save the ZIP file. | ||||
|  | @ -287,7 +288,8 @@ class FileUtil | |||
|     var zipBytes:Bytes = createZIPFromEntries(resources); | ||||
| 
 | ||||
|     var onSave:String->Void = function(path:String) { | ||||
|       onSave([path]); | ||||
|       trace('Saved FNF file to "$path"'); | ||||
|       if (onSave != null) onSave([path]); | ||||
|     }; | ||||
| 
 | ||||
|     // Prompt the user to save the ZIP file. | ||||
|  | @ -302,14 +304,14 @@ class FileUtil | |||
|    * Use `saveFilesAsZIP` instead. | ||||
|    * @param force Whether to force overwrite an existing file. | ||||
|    */ | ||||
|   public static function saveFilesAsZIPToPath(resources:Array<Entry>, path:String, force:Bool = false):Bool | ||||
|   public static function saveFilesAsZIPToPath(resources:Array<Entry>, path:String, mode:FileWriteMode = Skip):Bool | ||||
|   { | ||||
|     #if desktop | ||||
|     // Create a ZIP file. | ||||
|     var zipBytes:Bytes = createZIPFromEntries(resources); | ||||
| 
 | ||||
|     // Write the ZIP. | ||||
|     writeBytesToPath(path, zipBytes, force ? Force : Skip); | ||||
|     writeBytesToPath(path, zipBytes, mode); | ||||
| 
 | ||||
|     return true; | ||||
|     #else | ||||
|  | @ -344,13 +346,22 @@ class FileUtil | |||
|   public static function readBytesFromPath(path:String):Bytes | ||||
|   { | ||||
|     #if sys | ||||
|     if (!sys.FileSystem.exists(path)) return null; | ||||
|     if (!doesFileExist(path)) return null; | ||||
|     return sys.io.File.getBytes(path); | ||||
|     #else | ||||
|     return null; | ||||
|     #end | ||||
|   } | ||||
| 
 | ||||
|   public static function doesFileExist(path:String):Bool | ||||
|   { | ||||
|     #if sys | ||||
|     return sys.FileSystem.exists(path); | ||||
|     #else | ||||
|     return false; | ||||
|     #end | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Browse for a file to read and execute a callback once we have a file reference. | ||||
|    * Works great on HTML5 or desktop. | ||||
|  | @ -434,18 +445,20 @@ class FileUtil | |||
|       case Force: | ||||
|         sys.io.File.saveContent(path, data); | ||||
|       case Skip: | ||||
|         if (!sys.FileSystem.exists(path)) | ||||
|         if (!doesFileExist(path)) | ||||
|         { | ||||
|           sys.io.File.saveContent(path, data); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|           throw 'File already exists: $path'; | ||||
|           // Do nothing. | ||||
|           // throw 'File already exists: $path'; | ||||
|         } | ||||
|       case Ask: | ||||
|         if (sys.FileSystem.exists(path)) | ||||
|         if (doesFileExist(path)) | ||||
|         { | ||||
|           // TODO: We don't have the technology to use native popups yet. | ||||
|           throw 'File already exists: $path'; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|  | @ -475,18 +488,20 @@ class FileUtil | |||
|       case Force: | ||||
|         sys.io.File.saveBytes(path, data); | ||||
|       case Skip: | ||||
|         if (!sys.FileSystem.exists(path)) | ||||
|         if (!doesFileExist(path)) | ||||
|         { | ||||
|           sys.io.File.saveBytes(path, data); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|           throw 'File already exists: $path'; | ||||
|           // Do nothing. | ||||
|           // throw 'File already exists: $path'; | ||||
|         } | ||||
|       case Ask: | ||||
|         if (sys.FileSystem.exists(path)) | ||||
|         if (doesFileExist(path)) | ||||
|         { | ||||
|           // TODO: We don't have the technology to use native popups yet. | ||||
|           throw 'File already exists: $path'; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|  | @ -523,7 +538,7 @@ class FileUtil | |||
|   public static function createDirIfNotExists(dir:String):Void | ||||
|   { | ||||
|     #if sys | ||||
|     if (!sys.FileSystem.exists(dir)) | ||||
|     if (!doesFileExist(dir)) | ||||
|     { | ||||
|       sys.FileSystem.createDirectory(dir); | ||||
|     } | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue