From ba0660c2c0797afa1d146deb21e1b4c7f9acf51d Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Tue, 16 Jan 2024 08:58:31 -0500
Subject: [PATCH 01/15] ansi

---
 source/Main.hx                          |   2 +
 source/funkin/util/logging/AnsiTrace.hx | 120 ++++++++++++++++++++++++
 2 files changed, 122 insertions(+)
 create mode 100644 source/funkin/util/logging/AnsiTrace.hx

diff --git a/source/Main.hx b/source/Main.hx
index 86e520e69..754d0732f 100644
--- a/source/Main.hx
+++ b/source/Main.hx
@@ -33,6 +33,8 @@ class Main extends Sprite
 
   public static function main():Void
   {
+    haxe.Log.trace = funkin.util.logging.AnsiTrace.trace;
+    funkin.util.logging.AnsiTrace.traceBF();
     Lib.current.addChild(new Main());
   }
 
diff --git a/source/funkin/util/logging/AnsiTrace.hx b/source/funkin/util/logging/AnsiTrace.hx
new file mode 100644
index 000000000..993fcd4b9
--- /dev/null
+++ b/source/funkin/util/logging/AnsiTrace.hx
@@ -0,0 +1,120 @@
+package funkin.util.logging;
+
+class AnsiTrace
+{
+  // mostly a copy of haxe.Log.trace()
+  // but adds nice cute ANSI things
+  public static function trace(v:Dynamic, ?info:haxe.PosInfos)
+  {
+    var str = formatOutput(v, info);
+    #if js
+    if (js.Syntax.typeof(untyped console) != "undefined" && (untyped console).log != null) (untyped console).log(str);
+    #elseif lua
+    untyped __define_feature__("use._hx_print", _hx_print(str));
+    #elseif sys
+    Sys.println(str);
+    #else
+    throw new haxe.exceptions.NotImplementedException()
+    #end
+  }
+
+  public static var colorSupported:Bool = (Sys.getEnv("TERM") == "xterm" || Sys.getEnv("ANSICON") != null);
+
+  // ansi stuff
+  public static inline var RED = "\x1b[31m";
+  public static inline var YELLOW = "\x1b[33m";
+  public static inline var WHITE = "\x1b[37m";
+  public static inline var NORMAL = "\x1b[0m";
+  public static inline var BOLD = "\x1b[1m";
+  public static inline var ITALIC = "\x1b[3m";
+
+  // where the real mf magic happens with ansi stuff!
+  public static function formatOutput(v:Dynamic, infos:haxe.PosInfos):String
+  {
+    var str = Std.string(v);
+    if (infos == null) return str;
+
+    if (colorSupported)
+    {
+      var dirs:Array<String> = infos.fileName.split("/");
+      dirs[dirs.length - 1] = ansiWrap(dirs[dirs.length - 1], BOLD);
+
+      // rejoin the dirs
+      infos.fileName = dirs.join("/");
+    }
+
+    var pstr = infos.fileName + ":" + ansiWrap(infos.lineNumber, BOLD);
+    if (infos.customParams != null) for (v in infos.customParams)
+      str += ", " + Std.string(v);
+    return pstr + ": " + str;
+  }
+
+  public static function traceBF()
+  {
+    #if sys
+    if (colorSupported) Sys.println(ansiBF.join("\n"));
+    #end
+  }
+
+  public static function ansiWrap(str:Dynamic, ansiCol:String)
+  {
+    return ansify(ansiCol) + str + ansify(NORMAL);
+  }
+
+  public static function ansify(ansiCol:String)
+  {
+    return (colorSupported ? ansiCol : "");
+  }
+
+  // generated using https://dom111.github.io/image-to-ansi/
+  public static var ansiBF:Array<String> = [
+    "\x1b[39m\x1b[49m                                  \x1b[48;2;154;23;70m            \x1b[49m                                                \x1b[m",
+    "\x1b[39m\x1b[49m                              \x1b[48;2;154;23;70m    \x1b[48;2;184;46;83m  \x1b[48;2;246;87;102m        \x1b[48;2;239;83;100m  \x1b[48;2;154;23;70m          \x1b[48;2;154;23;69m  \x1b[49m                                    \x1b[m",
+    "\x1b[39m\x1b[49m                            \x1b[48;2;154;23;70m  \x1b[48;2;191;52;87m  \x1b[48;2;246;87;102m                        \x1b[48;2;241;84;100m  \x1b[48;2;191;52;87m        \x1b[48;2;153;23;69m  \x1b[49m                          \x1b[m",
+    "\x1b[39m\x1b[49m                          \x1b[48;2;154;23;70m  \x1b[48;2;246;87;102m                                  \x1b[48;2;154;23;70m      \x1b[49m                          \x1b[m",
+    "\x1b[39m\x1b[49m                          \x1b[48;2;154;23;70m  \x1b[48;2;246;87;102m                \x1b[48;2;234;94;114m  \x1b[48;2;160;97;151m  \x1b[48;2;246;87;102m            \x1b[48;2;154;23;70m    \x1b[48;2;205;63;93m    \x1b[48;2;36;35;46m  \x1b[49m                        \x1b[m",
+    "\x1b[39m\x1b[49m                          \x1b[48;2;47;49;144m        \x1b[48;2;246;87;102m          \x1b[48;2;80;121;206m  \x1b[48;2;193;167;177m  \x1b[48;2;246;87;102m          \x1b[48;2;184;46;83m  \x1b[48;2;205;63;93m          \x1b[48;2;20;19;31m  \x1b[49m                      \x1b[m",
+    "\x1b[39m\x1b[49m                          \x1b[48;2;47;49;144m  \x1b[48;2;110;187;236m      \x1b[48;2;109;66;125m  \x1b[48;2;246;87;102m      \x1b[48;2;74;107;200m  \x1b[48;2;141;248;252m  \x1b[48;2;107;177;226m  \x1b[48;2;234;94;114m  \x1b[48;2;246;87;102m      \x1b[48;2;237;81;99m  \x1b[48;2;205;63;93m              \x1b[48;2;20;19;31m  \x1b[49m                    \x1b[m",
+    "\x1b[39m\x1b[49m                    \x1b[48;2;74;106;196m  \x1b[48;2;87;133;210m  \x1b[48;2;64;105;174m  \x1b[48;2;141;248;252m        \x1b[48;2;126;219;244m  \x1b[48;2;57;65;148m  \x1b[48;2;47;49;144m  \x1b[48;2;141;248;252m    \x1b[48;2;129;225;245m  \x1b[48;2;157;94;147m  \x1b[48;2;246;87;102m        \x1b[48;2;159;27;72m  \x1b[48;2;205;63;93m              \x1b[48;2;55;27;43m  \x1b[48;2;21;21;31m  \x1b[49m                  \x1b[m",
+    "\x1b[39m\x1b[49m                  \x1b[48;2;74;107;200m  \x1b[48;2;125;216;244m  \x1b[48;2;141;248;252m              \x1b[48;2;126;219;244m  \x1b[48;2;60;97;187m  \x1b[48;2;141;248;252m  \x1b[48;2;126;219;244m  \x1b[48;2;104;173;229m  \x1b[48;2;146;68;123m  \x1b[48;2;246;87;102m      \x1b[48;2;180;44;82m  \x1b[48;2;205;63;93m                  \x1b[48;2;20;19;31m  \x1b[49m                  \x1b[m",
+    "\x1b[39m\x1b[49m                \x1b[48;2;74;107;200m  \x1b[48;2;141;248;252m        \x1b[48;2;110;187;236m        \x1b[48;2;141;248;252m        \x1b[48;2;110;187;236m  \x1b[48;2;104;173;229m  \x1b[48;2;146;68;123m  \x1b[48;2;246;87;102m      \x1b[48;2;154;23;70m  \x1b[48;2;205;63;93m                  \x1b[48;2;20;19;31m  \x1b[49m                  \x1b[m",
+    "\x1b[39m\x1b[49m              \x1b[48;2;73;106;199m  \x1b[48;2;132;230;247m  \x1b[48;2;141;248;252m    \x1b[48;2;110;187;236m                \x1b[48;2;141;248;252m    \x1b[48;2;110;187;236m    \x1b[48;2;78;118;190m  \x1b[48;2;239;83;100m  \x1b[48;2;246;87;102m    \x1b[48;2;205;63;93m                    \x1b[48;2;20;19;31m  \x1b[49m                  \x1b[m",
+    "\x1b[39m\x1b[49m              \x1b[48;2;73;106;199m  \x1b[48;2;132;230;247m  \x1b[48;2;141;248;252m  \x1b[48;2;110;187;236m        \x1b[48;2;20;19;31m  \x1b[48;2;110;187;236m          \x1b[48;2;141;248;252m  \x1b[48;2;110;187;236m    \x1b[48;2;78;118;190m  \x1b[48;2;239;83;100m  \x1b[48;2;246;87;102m  \x1b[48;2;154;23;70m  \x1b[48;2;205;63;93m                    \x1b[48;2;20;19;31m  \x1b[49m                  \x1b[m",
+    "\x1b[39m\x1b[49m              \x1b[48;2;73;106;199m  \x1b[48;2;132;230;247m  \x1b[48;2;110;187;236m          \x1b[48;2;20;19;31m  \x1b[48;2;110;187;236m                  \x1b[48;2;51;72;160m  \x1b[48;2;246;87;102m  \x1b[48;2;154;23;70m  \x1b[48;2;205;63;93m                    \x1b[48;2;20;19;31m  \x1b[49m                  \x1b[m",
+    "\x1b[39m\x1b[49m                \x1b[48;2;74;107;200m      \x1b[48;2;141;248;252m    \x1b[48;2;110;187;236m  \x1b[48;2;117;138;166m  \x1b[48;2;20;19;31m  \x1b[48;2;110;187;236m  \x1b[48;2;55;134;228m      \x1b[48;2;110;187;236m      \x1b[48;2;139;244;251m    \x1b[48;2;205;63;93m    \x1b[48;2;123;4;53m  \x1b[48;2;125;6;54m  \x1b[48;2;146;23;68m  \x1b[48;2;205;63;93m            \x1b[48;2;123;4;53m  \x1b[48;2;20;19;31m  \x1b[49m                  \x1b[m",
+    "\x1b[39m\x1b[49m                  \x1b[48;2;74;107;200m  \x1b[48;2;141;248;252m    \x1b[48;2;110;187;236m      \x1b[48;2;20;19;31m    \x1b[48;2;103;130;185m  \x1b[48;2;240;174;162m    \x1b[48;2;74;107;200m  \x1b[48;2;110;187;236m  \x1b[48;2;141;248;252m  \x1b[48;2;107;177;226m  \x1b[48;2;74;107;200m  \x1b[48;2;20;19;31m      \x1b[48;2;205;63;93m              \x1b[48;2;20;19;31m    \x1b[48;2;153;78;112m    \x1b[49m                \x1b[m",
+    "\x1b[39m\x1b[49m                \x1b[48;2;74;107;200m  \x1b[48;2;110;187;236m  \x1b[48;2;141;248;252m  \x1b[48;2;110;187;236m    \x1b[48;2;58;123;219m  \x1b[48;2;74;107;200m    \x1b[48;2;20;19;31m  \x1b[48;2;240;174;162m        \x1b[48;2;141;248;252m  \x1b[48;2;135;237;249m  \x1b[48;2;157;140;181m  \x1b[48;2;44;30;46m  \x1b[48;2;20;19;31m            \x1b[48;2;205;63;93m      \x1b[48;2;36;35;46m  \x1b[48;2;153;78;112m  \x1b[48;2;249;225;202m  \x1b[48;2;240;174;162m  \x1b[48;2;153;78;112m  \x1b[49m                \x1b[m",
+    "\x1b[39m\x1b[49m                \x1b[48;2;74;107;200m  \x1b[48;2;141;248;252m  \x1b[48;2;110;187;236m    \x1b[48;2;74;107;200m    \x1b[48;2;20;19;31m  \x1b[48;2;240;174;162m    \x1b[48;2;93;37;66m  \x1b[48;2;240;174;162m    \x1b[48;2;74;107;200m    \x1b[48;2;240;174;162m  \x1b[48;2;130;96;96m  \x1b[48;2;20;19;31m  \x1b[48;2;240;174;162m    \x1b[48;2;74;107;200m  \x1b[48;2;141;248;252m  \x1b[48;2;110;187;236m    \x1b[48;2;20;19;31m  \x1b[48;2;205;63;93m  \x1b[48;2;170;35;77m  \x1b[48;2;196;126;137m  \x1b[48;2;249;225;202m      \x1b[48;2;132;70;100m  \x1b[48;2;20;19;31m    \x1b[49m            \x1b[m",
+    "\x1b[39m\x1b[49m              \x1b[48;2;73;106;199m  \x1b[48;2;132;230;247m  \x1b[48;2;138;242;250m  \x1b[48;2;74;107;200m    \x1b[49m    \x1b[48;2;20;19;31m  \x1b[48;2;240;174;162m    \x1b[48;2;20;19;31m  \x1b[48;2;175;111;124m  \x1b[48;2;235;169;159m  \x1b[48;2;240;174;162m    \x1b[48;2;227;160;155m  \x1b[48;2;20;19;31m  \x1b[48;2;232;165;158m  \x1b[48;2;240;174;162m    \x1b[48;2;85;109;196m  \x1b[48;2;138;242;250m  \x1b[48;2;112;191;237m  \x1b[48;2;104;181;235m  \x1b[48;2;110;187;236m  \x1b[48;2;23;22;43m  \x1b[48;2;26;23;37m  \x1b[48;2;249;225;202m  \x1b[48;2;248;220;198m    \x1b[48;2;249;225;202m  \x1b[48;2;137;90;124m  \x1b[48;2;51;112;205m  \x1b[48;2;53;128;224m  \x1b[48;2;23;25;44m      \x1b[48;2;18;18;28m  \x1b[49m    \x1b[m",
+    "\x1b[39m\x1b[49m              \x1b[48;2;73;106;199m  \x1b[48;2;109;182;229m  \x1b[48;2;74;107;200m  \x1b[49m    \x1b[48;2;151;78;111m  \x1b[48;2;194;126;136m  \x1b[48;2;110;100;98m  \x1b[48;2;244;194;178m    \x1b[48;2;72;42;63m  \x1b[48;2;103;76;81m  \x1b[48;2;191;136;147m  \x1b[48;2;240;174;162m  \x1b[48;2;206;136;142m  \x1b[48;2;20;19;31m    \x1b[48;2;232;165;158m  \x1b[48;2;240;174;162m      \x1b[48;2;65;128;218m  \x1b[48;2;141;248;252m  \x1b[48;2;74;107;200m  \x1b[48;2;85;133;200m  \x1b[48;2;88;139;214m  \x1b[48;2;84;69;150m  \x1b[48;2;211;167;166m  \x1b[48;2;187;116;132m  \x1b[48;2;213;145;147m  \x1b[48;2;245;205;186m  \x1b[48;2;135;83;115m  \x1b[48;2;43;66;124m  \x1b[48;2;47;87;174m  \x1b[48;2;51;130;227m      \x1b[48;2;28;40;76m  \x1b[49m    \x1b[m",
+    "\x1b[39m\x1b[49m              \x1b[48;2;71;103;199m  \x1b[48;2;73;105;197m  \x1b[49m    \x1b[48;2;153;78;112m  \x1b[48;2;196;126;137m  \x1b[48;2;246;209;189m  \x1b[48;2;249;225;202m      \x1b[48;2;226;159;154m  \x1b[48;2;244;215;196m  \x1b[48;2;249;225;202m  \x1b[48;2;240;174;162m  \x1b[48;2;226;159;154m  \x1b[48;2;142;54;93m    \x1b[48;2;245;213;193m  \x1b[48;2;249;225;202m      \x1b[48;2;213;185;192m  \x1b[48;2;85;132;211m  \x1b[48;2;222;158;164m  \x1b[48;2;183;111;129m  \x1b[48;2;110;187;236m  \x1b[48;2;171;158;211m  \x1b[48;2;153;78;112m  \x1b[48;2;196;126;137m  \x1b[48;2;240;174;162m  \x1b[48;2;166;93;120m  \x1b[48;2;130;70;98m  \x1b[48;2;19;19;31m  \x1b[48;2;29;34;56m  \x1b[48;2;55;93;183m      \x1b[48;2;68;101;193m  \x1b[49m    \x1b[m",
+    "\x1b[39m\x1b[49m                      \x1b[48;2;153;78;112m  \x1b[48;2;249;225;202m                    \x1b[48;2;196;126;137m  \x1b[48;2;218;150;149m  \x1b[48;2;249;225;202m          \x1b[48;2;240;174;162m    \x1b[48;2;196;126;137m  \x1b[48;2;47;49;144m  \x1b[48;2;196;126;137m    \x1b[48;2;153;78;112m    \x1b[49m        \x1b[48;2;20;19;31m      \x1b[48;2;41;43;121m  \x1b[49m    \x1b[m",
+    "\x1b[39m\x1b[49m                      \x1b[48;2;153;78;112m  \x1b[48;2;249;225;202m            \x1b[48;2;244;215;196m  \x1b[48;2;249;225;202m            \x1b[48;2;145;49;90m  \x1b[48;2;249;225;202m      \x1b[48;2;240;174;162m    \x1b[48;2;153;78;112m        \x1b[49m                        \x1b[m",
+    "\x1b[39m\x1b[49m                      \x1b[48;2;153;78;112m  \x1b[48;2;196;126;137m  \x1b[48;2;249;225;202m          \x1b[48;2;200;131;139m  \x1b[48;2;249;225;202m          \x1b[48;2;154;63;98m  \x1b[48;2;145;49;90m  \x1b[48;2;240;174;162m  \x1b[48;2;249;225;202m    \x1b[48;2;240;174;162m  \x1b[48;2;153;78;112m  \x1b[48;2;255;224;255m  \x1b[48;2;153;78;112m  \x1b[49m                            \x1b[m",
+    "\x1b[39m\x1b[49m                        \x1b[48;2;153;78;112m  \x1b[48;2;210;141;145m  \x1b[48;2;241;181;168m  \x1b[48;2;249;225;202m        \x1b[48;2;196;126;137m      \x1b[48;2;195;125;136m  \x1b[48;2;170;94;119m  \x1b[48;2;237;73;115m  \x1b[48;2;244;75;120m  \x1b[48;2;145;49;90m  \x1b[48;2;249;225;202m  \x1b[48;2;241;181;167m  \x1b[48;2;181;121;161m  \x1b[48;2;255;224;255m      \x1b[48;2;178;117;159m  \x1b[49m                          \x1b[m",
+    "\x1b[39m\x1b[49m                              \x1b[48;2;136;72;102m        \x1b[48;2;190;119;133m        \x1b[48;2;171;99;123m  \x1b[48;2;152;74;109m  \x1b[48;2;244;75;120m  \x1b[48;2;145;49;90m  \x1b[48;2;190;119;133m  \x1b[48;2;185;128;172m  \x1b[48;2;180;121;164m  \x1b[48;2;255;224;255m        \x1b[48;2;153;78;112m  \x1b[49m                        \x1b[m",
+    "\x1b[39m\x1b[49m            \x1b[48;2;147;80;107m  \x1b[48;2;153;78;112m      \x1b[49m      \x1b[48;2;36;35;46m    \x1b[48;2;98;121;155m  \x1b[48;2;50;68;111m    \x1b[48;2;55;73;115m  \x1b[48;2;36;35;46m    \x1b[48;2;251;117;129m  \x1b[48;2;205;63;93m  \x1b[48;2;230;143;174m  \x1b[48;2;255;224;255m  \x1b[48;2;145;49;90m  \x1b[48;2;153;78;112m  \x1b[48;2;255;224;255m  \x1b[48;2;251;219;252m  \x1b[48;2;105;60;85m  \x1b[48;2;36;35;46m  \x1b[48;2;212;170;223m  \x1b[48;2;255;224;255m    \x1b[48;2;153;78;112m  \x1b[49m                        \x1b[m",
+    "\x1b[39m\x1b[49m          \x1b[48;2;153;78;112m  \x1b[48;2;156;82;114m  \x1b[48;2;240;174;162m    \x1b[48;2;153;78;112m    \x1b[49m    \x1b[48;2;65;84;125m  \x1b[48;2;98;121;155m  \x1b[48;2;124;146;175m  \x1b[48;2;194;215;238m    \x1b[48;2;50;68;111m  \x1b[48;2;124;146;175m  \x1b[48;2;98;121;155m  \x1b[48;2;36;35;46m  \x1b[48;2;254;224;245m  \x1b[48;2;231;143;176m  \x1b[48;2;255;224;255m  \x1b[48;2;145;49;90m    \x1b[48;2;255;224;255m    \x1b[48;2;72;85;110m  \x1b[48;2;240;174;162m  \x1b[48;2;196;126;137m  \x1b[48;2;153;78;112m      \x1b[49m                        \x1b[m",
+    "\x1b[39m\x1b[49m        \x1b[48;2;153;78;112m  \x1b[48;2;196;126;137m  \x1b[48;2;250;227;206m        \x1b[48;2;50;68;111m      \x1b[48;2;65;84;125m  \x1b[48;2;50;68;111m    \x1b[48;2;124;146;175m  \x1b[48;2;78;99;137m  \x1b[48;2;194;215;238m  \x1b[48;2;124;146;175m  \x1b[48;2;36;35;46m    \x1b[48;2;254;224;245m  \x1b[48;2;253;170;192m  \x1b[48;2;255;224;255m    \x1b[48;2;251;117;129m  \x1b[48;2;255;224;255m    \x1b[48;2;170;105;144m  \x1b[48;2;240;174;162m      \x1b[48;2;196;126;137m  \x1b[49m                          \x1b[m",
+    "\x1b[39m\x1b[49m  \x1b[48;2;153;76;111m  \x1b[48;2;165;91;118m  \x1b[48;2;180;108;128m    \x1b[48;2;250;227;206m          \x1b[48;2;116;138;169m  \x1b[48;2;124;146;175m  \x1b[48;2;36;35;46m  \x1b[48;2;116;138;169m  \x1b[48;2;172;193;218m  \x1b[48;2;168;206;237m  \x1b[48;2;49;62;121m  \x1b[48;2;73;92;131m  \x1b[48;2;115;137;168m  \x1b[48;2;116;138;169m  \x1b[48;2;124;146;175m  \x1b[48;2;50;40;54m  \x1b[48;2;57;43;58m  \x1b[48;2;251;170;183m  \x1b[48;2;255;206;227m  \x1b[48;2;251;117;129m  \x1b[48;2;252;132;140m  \x1b[48;2;255;224;255m      \x1b[48;2;153;78;112m  \x1b[48;2;243;190;174m    \x1b[48;2;249;226;203m  \x1b[48;2;196;126;137m  \x1b[48;2;44;70;156m  \x1b[48;2;47;86;175m  \x1b[49m                    \x1b[m",
+    "\x1b[39m\x1b[49m  \x1b[48;2;153;78;112m  \x1b[48;2;239;198;185m  \x1b[48;2;250;227;206m      \x1b[48;2;244;195;179m  \x1b[48;2;250;227;206m      \x1b[48;2;50;68;111m  \x1b[48;2;166;188;213m  \x1b[48;2;36;35;46m  \x1b[48;2;46;58;91m  \x1b[48;2;79;100;138m  \x1b[48;2;71;79;97m  \x1b[48;2;60;69;89m  \x1b[48;2;136;158;188m  \x1b[48;2;117;140;170m  \x1b[48;2;36;35;46m  \x1b[48;2;71;79;97m  \x1b[48;2;79;100;138m  \x1b[48;2;62;51;68m  \x1b[48;2;246;192;224m  \x1b[48;2;253;182;205m    \x1b[48;2;255;224;255m      \x1b[48;2;144;140;167m  \x1b[48;2;82;52;72m  \x1b[48;2;120;110;108m  \x1b[48;2;250;227;206m    \x1b[48;2;229;187;179m  \x1b[48;2;169;166;186m  \x1b[48;2;47;64;156m  \x1b[48;2;46;87;175m  \x1b[49m                  \x1b[m",
+    "\x1b[39m\x1b[49m  \x1b[48;2;153;78;112m  \x1b[48;2;250;227;206m        \x1b[48;2;240;174;162m  \x1b[48;2;220;168;164m  \x1b[48;2;250;227;206m    \x1b[48;2;50;68;111m  \x1b[48;2;194;215;238m  \x1b[48;2;36;35;46m  \x1b[48;2;50;68;111m  \x1b[48;2;98;121;155m  \x1b[48;2;36;35;46m  \x1b[48;2;50;68;111m  \x1b[48;2;98;121;155m  \x1b[48;2;95;115;147m  \x1b[48;2;93;115;150m  \x1b[48;2;50;68;111m    \x1b[48;2;37;37;48m  \x1b[48;2;51;44;59m            \x1b[48;2;66;116;206m  \x1b[48;2;47;83;172m    \x1b[48;2;68;134;227m  \x1b[48;2;250;227;206m    \x1b[48;2;61;91;174m  \x1b[48;2;47;85;173m  \x1b[48;2;47;87;175m  \x1b[48;2;47;86;175m  \x1b[49m                \x1b[m",
+    "\x1b[48;2;153;78;112m  \x1b[48;2;250;227;206m          \x1b[48;2;240;174;162m  \x1b[48;2;217;165;163m  \x1b[48;2;250;227;206m  \x1b[48;2;240;174;162m  \x1b[48;2;82;52;72m  \x1b[48;2;194;215;238m  \x1b[48;2;36;35;46m  \x1b[48;2;50;68;111m      \x1b[48;2;98;121;155m    \x1b[48;2;36;35;46m  \x1b[48;2;98;121;155m    \x1b[48;2;50;68;111m  \x1b[48;2;38;41;58m  \x1b[48;2;47;87;175m          \x1b[48;2;51;130;227m      \x1b[48;2;47;87;175m    \x1b[48;2;51;130;227m  \x1b[48;2;47;49;144m    \x1b[48;2;47;87;175m      \x1b[48;2;45;85;174m  \x1b[49m              \x1b[m",
+    "\x1b[48;2;153;78;112m  \x1b[48;2;250;227;206m  \x1b[48;2;242;185;171m  \x1b[48;2;250;227;206m      \x1b[48;2;240;174;162m  \x1b[48;2;217;165;163m  \x1b[48;2;250;227;206m  \x1b[48;2;240;174;162m  \x1b[48;2;36;35;46m  \x1b[48;2;124;146;175m  \x1b[48;2;36;35;46m    \x1b[48;2;50;68;111m    \x1b[48;2;98;121;155m  \x1b[48;2;50;68;111m    \x1b[48;2;36;35;46m  \x1b[48;2;98;121;155m  \x1b[48;2;36;35;46m  \x1b[48;2;38;41;58m  \x1b[48;2;47;68;159m  \x1b[48;2;47;87;175m      \x1b[48;2;51;130;227m          \x1b[48;2;47;87;175m    \x1b[48;2;51;130;227m      \x1b[48;2;47;87;175m    \x1b[48;2;45;85;174m  \x1b[49m              \x1b[m",
+    "\x1b[48;2;153;78;112m  \x1b[48;2;250;227;206m  \x1b[48;2;242;185;171m  \x1b[48;2;196;126;137m  \x1b[48;2;250;227;206m    \x1b[48;2;240;174;162m  \x1b[48;2;206;136;142m  \x1b[48;2;227;160;155m  \x1b[48;2;240;174;162m    \x1b[48;2;82;52;72m  \x1b[48;2;50;68;111m  \x1b[48;2;36;35;46m  \x1b[48;2;50;68;111m              \x1b[48;2;36;35;46m  \x1b[48;2;47;87;175m          \x1b[48;2;51;130;227m    \x1b[48;2;153;55;95m  \x1b[48;2;47;87;175m  \x1b[48;2;51;130;227m            \x1b[48;2;47;87;175m    \x1b[48;2;45;85;174m  \x1b[49m              \x1b[m",
+    "\x1b[39m\x1b[49m  \x1b[48;2;152;77;112m  \x1b[48;2;228;161;155m  \x1b[48;2;237;171;160m  \x1b[48;2;196;126;137m  \x1b[48;2;193;123;135m    \x1b[48;2;177;105;126m  \x1b[48;2;193;123;135m        \x1b[48;2;37;37;50m  \x1b[48;2;155;69;105m  \x1b[48;2;36;35;46m    \x1b[48;2;49;66;107m      \x1b[48;2;36;35;46m      \x1b[48;2;159;83;115m  \x1b[48;2;155;67;103m  \x1b[48;2;47;69;142m  \x1b[48;2;47;87;175m  \x1b[48;2;46;73;157m  \x1b[48;2;47;85;173m  \x1b[48;2;63;77;159m  \x1b[48;2;247;97;126m  \x1b[48;2;254;165;165m  \x1b[48;2;253;160;161m  \x1b[48;2;159;79;111m  \x1b[48;2;72;106;198m      \x1b[48;2;50;67;149m  \x1b[48;2;53;69;151m  \x1b[48;2;157;77;111m  \x1b[48;2;151;22;68m  \x1b[49m              \x1b[m",
+    "\x1b[39m\x1b[49m    \x1b[48;2;153;78;112m        \x1b[48;2;147;80;107m  \x1b[48;2;176;104;125m  \x1b[48;2;240;174;162m    \x1b[48;2;66;46;63m    \x1b[48;2;36;35;46m  \x1b[48;2;170;35;77m  \x1b[48;2;212;170;223m  \x1b[48;2;255;224;255m    \x1b[48;2;201;153;202m  \x1b[48;2;246;87;102m      \x1b[48;2;245;171;163m  \x1b[48;2;253;156;159m  \x1b[48;2;205;63;93m  \x1b[48;2;154;23;70m  \x1b[48;2;47;87;175m      \x1b[48;2;154;23;70m  \x1b[48;2;246;170;163m  \x1b[48;2;254;165;165m  \x1b[48;2;246;87;102m    \x1b[48;2;245;79;114m      \x1b[48;2;118;0;50m  \x1b[48;2;246;87;102m  \x1b[48;2;219;68;93m  \x1b[48;2;118;0;50m  \x1b[49m              \x1b[m",
+    "\x1b[39m\x1b[49m            \x1b[48;2;147;80;107m  \x1b[48;2;187;116;132m  \x1b[48;2;240;174;162m  \x1b[48;2;153;78;112m      \x1b[48;2;212;170;223m  \x1b[48;2;255;224;255m    \x1b[48;2;212;170;223m  \x1b[48;2;182;124;167m  \x1b[48;2;255;224;255m  \x1b[48;2;251;127;141m  \x1b[48;2;246;87;102m  \x1b[48;2;154;23;70m  \x1b[48;2;205;63;93m  \x1b[48;2;246;87;102m  \x1b[48;2;225;75;97m  \x1b[48;2;154;23;70m        \x1b[48;2;159;27;72m  \x1b[48;2;246;87;102m              \x1b[48;2;154;23;70m  \x1b[48;2;246;87;102m  \x1b[48;2;118;0;50m  \x1b[49m                \x1b[m",
+    "\x1b[39m\x1b[49m              \x1b[48;2;151;78;111m  \x1b[48;2;153;78;112m    \x1b[48;2;255;224;255m      \x1b[48;2;212;170;223m  \x1b[48;2;255;224;255m    \x1b[48;2;212;170;223m  \x1b[48;2;200;151;200m  \x1b[48;2;216;175;226m  \x1b[48;2;154;23;70m    \x1b[48;2;205;63;93m  \x1b[48;2;246;85;105m  \x1b[48;2;205;63;93m    \x1b[48;2;118;0;50m  \x1b[48;2;246;87;102m    \x1b[48;2;159;27;72m  \x1b[48;2;205;63;93m  \x1b[48;2;246;87;102m  \x1b[48;2;212;170;223m  \x1b[48;2;255;224;255m          \x1b[48;2;212;170;223m  \x1b[48;2;144;16;64m  \x1b[48;2;118;0;50m  \x1b[49m              \x1b[m",
+    "\x1b[39m\x1b[49m                \x1b[48;2;154;23;70m  \x1b[48;2;246;87;102m    \x1b[48;2;182;124;167m    \x1b[48;2;255;224;255m  \x1b[48;2;153;78;112m  \x1b[48;2;182;124;167m  \x1b[48;2;255;224;255m  \x1b[48;2;205;63;93m  \x1b[48;2;159;27;72m  \x1b[48;2;154;23;70m  \x1b[48;2;205;63;93m          \x1b[48;2;118;0;50m    \x1b[48;2;246;87;102m    \x1b[48;2;205;63;93m    \x1b[48;2;212;170;223m  \x1b[48;2;255;224;255m  \x1b[48;2;212;170;223m      \x1b[48;2;182;124;167m    \x1b[48;2;168;74;106m  \x1b[48;2;174;39;79m  \x1b[48;2;153;78;112m  \x1b[49m            \x1b[m",
+    "\x1b[39m\x1b[49m                \x1b[48;2;154;23;70m  \x1b[48;2;246;87;102m        \x1b[48;2;211;70;106m  \x1b[48;2;233;196;238m  \x1b[48;2;255;224;255m  \x1b[48;2;178;42;81m  \x1b[48;2;205;157;200m  \x1b[48;2;154;23;70m  \x1b[48;2;205;63;93m  \x1b[48;2;178;42;81m          \x1b[48;2;154;23;70m  \x1b[48;2;153;52;92m  \x1b[48;2;136;40;82m  \x1b[48;2;144;17;63m  \x1b[48;2;225;75;97m    \x1b[48;2;234;198;240m  \x1b[48;2;255;224;255m  \x1b[48;2;234;198;240m  \x1b[48;2;233;196;238m        \x1b[48;2;239;204;243m  \x1b[48;2;212;165;204m  \x1b[48;2;245;87;103m  \x1b[48;2;180;44;82m  \x1b[48;2;180;70;102m  \x1b[48;2;152;77;112m  \x1b[48;2;151;76;110m  \x1b[49m    \x1b[m",
+    "\x1b[39m\x1b[49m                \x1b[48;2;153;68;104m  \x1b[48;2;213;67;95m  \x1b[48;2;246;87;102m      \x1b[48;2;213;67;95m  \x1b[48;2;195;66;97m  \x1b[48;2;173;61;105m  \x1b[48;2;154;23;70m  \x1b[48;2;195;55;89m    \x1b[48;2;205;63;93m  \x1b[48;2;125;4;54m  \x1b[48;2;170;101;145m  \x1b[48;2;171;102;146m  \x1b[48;2;201;153;202m      \x1b[48;2;182;124;167m  \x1b[48;2;158;87;122m  \x1b[48;2;138;36;79m  \x1b[48;2;205;63;93m    \x1b[48;2;196;143;183m  \x1b[48;2;255;224;255m    \x1b[48;2;234;94;114m  \x1b[48;2;229;85;104m      \x1b[48;2;236;96;117m  \x1b[48;2;248;113;131m  \x1b[48;2;246;88;103m  \x1b[48;2;246;87;102m    \x1b[48;2;195;66;97m  \x1b[48;2;172;109;148m  \x1b[49m    \x1b[m",
+    "\x1b[39m\x1b[49m                \x1b[48;2;153;78;112m    \x1b[48;2;205;63;93m      \x1b[48;2;246;87;102m  \x1b[48;2;205;63;93m            \x1b[48;2;154;23;70m    \x1b[48;2;182;124;167m  \x1b[48;2;197;147;195m  \x1b[48;2;212;170;223m  \x1b[48;2;182;124;167m  \x1b[48;2;212;170;223m  \x1b[48;2;153;78;112m  \x1b[48;2;142;44;86m  \x1b[48;2;154;23;70m  \x1b[48;2;205;63;93m    \x1b[48;2;255;224;255m  \x1b[48;2;225;187;233m  \x1b[48;2;246;87;102m  \x1b[48;2;205;63;93m          \x1b[48;2;246;87;102m    \x1b[48;2;205;63;93m  \x1b[48;2;154;23;70m  \x1b[48;2;200;151;200m  \x1b[49m    \x1b[m",
+    "\x1b[39m\x1b[49m                  \x1b[48;2;153;78;112m  \x1b[48;2;255;224;255m  \x1b[48;2;212;170;223m  \x1b[48;2;205;63;93m  \x1b[48;2;154;23;70m              \x1b[48;2;182;124;167m    \x1b[48;2;147;64;101m  \x1b[48;2;36;35;46m      \x1b[48;2;153;78;112m  \x1b[49m  \x1b[48;2;66;78;122m  \x1b[48;2;200;100;119m  \x1b[48;2;205;63;93m  \x1b[48;2;246;87;102m      \x1b[48;2;205;63;93m                \x1b[48;2;154;23;70m    \x1b[48;2;200;151;200m  \x1b[49m    \x1b[m",
+    "\x1b[39m\x1b[49m                  \x1b[48;2;153;78;112m  \x1b[48;2;255;224;255m                \x1b[48;2;212;170;223m    \x1b[48;2;182;124;167m  \x1b[48;2;36;35;46m    \x1b[49m          \x1b[48;2;66;78;122m  \x1b[48;2;153;55;95m  \x1b[48;2;205;63;93m  \x1b[48;2;246;87;102m      \x1b[48;2;205;63;93m              \x1b[48;2;154;23;70m    \x1b[48;2;255;224;255m  \x1b[48;2;176;115;156m  \x1b[49m    \x1b[m",
+    "\x1b[39m\x1b[49m                    \x1b[48;2;153;78;112m    \x1b[48;2;255;224;255m      \x1b[48;2;212;170;223m  \x1b[48;2;255;224;255m    \x1b[48;2;185;128;172m  \x1b[48;2;153;78;112m  \x1b[48;2;36;35;46m  \x1b[49m              \x1b[48;2;66;78;122m  \x1b[48;2;153;55;95m  \x1b[48;2;154;23;70m  \x1b[48;2;246;87;102m      \x1b[48;2;205;63;93m            \x1b[48;2;154;23;70m    \x1b[48;2;255;224;255m  \x1b[48;2;212;170;223m  \x1b[48;2;176;115;156m  \x1b[49m    \x1b[m",
+    "\x1b[39m\x1b[49m                        \x1b[48;2;153;78;112m                \x1b[49m                \x1b[48;2;87;54;75m  \x1b[48;2;207;163;214m  \x1b[48;2;190;136;182m      \x1b[48;2;202;151;191m  \x1b[48;2;244;187;187m            \x1b[48;2;241;206;245m  \x1b[48;2;243;209;246m  \x1b[48;2;212;170;223m    \x1b[48;2;153;78;112m  \x1b[49m    \x1b[m",
+    "\x1b[39m\x1b[49m                                                          \x1b[48;2;56;42;57m    \x1b[48;2;212;170;223m    \x1b[48;2;255;224;255m      \x1b[48;2;212;170;223m  \x1b[48;2;255;224;255m  \x1b[48;2;243;208;246m  \x1b[48;2;238;203;242m  \x1b[48;2;255;224;255m  \x1b[48;2;212;170;223m    \x1b[48;2;153;78;112m  \x1b[48;2;153;77;111m  \x1b[49m    \x1b[m",
+    "\x1b[39m\x1b[49m                                                              \x1b[48;2;153;78;112m                        \x1b[49m        \x1b[m"
+  ];
+}

From 8be9c4f8edcb892c9670d77579b560e46a8d2fa2 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 2 Feb 2024 23:26:02 -0500
Subject: [PATCH 02/15] Fix web builds.

---
 source/funkin/util/logging/AnsiTrace.hx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/source/funkin/util/logging/AnsiTrace.hx b/source/funkin/util/logging/AnsiTrace.hx
index 993fcd4b9..c8d27b86f 100644
--- a/source/funkin/util/logging/AnsiTrace.hx
+++ b/source/funkin/util/logging/AnsiTrace.hx
@@ -18,7 +18,7 @@ class AnsiTrace
     #end
   }
 
-  public static var colorSupported:Bool = (Sys.getEnv("TERM") == "xterm" || Sys.getEnv("ANSICON") != null);
+  public static var colorSupported:Bool = #if sys (Sys.getEnv("TERM") == "xterm" || Sys.getEnv("ANSICON") != null) #else false #end;
 
   // ansi stuff
   public static inline var RED = "\x1b[31m";

From fa556dc1f24a955be93c60bec985433e06fc8200 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 7 Feb 2024 09:21:44 -0500
Subject: [PATCH 03/15] Rewrite conversation JSON parsing code.

---
 .vscode/settings.json                         |   5 +
 Project.xml                                   |   1 -
 hmm.json                                      |   5 -
 source/funkin/InitState.hx                    |  24 +-
 source/funkin/data/DataParse.hx               |  62 +++++
 source/funkin/modding/PolymodHandler.hx       |   8 +-
 source/funkin/play/PlayState.hx               |   4 +-
 .../play/cutscene/dialogue/Conversation.hx    |   2 +-
 .../cutscene/dialogue/ConversationData.hx     | 240 ------------------
 .../dialogue/ConversationDataParser.hx        | 163 ------------
 .../dialogue/ConversationDebugState.hx        |  61 -----
 .../play/cutscene/dialogue/DialogueBox.hx     |   2 +-
 .../play/cutscene/dialogue/DialogueBoxData.hx | 124 ---------
 .../dialogue/DialogueBoxDataParser.hx         | 159 ------------
 .../cutscene/dialogue/ScriptedConversation.hx |   6 +
 .../cutscene/dialogue/ScriptedDialogueBox.hx  |   5 +
 .../play/cutscene/dialogue/SpeakerData.hx     |  78 ------
 .../cutscene/dialogue/SpeakerDataParser.hx    | 159 ------------
 tests/unit/project.xml                        |   1 -
 19 files changed, 103 insertions(+), 1006 deletions(-)
 delete mode 100644 source/funkin/play/cutscene/dialogue/ConversationData.hx
 delete mode 100644 source/funkin/play/cutscene/dialogue/ConversationDataParser.hx
 delete mode 100644 source/funkin/play/cutscene/dialogue/ConversationDebugState.hx
 delete mode 100644 source/funkin/play/cutscene/dialogue/DialogueBoxData.hx
 delete mode 100644 source/funkin/play/cutscene/dialogue/DialogueBoxDataParser.hx
 delete mode 100644 source/funkin/play/cutscene/dialogue/SpeakerData.hx
 delete mode 100644 source/funkin/play/cutscene/dialogue/SpeakerDataParser.hx

diff --git a/.vscode/settings.json b/.vscode/settings.json
index 8979e4de6..3d1f488f7 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -110,6 +110,11 @@
       "target": "windows",
       "args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
     },
+    {
+      "label": "Windows / Debug (Conversation Test)",
+      "target": "windows",
+      "args": ["-debug", "-DDIALOGUE"]
+    },
     {
       "label": "Windows / Debug (Straight to Chart Editor)",
       "target": "windows",
diff --git a/Project.xml b/Project.xml
index f5d506688..9755a5e37 100644
--- a/Project.xml
+++ b/Project.xml
@@ -108,7 +108,6 @@
 	<haxelib name="hxCodec" /> <!-- Video playback -->
 
 	<haxelib name="json2object" /> <!-- JSON parsing -->
-	<haxelib name="tink_json" /> <!-- JSON parsing (DEPRECATED) -->
 	<haxelib name="thx.semver" /> <!-- Version string handling -->
 
 	<haxelib name="hmm" /> <!-- Read library version data at compile time so it can be baked into logs -->
diff --git a/hmm.json b/hmm.json
index c8b1d911e..fa9a67057 100644
--- a/hmm.json
+++ b/hmm.json
@@ -156,11 +156,6 @@
       "name": "thx.semver",
       "type": "haxelib",
       "version": "0.2.2"
-    },
-    {
-      "name": "tink_json",
-      "type": "haxelib",
-      "version": "0.11.0"
     }
   ]
 }
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 23bc255f1..12a937b2a 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -21,9 +21,9 @@ import funkin.data.level.LevelRegistry;
 import funkin.data.notestyle.NoteStyleRegistry;
 import funkin.data.event.SongEventRegistry;
 import funkin.data.stage.StageRegistry;
-import funkin.play.cutscene.dialogue.ConversationDataParser;
-import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
-import funkin.play.cutscene.dialogue.SpeakerDataParser;
+import funkin.data.dialogue.ConversationRegistry;
+import funkin.data.dialogue.DialogueBoxRegistry;
+import funkin.data.dialogue.SpeakerRegistry;
 import funkin.data.song.SongRegistry;
 import funkin.play.character.CharacterData.CharacterDataParser;
 import funkin.modding.module.ModuleHandler;
@@ -208,22 +208,30 @@ class InitState extends FlxState
     // GAME DATA PARSING
     //
 
+    trace('Parsing game data...');
+
+    var perf_gameDataParse_start = haxe.Timer.stamp();
+
     // NOTE: Registries and data parsers must be imported and not referenced with fully qualified names,
     // to ensure build macros work properly.
     SongRegistry.instance.loadEntries();
     LevelRegistry.instance.loadEntries();
     NoteStyleRegistry.instance.loadEntries();
     SongEventRegistry.loadEventCache();
-    ConversationDataParser.loadConversationCache();
-    DialogueBoxDataParser.loadDialogueBoxCache();
-    SpeakerDataParser.loadSpeakerCache();
+    ConversationRegistry.instance.loadEntries();
+    DialogueBoxRegistry.instance.loadEntries();
+    SpeakerRegistry.instance.loadEntries();
     StageRegistry.instance.loadEntries();
-    CharacterDataParser.loadCharacterCache();
+    CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
 
     ModuleHandler.buildModuleCallbacks();
     ModuleHandler.loadModuleCache();
 
     ModuleHandler.callOnCreate();
+
+    var perf_gameDataParse_end = haxe.Timer.stamp();
+
+    trace('Done parsing game data. Duration: ${perf_gameDataParse_end - perf_gameDataParse_start} seconds');
   }
 
   /**
@@ -241,6 +249,8 @@ class InitState extends FlxState
     startLevel(defineLevel(), defineDifficulty());
     #elseif FREEPLAY // -DFREEPLAY
     FlxG.switchState(new FreeplayState());
+    #elseif DIALOGUE // -DDIALOGUE
+    FlxG.switchState(new funkin.ui.debug.dialogue.ConversationDebugState());
     #elseif ANIMATE // -DANIMATE
     FlxG.switchState(new funkin.ui.debug.anim.FlxAnimateTest());
     #elseif WAVEFORM // -DWAVEFORM
diff --git a/source/funkin/data/DataParse.hx b/source/funkin/data/DataParse.hx
index 49dde0198..dbded3fba 100644
--- a/source/funkin/data/DataParse.hx
+++ b/source/funkin/data/DataParse.hx
@@ -120,6 +120,68 @@ class DataParse
     }
   }
 
+  public static function backdropData(json:Json, name:String):BackdropData
+  {
+    switch (json.value)
+    {
+      case JObject(fields):
+        var result:BackdropData = {};
+        var backdropType:String = '';
+
+        for (field in fields)
+        {
+          switch (field.name)
+          {
+            case 'backdropType':
+              backdropType = Tools.getValue(field.value);
+          }
+          Reflect.setField(result, field.name, Tools.getValue(field.value));
+        }
+
+        switch (backdropType)
+        {
+          case 'solid':
+            return SOLID(result);
+          default:
+            throw 'Expected Backdrop property $name to be specify a valid "type", but it was "${backdropType}".';
+        }
+
+        return null;
+      default:
+        throw 'Expected property $name to be an object, but it was ${json.value}.';
+    }
+  }
+
+  public static function outroData(json:Json, name:String):OutroData
+  {
+    switch (json.value)
+    {
+      case JObject(fields):
+        var result:OutroData = {};
+        var outroType:String = '';
+
+        for (field in fields)
+        {
+          switch (field.name)
+          {
+            case 'outroType':
+              outroType = Tools.getValue(field.value);
+          }
+          Reflect.setField(result, field.name, Tools.getValue(field.value));
+        }
+
+        switch (outroType)
+        {
+          case 'none':
+            return NONE(result);
+          case 'fade':
+            return FADE(result);
+          default:
+            throw 'Expected Outro property $name to be specify a valid "type", but it was "${outroType}".';
+        }
+    }
+  }
+
   /**
    * Parser which outputs a `Either<Float, LegacyScrollSpeeds>`.
    * Used by the FNF legacy JSON importer.
diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx
index 151e658b4..5488cbbbe 100644
--- a/source/funkin/modding/PolymodHandler.hx
+++ b/source/funkin/modding/PolymodHandler.hx
@@ -273,11 +273,11 @@ class PolymodHandler
     LevelRegistry.instance.loadEntries();
     NoteStyleRegistry.instance.loadEntries();
     SongEventRegistry.loadEventCache();
-    ConversationDataParser.loadConversationCache();
-    DialogueBoxDataParser.loadDialogueBoxCache();
-    SpeakerDataParser.loadSpeakerCache();
+    ConversationRegistry.instance.loadEntries();
+    DialogueBoxRegistry.instance.loadEntries();
+    SpeakerRegistry.instance.loadEntries();
     StageRegistry.instance.loadEntries();
-    CharacterDataParser.loadCharacterCache();
+    CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
     ModuleHandler.loadModuleCache();
   }
 }
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index cc9debf13..aee9f2210 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -39,7 +39,7 @@ import funkin.modding.events.ScriptEventDispatcher;
 import funkin.play.character.BaseCharacter;
 import funkin.play.character.CharacterData.CharacterDataParser;
 import funkin.play.cutscene.dialogue.Conversation;
-import funkin.play.cutscene.dialogue.ConversationDataParser;
+import funkin.data.dialogue.ConversationRegistry;
 import funkin.play.cutscene.VanillaCutscenes;
 import funkin.play.cutscene.VideoCutscene;
 import funkin.data.event.SongEventRegistry;
@@ -1662,7 +1662,7 @@ class PlayState extends MusicBeatSubState
   {
     isInCutscene = true;
 
-    currentConversation = ConversationDataParser.fetchConversation(conversationId);
+    currentConversation = ConversationRegistry.instance.fetchEntry(conversationId);
     if (currentConversation == null) return;
 
     currentConversation.completeCallback = onConversationComplete;
diff --git a/source/funkin/play/cutscene/dialogue/Conversation.hx b/source/funkin/play/cutscene/dialogue/Conversation.hx
index b2361c795..817c8caf3 100644
--- a/source/funkin/play/cutscene/dialogue/Conversation.hx
+++ b/source/funkin/play/cutscene/dialogue/Conversation.hx
@@ -13,7 +13,7 @@ import funkin.modding.IScriptedClass.IEventHandler;
 import funkin.play.cutscene.dialogue.DialogueBox;
 import funkin.modding.IScriptedClass.IDialogueScriptedClass;
 import funkin.modding.events.ScriptEventDispatcher;
-import funkin.play.cutscene.dialogue.ConversationData.DialogueEntryData;
+import funkin.data.dialogue.ConversationData.DialogueEntryData;
 import flixel.addons.display.FlxPieDial;
 
 /**
diff --git a/source/funkin/play/cutscene/dialogue/ConversationData.hx b/source/funkin/play/cutscene/dialogue/ConversationData.hx
deleted file mode 100644
index 8c4aa9684..000000000
--- a/source/funkin/play/cutscene/dialogue/ConversationData.hx
+++ /dev/null
@@ -1,240 +0,0 @@
-package funkin.play.cutscene.dialogue;
-
-import funkin.util.SerializerUtil;
-
-/**
- * Data about a conversation.
- * Includes what speakers are in the conversation, and what phrases they say.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class ConversationData
-{
-  public var version:String;
-  public var backdrop:BackdropData;
-  public var outro:OutroData;
-  public var music:MusicData;
-  public var dialogue:Array<DialogueEntryData>;
-
-  public function new(version:String, backdrop:BackdropData, outro:OutroData, music:MusicData, dialogue:Array<DialogueEntryData>)
-  {
-    this.version = version;
-    this.backdrop = backdrop;
-    this.outro = outro;
-    this.music = music;
-    this.dialogue = dialogue;
-  }
-
-  public static function fromString(i:String):ConversationData
-  {
-    if (i == null || i == '') return null;
-    var data:
-      {
-        version:String,
-        backdrop:Dynamic, // TODO: tink.Json doesn't like when these are typed
-        ?outro:Dynamic, // TODO: tink.Json doesn't like when these are typed
-        ?music:Dynamic, // TODO: tink.Json doesn't like when these are typed
-        dialogue:Array<Dynamic> // TODO: tink.Json doesn't like when these are typed
-      } = tink.Json.parse(i);
-    return fromJson(data);
-  }
-
-  public static function fromJson(j:Dynamic):ConversationData
-  {
-    // TODO: Check version and perform migrations if necessary.
-    if (j == null) return null;
-    return new ConversationData(j.version, BackdropData.fromJson(j.backdrop), OutroData.fromJson(j.outro), MusicData.fromJson(j.music),
-      j.dialogue.map(d -> DialogueEntryData.fromJson(d)));
-  }
-
-  public function toJson():Dynamic
-  {
-    return {
-      version: this.version,
-      backdrop: this.backdrop.toJson(),
-      dialogue: this.dialogue.map(d -> d.toJson())
-    };
-  }
-}
-
-/**
- * Data about a single dialogue entry.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.DialogueEntryData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class DialogueEntryData
-{
-  /**
-   * The speaker who says this phrase.
-   */
-  public var speaker:String;
-
-  /**
-   * The animation the speaker will play.
-   */
-  public var speakerAnimation:String;
-
-  /**
-   * The text box that will appear.
-   */
-  public var box:String;
-
-  /**
-   * The animation the dialogue box will play.
-   */
-  public var boxAnimation:String;
-
-  /**
-   * The lines of text that will appear in the text box.
-   */
-  public var text:Array<String>;
-
-  /**
-   * The relative speed at which the text will scroll.
-   * @default 1.0
-   */
-  public var speed:Float = 1.0;
-
-  public function new(speaker:String, speakerAnimation:String, box:String, boxAnimation:String, text:Array<String>, speed:Float = null)
-  {
-    this.speaker = speaker;
-    this.speakerAnimation = speakerAnimation;
-    this.box = box;
-    this.boxAnimation = boxAnimation;
-    this.text = text;
-    if (speed != null) this.speed = speed;
-  }
-
-  public static function fromJson(j:Dynamic):DialogueEntryData
-  {
-    if (j == null) return null;
-    return new DialogueEntryData(j.speaker, j.speakerAnimation, j.box, j.boxAnimation, j.text, j.speed);
-  }
-
-  public function toJson():Dynamic
-  {
-    var result:Dynamic =
-      {
-        speaker: this.speaker,
-        speakerAnimation: this.speakerAnimation,
-        box: this.box,
-        boxAnimation: this.boxAnimation,
-        text: this.text,
-      };
-
-    if (this.speed != 1.0) result.speed = this.speed;
-
-    return result;
-  }
-}
-
-/**
- * Data about a backdrop.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.BackdropData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class BackdropData
-{
-  public var type:BackdropType;
-  public var data:Dynamic;
-
-  public function new(typeStr:String, data:Dynamic)
-  {
-    this.type = typeStr;
-    this.data = data;
-  }
-
-  public static function fromJson(j:Dynamic):BackdropData
-  {
-    if (j == null) return null;
-    return new BackdropData(j.type, j.data);
-  }
-
-  public function toJson():Dynamic
-  {
-    return {
-      type: this.type,
-      data: this.data
-    };
-  }
-}
-
-enum abstract BackdropType(String) from String to String
-{
-  public var SOLID:BackdropType = 'solid';
-}
-
-/**
- * Data about a music track.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.MusicData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class MusicData
-{
-  public var asset:String;
-
-  public var fadeTime:Float;
-
-  @:optional
-  @:default(false)
-  public var looped:Bool;
-
-  public function new(asset:String, looped:Bool, fadeTime:Float = 0.0)
-  {
-    this.asset = asset;
-    this.looped = looped;
-    this.fadeTime = fadeTime;
-  }
-
-  public static function fromJson(j:Dynamic):MusicData
-  {
-    if (j == null) return null;
-    return new MusicData(j.asset, j.looped, j.fadeTime);
-  }
-
-  public function toJson():Dynamic
-  {
-    return {
-      asset: this.asset,
-      looped: this.looped,
-      fadeTime: this.fadeTime
-    };
-  }
-}
-
-/**
- * Data about an outro.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.OutroData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class OutroData
-{
-  public var type:OutroType;
-  public var data:Dynamic;
-
-  public function new(?typeStr:String, data:Dynamic)
-  {
-    this.type = typeStr ?? OutroType.NONE;
-    this.data = data;
-  }
-
-  public static function fromJson(j:Dynamic):OutroData
-  {
-    if (j == null) return null;
-    return new OutroData(j.type, j.data);
-  }
-
-  public function toJson():Dynamic
-  {
-    return {
-      type: this.type,
-      data: this.data
-    };
-  }
-}
-
-enum abstract OutroType(String) from String to String
-{
-  public var NONE:OutroType = 'none';
-  public var FADE:OutroType = 'fade';
-}
diff --git a/source/funkin/play/cutscene/dialogue/ConversationDataParser.hx b/source/funkin/play/cutscene/dialogue/ConversationDataParser.hx
deleted file mode 100644
index 9f80f8f9b..000000000
--- a/source/funkin/play/cutscene/dialogue/ConversationDataParser.hx
+++ /dev/null
@@ -1,163 +0,0 @@
-package funkin.play.cutscene.dialogue;
-
-import openfl.Assets;
-import funkin.util.assets.DataAssets;
-import funkin.play.cutscene.dialogue.ScriptedConversation;
-
-/**
- * Contains utilities for loading and parsing conversation data.
- * TODO: Refactor to use the json2object + BaseRegistry system that actually validates things for you.
- */
-class ConversationDataParser
-{
-  public static final CONVERSATION_DATA_VERSION:String = '1.0.0';
-  public static final CONVERSATION_DATA_VERSION_RULE:String = '1.0.x';
-
-  static final conversationCache:Map<String, Conversation> = new Map<String, Conversation>();
-  static final conversationScriptedClass:Map<String, String> = new Map<String, String>();
-
-  static final DEFAULT_CONVERSATION_ID:String = 'UNKNOWN';
-
-  /**
-   * Parses and preloads the game's conversation data and scripts when the game starts.
-   *
-   * If you want to force conversations to be reloaded, you can just call this function again.
-   */
-  public static function loadConversationCache():Void
-  {
-    clearConversationCache();
-    trace('Loading dialogue conversation cache...');
-
-    //
-    // SCRIPTED CONVERSATIONS
-    //
-    var scriptedConversationClassNames:Array<String> = ScriptedConversation.listScriptClasses();
-    trace('  Instantiating ${scriptedConversationClassNames.length} scripted conversations...');
-    for (conversationCls in scriptedConversationClassNames)
-    {
-      var conversation:Conversation = ScriptedConversation.init(conversationCls, DEFAULT_CONVERSATION_ID);
-      if (conversation != null)
-      {
-        trace('    Loaded scripted conversation: ${conversationCls}');
-        // Disable the rendering logic for conversation until it's loaded.
-        // Note that kill() =/= destroy()
-        conversation.kill();
-
-        // Then store it.
-        conversationCache.set(conversation.conversationId, conversation);
-      }
-      else
-      {
-        trace('    Failed to instantiate scripted conversation class: ${conversationCls}');
-      }
-    }
-
-    //
-    // UNSCRIPTED CONVERSATIONS
-    //
-    // Scripts refers to code here, not the actual dialogue.
-    var conversationIdList:Array<String> = DataAssets.listDataFilesInPath('dialogue/conversations/');
-    // Filter out conversations that are scripted.
-    var unscriptedConversationIds:Array<String> = conversationIdList.filter(function(conversationId:String):Bool {
-      return !conversationCache.exists(conversationId);
-    });
-    trace('  Fetching data for ${unscriptedConversationIds.length} conversations...');
-    for (conversationId in unscriptedConversationIds)
-    {
-      try
-      {
-        var conversation:Conversation = new Conversation(conversationId);
-        // Say something offensive to kill the conversation.
-        // We will revive it later.
-        conversation.kill();
-        if (conversation != null)
-        {
-          trace('    Loaded conversation data: ${conversation.conversationId}');
-          conversationCache.set(conversation.conversationId, conversation);
-        }
-      }
-      catch (e)
-      {
-        trace(e);
-        continue;
-      }
-    }
-  }
-
-  /**
-   * Fetches data for a conversation and returns a Conversation instance,
-   * ready to be displayed.
-   * @param conversationId The ID of the conversation to fetch.
-   * @return The conversation instance, or null if the conversation was not found.
-   */
-  public static function fetchConversation(conversationId:String):Null<Conversation>
-  {
-    if (conversationId != null && conversationId != '' && conversationCache.exists(conversationId))
-    {
-      trace('Successfully fetched conversation: ${conversationId}');
-      var conversation:Conversation = conversationCache.get(conversationId);
-      // ...ANYway...
-      conversation.revive();
-      return conversation;
-    }
-    else
-    {
-      trace('Failed to fetch conversation, not found in cache: ${conversationId}');
-      return null;
-    }
-  }
-
-  static function clearConversationCache():Void
-  {
-    if (conversationCache != null)
-    {
-      for (conversation in conversationCache)
-      {
-        conversation.destroy();
-      }
-      conversationCache.clear();
-    }
-  }
-
-  public static function listConversationIds():Array<String>
-  {
-    return conversationCache.keys().array();
-  }
-
-  /**
-   * Load a conversation's JSON file, parse its data, and return it.
-   *
-   * @param conversationId The conversation to load.
-   * @return The conversation data, or null if validation failed.
-   */
-  public static function parseConversationData(conversationId:String):Null<ConversationData>
-  {
-    trace('Parsing conversation data: ${conversationId}');
-    var rawJson:String = loadConversationFile(conversationId);
-
-    try
-    {
-      var conversationData:ConversationData = ConversationData.fromString(rawJson);
-      return conversationData;
-    }
-    catch (e)
-    {
-      trace('Failed to parse conversation ($conversationId).');
-      trace(e);
-      return null;
-    }
-  }
-
-  static function loadConversationFile(conversationPath:String):String
-  {
-    var conversationFilePath:String = Paths.json('dialogue/conversations/${conversationPath}');
-    var rawJson:String = Assets.getText(conversationFilePath).trim();
-
-    while (!rawJson.endsWith('}') && rawJson.length > 0)
-    {
-      rawJson = rawJson.substr(0, rawJson.length - 1);
-    }
-
-    return rawJson;
-  }
-}
diff --git a/source/funkin/play/cutscene/dialogue/ConversationDebugState.hx b/source/funkin/play/cutscene/dialogue/ConversationDebugState.hx
deleted file mode 100644
index 13697b9f4..000000000
--- a/source/funkin/play/cutscene/dialogue/ConversationDebugState.hx
+++ /dev/null
@@ -1,61 +0,0 @@
-package funkin.play.cutscene.dialogue;
-
-import flixel.FlxState;
-import funkin.modding.events.ScriptEventDispatcher;
-import funkin.modding.events.ScriptEvent;
-import flixel.util.FlxColor;
-import funkin.ui.MusicBeatState;
-
-/**
- * A state with displays a conversation with no background.
- * Used for testing.
- * @param conversationId The conversation to display.
- */
-class ConversationDebugState extends MusicBeatState
-{
-  final conversationId:String = 'senpai';
-
-  var conversation:Conversation;
-
-  public function new()
-  {
-    super();
-
-    // TODO: Fix this BS
-    Paths.setCurrentLevel('week6');
-  }
-
-  public override function create():Void
-  {
-    conversation = ConversationDataParser.fetchConversation(conversationId);
-    conversation.completeCallback = onConversationComplete;
-    add(conversation);
-
-    ScriptEventDispatcher.callEvent(conversation, new ScriptEvent(CREATE, false));
-  }
-
-  function onConversationComplete():Void
-  {
-    remove(conversation);
-    conversation = null;
-  }
-
-  public override function update(elapsed:Float):Void
-  {
-    super.update(elapsed);
-
-    if (conversation != null)
-    {
-      if (controls.CUTSCENE_ADVANCE) conversation.advanceConversation();
-
-      if (controls.CUTSCENE_SKIP)
-      {
-        conversation.trySkipConversation(elapsed);
-      }
-      else
-      {
-        conversation.trySkipConversation(-1);
-      }
-    }
-  }
-}
diff --git a/source/funkin/play/cutscene/dialogue/DialogueBox.hx b/source/funkin/play/cutscene/dialogue/DialogueBox.hx
index cdac3c233..4df34badc 100644
--- a/source/funkin/play/cutscene/dialogue/DialogueBox.hx
+++ b/source/funkin/play/cutscene/dialogue/DialogueBox.hx
@@ -235,7 +235,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
   {
     textDisplay = new FlxTypeText(0, 0, 300, '', 32);
     textDisplay.fieldWidth = boxData.text.width;
-    textDisplay.setFormat('Pixel Arial 11 Bold', boxData.text.size, FlxColor.fromString(boxData.text.color), LEFT, SHADOW,
+    textDisplay.setFormat(boxData.text.fontFamily, boxData.text.size, FlxColor.fromString(boxData.text.color), LEFT, SHADOW,
       FlxColor.fromString(boxData.text.shadowColor ?? '#00000000'), false);
     textDisplay.borderSize = boxData.text.shadowWidth ?? 2;
     textDisplay.sounds = [FlxG.sound.load(Paths.sound('pixelText'), 0.6)];
diff --git a/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx b/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx
deleted file mode 100644
index 801a01dd7..000000000
--- a/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx
+++ /dev/null
@@ -1,124 +0,0 @@
-package funkin.play.cutscene.dialogue;
-
-import funkin.data.animation.AnimationData;
-import funkin.util.SerializerUtil;
-
-/**
- * Data about a text box.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.DialogueBoxData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class DialogueBoxData
-{
-  public var version:String;
-  public var name:String;
-  public var assetPath:String;
-  public var flipX:Bool;
-  public var flipY:Bool;
-  public var isPixel:Bool;
-  public var offsets:Array<Float>;
-  public var text:DialogueBoxTextData;
-  public var scale:Float;
-  public var animations:Array<AnimationData>;
-
-  public function new(version:String, name:String, assetPath:String, flipX:Bool = false, flipY:Bool = false, isPixel:Bool = false, offsets:Null<Array<Float>>,
-      text:DialogueBoxTextData, scale:Float = 1.0, animations:Array<AnimationData>)
-  {
-    this.version = version;
-    this.name = name;
-    this.assetPath = assetPath;
-    this.flipX = flipX;
-    this.flipY = flipY;
-    this.isPixel = isPixel;
-    this.offsets = offsets ?? [0, 0];
-    this.text = text;
-    this.scale = scale;
-    this.animations = animations;
-  }
-
-  public static function fromString(i:String):DialogueBoxData
-  {
-    if (i == null || i == '') return null;
-    var data:
-      {
-        version:String,
-        name:String,
-        assetPath:String,
-        flipX:Bool,
-        flipY:Bool,
-        isPixel:Bool,
-        ?offsets:Array<Float>,
-        text:Dynamic,
-        scale:Float,
-        animations:Array<AnimationData>
-      } = tink.Json.parse(i);
-    return fromJson(data);
-  }
-
-  public static function fromJson(j:Dynamic):DialogueBoxData
-  {
-    // TODO: Check version and perform migrations if necessary.
-    if (j == null) return null;
-    return new DialogueBoxData(j.version, j.name, j.assetPath, j.flipX, j.flipY, j.isPixel, j.offsets, DialogueBoxTextData.fromJson(j.text), j.scale,
-      j.animations);
-  }
-
-  public function toJson():Dynamic
-  {
-    return {
-      version: this.version,
-      name: this.name,
-      assetPath: this.assetPath,
-      flipX: this.flipX,
-      flipY: this.flipY,
-      isPixel: this.isPixel,
-      offsets: this.offsets,
-      scale: this.scale,
-      animations: this.animations
-    };
-  }
-}
-
-/**
- * Data about text in a text box.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.DialogueBoxTextData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class DialogueBoxTextData
-{
-  public var offsets:Array<Float>;
-  public var width:Int;
-  public var size:Int;
-  public var color:String;
-  public var shadowColor:Null<String>;
-  public var shadowWidth:Null<Int>;
-
-  public function new(offsets:Null<Array<Float>>, ?width:Int, ?size:Int, color:String, ?shadowColor:String, shadowWidth:Null<Int>)
-  {
-    this.offsets = offsets ?? [0, 0];
-    this.width = width ?? 300;
-    this.size = size ?? 32;
-    this.color = color;
-    this.shadowColor = shadowColor;
-    this.shadowWidth = shadowWidth;
-  }
-
-  public static function fromJson(j:Dynamic):DialogueBoxTextData
-  {
-    // TODO: Check version and perform migrations if necessary.
-    if (j == null) return null;
-    return new DialogueBoxTextData(j.offsets, j.width, j.size, j.color, j.shadowColor, j.shadowWidth);
-  }
-
-  public function toJson():Dynamic
-  {
-    return {
-      offsets: this.offsets,
-      width: this.width,
-      size: this.size,
-      color: this.color,
-      shadowColor: this.shadowColor,
-      shadowWidth: this.shadowWidth,
-    };
-  }
-}
diff --git a/source/funkin/play/cutscene/dialogue/DialogueBoxDataParser.hx b/source/funkin/play/cutscene/dialogue/DialogueBoxDataParser.hx
deleted file mode 100644
index cb00dd80d..000000000
--- a/source/funkin/play/cutscene/dialogue/DialogueBoxDataParser.hx
+++ /dev/null
@@ -1,159 +0,0 @@
-package funkin.play.cutscene.dialogue;
-
-import openfl.Assets;
-import funkin.util.assets.DataAssets;
-import funkin.play.cutscene.dialogue.DialogueBox;
-import funkin.play.cutscene.dialogue.ScriptedDialogueBox;
-
-/**
- * Contains utilities for loading and parsing dialogueBox data.
- */
-class DialogueBoxDataParser
-{
-  public static final DIALOGUE_BOX_DATA_VERSION:String = '1.0.0';
-  public static final DIALOGUE_BOX_DATA_VERSION_RULE:String = '1.0.x';
-
-  static final dialogueBoxCache:Map<String, DialogueBox> = new Map<String, DialogueBox>();
-
-  static final dialogueBoxScriptedClass:Map<String, String> = new Map<String, String>();
-
-  static final DEFAULT_DIALOGUE_BOX_ID:String = 'UNKNOWN';
-
-  /**
-   * Parses and preloads the game's dialogueBox data and scripts when the game starts.
-   *
-   * If you want to force dialogue boxes to be reloaded, you can just call this function again.
-   */
-  public static function loadDialogueBoxCache():Void
-  {
-    clearDialogueBoxCache();
-    trace('Loading dialogue box cache...');
-
-    //
-    // SCRIPTED CONVERSATIONS
-    //
-    var scriptedDialogueBoxClassNames:Array<String> = ScriptedDialogueBox.listScriptClasses();
-    trace('  Instantiating ${scriptedDialogueBoxClassNames.length} scripted dialogue boxes...');
-    for (dialogueBoxCls in scriptedDialogueBoxClassNames)
-    {
-      var dialogueBox:DialogueBox = ScriptedDialogueBox.init(dialogueBoxCls, DEFAULT_DIALOGUE_BOX_ID);
-      if (dialogueBox != null)
-      {
-        trace('    Loaded scripted dialogue box: ${dialogueBox.dialogueBoxName}');
-        // Disable the rendering logic for dialogueBox until it's loaded.
-        // Note that kill() =/= destroy()
-        dialogueBox.kill();
-
-        // Then store it.
-        dialogueBoxCache.set(dialogueBox.dialogueBoxId, dialogueBox);
-      }
-      else
-      {
-        trace('    Failed to instantiate scripted dialogueBox class: ${dialogueBoxCls}');
-      }
-    }
-
-    //
-    // UNSCRIPTED CONVERSATIONS
-    //
-    // Scripts refers to code here, not the actual dialogue.
-    var dialogueBoxIdList:Array<String> = DataAssets.listDataFilesInPath('dialogue/boxes/');
-    // Filter out dialogue boxes that are scripted.
-    var unscriptedDialogueBoxIds:Array<String> = dialogueBoxIdList.filter(function(dialogueBoxId:String):Bool {
-      return !dialogueBoxCache.exists(dialogueBoxId);
-    });
-    trace('  Fetching data for ${unscriptedDialogueBoxIds.length} dialogue boxes...');
-    for (dialogueBoxId in unscriptedDialogueBoxIds)
-    {
-      try
-      {
-        var dialogueBox:DialogueBox = new DialogueBox(dialogueBoxId);
-        if (dialogueBox != null)
-        {
-          trace('    Loaded dialogueBox data: ${dialogueBox.dialogueBoxName}');
-          dialogueBoxCache.set(dialogueBox.dialogueBoxId, dialogueBox);
-        }
-      }
-      catch (e)
-      {
-        trace(e);
-        continue;
-      }
-    }
-  }
-
-  /**
-   * Fetches data for a dialogueBox and returns a DialogueBox instance,
-   * ready to be displayed.
-   * @param dialogueBoxId The ID of the dialogueBox to fetch.
-   * @return The dialogueBox instance, or null if the dialogueBox was not found.
-   */
-  public static function fetchDialogueBox(dialogueBoxId:String):Null<DialogueBox>
-  {
-    if (dialogueBoxId != null && dialogueBoxId != '' && dialogueBoxCache.exists(dialogueBoxId))
-    {
-      trace('Successfully fetched dialogueBox: ${dialogueBoxId}');
-      var dialogueBox:DialogueBox = dialogueBoxCache.get(dialogueBoxId);
-      dialogueBox.revive();
-      return dialogueBox;
-    }
-    else
-    {
-      trace('Failed to fetch dialogueBox, not found in cache: ${dialogueBoxId}');
-      return null;
-    }
-  }
-
-  static function clearDialogueBoxCache():Void
-  {
-    if (dialogueBoxCache != null)
-    {
-      for (dialogueBox in dialogueBoxCache)
-      {
-        dialogueBox.destroy();
-      }
-      dialogueBoxCache.clear();
-    }
-  }
-
-  public static function listDialogueBoxIds():Array<String>
-  {
-    return dialogueBoxCache.keys().array();
-  }
-
-  /**
-   * Load a dialogueBox's JSON file, parse its data, and return it.
-   *
-   * @param dialogueBoxId The dialogueBox to load.
-   * @return The dialogueBox data, or null if validation failed.
-   */
-  public static function parseDialogueBoxData(dialogueBoxId:String):Null<DialogueBoxData>
-  {
-    var rawJson:String = loadDialogueBoxFile(dialogueBoxId);
-
-    try
-    {
-      var dialogueBoxData:DialogueBoxData = DialogueBoxData.fromString(rawJson);
-      return dialogueBoxData;
-    }
-    catch (e)
-    {
-      trace('Failed to parse dialogueBox ($dialogueBoxId).');
-      trace(e);
-      return null;
-    }
-  }
-
-  static function loadDialogueBoxFile(dialogueBoxPath:String):String
-  {
-    var dialogueBoxFilePath:String = Paths.json('dialogue/boxes/${dialogueBoxPath}');
-    var rawJson:String = Assets.getText(dialogueBoxFilePath).trim();
-
-    while (!rawJson.endsWith('}') && rawJson.length > 0)
-    {
-      rawJson = rawJson.substr(0, rawJson.length - 1);
-    }
-
-    return rawJson;
-  }
-}
diff --git a/source/funkin/play/cutscene/dialogue/ScriptedConversation.hx b/source/funkin/play/cutscene/dialogue/ScriptedConversation.hx
index 4fe383a5e..cb7344273 100644
--- a/source/funkin/play/cutscene/dialogue/ScriptedConversation.hx
+++ b/source/funkin/play/cutscene/dialogue/ScriptedConversation.hx
@@ -1,4 +1,10 @@
 package funkin.play.cutscene.dialogue;
 
+/**
+ * A script that can be tied to a Conversation.
+ * Create a scripted class that extends Conversation to use this.
+ * This allows you to customize how a specific conversation appears and behaves.
+ * Someone clever could use this to add branching dialogue I think.
+ */
 @:hscriptClass
 class ScriptedConversation extends Conversation implements polymod.hscript.HScriptedClass {}
diff --git a/source/funkin/play/cutscene/dialogue/ScriptedDialogueBox.hx b/source/funkin/play/cutscene/dialogue/ScriptedDialogueBox.hx
index a1b36c7c2..7689fc0d9 100644
--- a/source/funkin/play/cutscene/dialogue/ScriptedDialogueBox.hx
+++ b/source/funkin/play/cutscene/dialogue/ScriptedDialogueBox.hx
@@ -1,4 +1,9 @@
 package funkin.play.cutscene.dialogue;
 
+/**
+ * A script that can be tied to a DialogueBox.
+ * Create a scripted class that extends DialogueBox to use this.
+ * This allows you to customize how a specific dialogue box appears.
+ */
 @:hscriptClass
 class ScriptedDialogueBox extends DialogueBox implements polymod.hscript.HScriptedClass {}
diff --git a/source/funkin/play/cutscene/dialogue/SpeakerData.hx b/source/funkin/play/cutscene/dialogue/SpeakerData.hx
deleted file mode 100644
index 88883ead8..000000000
--- a/source/funkin/play/cutscene/dialogue/SpeakerData.hx
+++ /dev/null
@@ -1,78 +0,0 @@
-package funkin.play.cutscene.dialogue;
-
-import funkin.data.animation.AnimationData;
-
-/**
- * Data about a conversation.
- * Includes what speakers are in the conversation, and what phrases they say.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.SpeakerData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class SpeakerData
-{
-  public var version:String;
-  public var name:String;
-  public var assetPath:String;
-  public var flipX:Bool;
-  public var isPixel:Bool;
-  public var offsets:Array<Float>;
-  public var scale:Float;
-  public var animations:Array<AnimationData>;
-
-  public function new(version:String, name:String, assetPath:String, animations:Array<AnimationData>, ?offsets:Array<Float>, flipX:Bool = false,
-      isPixel:Bool = false, ?scale:Float = 1.0)
-  {
-    this.version = version;
-    this.name = name;
-    this.assetPath = assetPath;
-    this.animations = animations;
-
-    this.offsets = offsets;
-    if (this.offsets == null || this.offsets == []) this.offsets = [0, 0];
-
-    this.flipX = flipX;
-    this.isPixel = isPixel;
-    this.scale = scale;
-  }
-
-  public static function fromString(i:String):SpeakerData
-  {
-    if (i == null || i == '') return null;
-    var data:
-      {
-        version:String,
-        name:String,
-        assetPath:String,
-        animations:Array<AnimationData>,
-        ?offsets:Array<Float>,
-        ?flipX:Bool,
-        ?isPixel:Bool,
-        ?scale:Float
-      } = tink.Json.parse(i);
-    return fromJson(data);
-  }
-
-  public static function fromJson(j:Dynamic):SpeakerData
-  {
-    // TODO: Check version and perform migrations if necessary.
-    if (j == null) return null;
-    return new SpeakerData(j.version, j.name, j.assetPath, j.animations, j.offsets, j.flipX, j.isPixel, j.scale);
-  }
-
-  public function toJson():Dynamic
-  {
-    var result:Dynamic =
-      {
-        version: this.version,
-        name: this.name,
-        assetPath: this.assetPath,
-        animations: this.animations,
-        flipX: this.flipX,
-        isPixel: this.isPixel
-      };
-
-    if (this.scale != 1.0) result.scale = this.scale;
-
-    return result;
-  }
-}
diff --git a/source/funkin/play/cutscene/dialogue/SpeakerDataParser.hx b/source/funkin/play/cutscene/dialogue/SpeakerDataParser.hx
deleted file mode 100644
index f7ddb099f..000000000
--- a/source/funkin/play/cutscene/dialogue/SpeakerDataParser.hx
+++ /dev/null
@@ -1,159 +0,0 @@
-package funkin.play.cutscene.dialogue;
-
-import openfl.Assets;
-import funkin.util.assets.DataAssets;
-import funkin.play.cutscene.dialogue.Speaker;
-import funkin.play.cutscene.dialogue.ScriptedSpeaker;
-
-/**
- * Contains utilities for loading and parsing speaker data.
- */
-class SpeakerDataParser
-{
-  public static final SPEAKER_DATA_VERSION:String = '1.0.0';
-  public static final SPEAKER_DATA_VERSION_RULE:String = '1.0.x';
-
-  static final speakerCache:Map<String, Speaker> = new Map<String, Speaker>();
-
-  static final speakerScriptedClass:Map<String, String> = new Map<String, String>();
-
-  static final DEFAULT_SPEAKER_ID:String = 'UNKNOWN';
-
-  /**
-   * Parses and preloads the game's speaker data and scripts when the game starts.
-   *
-   * If you want to force speakers to be reloaded, you can just call this function again.
-   */
-  public static function loadSpeakerCache():Void
-  {
-    clearSpeakerCache();
-    trace('Loading dialogue speaker cache...');
-
-    //
-    // SCRIPTED CONVERSATIONS
-    //
-    var scriptedSpeakerClassNames:Array<String> = ScriptedSpeaker.listScriptClasses();
-    trace('  Instantiating ${scriptedSpeakerClassNames.length} scripted speakers...');
-    for (speakerCls in scriptedSpeakerClassNames)
-    {
-      var speaker:Speaker = ScriptedSpeaker.init(speakerCls, DEFAULT_SPEAKER_ID);
-      if (speaker != null)
-      {
-        trace('    Loaded scripted speaker: ${speaker.speakerName}');
-        // Disable the rendering logic for speaker until it's loaded.
-        // Note that kill() =/= destroy()
-        speaker.kill();
-
-        // Then store it.
-        speakerCache.set(speaker.speakerId, speaker);
-      }
-      else
-      {
-        trace('    Failed to instantiate scripted speaker class: ${speakerCls}');
-      }
-    }
-
-    //
-    // UNSCRIPTED CONVERSATIONS
-    //
-    // Scripts refers to code here, not the actual dialogue.
-    var speakerIdList:Array<String> = DataAssets.listDataFilesInPath('dialogue/speakers/');
-    // Filter out speakers that are scripted.
-    var unscriptedSpeakerIds:Array<String> = speakerIdList.filter(function(speakerId:String):Bool {
-      return !speakerCache.exists(speakerId);
-    });
-    trace('  Fetching data for ${unscriptedSpeakerIds.length} speakers...');
-    for (speakerId in unscriptedSpeakerIds)
-    {
-      try
-      {
-        var speaker:Speaker = new Speaker(speakerId);
-        if (speaker != null)
-        {
-          trace('    Loaded speaker data: ${speaker.speakerName}');
-          speakerCache.set(speaker.speakerId, speaker);
-        }
-      }
-      catch (e)
-      {
-        trace(e);
-        continue;
-      }
-    }
-  }
-
-  /**
-   * Fetches data for a speaker and returns a Speaker instance,
-   * ready to be displayed.
-   * @param speakerId The ID of the speaker to fetch.
-   * @return The speaker instance, or null if the speaker was not found.
-   */
-  public static function fetchSpeaker(speakerId:String):Null<Speaker>
-  {
-    if (speakerId != null && speakerId != '' && speakerCache.exists(speakerId))
-    {
-      trace('Successfully fetched speaker: ${speakerId}');
-      var speaker:Speaker = speakerCache.get(speakerId);
-      speaker.revive();
-      return speaker;
-    }
-    else
-    {
-      trace('Failed to fetch speaker, not found in cache: ${speakerId}');
-      return null;
-    }
-  }
-
-  static function clearSpeakerCache():Void
-  {
-    if (speakerCache != null)
-    {
-      for (speaker in speakerCache)
-      {
-        speaker.destroy();
-      }
-      speakerCache.clear();
-    }
-  }
-
-  public static function listSpeakerIds():Array<String>
-  {
-    return speakerCache.keys().array();
-  }
-
-  /**
-   * Load a speaker's JSON file, parse its data, and return it.
-   *
-   * @param speakerId The speaker to load.
-   * @return The speaker data, or null if validation failed.
-   */
-  public static function parseSpeakerData(speakerId:String):Null<SpeakerData>
-  {
-    var rawJson:String = loadSpeakerFile(speakerId);
-
-    try
-    {
-      var speakerData:SpeakerData = SpeakerData.fromString(rawJson);
-      return speakerData;
-    }
-    catch (e)
-    {
-      trace('Failed to parse speaker ($speakerId).');
-      trace(e);
-      return null;
-    }
-  }
-
-  static function loadSpeakerFile(speakerPath:String):String
-  {
-    var speakerFilePath:String = Paths.json('dialogue/speakers/${speakerPath}');
-    var rawJson:String = Assets.getText(speakerFilePath).trim();
-
-    while (!rawJson.endsWith('}') && rawJson.length > 0)
-    {
-      rawJson = rawJson.substr(0, rawJson.length - 1);
-    }
-
-    return rawJson;
-  }
-}
diff --git a/tests/unit/project.xml b/tests/unit/project.xml
index 2e505e015..dfbf06502 100644
--- a/tests/unit/project.xml
+++ b/tests/unit/project.xml
@@ -27,7 +27,6 @@
 	<haxelib name="hxCodec" /> <!-- Video playback -->
 	<haxelib name="thx.semver" /> <!-- Semantic version handling -->
 	<haxelib name="json2object" /> <!-- JSON parsing -->
-	<haxelib name="tink_json" /> <!-- JSON parsing -->
 
 	<!-- Test dependencies -->
 	<haxelib name="munit" /> <!-- Unit test execution -->

From 31cd5b34146f67d58f440826f17d48f2961f9ebc Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 7 Feb 2024 18:45:13 -0500
Subject: [PATCH 04/15] Rework Conversation data parsing

---
 source/funkin/InitState.hx                    |  17 +-
 source/funkin/data/BaseRegistry.hx            |   7 +-
 source/funkin/data/DataParse.hx               |  15 +-
 .../funkin/data/dialogue/ConversationData.hx  | 168 ++++++++++++++++++
 .../data/dialogue/ConversationRegistry.hx     |  81 +++++++++
 .../funkin/data/dialogue/DialogueBoxData.hx   | 128 +++++++++++++
 .../data/dialogue/DialogueBoxRegistry.hx      |  81 +++++++++
 source/funkin/data/dialogue/SpeakerData.hx    |  68 +++++++
 .../funkin/data/dialogue/SpeakerRegistry.hx   |  81 +++++++++
 source/funkin/data/song/SongRegistry.hx       |   4 +-
 source/funkin/modding/PolymodHandler.hx       |  12 +-
 source/funkin/play/character/CharacterData.hx |   2 +-
 .../play/cutscene/dialogue/Conversation.hx    |  96 +++++-----
 .../play/cutscene/dialogue/DialogueBox.hx     |  68 ++++---
 .../funkin/play/cutscene/dialogue/Speaker.hx  |  55 ++++--
 .../debug/dialogue/ConversationDebugState.hx  |  76 ++++++++
 source/funkin/util/assets/DataAssets.hx       |   3 +-
 17 files changed, 849 insertions(+), 113 deletions(-)
 create mode 100644 source/funkin/data/dialogue/ConversationData.hx
 create mode 100644 source/funkin/data/dialogue/ConversationRegistry.hx
 create mode 100644 source/funkin/data/dialogue/DialogueBoxData.hx
 create mode 100644 source/funkin/data/dialogue/DialogueBoxRegistry.hx
 create mode 100644 source/funkin/data/dialogue/SpeakerData.hx
 create mode 100644 source/funkin/data/dialogue/SpeakerRegistry.hx
 create mode 100644 source/funkin/ui/debug/dialogue/ConversationDebugState.hx

diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 12a937b2a..625a33ad7 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -208,30 +208,29 @@ class InitState extends FlxState
     // GAME DATA PARSING
     //
 
-    trace('Parsing game data...');
-
-    var perf_gameDataParse_start = haxe.Timer.stamp();
-
-    // NOTE: Registries and data parsers must be imported and not referenced with fully qualified names,
+    // NOTE: Registries must be imported and not referenced with fully qualified names,
     // to ensure build macros work properly.
+    trace('Parsing game data...');
+    var perfStart = haxe.Timer.stamp();
+    SongEventRegistry.loadEventCache(); // SongEventRegistry is structured differently so it's not a BaseRegistry.
     SongRegistry.instance.loadEntries();
     LevelRegistry.instance.loadEntries();
     NoteStyleRegistry.instance.loadEntries();
-    SongEventRegistry.loadEventCache();
     ConversationRegistry.instance.loadEntries();
     DialogueBoxRegistry.instance.loadEntries();
     SpeakerRegistry.instance.loadEntries();
     StageRegistry.instance.loadEntries();
+
+    // TODO: CharacterDataParser doesn't use json2object, so it's way slower than the other parsers.
     CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
 
     ModuleHandler.buildModuleCallbacks();
     ModuleHandler.loadModuleCache();
-
     ModuleHandler.callOnCreate();
 
-    var perf_gameDataParse_end = haxe.Timer.stamp();
+    var perfEnd = haxe.Timer.stamp();
 
-    trace('Done parsing game data. Duration: ${perf_gameDataParse_end - perf_gameDataParse_start} seconds');
+    trace('Parsing game data took ${Math.floor((perfEnd - perfStart) * 1000)}ms.');
   }
 
   /**
diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx
index 70615069b..0ccbe2f18 100644
--- a/source/funkin/data/BaseRegistry.hx
+++ b/source/funkin/data/BaseRegistry.hx
@@ -46,6 +46,9 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
     this.entries = new Map<String, T>();
   }
 
+  /**
+   * TODO: Create a `loadEntriesAsync()` function.
+   */
   public function loadEntries():Void
   {
     clearEntries();
@@ -54,7 +57,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
     // SCRIPTED ENTRIES
     //
     var scriptedEntryClassNames:Array<String> = getScriptedClassNames();
-    log('Registering ${scriptedEntryClassNames.length} scripted entries...');
+    log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
 
     for (entryCls in scriptedEntryClassNames)
     {
@@ -78,7 +81,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
     var unscriptedEntryIds:Array<String> = entryIdList.filter(function(entryId:String):Bool {
       return !entries.exists(entryId);
     });
-    log('Fetching data for ${unscriptedEntryIds.length} unscripted entries...');
+    log('Parsing ${unscriptedEntryIds.length} unscripted entries...');
     for (entryId in unscriptedEntryIds)
     {
       try
diff --git a/source/funkin/data/DataParse.hx b/source/funkin/data/DataParse.hx
index dbded3fba..244d41132 100644
--- a/source/funkin/data/DataParse.hx
+++ b/source/funkin/data/DataParse.hx
@@ -120,19 +120,19 @@ class DataParse
     }
   }
 
-  public static function backdropData(json:Json, name:String):BackdropData
+  public static function backdropData(json:Json, name:String):funkin.data.dialogue.ConversationData.BackdropData
   {
     switch (json.value)
     {
       case JObject(fields):
-        var result:BackdropData = {};
+        var result:Dynamic = {};
         var backdropType:String = '';
 
         for (field in fields)
         {
           switch (field.name)
           {
-            case 'backdropType':
+            case 'type':
               backdropType = Tools.getValue(field.value);
           }
           Reflect.setField(result, field.name, Tools.getValue(field.value));
@@ -152,19 +152,19 @@ class DataParse
     }
   }
 
-  public static function outroData(json:Json, name:String):OutroData
+  public static function outroData(json:Json, name:String):Null<funkin.data.dialogue.ConversationData.OutroData>
   {
     switch (json.value)
     {
       case JObject(fields):
-        var result:OutroData = {};
+        var result:Dynamic = {};
         var outroType:String = '';
 
         for (field in fields)
         {
           switch (field.name)
           {
-            case 'outroType':
+            case 'type':
               outroType = Tools.getValue(field.value);
           }
           Reflect.setField(result, field.name, Tools.getValue(field.value));
@@ -179,6 +179,9 @@ class DataParse
           default:
             throw 'Expected Outro property $name to be specify a valid "type", but it was "${outroType}".';
         }
+        return null;
+      default:
+        throw 'Expected property $name to be an object, but it was ${json.value}.';
     }
   }
 
diff --git a/source/funkin/data/dialogue/ConversationData.hx b/source/funkin/data/dialogue/ConversationData.hx
new file mode 100644
index 000000000..795ddae9a
--- /dev/null
+++ b/source/funkin/data/dialogue/ConversationData.hx
@@ -0,0 +1,168 @@
+package funkin.data.dialogue;
+
+import funkin.data.animation.AnimationData;
+
+/**
+ * A type definition for the data for a specific conversation.
+ * It includes things like what dialogue boxes to use, what text to display, and what animations to play.
+ * @see https://lib.haxe.org/p/json2object/
+ */
+typedef ConversationData =
+{
+  /**
+   * Semantic version for conversation data.
+   */
+  public var version:String;
+
+  /**
+   * Data on the backdrop for the conversation.
+   */
+  @:jcustomparse(funkin.data.DataParse.backdropData)
+  public var backdrop:BackdropData;
+
+  /**
+   * Data on the outro for the conversation.
+   */
+  @:jcustomparse(funkin.data.DataParse.outroData)
+  @:optional
+  public var outro:Null<OutroData>;
+
+  /**
+   * Data on the music for the conversation.
+   */
+  @:optional
+  public var music:Null<MusicData>;
+
+  /**
+   * Data for each line of dialogue in the conversation.
+   */
+  public var dialogue:Array<DialogueEntryData>;
+}
+
+/**
+ * Data on the backdrop for the conversation, behind the dialogue box.
+ * A custom parser distinguishes between backdrop types based on the `type` field.
+ */
+enum BackdropData
+{
+  SOLID(data:BackdropData_Solid); // 'solid'
+}
+
+/**
+ * Data for a Solid color backdrop.
+ */
+typedef BackdropData_Solid =
+{
+  /**
+   * Used to distinguish between backdrop types. Should always be `solid` for this type.
+   */
+  var type:String;
+
+  /**
+   * The color of the backdrop.
+   */
+  var color:String;
+
+  /**
+   * Fade-in time for the backdrop.
+   * @default No fade-in
+   */
+  @:optional
+  @:default(0.0)
+  var fadeTime:Float;
+};
+
+enum OutroData
+{
+  NONE(data:OutroData_None); // 'none'
+  FADE(data:OutroData_Fade); // 'fade'
+}
+
+typedef OutroData_None =
+{
+  /**
+   * Used to distinguish between outro types. Should always be `none` for this type.
+   */
+  var type:String;
+}
+
+typedef OutroData_Fade =
+{
+  /**
+   * Used to distinguish between outro types. Should always be `fade` for this type.
+   */
+  var type:String;
+
+  /**
+   * The time to fade out the conversation.
+   * @default 1 second
+   */
+  @:optional
+  @:default(1.0)
+  var fadeTime:Float;
+}
+
+typedef MusicData =
+{
+  /**
+   * The asset to play for the music.
+   */
+  var asset:String;
+
+  /**
+   * The time to fade in the music.
+   */
+  @:optional
+  @:default(0.0)
+  var fadeTime:Float;
+
+  @:optional
+  @:default(false)
+  var looped:Bool;
+};
+
+/**
+ * Data on a single line of dialogue in a conversation.
+ */
+typedef DialogueEntryData =
+{
+  /**
+   * Which speaker is speaking.
+   * @see `SpeakerData.hx`
+   */
+  public var speaker:String;
+
+  /**
+   * The animation the speaker should play for this line of dialogue.
+   */
+  public var speakerAnimation:String;
+
+  /**
+   * Which dialogue box to use for this line of dialogue.
+   * @see `DialogueBoxData.hx`
+   */
+  public var box:String;
+
+  /**
+   * Which animation to play for the dialogue box.
+   */
+  public var boxAnimation:String;
+
+  /**
+   * The text that will display for this line of dialogue.
+   * Text will automatically wrap.
+   * When the user advances the dialogue, the next entry in the array will concatenate on.
+   * Advancing when the last entry is displayed will move to the next `DialogueEntryData`,
+   * or end the conversation if there are no more.
+   */
+  public var text:Array<String>;
+
+  /**
+   * The relative speed at which text gets "typed out".
+   * Setting `speed` to `1.5` would make it look like the character is speaking quickly,
+   * and setting `speed` to `0.5` would make it look like the character is emphasizing each word.
+   */
+  @:optional
+  @:default(1.0)
+  public var speed:Float;
+};
diff --git a/source/funkin/data/dialogue/ConversationRegistry.hx b/source/funkin/data/dialogue/ConversationRegistry.hx
new file mode 100644
index 000000000..9186ef786
--- /dev/null
+++ b/source/funkin/data/dialogue/ConversationRegistry.hx
@@ -0,0 +1,81 @@
+package funkin.data.dialogue;
+
+import funkin.play.cutscene.dialogue.Conversation;
+import funkin.data.dialogue.ConversationData;
+import funkin.play.cutscene.dialogue.ScriptedConversation;
+
+class ConversationRegistry extends BaseRegistry<Conversation, ConversationData>
+{
+  /**
+   * The current version string for the dialogue box data format.
+   * Handle breaking changes by incrementing this value
+   * and adding migration to the `migrateConversationData()` function.
+   */
+  public static final CONVERSATION_DATA_VERSION:thx.semver.Version = "1.0.0";
+
+  public static final CONVERSATION_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
+
+  public static final instance:ConversationRegistry = new ConversationRegistry();
+
+  public function new()
+  {
+    super('CONVERSATION', 'dialogue/conversations', CONVERSATION_DATA_VERSION_RULE);
+  }
+
+  /**
+   * Read, parse, and validate the JSON data and produce the corresponding data object.
+   */
+  public function parseEntryData(id:String):Null<ConversationData>
+  {
+    // JsonParser does not take type parameters,
+    // otherwise this function would be in BaseRegistry.
+    var parser = new json2object.JsonParser<ConversationData>();
+    parser.ignoreUnknownVariables = false;
+
+    switch (loadEntryFile(id))
+    {
+      case {fileName: fileName, contents: contents}:
+        parser.fromJson(contents, fileName);
+      default:
+        return null;
+    }
+
+    if (parser.errors.length > 0)
+    {
+      printErrors(parser.errors, id);
+      return null;
+    }
+    return parser.value;
+  }
+
+  /**
+   * Parse and validate the JSON data and produce the corresponding data object.
+   *
+   * NOTE: Must be implemented on the implementation class.
+   * @param contents The JSON as a string.
+   * @param fileName An optional file name for error reporting.
+   */
+  public function parseEntryDataRaw(contents:String, ?fileName:String):Null<ConversationData>
+  {
+    var parser = new json2object.JsonParser<ConversationData>();
+    parser.ignoreUnknownVariables = false;
+    parser.fromJson(contents, fileName);
+
+    if (parser.errors.length > 0)
+    {
+      printErrors(parser.errors, fileName);
+      return null;
+    }
+    return parser.value;
+  }
+
+  function createScriptedEntry(clsName:String):Conversation
+  {
+    return ScriptedConversation.init(clsName, "unknown");
+  }
+
+  function getScriptedClassNames():Array<String>
+  {
+    return ScriptedConversation.listScriptClasses();
+  }
+}
diff --git a/source/funkin/data/dialogue/DialogueBoxData.hx b/source/funkin/data/dialogue/DialogueBoxData.hx
new file mode 100644
index 000000000..a75a5595a
--- /dev/null
+++ b/source/funkin/data/dialogue/DialogueBoxData.hx
@@ -0,0 +1,128 @@
+package funkin.data.dialogue;
+
+import funkin.data.animation.AnimationData;
+
+/**
+ * A type definition for the data for a conversation text box.
+ * It includes things like the sprite to use, and the font and color for the text.
+ * The actual text is included in the ConversationData.
+ * @see https://lib.haxe.org/p/json2object/
+ */
+typedef DialogueBoxData =
+{
+  /**
+   * Semantic version for dialogue box data.
+   */
+  public var version:String;
+
+  /**
+   * A human readable name for the dialogue box type.
+   */
+  public var name:String;
+
+  /**
+   * The asset path for the sprite to use for the dialogue box.
+   * Takes a static sprite or a sprite sheet.
+   */
+  public var assetPath:String;
+
+  /**
+   * Whether to horizontally flip the dialogue box sprite.
+   */
+  @:optional
+  @:default(false)
+  public var flipX:Bool;
+
+  /**
+   * Whether to vertically flip the dialogue box sprite.
+   */
+  @:optional
+  @:default(false)
+  public var flipY:Bool;
+
+  /**
+   * Whether to disable anti-aliasing for the dialogue box sprite.
+   */
+  @:optional
+  @:default(false)
+  public var isPixel:Bool;
+
+  /**
+   * The relative horizontal and vertical offsets for the dialogue box sprite.
+   */
+  @:optional
+  @:default([0, 0])
+  public var offsets:Array<Float>;
+
+  /**
+   * Info about how to display text in the dialogue box.
+   */
+  public var text:DialogueBoxTextData;
+
+  /**
+   * Multiply the size of the dialogue box sprite.
+   */
+  @:optional
+  @:default(1)
+  public var scale:Float;
+
+  /**
+   * If using a spritesheet for the dialogue box, the animations to use.
+   */
+  @:optional
+  @:default([])
+  public var animations:Array<AnimationData>;
+}
+
+typedef DialogueBoxTextData =
+{
+  /**
+   * The position of the text in teh box.
+   */
+  @:optional
+  @:default([0, 0])
+  var offsets:Array<Float>;
+
+  /**
+   * The width of the
+   */
+  @:optional
+  @:default(300)
+  var width:Int;
+
+  /**
+   * The font size to use for the text.
+   */
+  @:optional
+  @:default(32)
+  var size:Int;
+
+  /**
+   * The color to use for the text.
+   * Use a string that can be translated to a color, like `#FF0000` for red.
+   */
+  @:optional
+  @:default("#000000")
+  var color:String;
+
+  /**
+   * The font to use for the text.
+   * @since v1.1.0
+   * @default `Arial`, make sure to switch this!
+   */
+  @:optional
+  @:default("Arial")
+  var fontFamily:String;
+
+  /**
+   * The color to use for the shadow of the text. Use transparent to disable.
+   */
+  var shadowColor:String;
+
+  /**
+   * The width of the shadow of the text.
+   */
+  @:optional
+  @:default(0)
+  var shadowWidth:Int;
+};
diff --git a/source/funkin/data/dialogue/DialogueBoxRegistry.hx b/source/funkin/data/dialogue/DialogueBoxRegistry.hx
new file mode 100644
index 000000000..87205d96c
--- /dev/null
+++ b/source/funkin/data/dialogue/DialogueBoxRegistry.hx
@@ -0,0 +1,81 @@
+package funkin.data.dialogue;
+
+import funkin.play.cutscene.dialogue.DialogueBox;
+import funkin.data.dialogue.DialogueBoxData;
+import funkin.play.cutscene.dialogue.ScriptedDialogueBox;
+
+class DialogueBoxRegistry extends BaseRegistry<DialogueBox, DialogueBoxData>
+{
+  /**
+   * The current version string for the dialogue box data format.
+   * Handle breaking changes by incrementing this value
+   * and adding migration to the `migrateDialogueBoxData()` function.
+   */
+  public static final DIALOGUEBOX_DATA_VERSION:thx.semver.Version = "1.1.0";
+
+  public static final DIALOGUEBOX_DATA_VERSION_RULE:thx.semver.VersionRule = "1.1.x";
+
+  public static final instance:DialogueBoxRegistry = new DialogueBoxRegistry();
+
+  public function new()
+  {
+    super('DIALOGUEBOX', 'dialogue/boxes', DIALOGUEBOX_DATA_VERSION_RULE);
+  }
+
+  /**
+   * Read, parse, and validate the JSON data and produce the corresponding data object.
+   */
+  public function parseEntryData(id:String):Null<DialogueBoxData>
+  {
+    // JsonParser does not take type parameters,
+    // otherwise this function would be in BaseRegistry.
+    var parser = new json2object.JsonParser<DialogueBoxData>();
+    parser.ignoreUnknownVariables = false;
+
+    switch (loadEntryFile(id))
+    {
+      case {fileName: fileName, contents: contents}:
+        parser.fromJson(contents, fileName);
+      default:
+        return null;
+    }
+
+    if (parser.errors.length > 0)
+    {
+      printErrors(parser.errors, id);
+      return null;
+    }
+    return parser.value;
+  }
+
+  /**
+   * Parse and validate the JSON data and produce the corresponding data object.
+   *
+   * NOTE: Must be implemented on the implementation class.
+   * @param contents The JSON as a string.
+   * @param fileName An optional file name for error reporting.
+   */
+  public function parseEntryDataRaw(contents:String, ?fileName:String):Null<DialogueBoxData>
+  {
+    var parser = new json2object.JsonParser<DialogueBoxData>();
+    parser.ignoreUnknownVariables = false;
+    parser.fromJson(contents, fileName);
+
+    if (parser.errors.length > 0)
+    {
+      printErrors(parser.errors, fileName);
+      return null;
+    }
+    return parser.value;
+  }
+
+  function createScriptedEntry(clsName:String):DialogueBox
+  {
+    return ScriptedDialogueBox.init(clsName, "unknown");
+  }
+
+  function getScriptedClassNames():Array<String>
+  {
+    return ScriptedDialogueBox.listScriptClasses();
+  }
+}
diff --git a/source/funkin/data/dialogue/SpeakerData.hx b/source/funkin/data/dialogue/SpeakerData.hx
new file mode 100644
index 000000000..e8a2eacf0
--- /dev/null
+++ b/source/funkin/data/dialogue/SpeakerData.hx
@@ -0,0 +1,68 @@
+package funkin.data.dialogue;
+
+import funkin.data.animation.AnimationData;
+
+/**
+ * A type definition for a specific speaker in a conversation.
+ * It includes things like what sprite to use and its available animations.
+ * @see https://lib.haxe.org/p/json2object/
+ */
+typedef SpeakerData =
+{
+  /**
+   * Semantic version of the speaker data.
+   */
+  public var version:String;
+
+  /**
+   * A human-readable name for the speaker.
+   */
+  public var name:String;
+
+  /**
+   * The path to the asset to use for the speaker's sprite.
+   */
+  public var assetPath:String;
+
+  /**
+   * Whether the sprite should be flipped horizontally.
+   */
+  @:optional
+  @:default(false)
+  public var flipX:Bool;
+
+  /**
+   * Whether the sprite should be flipped vertically.
+   */
+  @:optional
+  @:default(false)
+  public var flipY:Bool;
+
+  /**
+   * Whether to disable anti-aliasing for the dialogue box sprite.
+   */
+  @:optional
+  @:default(false)
+  public var isPixel:Bool;
+
+  /**
+   * The offsets to apply to the sprite's position.
+   */
+  @:optional
+  @:default([0, 0])
+  public var offsets:Array<Float>;
+
+  /**
+   * The scale to apply to the sprite.
+   */
+  @:optional
+  @:default(1.0)
+  public var scale:Float;
+
+  /**
+   * The available animations for the speaker.
+   */
+  @:optional
+  @:default([])
+  public var animations:Array<AnimationData>;
+}
diff --git a/source/funkin/data/dialogue/SpeakerRegistry.hx b/source/funkin/data/dialogue/SpeakerRegistry.hx
new file mode 100644
index 000000000..6bd301dd7
--- /dev/null
+++ b/source/funkin/data/dialogue/SpeakerRegistry.hx
@@ -0,0 +1,81 @@
+package funkin.data.dialogue;
+
+import funkin.play.cutscene.dialogue.Speaker;
+import funkin.data.dialogue.SpeakerData;
+import funkin.play.cutscene.dialogue.ScriptedSpeaker;
+
+class SpeakerRegistry extends BaseRegistry<Speaker, SpeakerData>
+{
+  /**
+   * The current version string for the speaker data format.
+   * Handle breaking changes by incrementing this value
+   * and adding migration to the `migrateSpeakerData()` function.
+   */
+  public static final SPEAKER_DATA_VERSION:thx.semver.Version = "1.0.0";
+
+  public static final SPEAKER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
+
+  public static final instance:SpeakerRegistry = new SpeakerRegistry();
+
+  public function new()
+  {
+    super('SPEAKER', 'dialogue/speakers', SPEAKER_DATA_VERSION_RULE);
+  }
+
+  /**
+   * Read, parse, and validate the JSON data and produce the corresponding data object.
+   */
+  public function parseEntryData(id:String):Null<SpeakerData>
+  {
+    // JsonParser does not take type parameters,
+    // otherwise this function would be in BaseRegistry.
+    var parser = new json2object.JsonParser<SpeakerData>();
+    parser.ignoreUnknownVariables = false;
+
+    switch (loadEntryFile(id))
+    {
+      case {fileName: fileName, contents: contents}:
+        parser.fromJson(contents, fileName);
+      default:
+        return null;
+    }
+
+    if (parser.errors.length > 0)
+    {
+      printErrors(parser.errors, id);
+      return null;
+    }
+    return parser.value;
+  }
+
+  /**
+   * Parse and validate the JSON data and produce the corresponding data object.
+   *
+   * NOTE: Must be implemented on the implementation class.
+   * @param contents The JSON as a string.
+   * @param fileName An optional file name for error reporting.
+   */
+  public function parseEntryDataRaw(contents:String, ?fileName:String):Null<SpeakerData>
+  {
+    var parser = new json2object.JsonParser<SpeakerData>();
+    parser.ignoreUnknownVariables = false;
+    parser.fromJson(contents, fileName);
+
+    if (parser.errors.length > 0)
+    {
+      printErrors(parser.errors, fileName);
+      return null;
+    }
+    return parser.value;
+  }
+
+  function createScriptedEntry(clsName:String):Speaker
+  {
+    return ScriptedSpeaker.init(clsName, "unknown");
+  }
+
+  function getScriptedClassNames():Array<String>
+  {
+    return ScriptedSpeaker.listScriptClasses();
+  }
+}
diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx
index b772349bc..0e3f380f6 100644
--- a/source/funkin/data/song/SongRegistry.hx
+++ b/source/funkin/data/song/SongRegistry.hx
@@ -58,7 +58,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
     // SCRIPTED ENTRIES
     //
     var scriptedEntryClassNames:Array<String> = getScriptedClassNames();
-    log('Registering ${scriptedEntryClassNames.length} scripted entries...');
+    log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
 
     for (entryCls in scriptedEntryClassNames)
     {
@@ -84,7 +84,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
     var unscriptedEntryIds:Array<String> = entryIdList.filter(function(entryId:String):Bool {
       return !entries.exists(entryId);
     });
-    log('Fetching data for ${unscriptedEntryIds.length} unscripted entries...');
+    log('Parsing ${unscriptedEntryIds.length} unscripted entries...');
     for (entryId in unscriptedEntryIds)
     {
       try
diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx
index 5488cbbbe..f1e82aee9 100644
--- a/source/funkin/modding/PolymodHandler.hx
+++ b/source/funkin/modding/PolymodHandler.hx
@@ -2,7 +2,6 @@ package funkin.modding;
 
 import funkin.util.macro.ClassMacro;
 import funkin.modding.module.ModuleHandler;
-import funkin.play.character.CharacterData.CharacterDataParser;
 import funkin.data.song.SongData;
 import funkin.data.stage.StageData;
 import polymod.Polymod;
@@ -13,10 +12,11 @@ import funkin.data.stage.StageRegistry;
 import funkin.util.FileUtil;
 import funkin.data.level.LevelRegistry;
 import funkin.data.notestyle.NoteStyleRegistry;
-import funkin.play.cutscene.dialogue.ConversationDataParser;
-import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
+import funkin.data.dialogue.ConversationRegistry;
+import funkin.data.dialogue.DialogueBoxRegistry;
+import funkin.data.dialogue.SpeakerRegistry;
+import funkin.play.character.CharacterData.CharacterDataParser;
 import funkin.save.Save;
-import funkin.play.cutscene.dialogue.SpeakerDataParser;
 import funkin.data.song.SongRegistry;
 
 class PolymodHandler
@@ -208,8 +208,8 @@ class PolymodHandler
   {
     return {
       assetLibraryPaths: [
-        "default" => "preload", "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,
     }
diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx
index 16cc8b299..69e3ca48e 100644
--- a/source/funkin/play/character/CharacterData.hx
+++ b/source/funkin/play/character/CharacterData.hx
@@ -43,7 +43,7 @@ class CharacterDataParser
   {
     // Clear any stages that are cached if there were any.
     clearCharacterCache();
-    trace('Loading character cache...');
+    trace('[CHARACTER] Parsing all entries...');
 
     //
     // UNSCRIPTED CHARACTERS
diff --git a/source/funkin/play/cutscene/dialogue/Conversation.hx b/source/funkin/play/cutscene/dialogue/Conversation.hx
index 817c8caf3..dc3fd8c8a 100644
--- a/source/funkin/play/cutscene/dialogue/Conversation.hx
+++ b/source/funkin/play/cutscene/dialogue/Conversation.hx
@@ -1,8 +1,10 @@
 package funkin.play.cutscene.dialogue;
 
+import funkin.data.IRegistryEntry;
 import flixel.FlxSprite;
 import flixel.group.FlxSpriteGroup;
 import flixel.util.FlxColor;
+import funkin.graphics.FunkinSprite;
 import flixel.tweens.FlxTween;
 import flixel.tweens.FlxEase;
 import flixel.sound.FlxSound;
@@ -13,27 +15,30 @@ import funkin.modding.IScriptedClass.IEventHandler;
 import funkin.play.cutscene.dialogue.DialogueBox;
 import funkin.modding.IScriptedClass.IDialogueScriptedClass;
 import funkin.modding.events.ScriptEventDispatcher;
-import funkin.data.dialogue.ConversationData.DialogueEntryData;
 import flixel.addons.display.FlxPieDial;
+import funkin.data.dialogue.ConversationData;
+import funkin.data.dialogue.ConversationData.DialogueEntryData;
+import funkin.data.dialogue.ConversationRegistry;
+import funkin.data.dialogue.SpeakerData;
+import funkin.data.dialogue.SpeakerRegistry;
+import funkin.data.dialogue.DialogueBoxData;
+import funkin.data.dialogue.DialogueBoxRegistry;
 
 /**
  * A high-level handler for dialogue.
  *
  * This shit is great for modders but it's pretty elaborate for how much it'll actually be used, lolol. -Eric
  */
-class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
+class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry<ConversationData>
 {
   static final CONVERSATION_SKIP_TIMER:Float = 1.5;
 
   var skipHeldTimer:Float = 0.0;
 
   /**
-   * DATA
+   * The ID of the conversation.
    */
-  /**
-   * The ID of the associated dialogue.
-   */
-  public final conversationId:String;
+  public final id:String;
 
   /**
    * The current state of the conversation.
@@ -41,9 +46,9 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
   var state:ConversationState = ConversationState.Start;
 
   /**
-   * The data for the associated dialogue.
+   * Conversation data as parsed from the JSON file.
    */
-  var conversationData:ConversationData;
+  public final _data:ConversationData;
 
   /**
    * The current entry in the dialogue.
@@ -54,7 +59,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
 
   function get_currentDialogueEntryCount():Int
   {
-    return conversationData.dialogue.length;
+    return _data.dialogue.length;
   }
 
   /**
@@ -73,10 +78,10 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
 
   function get_currentDialogueEntryData():DialogueEntryData
   {
-    if (conversationData == null || conversationData.dialogue == null) return null;
-    if (currentDialogueEntry < 0 || currentDialogueEntry >= conversationData.dialogue.length) return null;
+    if (_data == null || _data.dialogue == null) return null;
+    if (currentDialogueEntry < 0 || currentDialogueEntry >= _data.dialogue.length) return null;
 
-    return conversationData.dialogue[currentDialogueEntry];
+    return _data.dialogue[currentDialogueEntry];
   }
 
   var currentDialogueLineString(get, never):String;
@@ -94,7 +99,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
   /**
    * GRAPHICS
    */
-  var backdrop:FlxSprite;
+  var backdrop:FunkinSprite;
 
   var currentSpeaker:Speaker;
 
@@ -102,14 +107,17 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
 
   var skipTimer:FlxPieDial;
 
-  public function new(conversationId:String)
+  public function new(id:String)
   {
     super();
 
-    this.conversationId = conversationId;
-    this.conversationData = ConversationDataParser.parseConversationData(this.conversationId);
+    this.id = id;
+    this._data = _fetchData(id);
 
-    if (conversationData == null) throw 'Could not load conversation data for conversation ID "$conversationId"';
+    if (_data == null)
+    {
+      throw 'Could not parse conversation data for id: $id';
+    }
   }
 
   public function onCreate(event:ScriptEvent):Void
@@ -125,14 +133,14 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
 
   function setupMusic():Void
   {
-    if (conversationData.music == null) return;
+    if (_data.music == null) return;
 
-    music = new FlxSound().loadEmbedded(Paths.music(conversationData.music.asset), true, true);
+    music = new FlxSound().loadEmbedded(Paths.music(_data.music.asset), true, true);
     music.volume = 0;
 
-    if (conversationData.music.fadeTime > 0.0)
+    if (_data.music.fadeTime > 0.0)
     {
-      FlxTween.tween(music, {volume: 1.0}, conversationData.music.fadeTime, {ease: FlxEase.linear});
+      FlxTween.tween(music, {volume: 1.0}, _data.music.fadeTime, {ease: FlxEase.linear});
     }
     else
     {
@@ -145,19 +153,20 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
 
   function setupBackdrop():Void
   {
-    backdrop = new FlxSprite(0, 0);
+    backdrop = new FunkinSprite(0, 0);
 
-    if (conversationData.backdrop == null) return;
+    if (_data.backdrop == null) return;
 
     // Play intro
-    switch (conversationData?.backdrop.type)
+    switch (_data.backdrop)
     {
-      case SOLID:
-        backdrop.makeGraphic(Std.int(FlxG.width), Std.int(FlxG.height), FlxColor.fromString(conversationData.backdrop.data.color));
-        if (conversationData.backdrop.data.fadeTime > 0.0)
+      case SOLID(backdropData):
+        var targetColor:FlxColor = FlxColor.fromString(backdropData.color);
+        backdrop.makeSolidColor(Std.int(FlxG.width), Std.int(FlxG.height), targetColor);
+        if (backdropData.fadeTime > 0.0)
         {
           backdrop.alpha = 0.0;
-          FlxTween.tween(backdrop, {alpha: 1.0}, conversationData.backdrop.data.fadeTime, {ease: FlxEase.linear});
+          FlxTween.tween(backdrop, {alpha: 1.0}, backdropData.fadeTime, {ease: FlxEase.linear});
         }
         else
         {
@@ -190,9 +199,9 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
     var nextSpeakerId:String = currentDialogueEntryData.speaker;
 
     // Skip the next steps if the current speaker is already displayed.
-    if (currentSpeaker != null && nextSpeakerId == currentSpeaker.speakerId) return;
+    if (currentSpeaker != null && nextSpeakerId == currentSpeaker.id) return;
 
-    var nextSpeaker:Speaker = SpeakerDataParser.fetchSpeaker(nextSpeakerId);
+    var nextSpeaker:Speaker = SpeakerRegistry.instance.fetchEntry(nextSpeakerId);
 
     if (currentSpeaker != null)
     {
@@ -241,7 +250,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
     var nextDialogueBoxId:String = currentDialogueEntryData?.box;
 
     // Skip the next steps if the current speaker is already displayed.
-    if (currentDialogueBox != null && nextDialogueBoxId == currentDialogueBox.dialogueBoxId) return;
+    if (currentDialogueBox != null && nextDialogueBoxId == currentDialogueBox.id) return;
 
     if (currentDialogueBox != null)
     {
@@ -250,7 +259,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
       currentDialogueBox = null;
     }
 
-    var nextDialogueBox:DialogueBox = DialogueBoxDataParser.fetchDialogueBox(nextDialogueBoxId);
+    var nextDialogueBox:DialogueBox = DialogueBoxRegistry.instance.fetchEntry(nextDialogueBoxId);
 
     if (nextDialogueBox == null)
     {
@@ -378,20 +387,18 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
 
   public function startOutro():Void
   {
-    switch (conversationData?.outro?.type)
+    switch (_data?.outro)
     {
-      case FADE:
-        var fadeTime:Float = conversationData?.outro.data.fadeTime ?? 1.0;
-
-        outroTween = FlxTween.tween(this, {alpha: 0.0}, fadeTime,
+      case FADE(outroData):
+        outroTween = FlxTween.tween(this, {alpha: 0.0}, outroData.fadeTime,
           {
             type: ONESHOT, // holy shit like the game no way
             startDelay: 0,
             onComplete: (_) -> endOutro(),
           });
 
-        FlxTween.tween(this.music, {volume: 0.0}, fadeTime);
-      case NONE:
+        FlxTween.tween(this.music, {volume: 0.0}, outroData.fadeTime);
+      case NONE(_):
         // Immediately clean up.
         endOutro();
       default:
@@ -400,7 +407,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
     }
   }
 
-  public var completeCallback:Void->Void;
+  public var completeCallback:() -> Void;
 
   public function endOutro():Void
   {
@@ -596,7 +603,12 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
 
   public override function toString():String
   {
-    return 'Conversation($conversationId)';
+    return 'Conversation($id)';
+  }
+
+  static function _fetchData(id:String):Null<ConversationData>
+  {
+    return ConversationRegistry.instance.parseEntryDataWithMigration(id, ConversationRegistry.instance.fetchEntryVersion(id));
   }
 }
 
diff --git a/source/funkin/play/cutscene/dialogue/DialogueBox.hx b/source/funkin/play/cutscene/dialogue/DialogueBox.hx
index 4df34badc..6f8a0086a 100644
--- a/source/funkin/play/cutscene/dialogue/DialogueBox.hx
+++ b/source/funkin/play/cutscene/dialogue/DialogueBox.hx
@@ -1,6 +1,7 @@
 package funkin.play.cutscene.dialogue;
 
 import flixel.FlxSprite;
+import funkin.data.IRegistryEntry;
 import flixel.group.FlxSpriteGroup;
 import flixel.graphics.frames.FlxFramesCollection;
 import flixel.text.FlxText;
@@ -9,18 +10,21 @@ import funkin.util.assets.FlxAnimationUtil;
 import funkin.modding.events.ScriptEvent;
 import funkin.modding.IScriptedClass.IDialogueScriptedClass;
 import flixel.util.FlxColor;
+import funkin.data.dialogue.DialogueBoxData;
+import funkin.data.dialogue.DialogueBoxRegistry;
 
-class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
+class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry<DialogueBoxData>
 {
-  public final dialogueBoxId:String;
+  public final id:String;
+
   public var dialogueBoxName(get, never):String;
 
   function get_dialogueBoxName():String
   {
-    return boxData?.name ?? 'UNKNOWN';
+    return _data.name ?? 'UNKNOWN';
   }
 
-  var boxData:DialogueBoxData;
+  public final _data:DialogueBoxData;
 
   /**
    * Offset the speaker's sprite by this much when playing each animation.
@@ -88,13 +92,16 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
     return this.speed;
   }
 
-  public function new(dialogueBoxId:String)
+  public function new(id:String)
   {
     super();
-    this.dialogueBoxId = dialogueBoxId;
-    this.boxData = DialogueBoxDataParser.parseDialogueBoxData(this.dialogueBoxId);
+    this.id = id;
+    this._data = _fetchData(id);
 
-    if (boxData == null) throw 'Could not load dialogue box data for box ID "$dialogueBoxId"';
+    if (_data == null)
+    {
+      throw 'Could not parse dialogue box data for id: $id';
+    }
   }
 
   public function onCreate(event:ScriptEvent):Void
@@ -115,18 +122,18 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
 
   function loadSpritesheet():Void
   {
-    trace('[DIALOGUE BOX] Loading spritesheet ${boxData.assetPath} for ${dialogueBoxId}');
+    trace('[DIALOGUE BOX] Loading spritesheet ${_data.assetPath} for ${id}');
 
-    var tex:FlxFramesCollection = Paths.getSparrowAtlas(boxData.assetPath);
+    var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath);
     if (tex == null)
     {
-      trace('Could not load Sparrow sprite: ${boxData.assetPath}');
+      trace('Could not load Sparrow sprite: ${_data.assetPath}');
       return;
     }
 
     this.boxSprite.frames = tex;
 
-    if (boxData.isPixel)
+    if (_data.isPixel)
     {
       this.boxSprite.antialiasing = false;
     }
@@ -135,9 +142,10 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
       this.boxSprite.antialiasing = true;
     }
 
-    this.flipX = boxData.flipX;
-    this.globalOffsets = boxData.offsets;
-    this.setScale(boxData.scale);
+    this.flipX = _data.flipX;
+    this.flipY = _data.flipY;
+    this.globalOffsets = _data.offsets;
+    this.setScale(_data.scale);
   }
 
   public function setText(newText:String):Void
@@ -184,11 +192,11 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
 
   function loadAnimations():Void
   {
-    trace('[DIALOGUE BOX] Loading ${boxData.animations.length} animations for ${dialogueBoxId}');
+    trace('[DIALOGUE BOX] Loading ${_data.animations.length} animations for ${id}');
 
-    FlxAnimationUtil.addAtlasAnimations(this.boxSprite, boxData.animations);
+    FlxAnimationUtil.addAtlasAnimations(this.boxSprite, _data.animations);
 
-    for (anim in boxData.animations)
+    for (anim in _data.animations)
     {
       if (anim.offsets == null)
       {
@@ -201,7 +209,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
     }
 
     var animNames:Array<String> = this.boxSprite?.animation?.getNameList() ?? [];
-    trace('[DIALOGUE BOX] Successfully loaded ${animNames.length} animations for ${dialogueBoxId}');
+    trace('[DIALOGUE BOX] Successfully loaded ${animNames.length} animations for ${id}');
 
     boxSprite.animation.callback = this.onAnimationFrame;
     boxSprite.animation.finishCallback = this.onAnimationFinished;
@@ -234,16 +242,16 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
   function loadText():Void
   {
     textDisplay = new FlxTypeText(0, 0, 300, '', 32);
-    textDisplay.fieldWidth = boxData.text.width;
-    textDisplay.setFormat(boxData.text.fontFamily, boxData.text.size, FlxColor.fromString(boxData.text.color), LEFT, SHADOW,
-      FlxColor.fromString(boxData.text.shadowColor ?? '#00000000'), false);
-    textDisplay.borderSize = boxData.text.shadowWidth ?? 2;
+    textDisplay.fieldWidth = _data.text.width;
+    textDisplay.setFormat(_data.text.fontFamily, _data.text.size, FlxColor.fromString(_data.text.color), LEFT, SHADOW,
+      FlxColor.fromString(_data.text.shadowColor ?? '#00000000'), false);
+    textDisplay.borderSize = _data.text.shadowWidth ?? 2;
     textDisplay.sounds = [FlxG.sound.load(Paths.sound('pixelText'), 0.6)];
 
     textDisplay.completeCallback = onTypingComplete;
 
-    textDisplay.x += boxData.text.offsets[0];
-    textDisplay.y += boxData.text.offsets[1];
+    textDisplay.x += _data.text.offsets[0];
+    textDisplay.y += _data.text.offsets[1];
 
     add(textDisplay);
   }
@@ -374,4 +382,14 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
   }
 
   public function onScriptEvent(event:ScriptEvent):Void {}
+
+  public override function toString():String
+  {
+    return 'DialogueBox($id)';
+  }
+
+  static function _fetchData(id:String):Null<DialogueBoxData>
+  {
+    return DialogueBoxRegistry.instance.parseEntryDataWithMigration(id, DialogueBoxRegistry.instance.fetchEntryVersion(id));
+  }
 }
diff --git a/source/funkin/play/cutscene/dialogue/Speaker.hx b/source/funkin/play/cutscene/dialogue/Speaker.hx
index d7ed004f1..d5bffd7b0 100644
--- a/source/funkin/play/cutscene/dialogue/Speaker.hx
+++ b/source/funkin/play/cutscene/dialogue/Speaker.hx
@@ -1,27 +1,30 @@
 package funkin.play.cutscene.dialogue;
 
 import flixel.FlxSprite;
+import funkin.data.IRegistryEntry;
 import funkin.modding.events.ScriptEvent;
 import flixel.graphics.frames.FlxFramesCollection;
 import funkin.util.assets.FlxAnimationUtil;
 import funkin.modding.IScriptedClass.IDialogueScriptedClass;
+import funkin.data.dialogue.SpeakerData;
+import funkin.data.dialogue.SpeakerRegistry;
 
 /**
  * The character sprite which displays during dialogue.
  *
  * Most conversations have two speakers, with one being flipped.
  */
-class Speaker extends FlxSprite implements IDialogueScriptedClass
+class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRegistryEntry<SpeakerData>
 {
   /**
    * The internal ID for this speaker.
    */
-  public final speakerId:String;
+  public final id:String;
 
   /**
    * The full data for a speaker.
    */
-  var speakerData:SpeakerData;
+  public final _data:SpeakerData;
 
   /**
    * A readable name for this speaker.
@@ -30,7 +33,7 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
 
   function get_speakerName():String
   {
-    return speakerData.name;
+    return _data.name;
   }
 
   /**
@@ -75,14 +78,17 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
     return globalOffsets = value;
   }
 
-  public function new(speakerId:String)
+  public function new(id:String)
   {
     super();
 
-    this.speakerId = speakerId;
-    this.speakerData = SpeakerDataParser.parseSpeakerData(this.speakerId);
+    this.id = id;
+    this._data = _fetchData(id);
 
-    if (speakerData == null) throw 'Could not load speaker data for speaker ID "$speakerId"';
+    if (_data == null)
+    {
+      throw 'Could not parse speaker data for id: $id';
+    }
   }
 
   /**
@@ -102,18 +108,18 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
 
   function loadSpritesheet():Void
   {
-    trace('[SPEAKER] Loading spritesheet ${speakerData.assetPath} for ${speakerId}');
+    trace('[SPEAKER] Loading spritesheet ${_data.assetPath} for ${id}');
 
-    var tex:FlxFramesCollection = Paths.getSparrowAtlas(speakerData.assetPath);
+    var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath);
     if (tex == null)
     {
-      trace('Could not load Sparrow sprite: ${speakerData.assetPath}');
+      trace('Could not load Sparrow sprite: ${_data.assetPath}');
       return;
     }
 
     this.frames = tex;
 
-    if (speakerData.isPixel)
+    if (_data.isPixel)
     {
       this.antialiasing = false;
     }
@@ -122,9 +128,10 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
       this.antialiasing = true;
     }
 
-    this.flipX = speakerData.flipX;
-    this.globalOffsets = speakerData.offsets;
-    this.setScale(speakerData.scale);
+    this.flipX = _data.flipX;
+    this.flipY = _data.flipY;
+    this.globalOffsets = _data.offsets;
+    this.setScale(_data.scale);
   }
 
   /**
@@ -141,11 +148,11 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
 
   function loadAnimations():Void
   {
-    trace('[SPEAKER] Loading ${speakerData.animations.length} animations for ${speakerId}');
+    trace('[SPEAKER] Loading ${_data.animations.length} animations for ${id}');
 
-    FlxAnimationUtil.addAtlasAnimations(this, speakerData.animations);
+    FlxAnimationUtil.addAtlasAnimations(this, _data.animations);
 
-    for (anim in speakerData.animations)
+    for (anim in _data.animations)
     {
       if (anim.offsets == null)
       {
@@ -158,7 +165,7 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
     }
 
     var animNames:Array<String> = this.animation.getNameList();
-    trace('[SPEAKER] Successfully loaded ${animNames.length} animations for ${speakerId}');
+    trace('[SPEAKER] Successfully loaded ${animNames.length} animations for ${id}');
   }
 
   /**
@@ -271,4 +278,14 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
   }
 
   public function onScriptEvent(event:ScriptEvent):Void {}
+
+  public override function toString():String
+  {
+    return 'Speaker($id)';
+  }
+
+  static function _fetchData(id:String):Null<SpeakerData>
+  {
+    return SpeakerRegistry.instance.parseEntryDataWithMigration(id, SpeakerRegistry.instance.fetchEntryVersion(id));
+  }
 }
diff --git a/source/funkin/ui/debug/dialogue/ConversationDebugState.hx b/source/funkin/ui/debug/dialogue/ConversationDebugState.hx
new file mode 100644
index 000000000..33a6f365a
--- /dev/null
+++ b/source/funkin/ui/debug/dialogue/ConversationDebugState.hx
@@ -0,0 +1,76 @@
+package funkin.ui.debug.dialogue;
+
+import flixel.FlxState;
+import funkin.modding.events.ScriptEventDispatcher;
+import funkin.modding.events.ScriptEvent;
+import flixel.util.FlxColor;
+import funkin.ui.MusicBeatState;
+import funkin.data.dialogue.ConversationData;
+import funkin.data.dialogue.ConversationRegistry;
+import funkin.data.dialogue.DialogueBoxData;
+import funkin.data.dialogue.DialogueBoxRegistry;
+import funkin.data.dialogue.SpeakerData;
+import funkin.data.dialogue.SpeakerRegistry;
+import funkin.play.cutscene.dialogue.Conversation;
+import funkin.play.cutscene.dialogue.DialogueBox;
+import funkin.play.cutscene.dialogue.Speaker;
+
+/**
+ * A state with displays a conversation with no background.
+ * Used for testing.
+ * @param conversationId The conversation to display.
+ */
+class ConversationDebugState extends MusicBeatState
+{
+  final conversationId:String = 'senpai';
+
+  var conversation:Conversation;
+
+  public function new()
+  {
+    super();
+
+    // TODO: Fix this BS
+    Paths.setCurrentLevel('week6');
+  }
+
+  public override function create():Void
+  {
+    conversation = ConversationRegistry.instance.fetchEntry(conversationId);
+    conversation.completeCallback = onConversationComplete;
+    add(conversation);
+
+    ScriptEventDispatcher.callEvent(conversation, new ScriptEvent(CREATE, false));
+  }
+
+  function onConversationComplete():Void
+  {
+    remove(conversation);
+    conversation = null;
+  }
+
+  public override function dispatchEvent(event:ScriptEvent):Void
+  {
+    // Dispatch event to conversation script.
+    ScriptEventDispatcher.callEvent(conversation, event);
+  }
+
+  public override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    if (conversation != null)
+    {
+      if (controls.CUTSCENE_ADVANCE) conversation.advanceConversation();
+
+      if (controls.CUTSCENE_SKIP)
+      {
+        conversation.trySkipConversation(elapsed);
+      }
+      else
+      {
+        conversation.trySkipConversation(-1);
+      }
+    }
+  }
+}
diff --git a/source/funkin/util/assets/DataAssets.hx b/source/funkin/util/assets/DataAssets.hx
index ee75dd207..b38c993fe 100644
--- a/source/funkin/util/assets/DataAssets.hx
+++ b/source/funkin/util/assets/DataAssets.hx
@@ -9,7 +9,8 @@ class DataAssets
 
   public static function listDataFilesInPath(path:String, ?suffix:String = '.json'):Array<String>
   {
-    var textAssets = openfl.utils.Assets.list();
+    var textAssets = openfl.utils.Assets.list(TEXT);
+
     var queryPath = buildDataPath(path);
 
     var results:Array<String> = [];

From 2d4ca436ecc58c930954cdff32a9c005f7584582 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Wed, 7 Feb 2024 18:46:09 -0500
Subject: [PATCH 05/15] Update Polymod to drastically improve efficiency of
 openfl.utils.Assets.list()

---
 Project.xml | 6 ++++--
 assets      | 2 +-
 hmm.json    | 2 +-
 3 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/Project.xml b/Project.xml
index 9755a5e37..40f309e1f 100644
--- a/Project.xml
+++ b/Project.xml
@@ -52,6 +52,7 @@
 		<library name="week7" preload="false" />
 		<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" />
@@ -82,14 +83,15 @@
 		If we can exclude the `mods` folder from the manifest, we can re-enable this line.
 		<assets path='example_mods' rename='mods' embed='false' exclude="*.md" />
 	-->
-	<assets path="art/readme.txt" rename="do NOT readme.txt" />
-	<assets path="CHANGELOG.md" rename="changelog.txt" />
+	<assets path="art/readme.txt" rename="do NOT readme.txt" library="art"/>
+	<assets path="CHANGELOG.md" rename="changelog.txt" library="art"/>
 	<!-- NOTE FOR FUTURE SELF SINCE FONTS ARE ALWAYS FUCKY
 		TO FIX ONE OF THEM, I CONVERTED IT TO OTF. DUNNO IF YOU NEED TO
 		THEN UHHH I USED THE NAME OF THE FONT WITH SETFORMAT() ON THE TEXT!!!
 		NOT USING A DIRECT THING TO THE ASSET!!!
 	-->
 	<assets path="assets/fonts" embed="true" />
+
 	<!-- _______________________________ Libraries ______________________________ -->
 	<haxelib name="lime" /> <!-- Game engine backend -->
 	<haxelib name="openfl" /> <!-- Game engine backend -->
diff --git a/assets b/assets
index 251d4640b..e364e0ea6 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 251d4640bd77ee0f0b6122a13f123274c43dd3f5
+Subproject commit e364e0ea65fe943675afb7171ba748d9bda28269
diff --git a/hmm.json b/hmm.json
index fa9a67057..4a0895034 100644
--- a/hmm.json
+++ b/hmm.json
@@ -149,7 +149,7 @@
       "name": "polymod",
       "type": "git",
       "dir": null,
-      "ref": "cb11a95d0159271eb3587428cf4b9602e46dc469",
+      "ref": "6cec79e4f322fbb262170594ed67ab72b4714810",
       "url": "https://github.com/larsiusprime/polymod"
     },
     {

From 4491465bf9c6b047f6970b6d32c45861036fae3b Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Thu, 8 Feb 2024 19:22:28 -0500
Subject: [PATCH 06/15] Implement difficulty rank into Metadata toolbox.

---
 assets                                        |  2 +-
 source/funkin/data/song/SongData.hx           |  4 ++--
 .../ui/debug/charting/ChartEditorState.hx     | 23 +++++++++++++++++++
 .../toolboxes/ChartEditorMetadataToolbox.hx   |  5 ++++
 4 files changed, 31 insertions(+), 3 deletions(-)

diff --git a/assets b/assets
index 251d4640b..02da1c72a 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 251d4640bd77ee0f0b6122a13f123274c43dd3f5
+Subproject commit 02da1c72a22ddf8cc167f014888a94ebf468d5b1
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index 52b9c19d6..5959041a7 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -418,10 +418,10 @@ class SongPlayData implements ICloneable<SongPlayData>
 
   /**
    * The difficulty ratings for this song as displayed in Freeplay.
-   * Key is a difficulty ID or `default`.
+   * Key is a difficulty ID.
    */
   @:optional
-  @:default(['default' => 1])
+  @:default(['normal' => 0])
   public var ratings:Map<String, Int>;
 
   /**
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index d0326be30..14d3c606f 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1294,6 +1294,29 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     return currentSongChartData.events = value;
   }
 
+  /**
+   * Convenience property to get the rating for this difficulty in the Freeplay menu.
+   */
+  var currentSongChartDifficultyRating(get, set):Int;
+
+  function get_currentSongChartDifficultyRating():Int
+  {
+    var result:Null<Int> = currentSongMetadata.playData.ratings.get(selectedDifficulty);
+    if (result == null)
+    {
+      // Initialize to the default value if not set.
+      currentSongMetadata.playData.ratings.set(selectedDifficulty, 0);
+      return 0;
+    }
+    return result;
+  }
+
+  function set_currentSongChartDifficultyRating(value:Int):Int
+  {
+    currentSongMetadata.playData.ratings.set(selectedDifficulty, value);
+    return value;
+  }
+
   var currentSongNoteStyle(get, set):String;
 
   function get_currentSongNoteStyle():String
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
index 5d8c25bae..f85307c64 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
@@ -151,6 +151,10 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
       labelScrollSpeed.text = 'Scroll Speed: ${chartEditorState.currentSongChartScrollSpeed}x';
     };
 
+    inputDifficultyRating.onChange = function(event:UIEvent) {
+      chartEditorState.currentSongChartDifficultyRating = event.target.value;
+    };
+
     buttonCharacterOpponent.onClick = function(_) {
       chartEditorState.openCharacterDropdown(CharacterType.DAD, false);
     };
@@ -175,6 +179,7 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
     inputStage.value = chartEditorState.currentSongMetadata.playData.stage;
     inputNoteStyle.value = chartEditorState.currentSongMetadata.playData.noteStyle;
     inputBPM.value = chartEditorState.currentSongMetadata.timeChanges[0].bpm;
+    inputDifficultyRating.value = chartEditorState.currentSongChartDifficultyRating;
     inputScrollSpeed.value = chartEditorState.currentSongChartScrollSpeed;
     labelScrollSpeed.text = 'Scroll Speed: ${chartEditorState.currentSongChartScrollSpeed}x';
     frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}';

From 3ede16be16def08cf108bcfc8c0aa36ce191b199 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Sat, 10 Feb 2024 00:50:07 -0500
Subject: [PATCH 07/15] Fix issue where Freeplay could be silent sometimes

---
 source/funkin/ui/freeplay/FreeplayState.hx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index b23ca6e54..3c6b52c6f 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -164,9 +164,9 @@ class FreeplayState extends MusicBeatSubState
     isDebug = true;
     #end
 
-    if (FlxG.sound.music != null)
+    if (FlxG.sound.music == null || (FlxG.sound.music != null && !FlxG.sound.music.playing))
     {
-      if (!FlxG.sound.music.playing) FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
+      FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
     }
 
     // Add a null entry that represents the RANDOM option

From bb0b1764698407902f8fce32c8967dcd335b8094 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Sat, 10 Feb 2024 00:51:02 -0500
Subject: [PATCH 08/15] Fix bug with building specifically for freeplay.

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

diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 23bc255f1..4e6f12eed 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -240,7 +240,7 @@ class InitState extends FlxState
     #elseif LEVEL // -DLEVEL=week1 -DDIFFICULTY=hard
     startLevel(defineLevel(), defineDifficulty());
     #elseif FREEPLAY // -DFREEPLAY
-    FlxG.switchState(new FreeplayState());
+    FlxG.switchState(new funkin.ui.freeplay.FreeplayState());
     #elseif ANIMATE // -DANIMATE
     FlxG.switchState(new funkin.ui.debug.anim.FlxAnimateTest());
     #elseif WAVEFORM // -DWAVEFORM

From aaeea320e5ef188a71eaa69ba7b901be1d9c94e1 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Sat, 10 Feb 2024 00:51:53 -0500
Subject: [PATCH 09/15] Fix darnell's animations being flipped

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

diff --git a/assets b/assets
index 8378b838b..ec284b57d 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 8378b838bae80739eacd9a0316f9b49f888185f6
+Subproject commit ec284b57dec8903ad79c5d90bac063cbbc327705

From f3630dfff97d74400774d86dcb63fdcd079e8f0b Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Sat, 10 Feb 2024 02:17:07 -0500
Subject: [PATCH 10/15] assets submod

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

diff --git a/assets b/assets
index ec284b57d..590db1448 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit ec284b57dec8903ad79c5d90bac063cbbc327705
+Subproject commit 590db1448fb132a29160540b4656b2474f22f727

From f12847dc5965f9d57f490dfc7884f0a508086f52 Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Sat, 10 Feb 2024 02:23:58 -0500
Subject: [PATCH 11/15] assets submod

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

diff --git a/assets b/assets
index e364e0ea6..ba25cf692 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit e364e0ea65fe943675afb7171ba748d9bda28269
+Subproject commit ba25cf692f7a09174860d50f011fef5643f68b0c

From 31ef89e9e0285c36dd7074270c08385d43304cdb Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Sat, 10 Feb 2024 02:31:32 -0500
Subject: [PATCH 12/15] assets submod

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

diff --git a/assets b/assets
index 02da1c72a..594853037 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 02da1c72a22ddf8cc167f014888a94ebf468d5b1
+Subproject commit 594853037cbea06caa5c141b0d9ed3736818e592

From 86488a0964891aa2a411807e316c2b5777eda95c Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Mon, 12 Feb 2024 18:09:36 -0500
Subject: [PATCH 13/15] Ensure the CrashHandler ALWAYS initializes first so we
 have stack traces.

---
 source/Main.hx | 17 ++++++++++-------
 1 file changed, 10 insertions(+), 7 deletions(-)

diff --git a/source/Main.hx b/source/Main.hx
index 754d0732f..a40fda29d 100644
--- a/source/Main.hx
+++ b/source/Main.hx
@@ -33,8 +33,10 @@ class Main extends Sprite
 
   public static function main():Void
   {
-    haxe.Log.trace = funkin.util.logging.AnsiTrace.trace;
-    funkin.util.logging.AnsiTrace.traceBF();
+    // We need to make the crash handler LITERALLY FIRST so nothing EVER gets past it.
+    CrashHandler.initialize();
+    CrashHandler.queryStatus();
+
     Lib.current.addChild(new Main());
   }
 
@@ -42,7 +44,12 @@ class Main extends Sprite
   {
     super();
 
-    // TODO: Replace this with loadEnabledMods().
+    // Initialize custom logging.
+    haxe.Log.trace = funkin.util.logging.AnsiTrace.trace;
+    funkin.util.logging.AnsiTrace.traceBF();
+
+    // Load mods to override assets.
+    // TODO: Replace with loadEnabledMods() once the user can configure the mod list.
     funkin.modding.PolymodHandler.loadAllMods();
 
     if (stage != null)
@@ -82,10 +89,6 @@ class Main extends Sprite
      * -Eric
      */
 
-    CrashHandler.initialize();
-
-    CrashHandler.queryStatus();
-
     initHaxeUI();
 
     fpsCounter = new FPS(10, 3, 0xFFFFFF);

From 4346214dc8f099265a9ccbef53ee79b4e5254665 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Mon, 12 Feb 2024 18:18:50 -0500
Subject: [PATCH 14/15] Update Polymod to resolve a crash thrown when no mods
 are installed.

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

diff --git a/hmm.json b/hmm.json
index 4a0895034..43cbbb08a 100644
--- a/hmm.json
+++ b/hmm.json
@@ -149,7 +149,7 @@
       "name": "polymod",
       "type": "git",
       "dir": null,
-      "ref": "6cec79e4f322fbb262170594ed67ab72b4714810",
+      "ref": "0b53e478bc375ec51b760b650201ac7a965d2ef4",
       "url": "https://github.com/larsiusprime/polymod"
     },
     {

From 92f52b2c3f62c3402d95562de45849ded60b02df Mon Sep 17 00:00:00 2001
From: Cameron Taylor <cameron.taylor.ninja@gmail.com>
Date: Mon, 12 Feb 2024 23:15:18 -0500
Subject: [PATCH 15/15] flixel debugging on test builds

---
 .github/workflows/build-shit.yml |  8 ++++----
 Project.xml                      | 12 +++---------
 source/funkin/InitState.hx       | 13 ++++++-------
 3 files changed, 13 insertions(+), 20 deletions(-)

diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml
index 76126d106..8ea3b16f3 100644
--- a/.github/workflows/build-shit.yml
+++ b/.github/workflows/build-shit.yml
@@ -31,7 +31,7 @@ jobs:
           sudo apt-get install -y libx11-dev xorg-dev libgl-dev libxi-dev libxext-dev libasound2-dev libxinerama-dev libxrandr-dev libgl1-mesa-dev
       - name: build game
         run: |
-          haxelib run lime build html5 -release --times
+          haxelib run lime build html5 -release --times -DGITHUB_BUILD
           ls
       - uses: ./.github/actions/upload-itch
         with:
@@ -68,7 +68,7 @@ jobs:
           key: ${{ runner.os }}-build-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
       - name: build game
         run: |
-          haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER
+          haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER -DGITHUB_BUILD
           dir
         env:
           HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache"
@@ -110,7 +110,7 @@ jobs:
           key: ${{ runner.os }}-build-mac-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
       - name: Build game
         run: |
-          haxelib run lime build macos -release --times
+          haxelib run lime build macos -release --times -DGITHUB_BUILD
           ls
         env:
           HXCPP_COMPILE_CACHE: "${{ runner.temp }}/hxcpp_cache"
@@ -119,7 +119,7 @@ jobs:
           butler-key: ${{ secrets.BUTLER_API_KEY}}
           build-dir: export/release/macos/bin
           target: macos
-          
+
 #  test-unit-win:
 #    needs: create-nightly-win
 #    runs-on: windows-latest
diff --git a/Project.xml b/Project.xml
index 40f309e1f..560baeadf 100644
--- a/Project.xml
+++ b/Project.xml
@@ -91,7 +91,8 @@
 		NOT USING A DIRECT THING TO THE ASSET!!!
 	-->
 	<assets path="assets/fonts" embed="true" />
-
+	<!-- If compiled via github actions, enable force debug -->
+	<set name="FORCE_DEBUG_VERSION" if="GITHUB_BUILD"/>
 	<!-- _______________________________ Libraries ______________________________ -->
 	<haxelib name="lime" /> <!-- Game engine backend -->
 	<haxelib name="openfl" /> <!-- Game engine backend -->
@@ -118,7 +119,7 @@
 	<!--Disable the Flixel core focus lost screen-->
 	<haxedef name="FLX_NO_FOCUS_LOST_SCREEN" />
 	<!--Disable the Flixel core debugger. Automatically gets set whenever you compile in release mode!-->
-	<haxedef name="FLX_NO_DEBUG" unless="debug" />
+	<haxedef name="FLX_NO_DEBUG" unless="debug || FORCE_DEBUG_VERSION" />
 	<!--Enable this for Nape release builds for a serious peformance improvement-->
 	<haxedef name="NAPE_RELEASE_BUILD" unless="debug" />
 
@@ -211,13 +212,6 @@
 		<haxedef name="openfl-enable-handle-error" />
 	</section>
 
-	<section>
-		<!-- TODO: Add a flag to Github Actions to turn this on or something. -->
-
-		<!-- Forces the version string to include the Git hash even on release builds (which are used for performance reasons). -->
-		<haxedef name="FORCE_DEBUG_VERSION" />
-	</section>
-
 	<!-- Run a script before and after building. -->
 	<postbuild haxe="source/Prebuild.hx"/> -->
 	<postbuild haxe="source/Postbuild.hx"/> -->
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 625a33ad7..21946819f 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -144,13 +144,12 @@ class InitState extends FlxState
 
     // Make errors and warnings less annoying.
     // TODO: Disable this so we know to fix warnings.
-    if (false)
-    {
-      LogStyle.ERROR.openConsole = false;
-      LogStyle.ERROR.errorSound = null;
-      LogStyle.WARNING.openConsole = false;
-      LogStyle.WARNING.errorSound = null;
-    }
+    #if FORCE_DEBUG_VERSION
+    LogStyle.ERROR.openConsole = false;
+    LogStyle.ERROR.errorSound = null;
+    LogStyle.WARNING.openConsole = false;
+    LogStyle.WARNING.errorSound = null;
+    #end
     #end
 
     //