From 9e27095659faa8a458516fc43fd6848653fe891a Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 19 Dec 2023 01:21:26 -0500
Subject: [PATCH 1/9] Update assets.

---
 assets | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/assets b/assets
index c354795f7..6f17eb051 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit c354795f7f560fa096b855c6e6bca745f77fa414
+Subproject commit 6f17eb051e2609d59a591d4e6eb78e37c6e90adb

From 4ff5bd21df738f2b63e7679330afd56a0b53a975 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 19 Dec 2023 01:23:42 -0500
Subject: [PATCH 2/9] Fixes to the FNF Legacy JSON parser

---
 source/funkin/data/DataParse.hx               | 45 +++++++++++++++----
 source/funkin/data/level/LevelRegistry.hx     |  2 +
 .../data/notestyle/NoteStyleRegistry.hx       |  2 +
 source/funkin/data/song/SongData.hx           |  2 +-
 source/funkin/data/song/SongDataUtils.hx      |  1 +
 source/funkin/data/song/SongRegistry.hx       | 14 ++++++
 .../data/song/importer/ChartManifestData.hx   |  1 +
 .../data/song/importer/FNFLegacyData.hx       |  3 +-
 .../data/song/importer/FNFLegacyImporter.hx   | 22 ++++++++-
 .../source/funkin/data/BaseRegistryTest.hx    |  2 +
 10 files changed, 83 insertions(+), 11 deletions(-)

diff --git a/source/funkin/data/DataParse.hx b/source/funkin/data/DataParse.hx
index cbd168a61..49dde0198 100644
--- a/source/funkin/data/DataParse.hx
+++ b/source/funkin/data/DataParse.hx
@@ -178,7 +178,31 @@ class DataParse
     switch (json.value)
     {
       case JObject(fields):
-        return cast Tools.getValue(json);
+        var result:LegacyNoteSection =
+          {
+            mustHitSection: false,
+            sectionNotes: [],
+          };
+        for (field in fields)
+        {
+          switch (field.name)
+          {
+            case 'sectionNotes':
+              result.sectionNotes = legacyNotes(field.value, field.name);
+
+            case 'mustHitSection':
+              result.mustHitSection = Tools.getValue(field.value);
+            case 'typeOfSection':
+              result.typeOfSection = Tools.getValue(field.value);
+            case 'lengthInSteps':
+              result.lengthInSteps = Tools.getValue(field.value);
+            case 'changeBPM':
+              result.changeBPM = Tools.getValue(field.value);
+            case 'bpm':
+              result.bpm = Tools.getValue(field.value);
+          }
+        }
+        return result;
       default:
         throw 'Expected property $name to be an object, but it was ${json.value}.';
     }
@@ -189,7 +213,12 @@ class DataParse
     switch (json.value)
     {
       case JObject(fields):
-        return cast Tools.getValue(json);
+        var result = {};
+        for (field in fields)
+        {
+          Reflect.setField(result, field.name, legacyNoteSectionArray(field.value, field.name));
+        }
+        return result;
       default:
         throw 'Expected property $name to be an object, but it was ${json.value}.';
     }
@@ -211,13 +240,13 @@ class DataParse
     switch (json.value)
     {
       case JArray(values):
-        // var time:Null<Float> = values[0] == null ? null : Tools.getValue(values[0]);
-        // var data:Null<Int> = values[1] == null ? null : Tools.getValue(values[1]);
-        // var length:Null<Float> = values[2] == null ? null : Tools.getValue(values[2]);
-        // var alt:Null<Bool> = values[3] == null ? null : Tools.getValue(values[3]);
+        var time:Null<Float> = values[0] == null ? null : Tools.getValue(values[0]);
+        var data:Null<Int> = values[1] == null ? null : Tools.getValue(values[1]);
+        var length:Null<Float> = values[2] == null ? null : Tools.getValue(values[2]);
+        var alt:Null<Bool> = values[3] == null ? null : Tools.getValue(values[3]);
 
-        // return new LegacyNote(time, data, length, alt);
-        return null;
+        return new LegacyNote(time, data, length, alt);
+      // return null;
       default:
         throw 'Expected property $name to be a note, but it was ${json.value}.';
     }
diff --git a/source/funkin/data/level/LevelRegistry.hx b/source/funkin/data/level/LevelRegistry.hx
index 75b0b11f6..b5c15de0f 100644
--- a/source/funkin/data/level/LevelRegistry.hx
+++ b/source/funkin/data/level/LevelRegistry.hx
@@ -30,6 +30,7 @@ class LevelRegistry extends BaseRegistry<Level, LevelData>
     // JsonParser does not take type parameters,
     // otherwise this function would be in BaseRegistry.
     var parser = new json2object.JsonParser<LevelData>();
+    parser.ignoreUnknownVariables = false;
 
     switch (loadEntryFile(id))
     {
@@ -57,6 +58,7 @@ class LevelRegistry extends BaseRegistry<Level, LevelData>
   public function parseEntryDataRaw(contents:String, ?fileName:String):Null<LevelData>
   {
     var parser = new json2object.JsonParser<LevelData>();
+    parser.ignoreUnknownVariables = false;
     parser.fromJson(contents, fileName);
 
     if (parser.errors.length > 0)
diff --git a/source/funkin/data/notestyle/NoteStyleRegistry.hx b/source/funkin/data/notestyle/NoteStyleRegistry.hx
index 4255a644b..ffb9bf490 100644
--- a/source/funkin/data/notestyle/NoteStyleRegistry.hx
+++ b/source/funkin/data/notestyle/NoteStyleRegistry.hx
@@ -35,6 +35,7 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
     // JsonParser does not take type parameters,
     // otherwise this function would be in BaseRegistry.
     var parser = new json2object.JsonParser<NoteStyleData>();
+    parser.ignoreUnknownVariables = false;
 
     switch (loadEntryFile(id))
     {
@@ -62,6 +63,7 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
   public function parseEntryDataRaw(contents:String, ?fileName:String):Null<NoteStyleData>
   {
     var parser = new json2object.JsonParser<NoteStyleData>();
+    parser.ignoreUnknownVariables = false;
     parser.fromJson(contents, fileName);
 
     if (parser.errors.length > 0)
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index 7886ada4f..600871e2f 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -747,7 +747,7 @@ class SongNoteDataRaw
   /**
    * The kind of the note.
    * This can allow the note to include information used for custom behavior.
-   * Defaults to blank or `"normal"`.
+   * Defaults to blank or `Constants.DEFAULT_DIFFICULTY`.
    */
   @:alias("k")
   @:default("normal")
diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx
index 4ae4b1426..309676884 100644
--- a/source/funkin/data/song/SongDataUtils.hx
+++ b/source/funkin/data/song/SongDataUtils.hx
@@ -230,6 +230,7 @@ class SongDataUtils
     trace('Read ${notesString.length} characters from clipboard.');
 
     var parser = new json2object.JsonParser<SongClipboardItems>();
+    parser.ignoreUnknownVariables = false;
     parser.fromJson(notesString, 'clipboard');
     if (parser.errors.length > 0)
     {
diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx
index 850654eb7..5a0835f57 100644
--- a/source/funkin/data/song/SongRegistry.hx
+++ b/source/funkin/data/song/SongRegistry.hx
@@ -126,6 +126,8 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
     variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
 
     var parser = new json2object.JsonParser<SongMetadata>();
+    parser.ignoreUnknownVariables = false;
+
     switch (loadEntryMetadataFile(id, variation))
     {
       case {fileName: fileName, contents: contents}:
@@ -147,6 +149,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
     variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
 
     var parser = new json2object.JsonParser<SongMetadata>();
+    parser.ignoreUnknownVariables = false;
     parser.fromJson(contents, fileName);
 
     if (parser.errors.length > 0)
@@ -206,6 +209,8 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
     variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
 
     var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
+    parser.ignoreUnknownVariables = false;
+
     switch (loadEntryMetadataFile(id, variation))
     {
       case {fileName: fileName, contents: contents}:
@@ -226,6 +231,8 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
     variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
 
     var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
+    parser.ignoreUnknownVariables = false;
+
     switch (loadEntryMetadataFile(id, variation))
     {
       case {fileName: fileName, contents: contents}:
@@ -244,6 +251,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
   function parseEntryMetadataRaw_v2_1_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
   {
     var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
+    parser.ignoreUnknownVariables = false;
     parser.fromJson(contents, fileName);
 
     if (parser.errors.length > 0)
@@ -257,6 +265,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
   function parseEntryMetadataRaw_v2_0_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
   {
     var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
+    parser.ignoreUnknownVariables = false;
     parser.fromJson(contents, fileName);
 
     if (parser.errors.length > 0)
@@ -272,6 +281,8 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
     variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
 
     var parser = new json2object.JsonParser<SongMusicData>();
+    parser.ignoreUnknownVariables = false;
+
     switch (loadMusicDataFile(id, variation))
     {
       case {fileName: fileName, contents: contents}:
@@ -291,6 +302,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
   public function parseMusicDataRaw(contents:String, ?fileName:String = 'raw'):Null<SongMusicData>
   {
     var parser = new json2object.JsonParser<SongMusicData>();
+    parser.ignoreUnknownVariables = false;
     parser.fromJson(contents, fileName);
 
     if (parser.errors.length > 0)
@@ -334,6 +346,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
     variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
 
     var parser = new json2object.JsonParser<SongChartData>();
+    parser.ignoreUnknownVariables = false;
 
     switch (loadEntryChartFile(id, variation))
     {
@@ -356,6 +369,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
     variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
 
     var parser = new json2object.JsonParser<SongChartData>();
+    parser.ignoreUnknownVariables = false;
     parser.fromJson(contents, fileName);
 
     if (parser.errors.length > 0)
diff --git a/source/funkin/data/song/importer/ChartManifestData.hx b/source/funkin/data/song/importer/ChartManifestData.hx
index 0c7d2f0b0..dd0d28479 100644
--- a/source/funkin/data/song/importer/ChartManifestData.hx
+++ b/source/funkin/data/song/importer/ChartManifestData.hx
@@ -68,6 +68,7 @@ class ChartManifestData
   public static function deserialize(contents:String):Null<ChartManifestData>
   {
     var parser = new json2object.JsonParser<ChartManifestData>();
+    parser.ignoreUnknownVariables = false;
     parser.fromJson(contents, 'manifest.json');
 
     if (parser.errors.length > 0)
diff --git a/source/funkin/data/song/importer/FNFLegacyData.hx b/source/funkin/data/song/importer/FNFLegacyData.hx
index 5b75368c9..52380d344 100644
--- a/source/funkin/data/song/importer/FNFLegacyData.hx
+++ b/source/funkin/data/song/importer/FNFLegacyData.hx
@@ -19,7 +19,8 @@ class LegacySongData
 
   @:jcustomparse(funkin.data.DataParse.eitherLegacyScrollSpeeds)
   public var speed:Either<Float, LegacyScrollSpeeds>;
-  public var stageDefault:String;
+  @:optional
+  public var stageDefault:Null<String>;
   public var bpm:Float;
 
   @:jcustomparse(funkin.data.DataParse.eitherLegacyNoteData)
diff --git a/source/funkin/data/song/importer/FNFLegacyImporter.hx b/source/funkin/data/song/importer/FNFLegacyImporter.hx
index ee68513dc..ab2abda8e 100644
--- a/source/funkin/data/song/importer/FNFLegacyImporter.hx
+++ b/source/funkin/data/song/importer/FNFLegacyImporter.hx
@@ -14,6 +14,7 @@ class FNFLegacyImporter
   public static function parseLegacyDataRaw(input:String, fileName:String = 'raw'):FNFLegacyData
   {
     var parser = new json2object.JsonParser<FNFLegacyData>();
+    parser.ignoreUnknownVariables = true; // Set to true to ignore extra variables that might be included in the JSON.
     parser.fromJson(input, fileName);
 
     if (parser.errors.length > 0)
@@ -185,15 +186,34 @@ class FNFLegacyImporter
     return result;
   }
 
+  static final STRUMLINE_SIZE = 4;
+
   static function migrateNoteSections(input:Array<LegacyNoteSection>):Array<SongNoteData>
   {
     var result:Array<SongNoteData> = [];
 
     for (section in input)
     {
+      var mustHitSection = section.mustHitSection ?? false;
       for (note in section.sectionNotes)
       {
-        result.push(new SongNoteData(note.time, note.data, note.length, note.getKind()));
+        // Handle the dumb logic for mustHitSection.
+        var noteData = note.data;
+
+        // Flip notes if mustHitSection is FALSE (not true lol).
+        if (!mustHitSection)
+        {
+          if (noteData >= STRUMLINE_SIZE)
+          {
+            noteData -= STRUMLINE_SIZE;
+          }
+          else
+          {
+            noteData += STRUMLINE_SIZE;
+          }
+        }
+
+        result.push(new SongNoteData(note.time, noteData, note.length, note.getKind()));
       }
     }
 
diff --git a/tests/unit/source/funkin/data/BaseRegistryTest.hx b/tests/unit/source/funkin/data/BaseRegistryTest.hx
index 0be932d35..5f837ba97 100644
--- a/tests/unit/source/funkin/data/BaseRegistryTest.hx
+++ b/tests/unit/source/funkin/data/BaseRegistryTest.hx
@@ -156,6 +156,7 @@ class MyTypeRegistry extends BaseRegistry<MyType, MyTypeData>
     // JsonParser does not take type parameters,
     // otherwise this function would be in BaseRegistry.
     var parser = new json2object.JsonParser<MyTypeData>();
+    parser.ignoreUnknownVariables = false;
 
     switch (loadEntryFile(id))
     {
@@ -181,6 +182,7 @@ class MyTypeRegistry extends BaseRegistry<MyType, MyTypeData>
     // JsonParser does not take type parameters,
     // otherwise this function would be in BaseRegistry.
     var parser = new json2object.JsonParser<MyTypeData_v0_1_x>();
+    parser.ignoreUnknownVariables = false;
 
     switch (loadEntryFile(id))
     {

From 328bb7b938d65406926cf268a2b47fd78fc60870 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 19 Dec 2023 01:25:15 -0500
Subject: [PATCH 3/9] Fixes to Erect/Nightmare only

---
 source/funkin/input/Cursor.hx                        | 12 ++++++++++++
 source/funkin/play/stage/StageData.hx                |  1 +
 source/funkin/ui/debug/charting/ChartEditorState.hx  |  6 ++++--
 .../charting/handlers/ChartEditorDialogHandler.hx    |  3 +++
 4 files changed, 20 insertions(+), 2 deletions(-)

diff --git a/source/funkin/input/Cursor.hx b/source/funkin/input/Cursor.hx
index b4bf43808..39f399465 100644
--- a/source/funkin/input/Cursor.hx
+++ b/source/funkin/input/Cursor.hx
@@ -34,6 +34,18 @@ class Cursor
     Cursor.cursorMode = null;
   }
 
+  public static inline function toggle():Void
+  {
+    if (FlxG.mouse.visible)
+    {
+      hide();
+    }
+    else
+    {
+      show();
+    }
+  }
+
   public static final CURSOR_DEFAULT_PARAMS:CursorParams =
     {
       graphic: "assets/images/cursor/cursor-default.png",
diff --git a/source/funkin/play/stage/StageData.hx b/source/funkin/play/stage/StageData.hx
index d89995ef3..2d87dec31 100644
--- a/source/funkin/play/stage/StageData.hx
+++ b/source/funkin/play/stage/StageData.hx
@@ -164,6 +164,7 @@ class StageDataParser
     try
     {
       var parser = new json2object.JsonParser<StageData>();
+      parser.ignoreUnknownVariables = false;
       parser.fromJson(rawJson, '$stageId.json');
 
       if (parser.errors.length > 0)
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index fa55750bf..afb0a114d 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1010,7 +1010,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
   function get_availableDifficulties():Array<String>
   {
     var m:Null<SongMetadata> = songMetadata.get(selectedVariation);
-    return m?.playData?.difficulties ?? [];
+    return m?.playData?.difficulties ?? [Constants.DEFAULT_DIFFICULTY];
   }
 
   /**
@@ -1069,7 +1069,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     var result:Null<SongChartData> = songChartData.get(selectedVariation);
     if (result == null)
     {
-      result = new SongChartData(["normal" => 1.0], [], ["normal" => []]);
+      result = new SongChartData([Constants.DEFAULT_DIFFICULTY => 1.0], [], [Constants.DEFAULT_DIFFICULTY => []]);
       songChartData.set(selectedVariation, result);
     }
     return result;
@@ -1293,6 +1293,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
   function set_selectedDifficulty(value:String):String
   {
+    if (value == null) value = availableDifficulties[0] ?? Constants.DEFAULT_DIFFICULTY;
+
     selectedDifficulty = value;
 
     // Make sure view is updated when the difficulty changes.
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
index 666b3656c..a595b8195 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
@@ -687,6 +687,9 @@ class ChartEditorDialogHandler
       Conductor.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata.
       Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
 
+      state.selectedVariation = Constants.DEFAULT_VARIATION;
+      state.selectedDifficulty = state.availableDifficulties[0];
+
       state.difficultySelectDirty = true;
 
       dialog.hideDialog(DialogButton.APPLY);

From 3e65e7ecc59970d6060bd6f54f6097395d16f24a Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 19 Dec 2023 01:26:26 -0500
Subject: [PATCH 4/9] Fix for vocals not loading properly and not getting
 cleared properly.

---
 .../ui/debug/charting/ChartEditorState.hx     | 23 +++++++++++++---
 .../handlers/ChartEditorAudioHandler.hx       | 27 ++++++++++---------
 .../handlers/ChartEditorDialogHandler.hx      | 14 +++-------
 3 files changed, 38 insertions(+), 26 deletions(-)

diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index afb0a114d..8369e95b9 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -2548,8 +2548,25 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
     menubarItemPlayPause.onClick = _ -> toggleAudioPlayback();
 
-    menubarItemLoadInstrumental.onClick = _ -> this.openUploadInstDialog(true);
-    menubarItemLoadVocals.onClick = _ -> this.openUploadVocalsDialog(true);
+    menubarItemLoadInstrumental.onClick = _ -> {
+      var dialog = this.openUploadInstDialog(true);
+      // Ensure instrumental and vocals are reloaded properly.
+      dialog.onDialogClosed = function(_) {
+        this.isHaxeUIDialogOpen = false;
+        this.switchToCurrentInstrumental();
+        this.postLoadInstrumental();
+      }
+    };
+
+    menubarItemLoadVocals.onClick = _ -> {
+      var dialog = this.openUploadVocalsDialog(true);
+      // Ensure instrumental and vocals are reloaded properly.
+      dialog.onDialogClosed = function(_) {
+        this.isHaxeUIDialogOpen = false;
+        this.switchToCurrentInstrumental();
+        this.postLoadInstrumental();
+      }
+    };
 
     menubarItemVolumeMetronome.onChange = event -> {
       var volume:Float = event.value.toFloat() / 100.0;
@@ -4047,7 +4064,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
           }
           else
           {
-            // If we clicked and released outside the grid, do nothing.
+            // If we clicked and released outside the grid (or on HaxeUI), do nothing.
           }
         }
 
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
index 272291a94..990ab41ae 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
@@ -28,11 +28,11 @@ class ChartEditorAudioHandler
    * @param instId The instrumental this vocal track will be for.
    * @return Success or failure.
    */
-  public static function loadVocalsFromPath(state:ChartEditorState, path:Path, charId:String, instId:String = ''):Bool
+  public static function loadVocalsFromPath(state:ChartEditorState, path:Path, charId:String, instId:String = '', wipeFirst:Bool = false):Bool
   {
     #if sys
     var fileBytes:Bytes = sys.io.File.getBytes(path.toString());
-    return loadVocalsFromBytes(state, fileBytes, charId, instId);
+    return loadVocalsFromBytes(state, fileBytes, charId, instId, wipeFirst);
     #else
     trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
     return false;
@@ -47,12 +47,12 @@ class ChartEditorAudioHandler
    * @param instId The instrumental this vocal track will be for.
    * @return Success or failure.
    */
-  public static function loadVocalsFromAsset(state:ChartEditorState, path:String, charId:String, instId:String = ''):Bool
+  public static function loadVocalsFromAsset(state:ChartEditorState, path:String, charId:String, instId:String = '', wipeFirst:Bool = false):Bool
   {
     var trackData:Null<Bytes> = Assets.getBytes(path);
     if (trackData != null)
     {
-      return loadVocalsFromBytes(state, trackData, charId, instId);
+      return loadVocalsFromBytes(state, trackData, charId, instId, wipeFirst);
     }
     return false;
   }
@@ -63,10 +63,12 @@ class ChartEditorAudioHandler
    * @param bytes The audio byte data.
    * @param charId The character this vocal track will be for.
    * @param instId The instrumental this vocal track will be for.
+   * @param wipeFirst Whether to wipe the existing vocal data before loading.
    */
-  public static function loadVocalsFromBytes(state:ChartEditorState, bytes:Bytes, charId:String, instId:String = ''):Bool
+  public static function loadVocalsFromBytes(state:ChartEditorState, bytes:Bytes, charId:String, instId:String = '', wipeFirst:Bool = false):Bool
   {
     var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
+    if (wipeFirst) wipeVocalData(state);
     state.audioVocalTrackData.set(trackId, bytes);
     return true;
   }
@@ -78,11 +80,11 @@ class ChartEditorAudioHandler
    * @param instId The instrumental this vocal track will be for.
    * @return Success or failure.
    */
-  public static function loadInstFromPath(state:ChartEditorState, path:Path, instId:String = ''):Bool
+  public static function loadInstFromPath(state:ChartEditorState, path:Path, instId:String = '', wipeFirst:Bool = false):Bool
   {
     #if sys
     var fileBytes:Bytes = sys.io.File.getBytes(path.toString());
-    return loadInstFromBytes(state, fileBytes, instId);
+    return loadInstFromBytes(state, fileBytes, instId, wipeFirst);
     #else
     trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
     return false;
@@ -96,12 +98,12 @@ class ChartEditorAudioHandler
    * @param instId The instrumental this vocal track will be for.
    * @return Success or failure.
    */
-  public static function loadInstFromAsset(state:ChartEditorState, path:String, instId:String = ''):Bool
+  public static function loadInstFromAsset(state:ChartEditorState, path:String, instId:String = '', wipeFirst:Bool = false):Bool
   {
     var trackData:Null<Bytes> = Assets.getBytes(path);
     if (trackData != null)
     {
-      return loadInstFromBytes(state, trackData, instId);
+      return loadInstFromBytes(state, trackData, instId, wipeFirst);
     }
     return false;
   }
@@ -113,9 +115,10 @@ class ChartEditorAudioHandler
    * @param charId The character this vocal track will be for.
    * @param instId The instrumental this vocal track will be for.
    */
-  public static function loadInstFromBytes(state:ChartEditorState, bytes:Bytes, instId:String = ''):Bool
+  public static function loadInstFromBytes(state:ChartEditorState, bytes:Bytes, instId:String = '', wipeFirst:Bool = false):Bool
   {
     if (instId == '') instId = 'default';
+    if (wipeFirst) wipeInstrumentalData(state);
     state.audioInstTrackData.set(instId, bytes);
     return true;
   }
@@ -127,9 +130,9 @@ class ChartEditorAudioHandler
 
     stopExistingVocals(state);
     result = playVocals(state, BF, playerId, instId);
-    if (!result) return false;
+    // if (!result) return false;
     result = playVocals(state, DAD, opponentId, instId);
-    if (!result) return false;
+    // if (!result) return false;
 
     return true;
   }
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
index a595b8195..2ede1a39f 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
@@ -758,14 +758,9 @@ class ChartEditorDialogHandler
         trace('Selected file: $pathStr');
         var path:Path = new Path(pathStr);
 
-        if (!hasClearedVocals)
+        if (state.loadVocalsFromPath(path, charKey, instId, !hasClearedVocals))
         {
           hasClearedVocals = true;
-          state.stopExistingVocals();
-        }
-
-        if (state.loadVocalsFromPath(path, charKey, instId))
-        {
           // Tell the user the load was successful.
           state.success('Loaded Vocals', 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${state.selectedVariation}');
           #if FILE_DROP_SUPPORTED
@@ -799,13 +794,10 @@ class ChartEditorDialogHandler
             if (selectedFile != null && selectedFile.bytes != null)
             {
               trace('Selected file: ' + selectedFile.name);
-              if (!hasClearedVocals)
+
+              if (state.loadVocalsFromBytes(selectedFile.bytes, charKey, instId, !hasClearedVocals))
               {
                 hasClearedVocals = true;
-                state.stopExistingVocals();
-              }
-              if (state.loadVocalsFromBytes(selectedFile.bytes, charKey, instId))
-              {
                 // Tell the user the load was successful.
                 state.success('Loaded Vocals', 'Loaded vocals for $charName (${selectedFile.name}), variation ${state.selectedVariation}');
 

From bb0fb0281372c5843995a19cda7a9bba5545783f Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 19 Dec 2023 01:26:43 -0500
Subject: [PATCH 5/9] Fix to launching directly into Chart Editor

---
 source/funkin/Conductor.hx                         | 14 +++++++-------
 .../funkin/ui/debug/charting/ChartEditorState.hx   |  2 +-
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx
index c531678ad..7b34bffe2 100644
--- a/source/funkin/Conductor.hx
+++ b/source/funkin/Conductor.hx
@@ -37,7 +37,7 @@ class Conductor
   /**
    * The most recent time change for the current song position.
    */
-  public static var currentTimeChange(default, null):SongTimeChange;
+  public static var currentTimeChange(default, null):Null<SongTimeChange>;
 
   /**
    * The current position in the song in milliseconds.
@@ -132,32 +132,32 @@ class Conductor
   /**
    * Current position in the song, in measures.
    */
-  public static var currentMeasure(default, null):Int;
+  public static var currentMeasure(default, null):Int = 0;
 
   /**
    * Current position in the song, in beats.
    */
-  public static var currentBeat(default, null):Int;
+  public static var currentBeat(default, null):Int = 0;
 
   /**
    * Current position in the song, in steps.
    */
-  public static var currentStep(default, null):Int;
+  public static var currentStep(default, null):Int = 0;
 
   /**
    * Current position in the song, in measures and fractions of a measure.
    */
-  public static var currentMeasureTime(default, null):Float;
+  public static var currentMeasureTime(default, null):Float = 0;
 
   /**
    * Current position in the song, in beats and fractions of a measure.
    */
-  public static var currentBeatTime(default, null):Float;
+  public static var currentBeatTime(default, null):Float = 0;
 
   /**
    * Current position in the song, in steps and fractions of a step.
    */
-  public static var currentStepTime(default, null):Float;
+  public static var currentStepTime(default, null):Float = 0;
 
   /**
    * An offset tied to the current chart file to compensate for a delay in the instrumental.
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 8369e95b9..de38a8fda 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -4387,7 +4387,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
     playbarNoteSnap.text = '1/${noteSnapQuant}';
     playbarDifficulty.text = "Difficulty: " + selectedDifficulty.toTitleCase();
-    playbarBPM.text = "BPM: " + Conductor.currentTimeChange.bpm;
+    playbarBPM.text = "BPM: " + (Conductor.currentTimeChange?.bpm ?? 0.0);
   }
 
   function handlePlayhead():Void

From ad02bf2ee0c4d0bc8437069ada3d19fd0ab156dc Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 19 Dec 2023 01:27:58 -0500
Subject: [PATCH 6/9] Fix to GameOverSubstate exiting to Freeplay instead of
 Chart Editor

---
 source/funkin/play/GameOverSubState.hx     | 24 ++++++++++++++++++++--
 source/funkin/play/PlayState.hx            |  5 ++++-
 source/funkin/ui/freeplay/FreeplayState.hx |  2 +-
 source/funkin/ui/story/StoryMenuState.hx   |  2 +-
 source/funkin/util/Constants.hx            |  1 +
 5 files changed, 29 insertions(+), 5 deletions(-)

diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx
index 6eb53e2d5..18e3e0280 100644
--- a/source/funkin/play/GameOverSubState.hx
+++ b/source/funkin/play/GameOverSubState.hx
@@ -64,9 +64,13 @@ class GameOverSubState extends MusicBeatSubState
    */
   var isEnding:Bool = false;
 
-  public function new()
+  var isChartingMode:Bool = false;
+
+  public function new(?params:GameOverParams)
   {
     super();
+
+    this.isChartingMode = params?.isChartingMode ?? false;
   }
 
   /**
@@ -176,9 +180,20 @@ class GameOverSubState extends MusicBeatSubState
       // PlayState.seenCutscene = false; // old thing...
       gameOverMusic.stop();
 
-      if (PlayStatePlaylist.isStoryMode) FlxG.switchState(new StoryMenuState());
+      if (isChartingMode)
+      {
+        this.close();
+        if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
+        PlayState.instance.close(); // This only works because PlayState is a substate!
+      }
+      else if (PlayStatePlaylist.isStoryMode)
+      {
+        FlxG.switchState(new StoryMenuState());
+      }
       else
+      {
         FlxG.switchState(new FreeplayState());
+      }
     }
 
     if (gameOverMusic.playing)
@@ -307,3 +322,8 @@ class GameOverSubState extends MusicBeatSubState
     });
   }
 }
+
+typedef GameOverParams =
+{
+  var isChartingMode:Bool;
+}
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index e0932e756..c834e0abe 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -922,7 +922,10 @@ class PlayState extends MusicBeatSubState
         }
         #end
 
-        var gameOverSubState = new GameOverSubState();
+        var gameOverSubState = new GameOverSubState(
+          {
+            isChartingMode: isChartingMode
+          });
         FlxTransitionableSubState.skipNextTransIn = true;
         FlxTransitionableSubState.skipNextTransOut = true;
         openSubState(gameOverSubState);
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 7c69804d9..f17c3d91e 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -97,7 +97,7 @@ class FreeplayState extends MusicBeatSubState
   var stickerSubState:StickerSubState;
 
   //
-  static var rememberedDifficulty:Null<String> = "normal";
+  static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY;
   static var rememberedSongId:Null<String> = null;
 
   public function new(?stickers:StickerSubState = null)
diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index 456988873..6e4cdacaf 100644
--- a/source/funkin/ui/story/StoryMenuState.hx
+++ b/source/funkin/ui/story/StoryMenuState.hx
@@ -106,7 +106,7 @@ class StoryMenuState extends MusicBeatState
   var stickerSubState:StickerSubState;
 
   static var rememberedLevelId:Null<String> = null;
-  static var rememberedDifficulty:Null<String> = "normal";
+  static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY;
 
   public function new(?stickers:StickerSubState = null)
   {
diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx
index f8749567b..123267a49 100644
--- a/source/funkin/util/Constants.hx
+++ b/source/funkin/util/Constants.hx
@@ -123,6 +123,7 @@ class Constants
 
   /**
    * Default list of difficulties for charts.
+   * Assumes no Erect mode, etc.
    */
   public static final DEFAULT_DIFFICULTY_LIST:Array<String> = ['easy', 'normal', 'hard'];
 

From 70b7de94aa672bd607583c5763bf6e2d8921369c Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 19 Dec 2023 01:28:13 -0500
Subject: [PATCH 7/9] Fix to Metadata toolbox causing crash when no GF :(

---
 .../toolboxes/ChartEditorMetadataToolbox.hx   | 42 +++++++++++++++----
 1 file changed, 33 insertions(+), 9 deletions(-)

diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
index 509aa5b07..bc9384cf3 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
@@ -183,17 +183,41 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
 
     var LIMIT = 6;
 
-    var charDataOpponent:CharacterData = CharacterDataParser.fetchCharacterData(chartEditorState.currentSongMetadata.playData.characters.opponent);
-    buttonCharacterOpponent.icon = CharacterDataParser.getCharPixelIconAsset(chartEditorState.currentSongMetadata.playData.characters.opponent);
-    buttonCharacterOpponent.text = charDataOpponent.name.length > LIMIT ? '${charDataOpponent.name.substr(0, LIMIT)}.' : '${charDataOpponent.name}';
+    var charDataOpponent:Null<CharacterData> = CharacterDataParser.fetchCharacterData(chartEditorState.currentSongMetadata.playData.characters.opponent);
+    if (charDataOpponent != null)
+    {
+      buttonCharacterOpponent.icon = CharacterDataParser.getCharPixelIconAsset(chartEditorState.currentSongMetadata.playData.characters.opponent);
+      buttonCharacterOpponent.text = charDataOpponent.name.length > LIMIT ? '${charDataOpponent.name.substr(0, LIMIT)}.' : '${charDataOpponent.name}';
+    }
+    else
+    {
+      buttonCharacterOpponent.icon = null;
+      buttonCharacterOpponent.text = "None";
+    }
 
-    var charDataGirlfriend:CharacterData = CharacterDataParser.fetchCharacterData(chartEditorState.currentSongMetadata.playData.characters.girlfriend);
-    buttonCharacterGirlfriend.icon = CharacterDataParser.getCharPixelIconAsset(chartEditorState.currentSongMetadata.playData.characters.girlfriend);
-    buttonCharacterGirlfriend.text = charDataGirlfriend.name.length > LIMIT ? '${charDataGirlfriend.name.substr(0, LIMIT)}.' : '${charDataGirlfriend.name}';
+    var charDataGirlfriend:Null<CharacterData> = CharacterDataParser.fetchCharacterData(chartEditorState.currentSongMetadata.playData.characters.girlfriend);
+    if (charDataGirlfriend != null)
+    {
+      buttonCharacterGirlfriend.icon = CharacterDataParser.getCharPixelIconAsset(chartEditorState.currentSongMetadata.playData.characters.girlfriend);
+      buttonCharacterGirlfriend.text = charDataGirlfriend.name.length > LIMIT ? '${charDataGirlfriend.name.substr(0, LIMIT)}.' : '${charDataGirlfriend.name}';
+    }
+    else
+    {
+      buttonCharacterGirlfriend.icon = null;
+      buttonCharacterGirlfriend.text = "None";
+    }
 
-    var charDataPlayer:CharacterData = CharacterDataParser.fetchCharacterData(chartEditorState.currentSongMetadata.playData.characters.player);
-    buttonCharacterPlayer.icon = CharacterDataParser.getCharPixelIconAsset(chartEditorState.currentSongMetadata.playData.characters.player);
-    buttonCharacterPlayer.text = charDataPlayer.name.length > LIMIT ? '${charDataPlayer.name.substr(0, LIMIT)}.' : '${charDataPlayer.name}';
+    var charDataPlayer:Null<CharacterData> = CharacterDataParser.fetchCharacterData(chartEditorState.currentSongMetadata.playData.characters.player);
+    if (charDataPlayer != null)
+    {
+      buttonCharacterPlayer.icon = CharacterDataParser.getCharPixelIconAsset(chartEditorState.currentSongMetadata.playData.characters.player);
+      buttonCharacterPlayer.text = charDataPlayer.name.length > LIMIT ? '${charDataPlayer.name.substr(0, LIMIT)}.' : '${charDataPlayer.name}';
+    }
+    else
+    {
+      buttonCharacterPlayer.icon = null;
+      buttonCharacterPlayer.text = "None";
+    }
   }
 
   public static function build(chartEditorState:ChartEditorState):ChartEditorMetadataToolbox

From f01535e43165e9e0f833d6b9c283480af5c26e9d Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 19 Dec 2023 01:28:24 -0500
Subject: [PATCH 8/9] Don't forget to update hmm!

---
 hmm.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/hmm.json b/hmm.json
index 96ee75bc1..8a903eed6 100644
--- a/hmm.json
+++ b/hmm.json
@@ -100,7 +100,7 @@
       "name": "json2object",
       "type": "git",
       "dir": null,
-      "ref": "a0a78b60c41e47bae8bfa422488a199a58b4474e",
+      "ref": "a8c26f18463c98da32f744c214fe02273e1823fa",
       "url": "https://github.com/FunkinCrew/json2object"
     },
     {

From 92e0022fedcba609bb1d5320c2cffc6d70708676 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Mon, 1 Jan 2024 19:13:51 -0500
Subject: [PATCH 9/9] derp comma!

---
 source/funkin/play/PlayState.hx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index ea38d833f..3dcabf953 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -924,8 +924,8 @@ class PlayState extends MusicBeatSubState
 
         var gameOverSubState = new GameOverSubState(
           {
-            isChartingMode: isChartingMode
-            transparent: persistentDraw,
+            isChartingMode: isChartingMode,
+            transparent: persistentDraw
           });
         FlxTransitionableSubState.skipNextTransIn = true;
         FlxTransitionableSubState.skipNextTransOut = true;