From 2916bd6ff7ec9f1e136d84323d8880ca009a3f4e Mon Sep 17 00:00:00 2001
From: Hundrec <hundrecard@gmail.com>
Date: Fri, 14 Jun 2024 21:52:52 -0700
Subject: [PATCH 01/64] Reorder download Git step in compiling guide
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Moved “download Git” from the middle of the guide to the setup step
Should prevent errors with Git before installing Git
---
 docs/COMPILING.md | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/docs/COMPILING.md b/docs/COMPILING.md
index e7c19875a..b8ddee4a7 100644
--- a/docs/COMPILING.md
+++ b/docs/COMPILING.md
@@ -2,14 +2,14 @@
 
 0. Setup
     - Download Haxe from [Haxe.org](https://haxe.org)
+    - Download Git from [git-scm.com](https://www.git-scm.com)
 1. Cloning the Repository: Make sure when you clone, you clone the submodules to get the assets repo:
     - `git clone --recurse-submodules https://github.com/FunkinCrew/funkin.git`
     - If you accidentally cloned without the `assets` submodule (aka didn't follow the step above), you can run `git submodule update --init --recursive` to get the assets in a foolproof way.
 2. Install `hmm` (run `haxelib --global install hmm` and then `haxelib --global run hmm setup`)
-3. Download Git from [git-scm.com](https://www.git-scm.com)
-4. Install all haxelibs of the current branch by running `hmm install`
-5. Setup lime: `haxelib run lime setup`
-6. Platform setup
+3. Install all haxelibs of the current branch by running `hmm install`
+4. Setup lime: `haxelib run lime setup`
+5. Platform setup
    - For Windows, download the [Visual Studio Build Tools](https://aka.ms/vs/17/release/vs_BuildTools.exe)
         - When prompted, select "Individual Components" and make sure to download the following:
         - MSVC v143 VS 2022 C++ x64/x86 build tools
@@ -17,8 +17,8 @@
     - Mac: [`lime setup mac` Documentation](https://lime.openfl.org/docs/advanced-setup/macos/)
     - Linux: [`lime setup linux` Documentation](https://lime.openfl.org/docs/advanced-setup/linux/)
     - HTML5: Compiles without any extra setup
-7. If you are targeting for native, you may need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug`
-8. `lime test PLATFORM` ! Add `-debug` to enable several debug features such as time travel (`PgUp`/`PgDn` in Play State).
+6. If you are targeting for native, you may need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug`
+7. `lime test PLATFORM` ! Add `-debug` to enable several debug features such as time travel (`PgUp`/`PgDn` in Play State).
 
 # Troubleshooting
 

From 9fb4a8719a6cd07aa2af2638adc494786ca5425e Mon Sep 17 00:00:00 2001
From: Hundrec <hundrecard@gmail.com>
Date: Sat, 15 Jun 2024 13:37:21 -0400
Subject: [PATCH 02/64] Add ZIP button warning and restructured sentences

Each step should be easier to follow with this structure
---
 docs/COMPILING.md | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/docs/COMPILING.md b/docs/COMPILING.md
index b8ddee4a7..b2a106c86 100644
--- a/docs/COMPILING.md
+++ b/docs/COMPILING.md
@@ -3,12 +3,13 @@
 0. Setup
     - Download Haxe from [Haxe.org](https://haxe.org)
     - Download Git from [git-scm.com](https://www.git-scm.com)
-1. Cloning the Repository: Make sure when you clone, you clone the submodules to get the assets repo:
-    - `git clone --recurse-submodules https://github.com/FunkinCrew/funkin.git`
-    - If you accidentally cloned without the `assets` submodule (aka didn't follow the step above), you can run `git submodule update --init --recursive` to get the assets in a foolproof way.
-2. Install `hmm` (run `haxelib --global install hmm` and then `haxelib --global run hmm setup`)
-3. Install all haxelibs of the current branch by running `hmm install`
-4. Setup lime: `haxelib run lime setup`
+    - Do NOT download the repository using the Download ZIP button on GitHub or you may run into errors!
+    - Instead, open a command prompt and do the following steps...
+1. Run `git clone --recurse-submodules https://github.com/FunkinCrew/funkin.git` to clone the repository with the necessary assets submodule
+    - _If you accidentally cloned without the `assets` submodule (aka didn't follow the step above), you can run `git submodule update --init --recursive` to get the assets in a foolproof way._
+2. Run `haxelib --global install hmm` and then `haxelib --global run hmm setup` to install hmm.json
+3. Run `hmm install` to install all haxelibs of the current branch
+4. Run `haxelib run lime setup` to set up lime
 5. Platform setup
    - For Windows, download the [Visual Studio Build Tools](https://aka.ms/vs/17/release/vs_BuildTools.exe)
         - When prompted, select "Individual Components" and make sure to download the following:

From 841a61408c13ff34760a448c588e35bc3b203bf0 Mon Sep 17 00:00:00 2001
From: Eric <ericmyllyoja@gmail.com>
Date: Sun, 16 Jun 2024 18:48:01 -0400
Subject: [PATCH 03/64] Make downloading the assets submodule a separate step.

---
 docs/COMPILING.md | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/docs/COMPILING.md b/docs/COMPILING.md
index b2a106c86..cc90bd348 100644
--- a/docs/COMPILING.md
+++ b/docs/COMPILING.md
@@ -5,8 +5,9 @@
     - Download Git from [git-scm.com](https://www.git-scm.com)
     - Do NOT download the repository using the Download ZIP button on GitHub or you may run into errors!
     - Instead, open a command prompt and do the following steps...
-1. Run `git clone --recurse-submodules https://github.com/FunkinCrew/funkin.git` to clone the repository with the necessary assets submodule
-    - _If you accidentally cloned without the `assets` submodule (aka didn't follow the step above), you can run `git submodule update --init --recursive` to get the assets in a foolproof way._
+1. Run `git clone https://github.com/FunkinCrew/funkin.git` to clone the base repository.
+2. Run `git submodule update --init --recursive` to download the game's assets.
+    - NOTE: By performing this operation, you are downloading Content which is proprietary and protected by national and international copyright and trademark laws. See [the LICENSE.md file for the Funkin.assets](https://github.com/FunkinCrew/funkin.assets/blob/main/LICENSE.md) repo for more information.
 2. Run `haxelib --global install hmm` and then `haxelib --global run hmm setup` to install hmm.json
 3. Run `hmm install` to install all haxelibs of the current branch
 4. Run `haxelib run lime setup` to set up lime

From 8a4f19d603b5900692c8c646c05e8b409688e963 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 25 Jun 2024 18:22:30 -0400
Subject: [PATCH 04/64] Add change counts labels to Actions labeler

---
 .github/changed-lines-count-labeler.yml | 12 ++++++++++++
 .github/workflows/labeler.yml           | 15 ++++++++++++++-
 2 files changed, 26 insertions(+), 1 deletion(-)
 create mode 100644 .github/changed-lines-count-labeler.yml

diff --git a/.github/changed-lines-count-labeler.yml b/.github/changed-lines-count-labeler.yml
new file mode 100644
index 000000000..6f890f534
--- /dev/null
+++ b/.github/changed-lines-count-labeler.yml
@@ -0,0 +1,12 @@
+# Add 'small' to any changes below 10 lines
+small:
+  max: 9
+
+# Add 'medium' to any changes between 10 and 100 lines
+medium:
+  min: 10
+  max: 99
+
+# Add 'large' to any changes for more than 100 lines
+large:
+  min: 100
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
index 0bcc420d3..a861af578 100644
--- a/.github/workflows/labeler.yml
+++ b/.github/workflows/labeler.yml
@@ -9,6 +9,19 @@ jobs:
       pull-requests: write
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/labeler@v5
+    - name: Set basic labels
+      uses: actions/labeler@v5
       with:
         sync-labels: true
+  changed-lines-count-labeler:
+    permissions:
+      contents: read
+      pull-requests: write
+    runs-on: ubuntu-latest
+    name: An action for automatically labelling pull requests based on the changed lines count
+    steps:
+    - name: Set change count labels
+      uses: vkirilichev/changed-lines-count-labeler@v0.2
+      with:
+        repo-token: ${{ secrets.GITHUB_TOKEN }}
+        configuration-path: .github/changed-lines-count-labeler.yml

From fbc78adb65c7db8b045f1ea5d6599ae7348912a0 Mon Sep 17 00:00:00 2001
From: FlooferLand! <76737186+FlooferLand@users.noreply.github.com>
Date: Mon, 1 Jul 2024 14:42:50 +0300
Subject: [PATCH 05/64] Added new settings items

---
 source/funkin/ui/options/MenuItemEnums.hx     |  10 ++
 source/funkin/ui/options/PreferencesMenu.hx   | 149 +++++++++++-------
 .../options/items/CheckboxPreferenceItem.hx   |  49 ++++++
 .../ui/options/items/EnumPreferenceItem.hx    |  84 ++++++++++
 .../ui/options/items/NumberPreferenceItem.hx  | 136 ++++++++++++++++
 5 files changed, 372 insertions(+), 56 deletions(-)
 create mode 100644 source/funkin/ui/options/MenuItemEnums.hx
 create mode 100644 source/funkin/ui/options/items/CheckboxPreferenceItem.hx
 create mode 100644 source/funkin/ui/options/items/EnumPreferenceItem.hx
 create mode 100644 source/funkin/ui/options/items/NumberPreferenceItem.hx

diff --git a/source/funkin/ui/options/MenuItemEnums.hx b/source/funkin/ui/options/MenuItemEnums.hx
new file mode 100644
index 000000000..4513a92af
--- /dev/null
+++ b/source/funkin/ui/options/MenuItemEnums.hx
@@ -0,0 +1,10 @@
+package funkin.ui.options;
+
+// Add enums for use with `EnumPreferenceItem` here!
+/* Example:
+  class MyOptionEnum
+  {
+    public static inline var YuhUh = "true";  // "true" is the value's ID
+    public static inline var NuhUh = "false";
+  }
+ */
diff --git a/source/funkin/ui/options/PreferencesMenu.hx b/source/funkin/ui/options/PreferencesMenu.hx
index 783aef0ba..5fbefceed 100644
--- a/source/funkin/ui/options/PreferencesMenu.hx
+++ b/source/funkin/ui/options/PreferencesMenu.hx
@@ -8,6 +8,11 @@ import funkin.ui.AtlasText.AtlasFont;
 import funkin.ui.options.OptionsState.Page;
 import funkin.graphics.FunkinCamera;
 import funkin.ui.TextMenuList.TextMenuItem;
+import funkin.audio.FunkinSound;
+import funkin.ui.options.MenuItemEnums;
+import funkin.ui.options.items.CheckboxPreferenceItem;
+import funkin.ui.options.items.NumberPreferenceItem;
+import funkin.ui.options.items.EnumPreferenceItem;
 
 class PreferencesMenu extends Page
 {
@@ -69,11 +74,51 @@ class PreferencesMenu extends Page
     }, Preferences.autoPause);
   }
 
+  override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    // Indent the selected item.
+    items.forEach(function(daItem:TextMenuItem) {
+      var thyOffset:Int = 0;
+
+      // Initializing thy text width (if thou text present)
+      var thyTextWidth:Int = 0;
+      if (Std.isOfType(daItem, EnumPreferenceItem)) thyTextWidth = cast(daItem, EnumPreferenceItem).lefthandText.getWidth();
+      else if (Std.isOfType(daItem, NumberPreferenceItem)) thyTextWidth = cast(daItem, NumberPreferenceItem).lefthandText.getWidth();
+
+      if (thyTextWidth != 0)
+      {
+        // Magic number because of the weird offset thats being added by default
+        thyOffset += thyTextWidth - 75;
+      }
+
+      if (items.selectedItem == daItem)
+      {
+        thyOffset += 150;
+      }
+      else
+      {
+        thyOffset += 120;
+      }
+
+      daItem.x = thyOffset;
+    });
+  }
+
+  // - Preference item creation methods -
+  // Should be moved into a separate PreferenceItems class but you can't access PreferencesMenu.items and PreferencesMenu.preferenceItems from outside.
+
+  /**
+   * Creates a pref item that works with booleans
+   * @param onChange Gets called every time the player changes the value; use this to apply the value
+   * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
+   */
   function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool):Void
   {
     var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(0, 120 * (items.length - 1 + 1), defaultValue);
 
-    items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
+    items.createItem(0, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
       var value = !checkbox.currentValue;
       onChange(value);
       checkbox.currentValue = value;
@@ -82,62 +127,54 @@ class PreferencesMenu extends Page
     preferenceItems.add(checkbox);
   }
 
-  override function update(elapsed:Float)
+  /**
+   * Creates a pref item that works with general numbers
+   * @param onChange Gets called every time the player changes the value; use this to apply the value
+   * @param valueFormatter Will get called every time the game needs to display the float value; use this to change how the displayed value looks
+   * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
+   * @param min Minimum value (example: 0)
+   * @param max Maximum value (example: 10)
+   * @param step The value to increment/decrement by (default = 0.1)
+   * @param precision Rounds decimals up to a `precision` amount of digits (ex: 4 -> 0.1234, 2 -> 0.12)
+   */
+  function createPrefItemNumber(prefName:String, prefDesc:String, onChange:Float->Void, ?valueFormatter:Float->String, defaultValue:Int, min:Int, max:Int,
+      step:Float = 0.1, precision:Int):Void
   {
-    super.update(elapsed);
+    var item = new NumberPreferenceItem(0, (120 * items.length) + 30, prefName, defaultValue, min, max, step, precision, onChange, valueFormatter);
+    items.addItem(prefName, item);
+    preferenceItems.add(item.lefthandText);
+  }
 
-    // Indent the selected item.
-    // TODO: Only do this on menu change?
-    items.forEach(function(daItem:TextMenuItem) {
-      if (items.selectedItem == daItem) daItem.x = 150;
-      else
-        daItem.x = 120;
-    });
-  }
-}
-
-class CheckboxPreferenceItem extends FlxSprite
-{
-  public var currentValue(default, set):Bool;
-
-  public function new(x:Float, y:Float, defaultValue:Bool = false)
-  {
-    super(x, y);
-
-    frames = Paths.getSparrowAtlas('checkboxThingie');
-    animation.addByPrefix('static', 'Check Box unselected', 24, false);
-    animation.addByPrefix('checked', 'Check Box selecting animation', 24, false);
-
-    setGraphicSize(Std.int(width * 0.7));
-    updateHitbox();
-
-    this.currentValue = defaultValue;
-  }
-
-  override function update(elapsed:Float)
-  {
-    super.update(elapsed);
-
-    switch (animation.curAnim.name)
-    {
-      case 'static':
-        offset.set();
-      case 'checked':
-        offset.set(17, 70);
-    }
-  }
-
-  function set_currentValue(value:Bool):Bool
-  {
-    if (value)
-    {
-      animation.play('checked', true);
-    }
-    else
-    {
-      animation.play('static');
-    }
-
-    return currentValue = value;
+  /**
+   * Creates a pref item that works with number percentages
+   * @param onChange Gets called every time the player changes the value; use this to apply the value
+   * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
+   * @param min Minimum value (default = 0)
+   * @param max Maximum value (default = 100)
+   */
+  function createPrefItemPercentage(prefName:String, prefDesc:String, onChange:Int->Void, defaultValue:Int, min:Int = 0, max:Int = 100):Void
+  {
+    var newCallback = function(value:Float) {
+      onChange(Std.int(value));
+    };
+    var formatter = function(value:Float) {
+      return '${value}%';
+    };
+    var item = new NumberPreferenceItem(0, (120 * items.length) + 30, prefName, defaultValue, min, max, 10, 0, newCallback, formatter);
+    items.addItem(prefName, item);
+    preferenceItems.add(item.lefthandText);
+  }
+
+  /**
+   * Creates a pref item that works with enums
+   * @param values Maps enum values to display strings _(ex: `NoteHitSoundType.PingPong => "Ping pong"`)_
+   * @param onChange Gets called every time the player changes the value; use this to apply the value
+   * @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
+   */
+  function createPrefItemEnum(prefName:String, prefDesc:String, values:Map<String, String>, onChange:String->Void, defaultValue:String):Void
+  {
+    var item = new EnumPreferenceItem(0, (120 * items.length) + 30, prefName, values, defaultValue, onChange);
+    items.addItem(prefName, item);
+    preferenceItems.add(item.lefthandText);
   }
 }
diff --git a/source/funkin/ui/options/items/CheckboxPreferenceItem.hx b/source/funkin/ui/options/items/CheckboxPreferenceItem.hx
new file mode 100644
index 000000000..88c4fb6b0
--- /dev/null
+++ b/source/funkin/ui/options/items/CheckboxPreferenceItem.hx
@@ -0,0 +1,49 @@
+package funkin.ui.options.items;
+
+import flixel.FlxSprite.FlxSprite;
+
+class CheckboxPreferenceItem extends FlxSprite
+{
+  public var currentValue(default, set):Bool;
+
+  public function new(x:Float, y:Float, defaultValue:Bool = false)
+  {
+    super(x, y);
+
+    frames = Paths.getSparrowAtlas('checkboxThingie');
+    animation.addByPrefix('static', 'Check Box unselected', 24, false);
+    animation.addByPrefix('checked', 'Check Box selecting animation', 24, false);
+
+    setGraphicSize(Std.int(width * 0.7));
+    updateHitbox();
+
+    this.currentValue = defaultValue;
+  }
+
+  override function update(elapsed:Float)
+  {
+    super.update(elapsed);
+
+    switch (animation.curAnim.name)
+    {
+      case 'static':
+        offset.set();
+      case 'checked':
+        offset.set(17, 70);
+    }
+  }
+
+  function set_currentValue(value:Bool):Bool
+  {
+    if (value)
+    {
+      animation.play('checked', true);
+    }
+    else
+    {
+      animation.play('static');
+    }
+
+    return currentValue = value;
+  }
+}
diff --git a/source/funkin/ui/options/items/EnumPreferenceItem.hx b/source/funkin/ui/options/items/EnumPreferenceItem.hx
new file mode 100644
index 000000000..02a273353
--- /dev/null
+++ b/source/funkin/ui/options/items/EnumPreferenceItem.hx
@@ -0,0 +1,84 @@
+package funkin.ui.options.items;
+
+import funkin.ui.TextMenuList;
+import funkin.ui.AtlasText;
+import funkin.input.Controls;
+import funkin.ui.options.MenuItemEnums;
+import haxe.EnumTools;
+
+/**
+ * Preference item that allows the player to pick a value from an enum (list of values)
+ */
+class EnumPreferenceItem extends TextMenuItem
+{
+  function controls():Controls
+  {
+    return PlayerSettings.player1.controls;
+  }
+
+  public var lefthandText:AtlasText;
+
+  public var currentValue:String;
+  public var onChangeCallback:Null<String->Void>;
+  public var map:Map<String, String>;
+  public var keys:Array<String> = [];
+
+  var index = 0;
+
+  public function new(x:Float, y:Float, name:String, map:Map<String, String>, defaultValue:String, ?callback:String->Void)
+  {
+    super(x, y, name, function() {
+      callback(this.currentValue);
+    });
+
+    updateHitbox();
+
+    this.map = map;
+    this.currentValue = defaultValue;
+    this.onChangeCallback = callback;
+
+    var i:Int = 0;
+    for (key in map.keys())
+    {
+      this.keys.push(key);
+      if (this.currentValue == key) index = i;
+      i += 1;
+    }
+
+    lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT);
+  }
+
+  override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    // var fancyTextFancyColor:Color;
+    if (selected)
+    {
+      var shouldDecrease:Bool = controls().UI_LEFT_P;
+      var shouldIncrease:Bool = controls().UI_RIGHT_P;
+
+      if (shouldDecrease) index -= 1;
+      if (shouldIncrease) index += 1;
+
+      if (index > keys.length - 1) index = 0;
+      if (index < 0) index = keys.length - 1;
+
+      currentValue = keys[index];
+      if (onChangeCallback != null && (shouldIncrease || shouldDecrease))
+      {
+        onChangeCallback(currentValue);
+      }
+    }
+
+    lefthandText.text = formatted(currentValue);
+  }
+
+  function formatted(value:String):String
+  {
+    // FIXME: Can't add arrows around the text because the font doesn't support < >
+    // var leftArrow:String = selected ? '<' : '';
+    // var rightArrow:String = selected ? '>' : '';
+    return '${map.get(value) ?? value}';
+  }
+}
diff --git a/source/funkin/ui/options/items/NumberPreferenceItem.hx b/source/funkin/ui/options/items/NumberPreferenceItem.hx
new file mode 100644
index 000000000..f3cd3cd46
--- /dev/null
+++ b/source/funkin/ui/options/items/NumberPreferenceItem.hx
@@ -0,0 +1,136 @@
+package funkin.ui.options.items;
+
+import funkin.ui.TextMenuList;
+import funkin.ui.AtlasText;
+import funkin.input.Controls;
+
+/**
+ * Preference item that allows the player to pick a value between min and max
+ */
+class NumberPreferenceItem extends TextMenuItem
+{
+  function controls():Controls
+  {
+    return PlayerSettings.player1.controls;
+  }
+
+  // Widgets
+  public var lefthandText:AtlasText;
+
+  // Constants
+  static final HOLD_DELAY:Float = 0.3; // seconds
+  static final CHANGE_RATE:Float = 0.08; // seconds
+
+  // Constructor-initialized variables
+  public var currentValue:Float;
+  public var min:Float;
+  public var max:Float;
+  public var step:Float;
+  public var precision:Int;
+  public var onChangeCallback:Null<Float->Void>;
+  public var valueFormatter:Null<Float->String>;
+
+  // Variables
+  var holdDelayTimer:Float = HOLD_DELAY; // seconds
+  var changeRateTimer:Float = 0.0; // seconds
+
+  /**
+   * @param min Minimum value (example: 0)
+   * @param max Maximum value (example: 100)
+   * @param step The value to increment/decrement by (example: 10)
+   * @param callback Will get called every time the user changes the setting; use this to apply/save the setting.
+   * @param valueFormatter Will get called every time the game needs to display the float value; use this to change how the displayed string looks
+   */
+  public function new(x:Float, y:Float, name:String, defaultValue:Float, min:Float, max:Float, step:Float, precision:Int, ?callback:Float->Void,
+      ?valueFormatter:Float->String):Void
+  {
+    super(x, y, name, function() {
+      callback(this.currentValue);
+    });
+    lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT);
+
+    updateHitbox();
+
+    this.currentValue = defaultValue;
+    this.min = min;
+    this.max = max;
+    this.step = step;
+    this.precision = precision;
+    this.onChangeCallback = callback;
+    this.valueFormatter = valueFormatter;
+  }
+
+  override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    // var fancyTextFancyColor:Color;
+    if (selected)
+    {
+      holdDelayTimer -= elapsed;
+      if (holdDelayTimer <= 0.0)
+      {
+        changeRateTimer -= elapsed;
+      }
+
+      var jpLeft:Bool = controls().UI_LEFT_P;
+      var jpRight:Bool = controls().UI_RIGHT_P;
+
+      if (jpLeft || jpRight)
+      {
+        holdDelayTimer = HOLD_DELAY;
+        changeRateTimer = 0.0;
+      }
+
+      var shouldDecrease:Bool = jpLeft;
+      var shouldIncrease:Bool = jpRight;
+
+      if (controls().UI_LEFT && holdDelayTimer <= 0.0 && changeRateTimer <= 0.0)
+      {
+        shouldDecrease = true;
+        changeRateTimer = CHANGE_RATE;
+      }
+      else if (controls().UI_RIGHT && holdDelayTimer <= 0.0 && changeRateTimer <= 0.0)
+      {
+        shouldIncrease = true;
+        changeRateTimer = CHANGE_RATE;
+      }
+
+      // Actually increasing/decreasing the value
+      if (shouldDecrease)
+      {
+        var isBelowMin:Bool = currentValue - step < min;
+        currentValue = (currentValue - step).clamp(min, max);
+        if (onChangeCallback != null && !isBelowMin) onChangeCallback(currentValue);
+      }
+      else if (shouldIncrease)
+      {
+        var isAboveMax:Bool = currentValue + step > max;
+        currentValue = (currentValue + step).clamp(min, max);
+        if (onChangeCallback != null && !isAboveMax) onChangeCallback(currentValue);
+      }
+    }
+
+    lefthandText.text = formatted(currentValue);
+  }
+
+  /** Turns the float into a string */
+  function formatted(value:Float):String
+  {
+    var float:Float = toFixed(value);
+    if (valueFormatter != null)
+    {
+      return valueFormatter(float);
+    }
+    else
+    {
+      return '${float}';
+    }
+  }
+
+  function toFixed(value:Float):Float
+  {
+    var multiplier:Float = Math.pow(10, precision);
+    return Math.floor(value * multiplier) / multiplier;
+  }
+}

From cd08116aed64f4761087e287a00786b77f40d02b Mon Sep 17 00:00:00 2001
From: FlooferLand! <76737186+FlooferLand@users.noreply.github.com>
Date: Mon, 1 Jul 2024 14:44:23 +0300
Subject: [PATCH 06/64] Added getWidth

---
 source/funkin/ui/AtlasText.hx | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/source/funkin/ui/AtlasText.hx b/source/funkin/ui/AtlasText.hx
index 186d87c2a..ef74abc1e 100644
--- a/source/funkin/ui/AtlasText.hx
+++ b/source/funkin/ui/AtlasText.hx
@@ -152,6 +152,32 @@ class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
     }
   }
 
+  public function getWidth():Int
+  {
+    var width = 0;
+    for (char in this.text.split(""))
+    {
+      switch (char)
+      {
+        case " ":
+          {
+            width += 40;
+          }
+        case "\n":
+          {}
+        case char:
+          {
+            var sprite = new AtlasChar(atlas, char);
+            sprite.revive();
+            sprite.char = char;
+            sprite.alpha = 1;
+            width += Std.int(sprite.width);
+          }
+      }
+    }
+    return width;
+  }
+
   override function toString()
   {
     return "InputItem, " + FlxStringUtil.getDebugString([

From 4044a7d9360a391c59878bbca65ab65e5b69e16d Mon Sep 17 00:00:00 2001
From: Hundrec <hundrecard@gmail.com>
Date: Fri, 14 Jun 2024 21:52:52 -0700
Subject: [PATCH 07/64] Reorder download Git step in compiling guide
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Moved “download Git” from the middle of the guide to the setup step
Should prevent errors with Git before installing Git
---
 docs/COMPILING.md | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/docs/COMPILING.md b/docs/COMPILING.md
index e7c19875a..b8ddee4a7 100644
--- a/docs/COMPILING.md
+++ b/docs/COMPILING.md
@@ -2,14 +2,14 @@
 
 0. Setup
     - Download Haxe from [Haxe.org](https://haxe.org)
+    - Download Git from [git-scm.com](https://www.git-scm.com)
 1. Cloning the Repository: Make sure when you clone, you clone the submodules to get the assets repo:
     - `git clone --recurse-submodules https://github.com/FunkinCrew/funkin.git`
     - If you accidentally cloned without the `assets` submodule (aka didn't follow the step above), you can run `git submodule update --init --recursive` to get the assets in a foolproof way.
 2. Install `hmm` (run `haxelib --global install hmm` and then `haxelib --global run hmm setup`)
-3. Download Git from [git-scm.com](https://www.git-scm.com)
-4. Install all haxelibs of the current branch by running `hmm install`
-5. Setup lime: `haxelib run lime setup`
-6. Platform setup
+3. Install all haxelibs of the current branch by running `hmm install`
+4. Setup lime: `haxelib run lime setup`
+5. Platform setup
    - For Windows, download the [Visual Studio Build Tools](https://aka.ms/vs/17/release/vs_BuildTools.exe)
         - When prompted, select "Individual Components" and make sure to download the following:
         - MSVC v143 VS 2022 C++ x64/x86 build tools
@@ -17,8 +17,8 @@
     - Mac: [`lime setup mac` Documentation](https://lime.openfl.org/docs/advanced-setup/macos/)
     - Linux: [`lime setup linux` Documentation](https://lime.openfl.org/docs/advanced-setup/linux/)
     - HTML5: Compiles without any extra setup
-7. If you are targeting for native, you may need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug`
-8. `lime test PLATFORM` ! Add `-debug` to enable several debug features such as time travel (`PgUp`/`PgDn` in Play State).
+6. If you are targeting for native, you may need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug`
+7. `lime test PLATFORM` ! Add `-debug` to enable several debug features such as time travel (`PgUp`/`PgDn` in Play State).
 
 # Troubleshooting
 

From 562136ed554d73e04a015c32f9cb34b790b9cc07 Mon Sep 17 00:00:00 2001
From: Hundrec <hundrecard@gmail.com>
Date: Sat, 15 Jun 2024 13:37:21 -0400
Subject: [PATCH 08/64] Add ZIP button warning and restructured sentences

Each step should be easier to follow with this structure
---
 docs/COMPILING.md | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/docs/COMPILING.md b/docs/COMPILING.md
index b8ddee4a7..b2a106c86 100644
--- a/docs/COMPILING.md
+++ b/docs/COMPILING.md
@@ -3,12 +3,13 @@
 0. Setup
     - Download Haxe from [Haxe.org](https://haxe.org)
     - Download Git from [git-scm.com](https://www.git-scm.com)
-1. Cloning the Repository: Make sure when you clone, you clone the submodules to get the assets repo:
-    - `git clone --recurse-submodules https://github.com/FunkinCrew/funkin.git`
-    - If you accidentally cloned without the `assets` submodule (aka didn't follow the step above), you can run `git submodule update --init --recursive` to get the assets in a foolproof way.
-2. Install `hmm` (run `haxelib --global install hmm` and then `haxelib --global run hmm setup`)
-3. Install all haxelibs of the current branch by running `hmm install`
-4. Setup lime: `haxelib run lime setup`
+    - Do NOT download the repository using the Download ZIP button on GitHub or you may run into errors!
+    - Instead, open a command prompt and do the following steps...
+1. Run `git clone --recurse-submodules https://github.com/FunkinCrew/funkin.git` to clone the repository with the necessary assets submodule
+    - _If you accidentally cloned without the `assets` submodule (aka didn't follow the step above), you can run `git submodule update --init --recursive` to get the assets in a foolproof way._
+2. Run `haxelib --global install hmm` and then `haxelib --global run hmm setup` to install hmm.json
+3. Run `hmm install` to install all haxelibs of the current branch
+4. Run `haxelib run lime setup` to set up lime
 5. Platform setup
    - For Windows, download the [Visual Studio Build Tools](https://aka.ms/vs/17/release/vs_BuildTools.exe)
         - When prompted, select "Individual Components" and make sure to download the following:

From 6e65996180de85c42b74f6730773975f819e2efa Mon Sep 17 00:00:00 2001
From: Eric <ericmyllyoja@gmail.com>
Date: Sun, 16 Jun 2024 18:48:01 -0400
Subject: [PATCH 09/64] Make downloading the assets submodule a separate step.

---
 docs/COMPILING.md | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/docs/COMPILING.md b/docs/COMPILING.md
index b2a106c86..cc90bd348 100644
--- a/docs/COMPILING.md
+++ b/docs/COMPILING.md
@@ -5,8 +5,9 @@
     - Download Git from [git-scm.com](https://www.git-scm.com)
     - Do NOT download the repository using the Download ZIP button on GitHub or you may run into errors!
     - Instead, open a command prompt and do the following steps...
-1. Run `git clone --recurse-submodules https://github.com/FunkinCrew/funkin.git` to clone the repository with the necessary assets submodule
-    - _If you accidentally cloned without the `assets` submodule (aka didn't follow the step above), you can run `git submodule update --init --recursive` to get the assets in a foolproof way._
+1. Run `git clone https://github.com/FunkinCrew/funkin.git` to clone the base repository.
+2. Run `git submodule update --init --recursive` to download the game's assets.
+    - NOTE: By performing this operation, you are downloading Content which is proprietary and protected by national and international copyright and trademark laws. See [the LICENSE.md file for the Funkin.assets](https://github.com/FunkinCrew/funkin.assets/blob/main/LICENSE.md) repo for more information.
 2. Run `haxelib --global install hmm` and then `haxelib --global run hmm setup` to install hmm.json
 3. Run `hmm install` to install all haxelibs of the current branch
 4. Run `haxelib run lime setup` to set up lime

From 3f63bc35a4f27a51f07a5c1ac8053de2512d1757 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 25 Jun 2024 18:22:30 -0400
Subject: [PATCH 10/64] Add change counts labels to Actions labeler

---
 .github/changed-lines-count-labeler.yml | 12 ++++++++++++
 .github/workflows/labeler.yml           | 15 ++++++++++++++-
 2 files changed, 26 insertions(+), 1 deletion(-)
 create mode 100644 .github/changed-lines-count-labeler.yml

diff --git a/.github/changed-lines-count-labeler.yml b/.github/changed-lines-count-labeler.yml
new file mode 100644
index 000000000..6f890f534
--- /dev/null
+++ b/.github/changed-lines-count-labeler.yml
@@ -0,0 +1,12 @@
+# Add 'small' to any changes below 10 lines
+small:
+  max: 9
+
+# Add 'medium' to any changes between 10 and 100 lines
+medium:
+  min: 10
+  max: 99
+
+# Add 'large' to any changes for more than 100 lines
+large:
+  min: 100
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
index 0bcc420d3..a861af578 100644
--- a/.github/workflows/labeler.yml
+++ b/.github/workflows/labeler.yml
@@ -9,6 +9,19 @@ jobs:
       pull-requests: write
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/labeler@v5
+    - name: Set basic labels
+      uses: actions/labeler@v5
       with:
         sync-labels: true
+  changed-lines-count-labeler:
+    permissions:
+      contents: read
+      pull-requests: write
+    runs-on: ubuntu-latest
+    name: An action for automatically labelling pull requests based on the changed lines count
+    steps:
+    - name: Set change count labels
+      uses: vkirilichev/changed-lines-count-labeler@v0.2
+      with:
+        repo-token: ${{ secrets.GITHUB_TOKEN }}
+        configuration-path: .github/changed-lines-count-labeler.yml

From 7b9e4a054284dbe6e69c1659e04571247276a599 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Wed, 10 Jul 2024 00:23:06 +0200
Subject: [PATCH 11/64] Fix F5 chart not reloading

---
 source/funkin/play/PlayState.hx               | 59 ++-----------------
 source/funkin/ui/MusicBeatState.hx            |  7 +--
 source/funkin/ui/MusicBeatSubState.hx         |  5 +-
 .../util/plugins/ReloadAssetsDebugPlugin.hx   | 14 ++++-
 4 files changed, 17 insertions(+), 68 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index a4723611e..86e8e0241 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1347,64 +1347,13 @@ class PlayState extends MusicBeatSubState
   }
 
   /**
-   * Removes any references to the current stage, then clears the stage cache,
-   * then reloads all the stages.
-   *
-   * This is useful for when you want to edit a stage without reloading the whole game.
-   * Reloading works on both the JSON and the HXC, if applicable.
-   *
    * Call this by pressing F5 on a debug build.
    */
-  override function debug_refreshModules():Void
+  override function reloadAssets():Void
   {
-    // Prevent further gameplay updates, which will try to reference dead objects.
-    criticalFailure = true;
-
-    // Remove the current stage. If the stage gets deleted while it's still in use,
-    // it'll probably crash the game or something.
-    if (this.currentStage != null)
-    {
-      remove(currentStage);
-      var event:ScriptEvent = new ScriptEvent(DESTROY, false);
-      ScriptEventDispatcher.callEvent(currentStage, event);
-      currentStage = null;
-    }
-
-    if (!overrideMusic)
-    {
-      // Stop the instrumental.
-      if (FlxG.sound.music != null)
-      {
-        FlxG.sound.music.destroy();
-        FlxG.sound.music = null;
-      }
-
-      // Stop the vocals.
-      if (vocals != null && vocals.exists)
-      {
-        vocals.destroy();
-        vocals = null;
-      }
-    }
-    else
-    {
-      // Stop the instrumental.
-      if (FlxG.sound.music != null)
-      {
-        FlxG.sound.music.stop();
-      }
-
-      // Stop the vocals.
-      if (vocals != null && vocals.exists)
-      {
-        vocals.stop();
-      }
-    }
-
-    super.debug_refreshModules();
-
-    var event:ScriptEvent = new ScriptEvent(CREATE, false);
-    ScriptEventDispatcher.callEvent(currentSong, event);
+    funkin.modding.PolymodHandler.forceReloadAssets();
+    lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id);
+    LoadingState.loadPlayState(lastParams);
   }
 
   override function stepHit():Bool
diff --git a/source/funkin/ui/MusicBeatState.hx b/source/funkin/ui/MusicBeatState.hx
index 92169df75..8668b64c1 100644
--- a/source/funkin/ui/MusicBeatState.hx
+++ b/source/funkin/ui/MusicBeatState.hx
@@ -78,9 +78,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
   {
     // Emergency exit button.
     if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
-
-    // This can now be used in EVERY STATE YAY!
-    if (FlxG.keys.justPressed.F5) debug_refreshModules();
   }
 
   override function update(elapsed:Float)
@@ -114,12 +111,10 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
     ModuleHandler.callEvent(event);
   }
 
-  function debug_refreshModules()
+  function reloadAssets()
   {
     PolymodHandler.forceReloadAssets();
 
-    this.destroy();
-
     // Create a new instance of the current state, so old data is cleared.
     FlxG.resetState();
   }
diff --git a/source/funkin/ui/MusicBeatSubState.hx b/source/funkin/ui/MusicBeatSubState.hx
index 9035d12ff..5c40b37bc 100644
--- a/source/funkin/ui/MusicBeatSubState.hx
+++ b/source/funkin/ui/MusicBeatSubState.hx
@@ -72,9 +72,6 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
     // Emergency exit button.
     if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
 
-    // This can now be used in EVERY STATE YAY!
-    if (FlxG.keys.justPressed.F5) debug_refreshModules();
-
     // Display Conductor info in the watch window.
     FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0);
     Conductor.watchQuick(conductorInUse);
@@ -82,7 +79,7 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
     dispatchEvent(new UpdateScriptEvent(elapsed));
   }
 
-  function debug_refreshModules()
+  function reloadAssets()
   {
     PolymodHandler.forceReloadAssets();
 
diff --git a/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx b/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx
index f69609531..0e1e238ac 100644
--- a/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx
+++ b/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx
@@ -1,6 +1,9 @@
 package funkin.util.plugins;
 
+import flixel.FlxG;
 import flixel.FlxBasic;
+import funkin.ui.MusicBeatState;
+import funkin.ui.MusicBeatSubState;
 
 /**
  * A plugin which adds functionality to press `F5` to reload all game assets, then reload the current state.
@@ -28,10 +31,15 @@ class ReloadAssetsDebugPlugin extends FlxBasic
     if (FlxG.keys.justPressed.F5)
     #end
     {
-      funkin.modding.PolymodHandler.forceReloadAssets();
+      var state:Dynamic = FlxG.state;
+      if (state is MusicBeatState || state is MusicBeatSubState) state.reloadAssets();
+      else
+      {
+        funkin.modding.PolymodHandler.forceReloadAssets();
 
-      // Create a new instance of the current state, so old data is cleared.
-      FlxG.resetState();
+        // Create a new instance of the current state, so old data is cleared.
+        FlxG.resetState();
+      }
     }
   }
 

From 18b795d4f74e81e8f9bbabd3e3e3e0fe7930a167 Mon Sep 17 00:00:00 2001
From: anysad <anysadiscool@gmail.com>
Date: Thu, 11 Jul 2024 18:10:45 +0300
Subject: [PATCH 12/64] Add HEY! song events to Tutorial

---
 source/funkin/play/PlayState.hx | 12 +-----------
 1 file changed, 1 insertion(+), 11 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 8d7d82aab..216acca71 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1502,7 +1502,7 @@ class PlayState extends MusicBeatSubState
     if (opponentStrumline != null) opponentStrumline.onBeatHit();
 
     // Make the characters dance on the beat
-    danceOnBeat();
+    //danceOnBeat();
 
     return true;
   }
@@ -1522,16 +1522,6 @@ class PlayState extends MusicBeatSubState
   function danceOnBeat():Void
   {
     if (currentStage == null) return;
-
-    // TODO: Add HEY! song events to Tutorial.
-    if (Conductor.instance.currentBeat % 16 == 15
-      && currentStage.getDad().characterId == 'gf'
-      && Conductor.instance.currentBeat > 16
-      && Conductor.instance.currentBeat < 48)
-    {
-      currentStage.getBoyfriend().playAnimation('hey', true);
-      currentStage.getDad().playAnimation('cheer', true);
-    }
   }
 
   /**

From 305ab3146fe8891e925d34f5f2aafd9006ce1288 Mon Sep 17 00:00:00 2001
From: anysad <anysadiscool@gmail.com>
Date: Thu, 11 Jul 2024 18:12:50 +0300
Subject: [PATCH 13/64] Add HEY! song events to Tutorial

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

diff --git a/assets b/assets
index 005c96f85..81e61c287 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 005c96f85f4304865acb196e7cc4d6d83f9d76d8
+Subproject commit 81e61c287670f6aa8b7faf2c27561df57361f1ad

From 4a7545a0dbdf7e8e1f1acca59940c87d59151646 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Thu, 11 Jul 2024 21:45:06 -0400
Subject: [PATCH 14/64] Remove empty function.

---
 source/funkin/play/PlayState.hx | 13 -------------
 1 file changed, 13 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 216acca71..309c21438 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1501,9 +1501,6 @@ class PlayState extends MusicBeatSubState
     if (playerStrumline != null) playerStrumline.onBeatHit();
     if (opponentStrumline != null) opponentStrumline.onBeatHit();
 
-    // Make the characters dance on the beat
-    //danceOnBeat();
-
     return true;
   }
 
@@ -1514,16 +1511,6 @@ class PlayState extends MusicBeatSubState
     super.destroy();
   }
 
-  /**
-   * Handles characters dancing to the beat of the current song.
-   *
-   * TODO: Move some of this logic into `Bopper.hx`, or individual character scripts.
-   */
-  function danceOnBeat():Void
-  {
-    if (currentStage == null) return;
-  }
-
   /**
    * Initializes the game and HUD cameras.
    */

From 90155bcfbfa86561571b7c556b1f63486c18877d Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Wed, 10 Jul 2024 01:26:16 +0200
Subject: [PATCH 15/64] ChartEditor Live Input Code Refactor + 6 key fix

---
 .../ui/debug/charting/ChartEditorState.hx     | 57 ++++++-------------
 1 file changed, 18 insertions(+), 39 deletions(-)

diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index f72cca77f..5e7493840 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -282,6 +282,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
    */
   public static final WELCOME_MUSIC_FADE_IN_DURATION:Float = 10.0;
 
+  /**
+   * A map of the keys for every live input style.
+   */
+  public static final LIVE_INPUT_KEYS:Map<ChartEditorLiveInputStyle, Array<FlxKey>> = [
+    NumberKeys => [
+      FIVE, SIX, SEVEN, EIGHT,
+       ONE, TWO, THREE,  FOUR
+    ],
+    WASDKeys => [
+      LEFT, DOWN, UP, RIGHT,
+         A,    S,  W,     D
+    ],
+    None => []
+  ];
+
   /**
    * INSTANCE DATA
    */
@@ -5129,46 +5144,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
   function handlePlayhead():Void
   {
     // Place notes at the playhead with the keyboard.
-    switch (currentLiveInputStyle)
+    for (note => key in LIVE_INPUT_KEYS[currentLiveInputStyle])
     {
-      case ChartEditorLiveInputStyle.WASDKeys:
-        if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4);
-        if (FlxG.keys.justReleased.A) finishPlaceNoteAtPlayhead(4);
-        if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5);
-        if (FlxG.keys.justReleased.S) finishPlaceNoteAtPlayhead(5);
-        if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6);
-        if (FlxG.keys.justReleased.W) finishPlaceNoteAtPlayhead(6);
-        if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7);
-        if (FlxG.keys.justReleased.D) finishPlaceNoteAtPlayhead(7);
-
-        if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0);
-        if (FlxG.keys.justReleased.LEFT) finishPlaceNoteAtPlayhead(0);
-        if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1);
-        if (FlxG.keys.justReleased.DOWN) finishPlaceNoteAtPlayhead(1);
-        if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2);
-        if (FlxG.keys.justReleased.UP) finishPlaceNoteAtPlayhead(2);
-        if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3);
-        if (FlxG.keys.justReleased.RIGHT) finishPlaceNoteAtPlayhead(3);
-      case ChartEditorLiveInputStyle.NumberKeys:
-        // Flipped because Dad is on the left but represents data 0-3.
-        if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4);
-        if (FlxG.keys.justReleased.ONE) finishPlaceNoteAtPlayhead(4);
-        if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5);
-        if (FlxG.keys.justReleased.TWO) finishPlaceNoteAtPlayhead(5);
-        if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6);
-        if (FlxG.keys.justReleased.THREE) finishPlaceNoteAtPlayhead(6);
-        if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7);
-        if (FlxG.keys.justReleased.FOUR) finishPlaceNoteAtPlayhead(7);
-
-        if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0);
-        if (FlxG.keys.justReleased.FIVE) finishPlaceNoteAtPlayhead(0);
-        if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1);
-        if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2);
-        if (FlxG.keys.justReleased.SEVEN) finishPlaceNoteAtPlayhead(2);
-        if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3);
-        if (FlxG.keys.justReleased.EIGHT) finishPlaceNoteAtPlayhead(3);
-      case ChartEditorLiveInputStyle.None:
-        // Do nothing.
+      if (FlxG.keys.checkStatus(key, JUST_PRESSED)) placeNoteAtPlayhead(note)
+      else if (FlxG.keys.checkStatus(key, JUST_RELEASED)) finishPlaceNoteAtPlayhead(note);
     }
 
     // Place events at playhead.

From 17f5a06256b34ff5224fde5b893cdac7c5086a5f Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Mon, 20 May 2024 02:56:57 +0200
Subject: [PATCH 16/64] Add camOther to fix zooms on pause and stickers

---
 source/funkin/play/PlayState.hx | 19 +++++++++----------
 1 file changed, 9 insertions(+), 10 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 8d7d82aab..e87fde90c 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -503,9 +503,9 @@ class PlayState extends MusicBeatSubState
   public var camGame:FlxCamera;
 
   /**
-   * The camera which contains, and controls visibility of, a video cutscene.
+   * The camera which contains, and controls visibility of, a video cutscene, dialogue, pause menu and sticker transition.
    */
-  public var camCutscene:FlxCamera;
+  public var camOther:FlxCamera;
 
   /**
    * The combo popups. Includes the real-time combo counter and the rating.
@@ -975,7 +975,7 @@ class PlayState extends MusicBeatSubState
 
           FlxTransitionableState.skipNextTransIn = true;
           FlxTransitionableState.skipNextTransOut = true;
-          pauseSubState.camera = camHUD;
+          pauseSubState.camera = camOther;
           openSubState(pauseSubState);
           // boyfriendPos.put(); // TODO: Why is this here?
         }
@@ -1543,12 +1543,12 @@ class PlayState extends MusicBeatSubState
     camGame.bgColor = BACKGROUND_COLOR; // Show a pink background behind the stage.
     camHUD = new FlxCamera();
     camHUD.bgColor.alpha = 0; // Show the game scene behind the camera.
-    camCutscene = new FlxCamera();
-    camCutscene.bgColor.alpha = 0; // Show the game scene behind the camera.
+    camOther = new FlxCamera();
+    camOther.bgColor.alpha = 0; // Show the game scene behind the camera.
 
     FlxG.cameras.reset(camGame);
     FlxG.cameras.add(camHUD, false);
-    FlxG.cameras.add(camCutscene, false);
+    FlxG.cameras.add(camOther, false);
 
     // Configure camera follow point.
     if (previousCameraFollowPoint != null)
@@ -1934,7 +1934,6 @@ class PlayState extends MusicBeatSubState
     if (!result) return;
 
     isInCutscene = false;
-    camCutscene.visible = false;
 
     // TODO: Maybe tween in the camera after any cutscenes.
     camHUD.visible = true;
@@ -1953,7 +1952,7 @@ class PlayState extends MusicBeatSubState
     if (!currentConversation.alive) currentConversation.revive();
 
     currentConversation.completeCallback = onConversationComplete;
-    currentConversation.cameras = [camCutscene];
+    currentConversation.cameras = [camOther];
     currentConversation.zIndex = 1000;
     add(currentConversation);
     refresh();
@@ -2788,7 +2787,7 @@ class PlayState extends MusicBeatSubState
         persistentUpdate = false;
         FlxTransitionableState.skipNextTransIn = true;
         FlxTransitionableState.skipNextTransOut = true;
-        pauseSubState.camera = camCutscene;
+        pauseSubState.camera = camOther;
         openSubState(pauseSubState);
       }
     }
@@ -2804,7 +2803,7 @@ class PlayState extends MusicBeatSubState
         persistentUpdate = false;
         FlxTransitionableState.skipNextTransIn = true;
         FlxTransitionableState.skipNextTransOut = true;
-        pauseSubState.camera = camCutscene;
+        pauseSubState.camera = camOther;
         openSubState(pauseSubState);
       }
     }

From e23e6c160d9ed9243a380c4b6842a059a3954768 Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Mon, 20 May 2024 23:52:45 +0200
Subject: [PATCH 17/64] Fix references to camCutscene

---
 source/funkin/play/cutscene/VideoCutscene.hx | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/source/funkin/play/cutscene/VideoCutscene.hx b/source/funkin/play/cutscene/VideoCutscene.hx
index abbcd4f54..7dac97885 100644
--- a/source/funkin/play/cutscene/VideoCutscene.hx
+++ b/source/funkin/play/cutscene/VideoCutscene.hx
@@ -81,12 +81,11 @@ class VideoCutscene
     // Trigger the cutscene. Don't play the song in the background.
     PlayState.instance.isInCutscene = true;
     PlayState.instance.camHUD.visible = false;
-    PlayState.instance.camCutscene.visible = true;
 
     // Display a black screen to hide the game while the video is playing.
     blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
     blackScreen.scrollFactor.set(0, 0);
-    blackScreen.cameras = [PlayState.instance.camCutscene];
+    blackScreen.cameras = [PlayState.instance.camOther];
     PlayState.instance.add(blackScreen);
 
     VideoCutscene.cutsceneType = cutsceneType;
@@ -120,7 +119,7 @@ class VideoCutscene
 
       vid.finishCallback = finishVideo.bind(0.5);
 
-      vid.cameras = [PlayState.instance.camCutscene];
+      vid.cameras = [PlayState.instance.camOther];
 
       PlayState.instance.add(vid);
 
@@ -147,7 +146,7 @@ class VideoCutscene
       vid.bitmap.onEndReached.add(finishVideo.bind(0.5));
       vid.autoPause = FlxG.autoPause;
 
-      vid.cameras = [PlayState.instance.camCutscene];
+      vid.cameras = [PlayState.instance.camOther];
 
       PlayState.instance.add(vid);
 
@@ -305,7 +304,6 @@ class VideoCutscene
     vid = null;
     #end
 
-    PlayState.instance.camCutscene.visible = true;
     PlayState.instance.camHUD.visible = true;
 
     FlxTween.tween(blackScreen, {alpha: 0}, transitionTime,

From e6c97678002e1b4ba823b71471ec0d6052d96c0a Mon Sep 17 00:00:00 2001
From: gamerbross <55158797+gamerbross@users.noreply.github.com>
Date: Wed, 12 Jun 2024 00:34:04 +0200
Subject: [PATCH 18/64] Revert camCutscene rename

---
 source/funkin/play/PlayState.hx              | 16 ++++++++--------
 source/funkin/play/cutscene/VideoCutscene.hx |  6 +++---
 2 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index e87fde90c..0bb57b8cb 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -505,7 +505,7 @@ class PlayState extends MusicBeatSubState
   /**
    * The camera which contains, and controls visibility of, a video cutscene, dialogue, pause menu and sticker transition.
    */
-  public var camOther:FlxCamera;
+  public var camCutscene:FlxCamera;
 
   /**
    * The combo popups. Includes the real-time combo counter and the rating.
@@ -975,7 +975,7 @@ class PlayState extends MusicBeatSubState
 
           FlxTransitionableState.skipNextTransIn = true;
           FlxTransitionableState.skipNextTransOut = true;
-          pauseSubState.camera = camOther;
+          pauseSubState.camera = camCutscene;
           openSubState(pauseSubState);
           // boyfriendPos.put(); // TODO: Why is this here?
         }
@@ -1543,12 +1543,12 @@ class PlayState extends MusicBeatSubState
     camGame.bgColor = BACKGROUND_COLOR; // Show a pink background behind the stage.
     camHUD = new FlxCamera();
     camHUD.bgColor.alpha = 0; // Show the game scene behind the camera.
-    camOther = new FlxCamera();
-    camOther.bgColor.alpha = 0; // Show the game scene behind the camera.
+    camCutscene = new FlxCamera();
+    camCutscene.bgColor.alpha = 0; // Show the game scene behind the camera.
 
     FlxG.cameras.reset(camGame);
     FlxG.cameras.add(camHUD, false);
-    FlxG.cameras.add(camOther, false);
+    FlxG.cameras.add(camCutscene, false);
 
     // Configure camera follow point.
     if (previousCameraFollowPoint != null)
@@ -1952,7 +1952,7 @@ class PlayState extends MusicBeatSubState
     if (!currentConversation.alive) currentConversation.revive();
 
     currentConversation.completeCallback = onConversationComplete;
-    currentConversation.cameras = [camOther];
+    currentConversation.cameras = [camCutscene];
     currentConversation.zIndex = 1000;
     add(currentConversation);
     refresh();
@@ -2787,7 +2787,7 @@ class PlayState extends MusicBeatSubState
         persistentUpdate = false;
         FlxTransitionableState.skipNextTransIn = true;
         FlxTransitionableState.skipNextTransOut = true;
-        pauseSubState.camera = camOther;
+        pauseSubState.camera = camCutscene;
         openSubState(pauseSubState);
       }
     }
@@ -2803,7 +2803,7 @@ class PlayState extends MusicBeatSubState
         persistentUpdate = false;
         FlxTransitionableState.skipNextTransIn = true;
         FlxTransitionableState.skipNextTransOut = true;
-        pauseSubState.camera = camOther;
+        pauseSubState.camera = camCutscene;
         openSubState(pauseSubState);
       }
     }
diff --git a/source/funkin/play/cutscene/VideoCutscene.hx b/source/funkin/play/cutscene/VideoCutscene.hx
index 7dac97885..60454b881 100644
--- a/source/funkin/play/cutscene/VideoCutscene.hx
+++ b/source/funkin/play/cutscene/VideoCutscene.hx
@@ -85,7 +85,7 @@ class VideoCutscene
     // Display a black screen to hide the game while the video is playing.
     blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
     blackScreen.scrollFactor.set(0, 0);
-    blackScreen.cameras = [PlayState.instance.camOther];
+    blackScreen.cameras = [PlayState.instance.camCutscene];
     PlayState.instance.add(blackScreen);
 
     VideoCutscene.cutsceneType = cutsceneType;
@@ -119,7 +119,7 @@ class VideoCutscene
 
       vid.finishCallback = finishVideo.bind(0.5);
 
-      vid.cameras = [PlayState.instance.camOther];
+      vid.cameras = [PlayState.instance.camCutscene];
 
       PlayState.instance.add(vid);
 
@@ -146,7 +146,7 @@ class VideoCutscene
       vid.bitmap.onEndReached.add(finishVideo.bind(0.5));
       vid.autoPause = FlxG.autoPause;
 
-      vid.cameras = [PlayState.instance.camOther];
+      vid.cameras = [PlayState.instance.camCutscene];
 
       PlayState.instance.add(vid);
 

From a7482410b91e82fbd7b7cf37a123b0e7aaa72fec Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Fri, 31 May 2024 14:02:28 +0200
Subject: [PATCH 19/64] add note kind scripts

---
 source/funkin/InitState.hx                    |  3 ++
 source/funkin/play/PlayState.hx               |  8 +++-
 .../play/notes/notekind/NoteKindScript.hx     | 45 ++++++++++++++++++
 .../notes/notekind/NoteKindScriptManager.hx   | 46 +++++++++++++++++++
 .../notes/notekind/ScriptedNoteKindScript.hx  |  9 ++++
 .../charting/util/ChartEditorDropdowns.hx     |  2 +-
 6 files changed, 111 insertions(+), 2 deletions(-)
 create mode 100644 source/funkin/play/notes/notekind/NoteKindScript.hx
 create mode 100644 source/funkin/play/notes/notekind/NoteKindScriptManager.hx
 create mode 100644 source/funkin/play/notes/notekind/ScriptedNoteKindScript.hx

diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 6e370b5ff..a339d2655 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -27,6 +27,7 @@ import funkin.data.dialogue.speaker.SpeakerRegistry;
 import funkin.data.freeplay.album.AlbumRegistry;
 import funkin.data.song.SongRegistry;
 import funkin.play.character.CharacterData.CharacterDataParser;
+import funkin.play.notes.notekind.NoteKindScriptManager;
 import funkin.modding.module.ModuleHandler;
 import funkin.ui.title.TitleState;
 import funkin.util.CLIUtil;
@@ -176,6 +177,8 @@ class InitState extends FlxState
     // Move it to use a BaseRegistry.
     CharacterDataParser.loadCharacterCache();
 
+    NoteKindScriptManager.loadScripts();
+
     ModuleHandler.buildModuleCallbacks();
     ModuleHandler.loadModuleCache();
     ModuleHandler.callOnCreate();
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 8d7d82aab..93306e9d5 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -49,6 +49,7 @@ import funkin.play.notes.NoteSprite;
 import funkin.play.notes.notestyle.NoteStyle;
 import funkin.play.notes.Strumline;
 import funkin.play.notes.SustainTrail;
+import funkin.play.notes.notekind.NoteKindScriptManager;
 import funkin.play.scoring.Scoring;
 import funkin.play.song.Song;
 import funkin.play.stage.Stage;
@@ -1177,7 +1178,12 @@ class PlayState extends MusicBeatSubState
     // Dispatch event to conversation script.
     ScriptEventDispatcher.callEvent(currentConversation, event);
 
-    // TODO: Dispatch event to note scripts
+    // Dispatch event to note script
+    if (Std.isOfType(event, NoteScriptEvent))
+    {
+      var noteEvent:NoteScriptEvent = cast(event, NoteScriptEvent);
+      NoteKindScriptManager.callEvent(noteEvent.note.noteData.kind, noteEvent);
+    }
   }
 
   /**
diff --git a/source/funkin/play/notes/notekind/NoteKindScript.hx b/source/funkin/play/notes/notekind/NoteKindScript.hx
new file mode 100644
index 000000000..baa57b146
--- /dev/null
+++ b/source/funkin/play/notes/notekind/NoteKindScript.hx
@@ -0,0 +1,45 @@
+package funkin.play.notes.notekind;
+
+import funkin.modding.IScriptedClass.INoteScriptedClass;
+import funkin.modding.events.ScriptEvent;
+
+/**
+ * Class for note scripts
+ */
+class NoteKindScript implements INoteScriptedClass
+{
+  /**
+   * the name of the note kind
+   */
+  public var noteKind:String;
+
+  /**
+   * description used in chart editor
+   */
+  public var description:String = "";
+
+  public function new(noteKind:String, description:String = "")
+  {
+    this.noteKind = noteKind;
+    this.description = description;
+  }
+
+  public function toString():String
+  {
+    return noteKind;
+  }
+
+  public function onScriptEvent(event:ScriptEvent):Void {}
+
+  public function onCreate(event:ScriptEvent):Void {}
+
+  public function onDestroy(event:ScriptEvent):Void {}
+
+  public function onUpdate(event:UpdateScriptEvent):Void {}
+
+  public function onNoteIncoming(event:NoteScriptEvent):Void {}
+
+  public function onNoteHit(event:HitNoteScriptEvent):Void {}
+
+  public function onNoteMiss(event:NoteScriptEvent):Void {}
+}
diff --git a/source/funkin/play/notes/notekind/NoteKindScriptManager.hx b/source/funkin/play/notes/notekind/NoteKindScriptManager.hx
new file mode 100644
index 000000000..dc22732b6
--- /dev/null
+++ b/source/funkin/play/notes/notekind/NoteKindScriptManager.hx
@@ -0,0 +1,46 @@
+package funkin.play.notes.notekind;
+
+import funkin.modding.events.ScriptEventDispatcher;
+import funkin.modding.events.ScriptEvent;
+import funkin.ui.debug.charting.util.ChartEditorDropdowns;
+
+class NoteKindScriptManager
+{
+  static var noteKindScripts:Map<String, NoteKindScript> = [];
+
+  public static function loadScripts():Void
+  {
+    var scriptedClassName:Array<String> = ScriptedNoteKindScript.listScriptClasses();
+    if (scriptedClassName.length > 0)
+    {
+      trace('Instantiating ${scriptedClassName.length} scripted note kind...');
+      for (scriptedClass in scriptedClassName)
+      {
+        try
+        {
+          var script:NoteKindScript = ScriptedNoteKindScript.init(scriptedClass, "unknown");
+          trace(' Initialized scripted note kind: ${script.noteKind}');
+          noteKindScripts.set(script.noteKind, script);
+          ChartEditorDropdowns.NOTE_KINDS.set(script.noteKind, script.description);
+        }
+        catch (e)
+        {
+          trace(' FAILED to instantiate scripted note kind: ${scriptedClass}');
+          trace(e);
+        }
+      }
+    }
+  }
+
+  public static function callEvent(noteKind:String, event:ScriptEvent):Void
+  {
+    var noteKindScript:NoteKindScript = noteKindScripts.get(noteKind);
+
+    if (noteKindScript == null)
+    {
+      return;
+    }
+
+    ScriptEventDispatcher.callEvent(noteKindScript, event);
+  }
+}
diff --git a/source/funkin/play/notes/notekind/ScriptedNoteKindScript.hx b/source/funkin/play/notes/notekind/ScriptedNoteKindScript.hx
new file mode 100644
index 000000000..d54a0cde2
--- /dev/null
+++ b/source/funkin/play/notes/notekind/ScriptedNoteKindScript.hx
@@ -0,0 +1,9 @@
+package funkin.play.notes.notekind;
+
+/**
+ * A script that can be tied to a NoteKindScript.
+ * Create a scripted class that extends NoteKindScript,
+ * then call `super('noteKind')` in the constructor to use this.
+ */
+@:hscriptClass
+class ScriptedNoteKindScript extends NoteKindScript implements polymod.hscript.HScriptedClass {}
diff --git a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
index 55aab0ab0..f20b75650 100644
--- a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
+++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
@@ -146,7 +146,7 @@ class ChartEditorDropdowns
     return returnValue;
   }
 
-  static final NOTE_KINDS:Map<String, String> = [
+  public static final NOTE_KINDS:Map<String, String> = [
     // Base
     "" => "Default",
     "~CUSTOM~" => "Custom",

From 134b4678769de803b245c182fed9afe07fc29049 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Fri, 31 May 2024 16:55:42 +0200
Subject: [PATCH 20/64] rename stuff

---
 source/funkin/InitState.hx                       |  4 ++--
 source/funkin/play/PlayState.hx                  |  4 ++--
 .../notekind/{NoteKindScript.hx => NoteKind.hx}  |  2 +-
 ...teKindScriptManager.hx => NoteKindManager.hx} | 16 ++++++++--------
 .../play/notes/notekind/ScriptedNoteKind.hx      |  9 +++++++++
 .../notes/notekind/ScriptedNoteKindScript.hx     |  9 ---------
 6 files changed, 22 insertions(+), 22 deletions(-)
 rename source/funkin/play/notes/notekind/{NoteKindScript.hx => NoteKind.hx} (94%)
 rename source/funkin/play/notes/notekind/{NoteKindScriptManager.hx => NoteKindManager.hx} (64%)
 create mode 100644 source/funkin/play/notes/notekind/ScriptedNoteKind.hx
 delete mode 100644 source/funkin/play/notes/notekind/ScriptedNoteKindScript.hx

diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index a339d2655..34516dee1 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -27,7 +27,7 @@ import funkin.data.dialogue.speaker.SpeakerRegistry;
 import funkin.data.freeplay.album.AlbumRegistry;
 import funkin.data.song.SongRegistry;
 import funkin.play.character.CharacterData.CharacterDataParser;
-import funkin.play.notes.notekind.NoteKindScriptManager;
+import funkin.play.notes.notekind.NoteKindManager;
 import funkin.modding.module.ModuleHandler;
 import funkin.ui.title.TitleState;
 import funkin.util.CLIUtil;
@@ -177,7 +177,7 @@ class InitState extends FlxState
     // Move it to use a BaseRegistry.
     CharacterDataParser.loadCharacterCache();
 
-    NoteKindScriptManager.loadScripts();
+    NoteKindManager.loadScripts();
 
     ModuleHandler.buildModuleCallbacks();
     ModuleHandler.loadModuleCache();
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 93306e9d5..bc441a7d5 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -49,7 +49,7 @@ import funkin.play.notes.NoteSprite;
 import funkin.play.notes.notestyle.NoteStyle;
 import funkin.play.notes.Strumline;
 import funkin.play.notes.SustainTrail;
-import funkin.play.notes.notekind.NoteKindScriptManager;
+import funkin.play.notes.notekind.NoteKindManager;
 import funkin.play.scoring.Scoring;
 import funkin.play.song.Song;
 import funkin.play.stage.Stage;
@@ -1182,7 +1182,7 @@ class PlayState extends MusicBeatSubState
     if (Std.isOfType(event, NoteScriptEvent))
     {
       var noteEvent:NoteScriptEvent = cast(event, NoteScriptEvent);
-      NoteKindScriptManager.callEvent(noteEvent.note.noteData.kind, noteEvent);
+      NoteKindManager.callEvent(noteEvent.note.noteData.kind, noteEvent);
     }
   }
 
diff --git a/source/funkin/play/notes/notekind/NoteKindScript.hx b/source/funkin/play/notes/notekind/NoteKind.hx
similarity index 94%
rename from source/funkin/play/notes/notekind/NoteKindScript.hx
rename to source/funkin/play/notes/notekind/NoteKind.hx
index baa57b146..77b2bbc45 100644
--- a/source/funkin/play/notes/notekind/NoteKindScript.hx
+++ b/source/funkin/play/notes/notekind/NoteKind.hx
@@ -6,7 +6,7 @@ import funkin.modding.events.ScriptEvent;
 /**
  * Class for note scripts
  */
-class NoteKindScript implements INoteScriptedClass
+class NoteKind implements INoteScriptedClass
 {
   /**
    * the name of the note kind
diff --git a/source/funkin/play/notes/notekind/NoteKindScriptManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx
similarity index 64%
rename from source/funkin/play/notes/notekind/NoteKindScriptManager.hx
rename to source/funkin/play/notes/notekind/NoteKindManager.hx
index dc22732b6..eaee0d319 100644
--- a/source/funkin/play/notes/notekind/NoteKindScriptManager.hx
+++ b/source/funkin/play/notes/notekind/NoteKindManager.hx
@@ -4,13 +4,13 @@ import funkin.modding.events.ScriptEventDispatcher;
 import funkin.modding.events.ScriptEvent;
 import funkin.ui.debug.charting.util.ChartEditorDropdowns;
 
-class NoteKindScriptManager
+class NoteKindManager
 {
-  static var noteKindScripts:Map<String, NoteKindScript> = [];
+  static var noteKinds:Map<String, NoteKind> = [];
 
   public static function loadScripts():Void
   {
-    var scriptedClassName:Array<String> = ScriptedNoteKindScript.listScriptClasses();
+    var scriptedClassName:Array<String> = ScriptedNoteKind.listScriptClasses();
     if (scriptedClassName.length > 0)
     {
       trace('Instantiating ${scriptedClassName.length} scripted note kind...');
@@ -18,9 +18,9 @@ class NoteKindScriptManager
       {
         try
         {
-          var script:NoteKindScript = ScriptedNoteKindScript.init(scriptedClass, "unknown");
+          var script:NoteKind = ScriptedNoteKind.init(scriptedClass, "unknown");
           trace(' Initialized scripted note kind: ${script.noteKind}');
-          noteKindScripts.set(script.noteKind, script);
+          noteKinds.set(script.noteKind, script);
           ChartEditorDropdowns.NOTE_KINDS.set(script.noteKind, script.description);
         }
         catch (e)
@@ -34,13 +34,13 @@ class NoteKindScriptManager
 
   public static function callEvent(noteKind:String, event:ScriptEvent):Void
   {
-    var noteKindScript:NoteKindScript = noteKindScripts.get(noteKind);
+    var noteKind:NoteKind = noteKinds.get(noteKind);
 
-    if (noteKindScript == null)
+    if (noteKind == null)
     {
       return;
     }
 
-    ScriptEventDispatcher.callEvent(noteKindScript, event);
+    ScriptEventDispatcher.callEvent(noteKind, event);
   }
 }
diff --git a/source/funkin/play/notes/notekind/ScriptedNoteKind.hx b/source/funkin/play/notes/notekind/ScriptedNoteKind.hx
new file mode 100644
index 000000000..cd1781394
--- /dev/null
+++ b/source/funkin/play/notes/notekind/ScriptedNoteKind.hx
@@ -0,0 +1,9 @@
+package funkin.play.notes.notekind;
+
+/**
+ * A script that can be tied to a NoteKind.
+ * Create a scripted class that extends NoteKind,
+ * then call `super('noteKind')` in the constructor to use this.
+ */
+@:hscriptClass
+class ScriptedNoteKind extends NoteKind implements polymod.hscript.HScriptedClass {}
diff --git a/source/funkin/play/notes/notekind/ScriptedNoteKindScript.hx b/source/funkin/play/notes/notekind/ScriptedNoteKindScript.hx
deleted file mode 100644
index d54a0cde2..000000000
--- a/source/funkin/play/notes/notekind/ScriptedNoteKindScript.hx
+++ /dev/null
@@ -1,9 +0,0 @@
-package funkin.play.notes.notekind;
-
-/**
- * A script that can be tied to a NoteKindScript.
- * Create a scripted class that extends NoteKindScript,
- * then call `super('noteKind')` in the constructor to use this.
- */
-@:hscriptClass
-class ScriptedNoteKindScript extends NoteKindScript implements polymod.hscript.HScriptedClass {}

From 606d9d4af47ea50a89e921ef369aaa73b5dc3c7b Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Fri, 31 May 2024 17:24:51 +0200
Subject: [PATCH 21/64] Update NoteKindManager.hx

---
 source/funkin/play/notes/notekind/NoteKindManager.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/play/notes/notekind/NoteKindManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx
index eaee0d319..99502af08 100644
--- a/source/funkin/play/notes/notekind/NoteKindManager.hx
+++ b/source/funkin/play/notes/notekind/NoteKindManager.hx
@@ -13,7 +13,7 @@ class NoteKindManager
     var scriptedClassName:Array<String> = ScriptedNoteKind.listScriptClasses();
     if (scriptedClassName.length > 0)
     {
-      trace('Instantiating ${scriptedClassName.length} scripted note kind...');
+      trace('Instantiating ${scriptedClassName.length} scripted note kind(s)...');
       for (scriptedClass in scriptedClassName)
       {
         try

From c83e505f5bae0e1a61a1d010aec8765d53cab36d Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Fri, 31 May 2024 17:49:25 +0200
Subject: [PATCH 22/64] onUpdate, etc. works now too

---
 source/funkin/play/PlayState.hx                      | 6 +++++-
 source/funkin/play/notes/notekind/NoteKindManager.hx | 8 ++++++++
 2 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index bc441a7d5..3b098be14 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1178,12 +1178,16 @@ class PlayState extends MusicBeatSubState
     // Dispatch event to conversation script.
     ScriptEventDispatcher.callEvent(currentConversation, event);
 
-    // Dispatch event to note script
+    // Dispatch event to only the specific note script
     if (Std.isOfType(event, NoteScriptEvent))
     {
       var noteEvent:NoteScriptEvent = cast(event, NoteScriptEvent);
       NoteKindManager.callEvent(noteEvent.note.noteData.kind, noteEvent);
     }
+    else // Dispatch event to all note scripts
+    {
+      NoteKindManager.callEventForAll(event);
+    }
   }
 
   /**
diff --git a/source/funkin/play/notes/notekind/NoteKindManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx
index 99502af08..0de1a0a33 100644
--- a/source/funkin/play/notes/notekind/NoteKindManager.hx
+++ b/source/funkin/play/notes/notekind/NoteKindManager.hx
@@ -43,4 +43,12 @@ class NoteKindManager
 
     ScriptEventDispatcher.callEvent(noteKind, event);
   }
+
+  public static function callEventForAll(event:ScriptEvent):Void
+  {
+    for (noteKind in noteKinds.iterator())
+    {
+      ScriptEventDispatcher.callEvent(noteKind, event);
+    }
+  }
 }

From e3e4e9fac01844a807ecd54e34fc162205c59ed9 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Fri, 31 May 2024 20:28:56 +0200
Subject: [PATCH 23/64] helper function

---
 source/funkin/play/notes/notekind/NoteKind.hx | 22 +++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx
index 77b2bbc45..8b87e0442 100644
--- a/source/funkin/play/notes/notekind/NoteKind.hx
+++ b/source/funkin/play/notes/notekind/NoteKind.hx
@@ -18,6 +18,16 @@ class NoteKind implements INoteScriptedClass
    */
   public var description:String = "";
 
+  /**
+   * this only exists for people that don't like calling functions
+   */
+  var notes(get, never):Array<NoteSprite>;
+
+  function get_notes():Array<NoteSprite>
+  {
+    return this.getNotes();
+  }
+
   public function new(noteKind:String, description:String = "")
   {
     this.noteKind = noteKind;
@@ -29,6 +39,18 @@ class NoteKind implements INoteScriptedClass
     return noteKind;
   }
 
+  /**
+   * Retrieve all notes of this kind
+   * @return Array<NoteSprite>
+   */
+  function getNotes():Array<NoteSprite>
+  {
+    var allNotes:Array<NoteSprite> = PlayState.instance.playerStrumline.notes.members.concat(PlayState.instance.opponentStrumline.notes.members);
+    return allNotes.filter(function(note:NoteSprite) {
+      return note != null && note.noteData.kind == this.noteKind;
+    });
+  }
+
   public function onScriptEvent(event:ScriptEvent):Void {}
 
   public function onCreate(event:ScriptEvent):Void {}

From 3f39d9509c1e2a02d4ace85553991b6679a83d14 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Fri, 31 May 2024 23:56:57 +0200
Subject: [PATCH 24/64] make custom note style less clunky

---
 source/funkin/play/notes/notekind/NoteKind.hx | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx
index 8b87e0442..f4936c59e 100644
--- a/source/funkin/play/notes/notekind/NoteKind.hx
+++ b/source/funkin/play/notes/notekind/NoteKind.hx
@@ -1,5 +1,7 @@
 package funkin.play.notes.notekind;
 
+import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.play.notes.notestyle.NoteStyle;
 import funkin.modding.IScriptedClass.INoteScriptedClass;
 import funkin.modding.events.ScriptEvent;
 
@@ -39,6 +41,23 @@ class NoteKind implements INoteScriptedClass
     return noteKind;
   }
 
+  /**
+   * Changes the note style of the given note. Use this in `onNoteIncoming`
+   * @param note
+   * @param noteStyle
+   */
+  function setNoteStyle(note:NoteSprite, noteStyleId:String):Void
+  {
+    var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
+    noteStyle.buildNoteSprite(note);
+
+    note.setGraphicSize(Strumline.STRUMLINE_SIZE);
+    note.updateHitbox();
+
+    // this calls the setter for playing the correct animation
+    note.direction = note.direction;
+  }
+
   /**
    * Retrieve all notes of this kind
    * @return Array<NoteSprite>

From 14771e72deb7f08b9658e0801c9e088bb5d1aa96 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Fri, 31 May 2024 23:59:40 +0200
Subject: [PATCH 25/64] no more note getter

---
 source/funkin/play/notes/notekind/NoteKind.hx | 10 ----------
 1 file changed, 10 deletions(-)

diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx
index f4936c59e..262660eea 100644
--- a/source/funkin/play/notes/notekind/NoteKind.hx
+++ b/source/funkin/play/notes/notekind/NoteKind.hx
@@ -20,16 +20,6 @@ class NoteKind implements INoteScriptedClass
    */
   public var description:String = "";
 
-  /**
-   * this only exists for people that don't like calling functions
-   */
-  var notes(get, never):Array<NoteSprite>;
-
-  function get_notes():Array<NoteSprite>
-  {
-    return this.getNotes();
-  }
-
   public function new(noteKind:String, description:String = "")
   {
     this.noteKind = noteKind;

From b6eda8e498be12f40c6086678a48c7c038e4b69e Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sat, 1 Jun 2024 18:25:46 +0200
Subject: [PATCH 26/64] remove note kind logic from playstate

---
 source/funkin/play/PlayState.hx               | 12 ++-----
 .../play/notes/notekind/NoteKindManager.hx    | 34 ++++++++++++-------
 2 files changed, 23 insertions(+), 23 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 3b098be14..da343f43f 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1178,16 +1178,8 @@ class PlayState extends MusicBeatSubState
     // Dispatch event to conversation script.
     ScriptEventDispatcher.callEvent(currentConversation, event);
 
-    // Dispatch event to only the specific note script
-    if (Std.isOfType(event, NoteScriptEvent))
-    {
-      var noteEvent:NoteScriptEvent = cast(event, NoteScriptEvent);
-      NoteKindManager.callEvent(noteEvent.note.noteData.kind, noteEvent);
-    }
-    else // Dispatch event to all note scripts
-    {
-      NoteKindManager.callEventForAll(event);
-    }
+    // Dispatch event to note kind scripts
+    NoteKindManager.callEvent(event);
   }
 
   /**
diff --git a/source/funkin/play/notes/notekind/NoteKindManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx
index 0de1a0a33..6efd601c7 100644
--- a/source/funkin/play/notes/notekind/NoteKindManager.hx
+++ b/source/funkin/play/notes/notekind/NoteKindManager.hx
@@ -32,23 +32,31 @@ class NoteKindManager
     }
   }
 
-  public static function callEvent(noteKind:String, event:ScriptEvent):Void
+  /**
+   * Calls the given event for note kind scripts
+   * @param event The event
+   */
+  public static function callEvent(event:ScriptEvent):Void
   {
-    var noteKind:NoteKind = noteKinds.get(noteKind);
-
-    if (noteKind == null)
+    // if it is a note script event,
+    // then only call the event for the specific note kind script
+    if (Std.isOfType(event, NoteScriptEvent))
     {
-      return;
+      var noteEvent:NoteScriptEvent = cast(event, NoteScriptEvent);
+
+      var noteKind:NoteKind = noteKinds.get(noteEvent.note.noteData.kind);
+
+      if (noteKind != null)
+      {
+        ScriptEventDispatcher.callEvent(noteKind, event);
+      }
     }
-
-    ScriptEventDispatcher.callEvent(noteKind, event);
-  }
-
-  public static function callEventForAll(event:ScriptEvent):Void
-  {
-    for (noteKind in noteKinds.iterator())
+    else // call the event for all note kind scripts
     {
-      ScriptEventDispatcher.callEvent(noteKind, event);
+      for (noteKind in noteKinds.iterator())
+      {
+        ScriptEventDispatcher.callEvent(noteKind, event);
+      }
     }
   }
 }

From 328e590f92901f3dab711a725af02cc40763f359 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sat, 1 Jun 2024 19:47:45 +0200
Subject: [PATCH 27/64] hold assets are updated aswell

---
 source/funkin/play/notes/NoteSprite.hx        |  6 ++-
 source/funkin/play/notes/Strumline.hx         |  7 ++++
 source/funkin/play/notes/SustainTrail.hx      | 38 ++++++++++++++-----
 source/funkin/play/notes/notekind/NoteKind.hx | 33 +++++-----------
 .../play/notes/notekind/NoteKindManager.hx    | 16 +++++++-
 5 files changed, 65 insertions(+), 35 deletions(-)

diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx
index b16b88466..17a5e57fc 100644
--- a/source/funkin/play/notes/NoteSprite.hx
+++ b/source/funkin/play/notes/NoteSprite.hx
@@ -140,7 +140,11 @@ class NoteSprite extends FunkinSprite
     this.active = false;
   }
 
-  function setupNoteGraphic(noteStyle:NoteStyle):Void
+  /**
+   * Creates frames and animations
+   * @param noteStyle The `NoteStyle` instance
+   */
+  public function setupNoteGraphic(noteStyle:NoteStyle):Void
   {
     noteStyle.buildNoteSprite(this);
 
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index fdb32bb85..ebc48a8c7 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -16,6 +16,7 @@ import funkin.data.song.SongData.SongNoteData;
 import funkin.ui.options.PreferencesMenu;
 import funkin.util.SortUtil;
 import funkin.modding.events.ScriptEvent;
+import funkin.play.notes.notekind.NoteKindManager;
 
 /**
  * A group of sprites which handles the receptor, the note splashes, and the notes (with sustains) for a given player.
@@ -708,6 +709,9 @@ class Strumline extends FlxSpriteGroup
 
     if (noteSprite != null)
     {
+      var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind) ?? this.noteStyle;
+      noteSprite.setupNoteGraphic(noteKindStyle);
+
       noteSprite.direction = note.getDirection();
       noteSprite.noteData = note;
 
@@ -727,6 +731,9 @@ class Strumline extends FlxSpriteGroup
 
     if (holdNoteSprite != null)
     {
+      var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind) ?? this.noteStyle;
+      holdNoteSprite.setupHoldNoteGraphic(noteKindStyle);
+
       holdNoteSprite.parentStrumline = this;
       holdNoteSprite.noteData = note;
       holdNoteSprite.strumTime = note.time;
diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx
index f6d43b33f..570c05190 100644
--- a/source/funkin/play/notes/SustainTrail.hx
+++ b/source/funkin/play/notes/SustainTrail.hx
@@ -99,7 +99,27 @@ class SustainTrail extends FlxSprite
    */
   public function new(noteDirection:NoteDirection, sustainLength:Float, noteStyle:NoteStyle)
   {
-    super(0, 0, noteStyle.getHoldNoteAssetPath());
+    super(0, 0);
+
+    // BASIC SETUP
+    this.sustainLength = sustainLength;
+    this.fullSustainLength = sustainLength;
+    this.noteDirection = noteDirection;
+
+    setupHoldNoteGraphic(noteStyle);
+
+    indices = new DrawData<Int>(12, true, TRIANGLE_VERTEX_INDICES);
+
+    this.active = true; // This NEEDS to be true for the note to be drawn!
+  }
+
+  /**
+   * Creates hold note graphic and applies correct zooming
+   * @param noteStyle The note style
+   */
+  public function setupHoldNoteGraphic(noteStyle:NoteStyle):Void
+  {
+    loadGraphic(noteStyle.getHoldNoteAssetPath());
 
     antialiasing = true;
 
@@ -109,13 +129,9 @@ class SustainTrail extends FlxSprite
       endOffset = bottomClip = 1;
       antialiasing = false;
     }
+
+    zoom = 1.0;
     zoom *= noteStyle.fetchHoldNoteScale();
-
-    // BASIC SETUP
-    this.sustainLength = sustainLength;
-    this.fullSustainLength = sustainLength;
-    this.noteDirection = noteDirection;
-
     zoom *= 0.7;
 
     // CALCULATE SIZE
@@ -131,9 +147,6 @@ class SustainTrail extends FlxSprite
     updateColorTransform();
 
     updateClipping();
-    indices = new DrawData<Int>(12, true, TRIANGLE_VERTEX_INDICES);
-
-    this.active = true; // This NEEDS to be true for the note to be drawn!
   }
 
   function getBaseScrollSpeed()
@@ -195,6 +208,11 @@ class SustainTrail extends FlxSprite
    */
   public function updateClipping(songTime:Float = 0):Void
   {
+    if (graphic == null)
+    {
+      return;
+    }
+
     var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), parentStrumline?.scrollSpeed ?? 1.0), 0, graphicHeight);
     if (clipHeight <= 0.1)
     {
diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx
index 262660eea..53393623a 100644
--- a/source/funkin/play/notes/notekind/NoteKind.hx
+++ b/source/funkin/play/notes/notekind/NoteKind.hx
@@ -1,7 +1,5 @@
 package funkin.play.notes.notekind;
 
-import funkin.data.notestyle.NoteStyleRegistry;
-import funkin.play.notes.notestyle.NoteStyle;
 import funkin.modding.IScriptedClass.INoteScriptedClass;
 import funkin.modding.events.ScriptEvent;
 
@@ -11,19 +9,25 @@ import funkin.modding.events.ScriptEvent;
 class NoteKind implements INoteScriptedClass
 {
   /**
-   * the name of the note kind
+   * The name of the note kind
    */
   public var noteKind:String;
 
   /**
-   * description used in chart editor
+   * Description used in chart editor
    */
-  public var description:String = "";
+  public var description:String;
 
-  public function new(noteKind:String, description:String = "")
+  /**
+   * Custom note style
+   */
+  public var noteStyleId:String;
+
+  public function new(noteKind:String, description:String = "", noteStyleId:String = "")
   {
     this.noteKind = noteKind;
     this.description = description;
+    this.noteStyleId = noteStyleId;
   }
 
   public function toString():String
@@ -31,23 +35,6 @@ class NoteKind implements INoteScriptedClass
     return noteKind;
   }
 
-  /**
-   * Changes the note style of the given note. Use this in `onNoteIncoming`
-   * @param note
-   * @param noteStyle
-   */
-  function setNoteStyle(note:NoteSprite, noteStyleId:String):Void
-  {
-    var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
-    noteStyle.buildNoteSprite(note);
-
-    note.setGraphicSize(Strumline.STRUMLINE_SIZE);
-    note.updateHitbox();
-
-    // this calls the setter for playing the correct animation
-    note.direction = note.direction;
-  }
-
   /**
    * Retrieve all notes of this kind
    * @return Array<NoteSprite>
diff --git a/source/funkin/play/notes/notekind/NoteKindManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx
index 6efd601c7..849034fc4 100644
--- a/source/funkin/play/notes/notekind/NoteKindManager.hx
+++ b/source/funkin/play/notes/notekind/NoteKindManager.hx
@@ -3,6 +3,8 @@ package funkin.play.notes.notekind;
 import funkin.modding.events.ScriptEventDispatcher;
 import funkin.modding.events.ScriptEvent;
 import funkin.ui.debug.charting.util.ChartEditorDropdowns;
+import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.play.notes.notestyle.NoteStyle;
 
 class NoteKindManager
 {
@@ -44,7 +46,7 @@ class NoteKindManager
     {
       var noteEvent:NoteScriptEvent = cast(event, NoteScriptEvent);
 
-      var noteKind:NoteKind = noteKinds.get(noteEvent.note.noteData.kind);
+      var noteKind:NoteKind = noteKinds.get(noteEvent.note.kind);
 
       if (noteKind != null)
       {
@@ -59,4 +61,16 @@ class NoteKindManager
       }
     }
   }
+
+  /**
+   * Retrieve the note style from the given note kind
+   * @param noteKind note kind name
+   * @return NoteStyle
+   */
+  public static function getNoteStyle(noteKind:String):Null<NoteStyle>
+  {
+    var noteStyleId:String = noteKinds.get(noteKind)?.noteStyleId ?? "";
+
+    return NoteStyleRegistry.instance.fetchEntry(noteStyleId);
+  }
 }

From ca69e7b850332e0aa4a35574566867cca649cdbf Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sun, 2 Jun 2024 02:44:16 +0200
Subject: [PATCH 28/64] custom note styles in chart editor

---
 .../ui/debug/charting/ChartEditorState.hx     |   2 +
 .../components/ChartEditorNoteSprite.hx       | 106 ++++++++++--------
 .../toolboxes/ChartEditorNoteDataToolbox.hx   |   5 +
 3 files changed, 64 insertions(+), 49 deletions(-)

diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index f72cca77f..0117d8a51 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1663,6 +1663,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     return currentSongMetadata.playData.characters.instrumental = value;
   }
 
+  var currentCustomNoteKindStyle:Null<String>;
+
   /**
    * HAXEUI COMPONENTS
    */
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx
index 98f5a47aa..c97aee1f8 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx
@@ -7,7 +7,11 @@ import flixel.graphics.frames.FlxAtlasFrames;
 import flixel.graphics.frames.FlxFrame;
 import flixel.graphics.frames.FlxTileFrames;
 import flixel.math.FlxPoint;
+import funkin.data.animation.AnimationData;
 import funkin.data.song.SongData.SongNoteData;
+import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.play.notes.notestyle.NoteStyle;
+import funkin.play.notes.NoteDirection;
 
 /**
  * A sprite that can be used to display a note in a chart.
@@ -68,68 +72,62 @@ class ChartEditorNoteSprite extends FlxSprite
 
     if (noteFrameCollection == null)
     {
-      initFrameCollection();
+      buildEmptyFrameCollection();
+
+      addNoteStyleFrames(fetchNoteStyle('funkin'));
+      addNoteStyleFrames(fetchNoteStyle('pixel'));
     }
 
     if (noteFrameCollection == null) throw 'ERROR: Could not initialize note sprite animations.';
 
     this.frames = noteFrameCollection;
 
-    // Initialize all the animations, not just the one we're going to use immediately,
-    // so that later we can reuse the sprite without having to initialize more animations during scrolling.
-    this.animation.addByPrefix('tapLeftFunkin', 'purple instance');
-    this.animation.addByPrefix('tapDownFunkin', 'blue instance');
-    this.animation.addByPrefix('tapUpFunkin', 'green instance');
-    this.animation.addByPrefix('tapRightFunkin', 'red instance');
-
-    this.animation.addByPrefix('holdLeftFunkin', 'LeftHoldPiece');
-    this.animation.addByPrefix('holdDownFunkin', 'DownHoldPiece');
-    this.animation.addByPrefix('holdUpFunkin', 'UpHoldPiece');
-    this.animation.addByPrefix('holdRightFunkin', 'RightHoldPiece');
-
-    this.animation.addByPrefix('holdEndLeftFunkin', 'LeftHoldEnd');
-    this.animation.addByPrefix('holdEndDownFunkin', 'DownHoldEnd');
-    this.animation.addByPrefix('holdEndUpFunkin', 'UpHoldEnd');
-    this.animation.addByPrefix('holdEndRightFunkin', 'RightHoldEnd');
-
-    this.animation.addByPrefix('tapLeftPixel', 'pixel4');
-    this.animation.addByPrefix('tapDownPixel', 'pixel5');
-    this.animation.addByPrefix('tapUpPixel', 'pixel6');
-    this.animation.addByPrefix('tapRightPixel', 'pixel7');
+    addNoteStyleAnimations(fetchNoteStyle('funkin'));
+    addNoteStyleAnimations(fetchNoteStyle('pixel'));
   }
 
   static var noteFrameCollection:Null<FlxFramesCollection> = null;
 
-  /**
-   * We load all the note frames once, then reuse them.
-   */
-  static function initFrameCollection():Void
+  function fetchNoteStyle(noteStyleId:String):NoteStyle
   {
-    buildEmptyFrameCollection();
-    if (noteFrameCollection == null) return;
+    return NoteStyleRegistry.instance.fetchEntry(noteStyleId) ?? NoteStyleRegistry.instance.fetchDefault();
+  }
 
-    // TODO: Automatically iterate over the list of note skins.
+  @:access(funkin.play.notes.notestyle.NoteStyle)
+  @:nullSafety(Off)
+  static function addNoteStyleFrames(noteStyle:NoteStyle):Void
+  {
+    var prefix:String = noteStyle.id.toTitleCase();
 
-    // Normal notes
-    var frameCollectionNormal:FlxAtlasFrames = Paths.getSparrowAtlas('NOTE_assets');
-
-    for (frame in frameCollectionNormal.frames)
+    var frameCollection:FlxAtlasFrames = Paths.getSparrowAtlas(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary());
+    for (frame in frameCollection.frames)
     {
-      noteFrameCollection.pushFrame(frame);
+      // cloning the frame because else
+      // we will fuck up the frame data used in game
+      var clonedFrame:FlxFrame = frame.copyTo();
+      clonedFrame.name = '$prefix${clonedFrame.name}';
+      noteFrameCollection.pushFrame(clonedFrame);
     }
+  }
 
-    // Pixel notes
-    var graphicPixel = FlxG.bitmap.add(Paths.image('weeb/pixelUI/arrows-pixels', 'week6'), false, null);
-    if (graphicPixel == null) trace('ERROR: Could not load graphic: ' + Paths.image('weeb/pixelUI/arrows-pixels', 'week6'));
-    var frameCollectionPixel = FlxTileFrames.fromGraphic(graphicPixel, new FlxPoint(17, 17));
-    for (i in 0...frameCollectionPixel.frames.length)
-    {
-      var frame:Null<FlxFrame> = frameCollectionPixel.frames[i];
-      if (frame == null) continue;
+  @:access(funkin.play.notes.notestyle.NoteStyle)
+  @:nullSafety(Off)
+  function addNoteStyleAnimations(noteStyle:NoteStyle):Void
+  {
+    var prefix:String = noteStyle.id.toTitleCase();
+    var suffix:String = noteStyle.id.toTitleCase();
 
-      frame.name = 'pixel' + i;
-      noteFrameCollection.pushFrame(frame);
-    }
+    var leftData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.LEFT);
+    this.animation.addByPrefix('tapLeft$suffix', '$prefix${leftData.prefix}', leftData.frameRate, leftData.looped, leftData.flipX, leftData.flipY);
+
+    var downData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.DOWN);
+    this.animation.addByPrefix('tapDown$suffix', '$prefix${downData.prefix}', downData.frameRate, downData.looped, downData.flipX, downData.flipY);
+
+    var upData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.UP);
+    this.animation.addByPrefix('tapUp$suffix', '$prefix${upData.prefix}', upData.frameRate, upData.looped, upData.flipX, upData.flipY);
+
+    var rightData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.RIGHT);
+    this.animation.addByPrefix('tapRight$suffix', '$prefix${rightData.prefix}', rightData.frameRate, rightData.looped, rightData.flipX, rightData.flipY);
   }
 
   @:nullSafety(Off)
@@ -187,10 +185,20 @@ class ChartEditorNoteSprite extends FlxSprite
 
   function get_noteStyle():String
   {
-    // Fall back to Funkin' if it's not a valid note style.
-    return if (NOTE_STYLES.contains(this.parentState.currentSongNoteStyle)) this.parentState.currentSongNoteStyle else 'funkin';
+    if (this.parentState.currentCustomNoteKindStyle != null)
+    {
+      return this.parentState.currentCustomNoteKindStyle;
+    }
+
+    if (NOTE_STYLES.contains(this.parentState.currentSongNoteStyle))
+    {
+      return this.parentState.currentSongNoteStyle;
+    }
+
+    return 'funkin';
   }
 
+  @:nullSafety(Off)
   public function playNoteAnimation():Void
   {
     if (this.noteData == null) return;
@@ -213,8 +221,8 @@ class ChartEditorNoteSprite extends FlxSprite
     }
     this.updateHitbox();
 
-    // TODO: Make this an attribute of the note skin.
-    this.antialiasing = (this.parentState.currentSongNoteStyle != 'Pixel');
+    var bruhStyle:NoteStyle = fetchNoteStyle(this.noteStyle);
+    this.antialiasing = !bruhStyle._data?.assets?.note?.isPixel ?? true;
   }
 
   /**
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
index d4fc69fc1..952513f0e 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
@@ -4,6 +4,8 @@ import haxe.ui.components.DropDown;
 import haxe.ui.components.TextField;
 import haxe.ui.events.UIEvent;
 import funkin.ui.debug.charting.util.ChartEditorDropdowns;
+import funkin.play.notes.notestyle.NoteStyle;
+import funkin.play.notes.notekind.NoteKindManager;
 
 /**
  * The toolbox which allows modifying information like Note Kind.
@@ -73,6 +75,9 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
       var customKind:Null<String> = event?.target?.text;
       chartEditorState.noteKindToPlace = customKind;
 
+      var noteStyle:Null<NoteStyle> = NoteKindManager.getNoteStyle(customKind);
+      chartEditorState.currentCustomNoteKindStyle = noteStyle?.id;
+
       if (chartEditorState.currentEventSelection.length > 0)
       {
         // Edit the note data of any selected notes.

From 9d7846a0d551a005897f1ea0ca099d19f4ac359f Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sun, 2 Jun 2024 02:58:56 +0200
Subject: [PATCH 29/64] loop through all note style entries

---
 .../charting/components/ChartEditorNoteSprite.hx   | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx
index c97aee1f8..6517b61c2 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx
@@ -70,20 +70,26 @@ class ChartEditorNoteSprite extends FlxSprite
 
     this.parentState = parent;
 
+    var entries:Array<String> = NoteStyleRegistry.instance.listEntryIds();
+
     if (noteFrameCollection == null)
     {
       buildEmptyFrameCollection();
 
-      addNoteStyleFrames(fetchNoteStyle('funkin'));
-      addNoteStyleFrames(fetchNoteStyle('pixel'));
+      for (entry in entries)
+      {
+        addNoteStyleFrames(fetchNoteStyle(entry));
+      }
     }
 
     if (noteFrameCollection == null) throw 'ERROR: Could not initialize note sprite animations.';
 
     this.frames = noteFrameCollection;
 
-    addNoteStyleAnimations(fetchNoteStyle('funkin'));
-    addNoteStyleAnimations(fetchNoteStyle('pixel'));
+    for (entry in entries)
+    {
+      addNoteStyleAnimations(fetchNoteStyle(entry));
+    }
   }
 
   static var noteFrameCollection:Null<FlxFramesCollection> = null;

From ca2cbb44f520361b73c60378e0beaf0459e308c2 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sun, 2 Jun 2024 22:11:17 +0200
Subject: [PATCH 30/64] display custom note style in chart editor

---
 .../play/notes/notekind/NoteKindManager.hx    | 10 ++++++
 .../ui/debug/charting/ChartEditorState.hx     | 10 ++++--
 .../components/ChartEditorHoldNoteSprite.hx   | 34 +++++++++++++++++++
 .../components/ChartEditorNoteSprite.hx       | 22 ++++++------
 .../toolboxes/ChartEditorNoteDataToolbox.hx   |  3 --
 5 files changed, 61 insertions(+), 18 deletions(-)

diff --git a/source/funkin/play/notes/notekind/NoteKindManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx
index 849034fc4..82b2c4d39 100644
--- a/source/funkin/play/notes/notekind/NoteKindManager.hx
+++ b/source/funkin/play/notes/notekind/NoteKindManager.hx
@@ -73,4 +73,14 @@ class NoteKindManager
 
     return NoteStyleRegistry.instance.fetchEntry(noteStyleId);
   }
+
+  /**
+   * Retrieve the note style id from the given note kind
+   * @param noteKind note kind name
+   * @return Null<String>
+   */
+  public static function getNoteStyleId(noteKind:String):Null<String>
+  {
+    return noteKinds.get(noteKind)?.noteStyleId;
+  }
 }
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 0117d8a51..bc0acc6d6 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -45,6 +45,7 @@ import funkin.input.TurboActionHandler;
 import funkin.input.TurboButtonHandler;
 import funkin.input.TurboKeyHandler;
 import funkin.modding.events.ScriptEvent;
+import funkin.play.notes.notekind.NoteKindManager;
 import funkin.play.character.BaseCharacter.CharacterType;
 import funkin.play.character.CharacterData;
 import funkin.play.character.CharacterData.CharacterDataParser;
@@ -1663,8 +1664,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     return currentSongMetadata.playData.characters.instrumental = value;
   }
 
-  var currentCustomNoteKindStyle:Null<String>;
-
   /**
    * HAXEUI COMPONENTS
    */
@@ -3586,6 +3585,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
         // The note sprite handles animation playback and positioning.
         noteSprite.noteData = noteData;
+        noteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind) ?? currentSongNoteStyle;
         noteSprite.overrideStepTime = null;
         noteSprite.overrideData = null;
 
@@ -3606,6 +3606,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
           holdNoteSprite.noteData = noteSprite.noteData;
           holdNoteSprite.noteDirection = noteSprite.noteData.getDirection();
+          holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteSprite.noteData.kind) ?? currentSongNoteStyle;
 
           holdNoteSprite.setHeightDirectly(noteLengthPixels);
 
@@ -3671,7 +3672,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
         holdNoteSprite.noteData = noteData;
         holdNoteSprite.noteDirection = noteData.getDirection();
-
+        holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind) ?? currentSongNoteStyle;
         holdNoteSprite.setHeightDirectly(noteLengthPixels);
 
         holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
@@ -4570,6 +4571,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
             gridGhostHoldNote.visible = true;
             gridGhostHoldNote.noteData = currentPlaceNoteData;
             gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection();
+            gridGhostHoldNote.noteStyle = NoteKindManager.getNoteStyleId(currentPlaceNoteData.kind) ?? currentSongNoteStyle;
             gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
 
             gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
@@ -4893,6 +4895,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
             {
               noteData.kind = noteKindToPlace;
               noteData.data = cursorColumn;
+              gridGhostNote.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind) ?? currentSongNoteStyle;
               gridGhostNote.playNoteAnimation();
             }
             noteData.time = cursorSnappedMs;
@@ -5281,6 +5284,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         // Readd the new ghost hold note.
         ghostHold.noteData = targetNoteData.clone();
         ghostHold.noteDirection = ghostHold.noteData.getDirection();
+        ghostHold.noteStyle = NoteKindManager.getNoteStyleId(ghostHold.noteData.kind) ?? currentSongNoteStyle;
         ghostHold.visible = true;
         ghostHold.alpha = 0.6;
         ghostHold.setHeightDirectly(0);
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
index ded48abe3..ccf462c56 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
@@ -2,6 +2,7 @@ package funkin.ui.debug.charting.components;
 
 import funkin.play.notes.Strumline;
 import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.play.notes.notestyle.NoteStyle;
 import flixel.FlxObject;
 import flixel.FlxSprite;
 import flixel.graphics.frames.FlxFramesCollection;
@@ -15,6 +16,7 @@ import flixel.math.FlxMath;
  * A sprite that can be used to display the trail of a hold note in a chart.
  * Designed to be used and reused efficiently. Has no gameplay functionality.
  */
+@:access(funkin.ui.debug.charting.ChartEditorState)
 @:nullSafety
 class ChartEditorHoldNoteSprite extends SustainTrail
 {
@@ -23,6 +25,22 @@ class ChartEditorHoldNoteSprite extends SustainTrail
    */
   public var parentState:ChartEditorState;
 
+  @:isVar
+  public var noteStyle(get, set):Null<String>;
+
+  function get_noteStyle():Null<String>
+  {
+    return this.noteStyle ?? this.parentState.currentSongNoteStyle;
+  }
+
+  @:nullSafety(Off)
+  function set_noteStyle(value:Null<String>):Null<String>
+  {
+    this.noteStyle = value;
+    this.updateHoldNoteGraphic();
+    return value;
+  }
+
   public function new(parent:ChartEditorState)
   {
     var noteStyle = NoteStyleRegistry.instance.fetchDefault();
@@ -41,6 +59,22 @@ class ChartEditorHoldNoteSprite extends SustainTrail
     setup();
   }
 
+  @:nullSafety(Off)
+  function updateHoldNoteGraphic():Void
+  {
+    var bruhStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyle);
+    this.setupHoldNoteGraphic(bruhStyle);
+
+    zoom = 1.0;
+    zoom *= bruhStyle.fetchHoldNoteScale();
+    zoom *= 0.7;
+    zoom *= ChartEditorState.GRID_SIZE / Strumline.STRUMLINE_SIZE;
+
+    flipY = false;
+
+    setup();
+  }
+
   public override function updateHitbox():Void
   {
     // Expand the clickable hitbox to the full column width, then nudge to the left to re-center it.
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx
index 6517b61c2..009532401 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx
@@ -40,7 +40,8 @@ class ChartEditorNoteSprite extends FlxSprite
   /**
    * The name of the note style currently in use.
    */
-  public var noteStyle(get, never):String;
+  @:isVar
+  public var noteStyle(get, set):Null<String>;
 
   public var overrideStepTime(default, set):Null<Float> = null;
 
@@ -189,19 +190,16 @@ class ChartEditorNoteSprite extends FlxSprite
     }
   }
 
-  function get_noteStyle():String
+  function get_noteStyle():Null<String>
   {
-    if (this.parentState.currentCustomNoteKindStyle != null)
-    {
-      return this.parentState.currentCustomNoteKindStyle;
-    }
+    return this.noteStyle ?? this.parentState.currentSongNoteStyle;
+  }
 
-    if (NOTE_STYLES.contains(this.parentState.currentSongNoteStyle))
-    {
-      return this.parentState.currentSongNoteStyle;
-    }
-
-    return 'funkin';
+  function set_noteStyle(value:Null<String>):Null<String>
+  {
+    this.noteStyle = value;
+    this.playNoteAnimation();
+    return value;
   }
 
   @:nullSafety(Off)
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
index 952513f0e..f1223eb9c 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
@@ -75,9 +75,6 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
       var customKind:Null<String> = event?.target?.text;
       chartEditorState.noteKindToPlace = customKind;
 
-      var noteStyle:Null<NoteStyle> = NoteKindManager.getNoteStyle(customKind);
-      chartEditorState.currentCustomNoteKindStyle = noteStyle?.id;
-
       if (chartEditorState.currentEventSelection.length > 0)
       {
         // Edit the note data of any selected notes.

From 040fc85a6263408646e6cac60c1a25da2c99fcaf Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sun, 2 Jun 2024 23:31:49 +0200
Subject: [PATCH 31/64] change note style on change

---
 .../components/ChartEditorHoldNoteSprite.hx   |  2 +-
 .../toolboxes/ChartEditorNoteDataToolbox.hx   | 26 +++++++++++++++++++
 2 files changed, 27 insertions(+), 1 deletion(-)

diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
index ccf462c56..26322c6f9 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
@@ -63,7 +63,7 @@ class ChartEditorHoldNoteSprite extends SustainTrail
   function updateHoldNoteGraphic():Void
   {
     var bruhStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyle);
-    this.setupHoldNoteGraphic(bruhStyle);
+    setupHoldNoteGraphic(bruhStyle);
 
     zoom = 1.0;
     zoom *= bruhStyle.fetchHoldNoteScale();
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
index f1223eb9c..d872eda38 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
@@ -4,6 +4,8 @@ import haxe.ui.components.DropDown;
 import haxe.ui.components.TextField;
 import haxe.ui.events.UIEvent;
 import funkin.ui.debug.charting.util.ChartEditorDropdowns;
+import funkin.ui.debug.charting.components.ChartEditorNoteSprite;
+import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite;
 import funkin.play.notes.notestyle.NoteStyle;
 import funkin.play.notes.notekind.NoteKindManager;
 
@@ -59,8 +61,32 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
       if (!_initializing && chartEditorState.currentNoteSelection.length > 0)
       {
         // Edit the note data of any selected notes.
+        var noteSprites:Array<ChartEditorNoteSprite> = chartEditorState.renderedNotes.members.copy();
+        var holdNoteSprites:Array<ChartEditorHoldNoteSprite> = chartEditorState.renderedHoldNotes.members.copy();
         for (note in chartEditorState.currentNoteSelection)
         {
+          // update note sprites
+          for (noteSprite in noteSprites)
+          {
+            if (noteSprite.noteData == note)
+            {
+              noteSprite.noteStyle = NoteKindManager.getNoteStyleId(chartEditorState.noteKindToPlace) ?? chartEditorState.currentSongNoteStyle;
+              noteSprites.remove(noteSprite);
+              break;
+            }
+          }
+
+          // update hold note sprites
+          for (holdNoteSprite in holdNoteSprites)
+          {
+            if (holdNoteSprite.noteData == note)
+            {
+              holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(chartEditorState.noteKindToPlace) ?? chartEditorState.currentSongNoteStyle;
+              holdNoteSprites.remove(holdNoteSprite);
+              break;
+            }
+          }
+
           note.kind = chartEditorState.noteKindToPlace;
         }
         chartEditorState.saveDataDirty = true;

From bb975491712445a272f5e8817e34dacbd62a0d03 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Mon, 3 Jun 2024 00:03:47 +0200
Subject: [PATCH 32/64] fixed a bit

---
 .../ui/debug/charting/ChartEditorState.hx      | 11 ++++++-----
 .../components/ChartEditorHoldNoteSprite.hx    |  2 ++
 .../toolboxes/ChartEditorNoteDataToolbox.hx    | 18 +++++++-----------
 3 files changed, 15 insertions(+), 16 deletions(-)

diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index bc0acc6d6..c867ddfd2 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -3606,10 +3606,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
           holdNoteSprite.noteData = noteSprite.noteData;
           holdNoteSprite.noteDirection = noteSprite.noteData.getDirection();
-          holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteSprite.noteData.kind) ?? currentSongNoteStyle;
 
           holdNoteSprite.setHeightDirectly(noteLengthPixels);
 
+          holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteSprite.noteData.kind) ?? currentSongNoteStyle;
+
           holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
 
           trace(holdNoteSprite.x + ', ' + holdNoteSprite.y + ', ' + holdNoteSprite.width + ', ' + holdNoteSprite.height);
@@ -3672,9 +3673,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
         holdNoteSprite.noteData = noteData;
         holdNoteSprite.noteDirection = noteData.getDirection();
-        holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind) ?? currentSongNoteStyle;
         holdNoteSprite.setHeightDirectly(noteLengthPixels);
 
+        holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind) ?? currentSongNoteStyle;
+
         holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
 
         displayedHoldNoteData.push(noteData);
@@ -4571,9 +4573,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
             gridGhostHoldNote.visible = true;
             gridGhostHoldNote.noteData = currentPlaceNoteData;
             gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection();
-            gridGhostHoldNote.noteStyle = NoteKindManager.getNoteStyleId(currentPlaceNoteData.kind) ?? currentSongNoteStyle;
             gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
-
+            gridGhostHoldNote.noteStyle = NoteKindManager.getNoteStyleId(currentPlaceNoteData.kind) ?? currentSongNoteStyle;
             gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
           }
           else
@@ -5284,10 +5285,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         // Readd the new ghost hold note.
         ghostHold.noteData = targetNoteData.clone();
         ghostHold.noteDirection = ghostHold.noteData.getDirection();
-        ghostHold.noteStyle = NoteKindManager.getNoteStyleId(ghostHold.noteData.kind) ?? currentSongNoteStyle;
         ghostHold.visible = true;
         ghostHold.alpha = 0.6;
         ghostHold.setHeightDirectly(0);
+        ghostHold.noteStyle = NoteKindManager.getNoteStyleId(ghostHold.noteData.kind) ?? currentSongNoteStyle;
         ghostHold.updateHoldNotePosition(renderedHoldNotes);
       }
 
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
index 26322c6f9..e8ca991b6 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
@@ -73,6 +73,8 @@ class ChartEditorHoldNoteSprite extends SustainTrail
     flipY = false;
 
     setup();
+
+    triggerRedraw();
   }
 
   public override function updateHitbox():Void
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
index d872eda38..531bce255 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
@@ -60,34 +60,30 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
 
       if (!_initializing && chartEditorState.currentNoteSelection.length > 0)
       {
-        // Edit the note data of any selected notes.
-        var noteSprites:Array<ChartEditorNoteSprite> = chartEditorState.renderedNotes.members.copy();
-        var holdNoteSprites:Array<ChartEditorHoldNoteSprite> = chartEditorState.renderedHoldNotes.members.copy();
         for (note in chartEditorState.currentNoteSelection)
         {
+          // Edit the note data of any selected notes.
+          note.kind = chartEditorState.noteKindToPlace;
+
           // update note sprites
-          for (noteSprite in noteSprites)
+          for (noteSprite in chartEditorState.renderedNotes.members)
           {
             if (noteSprite.noteData == note)
             {
-              noteSprite.noteStyle = NoteKindManager.getNoteStyleId(chartEditorState.noteKindToPlace) ?? chartEditorState.currentSongNoteStyle;
-              noteSprites.remove(noteSprite);
+              noteSprite.noteStyle = NoteKindManager.getNoteStyleId(note.kind) ?? chartEditorState.currentSongNoteStyle;
               break;
             }
           }
 
           // update hold note sprites
-          for (holdNoteSprite in holdNoteSprites)
+          for (holdNoteSprite in chartEditorState.renderedHoldNotes.members)
           {
             if (holdNoteSprite.noteData == note)
             {
-              holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(chartEditorState.noteKindToPlace) ?? chartEditorState.currentSongNoteStyle;
-              holdNoteSprites.remove(holdNoteSprite);
+              holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(note.kind) ?? chartEditorState.currentSongNoteStyle;
               break;
             }
           }
-
-          note.kind = chartEditorState.noteKindToPlace;
         }
         chartEditorState.saveDataDirty = true;
         chartEditorState.noteDisplayDirty = true;

From 49d302be956edb19d470ccfdee3792d3fcdae324 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Wed, 5 Jun 2024 19:47:34 +0200
Subject: [PATCH 33/64] make code a bit simpler

---
 .../components/ChartEditorHoldNoteSprite.hx   | 36 ++++++++++++-------
 1 file changed, 24 insertions(+), 12 deletions(-)

diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
index e8ca991b6..e474ee93d 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
@@ -48,15 +48,6 @@ class ChartEditorHoldNoteSprite extends SustainTrail
     super(0, 100, noteStyle);
 
     this.parentState = parent;
-
-    zoom = 1.0;
-    zoom *= noteStyle.fetchHoldNoteScale();
-    zoom *= 0.7;
-    zoom *= ChartEditorState.GRID_SIZE / Strumline.STRUMLINE_SIZE;
-
-    flipY = false;
-
-    setup();
   }
 
   @:nullSafety(Off)
@@ -64,17 +55,38 @@ class ChartEditorHoldNoteSprite extends SustainTrail
   {
     var bruhStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyle);
     setupHoldNoteGraphic(bruhStyle);
+  }
+
+  override function setupHoldNoteGraphic(noteStyle:NoteStyle):Void
+  {
+    loadGraphic(noteStyle.getHoldNoteAssetPath());
+
+    antialiasing = true;
+
+    this.isPixel = noteStyle.isHoldNotePixel();
+    if (isPixel)
+    {
+      endOffset = bottomClip = 1;
+      antialiasing = false;
+    }
 
     zoom = 1.0;
-    zoom *= bruhStyle.fetchHoldNoteScale();
+    zoom *= noteStyle.fetchHoldNoteScale();
     zoom *= 0.7;
     zoom *= ChartEditorState.GRID_SIZE / Strumline.STRUMLINE_SIZE;
 
+    graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2
+    graphicHeight = sustainLength * 0.45; // sustainHeight
+
     flipY = false;
 
-    setup();
+    alpha = 1.0;
 
-    triggerRedraw();
+    updateColorTransform();
+
+    updateClipping();
+
+    setup();
   }
 
   public override function updateHitbox():Void

From c4855c0ca8e6e77d62b32b705c40ab0a0e106c11 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Tue, 11 Jun 2024 20:52:08 +0200
Subject: [PATCH 34/64] fix hold note

---
 source/funkin/play/notes/SustainTrail.hx                     | 5 +++++
 .../debug/charting/components/ChartEditorHoldNoteSprite.hx   | 5 +++++
 2 files changed, 10 insertions(+)

diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx
index 570c05190..90b36b009 100644
--- a/source/funkin/play/notes/SustainTrail.hx
+++ b/source/funkin/play/notes/SustainTrail.hx
@@ -129,6 +129,11 @@ class SustainTrail extends FlxSprite
       endOffset = bottomClip = 1;
       antialiasing = false;
     }
+    else
+    {
+      endOffset = 0.5;
+      bottomClip = 0.9;
+    }
 
     zoom = 1.0;
     zoom *= noteStyle.fetchHoldNoteScale();
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
index e474ee93d..b8d6ee22e 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
@@ -69,6 +69,11 @@ class ChartEditorHoldNoteSprite extends SustainTrail
       endOffset = bottomClip = 1;
       antialiasing = false;
     }
+    else
+    {
+      endOffset = 0.5;
+      bottomClip = 0.9;
+    }
 
     zoom = 1.0;
     zoom *= noteStyle.fetchHoldNoteScale();

From 928de7b8eb8861878b2e78a3502a8ee64b2784d9 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Tue, 11 Jun 2024 23:45:08 +0200
Subject: [PATCH 35/64] check for pixel style if necessary

---
 source/funkin/play/notes/Strumline.hx         |  4 ++--
 source/funkin/play/notes/notekind/NoteKind.hx |  4 ++--
 .../play/notes/notekind/NoteKindManager.hx    | 21 +++++++++++++++----
 .../ui/debug/charting/ChartEditorState.hx     | 17 +++++++++------
 4 files changed, 32 insertions(+), 14 deletions(-)

diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index ebc48a8c7..86b7a3ee1 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -709,7 +709,7 @@ class Strumline extends FlxSpriteGroup
 
     if (noteSprite != null)
     {
-      var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind) ?? this.noteStyle;
+      var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.isHoldNotePixel()) ?? this.noteStyle;
       noteSprite.setupNoteGraphic(noteKindStyle);
 
       noteSprite.direction = note.getDirection();
@@ -731,7 +731,7 @@ class Strumline extends FlxSpriteGroup
 
     if (holdNoteSprite != null)
     {
-      var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind) ?? this.noteStyle;
+      var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.isHoldNotePixel()) ?? this.noteStyle;
       holdNoteSprite.setupHoldNoteGraphic(noteKindStyle);
 
       holdNoteSprite.parentStrumline = this;
diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx
index 53393623a..6d7bad77f 100644
--- a/source/funkin/play/notes/notekind/NoteKind.hx
+++ b/source/funkin/play/notes/notekind/NoteKind.hx
@@ -21,9 +21,9 @@ class NoteKind implements INoteScriptedClass
   /**
    * Custom note style
    */
-  public var noteStyleId:String;
+  public var noteStyleId:Null<String>;
 
-  public function new(noteKind:String, description:String = "", noteStyleId:String = "")
+  public function new(noteKind:String, description:String = "", ?noteStyleId:String)
   {
     this.noteKind = noteKind;
     this.description = description;
diff --git a/source/funkin/play/notes/notekind/NoteKindManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx
index 82b2c4d39..110e1859b 100644
--- a/source/funkin/play/notes/notekind/NoteKindManager.hx
+++ b/source/funkin/play/notes/notekind/NoteKindManager.hx
@@ -65,11 +65,17 @@ class NoteKindManager
   /**
    * Retrieve the note style from the given note kind
    * @param noteKind note kind name
+   * @param isPixel whether to use pixel style
    * @return NoteStyle
    */
-  public static function getNoteStyle(noteKind:String):Null<NoteStyle>
+  public static function getNoteStyle(noteKind:String, isPixel:Bool = false):Null<NoteStyle>
   {
-    var noteStyleId:String = noteKinds.get(noteKind)?.noteStyleId ?? "";
+    var noteStyleId:Null<String> = getNoteStyleId(noteKind, isPixel);
+
+    if (noteStyleId == null)
+    {
+      return null;
+    }
 
     return NoteStyleRegistry.instance.fetchEntry(noteStyleId);
   }
@@ -77,10 +83,17 @@ class NoteKindManager
   /**
    * Retrieve the note style id from the given note kind
    * @param noteKind note kind name
+   * @param isPixel whether to use pixel style
    * @return Null<String>
    */
-  public static function getNoteStyleId(noteKind:String):Null<String>
+  public static function getNoteStyleId(noteKind:String, isPixel:Bool = false):Null<String>
   {
-    return noteKinds.get(noteKind)?.noteStyleId;
+    var noteStyleId:Null<String> = noteKinds.get(noteKind)?.noteStyleId;
+    if (isPixel && noteStyleId != null)
+    {
+      noteStyleId = NoteStyleRegistry.instance.hasEntry('$noteStyleId-pixel') ? '$noteStyleId-pixel' : noteStyleId;
+    }
+
+    return noteStyleId;
   }
 }
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index c867ddfd2..2a07be52d 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -3585,7 +3585,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
         // The note sprite handles animation playback and positioning.
         noteSprite.noteData = noteData;
-        noteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind) ?? currentSongNoteStyle;
+        noteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, isPixelStyle()) ?? currentSongNoteStyle;
         noteSprite.overrideStepTime = null;
         noteSprite.overrideData = null;
 
@@ -3609,7 +3609,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
           holdNoteSprite.setHeightDirectly(noteLengthPixels);
 
-          holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteSprite.noteData.kind) ?? currentSongNoteStyle;
+          holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteSprite.noteData.kind, isPixelStyle()) ?? currentSongNoteStyle;
 
           holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
 
@@ -3675,7 +3675,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         holdNoteSprite.noteDirection = noteData.getDirection();
         holdNoteSprite.setHeightDirectly(noteLengthPixels);
 
-        holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind) ?? currentSongNoteStyle;
+        holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, isPixelStyle()) ?? currentSongNoteStyle;
 
         holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
 
@@ -4574,7 +4574,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
             gridGhostHoldNote.noteData = currentPlaceNoteData;
             gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection();
             gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
-            gridGhostHoldNote.noteStyle = NoteKindManager.getNoteStyleId(currentPlaceNoteData.kind) ?? currentSongNoteStyle;
+            gridGhostHoldNote.noteStyle = NoteKindManager.getNoteStyleId(currentPlaceNoteData.kind, isPixelStyle()) ?? currentSongNoteStyle;
             gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
           }
           else
@@ -4896,7 +4896,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
             {
               noteData.kind = noteKindToPlace;
               noteData.data = cursorColumn;
-              gridGhostNote.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind) ?? currentSongNoteStyle;
+              gridGhostNote.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, isPixelStyle()) ?? currentSongNoteStyle;
               gridGhostNote.playNoteAnimation();
             }
             noteData.time = cursorSnappedMs;
@@ -5288,7 +5288,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         ghostHold.visible = true;
         ghostHold.alpha = 0.6;
         ghostHold.setHeightDirectly(0);
-        ghostHold.noteStyle = NoteKindManager.getNoteStyleId(ghostHold.noteData.kind) ?? currentSongNoteStyle;
+        ghostHold.noteStyle = NoteKindManager.getNoteStyleId(ghostHold.noteData.kind, isPixelStyle()) ?? currentSongNoteStyle;
         ghostHold.updateHoldNotePosition(renderedHoldNotes);
       }
 
@@ -6413,6 +6413,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     return note != null && currentNoteSelection.indexOf(note) != -1;
   }
 
+  function isPixelStyle():Bool
+  {
+    return currentSongNoteStyle == 'pixel';
+  }
+
   override function destroy():Void
   {
     super.destroy();

From 7a0c7ade357a7e6151295b182650dd807e52390f Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Tue, 11 Jun 2024 23:48:05 +0200
Subject: [PATCH 36/64] Update ChartEditorDropdowns.hx

---
 source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
index f20b75650..65ec2a0c3 100644
--- a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
+++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
@@ -126,7 +126,10 @@ class ChartEditorDropdowns
   {
     dropDown.dataSource.clear();
 
-    var noteStyleIds:Array<String> = NoteStyleRegistry.instance.listEntryIds();
+    // hardcoding this because i dont want note kind styles to be shown as well
+    // there is probably a better solution
+    // var noteStyleIds:Array<String> = NoteStyleRegistry.instance.listEntryIds();
+    var noteStyleIds:Array<String> = ['funkin', 'pixel'];
 
     var returnValue:DropDownEntry = {id: "funkin", text: "Funkin'"};
 

From fbcc73dceebdcfcba2b14922c6b7c38adae0b086 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Mon, 17 Jun 2024 17:21:52 +0200
Subject: [PATCH 37/64] unhardcode notestyledropdown

---
 .../ui/debug/charting/util/ChartEditorDropdowns.hx  | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
index 65ec2a0c3..6a426c391 100644
--- a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
+++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
@@ -126,10 +126,7 @@ class ChartEditorDropdowns
   {
     dropDown.dataSource.clear();
 
-    // hardcoding this because i dont want note kind styles to be shown as well
-    // there is probably a better solution
-    // var noteStyleIds:Array<String> = NoteStyleRegistry.instance.listEntryIds();
-    var noteStyleIds:Array<String> = ['funkin', 'pixel'];
+    var noteStyleIds:Array<String> = NoteStyleRegistry.instance.listEntryIds();
 
     var returnValue:DropDownEntry = {id: "funkin", text: "Funkin'"};
 
@@ -138,6 +135,14 @@ class ChartEditorDropdowns
       var noteStyle:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
       if (noteStyle == null) continue;
 
+      // check if the note style has all necessary assets (strums, notes, holdNotes)
+      if (noteStyle._data?.assets?.noteStrumline == null
+        || noteStyle._data?.assets?.note == null
+        || noteStyle._data?.assets?.holdNote == null)
+      {
+        continue;
+      }
+
       var value = {id: noteStyleId, text: noteStyle.getName()};
       if (startingStyleId == noteStyleId) returnValue = value;
 

From acd8912d162d71b3dd11d97e112e64da06815ec9 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Fri, 21 Jun 2024 23:54:29 +0200
Subject: [PATCH 38/64] Add Custom Params For NoteKind

still need to implement the chart editor stuff
---
 source/funkin/play/notes/notekind/NoteKind.hx | 70 ++++++++++++++++++-
 1 file changed, 69 insertions(+), 1 deletion(-)

diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx
index 6d7bad77f..6d7ddcd1b 100644
--- a/source/funkin/play/notes/notekind/NoteKind.hx
+++ b/source/funkin/play/notes/notekind/NoteKind.hx
@@ -23,11 +23,17 @@ class NoteKind implements INoteScriptedClass
    */
   public var noteStyleId:Null<String>;
 
-  public function new(noteKind:String, description:String = "", ?noteStyleId:String)
+  /**
+   * Custom parameters for the chart editor
+   */
+  public var params:Array<NoteKindParam>;
+
+  public function new(noteKind:String, description:String = "", ?noteStyleId:String, ?params:Array<NoteKindParam>)
   {
     this.noteKind = noteKind;
     this.description = description;
     this.noteStyleId = noteStyleId;
+    this.params = params ?? [];
   }
 
   public function toString():String
@@ -35,6 +41,25 @@ class NoteKind implements INoteScriptedClass
     return noteKind;
   }
 
+  /**
+   * Retrieve the param with the given name
+   * If there exists no param with the given name then `null` is returned
+   * @param name Name of the param
+   * @return Null<NoteKindParam>
+   */
+  public function getParam(name:String):Null<NoteKindParam>
+  {
+    for (param in params)
+    {
+      if (param.name == name)
+      {
+        return param;
+      }
+    }
+
+    return null;
+  }
+
   /**
    * Retrieve all notes of this kind
    * @return Array<NoteSprite>
@@ -61,3 +86,46 @@ class NoteKind implements INoteScriptedClass
 
   public function onNoteMiss(event:NoteScriptEvent):Void {}
 }
+
+/**
+ * Abstract for setting the type of the `NoteKindParam`
+ * This was supposed to be an enum but polymod kept being annoying
+ */
+abstract NoteKindParamType(String)
+{
+  public static var STRING:String = "String";
+
+  public static var INT:String = "Int";
+
+  public static var RANGED_INT:String = "RangedInt";
+
+  public static var FLOAT:String = "Float";
+
+  public static var RANGED_FLOAT:String = "RangedFloat";
+}
+
+typedef NoteKindParamData =
+{
+  /**
+   * Only used for `RangedInt` and `RangedFloat`
+   */
+  var min:Null<Float>;
+
+  /**
+   * Only used for `RangedInt` and `RangedFloat`
+   */
+  var max:Null<Float>;
+
+  var value:Dynamic;
+}
+
+/**
+ * Typedef for creating custom parameters in the chart editor
+ */
+typedef NoteKindParam =
+{
+  var name:String;
+  var description:String;
+  var type:NoteKindParamType;
+  var data:NoteKindParamData;
+}

From c8d019da2fdcc22627dbfb78c587a5374273f6b6 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sat, 22 Jun 2024 00:20:58 +0200
Subject: [PATCH 39/64] Update NoteKind.hx

---
 source/funkin/play/notes/notekind/NoteKind.hx | 38 ++++++++++++++++---
 1 file changed, 33 insertions(+), 5 deletions(-)

diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx
index 6d7ddcd1b..3aa02088b 100644
--- a/source/funkin/play/notes/notekind/NoteKind.hx
+++ b/source/funkin/play/notes/notekind/NoteKind.hx
@@ -2,6 +2,7 @@ package funkin.play.notes.notekind;
 
 import funkin.modding.IScriptedClass.INoteScriptedClass;
 import funkin.modding.events.ScriptEvent;
+import flixel.math.FlxMath;
 
 /**
  * Class for note scripts
@@ -42,24 +43,49 @@ class NoteKind implements INoteScriptedClass
   }
 
   /**
-   * Retrieve the param with the given name
+   * Retrieve the value of the param with the given name
    * If there exists no param with the given name then `null` is returned
    * @param name Name of the param
-   * @return Null<NoteKindParam>
+   * @return Null<Dynamic>
    */
-  public function getParam(name:String):Null<NoteKindParam>
+  public function getParam(name:String):Null<Dynamic>
   {
     for (param in params)
     {
       if (param.name == name)
       {
-        return param;
+        return param.data.value;
       }
     }
 
     return null;
   }
 
+  /**
+   * Set the value of the param with the given name
+   * @param name Name of the param
+   * @param value New value
+   */
+  public function setParam(name:String, value:Dynamic):Void
+  {
+    for (param in params)
+    {
+      if (param.name == name)
+      {
+        if (param.type == NoteKindParamType.RANGED_INT || param.type == NoteKindParamType.RANGED_FLOAT)
+        {
+          param.data.value = FlxMath.bound(value, param.data.min, param.data.max);
+        }
+        else
+        {
+          param.data.value = value;
+        }
+
+        break;
+      }
+    }
+  }
+
   /**
    * Retrieve all notes of this kind
    * @return Array<NoteSprite>
@@ -91,7 +117,7 @@ class NoteKind implements INoteScriptedClass
  * Abstract for setting the type of the `NoteKindParam`
  * This was supposed to be an enum but polymod kept being annoying
  */
-abstract NoteKindParamType(String)
+abstract NoteKindParamType(String) to String
 {
   public static var STRING:String = "String";
 
@@ -108,11 +134,13 @@ typedef NoteKindParamData =
 {
   /**
    * Only used for `RangedInt` and `RangedFloat`
+   * If `min` is null, there is no minimum
    */
   var min:Null<Float>;
 
   /**
    * Only used for `RangedInt` and `RangedFloat`
+   * If `max` is null, there is no maximum
    */
   var max:Null<Float>;
 

From 94fe4a06b90089fb8187c177c39cb255c90609fb Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sat, 22 Jun 2024 10:44:12 +0200
Subject: [PATCH 40/64] Update NoteKind.hx

---
 source/funkin/play/notes/notekind/NoteKind.hx | 17 +++++------------
 1 file changed, 5 insertions(+), 12 deletions(-)

diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx
index 3aa02088b..a0f759949 100644
--- a/source/funkin/play/notes/notekind/NoteKind.hx
+++ b/source/funkin/play/notes/notekind/NoteKind.hx
@@ -72,13 +72,12 @@ class NoteKind implements INoteScriptedClass
     {
       if (param.name == name)
       {
-        if (param.type == NoteKindParamType.RANGED_INT || param.type == NoteKindParamType.RANGED_FLOAT)
+        switch (param.type)
         {
-          param.data.value = FlxMath.bound(value, param.data.min, param.data.max);
-        }
-        else
-        {
-          param.data.value = value;
+          case NoteKindParamType.INT | NoteKindParamType.FLOAT:
+            param.data.value = FlxMath.bound(value, param.data.min, param.data.max);
+          default:
+            param.data.value = value;
         }
 
         break;
@@ -123,23 +122,17 @@ abstract NoteKindParamType(String) to String
 
   public static var INT:String = "Int";
 
-  public static var RANGED_INT:String = "RangedInt";
-
   public static var FLOAT:String = "Float";
-
-  public static var RANGED_FLOAT:String = "RangedFloat";
 }
 
 typedef NoteKindParamData =
 {
   /**
-   * Only used for `RangedInt` and `RangedFloat`
    * If `min` is null, there is no minimum
    */
   var min:Null<Float>;
 
   /**
-   * Only used for `RangedInt` and `RangedFloat`
    * If `max` is null, there is no maximum
    */
   var max:Null<Float>;

From 9a563ec46b56e4daacd70f301c1d107347e6738f Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sat, 22 Jun 2024 10:52:26 +0200
Subject: [PATCH 41/64] switch seemingly doesnt work

---
 source/funkin/play/notes/notekind/NoteKind.hx | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx
index a0f759949..f2a44dc8a 100644
--- a/source/funkin/play/notes/notekind/NoteKind.hx
+++ b/source/funkin/play/notes/notekind/NoteKind.hx
@@ -72,12 +72,13 @@ class NoteKind implements INoteScriptedClass
     {
       if (param.name == name)
       {
-        switch (param.type)
+        if (param.type == NoteKindParamType.INT || param.type == NoteKindParamType.FLOAT)
         {
-          case NoteKindParamType.INT | NoteKindParamType.FLOAT:
-            param.data.value = FlxMath.bound(value, param.data.min, param.data.max);
-          default:
-            param.data.value = value;
+          param.data.value = FlxMath.bound(value, param.data.min, param.data.max);
+        }
+        else
+        {
+          param.data.value = value;
         }
 
         break;
@@ -118,11 +119,11 @@ class NoteKind implements INoteScriptedClass
  */
 abstract NoteKindParamType(String) to String
 {
-  public static var STRING:String = "String";
+  public static final STRING:String = 'String';
 
-  public static var INT:String = "Int";
+  public static final INT:String = 'Int';
 
-  public static var FLOAT:String = "Float";
+  public static final FLOAT:String = 'Float';
 }
 
 typedef NoteKindParamData =

From d9b9854d9106d20d2ea7ec75468fcc315b281e98 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sat, 22 Jun 2024 14:17:07 +0200
Subject: [PATCH 42/64] add params to toolbox

not completely finished
---
 .../play/notes/notekind/NoteKindManager.hx    | 10 +++
 .../toolboxes/ChartEditorNoteDataToolbox.hx   | 73 ++++++++++++++++++-
 2 files changed, 80 insertions(+), 3 deletions(-)

diff --git a/source/funkin/play/notes/notekind/NoteKindManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx
index 110e1859b..3e2174a66 100644
--- a/source/funkin/play/notes/notekind/NoteKindManager.hx
+++ b/source/funkin/play/notes/notekind/NoteKindManager.hx
@@ -96,4 +96,14 @@ class NoteKindManager
 
     return noteStyleId;
   }
+
+  /**
+   * Retrive custom params of the given note kind
+   * @param noteKind Name of the note kind
+   * @return Array<NoteKind.NoteKindParam>
+   */
+  public static function getParams(noteKind:String):Array<NoteKind.NoteKindParam>
+  {
+    return noteKinds.get(noteKind)?.params ?? [];
+  }
 }
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
index 531bce255..472372a6e 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
@@ -2,11 +2,12 @@ package funkin.ui.debug.charting.toolboxes;
 
 import haxe.ui.components.DropDown;
 import haxe.ui.components.TextField;
+import haxe.ui.components.Label;
+import haxe.ui.components.NumberStepper;
+import haxe.ui.containers.Grid;
+import haxe.ui.core.Component;
 import haxe.ui.events.UIEvent;
 import funkin.ui.debug.charting.util.ChartEditorDropdowns;
-import funkin.ui.debug.charting.components.ChartEditorNoteSprite;
-import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite;
-import funkin.play.notes.notestyle.NoteStyle;
 import funkin.play.notes.notekind.NoteKindManager;
 
 /**
@@ -16,8 +17,12 @@ import funkin.play.notes.notekind.NoteKindManager;
 @:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/note-data.xml"))
 class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
 {
+  static final DIALOG_HEIGHT:Int = 100;
+
+  var toolboxNotesGrid:Grid;
   var toolboxNotesNoteKind:DropDown;
   var toolboxNotesCustomKind:TextField;
+  var toolboxNotesParams:Array<ToolboxNoteKindParam> = [];
 
   var _initializing:Bool = true;
 
@@ -49,6 +54,7 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
       if (noteKind == '~CUSTOM~')
       {
         showCustom();
+        clearNoteKindParams();
         toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
       }
       else
@@ -56,6 +62,25 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
         hideCustom();
         chartEditorState.noteKindToPlace = noteKind;
         toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
+
+        clearNoteKindParams();
+        for (param in NoteKindManager.getParams(noteKind))
+        {
+          var paramLabel:Label = new Label();
+          paramLabel.value = param.description;
+          paramLabel.verticalAlign = "center";
+          paramLabel.horizontalAlign = "right";
+
+          var paramStepper:NumberStepper = new NumberStepper();
+          paramStepper.min = param.data.min;
+          paramStepper.max = param.data.max;
+          paramStepper.value = param.data.value;
+          paramStepper.precision = 1;
+          paramStepper.step = 0.1;
+          paramStepper.percentWidth = 100;
+
+          addNoteKindParam(paramLabel, paramStepper);
+        }
       }
 
       if (!_initializing && chartEditorState.currentNoteSelection.length > 0)
@@ -110,6 +135,9 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
       }
     };
     toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
+
+    // just to be safe
+    clearNoteKindParams();
   }
 
   public override function refresh():Void
@@ -132,8 +160,47 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
     toolboxNotesCustomKind.hidden = true;
   }
 
+  function addNoteKindParam(label:Label, component:Component):Void
+  {
+    toolboxNotesParams.push({label: label, component: component});
+    toolboxNotesGrid.addComponent(label);
+    toolboxNotesGrid.addComponent(component);
+
+    this.height = Math.max(DIALOG_HEIGHT, DIALOG_HEIGHT - 30 + toolboxNotesParams.length * 30);
+  }
+
+  override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    // toolboxNotesGrid.height + 45
+    // this is what i found out is the calculation by printing this.height and grid.height
+    var heightToSet:Int = Std.int(Math.max(DIALOG_HEIGHT, toolboxNotesGrid.height + 45));
+    if (this.height != heightToSet)
+    {
+      this.height = heightToSet;
+    }
+  }
+
+  function clearNoteKindParams():Void
+  {
+    for (param in toolboxNotesParams)
+    {
+      toolboxNotesGrid.removeComponent(param.component);
+      toolboxNotesGrid.removeComponent(param.label);
+    }
+    toolboxNotesParams = [];
+    this.height = DIALOG_HEIGHT;
+  }
+
   public static function build(chartEditorState:ChartEditorState):ChartEditorNoteDataToolbox
   {
     return new ChartEditorNoteDataToolbox(chartEditorState);
   }
 }
+
+typedef ToolboxNoteKindParam =
+{
+  var label:Label;
+  var component:Component;
+}

From 93475ae8aa94ff5d12872dd5ebe4b1e207d3d85e Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sat, 22 Jun 2024 14:31:07 +0200
Subject: [PATCH 43/64] minimized check

---
 .../charting/toolboxes/ChartEditorNoteDataToolbox.hx     | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
index 472372a6e..c557eef1f 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
@@ -135,9 +135,6 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
       }
     };
     toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
-
-    // just to be safe
-    clearNoteKindParams();
   }
 
   public override function refresh():Void
@@ -173,6 +170,12 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
   {
     super.update(elapsed);
 
+    // current dialog is minimized, dont change the height
+    if (this.minimized)
+    {
+      return;
+    }
+
     // toolboxNotesGrid.height + 45
     // this is what i found out is the calculation by printing this.height and grid.height
     var heightToSet:Int = Std.int(Math.max(DIALOG_HEIGHT, toolboxNotesGrid.height + 45));

From 437cc68ba7af70f4060efc739dfc6d3ff21bbeed Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sat, 22 Jun 2024 14:49:04 +0200
Subject: [PATCH 44/64] adapt to minimize problem

---
 .../toolboxes/ChartEditorNoteDataToolbox.hx        | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
index c557eef1f..278fc1fd6 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
@@ -17,8 +17,18 @@ import funkin.play.notes.notekind.NoteKindManager;
 @:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/note-data.xml"))
 class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
 {
+  // 100 is the height used in note-data.xml
   static final DIALOG_HEIGHT:Int = 100;
 
+  // toolboxNotesGrid.height + 45
+  // this is what i found out by printing this.height and grid.height
+  // and then seeing that this.height is 100 and grid.height is 55
+  static final HEIGHT_OFFSET:Int = 45;
+
+  // minimizing creates a gray bar the bottom, which would obscure the components,
+  // which is why we use an extra offset of 20
+  static final MINIMIZE_FIX:Int = 20;
+
   var toolboxNotesGrid:Grid;
   var toolboxNotesNoteKind:DropDown;
   var toolboxNotesCustomKind:TextField;
@@ -176,9 +186,7 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
       return;
     }
 
-    // toolboxNotesGrid.height + 45
-    // this is what i found out is the calculation by printing this.height and grid.height
-    var heightToSet:Int = Std.int(Math.max(DIALOG_HEIGHT, toolboxNotesGrid.height + 45));
+    var heightToSet:Int = Std.int(Math.max(DIALOG_HEIGHT, toolboxNotesGrid.height + HEIGHT_OFFSET)) + MINIMIZE_FIX;
     if (this.height != heightToSet)
     {
       this.height = heightToSet;

From 0fe726cefa926b2e4de6963acc0284090e2fed99 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sat, 22 Jun 2024 15:38:04 +0200
Subject: [PATCH 45/64] fix min, max

---
 source/funkin/play/notes/notekind/NoteKind.hx | 58 +++----------------
 .../toolboxes/ChartEditorNoteDataToolbox.hx   | 14 ++++-
 2 files changed, 18 insertions(+), 54 deletions(-)

diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx
index f2a44dc8a..7efa93de6 100644
--- a/source/funkin/play/notes/notekind/NoteKind.hx
+++ b/source/funkin/play/notes/notekind/NoteKind.hx
@@ -42,50 +42,6 @@ class NoteKind implements INoteScriptedClass
     return noteKind;
   }
 
-  /**
-   * Retrieve the value of the param with the given name
-   * If there exists no param with the given name then `null` is returned
-   * @param name Name of the param
-   * @return Null<Dynamic>
-   */
-  public function getParam(name:String):Null<Dynamic>
-  {
-    for (param in params)
-    {
-      if (param.name == name)
-      {
-        return param.data.value;
-      }
-    }
-
-    return null;
-  }
-
-  /**
-   * Set the value of the param with the given name
-   * @param name Name of the param
-   * @param value New value
-   */
-  public function setParam(name:String, value:Dynamic):Void
-  {
-    for (param in params)
-    {
-      if (param.name == name)
-      {
-        if (param.type == NoteKindParamType.INT || param.type == NoteKindParamType.FLOAT)
-        {
-          param.data.value = FlxMath.bound(value, param.data.min, param.data.max);
-        }
-        else
-        {
-          param.data.value = value;
-        }
-
-        break;
-      }
-    }
-  }
-
   /**
    * Retrieve all notes of this kind
    * @return Array<NoteSprite>
@@ -131,14 +87,14 @@ typedef NoteKindParamData =
   /**
    * If `min` is null, there is no minimum
    */
-  var min:Null<Float>;
+  ?min:Null<Float>,
 
   /**
    * If `max` is null, there is no maximum
    */
-  var max:Null<Float>;
+  ?max:Null<Float>,
 
-  var value:Dynamic;
+  defaultValue:Dynamic
 }
 
 /**
@@ -146,8 +102,8 @@ typedef NoteKindParamData =
  */
 typedef NoteKindParam =
 {
-  var name:String;
-  var description:String;
-  var type:NoteKindParamType;
-  var data:NoteKindParamData;
+  name:String,
+  description:String,
+  type:NoteKindParamType,
+  data:NoteKindParamData
 }
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
index 278fc1fd6..751af5dff 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
@@ -82,13 +82,21 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
           paramLabel.horizontalAlign = "right";
 
           var paramStepper:NumberStepper = new NumberStepper();
-          paramStepper.min = param.data.min;
-          paramStepper.max = param.data.max;
-          paramStepper.value = param.data.value;
+          paramStepper.value = param.data.defaultValue;
           paramStepper.precision = 1;
           paramStepper.step = 0.1;
           paramStepper.percentWidth = 100;
 
+          // this check should be unnecessary but for some reason even when min or max is null it will set it to 0
+          if (param.data.min != null)
+          {
+            paramStepper.min = param.data.min;
+          }
+          if (param.data.max != null)
+          {
+            paramStepper.max = param.data.max;
+          }
+
           addNoteKindParam(paramLabel, paramStepper);
         }
       }

From 764cdee63de762395f070916369a99bc6d81c80d Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sat, 22 Jun 2024 15:48:07 +0200
Subject: [PATCH 46/64] add more options

---
 source/funkin/play/notes/notekind/NoteKind.hx          | 10 ++++++++++
 .../charting/toolboxes/ChartEditorNoteDataToolbox.hx   | 10 +++++++---
 2 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx
index 7efa93de6..a06670503 100644
--- a/source/funkin/play/notes/notekind/NoteKind.hx
+++ b/source/funkin/play/notes/notekind/NoteKind.hx
@@ -94,6 +94,16 @@ typedef NoteKindParamData =
    */
   ?max:Null<Float>,
 
+  /**
+   * If `step` is null, it will use 1.0
+   */
+  ?step:Null<Float>,
+
+  /**
+   * If `precision` is null, there will be 0 decimal places
+   */
+  ?precision:Null<Int>,
+
   defaultValue:Dynamic
 }
 
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
index 751af5dff..26e495dcc 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
@@ -83,11 +83,11 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
 
           var paramStepper:NumberStepper = new NumberStepper();
           paramStepper.value = param.data.defaultValue;
-          paramStepper.precision = 1;
-          paramStepper.step = 0.1;
           paramStepper.percentWidth = 100;
+          paramStepper.step = param.data.step ?? 1;
 
-          // this check should be unnecessary but for some reason even when min or max is null it will set it to 0
+          // this check should be unnecessary but for some reason
+          // even when these are null it will set it to 0
           if (param.data.min != null)
           {
             paramStepper.min = param.data.min;
@@ -96,6 +96,10 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
           {
             paramStepper.max = param.data.max;
           }
+          if (param.data.precision != null)
+          {
+            paramStepper.precision = param.data.precision;
+          }
 
           addNoteKindParam(paramLabel, paramStepper);
         }

From c41d846df5dfd5331dad37e71aa4d2b18595b3a9 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sat, 22 Jun 2024 20:49:30 +0200
Subject: [PATCH 47/64] store params in chart

chart editor still doesnt fully work
---
 source/funkin/data/song/SongData.hx           | 41 +++++++++++++++++--
 source/funkin/play/notes/NoteSprite.hx        | 34 +++++++++++++++
 .../play/notes/notekind/NoteKindManager.hx    |  5 ++-
 .../ui/debug/charting/ChartEditorState.hx     | 19 ++++++---
 .../toolboxes/ChartEditorNoteDataToolbox.hx   | 24 ++++++++++-
 5 files changed, 111 insertions(+), 12 deletions(-)

diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index 769af8f08..6bbeb4435 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -951,12 +951,18 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
     return this.kind = value;
   }
 
-  public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
+  @:alias("p")
+  @:default([])
+  @:optional
+  public var params:Array<NoteParamData>;
+
+  public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array<NoteParamData>)
   {
     this.time = time;
     this.data = data;
     this.length = length;
     this.kind = kind;
+    this.params = params ?? [];
   }
 
   /**
@@ -1053,7 +1059,7 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
 
   public function clone():SongNoteDataRaw
   {
-    return new SongNoteDataRaw(this.time, this.data, this.length, this.kind);
+    return new SongNoteDataRaw(this.time, this.data, this.length, this.kind, this.params);
   }
 
   public function toString():String
@@ -1069,9 +1075,9 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
 @:forward
 abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
 {
-  public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
+  public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array<NoteParamData>)
   {
-    this = new SongNoteDataRaw(time, data, length, kind);
+    this = new SongNoteDataRaw(time, data, length, kind, params);
   }
 
   public static function buildDirectionName(data:Int, strumlineSize:Int = 4):String
@@ -1183,3 +1189,30 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
       + (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
   }
 }
+
+class NoteParamData implements ICloneable<NoteParamData>
+{
+  @:alias("n")
+  public var name:String;
+
+  @:alias("v")
+  @:jcustomparse(funkin.data.DataParse.dynamicValue)
+  @:jcustomwrite(funkin.data.DataWrite.dynamicValue)
+  public var value:Dynamic;
+
+  public function new(name:String, value:Dynamic)
+  {
+    this.name = name;
+    this.value = value;
+  }
+
+  public function clone():NoteParamData
+  {
+    return new NoteParamData(this.name, this.value);
+  }
+
+  public function toString():String
+  {
+    return 'NoteParamData(${this.name}, ${this.value})';
+  }
+}
diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx
index 17a5e57fc..d8d471496 100644
--- a/source/funkin/play/notes/NoteSprite.hx
+++ b/source/funkin/play/notes/NoteSprite.hx
@@ -1,6 +1,7 @@
 package funkin.play.notes;
 
 import funkin.data.song.SongData.SongNoteData;
+import funkin.data.song.SongData.NoteParamData;
 import funkin.play.notes.notestyle.NoteStyle;
 import flixel.graphics.frames.FlxAtlasFrames;
 import flixel.FlxSprite;
@@ -65,6 +66,22 @@ class NoteSprite extends FunkinSprite
     return this.noteData.kind = value;
   }
 
+  /**
+   * An array of custom parameters for this note
+   */
+  public var params(get, set):Array<NoteParamData>;
+
+  function get_params():Array<NoteParamData>
+  {
+    return this.noteData?.params ?? [];
+  }
+
+  function set_params(value:Array<NoteParamData>):Array<NoteParamData>
+  {
+    if (this.noteData == null) return value;
+    return this.noteData.params = value;
+  }
+
   /**
    * The data of the note (i.e. the direction.)
    */
@@ -154,6 +171,23 @@ class NoteSprite extends FunkinSprite
     this.shader = hsvShader;
   }
 
+  /**
+   * Retrieve the value of the param with the given name
+   * @param name Name of the param
+   * @return Null<Dynamic>
+   */
+  public function getParam(name:String):Null<Dynamic>
+  {
+    for (param in params)
+    {
+      if (param.name == name)
+      {
+        return param.value;
+      }
+    }
+    return null;
+  }
+
   #if FLX_DEBUG
   /**
    * Call this to override how debug bounding boxes are drawn for this sprite.
diff --git a/source/funkin/play/notes/notekind/NoteKindManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx
index 3e2174a66..30eede978 100644
--- a/source/funkin/play/notes/notekind/NoteKindManager.hx
+++ b/source/funkin/play/notes/notekind/NoteKindManager.hx
@@ -5,6 +5,7 @@ import funkin.modding.events.ScriptEvent;
 import funkin.ui.debug.charting.util.ChartEditorDropdowns;
 import funkin.data.notestyle.NoteStyleRegistry;
 import funkin.play.notes.notestyle.NoteStyle;
+import funkin.play.notes.notekind.NoteKind.NoteKindParam;
 
 class NoteKindManager
 {
@@ -100,9 +101,9 @@ class NoteKindManager
   /**
    * Retrive custom params of the given note kind
    * @param noteKind Name of the note kind
-   * @return Array<NoteKind.NoteKindParam>
+   * @return Array<NoteKindParam>
    */
-  public static function getParams(noteKind:String):Array<NoteKind.NoteKindParam>
+  public static function getParams(noteKind:String):Array<NoteKindParam>
   {
     return noteKinds.get(noteKind)?.params ?? [];
   }
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 2a07be52d..22de29849 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -35,6 +35,7 @@ import funkin.data.song.SongData.SongEventData;
 import funkin.data.song.SongData.SongMetadata;
 import funkin.data.song.SongData.SongNoteData;
 import funkin.data.song.SongData.SongOffsets;
+import funkin.data.song.SongData.NoteParamData;
 import funkin.data.song.SongDataUtils;
 import funkin.data.song.SongRegistry;
 import funkin.data.stage.StageData;
@@ -539,6 +540,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
    */
   var noteKindToPlace:Null<String> = null;
 
+  /**
+   * The note params to use for notes being placed in the chart. Defaults to `[]`.
+   */
+  var noteParamsToPlace:Array<NoteParamData> = [];
+
   /**
    * The event type to use for events being placed in the chart. Defaults to `''`.
    */
@@ -2437,7 +2443,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
     gridGhostNote = new ChartEditorNoteSprite(this);
     gridGhostNote.alpha = 0.6;
-    gridGhostNote.noteData = new SongNoteData(0, 0, 0, "");
+    gridGhostNote.noteData = new SongNoteData(0, 0, 0, "", []);
     gridGhostNote.visible = false;
     add(gridGhostNote);
     gridGhostNote.zIndex = 11;
@@ -4731,7 +4737,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
                 else
                 {
                   // Create a note and place it in the chart.
-                  var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace);
+                  var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace, noteParamsToPlace.clone());
 
                   performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
 
@@ -4890,11 +4896,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
             if (gridGhostNote == null) throw "ERROR: Tried to handle cursor, but gridGhostNote is null! Check ChartEditorState.buildGrid()";
 
-            var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, noteKindToPlace);
+            var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, noteKindToPlace,
+              noteParamsToPlace.clone());
 
-            if (cursorColumn != noteData.data || noteKindToPlace != noteData.kind)
+            if (cursorColumn != noteData.data || noteKindToPlace != noteData.kind || noteParamsToPlace != noteData.params)
             {
               noteData.kind = noteKindToPlace;
+              noteData.params = noteParamsToPlace;
               noteData.data = cursorColumn;
               gridGhostNote.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, isPixelStyle()) ?? currentSongNoteStyle;
               gridGhostNote.playNoteAnimation();
@@ -5202,7 +5210,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     if (notesAtPos.length == 0 && !removeNoteInstead)
     {
       trace('Placing note. ${column}');
-      var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace);
+      var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace, noteParamsToPlace.clone());
       performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
       currentLiveInputPlaceNoteData[column] = newNoteData;
     }
@@ -5655,6 +5663,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     FlxG.watch.addQuick('musicTime', audioInstTrack?.time ?? 0.0);
 
     FlxG.watch.addQuick('noteKindToPlace', noteKindToPlace);
+    FlxG.watch.addQuick('noteParamsToPlace', noteParamsToPlace);
     FlxG.watch.addQuick('eventKindToPlace', eventKindToPlace);
 
     FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels);
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
index 26e495dcc..027ffdf81 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
@@ -9,6 +9,8 @@ import haxe.ui.core.Component;
 import haxe.ui.events.UIEvent;
 import funkin.ui.debug.charting.util.ChartEditorDropdowns;
 import funkin.play.notes.notekind.NoteKindManager;
+import funkin.play.notes.notekind.NoteKind.NoteKindParam;
+import funkin.data.song.SongData.NoteParamData;
 
 /**
  * The toolbox which allows modifying information like Note Kind.
@@ -60,6 +62,13 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
 
       trace('ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Note kind changed: $noteKind');
 
+      var noteKindParams:Array<NoteKindParam> = NoteKindManager.getParams(noteKind);
+      var noteParamData:Array<NoteParamData> = [];
+      for (noteKindParam in noteKindParams)
+      {
+        noteParamData.push(new NoteParamData(noteKindParam.name, noteKindParam.data.defaultValue));
+      }
+
       // Edit the note data to place.
       if (noteKind == '~CUSTOM~')
       {
@@ -71,10 +80,11 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
       {
         hideCustom();
         chartEditorState.noteKindToPlace = noteKind;
+        chartEditorState.noteParamsToPlace = noteParamData;
         toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
 
         clearNoteKindParams();
-        for (param in NoteKindManager.getParams(noteKind))
+        for (param in noteKindParams)
         {
           var paramLabel:Label = new Label();
           paramLabel.value = param.description;
@@ -107,10 +117,22 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
 
       if (!_initializing && chartEditorState.currentNoteSelection.length > 0)
       {
+        for (i in 0...toolboxNotesParams.length)
+        {
+          var toolboxComponent:Component = toolboxNotesParams[i].component;
+          toolboxComponent.onChange = function(event:UIEvent) {
+            for (note in chartEditorState.currentNoteSelection)
+            {
+              note.params[i].value = toolboxComponent.value;
+            }
+          }
+        }
+
         for (note in chartEditorState.currentNoteSelection)
         {
           // Edit the note data of any selected notes.
           note.kind = chartEditorState.noteKindToPlace;
+          note.params = noteParamData.clone();
 
           // update note sprites
           for (noteSprite in chartEditorState.renderedNotes.members)

From 44d978531727eba19e694ac0bed8e0f1f9c76306 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sat, 22 Jun 2024 22:36:39 +0200
Subject: [PATCH 48/64] editing them works now

still need to implement String and do some testing
---
 source/funkin/data/song/SongData.hx           |  18 +-
 .../play/notes/notekind/NoteKindManager.hx    |   7 +-
 .../ui/debug/charting/ChartEditorState.hx     |  17 +-
 .../toolboxes/ChartEditorNoteDataToolbox.hx   | 157 +++++++++++-------
 4 files changed, 128 insertions(+), 71 deletions(-)

diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index 6bbeb4435..7bf3f8f19 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -1057,9 +1057,19 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
     _stepLength = null;
   }
 
+  public function cloneParams():Array<NoteParamData>
+  {
+    var params:Array<NoteParamData> = [];
+    for (param in this.params)
+    {
+      params.push(param.clone());
+    }
+    return params;
+  }
+
   public function clone():SongNoteDataRaw
   {
-    return new SongNoteDataRaw(this.time, this.data, this.length, this.kind, this.params);
+    return new SongNoteDataRaw(this.time, this.data, this.length, this.kind, cloneParams());
   }
 
   public function toString():String
@@ -1121,7 +1131,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
       if (other.kind == '' || this.kind == null) return false;
     }
 
-    return this.time == other.time && this.data == other.data && this.length == other.length;
+    return this.time == other.time && this.data == other.data && this.length == other.length && this.params == other.params;
   }
 
   @:op(A != B)
@@ -1140,7 +1150,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
       if (other.kind == '') return true;
     }
 
-    return this.time != other.time || this.data != other.data || this.length != other.length;
+    return this.time != other.time || this.data != other.data || this.length != other.length || this.params != other.params;
   }
 
   @:op(A > B)
@@ -1177,7 +1187,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
 
   public function clone():SongNoteData
   {
-    return new SongNoteData(this.time, this.data, this.length, this.kind);
+    return new SongNoteData(this.time, this.data, this.length, this.kind, this.params);
   }
 
   /**
diff --git a/source/funkin/play/notes/notekind/NoteKindManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx
index 30eede978..8de3cdcca 100644
--- a/source/funkin/play/notes/notekind/NoteKindManager.hx
+++ b/source/funkin/play/notes/notekind/NoteKindManager.hx
@@ -103,8 +103,13 @@ class NoteKindManager
    * @param noteKind Name of the note kind
    * @return Array<NoteKindParam>
    */
-  public static function getParams(noteKind:String):Array<NoteKindParam>
+  public static function getParams(noteKind:Null<String>):Array<NoteKindParam>
   {
+    if (noteKind == null)
+    {
+      return [];
+    }
+
     return noteKinds.get(noteKind)?.params ?? [];
   }
 }
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 22de29849..f7abfba89 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -4737,7 +4737,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
                 else
                 {
                   // Create a note and place it in the chart.
-                  var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace, noteParamsToPlace.clone());
+                  var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace,
+                    ChartEditorState.cloneNoteParams(noteParamsToPlace));
 
                   performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
 
@@ -4897,7 +4898,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
             if (gridGhostNote == null) throw "ERROR: Tried to handle cursor, but gridGhostNote is null! Check ChartEditorState.buildGrid()";
 
             var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, noteKindToPlace,
-              noteParamsToPlace.clone());
+              ChartEditorState.cloneNoteParams(noteParamsToPlace));
 
             if (cursorColumn != noteData.data || noteKindToPlace != noteData.kind || noteParamsToPlace != noteData.params)
             {
@@ -5210,7 +5211,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     if (notesAtPos.length == 0 && !removeNoteInstead)
     {
       trace('Placing note. ${column}');
-      var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace, noteParamsToPlace.clone());
+      var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace, ChartEditorState.cloneNoteParams(noteParamsToPlace));
       performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
       currentLiveInputPlaceNoteData[column] = newNoteData;
     }
@@ -6532,6 +6533,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     }
     return input;
   }
+
+  public static function cloneNoteParams(paramsToClone:Array<NoteParamData>):Array<NoteParamData>
+  {
+    var params:Array<NoteParamData> = [];
+    for (param in paramsToClone)
+    {
+      params.push(param.clone());
+    }
+    return params;
+  }
 }
 
 /**
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
index 027ffdf81..a81cac5c2 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
@@ -62,77 +62,30 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
 
       trace('ChartEditorToolboxHandler.buildToolboxNoteDataLayout() - Note kind changed: $noteKind');
 
-      var noteKindParams:Array<NoteKindParam> = NoteKindManager.getParams(noteKind);
-      var noteParamData:Array<NoteParamData> = [];
-      for (noteKindParam in noteKindParams)
-      {
-        noteParamData.push(new NoteParamData(noteKindParam.name, noteKindParam.data.defaultValue));
-      }
-
       // Edit the note data to place.
       if (noteKind == '~CUSTOM~')
       {
         showCustom();
-        clearNoteKindParams();
         toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
       }
       else
       {
         hideCustom();
         chartEditorState.noteKindToPlace = noteKind;
-        chartEditorState.noteParamsToPlace = noteParamData;
         toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
-
-        clearNoteKindParams();
-        for (param in noteKindParams)
-        {
-          var paramLabel:Label = new Label();
-          paramLabel.value = param.description;
-          paramLabel.verticalAlign = "center";
-          paramLabel.horizontalAlign = "right";
-
-          var paramStepper:NumberStepper = new NumberStepper();
-          paramStepper.value = param.data.defaultValue;
-          paramStepper.percentWidth = 100;
-          paramStepper.step = param.data.step ?? 1;
-
-          // this check should be unnecessary but for some reason
-          // even when these are null it will set it to 0
-          if (param.data.min != null)
-          {
-            paramStepper.min = param.data.min;
-          }
-          if (param.data.max != null)
-          {
-            paramStepper.max = param.data.max;
-          }
-          if (param.data.precision != null)
-          {
-            paramStepper.precision = param.data.precision;
-          }
-
-          addNoteKindParam(paramLabel, paramStepper);
-        }
       }
 
+      createNoteKindParams(noteKind);
+
       if (!_initializing && chartEditorState.currentNoteSelection.length > 0)
       {
-        for (i in 0...toolboxNotesParams.length)
-        {
-          var toolboxComponent:Component = toolboxNotesParams[i].component;
-          toolboxComponent.onChange = function(event:UIEvent) {
-            for (note in chartEditorState.currentNoteSelection)
-            {
-              note.params[i].value = toolboxComponent.value;
-            }
-          }
-        }
-
         for (note in chartEditorState.currentNoteSelection)
         {
           // Edit the note data of any selected notes.
           note.kind = chartEditorState.noteKindToPlace;
-          note.params = noteParamData.clone();
+          trace(note.params);
+          note.params = ChartEditorState.cloneNoteParams(chartEditorState.noteParamsToPlace);
+          trace(note.params);
 
           // update note sprites
           for (noteSprite in chartEditorState.renderedNotes.members)
@@ -187,6 +140,8 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
 
     toolboxNotesNoteKind.value = ChartEditorDropdowns.lookupNoteKind(chartEditorState.noteKindToPlace);
     toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
+
+    createNoteKindParams(chartEditorState.noteKindToPlace);
   }
 
   function showCustom():Void
@@ -201,6 +156,82 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
     toolboxNotesCustomKind.hidden = true;
   }
 
+  function createNoteKindParams(noteKind:Null<String>):Void
+  {
+    clearNoteKindParams();
+
+    var setParamsToPlace:Bool = false;
+    if (!_initializing)
+    {
+      for (note in chartEditorState.currentNoteSelection)
+      {
+        if (note.kind == chartEditorState.noteKindToPlace)
+        {
+          chartEditorState.noteParamsToPlace = ChartEditorState.cloneNoteParams(note.params);
+          setParamsToPlace = true;
+          break;
+        }
+      }
+    }
+
+    var noteKindParams:Array<NoteKindParam> = NoteKindManager.getParams(noteKind);
+
+    for (i in 0...noteKindParams.length)
+    {
+      var param:NoteKindParam = noteKindParams[i];
+
+      var paramLabel:Label = new Label();
+      paramLabel.value = param.description;
+      paramLabel.verticalAlign = "center";
+      paramLabel.horizontalAlign = "right";
+
+      var paramStepper:NumberStepper = new NumberStepper();
+      paramStepper.value = (setParamsToPlace ? chartEditorState.noteParamsToPlace[i].value : param.data.defaultValue);
+      paramStepper.percentWidth = 100;
+      paramStepper.step = param.data.step ?? 1;
+
+      // this check should be unnecessary but for some reason
+      // even when these are null it will set it to 0
+      if (param.data.min != null)
+      {
+        paramStepper.min = param.data.min;
+      }
+      if (param.data.max != null)
+      {
+        paramStepper.max = param.data.max;
+      }
+      if (param.data.precision != null)
+      {
+        paramStepper.precision = param.data.precision;
+      }
+
+      paramStepper.onChange = function(event:UIEvent) {
+        chartEditorState.noteParamsToPlace[i].value = paramStepper.value;
+
+        for (note in chartEditorState.currentNoteSelection)
+        {
+          if (note.params[i].name == param.name)
+          {
+            note.params[i].value = paramStepper.value;
+            trace(note.params[i]);
+          }
+        }
+      }
+
+      addNoteKindParam(paramLabel, paramStepper);
+    }
+
+    if (!setParamsToPlace)
+    {
+      var noteParamData:Array<NoteParamData> = [];
+      for (param in noteKindParams)
+      {
+        noteParamData.push(new NoteParamData(param.name, param.data.defaultValue));
+      }
+      chartEditorState.noteParamsToPlace = noteParamData;
+    }
+  }
+
   function addNoteKindParam(label:Label, component:Component):Void
   {
     toolboxNotesParams.push({label: label, component: component});
@@ -210,6 +241,17 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
     this.height = Math.max(DIALOG_HEIGHT, DIALOG_HEIGHT - 30 + toolboxNotesParams.length * 30);
   }
 
+  function clearNoteKindParams():Void
+  {
+    for (param in toolboxNotesParams)
+    {
+      toolboxNotesGrid.removeComponent(param.component);
+      toolboxNotesGrid.removeComponent(param.label);
+    }
+    toolboxNotesParams = [];
+    this.height = DIALOG_HEIGHT;
+  }
+
   override function update(elapsed:Float):Void
   {
     super.update(elapsed);
@@ -227,17 +269,6 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
     }
   }
 
-  function clearNoteKindParams():Void
-  {
-    for (param in toolboxNotesParams)
-    {
-      toolboxNotesGrid.removeComponent(param.component);
-      toolboxNotesGrid.removeComponent(param.label);
-    }
-    toolboxNotesParams = [];
-    this.height = DIALOG_HEIGHT;
-  }
-
   public static function build(chartEditorState:ChartEditorState):ChartEditorNoteDataToolbox
   {
     return new ChartEditorNoteDataToolbox(chartEditorState);

From 492af8add4e575d7e648f67911d2fafb006c9219 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sun, 23 Jun 2024 15:25:53 +0200
Subject: [PATCH 49/64] String works now

---
 source/funkin/play/notes/notekind/NoteKind.hx |  4 +-
 .../toolboxes/ChartEditorNoteDataToolbox.hx   | 72 ++++++++++++-------
 2 files changed, 49 insertions(+), 27 deletions(-)

diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx
index a06670503..89c175e54 100644
--- a/source/funkin/play/notes/notekind/NoteKind.hx
+++ b/source/funkin/play/notes/notekind/NoteKind.hx
@@ -104,7 +104,7 @@ typedef NoteKindParamData =
    */
   ?precision:Null<Int>,
 
-  defaultValue:Dynamic
+  ?defaultValue:Dynamic
 }
 
 /**
@@ -115,5 +115,5 @@ typedef NoteKindParam =
   name:String,
   description:String,
   type:NoteKindParamType,
-  data:NoteKindParamData
+  ?data:NoteKindParamData
 }
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
index a81cac5c2..264e62c5a 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
@@ -10,6 +10,7 @@ import haxe.ui.events.UIEvent;
 import funkin.ui.debug.charting.util.ChartEditorDropdowns;
 import funkin.play.notes.notekind.NoteKindManager;
 import funkin.play.notes.notekind.NoteKind.NoteKindParam;
+import funkin.play.notes.notekind.NoteKind.NoteKindParamType;
 import funkin.data.song.SongData.NoteParamData;
 
 /**
@@ -83,9 +84,7 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
         {
           // Edit the note data of any selected notes.
           note.kind = chartEditorState.noteKindToPlace;
-          trace(note.params);
           note.params = ChartEditorState.cloneNoteParams(chartEditorState.noteParamsToPlace);
-          trace(note.params);
 
           // update note sprites
           for (noteSprite in chartEditorState.renderedNotes.members)
@@ -185,48 +184,71 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
       paramLabel.verticalAlign = "center";
       paramLabel.horizontalAlign = "right";
 
-      var paramStepper:NumberStepper = new NumberStepper();
-      paramStepper.value = (setParamsToPlace ? chartEditorState.noteParamsToPlace[i].value : param.data.defaultValue);
-      paramStepper.percentWidth = 100;
-      paramStepper.step = param.data.step ?? 1;
+      var paramComponent:Component = null;
 
-      // this check should be unnecessary but for some reason
-      // even when these are null it will set it to 0
-      if (param.data.min != null)
+      final paramType:String = param.type;
+      switch (paramType)
       {
-        paramStepper.min = param.data.min;
-      }
-      if (param.data.max != null)
-      {
-        paramStepper.max = param.data.max;
-      }
-      if (param.data.precision != null)
-      {
-        paramStepper.precision = param.data.precision;
+        case NoteKindParamType.INT | NoteKindParamType.FLOAT:
+          var paramStepper:NumberStepper = new NumberStepper();
+          paramStepper.value = (setParamsToPlace ? chartEditorState.noteParamsToPlace[i].value : param.data?.defaultValue) ?? 0.0;
+          paramStepper.percentWidth = 100;
+          paramStepper.step = param.data?.step ?? 1;
+
+          // this check should be unnecessary but for some reason
+          // even when these are null it will set it to 0
+          if (param.data?.min != null)
+          {
+            paramStepper.min = param.data.min;
+          }
+          if (param.data?.max != null)
+          {
+            paramStepper.max = param.data.max;
+          }
+          if (param.data?.precision != null)
+          {
+            paramStepper.precision = param.data.precision;
+          }
+          paramComponent = paramStepper;
+
+        case NoteKindParamType.STRING:
+          var paramTextField:TextField = new TextField();
+          paramTextField.value = (setParamsToPlace ? chartEditorState.noteParamsToPlace[i].value : param.data?.defaultValue) ?? '';
+          paramTextField.percentWidth = 100;
+          paramComponent = paramTextField;
       }
 
-      paramStepper.onChange = function(event:UIEvent) {
-        chartEditorState.noteParamsToPlace[i].value = paramStepper.value;
+      if (paramComponent == null)
+      {
+        continue;
+      }
+
+      paramComponent.onChange = function(event:UIEvent) {
+        chartEditorState.noteParamsToPlace[i].value = paramComponent.value;
 
         for (note in chartEditorState.currentNoteSelection)
         {
+          if (note.params.length != noteKindParams.length)
+          {
+            break;
+          }
+
           if (note.params[i].name == param.name)
           {
-            note.params[i].value = paramStepper.value;
-            trace(note.params[i]);
+            note.params[i].value = paramComponent.value;
           }
         }
       }
 
-      addNoteKindParam(paramLabel, paramStepper);
+      addNoteKindParam(paramLabel, paramComponent);
     }
 
     if (!setParamsToPlace)
     {
       var noteParamData:Array<NoteParamData> = [];
-      for (param in noteKindParams)
+      for (i in 0...noteKindParams.length)
       {
-        noteParamData.push(new NoteParamData(param.name, param.data.defaultValue));
+        noteParamData.push(new NoteParamData(noteKindParams[i].name, toolboxNotesParams[i].component.value));
       }
       chartEditorState.noteParamsToPlace = noteParamData;
     }

From f9bccb057a129443148e6031c907045c2ff8fb31 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Mon, 24 Jun 2024 21:45:58 +0200
Subject: [PATCH 50/64] use suffixes instead of pixel boolean

---
 source/funkin/play/notes/Strumline.hx         |  4 ++--
 .../play/notes/notekind/NoteKindManager.hx    | 23 +++++++++++--------
 .../ui/debug/charting/ChartEditorState.hx     | 17 +++++---------
 3 files changed, 22 insertions(+), 22 deletions(-)

diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 86b7a3ee1..5e76afa51 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -709,7 +709,7 @@ class Strumline extends FlxSpriteGroup
 
     if (noteSprite != null)
     {
-      var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.isHoldNotePixel()) ?? this.noteStyle;
+      var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id) ?? this.noteStyle;
       noteSprite.setupNoteGraphic(noteKindStyle);
 
       noteSprite.direction = note.getDirection();
@@ -731,7 +731,7 @@ class Strumline extends FlxSpriteGroup
 
     if (holdNoteSprite != null)
     {
-      var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.isHoldNotePixel()) ?? this.noteStyle;
+      var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id) ?? this.noteStyle;
       holdNoteSprite.setupHoldNoteGraphic(noteKindStyle);
 
       holdNoteSprite.parentStrumline = this;
diff --git a/source/funkin/play/notes/notekind/NoteKindManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx
index 8de3cdcca..d97eefcf8 100644
--- a/source/funkin/play/notes/notekind/NoteKindManager.hx
+++ b/source/funkin/play/notes/notekind/NoteKindManager.hx
@@ -66,12 +66,12 @@ class NoteKindManager
   /**
    * Retrieve the note style from the given note kind
    * @param noteKind note kind name
-   * @param isPixel whether to use pixel style
+   * @param suffix Used for song note styles
    * @return NoteStyle
    */
-  public static function getNoteStyle(noteKind:String, isPixel:Bool = false):Null<NoteStyle>
+  public static function getNoteStyle(noteKind:String, ?suffix:String):Null<NoteStyle>
   {
-    var noteStyleId:Null<String> = getNoteStyleId(noteKind, isPixel);
+    var noteStyleId:Null<String> = getNoteStyleId(noteKind, suffix);
 
     if (noteStyleId == null)
     {
@@ -83,16 +83,21 @@ class NoteKindManager
 
   /**
    * Retrieve the note style id from the given note kind
-   * @param noteKind note kind name
-   * @param isPixel whether to use pixel style
+   * @param noteKind Note kind name
+   * @param suffix Used for song note styles
    * @return Null<String>
    */
-  public static function getNoteStyleId(noteKind:String, isPixel:Bool = false):Null<String>
+  public static function getNoteStyleId(noteKind:String, ?suffix:String):Null<String>
   {
-    var noteStyleId:Null<String> = noteKinds.get(noteKind)?.noteStyleId;
-    if (isPixel && noteStyleId != null)
+    if (suffix == null)
     {
-      noteStyleId = NoteStyleRegistry.instance.hasEntry('$noteStyleId-pixel') ? '$noteStyleId-pixel' : noteStyleId;
+      suffix = '';
+    }
+
+    var noteStyleId:Null<String> = noteKinds.get(noteKind)?.noteStyleId;
+    if (noteStyleId != null)
+    {
+      noteStyleId = NoteStyleRegistry.instance.hasEntry('$noteStyleId-$suffix') ? '$noteStyleId-$suffix' : noteStyleId;
     }
 
     return noteStyleId;
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index f7abfba89..d3ddb1bca 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -3591,7 +3591,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
         // The note sprite handles animation playback and positioning.
         noteSprite.noteData = noteData;
-        noteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, isPixelStyle()) ?? currentSongNoteStyle;
+        noteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
         noteSprite.overrideStepTime = null;
         noteSprite.overrideData = null;
 
@@ -3615,7 +3615,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
           holdNoteSprite.setHeightDirectly(noteLengthPixels);
 
-          holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteSprite.noteData.kind, isPixelStyle()) ?? currentSongNoteStyle;
+          holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteSprite.noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
 
           holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
 
@@ -3681,7 +3681,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         holdNoteSprite.noteDirection = noteData.getDirection();
         holdNoteSprite.setHeightDirectly(noteLengthPixels);
 
-        holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, isPixelStyle()) ?? currentSongNoteStyle;
+        holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
 
         holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
 
@@ -4580,7 +4580,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
             gridGhostHoldNote.noteData = currentPlaceNoteData;
             gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection();
             gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
-            gridGhostHoldNote.noteStyle = NoteKindManager.getNoteStyleId(currentPlaceNoteData.kind, isPixelStyle()) ?? currentSongNoteStyle;
+            gridGhostHoldNote.noteStyle = NoteKindManager.getNoteStyleId(currentPlaceNoteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
             gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
           }
           else
@@ -4905,7 +4905,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
               noteData.kind = noteKindToPlace;
               noteData.params = noteParamsToPlace;
               noteData.data = cursorColumn;
-              gridGhostNote.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, isPixelStyle()) ?? currentSongNoteStyle;
+              gridGhostNote.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
               gridGhostNote.playNoteAnimation();
             }
             noteData.time = cursorSnappedMs;
@@ -5297,7 +5297,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         ghostHold.visible = true;
         ghostHold.alpha = 0.6;
         ghostHold.setHeightDirectly(0);
-        ghostHold.noteStyle = NoteKindManager.getNoteStyleId(ghostHold.noteData.kind, isPixelStyle()) ?? currentSongNoteStyle;
+        ghostHold.noteStyle = NoteKindManager.getNoteStyleId(ghostHold.noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
         ghostHold.updateHoldNotePosition(renderedHoldNotes);
       }
 
@@ -6423,11 +6423,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     return note != null && currentNoteSelection.indexOf(note) != -1;
   }
 
-  function isPixelStyle():Bool
-  {
-    return currentSongNoteStyle == 'pixel';
-  }
-
   override function destroy():Void
   {
     super.destroy();

From 4746c1da0eed756e2af8630d572ad9770a5185f1 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Mon, 24 Jun 2024 21:48:09 +0200
Subject: [PATCH 51/64] checking empty suffix is stupid

---
 source/funkin/play/notes/notekind/NoteKindManager.hx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/source/funkin/play/notes/notekind/NoteKindManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx
index d97eefcf8..3040c0a96 100644
--- a/source/funkin/play/notes/notekind/NoteKindManager.hx
+++ b/source/funkin/play/notes/notekind/NoteKindManager.hx
@@ -89,13 +89,13 @@ class NoteKindManager
    */
   public static function getNoteStyleId(noteKind:String, ?suffix:String):Null<String>
   {
-    if (suffix == null)
+    if (suffix == '')
     {
-      suffix = '';
+      suffix = null;
     }
 
     var noteStyleId:Null<String> = noteKinds.get(noteKind)?.noteStyleId;
-    if (noteStyleId != null)
+    if (noteStyleId != null && suffix != null)
     {
       noteStyleId = NoteStyleRegistry.instance.hasEntry('$noteStyleId-$suffix') ? '$noteStyleId-$suffix' : noteStyleId;
     }

From 1fe44fa3686b33f91b99efdb00070e4c59fdee45 Mon Sep 17 00:00:00 2001
From: lemz <ismael.amjad07@gmail.com>
Date: Sat, 29 Jun 2024 15:33:41 +0200
Subject: [PATCH 52/64] add from

---
 source/funkin/play/notes/notekind/NoteKind.hx                  | 2 +-
 .../ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx  | 3 +--
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/source/funkin/play/notes/notekind/NoteKind.hx b/source/funkin/play/notes/notekind/NoteKind.hx
index 89c175e54..c1c6e815a 100644
--- a/source/funkin/play/notes/notekind/NoteKind.hx
+++ b/source/funkin/play/notes/notekind/NoteKind.hx
@@ -73,7 +73,7 @@ class NoteKind implements INoteScriptedClass
  * Abstract for setting the type of the `NoteKindParam`
  * This was supposed to be an enum but polymod kept being annoying
  */
-abstract NoteKindParamType(String) to String
+abstract NoteKindParamType(String) from String to String
 {
   public static final STRING:String = 'String';
 
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
index 264e62c5a..ea46cf72a 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
@@ -186,8 +186,7 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
 
       var paramComponent:Component = null;
 
-      final paramType:String = param.type;
-      switch (paramType)
+      switch (param.type)
       {
         case NoteKindParamType.INT | NoteKindParamType.FLOAT:
           var paramStepper:NumberStepper = new NumberStepper();

From f76309c91ec2cd0ca4bad6163b47fefa2e57228c Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 12 Jul 2024 04:13:20 -0400
Subject: [PATCH 53/64] Update rendering for custom note styles

---
 source/funkin/data/notestyle/NoteStyleData.hx    |  8 ++++++++
 source/funkin/play/notes/NoteSprite.hx           | 16 +++++++++-------
 source/funkin/play/notes/Strumline.hx            |  1 +
 source/funkin/play/notes/notestyle/NoteStyle.hx  | 16 ++++++++++++++--
 .../charting/components/ChartEditorNoteSprite.hx | 10 ++++++++--
 .../toolboxes/ChartEditorNoteDataToolbox.hx      |  2 +-
 .../debug/charting/util/ChartEditorDropdowns.hx  |  6 +++---
 7 files changed, 44 insertions(+), 15 deletions(-)

diff --git a/source/funkin/data/notestyle/NoteStyleData.hx b/source/funkin/data/notestyle/NoteStyleData.hx
index 04fda67ca..fcdb3b4f9 100644
--- a/source/funkin/data/notestyle/NoteStyleData.hx
+++ b/source/funkin/data/notestyle/NoteStyleData.hx
@@ -109,6 +109,14 @@ typedef NoteStyleAssetData<T> =
   @:optional
   var isPixel:Bool;
 
+  /**
+   * If true, animations will be played on the graphic.
+   * @default `false` to save performance.
+   */
+  @:default(false)
+  @:optional
+  var animated:Bool;
+
   /**
    * The structure of this data depends on the asset.
    */
diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx
index d8d471496..e8cacaa4d 100644
--- a/source/funkin/play/notes/NoteSprite.hx
+++ b/source/funkin/play/notes/NoteSprite.hx
@@ -91,7 +91,7 @@ class NoteSprite extends FunkinSprite
   {
     if (frames == null) return value;
 
-    animation.play(DIRECTION_COLORS[value] + 'Scroll');
+    playNoteAnimation(value);
 
     this.direction = value;
     return this.direction;
@@ -152,9 +152,6 @@ class NoteSprite extends FunkinSprite
     this.hsvShader = new HSVShader();
 
     setupNoteGraphic(noteStyle);
-
-    // Disables the update() function for performance.
-    this.active = false;
   }
 
   /**
@@ -165,10 +162,10 @@ class NoteSprite extends FunkinSprite
   {
     noteStyle.buildNoteSprite(this);
 
-    setGraphicSize(Strumline.STRUMLINE_SIZE);
-    updateHitbox();
-
     this.shader = hsvShader;
+
+    // `false` disables the update() function for performance.
+    this.active = noteStyle.isNoteAnimated();
   }
 
   /**
@@ -211,6 +208,11 @@ class NoteSprite extends FunkinSprite
   }
   #end
 
+  function playNoteAnimation(value:Int):Void
+  {
+    animation.play(DIRECTION_COLORS[value] + 'Scroll');
+  }
+
   public function desaturate():Void
   {
     this.hsvShader.saturation = 0.2;
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 5e76afa51..1e5782ad2 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -717,6 +717,7 @@ class Strumline extends FlxSpriteGroup
 
       noteSprite.x = this.x;
       noteSprite.x += getXPos(DIRECTIONS[note.getDirection() % KEY_COUNT]);
+      noteSprite.x -= (noteSprite.width - Strumline.STRUMLINE_SIZE) / 2; // Center it
       noteSprite.x -= NUDGE;
       // noteSprite.x += INITIAL_OFFSET;
       noteSprite.y = -9999;
diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx
index d0cc09f6a..3993cce52 100644
--- a/source/funkin/play/notes/notestyle/NoteStyle.hx
+++ b/source/funkin/play/notes/notestyle/NoteStyle.hx
@@ -89,12 +89,14 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
 
     target.frames = atlas;
 
-    target.scale.x = _data.assets.note.scale;
-    target.scale.y = _data.assets.note.scale;
     target.antialiasing = !_data.assets.note.isPixel;
 
     // Apply the animations.
     buildNoteAnimations(target);
+
+    // Set the scale.
+    target.setGraphicSize(Strumline.STRUMLINE_SIZE * getNoteScale());
+    target.updateHitbox();
   }
 
   var noteFrames:FlxAtlasFrames = null;
@@ -156,6 +158,16 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
     target.animation.addByPrefix('redScroll', rightData.prefix, rightData.frameRate, rightData.looped, rightData.flipX, rightData.flipY);
   }
 
+  public function isNoteAnimated():Bool
+  {
+    return _data.assets.note.animated;
+  }
+
+  public function getNoteScale():Float
+  {
+    return _data.assets.note.scale;
+  }
+
   function fetchNoteAnimationData(dir:NoteDirection):AnimationData
   {
     var result:Null<AnimationData> = switch (dir)
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx
index 009532401..5fd0c74aa 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx
@@ -107,6 +107,12 @@ class ChartEditorNoteSprite extends FlxSprite
     var prefix:String = noteStyle.id.toTitleCase();
 
     var frameCollection:FlxAtlasFrames = Paths.getSparrowAtlas(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary());
+    if (frameCollection == null)
+    {
+      trace('Could not retrieve frame collection for ${noteStyle}: ${Paths.image(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary())}');
+      FlxG.log.error('Could not retrieve frame collection for ${noteStyle}: ${Paths.image(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary())}');
+      return;
+    }
     for (frame in frameCollection.frames)
     {
       // cloning the frame because else
@@ -221,9 +227,9 @@ class ChartEditorNoteSprite extends FlxSprite
     switch (baseAnimationName)
     {
       case 'tap':
-        this.setGraphicSize(0, ChartEditorState.GRID_SIZE);
+        this.setGraphicSize(ChartEditorState.GRID_SIZE, 0);
+        this.updateHitbox();
     }
-    this.updateHitbox();
 
     var bruhStyle:NoteStyle = fetchNoteStyle(this.noteStyle);
     this.antialiasing = !bruhStyle._data?.assets?.note?.isPixel ?? true;
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
index ea46cf72a..12f7f7d63 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorNoteDataToolbox.hx
@@ -283,7 +283,7 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
       return;
     }
 
-    var heightToSet:Int = Std.int(Math.max(DIALOG_HEIGHT, toolboxNotesGrid.height + HEIGHT_OFFSET)) + MINIMIZE_FIX;
+    var heightToSet:Int = Std.int(Math.max(DIALOG_HEIGHT, (toolboxNotesGrid?.height ?? 50) + HEIGHT_OFFSET)) + MINIMIZE_FIX;
     if (this.height != heightToSet)
     {
       this.height = heightToSet;
diff --git a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
index 6a426c391..21938b005 100644
--- a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
+++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
@@ -195,11 +195,11 @@ class ChartEditorDropdowns
   {
     dropDown.dataSource.clear();
 
-    var returnValue:DropDownEntry = lookupNoteKind('~CUSTOM');
+    var returnValue:DropDownEntry = lookupNoteKind('');
 
     for (noteKindId in NOTE_KINDS.keys())
     {
-      var noteKind:String = NOTE_KINDS.get(noteKindId) ?? 'Default';
+      var noteKind:String = NOTE_KINDS.get(noteKindId) ?? 'Unknown';
 
       var value:DropDownEntry = {id: noteKindId, text: noteKind};
       if (startingKindId == noteKindId) returnValue = value;
@@ -216,7 +216,7 @@ class ChartEditorDropdowns
   {
     if (noteKindId == null) return lookupNoteKind('');
     if (!NOTE_KINDS.exists(noteKindId)) return {id: '~CUSTOM~', text: 'Custom'};
-    return {id: noteKindId ?? '', text: NOTE_KINDS.get(noteKindId) ?? 'Default'};
+    return {id: noteKindId ?? '', text: NOTE_KINDS.get(noteKindId) ?? 'Unknown'};
   }
 
   /**

From ace762413e42a211a92ad37544ec22f1c2f3da8d Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 12 Jul 2024 16:39:42 -0400
Subject: [PATCH 54/64] Fix an issue where display would break on invalid note
 styles.

---
 assets                                               |  2 +-
 source/funkin/ui/debug/charting/ChartEditorState.hx  |  4 +++-
 .../charting/components/ChartEditorHoldNoteSprite.hx |  3 ++-
 .../charting/components/ChartEditorNoteSprite.hx     | 12 ++++++++++--
 4 files changed, 16 insertions(+), 5 deletions(-)

diff --git a/assets b/assets
index 005c96f85..4af95a506 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 005c96f85f4304865acb196e7cc4d6d83f9d76d8
+Subproject commit 4af95a506fc62cd683422dfb9c599877b26c27db
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index d3ddb1bca..6f5979c90 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1408,7 +1408,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
   function get_currentSongNoteStyle():String
   {
-    if (currentSongMetadata.playData.noteStyle == null)
+    if (currentSongMetadata.playData.noteStyle == null
+      || currentSongMetadata.playData.noteStyle == ''
+      || currentSongMetadata.playData.noteStyle == 'item')
     {
       // Initialize to the default value if not set.
       currentSongMetadata.playData.noteStyle = Constants.DEFAULT_NOTE_STYLE;
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
index b8d6ee22e..1e631f2cf 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
@@ -53,7 +53,8 @@ class ChartEditorHoldNoteSprite extends SustainTrail
   @:nullSafety(Off)
   function updateHoldNoteGraphic():Void
   {
-    var bruhStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyle);
+    var bruhStyle:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry(noteStyle);
+    if (bruhStyle == null) bruhStyle = NoteStyleRegistry.instance.fetchDefault();
     setupHoldNoteGraphic(bruhStyle);
   }
 
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx
index 5fd0c74aa..c8f40da62 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorNoteSprite.hx
@@ -97,7 +97,9 @@ class ChartEditorNoteSprite extends FlxSprite
 
   function fetchNoteStyle(noteStyleId:String):NoteStyle
   {
-    return NoteStyleRegistry.instance.fetchEntry(noteStyleId) ?? NoteStyleRegistry.instance.fetchDefault();
+    var result = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
+    if (result != null) return result;
+    return NoteStyleRegistry.instance.fetchDefault();
   }
 
   @:access(funkin.play.notes.notestyle.NoteStyle)
@@ -198,7 +200,12 @@ class ChartEditorNoteSprite extends FlxSprite
 
   function get_noteStyle():Null<String>
   {
-    return this.noteStyle ?? this.parentState.currentSongNoteStyle;
+    if (this.noteStyle == null)
+    {
+      var result = this.parentState.currentSongNoteStyle;
+      return result;
+    }
+    return this.noteStyle;
   }
 
   function set_noteStyle(value:Null<String>):Null<String>
@@ -218,6 +225,7 @@ class ChartEditorNoteSprite extends FlxSprite
 
     // Play the appropriate animation for the type, direction, and skin.
     var dirName:String = overrideData != null ? SongNoteData.buildDirectionName(overrideData) : this.noteData.getDirectionName();
+    var noteStyleSuffix:String = this.noteStyle?.toTitleCase() ?? Constants.DEFAULT_NOTE_STYLE.toTitleCase();
     var animationName:String = '${baseAnimationName}${dirName}${this.noteStyle.toTitleCase()}';
 
     this.animation.play(animationName);

From d4cbe74939425c5a86f0ad752e2620e560380e27 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 12 Jul 2024 21:40:46 -0400
Subject: [PATCH 55/64] Smaller fixes tied to note kinds

---
 source/funkin/data/song/importer/FNFLegacyImporter.hx | 2 ++
 source/funkin/modding/PolymodHandler.hx               | 2 ++
 source/funkin/play/PlayState.hx                       | 6 +++---
 source/funkin/play/character/BaseCharacter.hx         | 6 ++++++
 source/funkin/play/notes/notekind/NoteKindManager.hx  | 1 +
 source/funkin/play/stage/Bopper.hx                    | 4 ++--
 6 files changed, 16 insertions(+), 5 deletions(-)

diff --git a/source/funkin/data/song/importer/FNFLegacyImporter.hx b/source/funkin/data/song/importer/FNFLegacyImporter.hx
index acbb99342..96a1051cc 100644
--- a/source/funkin/data/song/importer/FNFLegacyImporter.hx
+++ b/source/funkin/data/song/importer/FNFLegacyImporter.hx
@@ -199,6 +199,8 @@ class FNFLegacyImporter
       {
         // Handle the dumb logic for mustHitSection.
         var noteData = note.data;
+        if (noteData < 0) continue; // Exclude Psych event notes.
+        if (noteData > (STRUMLINE_SIZE * 2)) noteData = noteData % (2 * STRUMLINE_SIZE); // Handle other engine event notes.
 
         // Flip notes if mustHitSection is FALSE (not true lol).
         if (!mustHitSection)
diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx
index c352aa606..59c8707f7 100644
--- a/source/funkin/modding/PolymodHandler.hx
+++ b/source/funkin/modding/PolymodHandler.hx
@@ -7,6 +7,7 @@ import funkin.data.dialogue.speaker.SpeakerRegistry;
 import funkin.data.event.SongEventRegistry;
 import funkin.data.story.level.LevelRegistry;
 import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.play.notes.notekind.NoteKindManager;
 import funkin.data.song.SongRegistry;
 import funkin.data.freeplay.player.PlayerRegistry;
 import funkin.data.stage.StageRegistry;
@@ -383,6 +384,7 @@ class PolymodHandler
     StageRegistry.instance.loadEntries();
 
     CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
+    NoteKindManager.loadScripts();
     ModuleHandler.loadModuleCache();
   }
 }
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index da343f43f..10546cdbd 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1166,6 +1166,9 @@ class PlayState extends MusicBeatSubState
     // super.dispatchEvent(event) dispatches event to module scripts.
     super.dispatchEvent(event);
 
+    // Dispatch event to note kind scripts
+    NoteKindManager.callEvent(event);
+
     // Dispatch event to stage script.
     ScriptEventDispatcher.callEvent(currentStage, event);
 
@@ -1177,9 +1180,6 @@ class PlayState extends MusicBeatSubState
 
     // Dispatch event to conversation script.
     ScriptEventDispatcher.callEvent(currentConversation, event);
-
-    // Dispatch event to note kind scripts
-    NoteKindManager.callEvent(event);
   }
 
   /**
diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx
index 0dab2101a..432881164 100644
--- a/source/funkin/play/character/BaseCharacter.hx
+++ b/source/funkin/play/character/BaseCharacter.hx
@@ -521,6 +521,9 @@ class BaseCharacter extends Bopper
   {
     super.onNoteHit(event);
 
+    // If another script cancelled the event, don't do anything.
+    if (event.eventCanceled) return;
+
     if (event.note.noteData.getMustHitNote() && characterType == BF)
     {
       // If the note is from the same strumline, play the sing animation.
@@ -553,6 +556,9 @@ class BaseCharacter extends Bopper
   {
     super.onNoteMiss(event);
 
+    // If another script cancelled the event, don't do anything.
+    if (event.eventCanceled) return;
+
     if (event.note.noteData.getMustHitNote() && characterType == BF)
     {
       // If the note is from the same strumline, play the sing animation.
diff --git a/source/funkin/play/notes/notekind/NoteKindManager.hx b/source/funkin/play/notes/notekind/NoteKindManager.hx
index 3040c0a96..e17e103d1 100644
--- a/source/funkin/play/notes/notekind/NoteKindManager.hx
+++ b/source/funkin/play/notes/notekind/NoteKindManager.hx
@@ -5,6 +5,7 @@ import funkin.modding.events.ScriptEvent;
 import funkin.ui.debug.charting.util.ChartEditorDropdowns;
 import funkin.data.notestyle.NoteStyleRegistry;
 import funkin.play.notes.notestyle.NoteStyle;
+import funkin.play.notes.notekind.ScriptedNoteKind;
 import funkin.play.notes.notekind.NoteKind.NoteKindParam;
 
 class NoteKindManager
diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx
index 87151de21..fa35b4e15 100644
--- a/source/funkin/play/stage/Bopper.hx
+++ b/source/funkin/play/stage/Bopper.hx
@@ -45,8 +45,8 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
   public var idleSuffix(default, set):String = '';
 
   /**
-   * If this bopper is rendered with pixel art,
-   * disable anti-aliasing and render at 6x scale.
+   * If this bopper is rendered with pixel art, disable anti-aliasing.
+   * @default `false`
    */
   public var isPixel(default, set):Bool = false;
 

From 558ec535320cf87c1ad4e31f3f611758c93a74a8 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Thu, 18 Jul 2024 23:27:12 -0400
Subject: [PATCH 56/64] Switch songs with no difficulties from an error to a
 warning.

---
 source/funkin/play/song/Song.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 91d35d8fa..147923add 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -277,7 +277,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
       // If there are no difficulties in the metadata, there's a problem.
       if (metadata.playData.difficulties.length == 0)
       {
-        throw 'Song $id has no difficulties listed in metadata!';
+        trace('[WARN] Song $id has no difficulties listed in metadata!');
       }
 
       // There may be more difficulties in the chart file than in the metadata,

From 729745899ede349dad49d58309e84b72b6126192 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Thu, 18 Jul 2024 23:27:24 -0400
Subject: [PATCH 57/64] Blacklist haxe.Unserializer in scripts.

---
 source/funkin/modding/PolymodHandler.hx | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx
index c352aa606..c5dfcdca3 100644
--- a/source/funkin/modding/PolymodHandler.hx
+++ b/source/funkin/modding/PolymodHandler.hx
@@ -251,6 +251,10 @@ class PolymodHandler
     // Lib.load() can load malicious DLLs
     Polymod.blacklistImport('cpp.Lib');
 
+    // `Unserializer`
+    // Unserializerr.DEFAULT_RESOLVER.resolveClass() can access blacklisted packages
+    Polymod.blacklistImport('Unserializer');
+
     // `polymod.*`
     // You can probably unblacklist a module
     for (cls in ClassMacro.listClassesInPackage('polymod'))

From 754787553593fe42a2479db09db728192673603d Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Thu, 18 Jul 2024 23:27:41 -0400
Subject: [PATCH 58/64] Allow hiding HUD on launcher builds.

---
 source/funkin/play/PlayState.hx | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 8d7d82aab..a39f10d97 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -578,7 +578,6 @@ class PlayState extends MusicBeatSubState
 
   // TODO: Refactor or document
   var generatedMusic:Bool = false;
-  var perfectMode:Bool = false;
 
   static final BACKGROUND_COLOR:FlxColor = FlxColor.BLACK;
 
@@ -2610,12 +2609,6 @@ class PlayState extends MusicBeatSubState
    */
   function debugKeyShit():Void
   {
-    #if !debug
-    perfectMode = false;
-    #else
-    if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
-    #end
-
     #if CHART_EDITOR_SUPPORTED
     // Open the stage editor overlaying the current state.
     if (controls.DEBUG_STAGE)
@@ -2647,6 +2640,9 @@ class PlayState extends MusicBeatSubState
     #end
 
     #if (debug || FORCE_DEBUG_VERSION)
+    // H: Hide the HUD.
+    if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
+
     // 1: End the song immediately.
     if (FlxG.keys.justPressed.ONE) endSong(true);
 

From 9b8961d4b5c8e150a2c77d39d53566bed5fe6ea7 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Mon, 22 Jul 2024 22:20:51 -0400
Subject: [PATCH 59/64] assets submod

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

diff --git a/assets b/assets
index 4af95a506..aa1231e8c 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 4af95a506fc62cd683422dfb9c599877b26c27db
+Subproject commit aa1231e8cf2990bb902eac3b37815c010fa9919a

From aaab24850df5b3882b2282d5b3c8f6d8174853be Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Mon, 24 Jun 2024 22:21:53 -0400
Subject: [PATCH 60/64] flixel haxelib updates

---
 hmm.json | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/hmm.json b/hmm.json
index 8eaf24212..07216a351 100644
--- a/hmm.json
+++ b/hmm.json
@@ -11,14 +11,14 @@
       "name": "flixel",
       "type": "git",
       "dir": null,
-      "ref": "a7d8e3bad89a0a3506a4714121f73d8e34522c49",
+      "ref": "10c2a203c43a78ff1ff26b8368fd736576829d8d",
       "url": "https://github.com/FunkinCrew/flixel"
     },
     {
       "name": "flixel-addons",
       "type": "git",
       "dir": null,
-      "ref": "a523c3b56622f0640933944171efed46929e360e",
+      "ref": "9c6fb47968e894eb36bf10e94725cd7640c49281",
       "url": "https://github.com/FunkinCrew/flixel-addons"
     },
     {
@@ -30,7 +30,7 @@
       "name": "flixel-ui",
       "type": "git",
       "dir": null,
-      "ref": "719b4f10d94186ed55f6fef1b6618d32abec8c15",
+      "ref": "d0afed7293c71ffdb1184751317fc709b44c9056",
       "url": "https://github.com/HaxeFlixel/flixel-ui"
     },
     {

From a15695c3dc8b3c38d8049cb39db4bbf27f2f1d9a Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Fri, 19 Jul 2024 19:13:58 -0400
Subject: [PATCH 61/64] compile dev version of hxcpp

---
 .github/workflows/build-game.yml | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/build-game.yml b/.github/workflows/build-game.yml
index 07802557c..ba8167607 100644
--- a/.github/workflows/build-game.yml
+++ b/.github/workflows/build-game.yml
@@ -45,7 +45,11 @@ jobs:
       uses: ./.github/actions/setup-haxe
       with:
         gh-token: ${{ steps.app_token.outputs.token }}
-
+    - name: Setup HXCPP dev commit
+      run: |
+        cd .haxelib/hxcpp/git/tools/hxcpp
+        haxe compile.hxml
+        cd ../../../../..
     - name: Build game
       if: ${{ matrix.target == 'windows' }}
       run: |

From 4dfb46955239664439fbb3c64d8bd40a41b1169c Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Mon, 22 Jul 2024 10:12:41 -0400
Subject: [PATCH 62/64] update hxcpp haxelib

---
 hmm.json | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/hmm.json b/hmm.json
index 8eaf24212..387d63f5e 100644
--- a/hmm.json
+++ b/hmm.json
@@ -99,8 +99,10 @@
     },
     {
       "name": "hxcpp",
-      "type": "haxelib",
-      "version": "4.3.2"
+      "type": "git",
+      "dir": null,
+      "url": "https://github.com/HaxeFoundation/hxcpp",
+      "ref": "01cfee282a9a783e10c5a7774a3baaf547e6b0a7"
     },
     {
       "name": "hxcpp-debug-server",

From 3db0cf3e0ab4817f211cd3d8a74c616b15be1239 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 26 Jul 2024 00:22:36 -0400
Subject: [PATCH 63/64] Update Polymod to support applying JSON patches in the
 _merge folder of mods.

---
 hmm.json | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/hmm.json b/hmm.json
index 375fdd0ac..fabd8b51d 100644
--- a/hmm.json
+++ b/hmm.json
@@ -123,6 +123,20 @@
       "ref": "a8c26f18463c98da32f744c214fe02273e1823fa",
       "url": "https://github.com/FunkinCrew/json2object"
     },
+    {
+      "name": "jsonpatch",
+      "type": "git",
+      "dir": null,
+      "ref": "f9b83215acd586dc28754b4ae7f69d4c06c3b4d3",
+      "url": "https://github.com/EliteMasterEric/jsonpatch"
+    },
+    {
+      "name": "jsonpath",
+      "type": "git",
+      "dir": null,
+      "ref": "7a24193717b36393458c15c0435bb7c4470ecdda",
+      "url": "https://github.com/EliteMasterEric/jsonpath"
+    },
     {
       "name": "lime",
       "type": "git",
@@ -169,7 +183,7 @@
       "name": "polymod",
       "type": "git",
       "dir": null,
-      "ref": "bfbe30d81601b3543d80dce580108ad6b7e182c7",
+      "ref": "98945c6c7f5ecde01a32c4623d3515bf012a023a",
       "url": "https://github.com/larsiusprime/polymod"
     },
     {

From f3624f7e76cdd8dcba0762a39ebe1be28389ecef Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Sun, 28 Jul 2024 01:42:09 -0400
Subject: [PATCH 64/64] Fixes for scripted song events, define vocal tracks per
 variation, display suffixed difficulties properly.

---
 source/funkin/data/event/SongEventRegistry.hx |   4 +-
 source/funkin/data/song/CHANGELOG.md          |   7 +
 source/funkin/data/song/SongData.hx           |  59 +++---
 source/funkin/modding/PolymodHandler.hx       |   2 +
 source/funkin/play/PlayState.hx               |   8 +-
 source/funkin/play/song/Song.hx               | 168 ++++++++++++------
 source/funkin/ui/freeplay/FreeplayState.hx    |  30 +++-
 source/funkin/ui/freeplay/SongMenuItem.hx     |   3 +-
 8 files changed, 188 insertions(+), 93 deletions(-)

diff --git a/source/funkin/data/event/SongEventRegistry.hx b/source/funkin/data/event/SongEventRegistry.hx
index 9b0163557..5ee2d39fa 100644
--- a/source/funkin/data/event/SongEventRegistry.hx
+++ b/source/funkin/data/event/SongEventRegistry.hx
@@ -46,7 +46,7 @@ class SongEventRegistry
 
       if (event != null)
       {
-        trace('  Loaded built-in song event: (${event.id})');
+        trace('  Loaded built-in song event: ${event.id}');
         eventCache.set(event.id, event);
       }
       else
@@ -59,9 +59,9 @@ class SongEventRegistry
   static function registerScriptedEvents()
   {
     var scriptedEventClassNames:Array<String> = ScriptedSongEvent.listScriptClasses();
+    trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
     if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0) return;
 
-    trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
     for (eventCls in scriptedEventClassNames)
     {
       var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN");
diff --git a/source/funkin/data/song/CHANGELOG.md b/source/funkin/data/song/CHANGELOG.md
index 4f1c66ade..ca36a1d6d 100644
--- a/source/funkin/data/song/CHANGELOG.md
+++ b/source/funkin/data/song/CHANGELOG.md
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [2.2.4]
+### Added
+- Added `playData.characters.opponentVocals` to specify which vocal track(s) to play for the opponent.
+  - If the value isn't present, it will use the `playData.characters.opponent`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the opponent)
+- Added `playData.characters.playerVocals` to specify which vocal track(s) to play for the player.
+  - If the value isn't present, it will use the `playData.characters.player`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the player)
+
 ## [2.2.3]
 ### Added
 - Added `charter` field to denote authorship of a chart.
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index 7bf3f8f19..f487eb54d 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -529,12 +529,26 @@ class SongCharacterData implements ICloneable<SongCharacterData>
   @:default([])
   public var altInstrumentals:Array<String> = [];
 
-  public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '')
+  @:optional
+  public var opponentVocals:Null<Array<String>> = null;
+
+  @:optional
+  public var playerVocals:Null<Array<String>> = null;
+
+  public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '', ?altInstrumentals:Array<String>,
+      ?opponentVocals:Array<String>, ?playerVocals:Array<String>)
   {
     this.player = player;
     this.girlfriend = girlfriend;
     this.opponent = opponent;
     this.instrumental = instrumental;
+
+    this.altInstrumentals = altInstrumentals;
+    this.opponentVocals = opponentVocals;
+    this.playerVocals = playerVocals;
+
+    if (opponentVocals == null) this.opponentVocals = [opponent];
+    if (playerVocals == null) this.playerVocals = [player];
   }
 
   public function clone():SongCharacterData
@@ -722,18 +736,6 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
   {
     return new SongEventDataRaw(this.time, this.eventKind, this.value);
   }
-}
-
-/**
- * Wrap SongEventData in an abstract so we can overload operators.
- */
-@:forward(time, eventKind, value, activated, getStepTime, clone)
-abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
-{
-  public function new(time:Float, eventKind:String, value:Dynamic = null)
-  {
-    this = new SongEventDataRaw(time, eventKind, value);
-  }
 
   public function valueAsStruct(?defaultKey:String = "key"):Dynamic
   {
@@ -757,27 +759,27 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
     }
   }
 
-  public inline function getHandler():Null<SongEvent>
+  public function getHandler():Null<SongEvent>
   {
     return SongEventRegistry.getEvent(this.eventKind);
   }
 
-  public inline function getSchema():Null<SongEventSchema>
+  public function getSchema():Null<SongEventSchema>
   {
     return SongEventRegistry.getEventSchema(this.eventKind);
   }
 
-  public inline function getDynamic(key:String):Null<Dynamic>
+  public function getDynamic(key:String):Null<Dynamic>
   {
     return this.value == null ? null : Reflect.field(this.value, key);
   }
 
-  public inline function getBool(key:String):Null<Bool>
+  public function getBool(key:String):Null<Bool>
   {
     return this.value == null ? null : cast Reflect.field(this.value, key);
   }
 
-  public inline function getInt(key:String):Null<Int>
+  public function getInt(key:String):Null<Int>
   {
     if (this.value == null) return null;
     var result = Reflect.field(this.value, key);
@@ -787,7 +789,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
     return cast result;
   }
 
-  public inline function getFloat(key:String):Null<Float>
+  public function getFloat(key:String):Null<Float>
   {
     if (this.value == null) return null;
     var result = Reflect.field(this.value, key);
@@ -797,17 +799,17 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
     return cast result;
   }
 
-  public inline function getString(key:String):String
+  public function getString(key:String):String
   {
     return this.value == null ? null : cast Reflect.field(this.value, key);
   }
 
-  public inline function getArray(key:String):Array<Dynamic>
+  public function getArray(key:String):Array<Dynamic>
   {
     return this.value == null ? null : cast Reflect.field(this.value, key);
   }
 
-  public inline function getBoolArray(key:String):Array<Bool>
+  public function getBoolArray(key:String):Array<Bool>
   {
     return this.value == null ? null : cast Reflect.field(this.value, key);
   }
@@ -839,6 +841,19 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
 
     return result;
   }
+}
+
+/**
+ * Wrap SongEventData in an abstract so we can overload operators.
+ */
+@:forward(time, eventKind, value, activated, getStepTime, clone, getHandler, getSchema, getDynamic, getBool, getInt, getFloat, getString, getArray,
+  getBoolArray, buildTooltip, valueAsStruct)
+abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
+{
+  public function new(time:Float, eventKind:String, value:Dynamic = null)
+  {
+    this = new SongEventDataRaw(time, eventKind, value);
+  }
 
   public function clone():SongEventData
   {
diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx
index 9a9ef9e66..5767199ba 100644
--- a/source/funkin/modding/PolymodHandler.hx
+++ b/source/funkin/modding/PolymodHandler.hx
@@ -234,6 +234,8 @@ class PolymodHandler
     // NOTE: Scripted classes are automatically aliased to their parent class.
     Polymod.addImportAlias('flixel.math.FlxPoint', flixel.math.FlxPoint.FlxBasePoint);
 
+    Polymod.addImportAlias('funkin.data.event.SongEventSchema', funkin.data.event.SongEventSchema.SongEventSchemaRaw);
+
     // Add blacklisting for prohibited classes and packages.
 
     // `Sys`
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 32b6e7b62..871c784df 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -580,6 +580,8 @@ class PlayState extends MusicBeatSubState
   // TODO: Refactor or document
   var generatedMusic:Bool = false;
 
+  var skipEndingTransition:Bool = false;
+
   static final BACKGROUND_COLOR:FlxColor = FlxColor.BLACK;
 
   /**
@@ -1926,7 +1928,9 @@ class PlayState extends MusicBeatSubState
       return;
     }
 
-    FlxG.sound.music.onComplete = endSong.bind(false);
+    FlxG.sound.music.onComplete = function() {
+      endSong(skipEndingTransition);
+    };
     // A negative instrumental offset means the song skips the first few milliseconds of the track.
     // This just gets added into the startTimestamp behavior so we don't need to do anything extra.
     FlxG.sound.music.play(true, startTimestamp - Conductor.instance.instrumentalOffset);
@@ -1965,7 +1969,7 @@ class PlayState extends MusicBeatSubState
     if (vocals == null) return;
 
     // Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.)
-    if (!FlxG.sound.music.playing) return;
+    if (!(FlxG?.sound?.music?.playing ?? false)) return;
 
     vocals.pause();
 
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 147923add..2e7e13f51 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -494,6 +494,24 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
     return diffFiltered;
   }
 
+  public function listSuffixedDifficulties(variationIds:Array<String>, ?showLocked:Bool, ?showHidden:Bool):Array<String>
+  {
+    var result = [];
+
+    for (variation in variationIds)
+    {
+      var difficulties = listDifficulties(variation, null, showLocked, showHidden);
+      for (difficulty in difficulties)
+      {
+        var suffixedDifficulty = (variation != Constants.DEFAULT_VARIATION
+          && variation != 'erect') ? '$difficulty-${variation}' : difficulty;
+        result.push(suffixedDifficulty);
+      }
+    }
+
+    return result;
+  }
+
   public function hasDifficulty(diffId:String, ?variationId:String, ?variationIds:Array<String>):Bool
   {
     if (variationIds == null) variationIds = [];
@@ -706,10 +724,11 @@ class SongDifficulty
    * Cache the vocals for a given character.
    * @param id The character we are about to play.
    */
-  public inline function cacheVocals():Void
+  public function cacheVocals():Void
   {
     for (voice in buildVoiceList())
     {
+      trace('Caching vocal track: $voice');
       FlxG.sound.cache(voice);
     }
   }
@@ -721,6 +740,20 @@ class SongDifficulty
    * @param id The character we are about to play.
    */
   public function buildVoiceList():Array<String>
+  {
+    var result:Array<String> = [];
+    result = result.concat(buildPlayerVoiceList());
+    result = result.concat(buildOpponentVoiceList());
+    if (result.length == 0)
+    {
+      var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
+      // Try to use `Voices.ogg` if no other voices are found.
+      if (Assets.exists(Paths.voices(this.song.id, ''))) result.push(Paths.voices(this.song.id, '$suffix'));
+    }
+    return result;
+  }
+
+  public function buildPlayerVoiceList():Array<String>
   {
     var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
 
@@ -728,62 +761,88 @@ class SongDifficulty
     // For example, if `Voices-bf-car-erect.ogg` does not exist, check for `Voices-bf-erect.ogg`.
     // Then, check for  `Voices-bf-car.ogg`, then `Voices-bf.ogg`.
 
-    var playerId:String = characters.player;
-    var voicePlayer:String = Paths.voices(this.song.id, '-$playerId$suffix');
-    while (voicePlayer != null && !Assets.exists(voicePlayer))
+    if (characters.playerVocals == null)
     {
-      // Remove the last suffix.
-      // For example, bf-car becomes bf.
-      playerId = playerId.split('-').slice(0, -1).join('-');
-      // Try again.
-      voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
-    }
-    if (voicePlayer == null)
-    {
-      // Try again without $suffix.
-      playerId = characters.player;
-      voicePlayer = Paths.voices(this.song.id, '-${playerId}');
-      while (voicePlayer != null && !Assets.exists(voicePlayer))
+      var playerId:String = characters.player;
+      var playerVoice:String = Paths.voices(this.song.id, '-${playerId}$suffix');
+
+      while (playerVoice != null && !Assets.exists(playerVoice))
       {
         // Remove the last suffix.
+        // For example, bf-car becomes bf.
         playerId = playerId.split('-').slice(0, -1).join('-');
         // Try again.
-        voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
+        playerVoice = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
+      }
+      if (playerVoice == null)
+      {
+        // Try again without $suffix.
+        playerId = characters.player;
+        playerVoice = Paths.voices(this.song.id, '-${playerId}');
+        while (playerVoice != null && !Assets.exists(playerVoice))
+        {
+          // Remove the last suffix.
+          playerId = playerId.split('-').slice(0, -1).join('-');
+          // Try again.
+          playerVoice = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
+        }
       }
-    }
 
-    var opponentId:String = characters.opponent;
-    var voiceOpponent:String = Paths.voices(this.song.id, '-${opponentId}$suffix');
-    while (voiceOpponent != null && !Assets.exists(voiceOpponent))
-    {
-      // Remove the last suffix.
-      opponentId = opponentId.split('-').slice(0, -1).join('-');
-      // Try again.
-      voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
+      return playerVoice != null ? [playerVoice] : [];
     }
-    if (voiceOpponent == null)
+    else
     {
-      // Try again without $suffix.
-      opponentId = characters.opponent;
-      voiceOpponent = Paths.voices(this.song.id, '-${opponentId}');
-      while (voiceOpponent != null && !Assets.exists(voiceOpponent))
+      // The metadata explicitly defines the list of voices.
+      var playerIds:Array<String> = characters?.playerVocals ?? [characters.player];
+      var playerVoices:Array<String> = playerIds.map((id) -> Paths.voices(this.song.id, '-$id$suffix'));
+
+      return playerVoices;
+    }
+  }
+
+  public function buildOpponentVoiceList():Array<String>
+  {
+    var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
+
+    // Automatically resolve voices by removing suffixes.
+    // For example, if `Voices-bf-car-erect.ogg` does not exist, check for `Voices-bf-erect.ogg`.
+    // Then, check for  `Voices-bf-car.ogg`, then `Voices-bf.ogg`.
+
+    if (characters.opponentVocals == null)
+    {
+      var opponentId:String = characters.opponent;
+      var opponentVoice:String = Paths.voices(this.song.id, '-${opponentId}$suffix');
+      while (opponentVoice != null && !Assets.exists(opponentVoice))
       {
         // Remove the last suffix.
         opponentId = opponentId.split('-').slice(0, -1).join('-');
         // Try again.
-        voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
+        opponentVoice = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
+      }
+      if (opponentVoice == null)
+      {
+        // Try again without $suffix.
+        opponentId = characters.opponent;
+        opponentVoice = Paths.voices(this.song.id, '-${opponentId}');
+        while (opponentVoice != null && !Assets.exists(opponentVoice))
+        {
+          // Remove the last suffix.
+          opponentId = opponentId.split('-').slice(0, -1).join('-');
+          // Try again.
+          opponentVoice = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
+        }
       }
-    }
 
-    var result:Array<String> = [];
-    if (voicePlayer != null) result.push(voicePlayer);
-    if (voiceOpponent != null) result.push(voiceOpponent);
-    if (voicePlayer == null && voiceOpponent == null)
-    {
-      // Try to use `Voices.ogg` if no other voices are found.
-      if (Assets.exists(Paths.voices(this.song.id, ''))) result.push(Paths.voices(this.song.id, '$suffix'));
+      return opponentVoice != null ? [opponentVoice] : [];
+    }
+    else
+    {
+      // The metadata explicitly defines the list of voices.
+      var opponentIds:Array<String> = characters?.opponentVocals ?? [characters.opponent];
+      var opponentVoices:Array<String> = opponentIds.map((id) -> Paths.voices(this.song.id, '-$id$suffix'));
+
+      return opponentVoices;
     }
-    return result;
   }
 
   /**
@@ -795,26 +854,19 @@ class SongDifficulty
   {
     var result:VoicesGroup = new VoicesGroup();
 
-    var voiceList:Array<String> = buildVoiceList();
-
-    if (voiceList.length == 0)
-    {
-      trace('Could not find any voices for song ${this.song.id}');
-      return result;
-    }
+    var playerVoiceList:Array<String> = this.buildPlayerVoiceList();
+    var opponentVoiceList:Array<String> = this.buildOpponentVoiceList();
 
     // Add player vocals.
-    if (voiceList[0] != null) result.addPlayerVoice(FunkinSound.load(voiceList[0]));
-    // Add opponent vocals.
-    if (voiceList[1] != null) result.addOpponentVoice(FunkinSound.load(voiceList[1]));
-
-    // Add additional vocals.
-    if (voiceList.length > 2)
+    for (playerVoice in playerVoiceList)
     {
-      for (i in 2...voiceList.length)
-      {
-        result.add(FunkinSound.load(Assets.getSound(voiceList[i])));
-      }
+      result.addPlayerVoice(FunkinSound.load(playerVoice));
+    }
+
+    // Add opponent vocals.
+    for (opponentVoice in opponentVoiceList)
+    {
+      result.addOpponentVoice(FunkinSound.load(opponentVoice));
     }
 
     result.playerVoicesOffset = offsets.getVocalOffset(characters.player);
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index dc42bd651..2341f04a6 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -339,7 +339,7 @@ class FreeplayState extends MusicBeatSubState
         // Only display songs which actually have available difficulties for the current character.
         var displayedVariations = song.getVariationsByCharacter(currentCharacter);
         trace('Displayed Variations (${songId}): $displayedVariations');
-        var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false);
+        var availableDifficultiesForSong:Array<String> = song.listSuffixedDifficulties(displayedVariations, false, false);
         trace('Available Difficulties: $availableDifficultiesForSong');
         if (availableDifficultiesForSong.length == 0) continue;
 
@@ -1120,7 +1120,7 @@ class FreeplayState extends MusicBeatSubState
 
               // NOW we can interact with the menu
               busy = false;
-              grpCapsules.members[curSelected].sparkle.alpha = 0.7;
+              capsule.sparkle.alpha = 0.7;
               playCurSongPreview(capsule);
             }, null);
 
@@ -1674,6 +1674,9 @@ class FreeplayState extends MusicBeatSubState
           songCapsule.init(null, null, null);
         }
       }
+
+      // Reset the song preview in case we changed variations (normal->erect etc)
+      playCurSongPreview();
     }
 
     // Set the album graphic and play the animation if relevant.
@@ -1912,8 +1915,10 @@ class FreeplayState extends MusicBeatSubState
     }
   }
 
-  public function playCurSongPreview(daSongCapsule:SongMenuItem):Void
+  public function playCurSongPreview(?daSongCapsule:SongMenuItem):Void
   {
+    if (daSongCapsule == null) daSongCapsule = grpCapsules.members[curSelected];
+
     if (curSelected == 0)
     {
       FunkinSound.playMusic('freeplayRandom',
@@ -2145,7 +2150,7 @@ class FreeplaySongData
 
   function updateValues(variations:Array<String>):Void
   {
-    this.songDifficulties = song.listDifficulties(null, variations, false, false);
+    this.songDifficulties = song.listSuffixedDifficulties(variations, false, false);
     if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
 
     var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, null, variations);
@@ -2207,15 +2212,26 @@ class DifficultySprite extends FlxSprite
 
     difficultyId = diffId;
 
-    if (Assets.exists(Paths.file('images/freeplay/freeplay${diffId}.xml')))
+    var assetDiffId:String = diffId;
+    while (!Assets.exists(Paths.image('freeplay/freeplay${assetDiffId}')))
     {
-      this.frames = Paths.getSparrowAtlas('freeplay/freeplay${diffId}');
+      // Remove the last suffix of the difficulty id until we find an asset or there are no more suffixes.
+      var assetDiffIdParts:Array<String> = assetDiffId.split('-');
+      assetDiffIdParts.pop();
+      if (assetDiffIdParts.length == 0) break;
+      assetDiffId = assetDiffIdParts.join('-');
+    }
+
+    // Check for an XML to use an animation instead of an image.
+    if (Assets.exists(Paths.file('images/freeplay/freeplay${assetDiffId}.xml')))
+    {
+      this.frames = Paths.getSparrowAtlas('freeplay/freeplay${assetDiffId}');
       this.animation.addByPrefix('idle', 'idle0', 24, true);
       if (Preferences.flashingLights) this.animation.play('idle');
     }
     else
     {
-      this.loadGraphic(Paths.image('freeplay/freeplay' + diffId));
+      this.loadGraphic(Paths.image('freeplay/freeplay' + assetDiffId));
     }
   }
 }
diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index 2eec83223..b4409d377 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -162,7 +162,7 @@ class SongMenuItem extends FlxSpriteGroup
 
     sparkle = new FlxSprite(ranking.x, ranking.y);
     sparkle.frames = Paths.getSparrowAtlas('freeplay/sparkle');
-    sparkle.animation.addByPrefix('sparkle', 'sparkle', 24, false);
+    sparkle.animation.addByPrefix('sparkle', 'sparkle Export0', 24, false);
     sparkle.animation.play('sparkle', true);
     sparkle.scale.set(0.8, 0.8);
     sparkle.blend = BlendMode.ADD;
@@ -523,7 +523,6 @@ class SongMenuItem extends FlxSpriteGroup
     checkWeek(songData?.songId);
   }
 
-
   var frameInTicker:Float = 0;
   var frameInTypeBeat:Int = 0;