From d0f81add959c5418e3dbeb5c61fc933afb0dad51 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Mon, 4 Mar 2024 21:19:24 -0500
Subject: [PATCH 01/31] Fix a bug where Chart Editor Playtest destroys the
 vocals and crashes

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

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index d090b4f8a..d7cc1493c 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -2787,14 +2787,19 @@ class PlayState extends MusicBeatSubState
       // TODO: Uncache the song.
     }
 
-    if (!overrideMusic)
+    if (overrideMusic)
     {
-      // Stop the music.
+      // Stop the music. Do NOT destroy it, something still references it!
       FlxG.sound.music.pause();
-      if (vocals != null) vocals.stop();
+      if (vocals != null)
+      {
+        vocals.pause();
+        remove(vocals);
+      }
     }
     else
     {
+      // Stop and destroy the music.
       FlxG.sound.music.pause();
       if (vocals != null)
       {

From ae5f48c29cf86fa1c9691eb1c6d854a5c1712d6b Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 5 Mar 2024 00:22:29 -0500
Subject: [PATCH 02/31] Fix issues with story mode colors breaking

---
 source/funkin/ui/story/Level.hx          | 11 +++++++++--
 source/funkin/ui/story/LevelProp.hx      | 14 ++++++++++++--
 source/funkin/ui/story/StoryMenuState.hx |  7 ++++---
 3 files changed, 25 insertions(+), 7 deletions(-)

diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx
index c93ad41a6..b548b7b1e 100644
--- a/source/funkin/ui/story/Level.hx
+++ b/source/funkin/ui/story/Level.hx
@@ -201,8 +201,15 @@ class Level implements IRegistryEntry<LevelData>
       if (existingProp != null)
       {
         existingProp.propData = propData;
-        existingProp.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex;
-        existingProp.visible = true;
+        if (existingProp.propData == null)
+        {
+          existingProp.visible = false;
+        }
+        else
+        {
+          existingProp.visible = true;
+          existingProp.x = propData.offsets[0] + FlxG.width * 0.25 * propIndex;
+        }
       }
       else
       {
diff --git a/source/funkin/ui/story/LevelProp.hx b/source/funkin/ui/story/LevelProp.hx
index 5af383de9..b126f0243 100644
--- a/source/funkin/ui/story/LevelProp.hx
+++ b/source/funkin/ui/story/LevelProp.hx
@@ -11,11 +11,11 @@ class LevelProp extends Bopper
   function set_propData(value:LevelPropData):LevelPropData
   {
     // Only reset the prop if the asset path has changed.
-    if (propData == null || value.assetPath != this.propData.assetPath)
+    if (propData == null || value?.assetPath != propData?.assetPath)
     {
       this.visible = (value != null);
       this.propData = value;
-      danceEvery = this.propData.danceEvery;
+      danceEvery = this.propData?.danceEvery ?? 0;
       applyData();
     }
 
@@ -35,6 +35,16 @@ class LevelProp extends Bopper
 
   function applyData():Void
   {
+    if (propData == null)
+    {
+      this.visible = false;
+      return;
+    }
+    else
+    {
+      this.visible = true;
+    }
+
     var isAnimated:Bool = propData.animations.length > 0;
     if (isAnimated)
     {
diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index ba1d2ed21..8b477feee 100644
--- a/source/funkin/ui/story/StoryMenuState.hx
+++ b/source/funkin/ui/story/StoryMenuState.hx
@@ -141,10 +141,10 @@ class StoryMenuState extends MusicBeatState
 
     persistentUpdate = persistentDraw = true;
 
-    updateData();
-
     rememberSelection();
 
+    updateData();
+
     // Explicitly define the background color.
     this.bgColor = FlxColor.BLACK;
 
@@ -403,7 +403,8 @@ class StoryMenuState extends MusicBeatState
 
   function hasModdedLevels():Bool
   {
-    return LevelRegistry.instance.listModdedLevelIds().length > 0;
+    return false;
+    // return LevelRegistry.instance.listModdedLevelIds().length > 0;
   }
 
   /**

From 9212ea9c90dad5902a9e4c4a93b162897647e0d5 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 5 Mar 2024 02:29:44 -0500
Subject: [PATCH 03/31] Script fixes for 2hot explosions breaking Polymod

---
 assets                                  |   2 +-
 hmm.json                                |   2 +-
 source/funkin/data/level/LevelData.hx   |  22 +++-
 source/funkin/modding/PolymodHandler.hx | 130 ++++++++++++++++--------
 source/funkin/ui/story/Level.hx         |  51 ++++++++--
 5 files changed, 152 insertions(+), 55 deletions(-)

diff --git a/assets b/assets
index 55c602f2a..14b86f436 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 55c602f2adbbd84de541ea86e5e646c4d2a1df0b
+Subproject commit 14b86f4369fddf61eb76116139eb33fa8f6e92d0
diff --git a/hmm.json b/hmm.json
index 836b01c9b..cbd5fea30 100644
--- a/hmm.json
+++ b/hmm.json
@@ -146,7 +146,7 @@
       "name": "polymod",
       "type": "git",
       "dir": null,
-      "ref": "d5a3b8995f64d20b95f844454e8c3b38c3d3a9fa",
+      "ref": "be712450e5d3ba446008884921bb56873b299a64",
       "url": "https://github.com/larsiusprime/polymod"
     },
     {
diff --git a/source/funkin/data/level/LevelData.hx b/source/funkin/data/level/LevelData.hx
index 843389cae..f5e58ae16 100644
--- a/source/funkin/data/level/LevelData.hx
+++ b/source/funkin/data/level/LevelData.hx
@@ -17,7 +17,7 @@ typedef LevelData =
   var version:String;
 
   /**
-   * The title of the week, as seen in the top corner.
+   * The title of the level, as seen in the top corner.
    */
   var name:String;
 
@@ -27,21 +27,35 @@ typedef LevelData =
   @:jcustomparse(funkin.data.DataParse.stringNotEmpty)
   var titleAsset:String;
 
+  /**
+   * The props to display over the colored background.
+   * In the base game this is usually Boyfriend and the opponent.
+   */
   @:default([])
   var props:Array<LevelPropData>;
-  @:default(["bopeebo"])
+
+  /**
+   * The list of song IDs included in this level.
+   */
+  @:default(['bopeebo'])
   var songs:Array<String>;
-  @:default("#F9CF51")
+
+  /**
+   * The background for the level behind the props.
+   */
+  @:default('#F9CF51')
   @:optional
   var background:String;
 }
 
+/**
+ * Data for a single prop for a story mode level.
+ */
 typedef LevelPropData =
 {
   /**
    * The image to use for the prop. May optionally be a sprite sheet.
    */
-  // @:jcustomparse(funkin.data.DataParse.stringNotEmpty)
   var assetPath:String;
 
   /**
diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx
index 889f63073..b1c6b511a 100644
--- a/source/funkin/modding/PolymodHandler.hx
+++ b/source/funkin/modding/PolymodHandler.hx
@@ -1,24 +1,25 @@
 package funkin.modding;
 
-import funkin.util.macro.ClassMacro;
-import funkin.modding.module.ModuleHandler;
-import funkin.data.song.SongData;
-import funkin.data.stage.StageData;
-import polymod.Polymod;
-import polymod.backends.PolymodAssets.PolymodAssetType;
-import polymod.format.ParseRules.TextFileFormat;
-import funkin.data.event.SongEventRegistry;
-import funkin.data.stage.StageRegistry;
-import funkin.util.FileUtil;
-import funkin.data.level.LevelRegistry;
-import funkin.data.notestyle.NoteStyleRegistry;
 import funkin.data.dialogue.ConversationRegistry;
 import funkin.data.dialogue.DialogueBoxRegistry;
 import funkin.data.dialogue.SpeakerRegistry;
+import funkin.data.event.SongEventRegistry;
+import funkin.data.level.LevelRegistry;
+import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.data.song.SongRegistry;
+import funkin.data.stage.StageRegistry;
+import funkin.modding.module.ModuleHandler;
 import funkin.play.character.CharacterData.CharacterDataParser;
 import funkin.save.Save;
-import funkin.data.song.SongRegistry;
+import funkin.util.FileUtil;
+import funkin.util.macro.ClassMacro;
+import polymod.backends.PolymodAssets.PolymodAssetType;
+import polymod.format.ParseRules.TextFileFormat;
+import polymod.Polymod;
 
+/**
+ * A class for interacting with Polymod, the atomic modding framework for Haxe.
+ */
 class PolymodHandler
 {
   /**
@@ -27,16 +28,33 @@ class PolymodHandler
    * Bug fixes increment the patch version, new features increment the minor version.
    * Changes that break old mods increment the major version.
    */
-  static final API_VERSION:String = "0.1.0";
+  static final API_VERSION:String = '0.1.0';
 
   /**
    * Where relative to the executable that mods are located.
    */
-  static final MOD_FOLDER:String = #if (REDIRECT_ASSETS_FOLDER && macos) "../../../../../../../example_mods" #elseif REDIRECT_ASSETS_FOLDER "../../../../example_mods" #else "mods" #end;
+  static final MOD_FOLDER:String =
+    #if (REDIRECT_ASSETS_FOLDER && macos)
+    '../../../../../../../example_mods'
+    #elseif REDIRECT_ASSETS_FOLDER
+    '../../../../example_mods'
+    #else
+    'mods'
+    #end;
 
-  static final CORE_FOLDER:Null<String> = #if (REDIRECT_ASSETS_FOLDER && macos) "../../../../../../../assets" #elseif REDIRECT_ASSETS_FOLDER "../../../../assets" #else null #end;
+  static final CORE_FOLDER:Null<String> =
+    #if (REDIRECT_ASSETS_FOLDER && macos)
+    '../../../../../../../assets'
+    #elseif REDIRECT_ASSETS_FOLDER
+    '../../../../assets'
+    #else
+    null
+    #end;
 
-  public static function createModRoot()
+  /**
+   * If the mods folder doesn't exist, create it.
+   */
+  public static function createModRoot():Void
   {
     FileUtil.createDirIfNotExists(MOD_FOLDER);
   }
@@ -44,40 +62,44 @@ class PolymodHandler
   /**
    * Loads the game with ALL mods enabled with Polymod.
    */
-  public static function loadAllMods()
+  public static function loadAllMods():Void
   {
     // Create the mod root if it doesn't exist.
     createModRoot();
-    trace("Initializing Polymod (using all mods)...");
+    trace('Initializing Polymod (using all mods)...');
     loadModsById(getAllModIds());
   }
 
   /**
    * Loads the game with configured mods enabled with Polymod.
    */
-  public static function loadEnabledMods()
+  public static function loadEnabledMods():Void
   {
     // Create the mod root if it doesn't exist.
     createModRoot();
 
-    trace("Initializing Polymod (using configured mods)...");
+    trace('Initializing Polymod (using configured mods)...');
     loadModsById(Save.instance.enabledModIds);
   }
 
   /**
    * Loads the game without any mods enabled with Polymod.
    */
-  public static function loadNoMods()
+  public static function loadNoMods():Void
   {
     // Create the mod root if it doesn't exist.
     createModRoot();
 
     // We still need to configure the debug print calls etc.
-    trace("Initializing Polymod (using no mods)...");
+    trace('Initializing Polymod (using no mods)...');
     loadModsById([]);
   }
 
-  public static function loadModsById(ids:Array<String>)
+  /**
+   * Load all the mods with the given ids.
+   * @param ids The ORDERED list of mod ids to load.
+   */
+  public static function loadModsById(ids:Array<String>):Void
   {
     if (ids.length == 0)
     {
@@ -90,7 +112,7 @@ class PolymodHandler
 
     buildImports();
 
-    var loadedModList = polymod.Polymod.init(
+    var loadedModList:Array<ModMetadata> = polymod.Polymod.init(
       {
         // Root directory for all mods.
         modRoot: MOD_FOLDER,
@@ -142,30 +164,40 @@ class PolymodHandler
     }
 
     #if debug
-    var fileList = Polymod.listModFiles(PolymodAssetType.IMAGE);
+    var fileList:Array<String> = Polymod.listModFiles(PolymodAssetType.IMAGE);
     trace('Installed mods have replaced ${fileList.length} images.');
     for (item in fileList)
+    {
       trace('  * $item');
+    }
 
     fileList = Polymod.listModFiles(PolymodAssetType.TEXT);
     trace('Installed mods have added/replaced ${fileList.length} text files.');
     for (item in fileList)
+    {
       trace('  * $item');
+    }
 
     fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_MUSIC);
     trace('Installed mods have replaced ${fileList.length} music files.');
     for (item in fileList)
+    {
       trace('  * $item');
+    }
 
     fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_SOUND);
     trace('Installed mods have replaced ${fileList.length} sound files.');
     for (item in fileList)
+    {
       trace('  * $item');
+    }
 
     fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_GENERIC);
     trace('Installed mods have replaced ${fileList.length} generic audio files.');
     for (item in fileList)
+    {
       trace('  * $item');
+    }
     #end
   }
 
@@ -183,21 +215,21 @@ class PolymodHandler
     for (cls in ClassMacro.listClassesInPackage('polymod'))
     {
       if (cls == null) continue;
-      var className = Type.getClassName(cls);
+      var className:String = Type.getClassName(cls);
       Polymod.blacklistImport(className);
     }
   }
 
   static function buildParseRules():polymod.format.ParseRules
   {
-    var output = polymod.format.ParseRules.getDefault();
+    var output:polymod.format.ParseRules = polymod.format.ParseRules.getDefault();
     // Ensure TXT files have merge support.
-    output.addType("txt", TextFileFormat.LINES);
+    output.addType('txt', TextFileFormat.LINES);
     // Ensure script files have merge support.
-    output.addType("hscript", TextFileFormat.PLAINTEXT);
-    output.addType("hxs", TextFileFormat.PLAINTEXT);
-    output.addType("hxc", TextFileFormat.PLAINTEXT);
-    output.addType("hx", TextFileFormat.PLAINTEXT);
+    output.addType('hscript', TextFileFormat.PLAINTEXT);
+    output.addType('hxs', TextFileFormat.PLAINTEXT);
+    output.addType('hxc', TextFileFormat.PLAINTEXT);
+    output.addType('hx', TextFileFormat.PLAINTEXT);
 
     // You can specify the format of a specific file, with file extension.
     // output.addFile("data/introText.txt", TextFileFormat.LINES)
@@ -208,17 +240,21 @@ class PolymodHandler
   {
     return {
       assetLibraryPaths: [
-        "default" => "preload", "shared" => "shared", "songs" => "songs", "tutorial" => "tutorial", "week1" => "week1",      "week2" => "week2",
-            "week3" => "week3",   "week4" => "week4", "week5" => "week5",       "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
+        'default' => 'preload', 'shared' => 'shared', 'songs' => 'songs', 'tutorial' => 'tutorial', 'week1' => 'week1',      'week2' => 'week2',
+            'week3' => 'week3',   'week4' => 'week4', 'week5' => 'week5',       'week6' => 'week6', 'week7' => 'week7', 'weekend1' => 'weekend1',
       ],
       coreAssetRedirect: CORE_FOLDER,
     }
   }
 
+  /**
+   * Retrieve a list of metadata for ALL installed mods, including disabled mods.
+   * @return An array of mod metadata
+   */
   public static function getAllMods():Array<ModMetadata>
   {
     trace('Scanning the mods folder...');
-    var modMetadata = Polymod.scan(
+    var modMetadata:Array<ModMetadata> = Polymod.scan(
       {
         modRoot: MOD_FOLDER,
         apiVersionRule: API_VERSION,
@@ -228,17 +264,25 @@ class PolymodHandler
     return modMetadata;
   }
 
+  /**
+   * Retrieve a list of ALL mod IDs, including disabled mods.
+   * @return An array of mod IDs
+   */
   public static function getAllModIds():Array<String>
   {
-    var modIds = [for (i in getAllMods()) i.id];
+    var modIds:Array<String> = [for (i in getAllMods()) i.id];
     return modIds;
   }
 
+  /**
+   * Retrieve a list of metadata for all enabled mods.
+   * @return An array of mod metadata
+   */
   public static function getEnabledMods():Array<ModMetadata>
   {
-    var modIds = Save.instance.enabledModIds;
-    var modMetadata = getAllMods();
-    var enabledMods = [];
+    var modIds:Array<String> = Save.instance.enabledModIds;
+    var modMetadata:Array<ModMetadata> = getAllMods();
+    var enabledMods:Array<ModMetadata> = [];
     for (item in modMetadata)
     {
       if (modIds.indexOf(item.id) != -1)
@@ -249,7 +293,11 @@ class PolymodHandler
     return enabledMods;
   }
 
-  public static function forceReloadAssets()
+  /**
+   * Clear and reload from disk all data assets.
+   * Useful for "hot reloading" for fast iteration!
+   */
+  public static function forceReloadAssets():Void
   {
     // Forcibly clear scripts so that scripts can be edited.
     ModuleHandler.clearModuleCache();
diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx
index b548b7b1e..626fb8e52 100644
--- a/source/funkin/ui/story/Level.hx
+++ b/source/funkin/ui/story/Level.hx
@@ -51,6 +51,7 @@ class Level implements IRegistryEntry<LevelData>
 
   /**
    * Retrieve the title of the level for display on the menu.
+   * @return Title of the level as a string
    */
   public function getTitle():String
   {
@@ -58,16 +59,21 @@ class Level implements IRegistryEntry<LevelData>
     return _data.name;
   }
 
+  /**
+   * Construct the title graphic for the level.
+   * @return The constructed graphic as a sprite.
+   */
   public function buildTitleGraphic():FlxSprite
   {
-    var result = new FlxSprite().loadGraphic(Paths.image(_data.titleAsset));
+    var result:FlxSprite = new FlxSprite().loadGraphic(Paths.image(_data.titleAsset));
 
     return result;
   }
 
   /**
    * Get the list of songs in this level, as an array of names, for display on the menu.
-   * @return Array<String>
+   * @param difficulty The difficulty of the level being displayed
+   * @return The display names of the songs in this level
    */
   public function getSongDisplayNames(difficulty:String):Array<String>
   {
@@ -88,7 +94,9 @@ class Level implements IRegistryEntry<LevelData>
 
   /**
    * Whether this level is unlocked. If not, it will be greyed out on the menu and have a lock icon.
-   * TODO: Change this behavior in a later release.
+   * Override this in a script.
+   * @default `true`
+   * @return Whether this level is unlocked
    */
   public function isUnlocked():Bool
   {
@@ -97,6 +105,9 @@ class Level implements IRegistryEntry<LevelData>
 
   /**
    * Whether this level is visible. If not, it will not be shown on the menu at all.
+   * Override this in a script.
+   * @default `true`
+   * @return Whether this level is visible in the menu
    */
   public function isVisible():Bool
   {
@@ -106,6 +117,7 @@ class Level implements IRegistryEntry<LevelData>
   /**
    * Build a sprite for the background of the level.
    * Can be overriden by ScriptedLevel. Not used if `isBackgroundSimple` returns true.
+   * @return The constructed sprite
    */
   public function buildBackground():FlxSprite
   {
@@ -124,6 +136,7 @@ class Level implements IRegistryEntry<LevelData>
   /**
    * Returns true if the background is a solid color.
    * If you have a ScriptedLevel with a fancy background, you may want to override this to false.
+   * @return Whether the background is a simple color
    */
   public function isBackgroundSimple():Bool
   {
@@ -133,30 +146,36 @@ class Level implements IRegistryEntry<LevelData>
   /**
    * Returns true if the background is a solid color.
    * If you have a ScriptedLevel with a fancy background, you may want to override this to false.
+   * @return The background as a simple color. May not be valid if `isBackgroundSimple` returns false.
    */
   public function getBackgroundColor():FlxColor
   {
     return FlxColor.fromString(_data.background);
   }
 
+  /**
+   * The list of difficulties the player can select from for this level.
+   * @return The difficulty IDs.
+   */
   public function getDifficulties():Array<String>
   {
     var difficulties:Array<String> = [];
 
-    var songList = getSongs();
+    var songList:Array<String> = getSongs();
 
     var firstSongId:String = songList[0];
     var firstSong:Song = SongRegistry.instance.fetchEntry(firstSongId);
 
     if (firstSong != null)
     {
-      // Don't display alternate characters in Story Mode.
-      for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, "erect"]))
+      // Don't display alternate characters in Story Mode. Only show `default` and `erect` variations.
+      for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, 'erect']))
       {
         difficulties.push(difficulty);
       }
     }
 
+    // Sort in a specific order! Fall back to alphabetical.
     difficulties.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_DIFFICULTY_LIST));
 
     // Filter to only include difficulties that are present in all songs
@@ -169,7 +188,7 @@ class Level implements IRegistryEntry<LevelData>
 
       for (difficulty in difficulties)
       {
-        if (!song.hasDifficulty(difficulty, [Constants.DEFAULT_VARIATION, "erect"]))
+        if (!song.hasDifficulty(difficulty, [Constants.DEFAULT_VARIATION, 'erect']))
         {
           difficulties.remove(difficulty);
         }
@@ -181,6 +200,11 @@ class Level implements IRegistryEntry<LevelData>
     return difficulties;
   }
 
+  /**
+   * Build the props for display over the colored background.
+   * @param existingProps The existing prop sprites, if any.
+   * @return The constructed prop sprites
+   */
   public function buildProps(?existingProps:Array<LevelProp>):Array<LevelProp>
   {
     var props:Array<LevelProp> = existingProps == null ? [] : [for (x in existingProps) x];
@@ -189,11 +213,13 @@ class Level implements IRegistryEntry<LevelData>
 
     var hiddenProps:Array<LevelProp> = props.splice(_data.props.length - 1, props.length - 1);
     for (hiddenProp in hiddenProps)
+    {
       hiddenProp.visible = false;
+    }
 
     for (propIndex in 0..._data.props.length)
     {
-      var propData = _data.props[propIndex];
+      var propData:LevelPropData = _data.props[propIndex];
 
       // Attempt to reuse the `LevelProp` object.
       // This prevents animations from resetting.
@@ -224,6 +250,10 @@ class Level implements IRegistryEntry<LevelData>
     return props;
   }
 
+  /**
+   * Called when the level is destroyed.
+   * TODO: Document when this gets called
+   */
   public function destroy():Void {}
 
   public function toString():String
@@ -231,6 +261,11 @@ class Level implements IRegistryEntry<LevelData>
     return 'Level($id)';
   }
 
+  /**
+   * Retrieve and parse the JSON data for a level by ID.
+   * @param id The ID of the level
+   * @return The parsed level data, or null if not found or invalid
+   */
   static function _fetchData(id:String):Null<LevelData>
   {
     return LevelRegistry.instance.parseEntryDataWithMigration(id, LevelRegistry.instance.fetchEntryVersion(id));

From 6db5d10a55932cb817d429b950313d4578096554 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Tue, 5 Mar 2024 19:35:51 -0500
Subject: [PATCH 04/31] jenny assets submod

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

diff --git a/assets b/assets
index 14b86f436..69ebdb6a7 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 14b86f4369fddf61eb76116139eb33fa8f6e92d0
+Subproject commit 69ebdb6a7aa57b6762ce509243679ab959615120

From 059e1c0e13c9560edbe3454e6cf7ae9be8b3886a Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 5 Mar 2024 21:48:04 -0500
Subject: [PATCH 05/31] Implement onNoteIncoming script event.

---
 assets                                        |  2 +-
 source/funkin/modding/IScriptedClass.hx       | 28 ++++++++++---------
 .../modding/events/ScriptEventDispatcher.hx   | 16 +++++++++--
 .../funkin/modding/events/ScriptEventType.hx  |  7 +++++
 source/funkin/modding/module/Module.hx        |  4 ++-
 source/funkin/play/PlayState.hx               | 13 +++++++--
 source/funkin/play/character/BaseCharacter.hx |  2 +-
 source/funkin/play/notes/Strumline.hx         |  7 +++++
 source/funkin/play/song/Song.hx               |  4 ++-
 source/funkin/play/stage/Bopper.hx            |  4 ++-
 source/funkin/play/stage/Stage.hx             |  4 ++-
 .../ui/debug/charting/ChartEditorState.hx     |  2 +-
 .../ui/haxeui/components/CharacterPlayer.hx   |  8 +++++-
 13 files changed, 76 insertions(+), 25 deletions(-)

diff --git a/assets b/assets
index 69ebdb6a7..518349369 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 69ebdb6a7aa57b6762ce509243679ab959615120
+Subproject commit 518349369ea3237504e8100c901313185bb5b80f
diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx
index b009aea41..5f2ff2b9e 100644
--- a/source/funkin/modding/IScriptedClass.hx
+++ b/source/funkin/modding/IScriptedClass.hx
@@ -56,7 +56,20 @@ interface IStateStageProp extends IScriptedClass
  */
 interface INoteScriptedClass extends IScriptedClass
 {
-  public function onNoteHit(event:NoteScriptEvent):Void;
+  /**
+   * Called when a note enters the field of view and approaches the strumline.
+   */
+  public function onNoteIncoming(event:NoteScriptEvent):Void;
+
+  /**
+   * Called when EITHER player hits a note.
+   * Query the note attached to the event to determine if it was hit by the player or CPU.
+   */
+  public function onNoteHit(event:HitNoteScriptEvent):Void;
+
+  /**
+   * Called when EITHER player (usually the player) misses a note.
+   */
   public function onNoteMiss(event:NoteScriptEvent):Void;
 }
 
@@ -73,7 +86,7 @@ interface INoteScriptedClass extends IScriptedClass
 /**
  * Defines a set of callbacks available to scripted classes that involve the lifecycle of the Play State.
  */
-interface IPlayStateScriptedClass extends IScriptedClass
+interface IPlayStateScriptedClass extends INoteScriptedClass
 {
   /**
    * Called when the game is paused.
@@ -113,17 +126,6 @@ interface IPlayStateScriptedClass extends IScriptedClass
    */
   public function onSongRetry(event:ScriptEvent):Void;
 
-  /**
-   * Called when EITHER player hits a note.
-   * Query the note attached to the event to determine if it was hit by the player or CPU.
-   */
-  public function onNoteHit(event:NoteScriptEvent):Void;
-
-  /**
-   * Called when EITHER player (usually the player) misses a note.
-   */
-  public function onNoteMiss(event:NoteScriptEvent):Void;
-
   /**
    * Called when the player presses a key when no note is on the strumline.
    */
diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx
index f5d797ea4..fd58d0fad 100644
--- a/source/funkin/modding/events/ScriptEventDispatcher.hx
+++ b/source/funkin/modding/events/ScriptEventDispatcher.hx
@@ -71,17 +71,29 @@ class ScriptEventDispatcher
       }
     }
 
-    if (Std.isOfType(target, IPlayStateScriptedClass))
+    if (Std.isOfType(target, INoteScriptedClass))
     {
-      var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass);
+      var t:INoteScriptedClass = cast(target, INoteScriptedClass);
       switch (event.type)
       {
+        case NOTE_INCOMING:
+          t.onNoteIncoming(cast event);
+          return;
         case NOTE_HIT:
           t.onNoteHit(cast event);
           return;
         case NOTE_MISS:
           t.onNoteMiss(cast event);
           return;
+        default: // Continue;
+      }
+    }
+
+    if (Std.isOfType(target, IPlayStateScriptedClass))
+    {
+      var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass);
+      switch (event.type)
+      {
         case NOTE_GHOST_MISS:
           t.onNoteGhostMiss(cast event);
           return;
diff --git a/source/funkin/modding/events/ScriptEventType.hx b/source/funkin/modding/events/ScriptEventType.hx
index e06b5ad24..eeeb8ef29 100644
--- a/source/funkin/modding/events/ScriptEventType.hx
+++ b/source/funkin/modding/events/ScriptEventType.hx
@@ -63,6 +63,13 @@ enum abstract ScriptEventType(String) from String to String
    */
   var SONG_STEP_HIT = 'STEP_HIT';
 
+  /**
+   * Called when a note comes on screen and starts approaching the strumline.
+   *
+   * This event is not cancelable.
+   */
+  var NOTE_INCOMING = 'NOTE_INCOMING';
+
   /**
    * Called when a character hits a note.
    * Important information such as judgement/timing, note data, player/opponent, etc. are all provided.
diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx
index f50c9936a..be9b7146b 100644
--- a/source/funkin/modding/module/Module.hx
+++ b/source/funkin/modding/module/Module.hx
@@ -83,7 +83,9 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
 
   public function onGameOver(event:ScriptEvent) {}
 
-  public function onNoteHit(event:NoteScriptEvent) {}
+  public function onNoteIncoming(event:NoteScriptEvent) {}
+
+  public function onNoteHit(event:HitNoteScriptEvent) {}
 
   public function onNoteMiss(event:NoteScriptEvent) {}
 
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index e7ad326d2..5564c46c2 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1614,8 +1614,10 @@ class PlayState extends MusicBeatSubState
     var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
     if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
 
-    playerStrumline = new Strumline(noteStyle, true);
+    playerStrumline = new Strumline(noteStyle, !isBotPlayMode);
+    playerStrumline.onNoteIncoming.add(onStrumlineNoteIncoming);
     opponentStrumline = new Strumline(noteStyle, false);
+    opponentStrumline.onNoteIncoming.add(onStrumlineNoteIncoming);
     add(playerStrumline);
     add(opponentStrumline);
 
@@ -1751,6 +1753,13 @@ class PlayState extends MusicBeatSubState
     opponentStrumline.applyNoteData(opponentNoteData);
   }
 
+  function onStrumlineNoteIncoming(noteSprite:NoteSprite):Void
+  {
+    var event:NoteScriptEvent = new NoteScriptEvent(NOTE_INCOMING, noteSprite, 0, false);
+
+    dispatchEvent(event);
+  }
+
   /**
    * Prepares to start the countdown.
    * Ends any running cutscenes, creates the strumlines, and starts the countdown.
@@ -1942,7 +1951,7 @@ class PlayState extends MusicBeatSubState
 
         // Call an event to allow canceling the note hit.
         // NOTE: This is what handles the character animations!
-        var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, note, 0, true);
+        var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', 0);
         dispatchEvent(event);
 
         // Calling event.cancelEvent() skips all the other logic! Neat!
diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx
index cf5311bdc..d39f19b76 100644
--- a/source/funkin/play/character/BaseCharacter.hx
+++ b/source/funkin/play/character/BaseCharacter.hx
@@ -485,7 +485,7 @@ class BaseCharacter extends Bopper
    * Every time a note is hit, check if the note is from the same strumline.
    * If it is, then play the sing animation.
    */
-  public override function onNoteHit(event:NoteScriptEvent)
+  public override function onNoteHit(event:HitNoteScriptEvent)
   {
     super.onNoteHit(event);
 
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 190aa3ee0..1ba5dcfc5 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -1,5 +1,6 @@
 package funkin.play.notes;
 
+import flixel.util.FlxSignal.FlxTypedSignal;
 import flixel.FlxG;
 import funkin.play.notes.notestyle.NoteStyle;
 import flixel.group.FlxSpriteGroup;
@@ -49,6 +50,8 @@ class Strumline extends FlxSpriteGroup
 
   public var holdNotes:FlxTypedSpriteGroup<SustainTrail>;
 
+  public var onNoteIncoming:FlxTypedSignal<NoteSprite->Void>;
+
   var strumlineNotes:FlxTypedSpriteGroup<StrumlineNote>;
   var noteSplashes:FlxTypedSpriteGroup<NoteSplash>;
   var noteHoldCovers:FlxTypedSpriteGroup<NoteHoldCover>;
@@ -106,6 +109,8 @@ class Strumline extends FlxSpriteGroup
 
     this.refresh();
 
+    this.onNoteIncoming = new FlxTypedSignal<NoteSprite->Void>();
+
     for (i in 0...KEY_COUNT)
     {
       var child:StrumlineNote = new StrumlineNote(noteStyle, isPlayer, DIRECTIONS[i]);
@@ -311,6 +316,8 @@ class Strumline extends FlxSpriteGroup
       }
 
       nextNoteIndex = noteIndex + 1; // Increment the nextNoteIndex rather than splicing the array, because splicing is slow.
+
+      onNoteIncoming.dispatch(noteSprite);
     }
 
     // Update rendering of notes.
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 61f83d1ed..3997692c2 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -364,7 +364,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
 
   public function onSongRetry(event:ScriptEvent):Void {};
 
-  public function onNoteHit(event:NoteScriptEvent):Void {};
+  public function onNoteIncoming(event:NoteScriptEvent) {}
+
+  public function onNoteHit(event:HitNoteScriptEvent) {}
 
   public function onNoteMiss(event:NoteScriptEvent):Void {};
 
diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx
index 7974900d8..3a57072e6 100644
--- a/source/funkin/play/stage/Bopper.hx
+++ b/source/funkin/play/stage/Bopper.hx
@@ -353,7 +353,9 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
 
   public function onGameOver(event:ScriptEvent) {}
 
-  public function onNoteHit(event:NoteScriptEvent) {}
+  public function onNoteIncoming(event:NoteScriptEvent) {}
+
+  public function onNoteHit(event:HitNoteScriptEvent) {}
 
   public function onNoteMiss(event:NoteScriptEvent) {}
 
diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx
index 32c0509a5..9605c6989 100644
--- a/source/funkin/play/stage/Stage.hx
+++ b/source/funkin/play/stage/Stage.hx
@@ -870,7 +870,9 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
 
   public function onCountdownEnd(event:CountdownScriptEvent) {}
 
-  public function onNoteHit(event:NoteScriptEvent) {}
+  public function onNoteIncoming(event:NoteScriptEvent) {}
+
+  public function onNoteHit(event:HitNoteScriptEvent) {}
 
   public function onNoteMiss(event:NoteScriptEvent) {}
 
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 78e73bf27..4449134b8 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -5928,7 +5928,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
       var tempNote:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault());
       tempNote.noteData = noteData;
       tempNote.scrollFactor.set(0, 0);
-      var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, tempNote, 1, true);
+      var event:NoteScriptEvent = new HitNoteScriptEvent(tempNote, 0.0, 0, 'perfect', 0);
       dispatchEvent(event);
 
       // Calling event.cancelEvent() skips all the other logic! Neat!
diff --git a/source/funkin/ui/haxeui/components/CharacterPlayer.hx b/source/funkin/ui/haxeui/components/CharacterPlayer.hx
index c7171fac7..77b23d68a 100644
--- a/source/funkin/ui/haxeui/components/CharacterPlayer.hx
+++ b/source/funkin/ui/haxeui/components/CharacterPlayer.hx
@@ -2,6 +2,7 @@ package funkin.ui.haxeui.components;
 
 import funkin.modding.events.ScriptEvent.GhostMissNoteScriptEvent;
 import funkin.modding.events.ScriptEvent.NoteScriptEvent;
+import funkin.modding.events.ScriptEvent.HitNoteScriptEvent;
 import funkin.modding.events.ScriptEvent.SongTimeScriptEvent;
 import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
 import haxe.ui.core.IDataComponent;
@@ -216,12 +217,17 @@ class CharacterPlayer extends Box
     if (character != null) character.onStepHit(event);
   }
 
+  public function onNoteIncoming(event:NoteScriptEvent)
+  {
+    if (character != null) character.onNoteIncoming(event);
+  }
+
   /**
    * Called when a note is hit in the song
    * Used to play character animations.
    * @param event The event.
    */
-  public function onNoteHit(event:NoteScriptEvent):Void
+  public function onNoteHit(event:HitNoteScriptEvent):Void
   {
     if (character != null) character.onNoteHit(event);
   }

From 1b1834e98b4a177504b866b53ab36caa83a920ff Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 5 Mar 2024 22:27:07 -0500
Subject: [PATCH 06/31] Implement a botplay checkbox in the chart editor

---
 assets                                        |  2 +-
 source/funkin/play/PlayState.hx               | 71 +++++++++++++++++--
 source/funkin/play/notes/Strumline.hx         |  4 ++
 .../ui/debug/charting/ChartEditorState.hx     |  6 ++
 .../handlers/ChartEditorToolboxHandler.hx     |  9 +++
 source/funkin/ui/freeplay/FreeplayState.hx    |  8 +--
 6 files changed, 88 insertions(+), 12 deletions(-)

diff --git a/assets b/assets
index 69ebdb6a7..51b02f0d4 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 69ebdb6a7aa57b6762ce509243679ab959615120
+Subproject commit 51b02f0d47e5b34bf8589065c092953c10c5040d
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index e7ad326d2..46c09090d 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -111,6 +111,11 @@ typedef PlayStateParams =
    * @default `false`
    */
   ?practiceMode:Bool,
+  /**
+   * Whether the song should start in Bot Play Mode.
+   * @default `false`
+   */
+  ?botPlayMode:Bool,
   /**
    * Whether the song should be in minimal mode.
    * @default `false`
@@ -282,6 +287,12 @@ class PlayState extends MusicBeatSubState
    */
   public var isPracticeMode:Bool = false;
 
+  /**
+   * Whether the game is currently in Bot Play Mode.
+   * If true, player will not lose gain or lose score from notes.
+   */
+  public var isBotPlayMode:Bool = false;
+
   /**
    * Whether the player has dropped below zero health,
    * and we are just waiting for an animation to play out before transitioning.
@@ -566,6 +577,7 @@ class PlayState extends MusicBeatSubState
     if (params.targetDifficulty != null) currentDifficulty = params.targetDifficulty;
     if (params.targetVariation != null) currentVariation = params.targetVariation;
     isPracticeMode = params.practiceMode ?? false;
+    isBotPlayMode = params.botPlayMode ?? false;
     isMinimalMode = params.minimalMode ?? false;
     startTimestamp = params.startTimestamp ?? 0.0;
     playbackRate = params.playbackRate ?? 1.0;
@@ -1614,7 +1626,7 @@ class PlayState extends MusicBeatSubState
     var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
     if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
 
-    playerStrumline = new Strumline(noteStyle, true);
+    playerStrumline = new Strumline(noteStyle, !isBotPlayMode);
     opponentStrumline = new Strumline(noteStyle, false);
     add(playerStrumline);
     add(opponentStrumline);
@@ -1876,7 +1888,14 @@ class PlayState extends MusicBeatSubState
   function updateScoreText():Void
   {
     // TODO: Add functionality for modules to update the score text.
-    scoreText.text = 'Score:' + songScore;
+    if (isBotPlayMode)
+    {
+      scoreText.text = 'Bot Play Enabled';
+    }
+    else
+    {
+      scoreText.text = 'Score:' + songScore;
+    }
   }
 
   /**
@@ -1884,7 +1903,14 @@ class PlayState extends MusicBeatSubState
    */
   function updateHealthBar():Void
   {
-    healthLerp = FlxMath.lerp(healthLerp, health, 0.15);
+    if (isBotPlayMode)
+    {
+      healthLerp = Constants.HEALTH_MAX;
+    }
+    else
+    {
+      healthLerp = FlxMath.lerp(healthLerp, health, 0.15);
+    }
   }
 
   /**
@@ -1928,13 +1954,16 @@ class PlayState extends MusicBeatSubState
 
       if (Conductor.instance.songPosition > hitWindowEnd)
       {
-        if (note.hasMissed) continue;
+        if (note.hasMissed || note.hasBeenHit) continue;
 
         note.tooEarly = false;
         note.mayHit = false;
         note.hasMissed = true;
 
-        if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
+        if (note.holdNoteSprite != null)
+        {
+          note.holdNoteSprite.missedNote = true;
+        }
       }
       else if (Conductor.instance.songPosition > hitWindowCenter)
       {
@@ -2021,10 +2050,38 @@ class PlayState extends MusicBeatSubState
 
       if (Conductor.instance.songPosition > hitWindowEnd)
       {
+        if (note.hasMissed || note.hasBeenHit) continue;
         note.tooEarly = false;
         note.mayHit = false;
         note.hasMissed = true;
-        if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
+        if (note.holdNoteSprite != null)
+        {
+          note.holdNoteSprite.missedNote = true;
+        }
+      }
+      else if (isBotPlayMode && Conductor.instance.songPosition > hitWindowCenter)
+      {
+        if (note.hasBeenHit) continue;
+
+        // We call onHitNote to play the proper animations,
+        // but not goodNoteHit! This means zero score and zero notes hit for the results screen!
+
+        // Call an event to allow canceling the note hit.
+        // NOTE: This is what handles the character animations!
+        var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', 0);
+        dispatchEvent(event);
+
+        // Calling event.cancelEvent() skips all the other logic! Neat!
+        if (event.eventCanceled) continue;
+
+        // Command the bot to hit the note on time.
+        // NOTE: This is what handles the strumline and cleaning up the note itself!
+        playerStrumline.hitNote(note);
+
+        if (note.holdNoteSprite != null)
+        {
+          playerStrumline.playNoteHoldCover(note.holdNoteSprite);
+        }
       }
       else if (Conductor.instance.songPosition > hitWindowStart)
       {
@@ -2069,7 +2126,7 @@ class PlayState extends MusicBeatSubState
       if (holdNote == null || !holdNote.alive) continue;
 
       // While the hold note is being hit, and there is length on the hold note...
-      if (holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0)
+      if (!isBotPlayMode && holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0)
       {
         // Grant the player health.
         health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed;
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 190aa3ee0..efe1c707a 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -38,6 +38,10 @@ class Strumline extends FlxSpriteGroup
     return FlxG.height / 0.45;
   }
 
+  /**
+   * Whether this strumline is controlled by the player's inputs.
+   * False means it's controlled by the opponent or Bot Play.
+   */
   public var isPlayer:Bool;
 
   /**
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 78e73bf27..85a2396b9 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -592,6 +592,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
    */
   var playtestPracticeMode:Bool = false;
 
+  /**
+   * If true, playtesting a chart will make the computer do it for you!
+   */
+  var playtestBotPlayMode:Bool = false;
+
   /**
    * Enables or disables the "debugger" popup that appears when you run into a flixel error.
    */
@@ -5359,6 +5364,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         targetDifficulty: selectedDifficulty,
         targetVariation: selectedVariation,
         practiceMode: playtestPracticeMode,
+        botPlayMode: playtestBotPlayMode,
         minimalMode: minimal,
         startTimestamp: startTimestamp,
         playbackRate: playbackRate,
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
index f32cc2bfb..3b32edf5d 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
@@ -299,6 +299,15 @@ class ChartEditorToolboxHandler
       state.playtestStartTime = checkboxStartTime.selected;
     };
 
+    var checkboxBotPlay:Null<CheckBox> = toolbox.findComponent('playtestBotPlayCheckbox', CheckBox);
+    if (checkboxBotPlay == null) throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestBotPlayCheckbox component.';
+
+    checkboxBotPlay.selected = state.playtestBotPlayMode;
+
+    checkboxBotPlay.onClick = _ -> {
+      state.playtestBotPlayMode = checkboxBotPlay.selected;
+    };
+
     var checkboxDebugger:Null<CheckBox> = toolbox.findComponent('playtestDebuggerCheckbox', CheckBox);
 
     if (checkboxDebugger == null) throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestDebuggerCheckbox component.';
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 50f85571b..45f9a4d27 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -1143,12 +1143,12 @@ class FreeplayState extends MusicBeatSubState
           targetSong: targetSong,
           targetDifficulty: targetDifficulty,
           targetVariation: targetVariation,
-          // TODO: Make this an option!
-          // startTimestamp: 0.0,
-          // TODO: Make this an option!
-          // playbackRate: 0.5,
           practiceMode: false,
           minimalMode: false,
+          // TODO: Make these an option! It's currently only accessible via chart editor.
+          // startTimestamp: 0.0,
+          // playbackRate: 0.5,
+          // botPlayMode: true,
         }, true);
     });
   }

From 607b5757fdc6f558c75eedfd1b7fee8d478a1e85 Mon Sep 17 00:00:00 2001
From: FabsTheFabs <flamingkitty24@gmail.com>
Date: Wed, 6 Mar 2024 05:05:03 +0000
Subject: [PATCH 07/31] added decimal point to waveform duration

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

diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
index af1d75444..bd2fd8ba3 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
@@ -272,19 +272,19 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox
     // waveformPlayer.waveform.forceUpdate = true;
     waveformPlayer.waveform.waveformData = playerVoice?.waveformData;
     // Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it.
-    waveformPlayer.waveform.duration = (playerVoice?.length ?? 1000) / Constants.MS_PER_SEC;
+    waveformPlayer.waveform.duration = (playerVoice?.length ?? 1000.0) / Constants.MS_PER_SEC;
 
     // Build opponent waveform.
     // waveformOpponent.waveform.forceUpdate = true;
     // note: if song only has one set of vocals (Vocals.ogg/mp3) then this is null and crashes charting editor
     // so we null check
     waveformOpponent.waveform.waveformData = opponentVoice?.waveformData;
-    waveformOpponent.waveform.duration = (opponentVoice?.length ?? 1000) / Constants.MS_PER_SEC;
+    waveformOpponent.waveform.duration = (opponentVoice?.length ?? 1000.0) / Constants.MS_PER_SEC;
 
     // Build instrumental waveform.
     // waveformInstrumental.waveform.forceUpdate = true;
     waveformInstrumental.waveform.waveformData = chartEditorState.audioInstTrack.waveformData;
-    waveformInstrumental.waveform.duration = (instTrack?.length ?? 1000) / Constants.MS_PER_SEC;
+    waveformInstrumental.waveform.duration = (instTrack?.lenth ?? 1000.0) / Constants.MS_PER_SEC;
 
     addOffsetsToAudioPreview();
   }

From 6bc1eb7278dab1e01f32125026c33cf1cc67a6ea Mon Sep 17 00:00:00 2001
From: FabsTheFabs <flamingkitty24@gmail.com>
Date: Wed, 6 Mar 2024 05:06:57 +0000
Subject: [PATCH 08/31] typo oops

---
 .../ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
index bd2fd8ba3..46721edfa 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
@@ -284,7 +284,7 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox
     // Build instrumental waveform.
     // waveformInstrumental.waveform.forceUpdate = true;
     waveformInstrumental.waveform.waveformData = chartEditorState.audioInstTrack.waveformData;
-    waveformInstrumental.waveform.duration = (instTrack?.lenth ?? 1000.0) / Constants.MS_PER_SEC;
+    waveformInstrumental.waveform.duration = (instTrack?.length ?? 1000.0) / Constants.MS_PER_SEC;
 
     addOffsetsToAudioPreview();
   }

From 67ede14685ad6199cc84cdab6230603c9bfb71cf Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Wed, 6 Mar 2024 02:17:45 -0500
Subject: [PATCH 09/31] raindrop assets

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

diff --git a/assets b/assets
index 095e91fb3..bd718379f 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 095e91fb33dad70a5b51e37542e335cedd025d09
+Subproject commit bd718379f18ea9562f5260a5e41e20720ebe9e9b

From a516e9199f714a2be5488d2ee4d063c3430f3c80 Mon Sep 17 00:00:00 2001
From: Mike Welsh <mwelsh@gmail.com>
Date: Tue, 5 Mar 2024 23:21:57 -0800
Subject: [PATCH 10/31] Remove DynamicTools; fix pause menu on HTML5

Calls intended for `ArrayTools.clone` were being routed to
`DynamicTools.clone` due to the order of `using` statements in
`imports.hx`. This caused the pause menu to break due to arrays
becoming fubar (missing length property).

Using `DynamicTools` is a little dangerous, so remove it in favor
of calling `Reflect.copy` directly.
---
 source/funkin/import.hx                            |  1 -
 .../funkin/ui/debug/charting/ChartEditorState.hx   |  4 ++--
 source/funkin/util/tools/DynamicTools.hx           | 14 --------------
 3 files changed, 2 insertions(+), 17 deletions(-)
 delete mode 100644 source/funkin/util/tools/DynamicTools.hx

diff --git a/source/funkin/import.hx b/source/funkin/import.hx
index 02055d4ed..250de99cb 100644
--- a/source/funkin/import.hx
+++ b/source/funkin/import.hx
@@ -13,7 +13,6 @@ using Lambda;
 using StringTools;
 using funkin.util.tools.ArraySortTools;
 using funkin.util.tools.ArrayTools;
-using funkin.util.tools.DynamicTools;
 using funkin.util.tools.FloatTools;
 using funkin.util.tools.Int64Tools;
 using funkin.util.tools.IntTools;
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index b9e412b36..0e1aa4f4d 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -4530,14 +4530,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
                 {
                   // Create an event and place it in the chart.
                   // TODO: Figure out configuring event data.
-                  var newEventData:SongEventData = new SongEventData(cursorSnappedMs, eventKindToPlace, eventDataToPlace.clone());
+                  var newEventData:SongEventData = new SongEventData(cursorSnappedMs, eventKindToPlace, Reflect.copy(eventDataToPlace));
 
                   performCommand(new AddEventsCommand([newEventData], FlxG.keys.pressed.CONTROL));
                 }
                 else
                 {
                   // Create a note and place it in the chart.
-                  var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace.clone());
+                  var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, Reflect.copy(noteKindToPlace));
 
                   performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
 
diff --git a/source/funkin/util/tools/DynamicTools.hx b/source/funkin/util/tools/DynamicTools.hx
deleted file mode 100644
index 47501ea22..000000000
--- a/source/funkin/util/tools/DynamicTools.hx
+++ /dev/null
@@ -1,14 +0,0 @@
-package funkin.util.tools;
-
-class DynamicTools
-{
-  /**
-   * Creates a full clone of the input `Dynamic`. Only guaranteed to work on anonymous structures.
-   * @param input The `Dynamic` to clone.
-   * @return A clone of the input `Dynamic`.
-   */
-  public static function clone(input:Dynamic):Dynamic
-  {
-    return Reflect.copy(input);
-  }
-}

From b2d3fe17d7b7af1e0d21786d357f12815a1aa498 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Wed, 6 Mar 2024 02:56:10 -0500
Subject: [PATCH 11/31] fun lil pitch effect on result scren

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

diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index f77c3fc6b..a78d61583 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -347,6 +347,10 @@ class ResultState extends MusicBeatSubState
 
     if (controls.PAUSE)
     {
+      FlxTween.tween(FlxG.sound.music, {volume: 0}, 0.8);
+      FlxTween.tween(FlxG.sound.music, {pitch: 3}, 0.1, {onComplete: _ -> {
+        FlxTween.tween(FlxG.sound.music, {pitch: 0.5}, 0.4);
+      }});
       if (params.storyMode)
       {
         openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker)));

From 7a9bff248e3351f051884ef01d7e3b083491000a Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 6 Mar 2024 12:24:25 -0500
Subject: [PATCH 12/31] Fix an issue with array.clone() on HTML5

---
 source/funkin/import.hx                        |  2 +-
 source/funkin/util/tools/DynamicAccessTools.hx | 16 ++++++++++++++++
 source/funkin/util/tools/DynamicTools.hx       | 14 --------------
 3 files changed, 17 insertions(+), 15 deletions(-)
 create mode 100644 source/funkin/util/tools/DynamicAccessTools.hx
 delete mode 100644 source/funkin/util/tools/DynamicTools.hx

diff --git a/source/funkin/import.hx b/source/funkin/import.hx
index 02055d4ed..66c3470ff 100644
--- a/source/funkin/import.hx
+++ b/source/funkin/import.hx
@@ -13,7 +13,7 @@ using Lambda;
 using StringTools;
 using funkin.util.tools.ArraySortTools;
 using funkin.util.tools.ArrayTools;
-using funkin.util.tools.DynamicTools;
+using funkin.util.tools.DynamicAccessTools;
 using funkin.util.tools.FloatTools;
 using funkin.util.tools.Int64Tools;
 using funkin.util.tools.IntTools;
diff --git a/source/funkin/util/tools/DynamicAccessTools.hx b/source/funkin/util/tools/DynamicAccessTools.hx
new file mode 100644
index 000000000..1c83ce039
--- /dev/null
+++ b/source/funkin/util/tools/DynamicAccessTools.hx
@@ -0,0 +1,16 @@
+package funkin.util.tools;
+
+import haxe.DynamicAccess;
+
+class DynamicAccessTools
+{
+  /**
+   * Creates a full clone of the input `DynamicAccess`.
+   * @param input The `Dynamic` to clone.
+   * @return A clone of the input `Dynamic`.
+   */
+  public static function clone(input:DynamicAccess<T>):DynamicAccess<T>
+  {
+    return Reflect.copy(input);
+  }
+}
diff --git a/source/funkin/util/tools/DynamicTools.hx b/source/funkin/util/tools/DynamicTools.hx
deleted file mode 100644
index 47501ea22..000000000
--- a/source/funkin/util/tools/DynamicTools.hx
+++ /dev/null
@@ -1,14 +0,0 @@
-package funkin.util.tools;
-
-class DynamicTools
-{
-  /**
-   * Creates a full clone of the input `Dynamic`. Only guaranteed to work on anonymous structures.
-   * @param input The `Dynamic` to clone.
-   * @return A clone of the input `Dynamic`.
-   */
-  public static function clone(input:Dynamic):Dynamic
-  {
-    return Reflect.copy(input);
-  }
-}

From 2fa1d18dce85a6127148406d5f6822279e48bfc6 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 6 Mar 2024 12:29:54 -0500
Subject: [PATCH 13/31] Fix build

---
 source/funkin/ui/debug/charting/ChartEditorState.hx | 2 +-
 source/funkin/util/tools/DynamicAccessTools.hx      | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 78e73bf27..191f3cb15 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -4532,7 +4532,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.clone());
+                  var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace);
 
                   performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
 
diff --git a/source/funkin/util/tools/DynamicAccessTools.hx b/source/funkin/util/tools/DynamicAccessTools.hx
index 1c83ce039..14b9a6c68 100644
--- a/source/funkin/util/tools/DynamicAccessTools.hx
+++ b/source/funkin/util/tools/DynamicAccessTools.hx
@@ -9,7 +9,7 @@ class DynamicAccessTools
    * @param input The `Dynamic` to clone.
    * @return A clone of the input `Dynamic`.
    */
-  public static function clone(input:DynamicAccess<T>):DynamicAccess<T>
+  public static function clone<T>(input:DynamicAccess<T>):DynamicAccess<T>
   {
     return Reflect.copy(input);
   }

From f671cc856902713618d270137592a4bb0a606348 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 6 Mar 2024 14:13:48 -0500
Subject: [PATCH 14/31] Remove DynamicAccessTools entirely.

---
 source/funkin/import.hx                          |  1 -
 .../funkin/ui/debug/charting/ChartEditorState.hx |  2 +-
 source/funkin/util/tools/DynamicAccessTools.hx   | 16 ----------------
 3 files changed, 1 insertion(+), 18 deletions(-)
 delete mode 100644 source/funkin/util/tools/DynamicAccessTools.hx

diff --git a/source/funkin/import.hx b/source/funkin/import.hx
index 66c3470ff..250de99cb 100644
--- a/source/funkin/import.hx
+++ b/source/funkin/import.hx
@@ -13,7 +13,6 @@ using Lambda;
 using StringTools;
 using funkin.util.tools.ArraySortTools;
 using funkin.util.tools.ArrayTools;
-using funkin.util.tools.DynamicAccessTools;
 using funkin.util.tools.FloatTools;
 using funkin.util.tools.Int64Tools;
 using funkin.util.tools.IntTools;
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 191f3cb15..29d7ddf97 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -4525,7 +4525,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
                 {
                   // Create an event and place it in the chart.
                   // TODO: Figure out configuring event data.
-                  var newEventData:SongEventData = new SongEventData(cursorSnappedMs, eventKindToPlace, eventDataToPlace.clone());
+                  var newEventData:SongEventData = new SongEventData(cursorSnappedMs, eventKindToPlace, eventDataToPlace.copy());
 
                   performCommand(new AddEventsCommand([newEventData], FlxG.keys.pressed.CONTROL));
                 }
diff --git a/source/funkin/util/tools/DynamicAccessTools.hx b/source/funkin/util/tools/DynamicAccessTools.hx
deleted file mode 100644
index 14b9a6c68..000000000
--- a/source/funkin/util/tools/DynamicAccessTools.hx
+++ /dev/null
@@ -1,16 +0,0 @@
-package funkin.util.tools;
-
-import haxe.DynamicAccess;
-
-class DynamicAccessTools
-{
-  /**
-   * Creates a full clone of the input `DynamicAccess`.
-   * @param input The `Dynamic` to clone.
-   * @return A clone of the input `Dynamic`.
-   */
-  public static function clone<T>(input:DynamicAccess<T>):DynamicAccess<T>
-  {
-    return Reflect.copy(input);
-  }
-}

From 332a81ec72a36af65757bf66a640d38e88f4ae77 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 6 Mar 2024 17:22:11 -0500
Subject: [PATCH 15/31] No longer miss notes during ending cutscene

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

diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index ab693ef46..a6e4b4632 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1032,7 +1032,7 @@ class PlayState extends MusicBeatSubState
     if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed);
 
     // Moving notes into position is now done by Strumline.update().
-    processNotes(elapsed);
+    if (!isInCutscene) processNotes(elapsed);
 
     justUnpaused = false;
   }

From aff7bbb4e9bd6d8307e1d0ef79d88cee035c7a40 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 6 Mar 2024 17:22:22 -0500
Subject: [PATCH 16/31] Update assets submodule

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

diff --git a/assets b/assets
index 095e91fb3..bf61ea3c9 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 095e91fb33dad70a5b51e37542e335cedd025d09
+Subproject commit bf61ea3c9b56bc82c36976d5aa8052bc16ff1b4e

From 69b28ca42c5f8f0be4052f09df05f3e782de2bc6 Mon Sep 17 00:00:00 2001
From: Mike Welsh <mwelsh@gmail.com>
Date: Wed, 6 Mar 2024 22:48:44 -0800
Subject: [PATCH 17/31] Add #if FLX_DEBUG in TrackerUtil

`Tracker.addProfile` only exists when FLX_DEBUG is set, so
add this conditional check to fix non-debug builds.
---
 source/funkin/util/TrackerUtil.hx | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/source/funkin/util/TrackerUtil.hx b/source/funkin/util/TrackerUtil.hx
index b8ed0c995..ffe374c5f 100644
--- a/source/funkin/util/TrackerUtil.hx
+++ b/source/funkin/util/TrackerUtil.hx
@@ -17,7 +17,9 @@ class TrackerUtil
    */
   public static function initTrackers():Void
   {
+    #if FLX_DEBUG
     Tracker.addProfile(new TrackerProfile(Highscore, ["tallies"]));
     FlxG.console.registerClass(Highscore);
+    #end
   }
 }

From b0abef0d527dcb90060a1b142fa4f4ed393a5ba2 Mon Sep 17 00:00:00 2001
From: Mike Welsh <mwelsh@gmail.com>
Date: Wed, 6 Mar 2024 23:37:50 -0800
Subject: [PATCH 18/31] Use correct resource URL when loading videos

On HTML5, `VideoCutscene` was not stripping the library prefix
from the video file path, causing the video to fail to load.

Fixes FPIQ-281.
---
 source/funkin/play/cutscene/VideoCutscene.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/play/cutscene/VideoCutscene.hx b/source/funkin/play/cutscene/VideoCutscene.hx
index 2d31b0a28..ff56e0919 100644
--- a/source/funkin/play/cutscene/VideoCutscene.hx
+++ b/source/funkin/play/cutscene/VideoCutscene.hx
@@ -61,7 +61,7 @@ class VideoCutscene
     VideoCutscene.cutsceneType = cutsceneType;
 
     #if html5
-    playVideoHTML5(filePath);
+    playVideoHTML5(rawFilePath);
     #elseif hxCodec
     playVideoNative(rawFilePath);
     #else

From 427e4810ad768879e5585915abb50f7ec5f43c7a Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Thu, 7 Mar 2024 03:57:16 -0500
Subject: [PATCH 19/31] faster bf processing...

---
 source/funkin/util/logging/AnsiTrace.hx | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/source/funkin/util/logging/AnsiTrace.hx b/source/funkin/util/logging/AnsiTrace.hx
index c8d27b86f..9fdc19e1b 100644
--- a/source/funkin/util/logging/AnsiTrace.hx
+++ b/source/funkin/util/logging/AnsiTrace.hx
@@ -52,7 +52,12 @@ class AnsiTrace
   public static function traceBF()
   {
     #if sys
-    if (colorSupported) Sys.println(ansiBF.join("\n"));
+    if (colorSupported)
+    {
+      for (line in ansiBF)
+        Sys.stdout().writeString(line + "\n");
+      Sys.stdout().flush();
+    }
     #end
   }
 

From c385b7887f83d4bf5309c1aaa16ea32e4058b312 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Thu, 7 Mar 2024 15:41:00 -0500
Subject: [PATCH 20/31] Update assets submodule

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

diff --git a/assets b/assets
index bf61ea3c9..3510a2459 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit bf61ea3c9b56bc82c36976d5aa8052bc16ff1b4e
+Subproject commit 3510a245986914ff72736f121347cf4a11db0b35

From a6c44412c50e2dc8ef663f9349bf3e08fde13e38 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Thu, 7 Mar 2024 23:20:02 -0500
Subject: [PATCH 21/31] Exclude WAV files from builds.

---
 Project.xml | 48 ++++++++++++++++++++++++------------------------
 1 file changed, 24 insertions(+), 24 deletions(-)

diff --git a/Project.xml b/Project.xml
index c368dacef..99c46ef9f 100644
--- a/Project.xml
+++ b/Project.xml
@@ -22,8 +22,8 @@
 	<set name="BUILD_DIR" value="export/release" unless="debug" />
 	<set name="BUILD_DIR" value="export/32bit" if="32bit" />
 	<classpath name="source" />
-	<assets path="assets/preload" rename="assets" exclude="*.ogg" if="web" />
-	<assets path="assets/preload" rename="assets" exclude="*.mp3" unless="web" />
+	<assets path="assets/preload" rename="assets" exclude="*.ogg|*.wav" if="web" />
+	<assets path="assets/preload" rename="assets" exclude="*.mp3|*.wav" unless="web" />
 	<define name="PRELOAD_ALL" unless="web" />
 	<define name="NO_PRELOAD_ALL" unless="PRELOAD_ALL" />
 	<section if="PRELOAD_ALL">
@@ -53,28 +53,28 @@
 		<library name="weekend1" preload="false" />
 	</section>
 	<library name="art" preload="false" />
-	<assets path="assets/songs" library="songs" exclude="*.fla|*.ogg" if="web" />
-	<assets path="assets/songs" library="songs" exclude="*.fla|*.mp3" unless="web" />
-	<assets path="assets/shared" library="shared" exclude="*.fla|*.ogg" if="web" />
-	<assets path="assets/shared" library="shared" exclude="*.fla|*.mp3" unless="web" />
-	<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.ogg" if="web" />
-	<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.mp3" unless="web" />
-	<assets path="assets/week1" library="week1" exclude="*.fla|*.ogg" if="web" />
-	<assets path="assets/week1" library="week1" exclude="*.fla|*.mp3" unless="web" />
-	<assets path="assets/week2" library="week2" exclude="*.fla|*.ogg" if="web" />
-	<assets path="assets/week2" library="week2" exclude="*.fla|*.mp3" unless="web" />
-	<assets path="assets/week3" library="week3" exclude="*.fla|*.ogg" if="web" />
-	<assets path="assets/week3" library="week3" exclude="*.fla|*.mp3" unless="web" />
-	<assets path="assets/week4" library="week4" exclude="*.fla|*.ogg" if="web" />
-	<assets path="assets/week4" library="week4" exclude="*.fla|*.mp3" unless="web" />
-	<assets path="assets/week5" library="week5" exclude="*.fla|*.ogg" if="web" />
-	<assets path="assets/week5" library="week5" exclude="*.fla|*.mp3" unless="web" />
-	<assets path="assets/week6" library="week6" exclude="*.fla|*.ogg" if="web" />
-	<assets path="assets/week6" library="week6" exclude="*.fla|*.mp3" unless="web" />
-	<assets path="assets/week7" library="week7" exclude="*.fla|*.ogg" if="web" />
-	<assets path="assets/week7" library="week7" exclude="*.fla|*.mp3" unless="web" />
-	<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.ogg" if="web" />
-	<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.mp3" unless="web" />
+	<assets path="assets/songs" library="songs" exclude="*.fla|*.ogg|*.wav" if="web" />
+	<assets path="assets/songs" library="songs" exclude="*.fla|*.mp3|*.wav" unless="web" />
+	<assets path="assets/shared" library="shared" exclude="*.fla|*.ogg|*.wav" if="web" />
+	<assets path="assets/shared" library="shared" exclude="*.fla|*.mp3|*.wav" unless="web" />
+	<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.ogg|*.wav" if="web" />
+	<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.mp3|*.wav" unless="web" />
+	<assets path="assets/week1" library="week1" exclude="*.fla|*.ogg|*.wav" if="web" />
+	<assets path="assets/week1" library="week1" exclude="*.fla|*.mp3|*.wav" unless="web" />
+	<assets path="assets/week2" library="week2" exclude="*.fla|*.ogg|*.wav" if="web" />
+	<assets path="assets/week2" library="week2" exclude="*.fla|*.mp3|*.wav" unless="web" />
+	<assets path="assets/week3" library="week3" exclude="*.fla|*.ogg|*.wav" if="web" />
+	<assets path="assets/week3" library="week3" exclude="*.fla|*.mp3|*.wav" unless="web" />
+	<assets path="assets/week4" library="week4" exclude="*.fla|*.ogg|*.wav" if="web" />
+	<assets path="assets/week4" library="week4" exclude="*.fla|*.mp3|*.wav" unless="web" />
+	<assets path="assets/week5" library="week5" exclude="*.fla|*.ogg|*.wav" if="web" />
+	<assets path="assets/week5" library="week5" exclude="*.fla|*.mp3|*.wav" unless="web" />
+	<assets path="assets/week6" library="week6" exclude="*.fla|*.ogg|*.wav" if="web" />
+	<assets path="assets/week6" library="week6" exclude="*.fla|*.mp3|*.wav" unless="web" />
+	<assets path="assets/week7" library="week7" exclude="*.fla|*.ogg|*.wav" if="web" />
+	<assets path="assets/week7" library="week7" exclude="*.fla|*.mp3|*.wav" unless="web" />
+	<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.ogg|*.wav" if="web" />
+	<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.mp3|*.wav" unless="web" />
 	<!-- <assets path='example_mods' rename='mods' embed='false'/> -->
 	<!--
 		AUTOMATICALLY MOVING EXAMPLE MODS INTO THE BUILD CAUSES ISSUES

From ab50b7b78ea3af1a2aa90812330dc8188ebc3980 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Thu, 7 Mar 2024 23:22:31 -0500
Subject: [PATCH 22/31] Lightning and rain SFX

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

diff --git a/assets b/assets
index 3510a2459..49c409b4c 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 3510a245986914ff72736f121347cf4a11db0b35
+Subproject commit 49c409b4c8321d8cd317787a78c479aaa64cb517

From d02ccf9abf4734aca60c891bbf4ed6c0e7eb6fad Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 8 Mar 2024 15:05:58 -0500
Subject: [PATCH 23/31] Implement pico bonk sfx

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

diff --git a/assets b/assets
index 095e91fb3..07b50c2a0 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 095e91fb33dad70a5b51e37542e335cedd025d09
+Subproject commit 07b50c2a0305bb23f663b7421b799e684bb35196

From a7b531e57fd769276017e8c7c0cd92762970aff1 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 8 Mar 2024 15:07:10 -0500
Subject: [PATCH 24/31] Fix swapped entries in Blazin' dropdown.

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

diff --git a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
index b53de174e..d2a0a053e 100644
--- a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
+++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
@@ -128,9 +128,9 @@ class ChartEditorDropdowns
     "weekend-1-picouppercutprep" => "Pico Uppercut (Prep) (Blazin')",
     "weekend-1-picouppercut" => "Pico Uppercut (Blazin')",
     "weekend-1-blockhigh" => "Block High (Blazin')",
-    "weekend-1-blocklow" => "Dodge High (Blazin')",
+    "weekend-1-blocklow" => "Block Low (Blazin')",
     "weekend-1-blockspin" => "Block High (Spin) (Blazin')",
-    "weekend-1-dodgehigh" => "Block Low (Blazin')",
+    "weekend-1-dodgehigh" => "Dodge High (Blazin')",
     "weekend-1-dodgelow" => "Dodge Low (Blazin')",
     "weekend-1-dodgespin" => "Dodge High (Spin) (Blazin')",
     "weekend-1-hithigh" => "Hit High (Blazin')",

From 083d66f879b877b7d301474d0aa9498e33c01e39 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 8 Mar 2024 15:29:02 -0500
Subject: [PATCH 25/31] Some animation fixes for Blazin

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

diff --git a/assets b/assets
index 095e91fb3..79ac27372 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 095e91fb33dad70a5b51e37542e335cedd025d09
+Subproject commit 79ac27372fd36431072d438caa76cec0fde69095

From b209e20fb4c08e20363df4d4039af8d047a4b9da Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Fri, 8 Mar 2024 16:47:33 -0500
Subject: [PATCH 26/31] asset submod

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

diff --git a/assets b/assets
index 07b50c2a0..4ca17de92 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 07b50c2a0305bb23f663b7421b799e684bb35196
+Subproject commit 4ca17de923291b1b6886a0dbc6b9878bdbf92937

From 5ece2936aa8026f292d32f18decae571cb63b6fd Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Fri, 8 Mar 2024 17:03:29 -0500
Subject: [PATCH 27/31] assets submod, and gitignore for my lil vim todo lists
 i make lol

---
 .gitignore | 1 +
 assets     | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 87fd97fc5..b6a415fe9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ export/
 RECOVER_*.fla
 shitAudio/
 .build_time
+.swp
diff --git a/assets b/assets
index bd718379f..d1f4f241a 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit bd718379f18ea9562f5260a5e41e20720ebe9e9b
+Subproject commit d1f4f241aa57ca8c9fa5045be989851b3f06f0a4

From 0cd7956b4fbc15f8570a2159ae0a374badaa589a Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Fri, 8 Mar 2024 17:42:34 -0500
Subject: [PATCH 28/31] asset submod

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

diff --git a/assets b/assets
index df8ca76ad..2d3db0cce 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit df8ca76ad94613e7e0a6ec7c27705adb5788007b
+Subproject commit 2d3db0cce9bd06cf280bbf6a0b10e57982f32fc3

From 9284f1495b84f6e0f553d5ff468d827e5f046cd7 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 8 Mar 2024 20:56:34 -0500
Subject: [PATCH 29/31] Fix default value for singTime to use correct unit.

---
 source/funkin/play/character/CharacterData.hx | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx
index 56d7b7793..7d3d6cfb9 100644
--- a/source/funkin/play/character/CharacterData.hx
+++ b/source/funkin/play/character/CharacterData.hx
@@ -378,12 +378,12 @@ class CharacterDataParser
   }
 
   /**
-   * The default time the character should sing for, in beats.
+   * The default time the character should sing for, in steps.
    * Values that are too low will cause the character to stop singing between notes.
-   * Originally, this value was set to 1, but it was changed to 2 because that became
-   * too low after some other code changes.
+   * Values that are too high will cause the character to hold their singing pose for too long after they're done.
+   * @default `8 steps`
    */
-  static final DEFAULT_SINGTIME:Float = 2.0;
+  static final DEFAULT_SINGTIME:Float = 8.0;
 
   static final DEFAULT_DANCEEVERY:Int = 1;
   static final DEFAULT_FLIPX:Bool = false;

From 09029718aaf44d79c2e961e24c24abf2e16a7b42 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 8 Mar 2024 21:33:06 -0500
Subject: [PATCH 30/31] Fix an issue where story menu characters bop too fast

---
 source/funkin/play/stage/Bopper.hx  | 28 +++++++++++++++++-----------
 source/funkin/ui/story/LevelProp.hx |  3 +++
 2 files changed, 20 insertions(+), 11 deletions(-)

diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx
index 3a57072e6..262aff7bc 100644
--- a/source/funkin/play/stage/Bopper.hx
+++ b/source/funkin/play/stage/Bopper.hx
@@ -167,10 +167,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
 
   function update_shouldAlternate():Void
   {
-    if (hasAnimation('danceLeft'))
-    {
-      this.shouldAlternate = true;
-    }
+    this.shouldAlternate = hasAnimation('danceLeft');
   }
 
   /**
@@ -228,10 +225,11 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
 
   /**
    * Ensure that a given animation exists before playing it.
-   * Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play.
-   * @param name
+   * Will gracefully check for name, then name with stripped suffixes, then fail to play.
+   * @param name The animation name to attempt to correct.
+   * @param fallback Instead of failing to play, try to play this animation instead.
    */
-  function correctAnimationName(name:String):String
+  function correctAnimationName(name:String, ?fallback:String):String
   {
     // If the animation exists, we're good.
     if (hasAnimation(name)) return name;
@@ -247,14 +245,22 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
     }
     else
     {
-      if (name != 'idle')
+      if (fallback != null)
       {
-        FlxG.log.warn('Bopper tried to play animation "$name" that does not exist, fallback to idle...');
-        return correctAnimationName('idle');
+        if (fallback == name)
+        {
+          FlxG.log.error('Bopper tried to play animation "$name" that does not exist! This is bad!');
+          return null;
+        }
+        else
+        {
+          FlxG.log.warn('Bopper tried to play animation "$name" that does not exist, fallback to idle...');
+          return correctAnimationName('idle');
+        }
       }
       else
       {
-        FlxG.log.error('Bopper tried to play animation "idle" that does not exist! This is bad!');
+        FlxG.log.error('Bopper tried to play animation "$name" that does not exist! This is bad!');
         return null;
       }
     }
diff --git a/source/funkin/ui/story/LevelProp.hx b/source/funkin/ui/story/LevelProp.hx
index b126f0243..d8eae9c77 100644
--- a/source/funkin/ui/story/LevelProp.hx
+++ b/source/funkin/ui/story/LevelProp.hx
@@ -45,6 +45,9 @@ class LevelProp extends Bopper
       this.visible = true;
     }
 
+    // Reset animation state.
+    this.shouldAlternate = null;
+
     var isAnimated:Bool = propData.animations.length > 0;
     if (isAnimated)
     {

From f1c13f77966542e9ef1d54572f3a24224069589f Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Sun, 10 Mar 2024 13:34:06 -0400
Subject: [PATCH 31/31] assets submod

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

diff --git a/assets b/assets
index 79ac27372..86248e6c9 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 79ac27372fd36431072d438caa76cec0fde69095
+Subproject commit 86248e6c9c64f70349fa7d3055f1df8facab894a