Added a lot of commands. Rotation to player still crashes the client.

This commit is contained in:
obvWolf
2024-04-28 22:42:22 -05:00
parent 6341bb6e63
commit 0b7bae081c
19 changed files with 1174 additions and 102 deletions

View File

@@ -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

View File

@@ -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";
}
}

View File

@@ -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<CommandSource> builder) {
builder.executes(context -> {
info("hi");
return SINGLE_SUCCESS;
});
}
}

View File

@@ -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<HudExample> 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);
}
}

View File

@@ -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.");
}
}

View File

@@ -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";
}
}

View File

@@ -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<CommandSource> 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<Connection> 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;
})
)
)
)
);
}
}

View File

@@ -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<String> ip = general.add(new StringSetting.Builder()
.name("address")
.defaultValue("localhost")
.build());
private final Setting<Integer> port = general.add(new IntSetting.Builder()
.name("port")
.defaultValue(25561)
.max(65535)
.min(0)
.build());
private final Setting<Boolean> 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");
}
}

View File

@@ -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<String> ip = general.add(new StringSetting.Builder()
.name("address")
.defaultValue("localhost")
.build()
);
private final Setting<Integer> port = general.add(new IntSetting.Builder()
.name("port")
.defaultValue(25561)
.max(65535)
.min(0)
.build()
);
private final Setting<Boolean> 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();
}
}

View File

@@ -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;
}
}

View File

@@ -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<String> 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;
}
}

View File

@@ -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<String> 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;
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<Connection> 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<Connection> getConnections() {
return connections;
}
}

View File

@@ -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;
}
}

View File

@@ -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 };
}
}

View File

@@ -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;
}
}
}

View File

@@ -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": [