From 5ebde826ba35f2c184073e2ead916a54f2ed4503 Mon Sep 17 00:00:00 2001
From: ChomeNS <95471003+ChomeNS@users.noreply.github.com>
Date: Wed, 3 May 2023 21:02:11 +0700
Subject: [PATCH] make it more sexy

---
 README.md                                     |  11 +-
 build.gradle                                  |   2 +
 gradle.properties                             |   9 +-
 .../chipmunk/chipmunkmod/ChipmunkMod.java     |   0
 .../chipmunk/chipmunkmod/Configuration.java   |   6 +
 .../chipmunkmod/command/CommandManager.java   |   7 +
 .../chipmunkmod/command/ComponentMessage.java |  26 ++
 .../arguments/LocationArgumentType.java       |  88 +++++
 .../arguments/TimestampArgumentType.java      |  36 ++
 .../chipmunkmod/commands/CloopCommand.java    | 130 ++++++
 .../chipmunkmod/commands/CoreCommand.java     |   0
 .../commands/CustomChatCommand.java           |  55 +++
 .../commands/FullBrightCommand.java           |  37 ++
 .../chipmunkmod/commands/MusicCommand.java    | 280 +++++++++++++
 .../commands/RainbowNameCommand.java          |  65 +++
 .../chipmunkmod/commands/SayCommand.java      |  33 ++
 .../chipmunkmod/commands/SelfCareCommand.java |  65 +++
 .../chipmunkmod/commands/TestCommand.java     |   0
 .../chipmunkmod/commands/UsernameCommand.java |   0
 .../chipmunkmod/commands/ValidateCommand.java |   0
 .../chipmunk/chipmunkmod/data/BlockArea.java  |   0
 .../chipmunkmod/data/CommandLoop.java         |  15 +
 .../data/MutablePlayerListEntry.java          |  21 +
 .../chipmunkmod/listeners/Listener.java       |  10 +
 .../listeners/ListenerManager.java            |  12 +
 .../chipmunkmod/mixin/ChatHudMixin.java       |  37 ++
 .../mixin/ChatInputSuggestorMixin.java        |   0
 .../chipmunkmod/mixin/ChatScreenMixin.java    |   7 +
 .../mixin/ClientConnectionMixin.java          |  26 +-
 .../ClientPlayNetworkHandlerAccessor.java     |   0
 .../mixin/ClientPlayNetworkHandlerMixin.java  |  12 +-
 .../mixin/ClientPlayerEntityMixin.java        |   0
 .../mixin/DecoderHandlerMixin.java            |  46 +++
 .../mixin/LightmapTextureManagerMixin.java    |  16 +
 .../mixin/MinecraftClientAccessor.java        |   0
 .../chipmunkmod/modules/CommandCore.java      |  29 +-
 .../chipmunkmod/modules/CommandLooper.java    |  78 ++++
 .../chipmunkmod/modules/CustomChat.java       |  60 +++
 .../chipmunkmod/modules/FullBright.java       |   8 +
 .../chipmunk/chipmunkmod/modules/Players.java | 146 +++++++
 .../chipmunkmod/modules/RainbowName.java      | 161 ++++++++
 .../chipmunkmod/modules/SelfCare.java         |  35 +-
 .../chipmunkmod/modules/SongPlayer.java       | 224 +++++++++++
 .../chipmunkmod/modules/TabComplete.java      |  62 +++
 .../chipmunk/chipmunkmod/song/Instrument.java |  48 +++
 .../chipmunkmod/song/MidiConverter.java       | 373 ++++++++++++++++++
 .../chipmunkmod/song/NBSConverter.java        | 201 ++++++++++
 .../land/chipmunk/chipmunkmod/song/Note.java  |  28 ++
 .../land/chipmunk/chipmunkmod/song/Song.java  | 131 ++++++
 .../chipmunkmod/song/SongLoaderException.java |  24 ++
 .../chipmunkmod/song/SongLoaderThread.java    |  68 ++++
 .../chipmunkmod/util/ColorUtilities.java      |  10 +
 .../chipmunkmod/util/ComponentUtilities.java  | 105 +++++
 .../chipmunkmod/util/DownloadUtilities.java   |  63 +++
 .../chipmunkmod/util/Hexadecimal.java         |  18 +-
 src/main/resources/chipmunkmod.mixins.json    |  11 +-
 src/main/resources/default_config.json        |  14 +-
 src/main/resources/fabric.mod.json            |  11 +-
 58 files changed, 2900 insertions(+), 60 deletions(-)
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/ChipmunkMod.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/Configuration.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/command/CommandManager.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/command/ComponentMessage.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/command/arguments/LocationArgumentType.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/command/arguments/TimestampArgumentType.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/commands/CloopCommand.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/commands/CoreCommand.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/commands/CustomChatCommand.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/commands/FullBrightCommand.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/commands/MusicCommand.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/commands/RainbowNameCommand.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/commands/SayCommand.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/commands/SelfCareCommand.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/commands/TestCommand.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/commands/UsernameCommand.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/commands/ValidateCommand.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/data/BlockArea.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/data/CommandLoop.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/data/MutablePlayerListEntry.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/listeners/Listener.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/listeners/ListenerManager.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/mixin/ChatHudMixin.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/mixin/ChatInputSuggestorMixin.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/mixin/ChatScreenMixin.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/mixin/ClientConnectionMixin.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/mixin/ClientPlayNetworkHandlerAccessor.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/mixin/ClientPlayNetworkHandlerMixin.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/mixin/ClientPlayerEntityMixin.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/mixin/DecoderHandlerMixin.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/mixin/LightmapTextureManagerMixin.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/mixin/MinecraftClientAccessor.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/modules/CommandCore.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/modules/CommandLooper.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/modules/CustomChat.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/modules/FullBright.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/modules/Players.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/modules/RainbowName.java
 mode change 100755 => 100644 src/main/java/land/chipmunk/chipmunkmod/modules/SelfCare.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/modules/SongPlayer.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/modules/TabComplete.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/song/Instrument.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/song/MidiConverter.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/song/NBSConverter.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/song/Note.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/song/Song.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/song/SongLoaderException.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/song/SongLoaderThread.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/util/ColorUtilities.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/util/ComponentUtilities.java
 create mode 100644 src/main/java/land/chipmunk/chipmunkmod/util/DownloadUtilities.java

diff --git a/README.md b/README.md
index fd96346..fbadeb1 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,4 @@
-# Fabric Example Mod
+# ChipmunkMod
+My fork of [Chipmunk Sex Mod](https://code.chipmunk.land/ChipmunkMC/chipmunkmod)
 
-## Setup
-
-For setup instructions please see the [fabric wiki page](https://fabricmc.net/wiki/tutorial:setup) that relates to the IDE that you are using.
-
-## License
-
-This template is available under the CC0 license. Feel free to learn from it and incorporate it in your own projects.
+ignore messy code pls,. .,,.,...,.,.,
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 68c9007..94302aa 100644
--- a/build.gradle
+++ b/build.gradle
@@ -29,6 +29,8 @@ dependencies {
 	// Fabric API. This is technically optional, but you probably want it anyway.
 	modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
 
+	modImplementation include("net.kyori:adventure-platform-fabric:5.8.0") // for Minecraft 1.19.4
+
 	// Uncomment the following line to enable the deprecated Fabric API modules. 
 	// These are included in the Fabric API production distribution and allow you to update your mod to the latest modules at a later more convenient time.
 
diff --git a/gradle.properties b/gradle.properties
index 855dfe8..e521a8a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,9 +4,9 @@ org.gradle.parallel=true
 
 # Fabric Properties
 	# check these on https://fabricmc.net/develop
-	minecraft_version=1.19.3
-	yarn_mappings=1.19.3+build.1
-	loader_version=0.14.11
+		minecraft_version=1.19.4
+		yarn_mappings=1.19.4+build.2
+		loader_version=0.14.19
 
 # Mod Properties
 	mod_version = 1.0.0
@@ -14,4 +14,5 @@ org.gradle.parallel=true
 	archives_base_name = chipmunkmod
 
 # Dependencies
-	fabric_version=0.68.1+1.19.3
+	fabric_version=0.76.0+1.19.4
+
diff --git a/src/main/java/land/chipmunk/chipmunkmod/ChipmunkMod.java b/src/main/java/land/chipmunk/chipmunkmod/ChipmunkMod.java
old mode 100755
new mode 100644
diff --git a/src/main/java/land/chipmunk/chipmunkmod/Configuration.java b/src/main/java/land/chipmunk/chipmunkmod/Configuration.java
old mode 100755
new mode 100644
index da1cc53..c70758e
--- a/src/main/java/land/chipmunk/chipmunkmod/Configuration.java
+++ b/src/main/java/land/chipmunk/chipmunkmod/Configuration.java
@@ -1,5 +1,6 @@
 package land.chipmunk.chipmunkmod;
 
+import com.google.gson.JsonObject;
 import land.chipmunk.chipmunkmod.data.BlockArea;
 import net.minecraft.util.math.BlockPos;
 import lombok.AllArgsConstructor;
@@ -8,6 +9,7 @@ public class Configuration {
   public CommandManager commands = new CommandManager();
   public CommandCore core = new CommandCore();
   public Bots bots = new Bots();
+  public CustomChat customChat = new CustomChat();
 
   public static class CommandManager {
     public String prefix = ".";
@@ -30,4 +32,8 @@ public class Configuration {
     public String prefix;
     public String key;
   }
+
+  public static class CustomChat {
+    public JsonObject format;
+  }
 }
diff --git a/src/main/java/land/chipmunk/chipmunkmod/command/CommandManager.java b/src/main/java/land/chipmunk/chipmunkmod/command/CommandManager.java
old mode 100755
new mode 100644
index 70587c9..96b0e12
--- a/src/main/java/land/chipmunk/chipmunkmod/command/CommandManager.java
+++ b/src/main/java/land/chipmunk/chipmunkmod/command/CommandManager.java
@@ -71,5 +71,12 @@ public class CommandManager {
     CoreCommand.register(dispatcher);
     UsernameCommand.register(dispatcher);
     ValidateCommand.register(dispatcher);
+    CloopCommand.register(dispatcher);
+    CustomChatCommand.register(dispatcher);
+    SayCommand.register(dispatcher);
+    SelfCareCommand.register(dispatcher);
+    FullBrightCommand.register(dispatcher);
+    MusicCommand.register(dispatcher);
+    RainbowNameCommand.register(dispatcher);
   }
 }
\ No newline at end of file
diff --git a/src/main/java/land/chipmunk/chipmunkmod/command/ComponentMessage.java b/src/main/java/land/chipmunk/chipmunkmod/command/ComponentMessage.java
new file mode 100644
index 0000000..8410da8
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/command/ComponentMessage.java
@@ -0,0 +1,26 @@
+package land.chipmunk.chipmunkmod.command;
+
+import land.chipmunk.chipmunkmod.util.ComponentUtilities;
+import net.kyori.adventure.text.Component;
+import com.mojang.brigadier.Message;
+import lombok.Getter;
+
+public class ComponentMessage implements Message {
+    @Getter private final Component component;
+
+    private ComponentMessage (Component component) {
+        this.component = component;
+    }
+
+    public static ComponentMessage wrap (Component component) {
+        return new ComponentMessage(component);
+    }
+
+    public String getString () {
+        return ComponentUtilities.stringify(component);
+    }
+
+    public String toString () {
+        return component.toString();
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/command/arguments/LocationArgumentType.java b/src/main/java/land/chipmunk/chipmunkmod/command/arguments/LocationArgumentType.java
new file mode 100644
index 0000000..28faffe
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/command/arguments/LocationArgumentType.java
@@ -0,0 +1,88 @@
+package land.chipmunk.chipmunkmod.command.arguments;
+
+import com.mojang.brigadier.arguments.ArgumentType;
+import com.mojang.brigadier.StringReader;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
+import net.kyori.adventure.text.Component;
+import land.chipmunk.chipmunkmod.command.ComponentMessage;
+import java.util.Collection;
+import java.util.Arrays;
+import java.net.URL;
+import java.net.MalformedURLException;
+import java.nio.file.Path;
+
+public class LocationArgumentType implements ArgumentType<Object> {
+    private static final Collection<String> EXAMPLES = Arrays.<String>asList("songs/amogus.mid", "images/cat.jpg", "videos/badapple.mp4");
+
+    private static final SimpleCommandExceptionType OOB_FILEPATH = new SimpleCommandExceptionType(ComponentMessage.wrap(Component.translatable("The specified file path is outside of the allowed directory")));
+
+    private boolean allowsUrls = false;
+    private boolean allowsPaths = false;
+    private Path root;
+
+    private LocationArgumentType (boolean allowsUrls, boolean allowsPaths, Path root) {
+        this.allowsUrls = allowsUrls;
+        this.allowsPaths = allowsPaths;
+        this.root = root.toAbsolutePath().normalize();
+    }
+
+    public static LocationArgumentType location (Path rootPath) { return new LocationArgumentType(true, true, rootPath); }
+    public static LocationArgumentType url () { return new LocationArgumentType(true, false, null); }
+    public static LocationArgumentType filepath (Path rootPath) { return new LocationArgumentType(false, true, rootPath); }
+
+    @Override
+    public Object parse (StringReader reader) throws CommandSyntaxException {
+        final String remaining = reader.getString().substring(reader.getCursor());
+        if (allowsUrls && isUrlStart(remaining)) return parseUrl(reader);
+        if (allowsPaths) return parsePath(reader);
+        return null;
+    }
+
+    public boolean isUrlStart (String string) { return string.startsWith("http://") || string.startsWith("https://") || string.startsWith("ftp://"); }
+
+    public URL parseUrl (StringReader reader) throws CommandSyntaxException {
+        final StringBuilder sb = new StringBuilder();
+        while (reader.canRead() && reader.peek() != ' ') {
+            sb.append(reader.read());
+        }
+
+        try {
+            return new URL(sb.toString());
+        } catch (MalformedURLException exception) {
+            throw new SimpleCommandExceptionType(ComponentMessage.wrap(Component.text(exception.getMessage()))).create();
+        }
+    }
+
+    public Path parsePath (StringReader reader) throws CommandSyntaxException {
+        final String pathString = reader.readString();
+        final Path path = Path.of(root.toString(), pathString).toAbsolutePath().normalize();
+        if (!path.startsWith(root)) throw OOB_FILEPATH.create();
+        return path;
+    }
+
+    private static Object getLocation (CommandContext<?> context, String name) {
+        return context.getArgument(name, Object.class);
+    }
+
+    public static URL getUrl (CommandContext<?> context, String name) {
+        final Object location = getLocation(context, name);
+        if (location instanceof URL) return (URL) location;
+        try {
+            if (location instanceof Path) return new URL("file", "", -1, location.toString());
+        } catch (MalformedURLException ignored) {
+            return null; // The real question is whether this will actually ever get called
+        }
+        return null;
+    }
+
+    public static Path getPath (CommandContext<?> context, String name) {
+        final Object location = getLocation(context, name);
+        if (location instanceof Path) return (Path) location;
+        return null;
+    }
+
+    @Override
+    public Collection<String> getExamples () { return EXAMPLES; }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/command/arguments/TimestampArgumentType.java b/src/main/java/land/chipmunk/chipmunkmod/command/arguments/TimestampArgumentType.java
new file mode 100644
index 0000000..c956f71
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/command/arguments/TimestampArgumentType.java
@@ -0,0 +1,36 @@
+package land.chipmunk.chipmunkmod.command.arguments;
+
+import com.mojang.brigadier.arguments.ArgumentType;
+import com.mojang.brigadier.StringReader;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import java.util.Collection;
+import java.util.Arrays;
+
+public class TimestampArgumentType implements ArgumentType<Long> {
+    private static final Collection<String> EXAMPLES = Arrays.<String>asList("0:01", "1:23", "6:09");
+
+    private TimestampArgumentType () {
+    }
+
+    public static TimestampArgumentType timestamp () { return new TimestampArgumentType(); }
+
+    @Override
+    public Long parse (StringReader reader) throws CommandSyntaxException {
+        long seconds = 0L;
+        long minutes = 0L;
+
+        seconds = reader.readLong();
+        if (reader.canRead() && reader.peek() == ':') {
+            reader.skip();
+            minutes = seconds;
+            seconds = reader.readLong();
+        }
+
+        return (seconds * 1000) + (minutes * 1000 * 60);
+    }
+
+    // ? Should I create a getter method? Seems like reinventing the wheel since LongArgumentType#getLong is already a thing.
+
+    @Override
+    public Collection<String> getExamples () { return EXAMPLES; }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/commands/CloopCommand.java b/src/main/java/land/chipmunk/chipmunkmod/commands/CloopCommand.java
new file mode 100644
index 0000000..f111546
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/commands/CloopCommand.java
@@ -0,0 +1,130 @@
+package land.chipmunk.chipmunkmod.commands;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.context.CommandContext;
+import land.chipmunk.chipmunkmod.data.CommandLoop;
+import land.chipmunk.chipmunkmod.modules.CommandLooper;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import java.util.List;
+
+import static com.mojang.brigadier.arguments.IntegerArgumentType.getInteger;
+import static com.mojang.brigadier.arguments.IntegerArgumentType.integer;
+import static com.mojang.brigadier.arguments.StringArgumentType.getString;
+import static com.mojang.brigadier.arguments.StringArgumentType.greedyString;
+import static land.chipmunk.chipmunkmod.command.CommandManager.argument;
+import static land.chipmunk.chipmunkmod.command.CommandManager.literal;
+
+public class CloopCommand {
+    public static void register (CommandDispatcher<FabricClientCommandSource> dispatcher) {
+        dispatcher.register(
+                literal("cloop")
+                        .then(
+                                literal("add")
+                                        .then(
+                                                argument("interval", integer())
+                                                        .then(
+                                                                argument("command", greedyString())
+                                                                        .executes(c -> add(c))
+                                                        )
+                                        )
+                        )
+                        .then(
+                                literal("remove")
+                                        .then(
+                                                argument("index", integer())
+                                                        .executes(c -> remove(c))
+                                        )
+                        )
+                        .then(
+                                literal("clear")
+                                        .executes(c -> clear(c))
+                        )
+                        .then(
+                                literal("list")
+                                        .executes(c -> list(c))
+                        )
+        );
+    }
+
+    public static int add (CommandContext<FabricClientCommandSource> context) {
+        final FabricClientCommandSource source = context.getSource();
+
+        final String command = getString(context, "command");
+        final int interval = getInteger(context, "interval");
+
+        CommandLooper.INSTANCE.addLoop(command, interval);
+
+        source.sendFeedback(
+                Text.translatable(
+                        "Added command %s with interval %s to the command loops",
+                        Text.literal(command).formatted(Formatting.AQUA),
+                        Text.literal(String.valueOf(interval)).formatted(Formatting.GOLD)
+                )
+        );
+
+        return Command.SINGLE_SUCCESS;
+    }
+
+    public static int remove (CommandContext<FabricClientCommandSource> context) {
+        final FabricClientCommandSource source = context.getSource();
+
+        final int index = getInteger(context, "index");
+
+        CommandLooper.INSTANCE.removeLoop(index);
+
+        source.sendFeedback(
+                Text.translatable(
+                        "Removed command loop %s",
+                        Text.literal(String.valueOf(index)).formatted(Formatting.GOLD)
+                )
+        );
+
+        return Command.SINGLE_SUCCESS;
+    }
+
+    public static int list (CommandContext<FabricClientCommandSource> context) {
+        final FabricClientCommandSource source = context.getSource();
+
+        final List<CommandLoop> cloops = CommandLooper.INSTANCE.loops();
+
+        MutableText text = Text.empty();
+        text.append(Text.literal("Command Loops:").formatted(Formatting.GREEN));
+        text.append("\n"); // should i use Text.literal("\n")?
+
+        for (int i = 0; i < cloops.size(); i++) {
+            final CommandLoop cloop = cloops.get(i);
+            text.append(
+                    Text.translatable(
+                            "%s > %s - %s", // should i use \u203a?
+                            Text.literal(String.valueOf(i)).formatted(Formatting.GREEN),
+                            Text.literal(cloop.command()).formatted(Formatting.AQUA),
+                            Text.literal(String.valueOf(cloop.interval())).formatted(Formatting.GOLD)
+                    ).formatted(Formatting.GRAY)
+            );
+
+            // PLS TELL ME HOW TO REMOVE THE LAST ONE OF MutableText SPLSPLpLSLPsSSPLPSPLSPSL
+            // btw is this the best way to do it lol
+            final int lastIndex = cloops.size() - 1;
+            if (i < lastIndex) { text.append("\n"); }
+        }
+
+        source.sendFeedback(text);
+
+        return Command.SINGLE_SUCCESS;
+    }
+
+    public static int clear (CommandContext<FabricClientCommandSource> context) {
+        final FabricClientCommandSource source = context.getSource();
+
+        CommandLooper.INSTANCE.clearLoops();
+
+        source.sendFeedback(Text.literal("Cleared all command loops"));
+
+        return Command.SINGLE_SUCCESS;
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/commands/CoreCommand.java b/src/main/java/land/chipmunk/chipmunkmod/commands/CoreCommand.java
old mode 100755
new mode 100644
diff --git a/src/main/java/land/chipmunk/chipmunkmod/commands/CustomChatCommand.java b/src/main/java/land/chipmunk/chipmunkmod/commands/CustomChatCommand.java
new file mode 100644
index 0000000..4a419b0
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/commands/CustomChatCommand.java
@@ -0,0 +1,55 @@
+package land.chipmunk.chipmunkmod.commands;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.context.CommandContext;
+import land.chipmunk.chipmunkmod.modules.CustomChat;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.minecraft.text.Text;
+
+import static com.mojang.brigadier.arguments.BoolArgumentType.bool;
+import static com.mojang.brigadier.arguments.BoolArgumentType.getBool;
+import static com.mojang.brigadier.arguments.StringArgumentType.getString;
+import static com.mojang.brigadier.arguments.StringArgumentType.greedyString;
+import static land.chipmunk.chipmunkmod.command.CommandManager.argument;
+import static land.chipmunk.chipmunkmod.command.CommandManager.literal;
+
+public class CustomChatCommand {
+    public static void register (CommandDispatcher<FabricClientCommandSource> dispatcher) {
+        dispatcher.register(
+                literal("customchat")
+                        .then(
+                                literal("enabled")
+                                    .then(
+                                        argument("boolean", bool())
+                                            .executes(CustomChatCommand::enabled)
+                                    )
+                        )
+                        .then(
+                                literal("format")
+                                        .then(
+                                                argument("format", greedyString())
+                                                        .executes(CustomChatCommand::setFormat)
+                                        )
+                        )
+        );
+    }
+
+    public static int enabled (CommandContext<FabricClientCommandSource> context) {
+        final FabricClientCommandSource source = context.getSource();
+        final boolean bool = getBool(context, "boolean");
+        CustomChat.INSTANCE.enabled(bool);
+        source.sendFeedback(Text.literal("Custom chat is now " + (bool ? "on" : "off")));
+
+        return Command.SINGLE_SUCCESS;
+    }
+
+    public static int setFormat (CommandContext<FabricClientCommandSource> context) {
+        final FabricClientCommandSource source = context.getSource();
+        final String format = getString(context, "format");
+        CustomChat.INSTANCE.format(format);
+        source.sendFeedback(Text.literal("Set the custom chat format to: " + format));
+
+        return Command.SINGLE_SUCCESS;
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/commands/FullBrightCommand.java b/src/main/java/land/chipmunk/chipmunkmod/commands/FullBrightCommand.java
new file mode 100644
index 0000000..a67f44f
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/commands/FullBrightCommand.java
@@ -0,0 +1,37 @@
+package land.chipmunk.chipmunkmod.commands;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.context.CommandContext;
+import land.chipmunk.chipmunkmod.modules.FullBright;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.minecraft.text.Text;
+
+import static com.mojang.brigadier.arguments.BoolArgumentType.bool;
+import static com.mojang.brigadier.arguments.BoolArgumentType.getBool;
+import static land.chipmunk.chipmunkmod.command.CommandManager.argument;
+import static land.chipmunk.chipmunkmod.command.CommandManager.literal;
+
+public class FullBrightCommand {
+    public static void register (CommandDispatcher<FabricClientCommandSource> dispatcher) {
+        dispatcher.register(
+                literal("fullbright")
+                        .then(
+                                argument("boolean", bool())
+                                        .executes(FullBrightCommand::set)
+                        )
+        );
+    }
+
+    public static int set (CommandContext<FabricClientCommandSource> context) {
+        final FabricClientCommandSource source = context.getSource();
+
+        final boolean bool = getBool(context, "boolean");
+
+        FullBright.enabled(bool);
+
+        source.sendFeedback(Text.literal("Fullbright is now " + (bool ? "enabled" : "disabled")));
+
+        return Command.SINGLE_SUCCESS;
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/commands/MusicCommand.java b/src/main/java/land/chipmunk/chipmunkmod/commands/MusicCommand.java
new file mode 100644
index 0000000..68097b4
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/commands/MusicCommand.java
@@ -0,0 +1,280 @@
+package land.chipmunk.chipmunkmod.commands;
+
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
+import land.chipmunk.chipmunkmod.command.CommandManager;
+import land.chipmunk.chipmunkmod.command.ComponentMessage;
+import land.chipmunk.chipmunkmod.modules.SongPlayer;
+import land.chipmunk.chipmunkmod.song.Song;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.JoinConfiguration;
+import net.kyori.adventure.text.event.ClickEvent;
+import net.kyori.adventure.text.event.HoverEvent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.mojang.brigadier.arguments.BoolArgumentType.bool;
+import static com.mojang.brigadier.arguments.BoolArgumentType.getBool;
+import static com.mojang.brigadier.arguments.IntegerArgumentType.getInteger;
+import static com.mojang.brigadier.arguments.IntegerArgumentType.integer;
+import static com.mojang.brigadier.arguments.LongArgumentType.getLong;
+import static land.chipmunk.chipmunkmod.command.CommandManager.argument;
+import static land.chipmunk.chipmunkmod.command.CommandManager.literal;
+import static land.chipmunk.chipmunkmod.command.arguments.LocationArgumentType.*;
+import static land.chipmunk.chipmunkmod.command.arguments.TimestampArgumentType.timestamp;
+
+public class MusicCommand {
+    private static SimpleCommandExceptionType NO_SONG_IS_CURRENTLY_PLAYING = new SimpleCommandExceptionType(ComponentMessage.wrap(Component.translatable("No song is currently playing")));
+    private static SimpleCommandExceptionType OOB_TIMESTAMP = new SimpleCommandExceptionType(ComponentMessage.wrap(Component.translatable("Invalid timestamp for the current song")));
+    private static SimpleCommandExceptionType DIRECTORY_DOES_NOT_EXIST = new SimpleCommandExceptionType(ComponentMessage.wrap(Component.translatable("The specified directory does not exist")));
+
+    public static void register (CommandDispatcher<FabricClientCommandSource> dispatcher) {
+        final MusicCommand instance = new MusicCommand();
+
+        Path root = Path.of(SongPlayer.SONG_DIR.getPath());
+
+        dispatcher.register(
+                literal("music")
+                        .then(
+                                literal("play")
+                                        .then(
+                                                argument("location", location(root))
+                                                        .executes(instance::play)
+                                        )
+                        )
+
+                        .then(literal("stop").executes(instance::stop))
+                        .then(literal("skip").executes(instance::skip))
+                        .then(literal("pause").executes(instance::pause))
+
+                        .then(
+                                literal("list")
+                                        .executes(c -> instance.list(c, root))
+                                        .then(
+                                                argument("location", filepath(root))
+                                                        .executes(c -> instance.list(c, getPath(c, "location")))
+                                        )
+                        )
+
+                        .then(
+                                literal("loop")
+                                        .executes(instance::toggleLoop)
+                                        .then(
+                                                argument("count", integer())
+                                                        .executes(instance::loop)
+                                        )
+                        )
+
+                        .then(
+                                literal("goto")
+                                        .then(
+                                                argument("timestamp", timestamp())
+                                                        .executes(instance::gotoCommand)
+                                        )
+                        )
+                        .then(
+                                literal("useCore")
+                                        .then(
+                                                argument("boolean", bool())
+                                                        .executes(instance::useCore)
+                                        )
+                        )
+        );
+    }
+
+    public int play (CommandContext<FabricClientCommandSource> context) {
+        final SongPlayer songPlayer = SongPlayer.INSTANCE;
+
+        final Path path = getPath(context, "location");
+
+        if (path != null) songPlayer.loadSong(path);
+        else songPlayer.loadSong(getUrl(context, "location"));
+
+        return 1;
+    }
+
+    public int stop (CommandContext<FabricClientCommandSource> context) throws CommandSyntaxException {
+        final FabricClientCommandSource source = context.getSource();
+        final SongPlayer songPlayer = SongPlayer.INSTANCE;
+
+        if (songPlayer.currentSong() == null) throw NO_SONG_IS_CURRENTLY_PLAYING.create();
+
+        songPlayer.stopPlaying();
+        songPlayer.songQueue().clear();
+        source.sendFeedback(Text.literal("Stopped music playback").formatted(Formatting.GREEN));
+
+        return 1;
+    }
+
+    public int skip (CommandContext<FabricClientCommandSource> context) throws CommandSyntaxException {
+        final FabricClientCommandSource source = context.getSource();
+        final SongPlayer songPlayer = SongPlayer.INSTANCE;
+
+        if (songPlayer.currentSong() == null) throw NO_SONG_IS_CURRENTLY_PLAYING.create();
+
+        songPlayer.stopPlaying();
+        source.sendFeedback(Text.literal("Skipped the current song").formatted(Formatting.GREEN));
+
+        return 1;
+    }
+
+    public int pause (CommandContext<FabricClientCommandSource> context) throws CommandSyntaxException {
+        final FabricClientCommandSource source = context.getSource();
+        final SongPlayer songPlayer = SongPlayer.INSTANCE;
+        final Song currentSong = songPlayer.currentSong();
+
+        if (currentSong == null) throw NO_SONG_IS_CURRENTLY_PLAYING.create();
+
+        if (!currentSong.paused) {
+            currentSong.pause();
+            source.sendFeedback(Text.literal("Paused the current song"));
+        } else {
+            currentSong.play();
+            source.sendFeedback(Text.literal("Unpaused the current song"));
+        }
+
+        return 1;
+    }
+
+    public int list (CommandContext<FabricClientCommandSource> context, Path path) throws CommandSyntaxException {
+        final FabricClientCommandSource source = context.getSource();
+        final String prefix = CommandManager.prefix;
+
+        final File directory = path.toFile();
+        final String[] filenames = directory.list();
+        if (filenames == null) throw DIRECTORY_DOES_NOT_EXIST.create();
+
+        final Path root = Path.of(SongPlayer.SONG_DIR.getAbsoluteFile().getPath()).toAbsolutePath();
+        String relativePath;
+        if (path.getNameCount() - root.getNameCount() > 0) relativePath = path.subpath(root.getNameCount(), path.getNameCount()).toString();
+        else relativePath = "";
+
+        final List<Component> directories = new ArrayList<>();
+        final List<Component> files = new ArrayList<>();
+        int i = 0;
+
+        for (String filename : filenames) {
+            final File file = new File(directory, filename);
+            if (!file.isDirectory()) continue;
+
+            final NamedTextColor color = (i++ & 1) == 0 ? NamedTextColor.DARK_GREEN : NamedTextColor.GREEN;
+
+            final Path relativeFilepath = Path.of(relativePath, filename);
+            final String escapedPath = escapePath(relativeFilepath.toString());
+
+            directories.add(
+                    Component.text(filename + "/", color)
+                            .clickEvent(ClickEvent.suggestCommand(prefix + "music list " + escapedPath))
+                            .hoverEvent(HoverEvent.showText(Component.translatable("Click to list %s", Component.text(filename))))
+            );
+        }
+
+        for (String filename : filenames) {
+            final File file = new File(directory, filename);
+            if (file.isDirectory()) continue;
+
+            final NamedTextColor color = (i++ & 1) == 0 ? NamedTextColor.DARK_GREEN : NamedTextColor.GREEN;
+
+            final Path relativeFilepath = Path.of(relativePath, filename);
+            final String escapedPath = escapePath(relativeFilepath.toString());
+
+            files.add(
+                    Component.text(filename, color)
+                            .clickEvent(ClickEvent.suggestCommand(prefix + "music play " + escapedPath))
+                            .hoverEvent(HoverEvent.showText(Component.translatable("Click to play %s", Component.text(filename))))
+            );
+        }
+
+        final ArrayList<Component> mergedList = new ArrayList<>();
+        for (Component component : directories) mergedList.add(component);
+        for (Component component : files) mergedList.add(component);
+        final Component component = Component.translatable("Songs - %s", Component.join(JoinConfiguration.separator(Component.space()), mergedList)).color(NamedTextColor.GREEN);
+
+        MinecraftClient.getInstance().player.sendMessage(component);
+
+        return 1;
+    }
+
+    // TODO: Move this into some utility class, as it is more related to brigadier strings in general than to the list command in specific
+    private String escapePath (String path) {
+        final StringBuilder sb = new StringBuilder("'");
+
+        for (char character : path.toCharArray()) {
+            if (character == '\'' || character == '\\') sb.append('\\');
+            sb.append(character);
+        }
+
+        sb.append("'");
+        return sb.toString();
+    }
+
+    public int toggleLoop (CommandContext<FabricClientCommandSource> context) throws CommandSyntaxException {
+        final FabricClientCommandSource source = context.getSource();
+        final SongPlayer songPlayer = SongPlayer.INSTANCE;
+        final Song currentSong = songPlayer.currentSong();
+
+        if (currentSong == null) throw NO_SONG_IS_CURRENTLY_PLAYING.create();
+
+        currentSong.looping = !currentSong.looping;
+
+        source.sendFeedback(Text.translatable(currentSong.looping ? "Enabled looping" : "Disabled looping"));
+
+        return 1;
+    }
+
+
+    public int loop (CommandContext<FabricClientCommandSource> context) throws CommandSyntaxException {
+        final FabricClientCommandSource source = context.getSource();
+        final SongPlayer songPlayer = SongPlayer.INSTANCE;
+        final Song currentSong = songPlayer.currentSong();
+        final int count = getInteger(context, "count");
+
+        if (currentSong == null) throw NO_SONG_IS_CURRENTLY_PLAYING.create();
+
+        currentSong.looping = true;
+        currentSong.loopCount = count;
+
+        source.sendFeedback(Text.translatable("Enabled looping for %s times", Text.literal(String.valueOf(count))));
+
+        return 1;
+    }
+
+    public int gotoCommand (CommandContext<FabricClientCommandSource> context) throws CommandSyntaxException {
+        final FabricClientCommandSource source = context.getSource();
+        final SongPlayer songPlayer = SongPlayer.INSTANCE;
+        final Song currentSong = songPlayer.currentSong();
+        final long millis = getLong(context, "timestamp");
+
+        if (currentSong == null) throw NO_SONG_IS_CURRENTLY_PLAYING.create();
+
+        if (millis < 0 || millis > currentSong.length) throw OOB_TIMESTAMP.create();
+
+        currentSong.setTime(millis);
+
+        source.sendFeedback(Text.translatable("Set the current time of the song to %s", songPlayer.formatTime(millis)));
+
+        return 1;
+    }
+
+    public int useCore (CommandContext<FabricClientCommandSource> context) {
+        final FabricClientCommandSource source = context.getSource();
+
+        final boolean enabled = getBool(context, "boolean");
+
+        SongPlayer.INSTANCE.useCore(enabled);
+
+        source.sendFeedback(Text.literal("Playing music using core is now " + (enabled ? "enabled" : "disabled")));
+
+        return 1;
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/commands/RainbowNameCommand.java b/src/main/java/land/chipmunk/chipmunkmod/commands/RainbowNameCommand.java
new file mode 100644
index 0000000..65ff64c
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/commands/RainbowNameCommand.java
@@ -0,0 +1,65 @@
+package land.chipmunk.chipmunkmod.commands;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.context.CommandContext;
+import land.chipmunk.chipmunkmod.modules.RainbowName;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.minecraft.text.Text;
+
+import static com.mojang.brigadier.arguments.BoolArgumentType.bool;
+import static com.mojang.brigadier.arguments.BoolArgumentType.getBool;
+import static com.mojang.brigadier.arguments.StringArgumentType.getString;
+import static com.mojang.brigadier.arguments.StringArgumentType.greedyString;
+import static land.chipmunk.chipmunkmod.command.CommandManager.argument;
+import static land.chipmunk.chipmunkmod.command.CommandManager.literal;
+
+public class RainbowNameCommand {
+    public static void register (CommandDispatcher<FabricClientCommandSource> dispatcher) {
+        dispatcher.register(
+                literal("rainbowname")
+                        .then(
+                                literal("enabled")
+                                        .then(
+                                                argument("boolean", bool())
+                                                        .executes(RainbowNameCommand::enabled)
+                                        )
+                        )
+                        .then(
+                                literal("setName")
+                                        .then(
+                                                argument("name", greedyString())
+                                                        .executes(RainbowNameCommand::setName)
+                                        )
+                        )
+        );
+    }
+
+    public static int enabled (CommandContext<FabricClientCommandSource> context) {
+        final FabricClientCommandSource source = context.getSource();
+
+        final boolean bool = getBool(context, "boolean");
+
+        if (bool) {
+            RainbowName.INSTANCE.enable();
+            source.sendFeedback(Text.literal("Rainbow name is now enabled"));
+        } else {
+            RainbowName.INSTANCE.disable();
+            source.sendFeedback(Text.literal("Rainbow name is now disabled"));
+        }
+
+        return Command.SINGLE_SUCCESS;
+    }
+
+    public static int setName (CommandContext<FabricClientCommandSource> context) {
+        final FabricClientCommandSource source = context.getSource();
+
+        final String name = getString(context, "name");
+
+        RainbowName.INSTANCE.displayName(name);
+
+        source.sendFeedback(Text.literal("Set the display name to: " + name));
+
+        return Command.SINGLE_SUCCESS;
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/commands/SayCommand.java b/src/main/java/land/chipmunk/chipmunkmod/commands/SayCommand.java
new file mode 100644
index 0000000..42db2ae
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/commands/SayCommand.java
@@ -0,0 +1,33 @@
+package land.chipmunk.chipmunkmod.commands;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.context.CommandContext;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+
+import static com.mojang.brigadier.arguments.StringArgumentType.getString;
+import static com.mojang.brigadier.arguments.StringArgumentType.greedyString;
+import static land.chipmunk.chipmunkmod.command.CommandManager.argument;
+import static land.chipmunk.chipmunkmod.command.CommandManager.literal;
+
+public class SayCommand {
+    public static void register (CommandDispatcher<FabricClientCommandSource> dispatcher) {
+        dispatcher.register(
+                literal("say")
+                        .then(
+                                argument("message", greedyString())
+                                        .executes(m -> say(m))
+                        )
+        );
+    }
+
+    public static int say (CommandContext<FabricClientCommandSource> context) {
+        final ClientPlayNetworkHandler networkHandler = MinecraftClient.getInstance().getNetworkHandler();
+
+        networkHandler.sendChatMessage(getString(context, "message"));
+
+        return Command.SINGLE_SUCCESS;
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/commands/SelfCareCommand.java b/src/main/java/land/chipmunk/chipmunkmod/commands/SelfCareCommand.java
new file mode 100644
index 0000000..0ef9be1
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/commands/SelfCareCommand.java
@@ -0,0 +1,65 @@
+package land.chipmunk.chipmunkmod.commands;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.context.CommandContext;
+import land.chipmunk.chipmunkmod.modules.SelfCare;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.minecraft.text.Text;
+
+import static com.mojang.brigadier.arguments.BoolArgumentType.bool;
+import static com.mojang.brigadier.arguments.BoolArgumentType.getBool;
+import static land.chipmunk.chipmunkmod.command.CommandManager.argument;
+import static land.chipmunk.chipmunkmod.command.CommandManager.literal;
+
+public class SelfCareCommand {
+    public static void register (CommandDispatcher<FabricClientCommandSource> dispatcher) {
+        dispatcher.register(
+                literal("selfcare")
+                        .then(
+                                literal("op")
+                                        .then(
+                                                argument("boolean", bool())
+                                                        .executes(m -> setSelfCare(m, "op"))
+                                        )
+                        )
+                        .then(
+                                literal("gamemode")
+                                        .then(
+                                                argument("boolean", bool())
+                                                        .executes(m -> setSelfCare(m, "gamemode"))
+                                        )
+                        )
+                        .then(
+                                literal("cspy")
+                                        .then(
+                                                argument("boolean", bool())
+                                                        .executes(m -> setSelfCare(m, "cspy"))
+                                        )
+                        )
+        );
+    }
+
+    // setSelfCare is probably not a good name for this
+    public static int setSelfCare (CommandContext<FabricClientCommandSource> context, String type) {
+        final FabricClientCommandSource source = context.getSource();
+        final boolean bool = getBool(context, "boolean");
+
+        switch (type) {
+            case "op" -> {
+                SelfCare.INSTANCE.opEnabled(bool);
+                source.sendFeedback(Text.literal("The op self care is now " + (bool ? "enabled" : "disabled")));
+            }
+            case "gamemode" -> {
+                SelfCare.INSTANCE.gamemodeEnabled(bool);
+                source.sendFeedback(Text.literal("The gamemode self care is now " + (bool ? "enabled" : "disabled")));
+            }
+            case "cspy" -> {
+                SelfCare.INSTANCE.cspyEnabled(bool);
+                source.sendFeedback(Text.literal("The CommandSpy self care is now " + (bool ? "enabled" : "disabled")));
+            }
+        }
+
+        return Command.SINGLE_SUCCESS;
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/commands/TestCommand.java b/src/main/java/land/chipmunk/chipmunkmod/commands/TestCommand.java
old mode 100755
new mode 100644
diff --git a/src/main/java/land/chipmunk/chipmunkmod/commands/UsernameCommand.java b/src/main/java/land/chipmunk/chipmunkmod/commands/UsernameCommand.java
old mode 100755
new mode 100644
diff --git a/src/main/java/land/chipmunk/chipmunkmod/commands/ValidateCommand.java b/src/main/java/land/chipmunk/chipmunkmod/commands/ValidateCommand.java
old mode 100755
new mode 100644
diff --git a/src/main/java/land/chipmunk/chipmunkmod/data/BlockArea.java b/src/main/java/land/chipmunk/chipmunkmod/data/BlockArea.java
old mode 100755
new mode 100644
diff --git a/src/main/java/land/chipmunk/chipmunkmod/data/CommandLoop.java b/src/main/java/land/chipmunk/chipmunkmod/data/CommandLoop.java
new file mode 100644
index 0000000..b2fdc22
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/data/CommandLoop.java
@@ -0,0 +1,15 @@
+package land.chipmunk.chipmunkmod.data;
+
+import lombok.Getter;
+
+public class CommandLoop {
+    @Getter
+    private String command;
+    @Getter
+    private long interval;
+
+    public CommandLoop (String command, long interval) {
+        this.command = command;
+        this.interval = interval;
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/data/MutablePlayerListEntry.java b/src/main/java/land/chipmunk/chipmunkmod/data/MutablePlayerListEntry.java
new file mode 100644
index 0000000..4e62570
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/data/MutablePlayerListEntry.java
@@ -0,0 +1,21 @@
+package land.chipmunk.chipmunkmod.data;
+
+import com.mojang.authlib.GameProfile;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket;
+import net.minecraft.text.Text;
+import net.minecraft.world.GameMode;
+
+@Data
+@AllArgsConstructor
+public class MutablePlayerListEntry {
+    private GameProfile profile;
+    private GameMode gamemode;
+    private int latency;
+    private Text displayName;
+
+    public MutablePlayerListEntry (PlayerListS2CPacket.Entry entry) {
+        this(entry.profile(), entry.gameMode(), entry.latency(), entry.displayName());
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/listeners/Listener.java b/src/main/java/land/chipmunk/chipmunkmod/listeners/Listener.java
new file mode 100644
index 0000000..cf46c0d
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/listeners/Listener.java
@@ -0,0 +1,10 @@
+package land.chipmunk.chipmunkmod.listeners;
+
+import net.minecraft.network.packet.Packet;
+import net.minecraft.text.Text;
+
+public class Listener {
+    public void chatMessageReceived (Text message) {}
+
+    public void packetReceived (Packet packet) {}
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/listeners/ListenerManager.java b/src/main/java/land/chipmunk/chipmunkmod/listeners/ListenerManager.java
new file mode 100644
index 0000000..061aca4
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/listeners/ListenerManager.java
@@ -0,0 +1,12 @@
+package land.chipmunk.chipmunkmod.listeners;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ListenerManager {
+    public static List<Listener> listeners = new ArrayList<>();
+
+    public static void addListener (Listener listener) {
+        listeners.add(listener);
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/mixin/ChatHudMixin.java b/src/main/java/land/chipmunk/chipmunkmod/mixin/ChatHudMixin.java
new file mode 100644
index 0000000..690b930
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/mixin/ChatHudMixin.java
@@ -0,0 +1,37 @@
+package land.chipmunk.chipmunkmod.mixin;
+
+import land.chipmunk.chipmunkmod.listeners.Listener;
+import land.chipmunk.chipmunkmod.listeners.ListenerManager;
+import land.chipmunk.chipmunkmod.modules.RainbowName;
+import net.minecraft.client.gui.hud.MessageIndicator;
+import net.minecraft.network.message.MessageSignatureData;
+import net.minecraft.text.Text;
+import net.minecraft.text.TranslatableTextContent;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(net.minecraft.client.gui.hud.ChatHud.class)
+public class ChatHudMixin {
+    @Inject(at = @At("HEAD"), method = "addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;ILnet/minecraft/client/gui/hud/MessageIndicator;Z)V", cancellable = true)
+    public void addMessage(Text message, MessageSignatureData signature, int ticks, MessageIndicator indicator, boolean refresh, CallbackInfo ci) {
+        try {
+            if (RainbowName.INSTANCE.enabled()) {
+                if (message.getString().contains("Your nickname is now ") || message.getString().contains("Nickname changed.")) {
+                    ci.cancel();
+                    return;
+                }
+            }
+
+            if (((TranslatableTextContent) message.getContent()).getKey().equals("advMode.setCommand.success")) {
+                ci.cancel();
+                return;
+            }
+        } catch (ClassCastException ignored) {}
+
+        for (Listener listener : ListenerManager.listeners) {
+            listener.chatMessageReceived(message);
+        }
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/mixin/ChatInputSuggestorMixin.java b/src/main/java/land/chipmunk/chipmunkmod/mixin/ChatInputSuggestorMixin.java
old mode 100755
new mode 100644
diff --git a/src/main/java/land/chipmunk/chipmunkmod/mixin/ChatScreenMixin.java b/src/main/java/land/chipmunk/chipmunkmod/mixin/ChatScreenMixin.java
old mode 100755
new mode 100644
index 969fdb0..9829104
--- a/src/main/java/land/chipmunk/chipmunkmod/mixin/ChatScreenMixin.java
+++ b/src/main/java/land/chipmunk/chipmunkmod/mixin/ChatScreenMixin.java
@@ -1,5 +1,6 @@
 package land.chipmunk.chipmunkmod.mixin;
 
+import land.chipmunk.chipmunkmod.modules.CustomChat;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
@@ -16,6 +17,12 @@ public class ChatScreenMixin {
 
       if (addToHistory) MinecraftClient.getInstance().inGameHud.getChatHud().addToMessageHistory(chatText);
 
+      cir.setReturnValue(true);
+    } else if (!chatText.startsWith("/")) {
+      CustomChat.INSTANCE.chat(chatText);
+
+      if (addToHistory) MinecraftClient.getInstance().inGameHud.getChatHud().addToMessageHistory(chatText);
+
       cir.setReturnValue(true);
     }
   }
diff --git a/src/main/java/land/chipmunk/chipmunkmod/mixin/ClientConnectionMixin.java b/src/main/java/land/chipmunk/chipmunkmod/mixin/ClientConnectionMixin.java
old mode 100755
new mode 100644
index a7025cd..29ed5a3
--- a/src/main/java/land/chipmunk/chipmunkmod/mixin/ClientConnectionMixin.java
+++ b/src/main/java/land/chipmunk/chipmunkmod/mixin/ClientConnectionMixin.java
@@ -1,13 +1,16 @@
 package land.chipmunk.chipmunkmod.mixin;
 
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.DecoderException;
+import land.chipmunk.chipmunkmod.listeners.Listener;
+import land.chipmunk.chipmunkmod.listeners.ListenerManager;
+import net.minecraft.network.listener.PacketListener;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.text.Text;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
-import net.minecraft.client.network.ClientPlayNetworkHandler;
-import net.minecraft.text.Text;
-import land.chipmunk.chipmunkmod.modules.CommandCore;
-import land.chipmunk.chipmunkmod.modules.SelfCare;
 
 @Mixin(net.minecraft.network.ClientConnection.class)
 public class ClientConnectionMixin {
@@ -17,4 +20,19 @@ public class ClientConnectionMixin {
       ci.cancel();
     }
   }
+
+  @Inject(method = "exceptionCaught", at = @At("HEAD"), cancellable = true)
+  private void exceptionCaught (ChannelHandlerContext context, Throwable ex, CallbackInfo ci) {
+    if (ex instanceof DecoderException) {
+      ci.cancel();
+      ex.printStackTrace();
+    }
+  }
+
+  @Inject(method = "handlePacket", at = @At("HEAD"))
+  private static void handlePacket (Packet packet, PacketListener _listener, CallbackInfo ci) {
+    for (Listener listener : ListenerManager.listeners) {
+      listener.packetReceived(packet);
+    }
+  }
 }
diff --git a/src/main/java/land/chipmunk/chipmunkmod/mixin/ClientPlayNetworkHandlerAccessor.java b/src/main/java/land/chipmunk/chipmunkmod/mixin/ClientPlayNetworkHandlerAccessor.java
old mode 100755
new mode 100644
diff --git a/src/main/java/land/chipmunk/chipmunkmod/mixin/ClientPlayNetworkHandlerMixin.java b/src/main/java/land/chipmunk/chipmunkmod/mixin/ClientPlayNetworkHandlerMixin.java
old mode 100755
new mode 100644
index e968e5e..fd09b0b
--- a/src/main/java/land/chipmunk/chipmunkmod/mixin/ClientPlayNetworkHandlerMixin.java
+++ b/src/main/java/land/chipmunk/chipmunkmod/mixin/ClientPlayNetworkHandlerMixin.java
@@ -1,16 +1,24 @@
 package land.chipmunk.chipmunkmod.mixin;
 
+import land.chipmunk.chipmunkmod.modules.*;
+import net.minecraft.network.packet.s2c.play.GameJoinS2CPacket;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
-import net.minecraft.network.packet.s2c.play.GameJoinS2CPacket;
-import land.chipmunk.chipmunkmod.modules.SelfCare;
 
 @Mixin(net.minecraft.client.network.ClientPlayNetworkHandler.class)
 public class ClientPlayNetworkHandlerMixin {
   @Inject(method = "onGameJoin", at = @At("TAIL"))
   private void onGameJoin (GameJoinS2CPacket packet, CallbackInfo ci) {
     SelfCare.INSTANCE.init();
+    CommandLooper.INSTANCE.init();
+    SongPlayer.INSTANCE.coreReady();
+    RainbowName.INSTANCE.init();
+  }
+
+  @Inject(method = "onGameJoin", at = @At("HEAD"))
+  private void onGameJoin2 (GameJoinS2CPacket packet, CallbackInfo ci) {
+    Players.INSTANCE.init();
   }
 }
diff --git a/src/main/java/land/chipmunk/chipmunkmod/mixin/ClientPlayerEntityMixin.java b/src/main/java/land/chipmunk/chipmunkmod/mixin/ClientPlayerEntityMixin.java
old mode 100755
new mode 100644
diff --git a/src/main/java/land/chipmunk/chipmunkmod/mixin/DecoderHandlerMixin.java b/src/main/java/land/chipmunk/chipmunkmod/mixin/DecoderHandlerMixin.java
new file mode 100644
index 0000000..687ae6c
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/mixin/DecoderHandlerMixin.java
@@ -0,0 +1,46 @@
+package land.chipmunk.chipmunkmod.mixin;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.ChannelHandlerContext;
+import net.minecraft.network.*;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.util.profiling.jfr.FlightProfiler;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Mutable;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import java.util.List;
+
+import static land.chipmunk.chipmunkmod.ChipmunkMod.LOGGER;
+
+@Mixin(DecoderHandler.class)
+public class DecoderHandlerMixin {
+    @Final @Mutable @Shadow private final NetworkSide side;
+
+    public DecoderHandlerMixin(NetworkSide side) {
+        this.side = side;
+    }
+
+    @Inject(method = "decode", at = @At("HEAD"), cancellable = true)
+    private void decode (ChannelHandlerContext ctx, ByteBuf buf, List<Object> objects, CallbackInfo ci) {
+        int i = buf.readableBytes();
+        if (i != 0) {
+            PacketByteBuf packetByteBuf = new PacketByteBuf(buf);
+            int j = packetByteBuf.readVarInt();
+            Packet<?> packet = ctx.channel().attr(ClientConnection.PROTOCOL_ATTRIBUTE_KEY).get().getPacketHandler(this.side, j, packetByteBuf);
+            if (packet != null) {
+                int k = ctx.channel().attr(ClientConnection.PROTOCOL_ATTRIBUTE_KEY).get().getId();
+                FlightProfiler.INSTANCE.onPacketReceived(k, j, ctx.channel().remoteAddress(), i);
+                objects.add(packet);
+                if (LOGGER.isDebugEnabled()) {
+                    LOGGER.debug(ClientConnection.PACKET_RECEIVED_MARKER, " IN: [{}:{}] {}", ctx.channel().attr(ClientConnection.PROTOCOL_ATTRIBUTE_KEY).get(), j, packet.getClass().getName());
+                }
+            }
+        }
+        ci.cancel();
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/mixin/LightmapTextureManagerMixin.java b/src/main/java/land/chipmunk/chipmunkmod/mixin/LightmapTextureManagerMixin.java
new file mode 100644
index 0000000..848e865
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/mixin/LightmapTextureManagerMixin.java
@@ -0,0 +1,16 @@
+package land.chipmunk.chipmunkmod.mixin;
+
+import land.chipmunk.chipmunkmod.modules.FullBright;
+import net.minecraft.client.render.LightmapTextureManager;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.ModifyArgs;
+import org.spongepowered.asm.mixin.injection.invoke.arg.Args;
+
+@Mixin(LightmapTextureManager.class)
+public class LightmapTextureManagerMixin {
+    @ModifyArgs(method = "update", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/texture/NativeImage;setColor(III)V"))
+    private void update (Args args) {
+        if (FullBright.enabled()) args.set(2, 0xFFFFFFFF);
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/mixin/MinecraftClientAccessor.java b/src/main/java/land/chipmunk/chipmunkmod/mixin/MinecraftClientAccessor.java
old mode 100755
new mode 100644
diff --git a/src/main/java/land/chipmunk/chipmunkmod/modules/CommandCore.java b/src/main/java/land/chipmunk/chipmunkmod/modules/CommandCore.java
old mode 100755
new mode 100644
index c23a0f5..b06cba2
--- a/src/main/java/land/chipmunk/chipmunkmod/modules/CommandCore.java
+++ b/src/main/java/land/chipmunk/chipmunkmod/modules/CommandCore.java
@@ -1,31 +1,28 @@
 package land.chipmunk.chipmunkmod.modules;
 
-import net.minecraft.client.MinecraftClient;
-import net.minecraft.network.ClientConnection;
-import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.Vec3d;
-import net.minecraft.network.packet.c2s.play.UpdateCommandBlockC2SPacket;
-import net.minecraft.block.entity.CommandBlockBlockEntity;
-import net.minecraft.nbt.NbtCompound;
+import land.chipmunk.chipmunkmod.data.BlockArea;
 import lombok.Getter;
 import lombok.Setter;
-import java.util.List;
-import java.util.ArrayList;
+import net.minecraft.block.entity.CommandBlockBlockEntity;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.network.ClientConnection;
+import net.minecraft.network.packet.c2s.play.UpdateCommandBlockC2SPacket;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Vec3d;
+
 import java.util.Timer;
 import java.util.TimerTask;
 import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
-import land.chipmunk.chipmunkmod.ChipmunkMod;
-import land.chipmunk.chipmunkmod.data.BlockArea;
 
 public class CommandCore {
-  private MinecraftClient client;
+  private final MinecraftClient client;
   @Getter @Setter private boolean ready = false; 
   @Getter @Setter private BlockPos origin;
   @Getter private final BlockArea relativeArea;
   @Getter @Setter private BlockPos currentBlockRelative;
 
-  public static CommandCore INSTANCE = new CommandCore(MinecraftClient.getInstance(), ChipmunkMod.CONFIG.core.relativeArea);
+  public static CommandCore INSTANCE = new CommandCore(MinecraftClient.getInstance(), new BlockArea(new BlockPos(0, 0, 0), new BlockPos(15, 2, 15)));
 
   public CommandCore (MinecraftClient client, BlockArea relativeArea) {
     this.client = client;
@@ -121,14 +118,14 @@ public class CommandCore {
 
     incrementCurrentBlock();
 
-    CompletableFuture<NbtCompound> future = new CompletableFuture<NbtCompound>();
+    CompletableFuture<NbtCompound> future = new CompletableFuture<>();
 
     final Timer timer = new Timer();
 
     final TimerTask queryTask = new TimerTask() {
       public void run () {
         client.getNetworkHandler().getDataQueryHandler().queryBlockNbt(currentBlock,
-          tag -> { future.complete(tag); });
+                future::complete);
 
         timer.cancel(); // ? Is this necesary?
         timer.purge();
diff --git a/src/main/java/land/chipmunk/chipmunkmod/modules/CommandLooper.java b/src/main/java/land/chipmunk/chipmunkmod/modules/CommandLooper.java
new file mode 100644
index 0000000..e494879
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/modules/CommandLooper.java
@@ -0,0 +1,78 @@
+package land.chipmunk.chipmunkmod.modules;
+
+import land.chipmunk.chipmunkmod.data.CommandLoop;
+import lombok.Getter;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class CommandLooper {
+    private final MinecraftClient client;
+
+    // sus
+    private final List<TimerTask> loopTasks = new ArrayList<>();
+    @Getter private final List<CommandLoop> loops = new ArrayList<>();
+
+    private Timer timer = null;
+
+    public static CommandLooper INSTANCE = new CommandLooper(MinecraftClient.getInstance());
+
+    public CommandLooper (MinecraftClient client) {
+        this.client = client;
+    }
+
+    public void init () {
+        TimerTask task = new TimerTask() {
+            @Override
+            public void run () {
+                final ClientPlayNetworkHandler networkHandler = client.getNetworkHandler();
+
+                if (networkHandler == null) { cleanup(); }
+            }
+        };
+
+        if (timer != null) cleanup();
+
+        timer = new Timer();
+        timer.schedule(task, 0, 50);
+    }
+
+    public void cleanup() {
+        if (timer == null) return;
+        clearLoops();
+    }
+
+    public void addLoop(String command, long interval) {
+        TimerTask loopTask = new TimerTask() {
+            public void run() {
+                CommandCore.INSTANCE.run(command);
+            }
+        };
+        loopTasks.add(loopTask);
+        loops.add(new CommandLoop(command, interval)); // mabe,.,..
+        // should i use 50 or 0?
+        timer.scheduleAtFixedRate(loopTask, 0, interval);
+    }
+
+    public void removeLoop(int index) {
+        TimerTask loopTask = loopTasks.remove(index);
+        if (loopTask != null) {
+            loopTask.cancel();
+        }
+
+        loops.remove(index);
+    }
+
+    public void clearLoops() {
+        for (TimerTask loopTask : loopTasks) {
+            loopTask.cancel();
+        }
+        loopTasks.clear();
+
+        loops.clear();
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/modules/CustomChat.java b/src/main/java/land/chipmunk/chipmunkmod/modules/CustomChat.java
new file mode 100644
index 0000000..bb8546d
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/modules/CustomChat.java
@@ -0,0 +1,60 @@
+package land.chipmunk.chipmunkmod.modules;
+
+import land.chipmunk.chipmunkmod.ChipmunkMod;
+import land.chipmunk.chipmunkmod.data.MutablePlayerListEntry;
+import lombok.Getter;
+import lombok.Setter;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.JoinConfiguration;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+
+public class CustomChat {
+    private final MinecraftClient client;
+
+    public static final CustomChat INSTANCE = new CustomChat(MinecraftClient.getInstance());
+
+    @Getter @Setter private boolean enabled = true;
+
+    @Getter @Setter private String format = ChipmunkMod.CONFIG.customChat.format.toString();
+
+    public CustomChat (MinecraftClient client) {
+        this.client = client;
+    }
+
+    public void chat (String message) {
+        if (!enabled) {
+            final ClientPlayNetworkHandler networkHandler = client.getNetworkHandler();
+            networkHandler.sendChatMessage(message);
+            return;
+        }
+
+        final String username = MinecraftClient.getInstance().getSession().getUsername();
+
+        final String sanitizedMessage = message
+                .replace("\\", "\\\\")
+                .replace("\"", "\\\"");
+
+        try {
+            final MutablePlayerListEntry entry = Players.INSTANCE.getEntry(client.getNetworkHandler().getProfile().getId());
+
+            final Component displayNameComponent = entry.displayName().asComponent();
+
+            final String prefix = GsonComponentSerializer.gson().serialize(Component.join(JoinConfiguration.separator(Component.empty()), displayNameComponent.children().get(0)));
+            final String displayName = GsonComponentSerializer.gson().serialize(Component.join(JoinConfiguration.separator(Component.empty()), displayNameComponent.children().get(1)));
+
+            final String sanitizedFormat = format
+                    .replace("{\"text\":\"PREFIX\"}", prefix)
+                    .replace("{\"text\":\"DISPLAYNAME\"}", displayName)
+                    .replace("USERNAME", username)
+                    .replace("MESSAGE", sanitizedMessage);
+
+            CommandCore.INSTANCE.run("minecraft:tellraw @a " + sanitizedFormat);
+        } catch (Exception e) {
+            if (client.player == null) return;
+            client.player.sendMessage(Component.text(e.toString()).color(NamedTextColor.RED));
+        }
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/modules/FullBright.java b/src/main/java/land/chipmunk/chipmunkmod/modules/FullBright.java
new file mode 100644
index 0000000..d9d2858
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/modules/FullBright.java
@@ -0,0 +1,8 @@
+package land.chipmunk.chipmunkmod.modules;
+
+import lombok.Getter;
+import lombok.Setter;
+
+public class FullBright {
+    @Getter @Setter private static boolean enabled = true;
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/modules/Players.java b/src/main/java/land/chipmunk/chipmunkmod/modules/Players.java
new file mode 100644
index 0000000..23642fb
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/modules/Players.java
@@ -0,0 +1,146 @@
+package land.chipmunk.chipmunkmod.modules;
+
+import com.mojang.brigadier.Message;
+import com.mojang.brigadier.suggestion.Suggestion;
+import com.mojang.brigadier.suggestion.Suggestions;
+import land.chipmunk.chipmunkmod.data.MutablePlayerListEntry;
+import land.chipmunk.chipmunkmod.listeners.Listener;
+import land.chipmunk.chipmunkmod.listeners.ListenerManager;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.network.packet.s2c.play.CommandSuggestionsS2CPacket;
+import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket;
+import net.minecraft.network.packet.s2c.play.PlayerRemoveS2CPacket;
+import net.minecraft.text.Text;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+public class Players extends Listener {
+    public List<MutablePlayerListEntry> list = new ArrayList<>();
+
+    public static Players INSTANCE = new Players();
+
+    public Players () {
+        ListenerManager.addListener(this);
+    }
+
+    public void init () {}
+
+    @Override
+    public void packetReceived (Packet packet) {
+        if (packet instanceof PlayerListS2CPacket) packetReceived((PlayerListS2CPacket) packet);
+        else if (packet instanceof PlayerRemoveS2CPacket) packetReceived((PlayerRemoveS2CPacket) packet);
+    }
+
+    public void packetReceived (PlayerListS2CPacket packet) {
+        try {
+            for (PlayerListS2CPacket.Action action : packet.getActions()) {
+                for (PlayerListS2CPacket.Entry entry : packet.getEntries()) {
+                    if (action == PlayerListS2CPacket.Action.ADD_PLAYER) addPlayer(entry);
+//                else if (action == PlayerListS2CPacket.Action.INITIALIZE_CHAT) initializeChat(entry);
+                    else if (action == PlayerListS2CPacket.Action.UPDATE_GAME_MODE) updateGamemode(entry);
+//                else if (action == PlayerListS2CPacket.Action.UPDATE_LISTED) updateListed(entry);
+                    else if (action == PlayerListS2CPacket.Action.UPDATE_LATENCY) updateLatency(entry);
+                    else if (action == PlayerListS2CPacket.Action.UPDATE_DISPLAY_NAME) updateDisplayName(entry);
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void packetReceived (PlayerRemoveS2CPacket packet) {
+        for (UUID uuid : packet.profileIds()) {
+            removePlayer(uuid);
+        }
+    }
+
+    public final MutablePlayerListEntry getEntry (UUID uuid) {
+        for (MutablePlayerListEntry candidate : list) {
+            if (candidate.profile().getId().equals(uuid)) {
+                return candidate;
+            }
+        }
+
+        return null;
+    }
+
+    public final MutablePlayerListEntry getEntry (String username) {
+        for (MutablePlayerListEntry candidate : list) {
+            if (candidate.profile().getName().equals(username)) {
+                return candidate;
+            }
+        }
+
+        return null;
+    }
+
+    public final MutablePlayerListEntry getEntry (Text displayName) {
+        for (MutablePlayerListEntry candidate : list) {
+            if (candidate.displayName() != null && candidate.displayName().equals(displayName)) {
+                return candidate;
+            }
+        }
+
+        return null;
+    }
+
+    private MutablePlayerListEntry getEntry (PlayerListS2CPacket.Entry other) {
+        return getEntry(other.profile().getId());
+    }
+
+    private void addPlayer (PlayerListS2CPacket.Entry newEntry) {
+        final MutablePlayerListEntry duplicate = getEntry(newEntry);
+        if (duplicate != null) list.remove(duplicate);
+
+        list.add(new MutablePlayerListEntry(newEntry));
+    }
+
+    private void updateGamemode (PlayerListS2CPacket.Entry newEntry) {
+        final MutablePlayerListEntry target = getEntry(newEntry);
+        if (target == null) return;
+
+        target.gamemode(newEntry.gameMode());
+    }
+
+    private void updateLatency (PlayerListS2CPacket.Entry newEntry) {
+        final MutablePlayerListEntry target = getEntry(newEntry);
+        if (target == null) return;
+
+        target.latency(newEntry.latency());
+    }
+
+    private void updateDisplayName (PlayerListS2CPacket.Entry newEntry) {
+        final MutablePlayerListEntry target = getEntry(newEntry);
+        if (target == null) return;
+
+        target.displayName(newEntry.displayName());
+    }
+
+    private void removePlayer (UUID uuid) {
+        final MutablePlayerListEntry target = getEntry(uuid);
+        if (target == null) return;
+
+        final CompletableFuture<CommandSuggestionsS2CPacket> future = TabComplete.INSTANCE.complete("/scoreboard players add ");
+
+        if (future == null) return;
+
+        future.thenApply(packet -> {
+            final Suggestions matches = packet.getSuggestions();
+            final String username = target.profile().getName();
+
+            for (int i = 0; i < matches.getList().size(); i++) {
+                final Suggestion suggestion = matches.getList().get(i);
+
+                final Message tooltip = suggestion.getTooltip();
+                if (tooltip != null || !suggestion.getText().equals(username)) continue;
+                return packet;
+            }
+
+            list.remove(target);
+            return packet;
+        });
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/modules/RainbowName.java b/src/main/java/land/chipmunk/chipmunkmod/modules/RainbowName.java
new file mode 100644
index 0000000..160d909
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/modules/RainbowName.java
@@ -0,0 +1,161 @@
+package land.chipmunk.chipmunkmod.modules;
+
+import land.chipmunk.chipmunkmod.util.ColorUtilities;
+import lombok.Getter;
+import lombok.Setter;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.TextColor;
+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+
+import java.util.Random;
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class RainbowName {
+    private final MinecraftClient client;
+
+    public static final RainbowName INSTANCE = new RainbowName(MinecraftClient.getInstance());
+
+    private static final String BUKKIT_COLOR_CODES = "123456789abcdefklmorx";
+    private static final String TEAM_NAME_CHARACTERS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-.+";
+
+    private Timer timer = null;
+
+    @Getter @Setter private boolean enabled = false;
+
+    private String[] team;
+
+    @Getter @Setter private String displayName;
+
+    private int startHue = 0;
+
+    public void init () {
+        final TimerTask task = new TimerTask() {
+            public void run () {
+                tick();
+            }
+        };
+
+        if (timer != null) cleanup();
+
+        timer = new Timer();
+        timer.schedule(task, 0, 75);
+    }
+
+    private String[] generateColorCodes(int length) {
+        String SALTCHARS = BUKKIT_COLOR_CODES;
+        StringBuilder salt = new StringBuilder();
+        Random rnd = new Random();
+        while (salt.length() < length) { // length of the random string.
+            int index = (int) (rnd.nextFloat() * SALTCHARS.length());
+            salt.append(SALTCHARS.charAt(index));
+        }
+        String saltStr = salt.toString();
+        return saltStr.split("");
+    }
+
+    private String generateUsername (String[] codes) {
+        StringBuilder string = new StringBuilder();
+        for (String code : codes) string.append("&").append(code);
+        return string.toString();
+    }
+
+    private String generateUsername (int _codes) {
+        StringBuilder string = new StringBuilder();
+
+        final String[] codes = generateColorCodes(_codes);
+
+        for (String code : codes) string.append("&").append(code);
+        return string.toString();
+    }
+
+    private String generateUsername (char[] codes, char character) {
+        StringBuilder string = new StringBuilder();
+        for (char code : codes) string.append(character + code);
+        return string.toString();
+    }
+
+    private String[] generateTeamName () {
+        String SALTCHARS = TEAM_NAME_CHARACTERS;
+        StringBuilder salt = new StringBuilder();
+        Random rnd = new Random();
+        while (salt.length() < TEAM_NAME_CHARACTERS.length()) { // length of the random string.
+            int index = (int) (rnd.nextFloat() * SALTCHARS.length());
+            salt.append(SALTCHARS.charAt(index));
+        }
+        String saltStr = salt.toString();
+        return saltStr.split("");
+    }
+
+    public void enable () {
+        final String[] colorCodes = generateColorCodes(8);
+        client.getNetworkHandler().sendChatCommand("extras:username " + generateUsername(colorCodes));
+
+        team = generateTeamName();
+
+        CommandCore.INSTANCE.run("minecraft:team add " + String.join("", team));
+
+        CommandCore.INSTANCE.run("minecraft:execute as " + client.getNetworkHandler().getProfile().getId() + " run team join " + String.join("", team));
+
+        enabled = true;
+    }
+
+    public void disable () {
+        client.getNetworkHandler().sendChatCommand("extras:username " + client.getSession().getUsername());
+
+        CommandCore.INSTANCE.run("minecraft:team remove " + String.join("", team));
+        team = null;
+
+        CommandCore.INSTANCE.run("essentials:nick " + client.getSession().getUsername() + " off");
+
+        enabled = false;
+    }
+
+    public RainbowName (MinecraftClient client) {
+        this.client = client;
+        this.displayName = client.getSession().getUsername();
+    }
+
+    private void tick () {
+        try {
+            final ClientPlayNetworkHandler networkHandler = client.getNetworkHandler();
+
+            if (networkHandler == null) {
+                cleanup();
+                return;
+            }
+
+            if (!enabled) return;
+
+            int hue = startHue;
+            int increment = (int) (360.0 / Math.max(displayName.length(), 20));
+
+            Component component = Component.empty();
+            StringBuilder essentialsNickname = new StringBuilder();
+
+            for (char character : displayName.toCharArray()) {
+                String color = String.format("%06x", ColorUtilities.hsvToRgb(hue, 100, 100));
+                component = component.append(Component.text(character).color(TextColor.fromHexString("#" + color)));
+                essentialsNickname.append("\u00a7#").append(color).append(character != ' ' ? character : '_');
+                hue = (hue + increment) % 360;
+            }
+
+            CommandCore.INSTANCE.run("minecraft:team modify " + String.join("", team) + " prefix " + GsonComponentSerializer.gson().serialize(component));
+            CommandCore.INSTANCE.run("essentials:nick " + client.getSession().getUsername() + " " + essentialsNickname);
+
+            startHue = (startHue + increment) % 360;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public void cleanup () {
+        if (timer == null) return;
+
+        timer.cancel();
+        timer.purge();
+        timer = null;
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/modules/SelfCare.java b/src/main/java/land/chipmunk/chipmunkmod/modules/SelfCare.java
old mode 100755
new mode 100644
index af72cd3..3a6b4d0
--- a/src/main/java/land/chipmunk/chipmunkmod/modules/SelfCare.java
+++ b/src/main/java/land/chipmunk/chipmunkmod/modules/SelfCare.java
@@ -1,17 +1,28 @@
 package land.chipmunk.chipmunkmod.modules;
 
+import land.chipmunk.chipmunkmod.listeners.Listener;
+import land.chipmunk.chipmunkmod.listeners.ListenerManager;
+import lombok.Setter;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.network.ClientPlayNetworkHandler;
 import net.minecraft.client.network.ClientPlayerEntity;
 import com.mojang.brigadier.tree.CommandNode;
 import com.mojang.brigadier.tree.LiteralCommandNode;
 import lombok.Getter;
+import net.minecraft.text.Text;
+
 import java.util.Timer;
 import java.util.TimerTask;
 
-public class SelfCare {
+public class SelfCare extends Listener {
   private final MinecraftClient client;
-  @Getter private long interval;
+  @Getter private final long interval;
+
+  @Getter @Setter private boolean opEnabled = true;
+  @Getter @Setter private boolean gamemodeEnabled = true;
+  @Getter @Setter private boolean cspyEnabled = true;
+
+  private boolean cspy = false;
 
   private Timer timer = null;
 
@@ -20,6 +31,8 @@ public class SelfCare {
   public SelfCare (MinecraftClient client, long interval) {
     this.client = client;
     this.interval = interval;
+
+    ListenerManager.addListener(this);
   }
 
   public void init () {
@@ -43,6 +56,14 @@ public class SelfCare {
     timer = null;
   }
 
+  @Override
+  public void chatMessageReceived (Text message) {
+    final String stringMessage = message.getString();
+
+    if (stringMessage.equals("Successfully enabled CommandSpy")) cspy = true;
+    else if (stringMessage.equals("Successfully disabled CommandSpy")) cspy = false;
+  }
+
   public void tick () {
     final ClientPlayerEntity player = client.player;
     final ClientPlayNetworkHandler networkHandler = client.getNetworkHandler();
@@ -52,17 +73,19 @@ public class SelfCare {
       return;
     }
 
-    if (!player.hasPermissionLevel(2)) { if (serverHasCommand("op")) networkHandler.sendChatCommand("op @s[type=player]"); }
-    else if (!client.player.isCreative()) networkHandler.sendChatCommand("gamemode creative");
+    if (player != null && !player.hasPermissionLevel(2) && opEnabled) { if (serverHasCommand("op")) networkHandler.sendChatCommand("op @s[type=player]"); }
+    else if (client.player != null && !client.player.isCreative() && gamemodeEnabled) networkHandler.sendChatCommand("gamemode creative");
+    else if (!cspy && cspyEnabled) { if (serverHasCommand("c")) networkHandler.sendChatCommand("c on"); }
   }
 
   // TODO: Move this into a separate class related to server info gathering (and yes, I plan on making this d y n a m i c and require little to no configuration for most servers)
   private boolean serverHasCommand (String name) {
     final ClientPlayNetworkHandler networkHandler = client.getNetworkHandler();
 
+    if (networkHandler == null) return false;
+
     for (CommandNode node : networkHandler.getCommandDispatcher().getRoot().getChildren()) {
-      if (!(node instanceof LiteralCommandNode)) continue;
-      final LiteralCommandNode literal = (LiteralCommandNode) node;
+      if (!(node instanceof LiteralCommandNode literal)) continue;
 
       if (literal.getLiteral().equals(name)) return true;
     }
diff --git a/src/main/java/land/chipmunk/chipmunkmod/modules/SongPlayer.java b/src/main/java/land/chipmunk/chipmunkmod/modules/SongPlayer.java
new file mode 100644
index 0000000..2f537d9
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/modules/SongPlayer.java
@@ -0,0 +1,224 @@
+package land.chipmunk.chipmunkmod.modules;
+
+import land.chipmunk.chipmunkmod.song.Note;
+import land.chipmunk.chipmunkmod.song.Song;
+import land.chipmunk.chipmunkmod.song.SongLoaderException;
+import land.chipmunk.chipmunkmod.song.SongLoaderThread;
+import lombok.Getter;
+import lombok.Setter;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.sound.SoundCategory;
+import net.minecraft.sound.SoundEvent;
+import net.minecraft.util.Identifier;
+
+import java.io.File;
+import java.net.URL;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class SongPlayer {
+    public static final String SELECTOR  = "@a[tag=!nomusic,tag=!chipmunkmod_nomusic]";
+    public static File SONG_DIR = new File("songs");
+    static {
+        if (!SONG_DIR.exists()) {
+            SONG_DIR.mkdir();
+        }
+    }
+
+    public static final SongPlayer INSTANCE = new SongPlayer(MinecraftClient.getInstance());
+
+    @Getter @Setter private Song currentSong;
+    @Getter @Setter private LinkedList<Song> songQueue = new LinkedList<>();
+    @Getter @Setter private Timer playTimer;
+    @Getter @Setter private SongLoaderThread loaderThread;
+    private int ticksUntilPausedActionbar = 20;
+
+    @Getter @Setter private boolean useCore = true;
+
+    private final MinecraftClient client;
+
+    public SongPlayer (MinecraftClient client) {
+        this.client = client;
+    }
+
+    // TODO: Less duplicate code
+
+    public void loadSong (Path location) {
+        if (loaderThread != null) {
+            client.player.sendMessage(Component.translatable("Already loading a song, cannot load another", NamedTextColor.RED));
+            return;
+        }
+
+        try {
+            final SongLoaderThread _loaderThread = new SongLoaderThread(location);
+            client.player.sendMessage(Component.translatable("Loading %s", Component.text(location.getFileName().toString(), NamedTextColor.DARK_GREEN)).color(NamedTextColor.GREEN));
+            _loaderThread.start();
+            loaderThread = _loaderThread;
+        } catch (SongLoaderException e) {
+            client.player.sendMessage(Component.translatable("Failed to load song: %s", e.message()).color(NamedTextColor.RED));
+            loaderThread = null;
+        }
+    }
+
+    public void loadSong (URL location) {
+        if (loaderThread != null) {
+            client.player.sendMessage(Component.translatable("Already loading a song, cannot load another", NamedTextColor.RED));
+            return;
+        }
+
+        try {
+            final SongLoaderThread _loaderThread = new SongLoaderThread(location);
+            client.player.sendMessage(Component.translatable("Loading %s", Component.text(location.toString(), NamedTextColor.DARK_GREEN)).color(NamedTextColor.GREEN));
+            _loaderThread.start();
+            loaderThread = _loaderThread;
+        } catch (SongLoaderException e) {
+            client.player.sendMessage(Component.translatable("Failed to load song: %s", e.message()).color(NamedTextColor.RED));
+            loaderThread = null;
+        }
+    }
+
+    public void coreReady () {
+        playTimer = new Timer();
+
+        final TimerTask playTask = new TimerTask() {
+            @Override
+            public void run () {
+                final ClientPlayNetworkHandler networkHandler = client.getNetworkHandler();
+
+                if (networkHandler == null) {
+                    disconnected();
+                    return;
+                }
+
+                if (loaderThread != null && !loaderThread.isAlive()) {
+                    if (loaderThread.exception != null) {
+                        client.player.sendMessage(Component.translatable("Failed to load song: %s", loaderThread.exception.message()).color(NamedTextColor.RED));
+                    } else {
+                        songQueue.add(loaderThread.song);
+                        client.player.sendMessage(Component.translatable("Added %s to the song queue", Component.empty().append(loaderThread.song.name).color(NamedTextColor.DARK_GREEN)).color(NamedTextColor.GREEN));
+                    }
+                    loaderThread = null;
+                }
+
+                if (currentSong == null) {
+                    if (songQueue.size() == 0) return;
+
+                    currentSong = songQueue.poll();
+                    client.player.sendMessage(Component.translatable("Now playing %s", Component.empty().append(currentSong.name).color(NamedTextColor.DARK_GREEN)).color(NamedTextColor.GREEN));
+                    currentSong.play();
+                }
+
+                if (currentSong.paused && ticksUntilPausedActionbar-- < 0) return;
+                else ticksUntilPausedActionbar = 20;
+
+                try {
+                    if (!useCore) client.player.sendActionBar(generateActionbar());
+                    else CommandCore.INSTANCE.run("title " + SELECTOR + " actionbar " + GsonComponentSerializer.gson().serialize(generateActionbar()));
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+
+                if (currentSong.paused) return;
+
+                handlePlaying();
+
+                if (currentSong.finished()) {
+                    client.player.sendMessage(Component.translatable("Finished playing %s", Component.empty().append(currentSong.name).color(NamedTextColor.DARK_GREEN)).color(NamedTextColor.GREEN));
+                    currentSong = null;
+                }
+            }
+        };
+
+        playTimer.schedule(playTask, 50, 50);
+
+        if (currentSong != null) currentSong.play();
+    }
+
+    public Component generateActionbar () {
+        final ClientPlayerEntity player = client.player;
+
+        Component component = Component.empty()
+                .append(Component.translatable("%s", player.getName()).color(NamedTextColor.GREEN))
+                .append(Component.translatable(" | ", NamedTextColor.DARK_GRAY))
+                .append(Component.translatable("Now playing %s", Component.empty().append(currentSong.name).color(NamedTextColor.DARK_GREEN)).color(NamedTextColor.GREEN))
+                .append(Component.translatable(" | ", NamedTextColor.DARK_GRAY))
+                .append(Component.translatable("%s / %s", formatTime(currentSong.time).color(NamedTextColor.GREEN), formatTime(currentSong.length).color(NamedTextColor.GREEN)).color(NamedTextColor.GRAY))
+                .append(Component.translatable(" | ", NamedTextColor.DARK_GRAY))
+                .append(Component.translatable("%s / %s", Component.text(currentSong.position, NamedTextColor.GREEN), Component.text(currentSong.size(), NamedTextColor.GREEN)).color(NamedTextColor.GRAY));
+
+        if (currentSong.paused) {
+            return component
+                    .append(Component.translatable(" | ", NamedTextColor.DARK_GRAY))
+                    .append(Component.translatable("Paused", NamedTextColor.DARK_GREEN));
+        }
+
+        if (currentSong.looping) {
+            if (currentSong.loopCount > 0) {
+                return component
+                        .append(Component.translatable(" | ", NamedTextColor.DARK_GRAY))
+                        .append(Component.translatable("Looping (%s/%s)", Component.text(currentSong.currentLoop), Component.text(currentSong.loopCount)).color(NamedTextColor.DARK_GREEN));
+            }
+
+            return component
+                    .append(Component.translatable(" | ", NamedTextColor.DARK_GRAY))
+                    .append(Component.translatable("Looping", NamedTextColor.DARK_GREEN));
+        }
+
+        return component;
+    }
+
+    public Component formatTime (long millis) {
+        final int seconds = (int) millis / 1000;
+
+        final String minutePart = String.valueOf(seconds / 60);
+        final String unpaddedSecondPart = String.valueOf(seconds % 60);
+
+        return Component.translatable(
+                "%s:%s",
+                Component.text(minutePart),
+                Component.text(unpaddedSecondPart.length() < 2 ? "0" + unpaddedSecondPart : unpaddedSecondPart)
+        );
+    }
+
+    public void stopPlaying () {
+        currentSong = null;
+    }
+
+    public void disconnected () {
+        playTimer.cancel();
+        playTimer.purge();
+
+        if (currentSong != null) currentSong.pause();
+    }
+
+    public void handlePlaying () {
+        currentSong.advanceTime();
+        while (currentSong.reachedNextNote()) {
+            final Note note = currentSong.getNextNote();
+
+            final float floatingPitch = (float) Math.pow(2, (note.pitch - 12) / 12.0);
+
+            try {
+                if (!useCore) {
+                    if (floatingPitch < 0 || floatingPitch > 2) return;
+
+                    final String[] thing = note.instrument.sound.split(":");
+
+                    if (thing[1] == null) return; // idk if this can be null but ill just protect it for now i guess
+
+                    client.player.playSound(SoundEvent.of(Identifier.of(thing[0], thing[1])), SoundCategory.RECORDS, note.volume, floatingPitch);
+                } else CommandCore.INSTANCE.run("execute as " + SELECTOR + " at @s run playsound " + note.instrument.sound + " record @s ~ ~ ~ " + note.volume + " " + floatingPitch);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/modules/TabComplete.java b/src/main/java/land/chipmunk/chipmunkmod/modules/TabComplete.java
new file mode 100644
index 0000000..aad937b
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/modules/TabComplete.java
@@ -0,0 +1,62 @@
+package land.chipmunk.chipmunkmod.modules;
+
+import land.chipmunk.chipmunkmod.listeners.Listener;
+import land.chipmunk.chipmunkmod.listeners.ListenerManager;
+import lombok.Getter;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.network.ClientConnection;
+import net.minecraft.network.packet.Packet;
+import net.minecraft.network.packet.c2s.play.RequestCommandCompletionsC2SPacket;
+import net.minecraft.network.packet.s2c.play.CommandSuggestionsS2CPacket;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+public class TabComplete extends Listener {
+    private final MinecraftClient client;
+
+    private int nextTransactionId = 0;
+    private final Map<Integer, CompletableFuture<CommandSuggestionsS2CPacket>> transactions = new HashMap<>();
+
+    public static TabComplete INSTANCE = new TabComplete(MinecraftClient.getInstance());
+
+    @Getter private boolean loggedIn = false;
+
+    public TabComplete (MinecraftClient client) {
+        this.client = client;
+        ListenerManager.addListener(this);
+    }
+
+    public CompletableFuture<CommandSuggestionsS2CPacket> complete (String command) {
+        final ClientPlayNetworkHandler networkHandler = client.getNetworkHandler();
+
+        if (networkHandler == null) return null;
+
+        final ClientConnection connection = networkHandler.getConnection();
+
+        if (connection == null) return null;
+
+        final int transactionId = nextTransactionId++;
+        if (nextTransactionId > Integer.MAX_VALUE) nextTransactionId = 0; // ? Can and should I use negative numbers too?
+        connection.send(new RequestCommandCompletionsC2SPacket(transactionId, command));
+
+        final CompletableFuture<CommandSuggestionsS2CPacket> future = new CompletableFuture<>();
+        transactions.put(transactionId, future);
+        return future;
+    }
+
+    @Override
+    public void packetReceived (Packet packet) {
+        if (packet instanceof CommandSuggestionsS2CPacket) packetReceived((CommandSuggestionsS2CPacket) packet);
+    }
+
+    public void packetReceived (CommandSuggestionsS2CPacket packet) {
+        final CompletableFuture<CommandSuggestionsS2CPacket> future = transactions.get(packet.getCompletionId());
+
+        if (future == null) return;
+        future.complete(packet);
+        transactions.remove(future);
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/song/Instrument.java b/src/main/java/land/chipmunk/chipmunkmod/song/Instrument.java
new file mode 100644
index 0000000..91462cb
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/song/Instrument.java
@@ -0,0 +1,48 @@
+package land.chipmunk.chipmunkmod.song;
+
+public class Instrument {
+  public static final Instrument HARP = new Instrument(0, "harp", 54);
+  public static final Instrument BASEDRUM = new Instrument(1, "basedrum", 0);
+  public static final Instrument SNARE = new Instrument(2, "snare", 0);
+  public static final Instrument HAT = new Instrument(3, "hat", 0);
+  public static final Instrument BASS = new Instrument(4, "bass", 30);
+  public static final Instrument FLUTE = new Instrument(5, "flute", 66);
+  public static final Instrument BELL = new Instrument(6, "bell", 78);
+  public static final Instrument GUITAR = new Instrument(7, "guitar", 42);
+  public static final Instrument CHIME = new Instrument(8, "chime", 78);
+  public static final Instrument XYLOPHONE = new Instrument(9, "xylophone", 78);
+  public static final Instrument IRON_XYLOPHONE = new Instrument(10, "iron_xylophone", 54);
+  public static final Instrument COW_BELL = new Instrument(11, "cow_bell", 66);
+  public static final Instrument DIDGERIDOO = new Instrument(12, "didgeridoo", 30);
+  public static final Instrument BIT = new Instrument(13, "bit", 54);
+  public static final Instrument BANJO = new Instrument(14, "banjo", 54);
+  public static final Instrument PLING = new Instrument(15, "pling", 54);
+
+  public final int id;
+  public final String name;
+  public final int offset;
+  public final String sound;
+
+  private Instrument (int id, String name, int offset, String sound) {
+    this.id = id;
+    this.name = name;
+    this.offset = offset;
+    this.sound = name;
+  }
+
+  private Instrument (int id, String name, int offset) {
+    this.id = id;
+    this.name = name;
+    this.offset = offset;
+    this.sound = "minecraft:block.note_block." + name;
+  }
+
+  public static Instrument of (String sound) {
+    return new Instrument(-1, null, 0, sound);
+  }
+
+  private static Instrument[] values = {HARP, BASEDRUM, SNARE, HAT, BASS, FLUTE, BELL, GUITAR, CHIME, XYLOPHONE, IRON_XYLOPHONE, COW_BELL, DIDGERIDOO, BIT, BANJO, PLING};
+  public static Instrument fromId (int id) {
+    return values[id];
+  }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/song/MidiConverter.java b/src/main/java/land/chipmunk/chipmunkmod/song/MidiConverter.java
new file mode 100644
index 0000000..abe34dc
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/song/MidiConverter.java
@@ -0,0 +1,373 @@
+package land.chipmunk.chipmunkmod.song;
+
+import land.chipmunk.chipmunkmod.util.DownloadUtilities;
+import java.io.*;
+import java.net.*;
+import java.nio.file.Paths;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+
+import javax.sound.midi.*;
+
+public class MidiConverter {
+  public static final int SET_INSTRUMENT = 0xC0;
+  public static final int SET_TEMPO = 0x51;
+  public static final int NOTE_ON = 0x90;
+  public static final int NOTE_OFF = 0x80;
+
+  public static Song getSongFromUrl(URL url) throws IOException, InvalidMidiDataException, URISyntaxException, NoSuchAlgorithmException, KeyManagementException {
+    Sequence sequence = MidiSystem.getSequence(DownloadUtilities.DownloadToInputStream(url, 5*1024*1024));
+    return getSong(sequence, Paths.get(url.toURI().getPath()).getFileName().toString());
+  }
+
+  public static Song getSongFromFile(File file) throws InvalidMidiDataException, IOException {
+    Sequence sequence = MidiSystem.getSequence(file);
+    return getSong(sequence, file.getName());
+  }
+
+  public static Song getSongFromBytes(byte[] bytes, String name) throws InvalidMidiDataException, IOException {
+    Sequence sequence = MidiSystem.getSequence(new ByteArrayInputStream(bytes));
+    return getSong(sequence, name);
+  }
+  
+  public static Song getSong(Sequence sequence, String name) {
+    Song song = new Song(name);
+
+    long tpq = sequence.getResolution();
+
+    ArrayList<MidiEvent> tempoEvents = new ArrayList<>();
+    for (Track track : sequence.getTracks()) {
+      for (int i = 0; i < track.size(); i++) {
+        MidiEvent event = track.get(i);
+        MidiMessage message = event.getMessage();
+        if (message instanceof MetaMessage) {
+          MetaMessage mm = (MetaMessage) message;
+          if (mm.getType() == SET_TEMPO) {
+            tempoEvents.add(event);
+          }
+        }
+      }
+    }
+    
+    Collections.sort(tempoEvents, (a, b) -> Long.compare(a.getTick(), b.getTick()));
+    
+    for (Track track : sequence.getTracks()) {
+
+      long microTime = 0;
+      int[] ids = new int[16];
+      int mpq = 500000;
+      int tempoEventIdx = 0;
+      long prevTick = 0;
+      
+      for (int i = 0; i < track.size(); i++) {
+        MidiEvent event = track.get(i);
+        MidiMessage message = event.getMessage();
+        
+        while (tempoEventIdx < tempoEvents.size() && event.getTick() > tempoEvents.get(tempoEventIdx).getTick()) {
+          long deltaTick = tempoEvents.get(tempoEventIdx).getTick() - prevTick;
+          prevTick = tempoEvents.get(tempoEventIdx).getTick();
+          microTime += (mpq/tpq) * deltaTick;
+          
+          MetaMessage mm = (MetaMessage) tempoEvents.get(tempoEventIdx).getMessage();
+          byte[] data = mm.getData();
+          int new_mpq = (data[2]&0xFF) | ((data[1]&0xFF)<<8) | ((data[0]&0xFF)<<16);
+          if (new_mpq != 0) mpq = new_mpq;
+          tempoEventIdx++;
+        }
+        
+        if (message instanceof ShortMessage) {
+          ShortMessage sm = (ShortMessage) message;
+          if (sm.getCommand() == SET_INSTRUMENT) {
+            ids[sm.getChannel()] = sm.getData1();
+          }
+          else if (sm.getCommand() == NOTE_ON) {
+            if (sm.getData2() == 0) continue;
+            int pitch = sm.getData1();
+            int velocity = sm.getData2();
+            long deltaTick = event.getTick() - prevTick;
+            prevTick = event.getTick();
+            microTime += (mpq/tpq) * deltaTick;
+
+            Note note;
+            if (sm.getChannel() == 9) {
+              note = getMidiPercussionNote(pitch, velocity, microTime);
+            }
+            else {
+              note = getMidiInstrumentNote(ids[sm.getChannel()], pitch, velocity, microTime);
+            }
+            if (note != null) {
+              song.add(note);
+            }
+
+            long time = microTime / 1000L;
+            if (time > song.length) {
+              song.length = time;
+            }
+          }
+          else if (sm.getCommand() == NOTE_OFF) {
+            long deltaTick = event.getTick() - prevTick;
+            prevTick = event.getTick();
+            microTime += (mpq/tpq) * deltaTick;
+            long time = microTime / 1000L;
+            if (time > song.length) {
+              song.length = time;
+            }
+          }
+        }
+      }
+    }
+
+    song.sort();
+    
+    return song;
+  }
+
+  public static Note getMidiInstrumentNote(int midiInstrument, int midiPitch, int velocity, long microTime) {
+    Instrument instrument = null;
+    Instrument[] instrumentList = instrumentMap.get(midiInstrument);
+    if (instrumentList != null) {
+      for (Instrument candidateInstrument : instrumentList) {
+        if (midiPitch >= candidateInstrument.offset && midiPitch <= candidateInstrument.offset+24) {
+          instrument = candidateInstrument;
+          break;
+        }
+      }
+    }
+
+    if (instrument == null) {
+      return null;
+    }
+
+    int pitch = midiPitch-instrument.offset;
+    float volume = (float) velocity / 127.0f;
+    long time = microTime / 1000L;
+
+    return new Note(instrument, pitch, volume, time);
+  }
+
+  private static Note getMidiPercussionNote (int midiPitch, int velocity, long microTime) {
+    if (percussionMap.containsKey(midiPitch)) {
+      int noteId = percussionMap.get(midiPitch);
+      int pitch = noteId % 25;
+      float volume = (float) velocity / 127.0f;
+      Instrument instrument = Instrument.fromId(noteId / 25);
+      long time = microTime / 1000L;
+
+      return new Note(instrument, pitch, volume, time);
+    }
+    return null;
+  }
+
+  public static HashMap<Integer, Instrument[]> instrumentMap = new HashMap<>();
+  static {
+    // Piano (HARP BASS BELL)
+    instrumentMap.put(0, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Acoustic Grand Piano
+    instrumentMap.put(1, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Bright Acoustic Piano
+    instrumentMap.put(2, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); // Electric Grand Piano
+    instrumentMap.put(3, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Honky-tonk Piano
+    instrumentMap.put(4, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); // Electric Piano 1
+    instrumentMap.put(5, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); // Electric Piano 2
+    instrumentMap.put(6, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Harpsichord
+    instrumentMap.put(7, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Clavinet
+
+    // Chromatic Percussion (IRON_XYLOPHONE XYLOPHONE BASS)
+    instrumentMap.put(8, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Celesta
+    instrumentMap.put(9, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Glockenspiel
+    instrumentMap.put(10, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Music Box
+    instrumentMap.put(11, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Vibraphone
+    instrumentMap.put(12, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Marimba
+    instrumentMap.put(13, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Xylophone
+    instrumentMap.put(14, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Tubular Bells
+    instrumentMap.put(15, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE}); // Dulcimer
+
+    // Organ (BIT DIDGERIDOO BELL)
+    instrumentMap.put(16, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Drawbar Organ
+    instrumentMap.put(17, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Percussive Organ
+    instrumentMap.put(18, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Rock Organ
+    instrumentMap.put(19, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Church Organ
+    instrumentMap.put(20, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Reed Organ
+    instrumentMap.put(21, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Accordian
+    instrumentMap.put(22, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Harmonica
+    instrumentMap.put(23, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Tango Accordian
+
+    // Guitar (BIT DIDGERIDOO BELL)
+    instrumentMap.put(24, new Instrument[]{Instrument.GUITAR, Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Acoustic Guitar (nylon)
+    instrumentMap.put(25, new Instrument[]{Instrument.GUITAR, Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Acoustic Guitar (steel)
+    instrumentMap.put(26, new Instrument[]{Instrument.GUITAR, Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Electric Guitar (jazz)
+    instrumentMap.put(27, new Instrument[]{Instrument.GUITAR, Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Electric Guitar (clean)
+    instrumentMap.put(28, new Instrument[]{Instrument.GUITAR, Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Electric Guitar (muted)
+    instrumentMap.put(29, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Overdriven Guitar
+    instrumentMap.put(30, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Distortion Guitar
+    instrumentMap.put(31, new Instrument[]{Instrument.GUITAR, Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Guitar Harmonics
+
+    // Bass
+    instrumentMap.put(32, new Instrument[]{Instrument.BASS, Instrument.HARP, Instrument.BELL}); // Acoustic Bass
+    instrumentMap.put(33, new Instrument[]{Instrument.BASS, Instrument.HARP, Instrument.BELL}); // Electric Bass (finger)
+    instrumentMap.put(34, new Instrument[]{Instrument.BASS, Instrument.HARP, Instrument.BELL}); // Electric Bass (pick)
+    instrumentMap.put(35, new Instrument[]{Instrument.BASS, Instrument.HARP, Instrument.BELL}); // Fretless Bass
+    instrumentMap.put(36, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Slap Bass 1
+    instrumentMap.put(37, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Slap Bass 2
+    instrumentMap.put(38, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Synth Bass 1
+    instrumentMap.put(39, new Instrument[]{Instrument.DIDGERIDOO, Instrument.BIT, Instrument.XYLOPHONE}); // Synth Bass 2
+
+    // Strings
+    instrumentMap.put(40, new Instrument[]{Instrument.FLUTE, Instrument.GUITAR, Instrument.BASS, Instrument.BELL}); // Violin
+    instrumentMap.put(41, new Instrument[]{Instrument.FLUTE, Instrument.GUITAR, Instrument.BASS, Instrument.BELL}); // Viola
+    instrumentMap.put(42, new Instrument[]{Instrument.FLUTE, Instrument.GUITAR, Instrument.BASS, Instrument.BELL}); // Cello
+    instrumentMap.put(43, new Instrument[]{Instrument.FLUTE, Instrument.GUITAR, Instrument.BASS, Instrument.BELL}); // Contrabass
+    instrumentMap.put(44, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL}); // Tremolo Strings
+    instrumentMap.put(45, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Pizzicato Strings
+    instrumentMap.put(46, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.CHIME}); // Orchestral Harp
+    instrumentMap.put(47, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Timpani
+
+    // Ensenble
+    instrumentMap.put(48, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // String Ensemble 1
+    instrumentMap.put(49, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // String Ensemble 2
+    instrumentMap.put(50, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Synth Strings 1
+    instrumentMap.put(51, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Synth Strings 2
+    instrumentMap.put(52, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Choir Aahs
+    instrumentMap.put(53, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Voice Oohs
+    instrumentMap.put(54, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Synth Choir
+    instrumentMap.put(55, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL}); // Orchestra Hit
+
+    // Brass
+    instrumentMap.put(56, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL});
+    instrumentMap.put(57, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL});
+    instrumentMap.put(58, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL});
+    instrumentMap.put(59, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL});
+    instrumentMap.put(60, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL});
+    instrumentMap.put(61, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL});
+    instrumentMap.put(62, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL});
+    instrumentMap.put(63, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL});
+
+    // Reed
+    instrumentMap.put(64, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+    instrumentMap.put(65, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+    instrumentMap.put(66, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+    instrumentMap.put(67, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+    instrumentMap.put(68, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+    instrumentMap.put(69, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+    instrumentMap.put(70, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+    instrumentMap.put(71, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+
+    // Pipe
+    instrumentMap.put(72, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+    instrumentMap.put(73, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+    instrumentMap.put(74, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+    instrumentMap.put(75, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+    instrumentMap.put(76, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+    instrumentMap.put(77, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+    instrumentMap.put(78, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+    instrumentMap.put(79, new Instrument[]{Instrument.FLUTE, Instrument.DIDGERIDOO, Instrument.IRON_XYLOPHONE, Instrument.BELL});
+
+    // Synth Lead
+    instrumentMap.put(80, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(81, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(82, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(83, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(84, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(85, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(86, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(87, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+
+    // Synth Pad
+    instrumentMap.put(88, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(89, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(90, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(91, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(92, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(93, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(94, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(95, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+
+    // Synth Effects
+//    instrumentMap.put(96, new Instrument[]{});
+//    instrumentMap.put(97, new Instrument[]{});
+    instrumentMap.put(98, new Instrument[]{Instrument.BIT, Instrument.DIDGERIDOO, Instrument.BELL});
+    instrumentMap.put(99, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(100, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(101, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(102, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(103, new Instrument[]{Instrument.HARP, Instrument.BASS, Instrument.BELL});
+
+    // Ethnic
+    instrumentMap.put(104, new Instrument[]{Instrument.BANJO, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(105, new Instrument[]{Instrument.BANJO, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(106, new Instrument[]{Instrument.BANJO, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(107, new Instrument[]{Instrument.BANJO, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(108, new Instrument[]{Instrument.BANJO, Instrument.BASS, Instrument.BELL});
+    instrumentMap.put(109, new Instrument[]{Instrument.HARP, Instrument.DIDGERIDOO, Instrument.BELL});
+    instrumentMap.put(110, new Instrument[]{Instrument.HARP, Instrument.DIDGERIDOO, Instrument.BELL});
+    instrumentMap.put(111, new Instrument[]{Instrument.HARP, Instrument.DIDGERIDOO, Instrument.BELL});
+
+    // Percussive
+    instrumentMap.put(112, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE});
+    instrumentMap.put(113, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE});
+    instrumentMap.put(114, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE});
+    instrumentMap.put(115, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE});
+    instrumentMap.put(116, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE});
+    instrumentMap.put(117, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE});
+    instrumentMap.put(118, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE});
+    instrumentMap.put(119, new Instrument[]{Instrument.IRON_XYLOPHONE, Instrument.BASS, Instrument.XYLOPHONE});
+  }
+
+  public static HashMap<Integer, Integer> percussionMap = new HashMap<>();
+  static {
+    percussionMap.put(35, 10 + 25*Instrument.BASEDRUM.id);
+    percussionMap.put(36, 6  + 25*Instrument.BASEDRUM.id);
+    percussionMap.put(37, 6  + 25*Instrument.HAT.id);
+    percussionMap.put(38, 8  + 25*Instrument.SNARE.id);
+    percussionMap.put(39, 6  + 25*Instrument.HAT.id);
+    percussionMap.put(40, 4  + 25*Instrument.SNARE.id);
+    percussionMap.put(41, 6  + 25*Instrument.BASEDRUM.id);
+    percussionMap.put(42, 22 + 25*Instrument.SNARE.id);
+    percussionMap.put(43, 13 + 25*Instrument.BASEDRUM.id);
+    percussionMap.put(44, 22 + 25*Instrument.SNARE.id);
+    percussionMap.put(45, 15 + 25*Instrument.BASEDRUM.id);
+    percussionMap.put(46, 18 + 25*Instrument.SNARE.id);
+    percussionMap.put(47, 20 + 25*Instrument.BASEDRUM.id);
+    percussionMap.put(48, 23 + 25*Instrument.BASEDRUM.id);
+    percussionMap.put(49, 17 + 25*Instrument.SNARE.id);
+    percussionMap.put(50, 23 + 25*Instrument.BASEDRUM.id);
+    percussionMap.put(51, 24 + 25*Instrument.SNARE.id);
+    percussionMap.put(52, 8  + 25*Instrument.SNARE.id);
+    percussionMap.put(53, 13 + 25*Instrument.SNARE.id);
+    percussionMap.put(54, 18 + 25*Instrument.HAT.id);
+    percussionMap.put(55, 18 + 25*Instrument.SNARE.id);
+    percussionMap.put(56, 1  + 25*Instrument.HAT.id);
+    percussionMap.put(57, 13 + 25*Instrument.SNARE.id);
+    percussionMap.put(58, 2  + 25*Instrument.HAT.id);
+    percussionMap.put(59, 13 + 25*Instrument.SNARE.id);
+    percussionMap.put(60, 9  + 25*Instrument.HAT.id);
+    percussionMap.put(61, 2  + 25*Instrument.HAT.id);
+    percussionMap.put(62, 8  + 25*Instrument.HAT.id);
+    percussionMap.put(63, 22 + 25*Instrument.BASEDRUM.id);
+    percussionMap.put(64, 15 + 25*Instrument.BASEDRUM.id);
+    percussionMap.put(65, 13 + 25*Instrument.SNARE.id);
+    percussionMap.put(66, 8  + 25*Instrument.SNARE.id);
+    percussionMap.put(67, 8  + 25*Instrument.HAT.id);
+    percussionMap.put(68, 3  + 25*Instrument.HAT.id);
+    percussionMap.put(69, 20 + 25*Instrument.HAT.id);
+    percussionMap.put(70, 23 + 25*Instrument.HAT.id);
+    percussionMap.put(71, 24 + 25*Instrument.HAT.id);
+    percussionMap.put(72, 24 + 25*Instrument.HAT.id);
+    percussionMap.put(73, 17 + 25*Instrument.HAT.id);
+    percussionMap.put(74, 11 + 25*Instrument.HAT.id);
+    percussionMap.put(75, 18 + 25*Instrument.HAT.id);
+    percussionMap.put(76, 9  + 25*Instrument.HAT.id);
+    percussionMap.put(77, 5  + 25*Instrument.HAT.id);
+    percussionMap.put(78, 22 + 25*Instrument.HAT.id);
+    percussionMap.put(79, 19 + 25*Instrument.SNARE.id);
+    percussionMap.put(80, 17 + 25*Instrument.HAT.id);
+    percussionMap.put(81, 22 + 25*Instrument.HAT.id);
+    percussionMap.put(82, 22 + 25*Instrument.SNARE.id);
+    percussionMap.put(83, 24 + 25*Instrument.CHIME.id);
+    percussionMap.put(84, 24 + 25*Instrument.CHIME.id);
+    percussionMap.put(85, 21 + 25*Instrument.HAT.id);
+    percussionMap.put(86, 14 + 25*Instrument.BASEDRUM.id);
+    percussionMap.put(87, 7  + 25*Instrument.BASEDRUM.id);
+  }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/song/NBSConverter.java b/src/main/java/land/chipmunk/chipmunkmod/song/NBSConverter.java
new file mode 100644
index 0000000..c1bf1f6
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/song/NBSConverter.java
@@ -0,0 +1,201 @@
+package land.chipmunk.chipmunkmod.song;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+
+public class NBSConverter {
+  public static Instrument[] instrumentIndex = new Instrument[] {
+    Instrument.HARP,
+    Instrument.BASS,
+    Instrument.BASEDRUM,
+    Instrument.SNARE,
+    Instrument.HAT,
+    Instrument.GUITAR,
+    Instrument.FLUTE,
+    Instrument.BELL,
+    Instrument.CHIME,
+    Instrument.XYLOPHONE,
+    Instrument.IRON_XYLOPHONE,
+    Instrument.COW_BELL,
+    Instrument.DIDGERIDOO,
+    Instrument.BIT,
+    Instrument.BANJO,
+    Instrument.PLING,
+  };
+
+  private static class NBSNote {
+    public int tick;
+    public short layer;
+    public byte instrument;
+    public byte key;
+    public byte velocity = 100;
+    public byte panning = 100;
+    public short pitch = 0;
+  }
+
+  private static class NBSLayer {
+    public String name;
+    public byte lock = 0;
+    public byte volume;
+    public byte stereo = 100;
+  }
+
+  private static class NBSCustomInstrument {
+    public String name;
+    public String file;
+    public byte pitch = 0;
+    public boolean key = false;
+  }
+
+  public static Song getSongFromBytes(byte[] bytes, String fileName) throws IOException {
+    ByteBuffer buffer = ByteBuffer.wrap(bytes);
+    buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+    short songLength = 0;
+    byte format = 0;
+    byte vanillaInstrumentCount = 0;
+    songLength = buffer.getShort(); // If it's not 0, then it uses the old format
+    if (songLength == 0) {
+      format = buffer.get();
+    }
+
+    if (format >= 1) {
+      vanillaInstrumentCount = buffer.get();
+    }
+    if (format >= 3) {
+      songLength = buffer.getShort();
+    }
+
+    short layerCount = buffer.getShort();
+    String songName = getString(buffer, bytes.length);
+    String songAuthor = getString(buffer, bytes.length);
+    String songOriginalAuthor = getString(buffer, bytes.length);
+    String songDescription = getString(buffer, bytes.length);
+    short tempo = buffer.getShort();
+    byte autoSaving = buffer.get();
+    byte autoSavingDuration = buffer.get();
+    byte timeSignature = buffer.get();
+    int minutesSpent = buffer.getInt();
+    int leftClicks = buffer.getInt();
+    int rightClicks = buffer.getInt();
+    int blocksAdded = buffer.getInt();
+    int blocksRemoved = buffer.getInt();
+    String origFileName = getString(buffer, bytes.length);
+
+    byte loop = 0;
+    byte maxLoopCount = 0;
+    short loopStartTick = 0;
+    if (format >= 4) {
+      loop = buffer.get();
+      maxLoopCount = buffer.get();
+      loopStartTick = buffer.getShort();
+    }
+
+    ArrayList<NBSNote> nbsNotes = new ArrayList<>();
+    short tick = -1;
+    while (true) {
+      int tickJumps = buffer.getShort();
+      if (tickJumps == 0) break;
+      tick += tickJumps;
+
+      short layer = -1;
+      while (true) {
+        int layerJumps = buffer.getShort();
+        if (layerJumps == 0) break;
+        layer += layerJumps;
+        NBSNote note = new NBSNote();
+        note.tick = tick;
+        note.layer = layer;
+        note.instrument = buffer.get();
+        note.key = buffer.get();
+        if (format >= 4) {
+          note.velocity = buffer.get();
+          note.panning = buffer.get();
+          note.pitch = buffer.getShort();
+        }
+        nbsNotes.add(note);
+      }
+    }
+
+    ArrayList<NBSLayer> nbsLayers = new ArrayList<>();
+    if (buffer.hasRemaining()) {
+      for (int i=0; i<layerCount; i++) {
+        NBSLayer layer = new NBSLayer();
+        layer.name = getString(buffer, bytes.length);
+        if (format >= 4) {
+          layer.lock = buffer.get();
+        }
+        layer.volume = buffer.get();
+        if (format >= 2) {
+          layer.stereo = buffer.get();
+        }
+        nbsLayers.add(layer);
+      }
+    }
+
+    ArrayList<NBSCustomInstrument> customInstruments = new ArrayList<>();
+    if (buffer.hasRemaining()) {
+      byte customInstrumentCount = buffer.get();
+      for (int i = 0; i < customInstrumentCount; i++) {
+        NBSCustomInstrument customInstrument = new NBSCustomInstrument();
+        customInstrument.name = getString(buffer, bytes.length);
+        customInstrument.file = getString(buffer, bytes.length);
+        customInstrument.pitch = buffer.get();
+        customInstrument.key = buffer.get() == 0 ? false : true;
+        customInstruments.add(customInstrument);
+      }
+    }
+
+    Song song = new Song(songName.trim().length() > 0 ? songName : fileName);
+    if (loop > 0) {
+      song.looping = true;
+      song.loopPosition = getMilliTime(loopStartTick, tempo);
+      song.loopCount = maxLoopCount;
+    }
+    for (NBSNote note : nbsNotes) {
+      Instrument instrument;
+      int key = note.key;
+      if (note.instrument < instrumentIndex.length) {
+        instrument = instrumentIndex[note.instrument];
+      } else {
+        int index = note.instrument - instrumentIndex.length;
+        if (index >= customInstruments.size()) continue;
+        NBSCustomInstrument customInstrument = customInstruments.get(index);
+        instrument = Instrument.of(customInstrument.name);
+        // key += customInstrument.pitch;
+      }
+
+      if (key < 33 || key > 57) {
+        continue;
+      }
+
+      byte layerVolume = 100;
+      if (nbsLayers.size() > note.layer) {
+        layerVolume = nbsLayers.get(note.layer).volume;
+      }
+
+      int pitch = key-33;
+      song.add(new Note(instrument, pitch, (float) note.velocity * (float) layerVolume / 10000f, getMilliTime(note.tick, tempo)));
+    }
+
+    song.length = song.get(song.size()-1).time + 50;
+
+    return song;
+  }
+
+  private static String getString (ByteBuffer buffer, int maxSize) throws IOException {
+    int length = buffer.getInt();
+    if (length > maxSize) {
+      throw new IOException("String is too large");
+    }
+    byte arr[] = new byte[length];
+    buffer.get(arr, 0, length);
+    return new String(arr);
+  }
+
+  private static int getMilliTime(int tick, int tempo) {
+    return 1000 * tick * 100 / tempo;
+  }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/song/Note.java b/src/main/java/land/chipmunk/chipmunkmod/song/Note.java
new file mode 100644
index 0000000..94e5b09
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/song/Note.java
@@ -0,0 +1,28 @@
+package land.chipmunk.chipmunkmod.song;
+
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
+public class Note implements Comparable<Note> {
+  public Instrument instrument;
+  public int pitch;
+  public float volume;
+  public long time;
+
+  @Override
+  public int compareTo(Note other) {
+    if (time < other.time) {
+      return -1;
+    }
+    else if (time > other.time) {
+      return 1;
+    }
+    else {
+      return 0;
+    }
+  }
+
+  public int noteId () {
+    return pitch + instrument.id * 25;
+  }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/song/Song.java b/src/main/java/land/chipmunk/chipmunkmod/song/Song.java
new file mode 100644
index 0000000..6220a2a
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/song/Song.java
@@ -0,0 +1,131 @@
+package land.chipmunk.chipmunkmod.song;
+
+import net.kyori.adventure.text.Component;
+import java.util.ArrayList;
+import java.util.Collections;
+
+public class Song {
+  public ArrayList<Note> notes = new ArrayList<>();
+  public Component name;
+  public int position = 0; // Current note index
+  public boolean looping = false;
+  public boolean paused = true;
+  public long startTime = 0; // Start time in millis since unix epoch
+  public long length = 0; // Milliseconds in the song
+  public long time = 0; // Time since start of song
+  public long loopPosition = 0; // Milliseconds into the song to start looping
+  public int loopCount = 0; // Number of times to loop
+  public int currentLoop = 0; // Number of loops so far
+  
+  public Song (Component name) {
+    this.name = name;
+  }
+
+  public Song (String name) {
+    this(Component.text(name));
+  }
+  
+  public Note get (int i) {
+    return notes.get(i);
+  }
+  
+  public void add (Note e) {
+    notes.add(e);
+  }
+
+  public void sort () {
+    Collections.sort(notes);
+  }
+
+  /**
+   * Starts playing song (does nothing if already playing)
+   */
+  public void play () {
+    if (paused) {
+      paused = false;
+      startTime = System.currentTimeMillis() - time;
+    }
+  }
+
+  /**
+   * Pauses song (does nothing if already paused)
+   */
+  public void pause () {
+    if (!paused) {
+      paused = true;
+      // Recalculates time so that the song will continue playing after the exact point it was paused
+      advanceTime();
+    }
+  }
+
+  public void setTime (long t) {
+    time = t;
+    startTime = System.currentTimeMillis() - time;
+    position = 0;
+    while (position < notes.size() && notes.get(position).time < t) {
+      position++;
+    }
+  }
+
+  public void advanceTime () {
+    time = System.currentTimeMillis() - startTime;
+  }
+
+  public boolean reachedNextNote () {
+    if (position < notes.size()) {
+      return notes.get(position).time <= time;
+    } else {
+      if (time > length && shouldLoop()) {
+        loop();
+        if (position < notes.size()) {
+          return notes.get(position).time <= time;
+        } else {
+          return false;
+        }
+      } else {
+        return false;
+      }
+    }
+  }
+
+  public Note getNextNote () {
+    if (position >= notes.size()) {
+      if (shouldLoop()) {
+        loop();
+      } else {
+        return null;
+      }
+    }
+    return notes.get(position++);
+  }
+
+  public boolean finished () {
+    return time > length && !shouldLoop();
+  }
+
+  private void loop () {
+    position = 0;
+    startTime += length - loopPosition;
+    time -= length - loopPosition;
+    while (position < notes.size() && notes.get(position).time < loopPosition) {
+      position++;
+    }
+    currentLoop++;
+  }
+
+  private boolean shouldLoop () {
+    if (looping) {
+      if (loopCount == 0) {
+        return true;
+      } else {
+        return currentLoop < loopCount;
+      }
+    } else {
+      return false;
+    }
+  }
+
+  public int size () {
+    return notes.size();
+  }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/song/SongLoaderException.java b/src/main/java/land/chipmunk/chipmunkmod/song/SongLoaderException.java
new file mode 100644
index 0000000..52ed2f1
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/song/SongLoaderException.java
@@ -0,0 +1,24 @@
+package land.chipmunk.chipmunkmod.song;
+
+import net.kyori.adventure.text.Component;
+import lombok.Getter;
+import land.chipmunk.chipmunkmod.util.ComponentUtilities;
+
+public class SongLoaderException extends Exception {
+  @Getter private final Component message;
+
+  public SongLoaderException (Component message) {
+    super();
+    this.message = message;
+  }
+
+  public SongLoaderException (Component message, Throwable cause) {
+    super(null, cause);
+    this.message = message;
+  }
+
+  @Override
+  public String getMessage () {
+    return ComponentUtilities.stringify(message);
+  }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/song/SongLoaderThread.java b/src/main/java/land/chipmunk/chipmunkmod/song/SongLoaderThread.java
new file mode 100644
index 0000000..2353eb3
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/song/SongLoaderThread.java
@@ -0,0 +1,68 @@
+package land.chipmunk.chipmunkmod.song;
+
+import land.chipmunk.chipmunkmod.modules.SongPlayer;
+import land.chipmunk.chipmunkmod.util.DownloadUtilities;
+import net.kyori.adventure.text.Component;
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+public class SongLoaderThread extends Thread {
+  private String location;
+  private File songPath;
+  private URL songUrl;
+  public SongLoaderException exception;
+  public Song song;
+
+  private boolean isUrl = false;
+
+  public SongLoaderThread (URL location) throws SongLoaderException {
+    isUrl = true;
+    songUrl = location;
+  }
+
+  public SongLoaderThread (Path location) throws SongLoaderException {
+    isUrl = false;
+    songPath = location.toFile();
+  }
+
+  public void run () {
+    byte[] bytes;
+    String name;
+    try {
+      if (isUrl) {
+        bytes = DownloadUtilities.DownloadToByteArray(songUrl, 10*1024*1024);
+        name = Paths.get(songUrl.toURI().getPath()).getFileName().toString();
+      } else {
+        bytes = Files.readAllBytes(songPath.toPath());
+        name = songPath.getName();
+      }
+    } catch (Exception e) {
+      exception = new SongLoaderException(Component.text(e.getMessage()), e);
+      return;
+    }
+
+    try {
+      song = MidiConverter.getSongFromBytes(bytes, name);
+    } catch (Exception e) {
+    }
+
+    if (song == null) {
+      try {
+        song = NBSConverter.getSongFromBytes(bytes, name);
+      } catch (Exception e) {
+      }
+    }
+
+    if (song == null) {
+      exception = new SongLoaderException(Component.translatable("Invalid song format"));
+    }
+  }
+
+  private File getSongFile (String name) {
+    return new File(SongPlayer.SONG_DIR, name);
+  }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/util/ColorUtilities.java b/src/main/java/land/chipmunk/chipmunkmod/util/ColorUtilities.java
new file mode 100644
index 0000000..3ffb4a5
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/util/ColorUtilities.java
@@ -0,0 +1,10 @@
+package land.chipmunk.chipmunkmod.util;
+
+import java.awt.*;
+
+public class ColorUtilities {
+    public static int hsvToRgb (int hue, int saturation, int value) {
+        Color color = Color.getHSBColor(hue / 360.0f, saturation / 100.0f, value / 100.0f);
+        return color.getRGB() & 0xFFFFFF;
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/util/ComponentUtilities.java b/src/main/java/land/chipmunk/chipmunkmod/util/ComponentUtilities.java
new file mode 100644
index 0000000..519e4f0
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/util/ComponentUtilities.java
@@ -0,0 +1,105 @@
+package land.chipmunk.chipmunkmod.util;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.TextComponent;
+import net.kyori.adventure.text.TranslatableComponent;
+import net.kyori.adventure.text.SelectorComponent;
+import net.kyori.adventure.text.KeybindComponent;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ComponentUtilities {
+    private static final Map<String, String> language = loadJsonStringMap("language.json");
+    private static final Map<String, String> keybinds = loadJsonStringMap("keybinds.json");
+
+    public static final Pattern ARG_PATTERN = Pattern.compile("%(?:(\\d+)\\$)?(s|%)");
+
+    private ComponentUtilities () {
+    }
+
+    private static Map<String, String> loadJsonStringMap (String name) {
+        Map<String, String> map = new HashMap<>();
+
+        InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(name);
+        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+        JsonObject json = JsonParser.parseReader(reader).getAsJsonObject();
+
+        for (Map.Entry<String, JsonElement> entry : json.entrySet()) {
+            map.put(entry.getKey(), json.get(entry.getKey()).getAsString());
+        }
+
+        return map;
+    }
+
+    private static String getOrReturnKey (Map<String, String> map, String key) {
+        return map.containsKey(key) ? map.get(key) : key;
+    }
+
+    public static String stringify (Component message) {
+        StringBuilder builder = new StringBuilder();
+
+        builder.append(stringifyPartially(message));
+
+        for (Component child : message.children()) builder.append(stringify(child));
+
+        return builder.toString();
+    }
+
+    public static String stringifyPartially (Component message) {
+        if (message instanceof TextComponent) return stringifyPartially((TextComponent) message);
+        if (message instanceof TranslatableComponent) return stringifyPartially((TranslatableComponent) message);
+        if (message instanceof SelectorComponent) return stringifyPartially((SelectorComponent) message);
+        if (message instanceof KeybindComponent) return stringifyPartially((KeybindComponent) message);
+
+        return "";
+    }
+
+    public static String stringifyPartially (TextComponent message) {
+        return message.content();
+    }
+
+    public static String stringifyPartially (TranslatableComponent message) {
+        String format = getOrReturnKey(language, message.key());
+
+        // totallynotskidded™️ from HBot (and changed a bit)
+        Matcher matcher = ARG_PATTERN.matcher(format);
+        StringBuffer sb = new StringBuffer();
+
+        int i = 0;
+        while (matcher.find()) {
+            if (matcher.group().equals("%%")) {
+                matcher.appendReplacement(sb, "%");
+            } else {
+                String idxStr = matcher.group(1);
+                int idx = idxStr == null ? i++ : (Integer.parseInt(idxStr) - 1);
+                if (idx >= 0 && idx < message.args().size()) {
+                    matcher.appendReplacement(sb, Matcher.quoteReplacement( stringify(message.args().get(idx)) ));
+                } else {
+                    matcher.appendReplacement(sb, "");
+                }
+            }
+        }
+        matcher.appendTail(sb);
+
+        return sb.toString();
+    }
+
+    public static String stringifyPartially (SelectorComponent message) {
+        return message.pattern(); // * Client-side selector components are equivalent to text ones, and do NOT list entities.
+    }
+
+    public static String stringifyPartially (KeybindComponent message) {
+        String keybind = message.keybind();
+        Component component = keybinds.containsKey(keybind) ? Component.translatable(keybind) : Component.text(keybind); // TODO: Fix some keys like `key.keyboard.a`
+        return stringifyPartially(component);
+    }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/util/DownloadUtilities.java b/src/main/java/land/chipmunk/chipmunkmod/util/DownloadUtilities.java
new file mode 100644
index 0000000..1ae54fc
--- /dev/null
+++ b/src/main/java/land/chipmunk/chipmunkmod/util/DownloadUtilities.java
@@ -0,0 +1,63 @@
+package land.chipmunk.chipmunkmod.util;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import java.io.*;
+import java.net.URL;
+import java.net.URLConnection;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+
+public class DownloadUtilities {
+
+  private static class DefaultTrustManager implements X509TrustManager {
+
+    @Override
+    public void checkClientTrusted(X509Certificate[] arg0, String arg1) {}
+
+    @Override
+    public void checkServerTrusted(X509Certificate[] arg0, String arg1) {}
+
+    @Override
+    public X509Certificate[] getAcceptedIssuers() {
+      return null;
+    }
+  }
+
+  public static byte[] DownloadToByteArray(URL url, int maxSize) throws IOException, KeyManagementException, NoSuchAlgorithmException {
+    SSLContext ctx = SSLContext.getInstance("TLS");
+    ctx.init(new KeyManager[0], new TrustManager[] {new DefaultTrustManager()}, new SecureRandom());
+    SSLContext.setDefault(ctx);
+    URLConnection conn = url.openConnection();
+    conn.setConnectTimeout(5000);
+    conn.setReadTimeout(10000);
+    conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0");
+
+    try (BufferedInputStream downloadStream = new BufferedInputStream(conn.getInputStream())) {
+      ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
+      byte[] buf = new byte[1024];
+      int n;
+      int tot = 0;
+      while ((n = downloadStream.read(buf)) > 0) {
+        byteArrayStream.write(buf, 0, n);
+        tot += n;
+        if (tot > maxSize) {
+          throw new IOException("File is too large");
+        }
+        if (Thread.interrupted()) {
+          return null;
+        }
+      }
+      return byteArrayStream.toByteArray();
+    }
+    // Closing a ByteArrayInputStream has no effect, so I do not close it.
+  }
+
+  public static InputStream DownloadToInputStream(URL url, int maxSize) throws KeyManagementException, NoSuchAlgorithmException, IOException {
+    return new ByteArrayInputStream(DownloadToByteArray(url, maxSize));
+  }
+}
diff --git a/src/main/java/land/chipmunk/chipmunkmod/util/Hexadecimal.java b/src/main/java/land/chipmunk/chipmunkmod/util/Hexadecimal.java
index a9b95b4..e71c25b 100644
--- a/src/main/java/land/chipmunk/chipmunkmod/util/Hexadecimal.java
+++ b/src/main/java/land/chipmunk/chipmunkmod/util/Hexadecimal.java
@@ -1,15 +1,15 @@
 package land.chipmunk.chipmunkmod.util;
 
 public interface Hexadecimal {
-  static String encode (byte b) {
-    return "" + Character.forDigit((b >> 4) & 0xF, 16) + Character.forDigit((b & 0xF), 16);
-  }
+    static String encode (byte b) {
+        return "" + Character.forDigit((b >> 4) & 0xF, 16) + Character.forDigit((b & 0xF), 16);
+    }
 
-  static String encode (byte[] array) {
-    StringBuilder sb = new StringBuilder();
-    for (int i = 0; i < array.length; i++) sb.append(encode(array[i]));
-    return sb.toString();
-  }
+    static String encode (byte[] array) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < array.length; i++) sb.append(encode(array[i]));
+        return sb.toString();
+    }
 
-  // TODO: Decode
+    // TODO: Decode
 }
diff --git a/src/main/resources/chipmunkmod.mixins.json b/src/main/resources/chipmunkmod.mixins.json
index 4156942..8bd5556 100644
--- a/src/main/resources/chipmunkmod.mixins.json
+++ b/src/main/resources/chipmunkmod.mixins.json
@@ -3,16 +3,17 @@
   "minVersion": "0.8",
   "package": "land.chipmunk.chipmunkmod.mixin",
   "compatibilityLevel": "JAVA_17",
-  "mixins": [
-  ],
   "client": [
-    "ChatScreenMixin",
+    "ChatHudMixin",
     "ChatInputSuggestorMixin",
-    "ClientPlayNetworkHandlerAccessor",
+    "ChatScreenMixin",
     "ClientConnectionMixin",
     "ClientPlayerEntityMixin",
+    "ClientPlayNetworkHandlerAccessor",
     "ClientPlayNetworkHandlerMixin",
-    "MinecraftClientAccessor"
+    "MinecraftClientAccessor",
+    "LightmapTextureManagerMixin",
+    "DecoderHandlerMixin"
   ],
   "injectors": {
     "defaultRequire": 1
diff --git a/src/main/resources/default_config.json b/src/main/resources/default_config.json
index fe65157..2e479e3 100644
--- a/src/main/resources/default_config.json
+++ b/src/main/resources/default_config.json
@@ -16,5 +16,17 @@
     "chipmunk": { "prefix": "'", "key": null },
     "chomens": { "prefix": "*", "key": null },
     "kittycorp": { "prefix": "^", "key": null }
+  },
+
+  "customChat": {
+    "format": {
+      "translate": "chat.type.text",
+      "with": [
+        {
+          "selector": "USERNAME"
+        },
+        "MESSAGE"
+      ]
+    }
   }
-}
\ No newline at end of file
+}
diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json
index 5f0aecf..9d5e143 100644
--- a/src/main/resources/fabric.mod.json
+++ b/src/main/resources/fabric.mod.json
@@ -3,14 +3,15 @@
   "id": "chipmunkmod",
   "version": "${version}",
 
-  "name": "ChipmunkMod",
-  "description": "A utility mod created for free-op servers with minimal or no restrictions",
+  "name": "ChipmunkMod (chayapak's fork)",
+  "description": "My fork of ChipmunkMod",
   "authors": [
-    "_ChipMC_"
+    "_ChipMC_",
+    "chayapak"
   ],
   "contact": {
-    "homepage": "https://chipmunk.land/",
-    "sources": "https://code.chipmunk.land/ChipmunkMC/fabric-mod-mabe"
+    "homepage": "https://chayapak.chipmunk.land/",
+    "sources": "https://code.chipmunk.land/ChomeNS/chipmunkmod"
   },
 
   "license": "CC0-1.0",