diff --git a/gradle.properties b/gradle.properties index 1438b6a..953a35e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ loader_version=0.15.1 # Mod Properties mod_version=0.1.0 maven_group=com.example -archives_base_name=addon-template +archives_base_name=meteor-butler # Dependencies diff --git a/src/main/java/com/example/addon/Addon.java b/src/main/java/com/example/addon/Addon.java deleted file mode 100644 index bafcd47..0000000 --- a/src/main/java/com/example/addon/Addon.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.example.addon; - -import com.example.addon.commands.CommandExample; -import com.example.addon.hud.HudExample; -import com.example.addon.modules.ModuleExample; -import com.mojang.logging.LogUtils; -import meteordevelopment.meteorclient.addons.MeteorAddon; -import meteordevelopment.meteorclient.commands.Commands; -import meteordevelopment.meteorclient.systems.hud.Hud; -import meteordevelopment.meteorclient.systems.hud.HudGroup; -import meteordevelopment.meteorclient.systems.modules.Category; -import meteordevelopment.meteorclient.systems.modules.Modules; -import org.slf4j.Logger; - -public class Addon extends MeteorAddon { - public static final Logger LOG = LogUtils.getLogger(); - public static final Category CATEGORY = new Category("Example"); - public static final HudGroup HUD_GROUP = new HudGroup("Example"); - - @Override - public void onInitialize() { - LOG.info("Initializing Meteor Addon Template"); - - // Modules - Modules.get().add(new ModuleExample()); - - // Commands - Commands.add(new CommandExample()); - - // HUD - Hud.get().register(HudExample.INFO); - } - - @Override - public void onRegisterCategories() { - Modules.registerCategory(CATEGORY); - } - - @Override - public String getPackage() { - return "com.example.addon"; - } -} diff --git a/src/main/java/com/example/addon/commands/CommandExample.java b/src/main/java/com/example/addon/commands/CommandExample.java deleted file mode 100644 index 94dfdf9..0000000 --- a/src/main/java/com/example/addon/commands/CommandExample.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.addon.commands; - -import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import meteordevelopment.meteorclient.commands.Command; -import net.minecraft.command.CommandSource; - -import static com.mojang.brigadier.Command.SINGLE_SUCCESS; - -public class CommandExample extends Command { - public CommandExample() { - super("example", "Sends a message."); - } - - @Override - public void build(LiteralArgumentBuilder builder) { - builder.executes(context -> { - info("hi"); - return SINGLE_SUCCESS; - }); - } -} diff --git a/src/main/java/com/example/addon/hud/HudExample.java b/src/main/java/com/example/addon/hud/HudExample.java deleted file mode 100644 index 9b2622d..0000000 --- a/src/main/java/com/example/addon/hud/HudExample.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.addon.hud; - -import com.example.addon.Addon; -import meteordevelopment.meteorclient.systems.hud.HudElement; -import meteordevelopment.meteorclient.systems.hud.HudElementInfo; -import meteordevelopment.meteorclient.systems.hud.HudRenderer; -import meteordevelopment.meteorclient.utils.render.color.Color; - -public class HudExample extends HudElement { - public static final HudElementInfo INFO = new HudElementInfo<>(Addon.HUD_GROUP, "example", "HUD element example.", HudExample::new); - - public HudExample() { - super(INFO); - } - - @Override - public void render(HudRenderer renderer) { - setSize(renderer.textWidth("Example element", true), renderer.textHeight(true)); - - renderer.text("Example element", x, y, Color.WHITE, true); - } -} diff --git a/src/main/java/com/example/addon/modules/ModuleExample.java b/src/main/java/com/example/addon/modules/ModuleExample.java deleted file mode 100644 index 2563056..0000000 --- a/src/main/java/com/example/addon/modules/ModuleExample.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.example.addon.modules; - -import com.example.addon.Addon; -import meteordevelopment.meteorclient.systems.modules.Module; - -public class ModuleExample extends Module { - public ModuleExample() { - super(Addon.CATEGORY, "example", "An example module in a custom category."); - } -} diff --git a/src/main/java/me/trouper/butler/Addon.java b/src/main/java/me/trouper/butler/Addon.java new file mode 100644 index 0000000..0c4c7f2 --- /dev/null +++ b/src/main/java/me/trouper/butler/Addon.java @@ -0,0 +1,38 @@ +package me.trouper.butler; + +import com.mojang.logging.LogUtils; +import me.trouper.butler.commands.SwarmManager; +import me.trouper.butler.modules.SwarmPlusMaster; +import me.trouper.butler.modules.SwarmPlusWorker; +import meteordevelopment.meteorclient.addons.MeteorAddon; +import meteordevelopment.meteorclient.commands.Commands; +import meteordevelopment.meteorclient.systems.modules.Category; +import meteordevelopment.meteorclient.systems.modules.Modules; +import org.slf4j.Logger; + +public class Addon extends MeteorAddon { + public static final Logger LOG = LogUtils.getLogger(); + public static final Category CATEGORY = new Category("Butler"); + + @Override + public void onInitialize() { + LOG.info("Initializing Butler Addon for Meteor"); + + // Modules + Modules.get().add(new SwarmPlusMaster()); + Modules.get().add(new SwarmPlusWorker()); + + // Commands + Commands.add(new SwarmManager()); + } + + @Override + public void onRegisterCategories() { + Modules.registerCategory(CATEGORY); + } + + @Override + public String getPackage() { + return "me.trouper.addon"; + } +} diff --git a/src/main/java/me/trouper/butler/commands/SwarmManager.java b/src/main/java/me/trouper/butler/commands/SwarmManager.java new file mode 100644 index 0000000..137d276 --- /dev/null +++ b/src/main/java/me/trouper/butler/commands/SwarmManager.java @@ -0,0 +1,290 @@ +package me.trouper.butler.commands; + +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.DoubleArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import me.trouper.butler.modules.SwarmPlusMaster; +import me.trouper.butler.server.Connection; +import me.trouper.butler.utils.MathUtils; +import meteordevelopment.meteorclient.MeteorClient; +import meteordevelopment.meteorclient.commands.Command; +import meteordevelopment.meteorclient.commands.arguments.ModuleArgumentType; +import meteordevelopment.meteorclient.commands.arguments.PlayerArgumentType; +import meteordevelopment.meteorclient.commands.arguments.SettingArgumentType; +import meteordevelopment.meteorclient.commands.arguments.SettingValueArgumentType; +import meteordevelopment.meteorclient.pathing.PathManagers; +import meteordevelopment.meteorclient.settings.Setting; +import meteordevelopment.meteorclient.systems.modules.Module; +import net.minecraft.client.MinecraftClient; +import net.minecraft.command.CommandSource; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.math.BlockPos; + +import java.awt.geom.Point2D; +import java.util.List; +import java.util.Random; + +import static com.mojang.brigadier.Command.SINGLE_SUCCESS; + +public class SwarmManager extends Command { + public SwarmManager() { + super("manager", "Sends a message."); + } + + @Override + public void build(LiteralArgumentBuilder builder) { + builder.then(literal("chat") + .then(argument("command", StringArgumentType.greedyString()) + .executes(context -> { + String exec = context.getArgument("command", String.class); + if (SwarmPlusMaster.swarmServer == null) { + error("SwarmPlusMaster module is disabled. Start a swarm server to send commands to it!"); + return SINGLE_SUCCESS; + } + SwarmPlusMaster.swarmServer.broadcast("[CHAT] " + exec); + return SINGLE_SUCCESS; + })) + ) + .then(literal("list").executes(context -> { + List connections = SwarmPlusMaster.swarmServer.getConnections().stream().toList(); + StringBuilder connectionList = new StringBuilder(); + int pointer = 0; + for (Connection connection : connections) { + pointer++; + connectionList.append("\n%s: %s on %s".formatted(pointer, connection.getClientSideName(),connection.getAddress())); + } + info("Swarm connections: " + connectionList.toString()); + return SINGLE_SUCCESS; + })) + .then(literal("kick").then(argument("target",StringArgumentType.string()) + .executes(context -> { + String target = context.getArgument("target", String.class); + if (SwarmPlusMaster.swarmServer == null) { + error("SwarmPlusMaster module is disabled. Start a swarm server to send commands to it!"); + return SINGLE_SUCCESS; + } + //info("Looping %s connections".formatted(SwarmPlusMaster.swarmServer.connectionCount())); + + for (Connection connection : SwarmPlusMaster.swarmServer.getConnections()) { + //info("Looping connections: %s user %s".formatted(connection.getAddress(),connection.getClientSideName())); + if (connection.getClientSideName().equals(target)) connection.disconnect(); + //info("Disconnected %s".formatted(target),"DO YOU NEED AN ARGUMENT AGAIN?????"); + } + return SINGLE_SUCCESS; + })) + ) + .then(literal("toggle") + .then(argument("module", ModuleArgumentType.create()) + .executes(context -> { + if (SwarmPlusMaster.swarmServer == null) { + error("SwarmPlusMaster module is disabled. Start a swarm server to send commands to it!"); + return SINGLE_SUCCESS; + } + Module m = ModuleArgumentType.get(context); + SwarmPlusMaster.swarmServer.broadcast("[METEOR] toggle " + m.name); + return SINGLE_SUCCESS; + }).then(literal("on") + .executes(context -> { + if (SwarmPlusMaster.swarmServer == null) { + error("SwarmPlusMaster module is disabled. Start a swarm server to send commands to it!"); + return SINGLE_SUCCESS; + } + Module m = ModuleArgumentType.get(context); + SwarmPlusMaster.swarmServer.broadcast("[METEOR] toggle " + m.name + " on"); + return SINGLE_SUCCESS; + })) + .then(literal("off") + .executes(context -> { + if (SwarmPlusMaster.swarmServer == null) { + error("SwarmPlusMaster module is disabled. Start a swarm server to send commands to it!"); + return SINGLE_SUCCESS; + } + Module m = ModuleArgumentType.get(context); + SwarmPlusMaster.swarmServer.broadcast("[METEOR] toggle " + m.name + " off"); + return SINGLE_SUCCESS; + })) + ) + ) + .then(literal("settings") + .then(argument("module", ModuleArgumentType.create()) + .then(argument("setting", SettingArgumentType.create()) + .then(argument("value", SettingValueArgumentType.create()) + .executes(context -> { + if (SwarmPlusMaster.swarmServer == null) { + error("SwarmPlusMaster module is disabled. Start a swarm server to send commands to it!"); + return SINGLE_SUCCESS; + } + Module module = ModuleArgumentType.get(context); + Setting setting = SettingArgumentType.get(context); + String value = SettingValueArgumentType.get(context); + + SwarmPlusMaster.swarmServer.broadcast("[METEOR] settings %s %s %s".formatted(module.name,setting.name,value)); + ModuleArgumentType.get(context).info("Setting %s changed in %s to %s for all swarm members.", module.title, setting.title, value); + + return SINGLE_SUCCESS; + })) + ) + ) + ) + .then(literal("spread") + .then(argument("radius", IntegerArgumentType.integer(1)) + .executes(context -> { + if (MeteorClient.mc.player == null) { + info("How did we get here?"); + return SINGLE_SUCCESS; + } + int rad = context.getArgument("radius",Integer.class); + int n = SwarmPlusMaster.swarmServer.connectionCount(); + Point2D.Double[] distribution = MathUtils.distributePoints(MeteorClient.mc.player.getX(),MeteorClient.mc.player.getZ(),rad,n); + int index = 0; + for (Connection connection : SwarmPlusMaster.swarmServer.getConnections()) { + int x = (int) Math.round(distribution[index].x); + int z = (int) Math.round(distribution[index].y); + connection.sendMessage("[BARITONE] gotoxz %s %s".formatted(x,z)); + index++; + } + SwarmManager.this.info("Bots moving to a circle with radius (highlight)%s(default).",rad); + return SINGLE_SUCCESS; + })) + ) + .then(literal("here") + .executes(context -> { + int roundX = (int) Math.round(MeteorClient.mc.player.getX()); + int roundY = (int) Math.round(MeteorClient.mc.player.getY()); + int roundZ = (int) Math.round(MeteorClient.mc.player.getZ()); + SwarmPlusMaster.swarmServer.broadcast("[BARITONE] gotoxyz %s %s %s".formatted( + roundX, + roundY, + roundZ + )); + SwarmManager.this.info("Pathing (highlight)all bots(default) to the host."); + return SINGLE_SUCCESS; + }) + .then(argument("target",StringArgumentType.string()) + .executes(context -> { + String target = StringArgumentType.getString(context,"target"); + int roundX = (int) Math.round(MeteorClient.mc.player.getX()); + int roundY = (int) Math.round(MeteorClient.mc.player.getY()); + int roundZ = (int) Math.round(MeteorClient.mc.player.getZ()); + for (Connection connection : SwarmPlusMaster.swarmServer.getConnections().stream().toList()) { + if (!connection.getClientSideName().equalsIgnoreCase(target)) continue; + connection.sendMessage("[BARITONE] gotoxyz %s %s %s".formatted( + roundX, + roundY, + roundZ + )); + SwarmManager.this.info("Pathing (highlight)%s(default) to the host.",target); + return SINGLE_SUCCESS; + } + SwarmManager.this.error("Could not find a connection with the name (highlight)%s",target); + return SINGLE_SUCCESS; + })) + ) + .then(literal("goto") + .then(argument("y", IntegerArgumentType.integer()).executes(context -> { + SwarmPlusMaster.swarmServer.broadcast("[BARITONE] gotoy %s".formatted( + IntegerArgumentType.getInteger(context,"y") + )); + SwarmManager.this.info("Pathing all bots to (highlight)%s", + IntegerArgumentType.getInteger(context,"y") + ); + return SINGLE_SUCCESS; + })) + .then(argument("x",IntegerArgumentType.integer()) + .then(argument("z",IntegerArgumentType.integer()) + .executes(context -> { + SwarmPlusMaster.swarmServer.broadcast("[BARITONE] gotoxz %s %s".formatted( + IntegerArgumentType.getInteger(context,"x"), + IntegerArgumentType.getInteger(context,"z") + )); + SwarmManager.this.info("Pathing all bots to (highlight)%s %s", + IntegerArgumentType.getInteger(context,"x"), + IntegerArgumentType.getInteger(context,"z")); + return SINGLE_SUCCESS; + }))) + .then(argument("x",IntegerArgumentType.integer()) + .then(argument("y",IntegerArgumentType.integer()) + .then(argument("z",IntegerArgumentType.integer()) + .executes(context -> { + SwarmPlusMaster.swarmServer.broadcast("[BARITONE] gotoxyz %s %s %s".formatted( + IntegerArgumentType.getInteger(context,"x"), + IntegerArgumentType.getInteger(context,"y"), + IntegerArgumentType.getInteger(context,"z") + )); + SwarmManager.this.info("Pathing all bots to (highlight)%s %s %s", + IntegerArgumentType.getInteger(context,"x"), + IntegerArgumentType.getInteger(context,"y"), + IntegerArgumentType.getInteger(context,"z") + ); + return SINGLE_SUCCESS; + })))) + ) + .then(literal("rotate") + .then(literal("absolute") + .then(argument("pitch", DoubleArgumentType.doubleArg(0,360)) + .then(argument("yaw", DoubleArgumentType.doubleArg(0,360)) + .executes(context -> { + SwarmPlusMaster.swarmServer.broadcast("[LOOK] absolute %s %s".formatted( + context.getArgument("pitch",double.class), + context.getArgument("yaw",double.class))); + SwarmManager.this.info("Bots now facing (highlight)%s %s", + context.getArgument("pitch",double.class), + context.getArgument("yaw",double.class)); + return SINGLE_SUCCESS; + })) + ) + ) + .then(literal("player") + .then(argument("target",PlayerArgumentType.create()) + .executes(context -> { + SwarmPlusMaster.swarmServer.broadcast("[LOOK] player %s".formatted(context.getArgument("target",String.class))); + SwarmManager.this.info("Bots now targeting (highlight)%s",context.getArgument("target",String.class)); + return SINGLE_SUCCESS; + })) + ) + ) + .then(literal("follow") + .then(argument("player", PlayerArgumentType.create()) + .executes(context -> { + PlayerEntity pe = PlayerArgumentType.get(context); + SwarmPlusMaster.swarmServer.broadcast("[BARITONE] follow %s".formatted(pe.getName().getString())); + SwarmManager.this.info("Bots now following (highlight)%s(default).",pe.getName().getString()); + return SINGLE_SUCCESS; + })) + ) + .then(literal("closegame").executes(context -> { + SwarmPlusMaster.swarmServer.broadcast("[CLOSEGAME]"); + return SINGLE_SUCCESS; + })) + .then(literal("stop").executes(context -> { + SwarmPlusMaster.swarmServer.broadcast("[STOP]"); + return SINGLE_SUCCESS; + })) + .then(literal("raw") + .then(literal("all") + .then(argument("packet",StringArgumentType.greedyString()) + .executes(context -> { + SwarmPlusMaster.swarmServer.broadcast(StringArgumentType.getString(context,"packet")); + return SINGLE_SUCCESS; + }))) + .then(literal("dm") + .then(argument("target",StringArgumentType.string()) + .then(argument("packet",StringArgumentType.greedyString()) + .executes(context -> { + String target = StringArgumentType.getString(context,"target"); + for (Connection connection : SwarmPlusMaster.swarmServer.getConnections().stream().toList()) { + if (!connection.getClientSideName().equalsIgnoreCase(target)) continue; + connection.sendMessage(StringArgumentType.getString(context,"packet")); + return SINGLE_SUCCESS; + } + return SINGLE_SUCCESS; + }) + ) + ) + ) + ); + } + +} diff --git a/src/main/java/me/trouper/butler/modules/SwarmPlusMaster.java b/src/main/java/me/trouper/butler/modules/SwarmPlusMaster.java new file mode 100644 index 0000000..7333197 --- /dev/null +++ b/src/main/java/me/trouper/butler/modules/SwarmPlusMaster.java @@ -0,0 +1,76 @@ +package me.trouper.butler.modules; + +import me.trouper.butler.Addon; +import me.trouper.butler.server.Address; +import me.trouper.butler.server.Server; +import meteordevelopment.meteorclient.settings.*; +import meteordevelopment.meteorclient.systems.modules.Module; + +import java.net.Socket; + +public class SwarmPlusMaster extends Module { + + public static Server swarmServer = null; + + private final SettingGroup general = settings.getDefaultGroup(); + + public SwarmPlusMaster() { + super(Addon.CATEGORY, "swarm-plus-host", "(Host/Master) Control multiple instances of meteor through one account, but better."); + } + + private final Setting ip = general.add(new StringSetting.Builder() + .name("address") + .defaultValue("localhost") + .build()); + + private final Setting port = general.add(new IntSetting.Builder() + .name("port") + .defaultValue(25561) + .max(65535) + .min(0) + .build()); + + private final Setting verbose = general.add(new BoolSetting.Builder() + .name("verbose") + .defaultValue(false) + .build() + ); + + @Override + public void onActivate() { + swarmServer = new Server(new Address(ip.get(),port.get())) { + + @Override + public void onHandShake(Address address, String clientSideName) { + super.onHandShake(address, clientSideName); + SwarmPlusMaster.this.info("%s connected on %s".formatted(clientSideName,address)); + } + + @Override + public synchronized boolean removeConnection(Address address) { + SwarmPlusMaster.this.info("Client disconnecting: " + address); + return super.removeConnection(address); + } + + @Override + protected void info(String str, Object... args) { + super.info(str, args); + if (verbose.get()) SwarmPlusMaster.this.info(str.formatted(args)); + } + + @Override + protected void error(String str, Object... args) { + super.error(str, args); + if (verbose.get()) SwarmPlusMaster.this.info("Error: " + str.formatted(args)); + } + }; + info("Started a new server on IP %s with port %s".formatted(ip,port)); + } + + @Override + public void onDeactivate() { + swarmServer.disconnect(); + info("Closed the swarm server.","THERE YOU GO METEOR HERE IS YOUR ARG NOW DON'T CRASH"); + } + +} diff --git a/src/main/java/me/trouper/butler/modules/SwarmPlusWorker.java b/src/main/java/me/trouper/butler/modules/SwarmPlusWorker.java new file mode 100644 index 0000000..be20bec --- /dev/null +++ b/src/main/java/me/trouper/butler/modules/SwarmPlusWorker.java @@ -0,0 +1,160 @@ +package me.trouper.butler.modules; + +import me.trouper.butler.Addon; +import me.trouper.butler.server.Address; +import me.trouper.butler.server.Client; +import me.trouper.butler.server.Response; +import me.trouper.butler.utils.MathUtils; +import me.trouper.butler.utils.Text; +import meteordevelopment.meteorclient.settings.*; +import meteordevelopment.meteorclient.systems.config.Config; +import meteordevelopment.meteorclient.systems.modules.Module; +import meteordevelopment.meteorclient.utils.player.ChatUtils; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.math.Vec3d; + +import java.util.Arrays; +import java.util.Objects; + +public class SwarmPlusWorker extends Module { + + private Client client = null; + + private final SettingGroup general = settings.getDefaultGroup(); + + public SwarmPlusWorker() { + super(Addon.CATEGORY, "swarm-plus-worker", "(Worker/Client) Control multiple instances of meteor through one account, but better."); + } + + private final Setting ip = general.add(new StringSetting.Builder() + .name("address") + .defaultValue("localhost") + .build() + ); + + private final Setting port = general.add(new IntSetting.Builder() + .name("port") + .defaultValue(25561) + .max(65535) + .min(0) + .build() + ); + + private final Setting verbose = general.add(new BoolSetting.Builder() + .name("verbose") + .defaultValue(false) + .build() + ); + + @Override + public void onActivate() { + client = new Client(new Address(ip.get(),port.get())) { + @Override + protected void info(String str, Object... args) { + super.info(str, args); + String full = str.formatted(args); + if (verbose.get()) SwarmPlusWorker.this.info(full); + String packetType = Text.getPacketType(full); + String packetArgs = Text.getPacketArgs(full); + if (packetType == null) packetType = "ERR"; + switch (packetType) { + case "CHAT" -> { + if (packetArgs == null) { + SwarmPlusWorker.this.error("Chat message returned null. (highlight)%s",full); + return; + } + SwarmPlusWorker.this.info("Received chat command. Sending (highlight)%s".formatted(packetArgs)); + if (MinecraftClient.getInstance().player == null) return; + ChatUtils.sendPlayerMsg(packetArgs); + } + case "METEOR" -> { + SwarmPlusWorker.this.info("Received meteor command (highlight)%s(default).".formatted(packetArgs)); + if (MinecraftClient.getInstance().player == null) return; + ChatUtils.sendPlayerMsg(Config.get().prefix.get() + packetArgs); + } + case "LOOK" -> { + String[] largs = packetArgs.split(" "); + switch (largs[0]) { + case "player" -> { + String target = largs[1]; + for (Entity entity : mc.player.clientWorld.getEntities()) { + if (!(entity instanceof PlayerEntity)) continue; + if (!entity.getName().getString().equalsIgnoreCase(target)) continue; + Vec3d vec = entity.getEyePos().subtract(mc.player.getEyePos()).normalize(); + float[] rot = MathUtils.toPolar(vec.x,vec.y,vec.z); + mc.player.setPitch(rot[0]); + mc.player.setYaw(rot[1]); + return; + } + } + case "absolute" -> { + float pitch = Float.parseFloat(largs[1]); + float yaw = Float.parseFloat(largs[2]); + mc.player.setPitch(pitch); + mc.player.setYaw(yaw); + } + } + } + case "STOP" -> { + SwarmPlusWorker.this.info("Received Stop Command"); + ChatUtils.sendPlayerMsg("#stop"); + } + case "BARITONE" -> { + String[] bargs = packetArgs.split(" "); + if (verbose.get()) SwarmPlusWorker.this.info("Received command addressed to baritone. Full: > (highlight)%s(default) < Arguments: > (highlight)%s(default) <".formatted(full, Arrays.stream(bargs).toList().toString())); + switch (bargs[0]) { + case "gotoxyz" -> { + int x = Integer.parseInt(bargs[1]); + int y = Integer.parseInt(bargs[2]); + int z = Integer.parseInt(bargs[3]); + if (verbose.get()) SwarmPlusWorker.this.info("Received baritone command (highlight)gotoxyz %s %s %s".formatted(x,y,z)); + ChatUtils.sendPlayerMsg("#goto %s %s %s".formatted(x,y,z)); + } + case "gotoxz" -> { + int x = Integer.parseInt(bargs[1]); + int z = Integer.parseInt(bargs[2]); + if (verbose.get()) SwarmPlusWorker.this.info("Received baritone command (highlight)gotoxz %s %s".formatted(x,z)); + ChatUtils.sendPlayerMsg("#goto %s %s".formatted(x,z)); + } + case "gotoy" -> { + int y = Integer.parseInt(bargs[1]); + if (verbose.get()) SwarmPlusWorker.this.info("Received baritone command (highlight)gotoy %s".formatted(y)); + ChatUtils.sendPlayerMsg("#goto %s".formatted(y)); + } + case "follow" -> { + String player = bargs[1]; + if (verbose.get()) SwarmPlusWorker.this.info("Received baritone command (highlight)follow %s".formatted(player)); + ChatUtils.sendPlayerMsg("#follow player %s".formatted(player)); + SwarmPlusWorker.this.info("Following (highlight)%s",player); + } + } + } + case "CLOSEGAME" -> { + SwarmPlusWorker.this.info("Close game call from host!"); + System.exit(0); + } + default -> { + SwarmPlusWorker.this.error("An error occurred when receiving a packet from the host. (highlight)%s",full); + } + } + } + + @Override + protected void error(String str, Object... args) { + super.error(str, args); + if (verbose.get()) SwarmPlusWorker.this.info("Error: " + str.formatted(args)); + } + }; + + client.sendToServer(new Response(Response.Method.TO_SERVER, Response.Type.HANDSHAKE,mc.getSession().getUsername())); + + } + + @Override + public void onDeactivate() { + client.disconnect(); + } + +} diff --git a/src/main/java/me/trouper/butler/server/Address.java b/src/main/java/me/trouper/butler/server/Address.java new file mode 100644 index 0000000..bcce45d --- /dev/null +++ b/src/main/java/me/trouper/butler/server/Address.java @@ -0,0 +1,27 @@ +package me.trouper.butler.server; + +import java.net.Socket; + +public record Address(String ip, int port) { + + public Address(String ip, int port) { + this.ip = "127.0.0.1".equals(ip) ? "localhost" : ip; + this.port = port; + } + + public Address(Socket socket) { + this(socket.getInetAddress().getHostAddress(), socket.getPort()); + } + + @Override + public String toString() { + return ip + ":" + port; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Address ad)) + return false; + return ad.ip.equals(this.ip) && ad.port == this.port; + } +} diff --git a/src/main/java/me/trouper/butler/server/Client.java b/src/main/java/me/trouper/butler/server/Client.java new file mode 100644 index 0000000..eb25caa --- /dev/null +++ b/src/main/java/me/trouper/butler/server/Client.java @@ -0,0 +1,120 @@ +package me.trouper.butler.server; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.net.Socket; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class Client extends ConnectionThread { + + private final Address serverAddress; + private Address serverSideAddress; + private final ConcurrentLinkedQueue queuedMessages; + private Socket socket; + private final UUID id; + + public Client(Address connectingTo) { + this.serverAddress = connectingTo; + this.id = UUID.randomUUID(); + this.setName("[CLIENT:%s]".formatted(id)); + this.queuedMessages = new ConcurrentLinkedQueue<>(); + + try { + info("connecting to server %s ...", connectingTo); + socket = new Socket(connectingTo.ip(), connectingTo.port()); + start(); + info("server connected! (%s)", connectingTo); + new Thread(this::upload, "[CLIENT-UPLOADER:%s]".formatted(id)).start(); + } + catch (Exception ex) { + error("cannot connect to server %s: %s", connectingTo, ex.getMessage()); + } + } + + @Override + public void run() { + try { + DataInputStream dis = new DataInputStream(socket.getInputStream()); + + while (!isInterrupted()) { + onReceiveMessage(dis.readUTF()); + } + + dis.close(); + } + catch (Exception ex) { + error("cannot receive data from server: %s", serverAddress); + } + } + + public void upload() { + try { + DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); + while (!isInterrupted()) { + if (!queuedMessages.isEmpty()) { + String msg = queuedMessages.poll(); + info(msg); + dos.writeUTF(msg); + dos.flush(); + } + } + dos.close(); + } + catch (Exception ex) { + error("error transmitting message: %s", ex.getMessage()); + } + } + + public synchronized void onReceiveMessage(String message) { + if (!Response.isResponsePattern(message)) { + info("message received: %s", message); + return; + } + + try { + Response res = Response.parse(message); + if (res.getMethod() == Response.Method.TO_CLIENT && res.getType() == Response.Type.HANDSHAKE) + this.handeShake(res); + } + catch (Exception ex) { + error("Response parse failure: " + ex.getMessage()); + } + } + + public synchronized void handeShake(Response res) { + String serverSideIp = (String) res.getArgs()[0]; + String serverSidePort = (String) res.getArgs()[1]; + this.serverSideAddress = new Address(serverSideIp, Integer.parseInt(serverSidePort)); + } + + public synchronized void sendToServer(String str) { + if (str != null) + queuedMessages.add(str); + } + + public synchronized void sendToServer(Response res) { + sendToServer(res.toString()); + } + + public void disconnect() { + sendToServer(new Response(Response.Method.TO_SERVER, Response.Type.DEAD_FISH, "disconnected manually")); + + try { + socket.close(); + socket = null; + } + catch (Exception ex) { + error("server connection closed"); + } + interrupt(); + } + + public UUID getUniqueId() { + return id; + } + + public Address getServerSideAddress() { + return serverSideAddress; + } +} diff --git a/src/main/java/me/trouper/butler/server/Connection.java b/src/main/java/me/trouper/butler/server/Connection.java new file mode 100644 index 0000000..d95ff88 --- /dev/null +++ b/src/main/java/me/trouper/butler/server/Connection.java @@ -0,0 +1,120 @@ +package me.trouper.butler.server; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.net.Socket; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class Connection extends ConnectionThread { + + private final ConcurrentLinkedQueue queuedMessages; + private final Socket socket; + private final Server host; + private String clientSideName; + + public Connection(Server host, Socket socket) { + this.socket = socket; + this.host = host; + this.queuedMessages = new ConcurrentLinkedQueue<>(); + this.setName("[CONNECTION:%s]".formatted(getAddress())); + start(); + new Thread(this::receive, "[CONNECTION-RECEIVER:%s]".formatted(getAddress())).start(); + } + + @Override + public void run() { + try { + DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); + while (!isInterrupted()) { + if (!queuedMessages.isEmpty()) { + String msg = queuedMessages.poll(); + info(msg); + dos.writeUTF(msg); + dos.flush(); + } + } + dos.close(); + } + catch (Exception ex) { + error("error transmitting message: %s", ex.getMessage()); + } + } + + public void receive() { + try { + DataInputStream dis = new DataInputStream(socket.getInputStream()); + + while (!isInterrupted()) { + onReceiveMessage(dis.readUTF()); + } + + dis.close(); + } + catch (Exception ex) { + error("cannot receive data from client: %s", getAddress()); + } + deadFish(); + } + + public synchronized void onReceiveMessage(String message) { + if (!Response.isResponsePattern(message)) { + host.info("(%s) message received: %s", getAddress(), message); + return; + } + + try { + Response res = Response.parse(message); + if (res.getMethod() != Response.Method.TO_SERVER) return; + if (res.getType() == Response.Type.DEAD_FISH) + this.deadFish(); + if (res.getType() == Response.Type.HANDSHAKE) { + this.clientSideName = (String) res.getArgs()[0]; + host.onHandShake(getAddress(), clientSideName); + } + } + catch (Exception ex) { + host.error("Response parse failure: " + ex.getMessage()); + } + } + + public synchronized void deadFish() { + host.info("disconnecting %s ...", this.getAddress()); + host.removeConnection(getAddress()); + } + + public void disconnect() { + info("disconnecting from %s ...", getAddress()); + queuedMessages.clear(); + try { + socket.close(); + } + catch (Exception ex) { + error("connection closed"); + } + interrupt(); + } + + public void sendMessage(String str) { + if (str != null) + queuedMessages.add(str); + } + + public void sendMessage(Response res) { + sendMessage(res.toString()); + } + + public Address getAddress() { + return new Address(socket); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Connection conn)) + return false; + return this.getAddress().equals(conn.getAddress()); + } + + public String getClientSideName() { + return clientSideName; + } +} diff --git a/src/main/java/me/trouper/butler/server/ConnectionThread.java b/src/main/java/me/trouper/butler/server/ConnectionThread.java new file mode 100644 index 0000000..5239566 --- /dev/null +++ b/src/main/java/me/trouper/butler/server/ConnectionThread.java @@ -0,0 +1,12 @@ +package me.trouper.butler.server; + +public class ConnectionThread extends Thread { + + protected void error(String str, Object... args) { + System.err.println(getName() + " Error: " + str.formatted(args)); + } + + protected void info(String str, Object... args) { + System.out.println(getName() + " Info: " + str.formatted(args)); + } +} diff --git a/src/main/java/me/trouper/butler/server/Response.java b/src/main/java/me/trouper/butler/server/Response.java new file mode 100644 index 0000000..cbed897 --- /dev/null +++ b/src/main/java/me/trouper/butler/server/Response.java @@ -0,0 +1,111 @@ +package me.trouper.butler.server; + +import java.util.Arrays; + +public class Response { + + private final String id; + private final Method method; + private final Type type; + private final Object[] args; + + public Response(Method method, Type type, Object... args) { + this.method = method; + this.type = type; + this.args = args; + this.id = "%s:%s(%s)".formatted(method.id, type.id, Arrays.toString(args).replaceAll("(^\\[)|(]$)", "")); + } + + public Method getMethod() { + return method; + } + + public Type getType() { + return type; + } + + public Object[] getArgs() { + return args; + } + + @Override + public String toString() { + return id; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Response res)) + return false; + return res.id.equals(this.id); + } + + public static Response parse(String responseString) throws IllegalArgumentException { + try { + String[] split = responseString.split(":"); + String body = split[1]; + String methodStr = split[0].trim(); + String typeStr = body.replaceFirst("\\(.*\\)", "").trim(); + String argsStr = body.replaceFirst("^.*\\(", "").replaceAll("\\)$", "").trim(); + Object[] args = argsStr.split("\s*,\s*"); + + Method method = null; + Type type = null; + + for (var v : Method.values()) { + if (v.id.equals(methodStr)) { + method = v; + break; + } + } + for (var v : Type.values()) { + if (v.id.equals(typeStr)) { + type = v; + break; + } + } + + if (method == null || type == null) + throw new IllegalArgumentException("method or type cannot be null! (provided: %s, %s)".formatted(methodStr, typeStr)); + + return new Response(method, type, args); + } + catch (Exception ex) { + throw new IllegalArgumentException("response syntax is invalid: " + ex.getMessage()); + } + } + + public static boolean isResponsePattern(String str) { + return str.matches(".+:.+\\((.+,?)\\)"); + } + + public enum Method { + TO_SERVER("improperC2S"), + TO_CLIENT("improperS2C"); + + private final String id; + + Method(String id) { + this.id = id; + } + + public String getId() { + return id; + } + } + + public enum Type { + HANDSHAKE("handshake"), + DEAD_FISH("dead_fish"); + + private final String id; + + Type(String id) { + this.id = id; + } + + public String getId() { + return id; + } + } +} diff --git a/src/main/java/me/trouper/butler/server/Server.java b/src/main/java/me/trouper/butler/server/Server.java new file mode 100644 index 0000000..f070de1 --- /dev/null +++ b/src/main/java/me/trouper/butler/server/Server.java @@ -0,0 +1,108 @@ +package me.trouper.butler.server; + +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class Server extends ConnectionThread { + + private final ConcurrentLinkedQueue connections = new ConcurrentLinkedQueue<>(); + private ServerSocket serverSocket; + private final Address address; + + public Server(Address address) { + this.address = address; + this.setName("[SERVER:%s]".formatted(address.port())); + + try { + info("starting server..."); + this.serverSocket = new ServerSocket(address.port()); + start(); + info("server has started on %s:%s", serverSocket.getInetAddress().getHostAddress(), address.port()); + } + catch (Exception ex) { + error("error starting server: %s", ex.getMessage()); + } + } + + @Override + public void run() { + while (!isInterrupted()) { + try { + + Socket socket = serverSocket.accept(); + this.onClientConnect(socket); + } + catch (Exception ex) { + error("cannot handle client connect event: %s", ex.getMessage()); + } + } + } + + protected synchronized void onClientConnect(Socket socket) { + Connection conn = new Connection(this, socket); + connections.add(conn); + Address connAddress = conn.getAddress(); + conn.sendMessage(new Response(Response.Method.TO_CLIENT, Response.Type.HANDSHAKE, connAddress.ip(), connAddress.port())); + } + + public synchronized void broadcast(String str) { + for (var conn : connections) + conn.sendMessage(str); + } + + public synchronized Connection getConnectionOfAddress(Address address) { + for (var conn : connections) + if (conn.getAddress().equals(address)) + return conn; + return null; + } + + public synchronized void dmConnection(Address address, String message) { + Connection con = getConnectionOfAddress(address); + if (con != null) con.sendMessage(message); + } + + public synchronized void dmConnection(Client client, String message) { + if (client != null && client.getServerSideAddress() != null) dmConnection(client.getServerSideAddress(),message); + } + + public synchronized boolean removeConnection(Address address) { + for (var conn : connections) + if (conn.getAddress().equals(address)) + return connections.remove(conn); + return false; + } + + public void disconnect() { + interrupt(); + info("stopping server..."); + + for (var conn : connections) + conn.disconnect(); + connections.clear(); + + try { + serverSocket.close(); + } + catch (Exception ex) { + error("server closed"); + } + } + + public int connectionCount() { + return connections.size(); + } + + public int getPort() { + return address.port(); + } + + public void onHandShake(Address address, String clientSideName) { + + } + + public ConcurrentLinkedQueue getConnections() { + return connections; + } +} diff --git a/src/main/java/me/trouper/butler/utils/CipherUtils.java b/src/main/java/me/trouper/butler/utils/CipherUtils.java new file mode 100644 index 0000000..a3905e8 --- /dev/null +++ b/src/main/java/me/trouper/butler/utils/CipherUtils.java @@ -0,0 +1,35 @@ +package me.trouper.butler.utils; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.util.Base64; + +public class CipherUtils { + private static final String secretKey = "GG8T885O4Yd/86OMVFdL0w=="; // 16, 24, or 32 bytes + private static final String algorithm = "AES"; + public static String encrypt(String strToEncrypt) { + try { + SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(), algorithm); + Cipher cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); + byte[] encryptedBytes = cipher.doFinal(strToEncrypt.getBytes()); + return Base64.getEncoder().encodeToString(encryptedBytes); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static String decrypt(String strToDecrypt) { + try { + SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(), algorithm); + Cipher cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); + byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(strToDecrypt)); + return new String(decryptedBytes); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/src/main/java/me/trouper/butler/utils/MathUtils.java b/src/main/java/me/trouper/butler/utils/MathUtils.java new file mode 100644 index 0000000..3f6ddc6 --- /dev/null +++ b/src/main/java/me/trouper/butler/utils/MathUtils.java @@ -0,0 +1,40 @@ +package me.trouper.butler.utils; + +import java.awt.geom.Point2D; + +public class MathUtils { + + // WARNING! This class contains SIN!!!!! + + public static Point2D.Double[] distributePoints(double centerX, double centerZ, double radius, int n) { + Point2D.Double[] points = new Point2D.Double[n]; + double angleIncrement = 2 * Math.PI / n; + + for (int i = 0; i < n; i++) { + double angle = i * angleIncrement; + double x = centerX + radius * Math.cos(angle); + double z = centerZ + radius * Math.sin(angle); + points[i] = new Point2D.Double(x, z); + } + + return points; + } + + public static float[] toPolar(double x, double y, double z) { + double pi2 = 2 * Math.PI; + float pitch, yaw; + + if (x == 0 && z == 0) { + pitch = y > 0 ? -90 : 90; + return new float[] { pitch, 0.0F }; + } + + double theta = Math.atan2(-x, z); + yaw = (float)Math.toDegrees((theta + pi2) % pi2); + + double xz = Math.sqrt(x * x + z * z); + pitch = (float)Math.toDegrees(Math.atan(-y / xz)); + + return new float[] { pitch, yaw }; + } +} diff --git a/src/main/java/me/trouper/butler/utils/Text.java b/src/main/java/me/trouper/butler/utils/Text.java new file mode 100644 index 0000000..1ea0d0c --- /dev/null +++ b/src/main/java/me/trouper/butler/utils/Text.java @@ -0,0 +1,30 @@ +package me.trouper.butler.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Text { + + public static String getPacketType(String input) { + Pattern pattern = Pattern.compile("\\[(.*?)\\]"); + Matcher matcher = pattern.matcher(input); + + if (matcher.find()) { + return matcher.group(1); + } else { + return null; + } + } + + public static String getPacketArgs(String input) { + Pattern pattern = Pattern.compile("\\[\\s*(.*?)\\s*\\]"); + Matcher matcher = pattern.matcher(input); + + if (matcher.find()) { + int endIndex = matcher.end(); + return input.substring(endIndex).trim(); + } else { + return null; + } + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 207242a..9d70b8c 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,11 +1,12 @@ { "schemaVersion": 1, - "id": "addon-template", + "id": "butler-addon", "version": "${version}", - "name": "Addon Template", - "description": "An addon template for the Meteor addons.", + "name": "Butler Addon", + "description": "Added functionality to the swarm module.", "authors": [ - "seasnail" + "obvWolf", + "ImproperIssues" ], "contact": { "repo": "https://github.com/MeteorDevelopment/meteor-addon-template" @@ -14,7 +15,7 @@ "environment": "client", "entrypoints": { "meteor": [ - "com.example.addon.Addon" + "me.trouper.butler.Addon" ] }, "mixins": [