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 { + private static final Collection EXAMPLES = Arrays.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 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 { + private static final Collection EXAMPLES = Arrays.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 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 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 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 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 context) { + final FabricClientCommandSource source = context.getSource(); + + final List 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 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 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 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 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 dispatcher) { + dispatcher.register( + literal("fullbright") + .then( + argument("boolean", bool()) + .executes(FullBrightCommand::set) + ) + ); + } + + public static int set (CommandContext 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 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 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 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 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 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 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 directories = new ArrayList<>(); + final List 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 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 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 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 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 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 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 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 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 dispatcher) { + dispatcher.register( + literal("say") + .then( + argument("message", greedyString()) + .executes(m -> say(m)) + ) + ); + } + + public static int say (CommandContext 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 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 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 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 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 future = new CompletableFuture(); + CompletableFuture 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 loopTasks = new ArrayList<>(); + @Getter private final List 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 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 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 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> 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 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 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 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 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 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 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 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 nbsLayers = new ArrayList<>(); + if (buffer.hasRemaining()) { + for (int i=0; i= 4) { + layer.lock = buffer.get(); + } + layer.volume = buffer.get(); + if (format >= 2) { + layer.stereo = buffer.get(); + } + nbsLayers.add(layer); + } + } + + ArrayList 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 { + 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 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 language = loadJsonStringMap("language.json"); + private static final Map keybinds = loadJsonStringMap("keybinds.json"); + + public static final Pattern ARG_PATTERN = Pattern.compile("%(?:(\\d+)\\$)?(s|%)"); + + private ComponentUtilities () { + } + + private static Map loadJsonStringMap (String name) { + Map 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 entry : json.entrySet()) { + map.put(entry.getKey(), json.get(entry.getKey()).getAsString()); + } + + return map; + } + + private static String getOrReturnKey (Map 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",