From 68c85d9214bc8e6d36bdb7af019855618a78b356 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Thu, 10 Aug 2023 15:34:40 -0400
Subject: [PATCH 1/5] Made the currently-unused Divisions field optional.

---
 source/funkin/play/song/Song.hx          | 2 +-
 source/funkin/play/song/SongData.hx      | 4 ++--
 source/funkin/play/song/SongValidator.hx | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 398c28753..ec89d8706 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -297,7 +297,7 @@ class SongDifficulty
   public var songName:String = SongValidator.DEFAULT_SONGNAME;
   public var songArtist:String = SongValidator.DEFAULT_ARTIST;
   public var timeFormat:SongTimeFormat = SongValidator.DEFAULT_TIMEFORMAT;
-  public var divisions:Int = SongValidator.DEFAULT_DIVISIONS;
+  public var divisions:Null<Int> = SongValidator.DEFAULT_DIVISIONS;
   public var looped:Bool = SongValidator.DEFAULT_LOOPED;
   public var generatedBy:String = SongValidator.DEFAULT_GENERATEDBY;
 
diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx
index 6f2475cf9..fbd7e3383 100644
--- a/source/funkin/play/song/SongData.hx
+++ b/source/funkin/play/song/SongData.hx
@@ -269,7 +269,7 @@ typedef RawSongMetadata =
   var songName:String;
   var artist:String;
   var timeFormat:SongTimeFormat;
-  var divisions:Int;
+  var divisions:Null<Int>; // Optional field
   var timeChanges:Array<SongTimeChange>;
   var looped:Bool;
   var playData:SongPlayData;
@@ -292,7 +292,7 @@ abstract SongMetadata(RawSongMetadata)
         songName: songName,
         artist: artist,
         timeFormat: 'ms',
-        divisions: 96,
+        divisions: null,
         timeChanges: [new SongTimeChange(-1, 0, 100, 4, 4, [4, 4, 4, 4])],
         looped: false,
         playData:
diff --git a/source/funkin/play/song/SongValidator.hx b/source/funkin/play/song/SongValidator.hx
index 936ad46f7..d91dda1d9 100644
--- a/source/funkin/play/song/SongValidator.hx
+++ b/source/funkin/play/song/SongValidator.hx
@@ -15,7 +15,7 @@ class SongValidator
   public static final DEFAULT_SONGNAME:String = "Unknown";
   public static final DEFAULT_ARTIST:String = "Unknown";
   public static final DEFAULT_TIMEFORMAT:SongTimeFormat = SongTimeFormat.MILLISECONDS;
-  public static final DEFAULT_DIVISIONS:Int = -1;
+  public static final DEFAULT_DIVISIONS:Null<Int> = null;
   public static final DEFAULT_LOOPED:Bool = false;
   public static final DEFAULT_STAGE:String = "mainStage";
   public static final DEFAULT_SCROLLSPEED:Float = 1.0;

From c6a1f5ffeac51686b23b4764edeb569a173e910e Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 11 Aug 2023 14:00:38 -0400
Subject: [PATCH 2/5] Work on fixing issues with difficulty/variation handling
 in charts

---
 source/funkin/MainMenuState.hx                |  2 +-
 source/funkin/play/song/SongData.hx           | 17 ++++-----
 .../charting/ChartEditorDialogHandler.hx      |  7 +++-
 .../ui/debug/charting/ChartEditorState.hx     | 10 ++++--
 .../charting/ChartEditorToolboxHandler.hx     |  6 +++-
 source/funkin/util/SerializerUtil.hx          | 11 +++++-
 source/funkin/util/SortUtil.hx                | 35 ++++++++++++++++++-
 7 files changed, 70 insertions(+), 18 deletions(-)

diff --git a/source/funkin/MainMenuState.hx b/source/funkin/MainMenuState.hx
index fc493ef4b..5a69ea83a 100644
--- a/source/funkin/MainMenuState.hx
+++ b/source/funkin/MainMenuState.hx
@@ -54,7 +54,7 @@ class MainMenuState extends MusicBeatState
     transIn = FlxTransitionableState.defaultTransIn;
     transOut = FlxTransitionableState.defaultTransOut;
 
-    if (!FlxG.sound.music.playing)
+    if (!(FlxG?.sound?.music?.playing ?? false))
     {
       FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
     }
diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx
index fbd7e3383..938ee0708 100644
--- a/source/funkin/play/song/SongData.hx
+++ b/source/funkin/play/song/SongData.hx
@@ -11,6 +11,7 @@ import haxe.DynamicAccess;
 import haxe.Json;
 import openfl.utils.Assets;
 import thx.semver.Version;
+import funkin.util.SerializerUtil;
 
 /**
  * Contains utilities for loading and parsing stage data.
@@ -138,13 +139,8 @@ class SongDataParser
   {
     var result:Array<SongMetadata> = [];
 
-    var rawJson:String = loadSongMetadataFile(songId);
-    var jsonData:Dynamic = null;
-    try
-    {
-      jsonData = Json.parse(rawJson);
-    }
-    catch (e) {}
+    var jsonStr:String = loadSongMetadataFile(songId);
+    var jsonData:Dynamic = SerializerUtil.fromJSON(jsonStr);
 
     var songMetadata:SongMetadata = SongMigrator.migrateSongMetadata(jsonData, songId);
     songMetadata = SongValidator.validateSongMetadata(songMetadata, songId);
@@ -160,9 +156,10 @@ class SongDataParser
 
     for (variation in variations)
     {
-      var variationRawJson:String = loadSongMetadataFile(songId, variation);
-      var variationSongMetadata:SongMetadata = SongMigrator.migrateSongMetadata(variationRawJson, '${songId}_${variation}');
-      variationSongMetadata = SongValidator.validateSongMetadata(variationSongMetadata, '${songId}_${variation}');
+      var variationJsonStr:String = loadSongMetadataFile(songId, variation);
+      var variationJsonData:Dynamic = SerializerUtil.fromJSON(variationJsonStr);
+      var variationSongMetadata:SongMetadata = SongMigrator.migrateSongMetadata(variationJsonData, '${songId}:${variation}');
+      variationSongMetadata = SongValidator.validateSongMetadata(variationSongMetadata, '${songId}:${variation}');
       if (variationSongMetadata != null)
       {
         variationSongMetadata.variation = variation;
diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
index 6641a16c0..43e8ac96c 100644
--- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
@@ -115,7 +115,12 @@ class ChartEditorDialogHandler
 
       if (songData == null) continue;
 
-      var songName:String = songData.getDifficulty().songName;
+      var songName:Null<String> = songData.getDifficulty('normal') ?.songName;
+      if (songName == null) songName = songData.getDifficulty() ?.songName;
+      if (songName == null)
+      {
+        trace('[WARN] Could not fetch song name for ${targetSongId}');
+      }
 
       var linkTemplateSong:Link = new Link();
       linkTemplateSong.text = songName;
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 26b001d7e..da8dd2d86 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -2804,7 +2804,10 @@ class ChartEditorState extends HaxeUIState
       var treeSong:TreeViewNode = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName', icon: 'haxeui-core/styles/default/haxeui_tiny.png'});
       treeSong.expanded = true;
 
-      for (curVariation in availableVariations)
+      var variations = Reflect.copy(availableVariations);
+      variations.sort(SortUtil.alphabetically);
+
+      for (curVariation in variations)
       {
         var variationMetadata:SongMetadata = songMetadata.get(curVariation);
 
@@ -2940,13 +2943,14 @@ class ChartEditorState extends HaxeUIState
 
   function getCurrentTreeDifficultyNode():TreeViewNode
   {
-    var treeView:TreeView = findComponent('difficultyToolboxTree');
+    var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
+    if (difficultyToolbox == null) return null;
 
+    var treeView:TreeView = difficultyToolbox.findComponent('difficultyToolboxTree');
     if (treeView == null) return null;
 
     var result:TreeViewNode = treeView.findNodeByPath('stv_song/stv_variation_$selectedVariation/stv_difficulty_${selectedVariation}_$selectedDifficulty',
       'id');
-
     if (result == null) return null;
 
     return result;
diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
index 5cace2ff6..d35323f1b 100644
--- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
@@ -489,6 +489,8 @@ class ChartEditorToolboxHandler
     };
     inputBPM.value = state.currentSongMetadata.timeChanges[0].bpm;
 
+    var labelScrollSpeed:Label = toolbox.findComponent('labelScrollSpeed', Label);
+
     var inputScrollSpeed:Slider = toolbox.findComponent('inputScrollSpeed', Slider);
     inputScrollSpeed.onChange = function(event:UIEvent) {
       var valid:Bool = event.target.value != null && event.target.value > 0;
@@ -502,8 +504,10 @@ class ChartEditorToolboxHandler
       {
         state.currentSongChartScrollSpeed = 1.0;
       }
+      labelScrollSpeed.text = 'Scroll Speed: ${state.currentSongChartScrollSpeed}x';
     };
-    inputScrollSpeed.value = state.currentSongChartData.scrollSpeed;
+    inputScrollSpeed.value = state.currentSongChartScrollSpeed;
+    labelScrollSpeed.text = 'Scroll Speed: ${state.currentSongChartScrollSpeed}x';
 
     return toolbox;
   }
diff --git a/source/funkin/util/SerializerUtil.hx b/source/funkin/util/SerializerUtil.hx
index 9452b7785..d731aee8f 100644
--- a/source/funkin/util/SerializerUtil.hx
+++ b/source/funkin/util/SerializerUtil.hx
@@ -36,7 +36,16 @@ class SerializerUtil
    */
   public static function fromJSON(input:String):Dynamic
   {
-    return Json.parse(input);
+    try
+    {
+      return Json.parse(input);
+    }
+    catch (e)
+    {
+      trace('An error occurred while parsing JSON from string data');
+      trace(e);
+      return null;
+    }
   }
 
   /**
diff --git a/source/funkin/util/SortUtil.hx b/source/funkin/util/SortUtil.hx
index 61418c299..082de4b41 100644
--- a/source/funkin/util/SortUtil.hx
+++ b/source/funkin/util/SortUtil.hx
@@ -30,8 +30,10 @@ class SortUtil
 
   /**
    * Sort predicate for sorting strings alphabetically.
+   * @param a The first string to compare.
+   * @param b The second string to compare.
    */
-  public static function alphabetically(a:String, b:String)
+  public static function alphabetically(a:String, b:String):Int
   {
     a = a.toUpperCase();
     b = b.toUpperCase();
@@ -39,4 +41,35 @@ class SortUtil
     // Sort alphabetically. Yes that's how this works.
     return a == b ? 0 : a > b ? 1 : -1;
   }
+
+  /**
+   * Sort predicate which sorts two strings alphabetically, but prioritizes a specific string first.
+   * Example usage: `array.sort(defaultThenAlphabetical.bind('test'))` will sort the array so that the string 'test' is first.
+   * @param a The first string to compare.
+   * @param b The second string to compare.
+   * @param defaultValue The value to prioritize. Defaults to `default`.
+   */
+  public static function defaultThenAlphabetically(a:String, b:String, defaultValue:String):Int
+  {
+    if (a == b) return 0;
+    if (a == defaultValue) return 1;
+    if (b == defaultValue) return -1;
+    return alphabetically(a, b);
+  }
+
+  /**
+   * Sort predicate which sorts two strings alphabetically, but prioritizes a specific string first.
+   * Example usage: `array.sort(defaultsThenAlphabetical.bind(['test']))` will sort the array so that the string 'test' is first.
+   * @param a The first string to compare.
+   * @param b The second string to compare.
+   * @param defaultValue The value to prioritize. Defaults to `default`.
+   */
+  public static function defaultsThenAlphabetically(a:String, b:String, defaultValues:Array<String>):Int
+  {
+    if (a == b) return 0;
+    if (defaultValues.contains(a) && defaultValues.contains(b)) return alphabetically(a, b);
+    if (defaultValues.contains(a)) return 1;
+    if (defaultValues.contains(b)) return -1;
+    return alphabetically(a, b);
+  }
 }

From 9c7aec44855161e47aa9837927fe748b9331aa4a Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Mon, 14 Aug 2023 23:12:30 -0400
Subject: [PATCH 3/5] Asset redirect fix (now disabled for Github Actions
 builds)

---
 .github/workflows/build-shit.yml | 2 +-
 Project.xml                      | 4 +++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml
index ddd6e8be0..794457917 100644
--- a/.github/workflows/build-shit.yml
+++ b/.github/workflows/build-shit.yml
@@ -51,7 +51,7 @@ jobs:
       - uses: ./.github/actions/setup-haxeshit
       - name: Build game
         run: |
-          haxelib run lime build windows -debug
+          haxelib run lime build windows -debug -DNO_REDIRECT_ASSETS_FOLDER
           dir
       - uses: ./.github/actions/upload-itch
         with:
diff --git a/Project.xml b/Project.xml
index f34c9bc06..8dbb43618 100644
--- a/Project.xml
+++ b/Project.xml
@@ -181,7 +181,8 @@
 		<!-- pretends that the saved session Id was expired, forcing the reconnect prompt -->
 		<!-- <haxedef name="NG_FORCE_EXPIRED_SESSION" if="debug" /> -->
 	</section>
-	<section if="debug">
+
+	<section if="debug" unless="NO_REDIRECT_ASSETS_FOLDER || html5">
 		<!--
 			Use the parent assets folder rather than the exported one
 			No more will we accidentally undo our changes!
@@ -189,6 +190,7 @@
 		-->
 		<haxedef name="REDIRECT_ASSETS_FOLDER" />
 	</section>
+
 	<!-- <prebuild haxe="trace('prebuilding');"/> -->
 	<!-- <postbuild haxe="art/Postbuild.hx"/> -->
 	<!-- <config:ios allow-provisioning-updates="true" team-id="" /> -->

From d03a2f015795ef01155b861ba47a5a6387fe2fdf Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Mon, 14 Aug 2023 23:13:12 -0400
Subject: [PATCH 4/5] WIP on improving the difficulty toolbox

---
 .../ui/debug/charting/ChartEditorState.hx     | 172 ++++++++++++++++--
 .../charting/ChartEditorToolboxHandler.hx     |  89 +++++++++
 source/funkin/util/SortUtil.hx                |  10 +-
 3 files changed, 256 insertions(+), 15 deletions(-)

diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index da8dd2d86..f925d58ca 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -722,11 +722,38 @@ class ChartEditorState extends HaxeUIState
    */
   var songMetadata:Map<String, SongMetadata>;
 
+  /**
+   * Retrieves the list of variations for the current song.
+   */
   var availableVariations(get, null):Array<String>;
 
   function get_availableVariations():Array<String>
   {
-    return [for (x in songMetadata.keys()) x];
+    var variations:Array<String> = [for (x in songMetadata.keys()) x];
+    variations.sort(SortUtil.defaultThenAlphabetically.bind('default'));
+    return variations;
+  }
+
+  /**
+   * Retrieves the list of difficulties for the current variation of the current song.
+   * ONLY CONTAINS DIFFICULTIES FOR THE CURRENT VARIATION so if on the default variation, erect/nightmare won't be included.
+   */
+  var availableDifficulties(get, null):Array<String>;
+
+  function get_availableDifficulties():Array<String>
+  {
+    return songMetadata.get(selectedVariation).playData.difficulties;
+  }
+
+  /**
+   * Retrieves the list of difficulties for ALL variations of the current song.
+   */
+  var allDifficulties(get, null):Array<String>;
+
+  function get_allDifficulties():Array<String>
+  {
+    var result:Array<Array<String>> = [for (x in availableVariations) songMetadata.get(x).playData.difficulties];
+    return result.flatten();
   }
 
   /**
@@ -976,6 +1003,11 @@ class ChartEditorState extends HaxeUIState
     return playableCharData.opponent = value;
   }
 
+  /**
+   * SIGNALS
+   */
+  // ==============================
+  // public var onDifficultyChange(default, null):FlxTypedSignal<ChartEditorState->Void> = new FlxTypedSignal<ChartEditorState->Void>();
   /**
    * RENDER OBJECTS
    */
@@ -1247,7 +1279,6 @@ class ChartEditorState extends HaxeUIState
     var height:Int = FlxG.height - MENU_BAR_HEIGHT - GRID_TOP_PAD - 200;
     notePreview = new ChartEditorNotePreview(height);
     notePreview.y = MENU_BAR_HEIGHT + GRID_TOP_PAD;
-    // TODO: Re-enable.
     // add(notePreview);
   }
 
@@ -1438,6 +1469,9 @@ class ChartEditorState extends HaxeUIState
     addUIChangeListener('menubarItemDownscroll', event -> isViewDownscroll = event.value);
     setUICheckboxSelected('menubarItemDownscroll', isViewDownscroll);
 
+    addUIClickListener('menubarItemDifficultyUp', _ -> incrementDifficulty(1));
+    addUIClickListener('menubarItemDifficultyDown', _ -> incrementDifficulty(-1));
+
     addUIChangeListener('menubarItemPlaytestStartTime', event -> playtestStartTime = event.value);
     setUICheckboxSelected('menubarItemPlaytestStartTime', playtestStartTime);
 
@@ -1584,6 +1618,7 @@ class ChartEditorState extends HaxeUIState
     handleToolboxes();
     handlePlaybar();
     handlePlayhead();
+    // handleNotePreview();
 
     handleFileKeybinds();
     handleEditKeybinds();
@@ -2755,7 +2790,95 @@ class ChartEditorState extends HaxeUIState
   /**
    * Handle keybinds for View menu items.
    */
-  function handleViewKeybinds():Void {}
+  function handleViewKeybinds():Void
+  {
+    if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.LEFT)
+    {
+      incrementDifficulty(-1);
+    }
+    if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.RIGHT)
+    {
+      incrementDifficulty(1);
+    }
+  }
+
+  function incrementDifficulty(change:Int):Void
+  {
+    var currentDifficultyIndex:Int = availableDifficulties.indexOf(selectedDifficulty);
+    var currentAllDifficultyIndex:Int = allDifficulties.indexOf(selectedDifficulty);
+
+    if (currentDifficultyIndex == -1 || currentAllDifficultyIndex == -1)
+    {
+      trace('ERROR determining difficulty index!');
+    }
+
+    var isFirstDiff:Bool = currentAllDifficultyIndex == 0;
+    var isLastDiff:Bool = (currentAllDifficultyIndex == allDifficulties.length - 1);
+
+    var isFirstDiffInVariation:Bool = currentDifficultyIndex == 0;
+    var isLastDiffInVariation:Bool = (currentDifficultyIndex == availableDifficulties.length - 1);
+
+    trace(allDifficulties);
+
+    if (change < 0 && isFirstDiff)
+    {
+      trace('At lowest difficulty! Do nothing.');
+      return;
+    }
+
+    if (change > 0 && isLastDiff)
+    {
+      trace('At highest difficulty! Do nothing.');
+      return;
+    }
+
+    if (change < 0)
+    {
+      // Decrement difficulty.
+      // If we reached this point, we are not at the lowest difficulty.
+      if (isFirstDiffInVariation)
+      {
+        // Go to the previous variation, then last difficulty in that variation.
+        var currentVariationIndex:Int = availableVariations.indexOf(selectedVariation);
+        var prevVariation = availableVariations[currentVariationIndex - 1];
+        selectedVariation = prevVariation;
+
+        var prevDifficulty = availableDifficulties[availableDifficulties.length - 1];
+        selectedDifficulty = prevDifficulty;
+
+        refreshDifficultyTreeSelection();
+      }
+      else
+      {
+        // Go to previous difficulty in this variation.
+        var prevDifficulty = availableDifficulties[currentDifficultyIndex - 1];
+        selectedDifficulty = prevDifficulty;
+
+        refreshDifficultyTreeSelection();
+      }
+    }
+    else
+    {
+      // Increment difficulty.
+      // If we reached this point, we are not at the highest difficulty.
+      if (isLastDiffInVariation)
+      {
+        // Go to next variation, then first difficulty in that variation.
+        var currentVariationIndex:Int = availableVariations.indexOf(selectedVariation);
+        var nextVariation = availableVariations[currentVariationIndex + 1];
+        selectedVariation = nextVariation;
+
+        var nextDifficulty = availableDifficulties[0];
+        selectedDifficulty = nextDifficulty;
+      }
+      else
+      {
+        // Go to next difficulty in this variation.
+        var nextDifficulty = availableDifficulties[currentDifficultyIndex + 1];
+        selectedDifficulty = nextDifficulty;
+      }
+    }
+  }
 
   /**
    * Handle keybinds for the Test menu items.
@@ -2801,7 +2924,8 @@ class ChartEditorState extends HaxeUIState
       // Clear the tree view so we can rebuild it.
       treeView.clearNodes();
 
-      var treeSong:TreeViewNode = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName', icon: 'haxeui-core/styles/default/haxeui_tiny.png'});
+      // , icon: 'haxeui-core/styles/default/haxeui_tiny.png'
+      var treeSong:TreeViewNode = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName'});
       treeSong.expanded = true;
 
       var variations = Reflect.copy(availableVariations);
@@ -2831,10 +2955,26 @@ class ChartEditorState extends HaxeUIState
       }
 
       treeView.onChange = onChangeTreeDifficulty;
-      treeView.selectedNode = getCurrentTreeDifficultyNode();
+      refreshDifficultyTreeSelection(treeView);
     }
   }
 
+  function refreshDifficultyTreeSelection(?treeView:TreeView):Void
+  {
+    if (treeView == null)
+    {
+      // Manage the Select Difficulty tree view.
+      var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
+      if (difficultyToolbox == null) return;
+
+      treeView = difficultyToolbox.findComponent('difficultyToolboxTree');
+      if (treeView == null) return;
+    }
+
+    trace(treeView);
+    treeView.selectedNode = getCurrentTreeDifficultyNode(treeView);
+  }
+
   function handlePlayerPreviewToolbox():Void
   {
     // Manage the Select Difficulty tree view.
@@ -2941,13 +3081,16 @@ class ChartEditorState extends HaxeUIState
     }
   }
 
-  function getCurrentTreeDifficultyNode():TreeViewNode
+  function getCurrentTreeDifficultyNode(?treeView:TreeView = null):TreeViewNode
   {
-    var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
-    if (difficultyToolbox == null) return null;
+    if (treeView == null)
+    {
+      var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
+      if (difficultyToolbox == null) return null;
 
-    var treeView:TreeView = difficultyToolbox.findComponent('difficultyToolboxTree');
-    if (treeView == null) return null;
+      treeView = difficultyToolbox.findComponent('difficultyToolboxTree');
+      if (treeView == null) return null;
+    }
 
     var result:TreeViewNode = treeView.findNodeByPath('stv_song/stv_variation_$selectedVariation/stv_difficulty_${selectedVariation}_$selectedDifficulty',
       'id');
@@ -2956,6 +3099,10 @@ class ChartEditorState extends HaxeUIState
     return result;
   }
 
+  /**
+   * Called when selecting a tree element in the Difficulty toolbox.
+   * @param event The click event.
+   */
   function onChangeTreeDifficulty(event:UIEvent):Void
   {
     // Get the newly selected node.
@@ -2966,7 +3113,7 @@ class ChartEditorState extends HaxeUIState
     {
       trace('No target node!');
       // Reset the user's selection.
-      treeView.selectedNode = getCurrentTreeDifficultyNode();
+      treeView.selectedNode = getCurrentTreeDifficultyNode(treeView);
       return;
     }
 
@@ -2981,13 +3128,14 @@ class ChartEditorState extends HaxeUIState
           trace('Changing difficulty to $variation:$difficulty');
           selectedVariation = variation;
           selectedDifficulty = difficulty;
+          // refreshDifficultyTreeSelection(treeView);
         }
       // case 'song':
       // case 'variation':
       default:
         // Reset the user's selection.
         trace('Selected wrong node type, resetting selection.');
-        treeView.selectedNode = getCurrentTreeDifficultyNode();
+        treeView.selectedNode = getCurrentTreeDifficultyNode(treeView);
     }
   }
 
diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
index d35323f1b..cd3172ebf 100644
--- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
@@ -1,5 +1,7 @@
 package funkin.ui.debug.charting;
 
+import haxe.ui.containers.TreeView;
+import haxe.ui.containers.TreeViewNode;
 import funkin.play.character.BaseCharacter.CharacterType;
 import funkin.play.event.SongEvent;
 import funkin.play.event.SongEventData;
@@ -36,6 +38,7 @@ enum ChartEditorToolMode
 /**
  * Static functions which handle building themed UI elements for a provided ChartEditorState.
  */
+@:allow(funkin.ui.debug.charting.ChartEditorState)
 class ChartEditorToolboxHandler
 {
   public static function setToolboxState(state:ChartEditorState, id:String, shown:Bool):Void
@@ -59,6 +62,29 @@ class ChartEditorToolboxHandler
     if (toolbox != null)
     {
       toolbox.showDialog(false);
+
+      switch (id)
+      {
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:
+          onShowToolboxTools(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
+          onShowToolboxNoteData(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:
+          onShowToolboxEventData(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
+          onShowToolboxDifficulty(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
+          onShowToolboxMetadata(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
+          onShowToolboxCharacters(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
+          onShowToolboxPlayerPreview(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
+          onShowToolboxOpponentPreview(state, toolbox);
+        default:
+          // This happens if you try to load an unknown layout.
+          trace('ChartEditorToolboxHandler.showToolbox() - Unknown toolbox ID: $id');
+      }
     }
     else
     {
@@ -75,6 +101,29 @@ class ChartEditorToolboxHandler
     if (toolbox != null)
     {
       toolbox.hideDialog(DialogButton.CANCEL);
+
+      switch (id)
+      {
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:
+          onHideToolboxTools(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
+          onHideToolboxNoteData(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:
+          onHideToolboxEventData(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
+          onHideToolboxDifficulty(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
+          onHideToolboxMetadata(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
+          onHideToolboxCharacters(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
+          onHideToolboxPlayerPreview(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
+          onHideToolboxOpponentPreview(state, toolbox);
+        default:
+          // This happens if you try to load an unknown layout.
+          trace('ChartEditorToolboxHandler.hideToolbox() - Unknown toolbox ID: $id');
+      }
     }
     else
     {
@@ -186,6 +235,10 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
+  static function onShowToolboxTools(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
+  static function onHideToolboxTools(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
   static function buildToolboxNoteDataLayout(state:ChartEditorState):CollapsibleDialog
   {
     var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
@@ -230,6 +283,10 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
+  static function onShowToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
+  static function onHideToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
   static function buildToolboxEventDataLayout(state:ChartEditorState):CollapsibleDialog
   {
     var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT);
@@ -275,6 +332,10 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
+  static function onShowToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
+  static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
   static function buildEventDataFormFromSchema(state:ChartEditorState, target:Box, schema:SongEventSchema):Void
   {
     trace(schema);
@@ -405,6 +466,18 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
+  static function onShowToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void
+  {
+    // Update the selected difficulty when reopening the toolbox.
+    var treeView:TreeView = toolbox.findComponent('difficultyToolboxTree');
+    if (treeView == null) return;
+
+    treeView.selectedNode = state.getCurrentTreeDifficultyNode(treeView);
+    trace('selected node: ${treeView.selectedNode}');
+  }
+
+  static function onHideToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
   static function buildToolboxMetadataLayout(state:ChartEditorState):CollapsibleDialog
   {
     var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
@@ -512,6 +585,10 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
+  static function onShowToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
+  static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
   static function buildToolboxCharactersLayout(state:ChartEditorState):CollapsibleDialog
   {
     var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT);
@@ -529,6 +606,10 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
+  static function onShowToolboxCharacters(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
+  static function onHideToolboxCharacters(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
   static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):CollapsibleDialog
   {
     var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
@@ -553,6 +634,10 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
+  static function onShowToolboxPlayerPreview(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
+  static function onHideToolboxPlayerPreview(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
   static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):CollapsibleDialog
   {
     var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
@@ -576,4 +661,8 @@ class ChartEditorToolboxHandler
 
     return toolbox;
   }
+
+  static function onShowToolboxOpponentPreview(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
+  static function onHideToolboxOpponentPreview(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
 }
diff --git a/source/funkin/util/SortUtil.hx b/source/funkin/util/SortUtil.hx
index 082de4b41..6f3b9c0fb 100644
--- a/source/funkin/util/SortUtil.hx
+++ b/source/funkin/util/SortUtil.hx
@@ -47,7 +47,7 @@ class SortUtil
    * Example usage: `array.sort(defaultThenAlphabetical.bind('test'))` will sort the array so that the string 'test' is first.
    * @param a The first string to compare.
    * @param b The second string to compare.
-   * @param defaultValue The value to prioritize. Defaults to `default`.
+   * @param defaultValue The value to prioritize.
    */
   public static function defaultThenAlphabetically(a:String, b:String, defaultValue:String):Int
   {
@@ -62,12 +62,16 @@ class SortUtil
    * Example usage: `array.sort(defaultsThenAlphabetical.bind(['test']))` will sort the array so that the string 'test' is first.
    * @param a The first string to compare.
    * @param b The second string to compare.
-   * @param defaultValue The value to prioritize. Defaults to `default`.
+   * @param defaultValues The values to prioritize.
    */
   public static function defaultsThenAlphabetically(a:String, b:String, defaultValues:Array<String>):Int
   {
     if (a == b) return 0;
-    if (defaultValues.contains(a) && defaultValues.contains(b)) return alphabetically(a, b);
+    if (defaultValues.contains(a) && defaultValues.contains(b))
+    {
+      // Sort by index in defaultValues
+      return defaultValues.indexOf(a) - defaultValues.indexOf(b);
+    };
     if (defaultValues.contains(a)) return 1;
     if (defaultValues.contains(b)) return -1;
     return alphabetically(a, b);

From 8732adc144af3f49f76a61e50002b679d962e6ff Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 15 Aug 2023 16:08:12 -0400
Subject: [PATCH 5/5] Difficulty selection now updates metadata toolbox

---
 .../ui/debug/charting/ChartEditorState.hx     | 56 ++++++++++++++++++-
 .../charting/ChartEditorToolboxHandler.hx     | 14 ++++-
 source/funkin/util/SortUtil.hx                | 25 +++++++--
 3 files changed, 84 insertions(+), 11 deletions(-)

diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index f925d58ca..c26fb8998 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1,5 +1,9 @@
 package funkin.ui.debug.charting;
 
+import haxe.ui.components.TextField;
+import haxe.ui.components.DropDown;
+import haxe.ui.components.NumberStepper;
+import haxe.ui.containers.Frame;
 import flixel.addons.display.FlxSliceSprite;
 import flixel.addons.display.FlxTiledSprite;
 import flixel.FlxCamera;
@@ -2834,7 +2838,8 @@ class ChartEditorState extends HaxeUIState
 
     if (change < 0)
     {
-      // Decrement difficulty.
+      trace('Decrement difficulty.');
+
       // If we reached this point, we are not at the lowest difficulty.
       if (isFirstDiffInVariation)
       {
@@ -2847,6 +2852,7 @@ class ChartEditorState extends HaxeUIState
         selectedDifficulty = prevDifficulty;
 
         refreshDifficultyTreeSelection();
+        refreshSongMetadataToolbox();
       }
       else
       {
@@ -2855,11 +2861,13 @@ class ChartEditorState extends HaxeUIState
         selectedDifficulty = prevDifficulty;
 
         refreshDifficultyTreeSelection();
+        refreshSongMetadataToolbox();
       }
     }
     else
     {
-      // Increment difficulty.
+      trace('Increment difficulty.');
+
       // If we reached this point, we are not at the highest difficulty.
       if (isLastDiffInVariation)
       {
@@ -2870,12 +2878,18 @@ class ChartEditorState extends HaxeUIState
 
         var nextDifficulty = availableDifficulties[0];
         selectedDifficulty = nextDifficulty;
+
+        refreshDifficultyTreeSelection();
+        refreshSongMetadataToolbox();
       }
       else
       {
         // Go to next difficulty in this variation.
         var nextDifficulty = availableDifficulties[currentDifficultyIndex + 1];
         selectedDifficulty = nextDifficulty;
+
+        refreshDifficultyTreeSelection();
+        refreshSongMetadataToolbox();
       }
     }
   }
@@ -3125,10 +3139,11 @@ class ChartEditorState extends HaxeUIState
 
         if (variation != null && difficulty != null)
         {
-          trace('Changing difficulty to $variation:$difficulty');
+          trace('Changing difficulty to "$variation:$difficulty"');
           selectedVariation = variation;
           selectedDifficulty = difficulty;
           // refreshDifficultyTreeSelection(treeView);
+          refreshSongMetadataToolbox();
         }
       // case 'song':
       // case 'variation':
@@ -3136,9 +3151,44 @@ class ChartEditorState extends HaxeUIState
         // Reset the user's selection.
         trace('Selected wrong node type, resetting selection.');
         treeView.selectedNode = getCurrentTreeDifficultyNode(treeView);
+        refreshSongMetadataToolbox();
     }
   }
 
+  /**
+   * When the difficulty changes, update the song metadata toolbox to reflect the new data.
+   */
+  function refreshSongMetadataToolbox():Void
+  {
+    var toolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
+
+    var inputSongName:TextField = toolbox.findComponent('inputSongName', TextField);
+    inputSongName.value = currentSongMetadata.songName;
+
+    var inputSongArtist:TextField = toolbox.findComponent('inputSongArtist', TextField);
+    inputSongArtist.value = currentSongMetadata.artist;
+
+    var inputStage:DropDown = toolbox.findComponent('inputStage', DropDown);
+    inputStage.value = currentSongMetadata.playData.stage;
+
+    var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown);
+    inputNoteSkin.value = currentSongMetadata.playData.noteSkin;
+
+    var inputBPM:NumberStepper = toolbox.findComponent('inputBPM', NumberStepper);
+    inputBPM.value = currentSongMetadata.timeChanges[0].bpm;
+
+    var labelScrollSpeed:Label = toolbox.findComponent('labelScrollSpeed', Label);
+    labelScrollSpeed.text = 'Scroll Speed: ${currentSongChartScrollSpeed}x';
+
+    var inputScrollSpeed:Slider = toolbox.findComponent('inputScrollSpeed', Slider);
+    inputScrollSpeed.value = currentSongChartScrollSpeed;
+
+    var frameVariation:Frame = toolbox.findComponent('frameVariation', Frame);
+    frameVariation.text = 'Variation: ${selectedVariation.toTitleCase()}';
+    var frameDifficulty:Frame = toolbox.findComponent('frameDifficulty', Frame);
+    frameDifficulty.text = 'Difficulty: ${selectedDifficulty.toTitleCase()}';
+  }
+
   function addDifficulty(variation:String):Void {}
 
   function addVariation(variationId:String):Void
diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
index cd3172ebf..a6e230f6e 100644
--- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
@@ -19,6 +19,7 @@ import haxe.ui.containers.Box;
 import haxe.ui.containers.Grid;
 import haxe.ui.containers.Group;
 import haxe.ui.containers.VBox;
+import haxe.ui.containers.Frame;
 import haxe.ui.containers.dialogs.CollapsibleDialog;
 import haxe.ui.containers.dialogs.Dialog.DialogButton;
 import haxe.ui.containers.dialogs.Dialog.DialogEvent;
@@ -537,7 +538,7 @@ class ChartEditorToolboxHandler
 
     var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown);
     inputNoteSkin.onChange = function(event:UIEvent) {
-      if (event.data.id == null) return;
+      if ((event?.data?.id ?? null) == null) return;
       state.currentSongMetadata.playData.noteSkin = event.data.id;
     };
     inputNoteSkin.value = state.currentSongMetadata.playData.noteSkin;
@@ -582,10 +583,19 @@ class ChartEditorToolboxHandler
     inputScrollSpeed.value = state.currentSongChartScrollSpeed;
     labelScrollSpeed.text = 'Scroll Speed: ${state.currentSongChartScrollSpeed}x';
 
+    var frameVariation:Frame = toolbox.findComponent('frameVariation', Frame);
+    frameVariation.text = 'Variation: ${state.selectedVariation.toTitleCase()}';
+
+    var frameDifficulty:Frame = toolbox.findComponent('frameDifficulty', Frame);
+    frameDifficulty.text = 'Difficulty: ${state.selectedDifficulty.toTitleCase()}';
+
     return toolbox;
   }
 
-  static function onShowToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+  static function onShowToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void
+  {
+    state.refreshSongMetadataToolbox();
+  }
 
   static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
 
diff --git a/source/funkin/util/SortUtil.hx b/source/funkin/util/SortUtil.hx
index 6f3b9c0fb..df45e0717 100644
--- a/source/funkin/util/SortUtil.hx
+++ b/source/funkin/util/SortUtil.hx
@@ -6,6 +6,19 @@ import flixel.util.FlxSort;
 #end
 import funkin.play.notes.NoteSprite;
 
+/**
+ * A set of functions related to sorting.
+ *
+ * NOTE: `Array.sort()` takes a function `(x, y) -> Int`.
+ * If the objects are in the correct order (x before y), return a negative value.
+ * If the objects need to be swapped (y before x), return a negative value.
+ * If the objects are equal, return 0.
+ *
+ * NOTE: `Array.sort()` does NOT guarantee that the order of equal elements. `haxe.ds.ArraySort.sort()` does guarantee this.
+ * NOTE: `Array.sort()` may not be the most efficient sorting algorithm for all use cases (especially if the array is known to be mostly sorted).
+ *    You may consider using one of the functions in `funkin.util.tools.ArraySortTools` instead.
+ * NOTE: Both sort functions modify the array in-place. You may consider using `Reflect.copy()` to make a copy of the array before sorting.
+ */
 class SortUtil
 {
   /**
@@ -49,11 +62,11 @@ class SortUtil
    * @param b The second string to compare.
    * @param defaultValue The value to prioritize.
    */
-  public static function defaultThenAlphabetically(a:String, b:String, defaultValue:String):Int
+  public static function defaultThenAlphabetically(defaultValue:String, a:String, b:String):Int
   {
     if (a == b) return 0;
-    if (a == defaultValue) return 1;
-    if (b == defaultValue) return -1;
+    if (a == defaultValue) return -1;
+    if (b == defaultValue) return 1;
     return alphabetically(a, b);
   }
 
@@ -64,7 +77,7 @@ class SortUtil
    * @param b The second string to compare.
    * @param defaultValues The values to prioritize.
    */
-  public static function defaultsThenAlphabetically(a:String, b:String, defaultValues:Array<String>):Int
+  public static function defaultsThenAlphabetically(defaultValues:Array<String>, a:String, b:String):Int
   {
     if (a == b) return 0;
     if (defaultValues.contains(a) && defaultValues.contains(b))
@@ -72,8 +85,8 @@ class SortUtil
       // Sort by index in defaultValues
       return defaultValues.indexOf(a) - defaultValues.indexOf(b);
     };
-    if (defaultValues.contains(a)) return 1;
-    if (defaultValues.contains(b)) return -1;
+    if (defaultValues.contains(a)) return -1;
+    if (defaultValues.contains(b)) return 1;
     return alphabetically(a, b);
   }
 }