Automate the stress testing of a Minecraft server network using bots
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'MartijnMuijsers-feature/1.19.3-and-more'

PureGero cd1be7bf c0f13564

+176 -79
+5 -2
README.md
··· 1 # minecraft-stress-test 2 3 - Automate the stress testing of your 1.19.2 Minecraft server with bots. 4 This project will log offline-mode bots into the specified server which will 5 fly around and explore the world. 6 ··· 55 Set the bot count to 1000. This will either connect more bots, or disconnect existing bots as needed. 56 57 `speed 0.2` 58 - Set the bots' movement speed. The default is 0.2 blocks/tick.
··· 1 # minecraft-stress-test 2 3 + Automate the stress testing of your 1.19.3 Minecraft server with bots. 4 This project will log offline-mode bots into the specified server which will 5 fly around and explore the world. 6 ··· 55 Set the bot count to 1000. This will either connect more bots, or disconnect existing bots as needed. 56 57 `speed 0.2` 58 + Set the bots' movement speed. The default is 0.1 blocks/tick. 59 + 60 + `radius 600` 61 + Set the radius of the area the bots can move around in. The default is 1000 blocks.
+54 -60
src/main/java/com/github/puregero/minecraftstresstest/Bot.java
··· 9 import java.util.concurrent.CompletableFuture; 10 import java.util.concurrent.Executor; 11 import java.util.concurrent.TimeUnit; 12 13 public class Bot extends ChannelInboundHandlerAdapter { 14 - private static final int PROTOCOL_VERSION = Integer.parseInt(System.getProperty("bot.protocol.version", "760")); // 760 is 1.19.2 https://wiki.vg/Protocol_version_numbers 15 - private static final double RADIUS = Double.parseDouble(System.getProperty("bot.radius", "1000")); 16 private static final double CENTER_X = Double.parseDouble(System.getProperty("bot.x", "0")); 17 private static final double CENTER_Z = Double.parseDouble(System.getProperty("bot.z", "0")); 18 private static final boolean LOGS = Boolean.parseBoolean(System.getProperty("bot.logs", "true")); ··· 22 23 private static final Executor ONE_TICK_DELAY = CompletableFuture.delayedExecutor(50,TimeUnit.MILLISECONDS); 24 25 - public static double SPEED = Double.parseDouble(System.getProperty("bot.speed", "0.1")); 26 27 public SocketChannel channel; 28 private String username; ··· 47 48 @Override 49 public void channelActive(final ChannelHandlerContext ctx) { 50 - FriendlyByteBuf handshakePacket = new FriendlyByteBuf(ctx.alloc().buffer()); 51 - handshakePacket.writeVarInt(0x00); 52 - handshakePacket.writeVarInt(PROTOCOL_VERSION); 53 - handshakePacket.writeUtf(address); 54 - handshakePacket.writeShort(port); 55 - handshakePacket.writeVarInt(2); 56 - ctx.write(handshakePacket); 57 58 - FriendlyByteBuf loginStartPacket = new FriendlyByteBuf(ctx.alloc().buffer()); 59 - loginStartPacket.writeVarInt(0x00); 60 - loginStartPacket.writeUtf(username); 61 - loginStartPacket.writeBoolean(false); 62 - loginStartPacket.writeBoolean(false); 63 - ctx.writeAndFlush(loginStartPacket); 64 } 65 66 @Override ··· 87 private void channelReadLogin(ChannelHandlerContext ctx, FriendlyByteBuf byteBuf) { 88 int packetId = byteBuf.readVarInt(); 89 90 - if (packetId == 0) { 91 System.out.println(username + " was disconnected during login due to " + byteBuf.readUtf()); 92 ctx.close(); 93 - } else if (packetId == 2) { 94 UUID uuid = byteBuf.readUUID(); 95 String username = byteBuf.readUtf(); 96 loggedIn(ctx, uuid, username); 97 - } else if (packetId == 3) { 98 byteBuf.readVarInt(); 99 ctx.pipeline().addAfter("packetDecoder", "compressionDecoder", new CompressionDecoder()); 100 ctx.pipeline().addAfter("packetEncoder", "compressionEncoder", new CompressionEncoder()); ··· 110 loginState = false; 111 112 CompletableFuture.delayedExecutor(1000,TimeUnit.MILLISECONDS).execute(() -> { 113 - FriendlyByteBuf settingsPacket = new FriendlyByteBuf(ctx.alloc().buffer()); 114 - settingsPacket.writeVarInt(0x08); 115 - settingsPacket.writeUtf("en_GB"); 116 - settingsPacket.writeByte(VIEW_DISTANCE); 117 - settingsPacket.writeVarInt(0); 118 - settingsPacket.writeBoolean(true); 119 - settingsPacket.writeByte(0); 120 - settingsPacket.writeVarInt(0); 121 - settingsPacket.writeBoolean(false); 122 - settingsPacket.writeBoolean(true); 123 - ctx.writeAndFlush(settingsPacket); 124 125 CompletableFuture.delayedExecutor(1000, TimeUnit.MILLISECONDS).execute(() -> tick(ctx)); 126 }); ··· 166 y -= SPEED / 10; 167 } 168 169 - FriendlyByteBuf movePacket = new FriendlyByteBuf(ctx.alloc().buffer()); 170 - movePacket.writeVarInt(0x15); 171 - movePacket.writeDouble(x); 172 - movePacket.writeDouble(y); 173 - movePacket.writeDouble(z); 174 - movePacket.writeFloat(yaw); 175 - movePacket.writeFloat(0); 176 - movePacket.writeBoolean(true); 177 - ctx.writeAndFlush(movePacket); 178 } 179 180 private void channelReadPlay(ChannelHandlerContext ctx, FriendlyByteBuf byteBuf) { 181 int packetId = byteBuf.readVarInt(); 182 // System.out.println("id 0x" + Integer.toHexString(packetId) + " (" + (dataLength == 0 ? length : dataLength) + ")"); 183 184 - if (packetId == 0x19) { 185 System.out.println(username + " (" + uuid + ") was kicked due to " + byteBuf.readUtf()); 186 ctx.close(); 187 - } else if (packetId == 0x20) { 188 long id = byteBuf.readLong(); 189 190 - FriendlyByteBuf keepAlivePacket = new FriendlyByteBuf(ctx.alloc().buffer()); 191 - keepAlivePacket.writeVarInt(0x12); 192 - keepAlivePacket.writeLong(id); 193 - ctx.writeAndFlush(keepAlivePacket); 194 - } else if (packetId == 0x2F) { 195 int id = byteBuf.readInt(); 196 197 - FriendlyByteBuf keepAlivePacket = new FriendlyByteBuf(ctx.alloc().buffer()); 198 - keepAlivePacket.writeVarInt(0x20); 199 - keepAlivePacket.writeInt(id); 200 - ctx.writeAndFlush(keepAlivePacket); 201 - } else if (packetId == 0x39) { 202 double dx = byteBuf.readDouble(); 203 double dy = byteBuf.readDouble(); 204 double dz = byteBuf.readDouble(); ··· 227 if (!goDown) yaw = (float) (Math.random() * 360); 228 } 229 230 - FriendlyByteBuf teleportConfirmPacket = new FriendlyByteBuf(ctx.alloc().buffer()); 231 - teleportConfirmPacket.writeVarInt(0x00); 232 - teleportConfirmPacket.writeVarInt(id); 233 - ctx.writeAndFlush(teleportConfirmPacket); 234 - } else if (packetId == 0x3D) { 235 String url = byteBuf.readUtf(); 236 String hash = byteBuf.readUtf(); 237 boolean forced = byteBuf.readBoolean(); 238 String message = null; 239 if (byteBuf.readBoolean()) message = byteBuf.readUtf(); 240 System.out.println("Resource pack info:\n" + url + "\n" + hash + "\n" + forced + "\n" + message); 241 - FriendlyByteBuf resourcePackResponsePacket = new FriendlyByteBuf(ctx.alloc().buffer()); 242 - resourcePackResponsePacket.writeVarInt(0x24); 243 - resourcePackResponsePacket.writeVarInt(RESOURCE_PACK_RESPONSE); 244 - ctx.writeAndFlush(resourcePackResponsePacket); 245 } 246 } 247 248 public void close() { 249 channel.close(); 250 } 251 }
··· 9 import java.util.concurrent.CompletableFuture; 10 import java.util.concurrent.Executor; 11 import java.util.concurrent.TimeUnit; 12 + import java.util.function.Consumer; 13 14 public class Bot extends ChannelInboundHandlerAdapter { 15 + private static final int PROTOCOL_VERSION = Integer.parseInt(System.getProperty("bot.protocol.version", "761")); // 761 is 1.19.3 https://wiki.vg/Protocol_version_numbers 16 private static final double CENTER_X = Double.parseDouble(System.getProperty("bot.x", "0")); 17 private static final double CENTER_Z = Double.parseDouble(System.getProperty("bot.z", "0")); 18 private static final boolean LOGS = Boolean.parseBoolean(System.getProperty("bot.logs", "true")); ··· 22 23 private static final Executor ONE_TICK_DELAY = CompletableFuture.delayedExecutor(50,TimeUnit.MILLISECONDS); 24 25 + public static final String DEFAULT_SPEED = "0.1"; 26 + public static double SPEED = Double.parseDouble(System.getProperty("bot.speed", DEFAULT_SPEED)); 27 + public static final String DEFAULT_RADIUS = "1000"; 28 + public static double RADIUS = Double.parseDouble(System.getProperty("bot.radius", DEFAULT_RADIUS)); 29 30 public SocketChannel channel; 31 private String username; ··· 50 51 @Override 52 public void channelActive(final ChannelHandlerContext ctx) { 53 + sendPacket(ctx, PacketIds.Serverbound.Handshaking.HANDSHAKE, buffer -> { 54 + buffer.writeVarInt(PROTOCOL_VERSION); 55 + buffer.writeUtf(address); 56 + buffer.writeShort(port); 57 + buffer.writeVarInt(2); 58 + }); 59 60 + sendPacket(ctx, PacketIds.Serverbound.Login.LOGIN_START, buffer -> { 61 + buffer.writeUtf(username); 62 + buffer.writeBoolean(false); 63 + }); 64 } 65 66 @Override ··· 87 private void channelReadLogin(ChannelHandlerContext ctx, FriendlyByteBuf byteBuf) { 88 int packetId = byteBuf.readVarInt(); 89 90 + if (packetId == PacketIds.Clientbound.Login.DISCONNECT) { 91 System.out.println(username + " was disconnected during login due to " + byteBuf.readUtf()); 92 ctx.close(); 93 + } else if (packetId == PacketIds.Clientbound.Login.LOGIN_SUCCESS) { 94 UUID uuid = byteBuf.readUUID(); 95 String username = byteBuf.readUtf(); 96 loggedIn(ctx, uuid, username); 97 + } else if (packetId == PacketIds.Clientbound.Login.SET_COMPRESSION) { 98 byteBuf.readVarInt(); 99 ctx.pipeline().addAfter("packetDecoder", "compressionDecoder", new CompressionDecoder()); 100 ctx.pipeline().addAfter("packetEncoder", "compressionEncoder", new CompressionEncoder()); ··· 110 loginState = false; 111 112 CompletableFuture.delayedExecutor(1000,TimeUnit.MILLISECONDS).execute(() -> { 113 + sendPacket(ctx, PacketIds.Serverbound.Play.CLIENT_INFORMATION, buffer -> { 114 + buffer.writeUtf("en_GB"); 115 + buffer.writeByte(VIEW_DISTANCE); 116 + buffer.writeVarInt(0); 117 + buffer.writeBoolean(true); 118 + buffer.writeByte(0); 119 + buffer.writeVarInt(0); 120 + buffer.writeBoolean(false); 121 + buffer.writeBoolean(true); 122 + }); 123 124 CompletableFuture.delayedExecutor(1000, TimeUnit.MILLISECONDS).execute(() -> tick(ctx)); 125 }); ··· 165 y -= SPEED / 10; 166 } 167 168 + sendPacket(ctx, PacketIds.Serverbound.Play.SET_PLAYER_POSITION_AND_ROTATION, buffer -> { 169 + buffer.writeDouble(x); 170 + buffer.writeDouble(y); 171 + buffer.writeDouble(z); 172 + buffer.writeFloat(yaw); 173 + buffer.writeFloat(0); 174 + buffer.writeBoolean(true); 175 + }); 176 } 177 178 private void channelReadPlay(ChannelHandlerContext ctx, FriendlyByteBuf byteBuf) { 179 int packetId = byteBuf.readVarInt(); 180 // System.out.println("id 0x" + Integer.toHexString(packetId) + " (" + (dataLength == 0 ? length : dataLength) + ")"); 181 182 + if (packetId == PacketIds.Clientbound.Play.DISCONNECT) { 183 System.out.println(username + " (" + uuid + ") was kicked due to " + byteBuf.readUtf()); 184 ctx.close(); 185 + } else if (packetId == PacketIds.Clientbound.Play.KEEP_ALIVE) { 186 long id = byteBuf.readLong(); 187 188 + sendPacket(ctx, PacketIds.Serverbound.Play.KEEP_ALIVE, buffer -> buffer.writeLong(id)); 189 + } else if (packetId == PacketIds.Clientbound.Play.PING) { 190 int id = byteBuf.readInt(); 191 192 + sendPacket(ctx, PacketIds.Serverbound.Play.PONG, buffer -> buffer.writeInt(id)); 193 + } else if (packetId == PacketIds.Clientbound.Play.SYNCHRONIZE_PLAYER_POSITION) { 194 double dx = byteBuf.readDouble(); 195 double dy = byteBuf.readDouble(); 196 double dz = byteBuf.readDouble(); ··· 219 if (!goDown) yaw = (float) (Math.random() * 360); 220 } 221 222 + sendPacket(ctx, PacketIds.Serverbound.Play.CONFIRM_TELEPORTATION, buffer -> buffer.writeVarInt(id)); 223 + } else if (packetId == PacketIds.Clientbound.Play.RESOURCE_PACK) { 224 String url = byteBuf.readUtf(); 225 String hash = byteBuf.readUtf(); 226 boolean forced = byteBuf.readBoolean(); 227 String message = null; 228 if (byteBuf.readBoolean()) message = byteBuf.readUtf(); 229 System.out.println("Resource pack info:\n" + url + "\n" + hash + "\n" + forced + "\n" + message); 230 + 231 + sendPacket(ctx, PacketIds.Serverbound.Play.RESOURCE_PACK, buffer -> buffer.writeVarInt(RESOURCE_PACK_RESPONSE)); 232 } 233 } 234 235 public void close() { 236 channel.close(); 237 + } 238 + 239 + public static void sendPacket(ChannelHandlerContext ctx, int packetId, Consumer<FriendlyByteBuf> applyToBuffer) { 240 + FriendlyByteBuf buffer = new FriendlyByteBuf(ctx.alloc().buffer()); 241 + buffer.writeVarInt(packetId); 242 + applyToBuffer.accept(buffer); 243 + ctx.writeAndFlush(buffer); 244 } 245 }
+60 -17
src/main/java/com/github/puregero/minecraftstresstest/MinecraftStressTest.java
··· 11 import java.util.ArrayList; 12 import java.util.List; 13 import java.util.Scanner; 14 15 public class MinecraftStressTest { 16 17 - private static final int BOT_COUNT = Integer.parseInt(System.getProperty("bot.count", "1")); 18 private static final String ADDRESS = System.getProperty("bot.ip", "127.0.0.1"); 19 private static final int PORT = Integer.parseInt(System.getProperty("bot.port", "25565")); 20 private static final int DELAY_BETWEEN_BOTS_MS = Integer.parseInt(System.getProperty("bot.login.delay.ms", "100")); 21 22 private static final EventLoopGroup workerGroup = new NioEventLoopGroup(); 23 24 public static void main(String[] a) { 25 - List<Bot> bots = new ArrayList<>(); 26 - 27 - updateBotCount(bots, BOT_COUNT); 28 29 Scanner scanner = new Scanner(System.in); 30 ··· 34 35 try { 36 if (args[0].equalsIgnoreCase("count") || args[0].equalsIgnoreCase("botcount")) { 37 - int botCount = Integer.parseInt(args[1]); 38 System.out.println("Setting bot count to " + botCount); 39 - updateBotCount(bots, botCount); 40 } else if (args[0].equalsIgnoreCase("speed")) { 41 - double speed = Double.parseDouble(args[1]); 42 System.out.println("Setting speed to " + speed); 43 Bot.SPEED = speed; 44 } else { 45 System.out.println("Commands:"); 46 - System.out.println("count <number of bots>"); 47 - System.out.println("speed <0.2>"); 48 } 49 } catch (Exception e) { 50 e.printStackTrace(); ··· 54 System.out.println("stdin ended"); 55 } 56 57 - private static void updateBotCount(List<Bot> bots, int botCount) { 58 - while (bots.size() > botCount && !bots.isEmpty()) { 59 - bots.remove(bots.size() - 1).close(); 60 } 61 62 - while (bots.size() < botCount) { 63 - bots.add(connectBot(System.getProperty("bot.name", "Bot") + (bots.size() + 1), ADDRESS, PORT)); 64 try { 65 - Thread.sleep(DELAY_BETWEEN_BOTS_MS); 66 - } catch (InterruptedException e) { 67 - e.printStackTrace(); 68 } 69 } 70 }
··· 11 import java.util.ArrayList; 12 import java.util.List; 13 import java.util.Scanner; 14 + import java.util.concurrent.CompletableFuture; 15 + import java.util.concurrent.TimeUnit; 16 + import java.util.concurrent.atomic.AtomicBoolean; 17 + import java.util.concurrent.locks.Lock; 18 + import java.util.concurrent.locks.ReentrantLock; 19 20 public class MinecraftStressTest { 21 22 private static final String ADDRESS = System.getProperty("bot.ip", "127.0.0.1"); 23 private static final int PORT = Integer.parseInt(System.getProperty("bot.port", "25565")); 24 private static final int DELAY_BETWEEN_BOTS_MS = Integer.parseInt(System.getProperty("bot.login.delay.ms", "100")); 25 26 + private static final String DEFAULT_BOT_COUNT = "1"; 27 + private static int BOT_COUNT = Integer.parseInt(System.getProperty("bot.count", DEFAULT_BOT_COUNT)); 28 + 29 + private static final List<Bot> bots = new ArrayList<>(); 30 + private static final Lock botsLock = new ReentrantLock(); 31 + private static final AtomicBoolean addingBots = new AtomicBoolean(); 32 + 33 private static final EventLoopGroup workerGroup = new NioEventLoopGroup(); 34 35 public static void main(String[] a) { 36 + updateBotCount(); 37 38 Scanner scanner = new Scanner(System.in); 39 ··· 43 44 try { 45 if (args[0].equalsIgnoreCase("count") || args[0].equalsIgnoreCase("botcount")) { 46 + int botCount = Math.max(0, Integer.parseInt(args[1])); 47 System.out.println("Setting bot count to " + botCount); 48 + BOT_COUNT = botCount; 49 + updateBotCount(); 50 } else if (args[0].equalsIgnoreCase("speed")) { 51 + double speed = Math.max(0.0, Double.parseDouble(args[1])); 52 System.out.println("Setting speed to " + speed); 53 Bot.SPEED = speed; 54 + } else if (args[0].equalsIgnoreCase("radius")) { 55 + double radius = Math.max(0.0, Double.parseDouble(args[1])); 56 + System.out.println("Setting radius to " + radius); 57 + Bot.RADIUS = radius; 58 } else { 59 System.out.println("Commands:"); 60 + System.out.println("count <number of bots> (Default: " + DEFAULT_BOT_COUNT + ")"); 61 + System.out.println("speed <value> (Default: " + Bot.DEFAULT_SPEED + ")"); 62 + System.out.println("radius <value> (Default: " + Bot.DEFAULT_RADIUS + ")"); 63 } 64 } catch (Exception e) { 65 e.printStackTrace(); ··· 69 System.out.println("stdin ended"); 70 } 71 72 + private static void updateBotCount() { 73 + removeBotsIfNeeded(); 74 + addBotIfNeeded(true); 75 + } 76 + 77 + private static void removeBotsIfNeeded() { 78 + botsLock.lock(); 79 + while (true) { 80 + Bot removedBot; 81 + try { 82 + if (bots.size() <= BOT_COUNT) { 83 + break; 84 + } 85 + removedBot = bots.remove(bots.size() - 1); 86 + } finally { 87 + botsLock.unlock(); 88 + } 89 + removedBot.close(); 90 } 91 + } 92 93 + private static void addBotIfNeeded(boolean firstCall) { 94 + if (!firstCall || !addingBots.getAndSet(true)) { 95 + boolean scheduledNextCall = false; 96 try { 97 + botsLock.lock(); 98 + try { 99 + if (bots.size() < BOT_COUNT) { 100 + bots.add(connectBot(System.getProperty("bot.name", "Bot") + (bots.size() + 1), ADDRESS, PORT)); 101 + CompletableFuture.delayedExecutor(DELAY_BETWEEN_BOTS_MS, TimeUnit.MILLISECONDS).execute(() -> addBotIfNeeded(false)); 102 + scheduledNextCall = true; 103 + } 104 + } finally { 105 + botsLock.unlock(); 106 + } 107 + } finally { 108 + if (!scheduledNextCall) { 109 + addingBots.set(false); 110 + } 111 } 112 } 113 }
+57
src/main/java/com/github/puregero/minecraftstresstest/PacketIds.java
···
··· 1 + package com.github.puregero.minecraftstresstest; 2 + 3 + public final class PacketIds { 4 + private PacketIds() {} 5 + 6 + public static final class Clientbound { 7 + private Clientbound() {} 8 + 9 + public static final class Login { 10 + private Login() {} 11 + public static final int 12 + DISCONNECT = 0x00, 13 + LOGIN_SUCCESS = 0x02, 14 + SET_COMPRESSION = 0x03; 15 + } 16 + 17 + public static final class Play { 18 + private Play() {} 19 + public static final int 20 + DISCONNECT = 0x17, 21 + KEEP_ALIVE = 0x1F, 22 + PING = 0x2E, 23 + SYNCHRONIZE_PLAYER_POSITION = 0x38, 24 + RESOURCE_PACK = 0x3C; 25 + } 26 + 27 + } 28 + 29 + public static final class Serverbound { 30 + private Serverbound() {} 31 + 32 + public static final class Handshaking { 33 + private Handshaking() {} 34 + public static final int 35 + HANDSHAKE = 0x00; 36 + } 37 + 38 + public static final class Login { 39 + private Login() {} 40 + public static final int 41 + LOGIN_START = 0x00; 42 + } 43 + 44 + public static final class Play { 45 + private Play() {} 46 + public static final int 47 + CONFIRM_TELEPORTATION = 0x00, 48 + CLIENT_INFORMATION = 0x07, 49 + KEEP_ALIVE = 0x11, 50 + SET_PLAYER_POSITION_AND_ROTATION = 0x14, 51 + PONG = 0x1F, 52 + RESOURCE_PACK = 0x24; 53 + } 54 + 55 + } 56 + 57 + }