Automate the stress testing of a Minecraft server network using bots

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

PureGero cd1be7bf c0f13564

Changed files
+176 -79
src
main
java
com
github
puregero
+5 -2
README.md
··· 1 1 # minecraft-stress-test 2 2 3 - Automate the stress testing of your 1.19.2 Minecraft server with bots. 3 + Automate the stress testing of your 1.19.3 Minecraft server with bots. 4 4 This project will log offline-mode bots into the specified server which will 5 5 fly around and explore the world. 6 6 ··· 55 55 Set the bot count to 1000. This will either connect more bots, or disconnect existing bots as needed. 56 56 57 57 `speed 0.2` 58 - Set the bots' movement speed. The default is 0.2 blocks/tick. 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 9 import java.util.concurrent.CompletableFuture; 10 10 import java.util.concurrent.Executor; 11 11 import java.util.concurrent.TimeUnit; 12 + import java.util.function.Consumer; 12 13 13 14 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")); 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 16 private static final double CENTER_X = Double.parseDouble(System.getProperty("bot.x", "0")); 17 17 private static final double CENTER_Z = Double.parseDouble(System.getProperty("bot.z", "0")); 18 18 private static final boolean LOGS = Boolean.parseBoolean(System.getProperty("bot.logs", "true")); ··· 22 22 23 23 private static final Executor ONE_TICK_DELAY = CompletableFuture.delayedExecutor(50,TimeUnit.MILLISECONDS); 24 24 25 - public static double SPEED = Double.parseDouble(System.getProperty("bot.speed", "0.1")); 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)); 26 29 27 30 public SocketChannel channel; 28 31 private String username; ··· 47 50 48 51 @Override 49 52 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); 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 + }); 57 59 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); 60 + sendPacket(ctx, PacketIds.Serverbound.Login.LOGIN_START, buffer -> { 61 + buffer.writeUtf(username); 62 + buffer.writeBoolean(false); 63 + }); 64 64 } 65 65 66 66 @Override ··· 87 87 private void channelReadLogin(ChannelHandlerContext ctx, FriendlyByteBuf byteBuf) { 88 88 int packetId = byteBuf.readVarInt(); 89 89 90 - if (packetId == 0) { 90 + if (packetId == PacketIds.Clientbound.Login.DISCONNECT) { 91 91 System.out.println(username + " was disconnected during login due to " + byteBuf.readUtf()); 92 92 ctx.close(); 93 - } else if (packetId == 2) { 93 + } else if (packetId == PacketIds.Clientbound.Login.LOGIN_SUCCESS) { 94 94 UUID uuid = byteBuf.readUUID(); 95 95 String username = byteBuf.readUtf(); 96 96 loggedIn(ctx, uuid, username); 97 - } else if (packetId == 3) { 97 + } else if (packetId == PacketIds.Clientbound.Login.SET_COMPRESSION) { 98 98 byteBuf.readVarInt(); 99 99 ctx.pipeline().addAfter("packetDecoder", "compressionDecoder", new CompressionDecoder()); 100 100 ctx.pipeline().addAfter("packetEncoder", "compressionEncoder", new CompressionEncoder()); ··· 110 110 loginState = false; 111 111 112 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); 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 + }); 124 123 125 124 CompletableFuture.delayedExecutor(1000, TimeUnit.MILLISECONDS).execute(() -> tick(ctx)); 126 125 }); ··· 166 165 y -= SPEED / 10; 167 166 } 168 167 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); 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 + }); 178 176 } 179 177 180 178 private void channelReadPlay(ChannelHandlerContext ctx, FriendlyByteBuf byteBuf) { 181 179 int packetId = byteBuf.readVarInt(); 182 180 // System.out.println("id 0x" + Integer.toHexString(packetId) + " (" + (dataLength == 0 ? length : dataLength) + ")"); 183 181 184 - if (packetId == 0x19) { 182 + if (packetId == PacketIds.Clientbound.Play.DISCONNECT) { 185 183 System.out.println(username + " (" + uuid + ") was kicked due to " + byteBuf.readUtf()); 186 184 ctx.close(); 187 - } else if (packetId == 0x20) { 185 + } else if (packetId == PacketIds.Clientbound.Play.KEEP_ALIVE) { 188 186 long id = byteBuf.readLong(); 189 187 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) { 188 + sendPacket(ctx, PacketIds.Serverbound.Play.KEEP_ALIVE, buffer -> buffer.writeLong(id)); 189 + } else if (packetId == PacketIds.Clientbound.Play.PING) { 195 190 int id = byteBuf.readInt(); 196 191 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) { 192 + sendPacket(ctx, PacketIds.Serverbound.Play.PONG, buffer -> buffer.writeInt(id)); 193 + } else if (packetId == PacketIds.Clientbound.Play.SYNCHRONIZE_PLAYER_POSITION) { 202 194 double dx = byteBuf.readDouble(); 203 195 double dy = byteBuf.readDouble(); 204 196 double dz = byteBuf.readDouble(); ··· 227 219 if (!goDown) yaw = (float) (Math.random() * 360); 228 220 } 229 221 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) { 222 + sendPacket(ctx, PacketIds.Serverbound.Play.CONFIRM_TELEPORTATION, buffer -> buffer.writeVarInt(id)); 223 + } else if (packetId == PacketIds.Clientbound.Play.RESOURCE_PACK) { 235 224 String url = byteBuf.readUtf(); 236 225 String hash = byteBuf.readUtf(); 237 226 boolean forced = byteBuf.readBoolean(); 238 227 String message = null; 239 228 if (byteBuf.readBoolean()) message = byteBuf.readUtf(); 240 229 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); 230 + 231 + sendPacket(ctx, PacketIds.Serverbound.Play.RESOURCE_PACK, buffer -> buffer.writeVarInt(RESOURCE_PACK_RESPONSE)); 245 232 } 246 233 } 247 234 248 235 public void close() { 249 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); 250 244 } 251 245 }
+60 -17
src/main/java/com/github/puregero/minecraftstresstest/MinecraftStressTest.java
··· 11 11 import java.util.ArrayList; 12 12 import java.util.List; 13 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; 14 19 15 20 public class MinecraftStressTest { 16 21 17 - private static final int BOT_COUNT = Integer.parseInt(System.getProperty("bot.count", "1")); 18 22 private static final String ADDRESS = System.getProperty("bot.ip", "127.0.0.1"); 19 23 private static final int PORT = Integer.parseInt(System.getProperty("bot.port", "25565")); 20 24 private static final int DELAY_BETWEEN_BOTS_MS = Integer.parseInt(System.getProperty("bot.login.delay.ms", "100")); 21 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 + 22 33 private static final EventLoopGroup workerGroup = new NioEventLoopGroup(); 23 34 24 35 public static void main(String[] a) { 25 - List<Bot> bots = new ArrayList<>(); 26 - 27 - updateBotCount(bots, BOT_COUNT); 36 + updateBotCount(); 28 37 29 38 Scanner scanner = new Scanner(System.in); 30 39 ··· 34 43 35 44 try { 36 45 if (args[0].equalsIgnoreCase("count") || args[0].equalsIgnoreCase("botcount")) { 37 - int botCount = Integer.parseInt(args[1]); 46 + int botCount = Math.max(0, Integer.parseInt(args[1])); 38 47 System.out.println("Setting bot count to " + botCount); 39 - updateBotCount(bots, botCount); 48 + BOT_COUNT = botCount; 49 + updateBotCount(); 40 50 } else if (args[0].equalsIgnoreCase("speed")) { 41 - double speed = Double.parseDouble(args[1]); 51 + double speed = Math.max(0.0, Double.parseDouble(args[1])); 42 52 System.out.println("Setting speed to " + speed); 43 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; 44 58 } else { 45 59 System.out.println("Commands:"); 46 - System.out.println("count <number of bots>"); 47 - System.out.println("speed <0.2>"); 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 + ")"); 48 63 } 49 64 } catch (Exception e) { 50 65 e.printStackTrace(); ··· 54 69 System.out.println("stdin ended"); 55 70 } 56 71 57 - private static void updateBotCount(List<Bot> bots, int botCount) { 58 - while (bots.size() > botCount && !bots.isEmpty()) { 59 - bots.remove(bots.size() - 1).close(); 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(); 60 90 } 91 + } 61 92 62 - while (bots.size() < botCount) { 63 - bots.add(connectBot(System.getProperty("bot.name", "Bot") + (bots.size() + 1), ADDRESS, PORT)); 93 + private static void addBotIfNeeded(boolean firstCall) { 94 + if (!firstCall || !addingBots.getAndSet(true)) { 95 + boolean scheduledNextCall = false; 64 96 try { 65 - Thread.sleep(DELAY_BETWEEN_BOTS_MS); 66 - } catch (InterruptedException e) { 67 - e.printStackTrace(); 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 + } 68 111 } 69 112 } 70 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 + }