14 Commits
7 changed files with 372 additions and 26 deletions

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshot or app console output**
If applicable, add screenshots to help explain your problem.
**Whole command used to start the app**
Please insert the whole command you used to start the app.
Censoring ip address is recommended.
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+20 -7
View File
@@ -1,15 +1,18 @@
# Minecraft bot stress tester # Minecraft bot stress tester
🤖 A simple open source app written in Java used for stress testing Minecraft servers with bots (fake players). 🤖 A simple open source app written in Java used for stress testing Minecraft servers with bots (fake players).
💥 It can be also used to test plugins or minigames. 💥 It can be also used to test plugins or minigames.
✔️ The MC version of the bots is 1.19.1 (and 1.19.2) ✔️ The MC version of the bots is 1.19.1 (supports 1.19.2).
For older MC versions please look in the [releases](https://github.com/crpmax/mc-bots/releases " releases").
## 🆒 Features ## 🆒 Features
- ✅ Connect as many bots as you want - ✅ Connect as many bots as you want
- ✅ Use SOCKS4 or SOCKS5 proxies from file or URL - ✅ Use SOCKS4 or SOCKS5 proxies from file or URL
- ✅ Receive colored or noncolored chat - ✅ Receive colored or noncolored chat
- ✅ Set connection delay - ✅ Set connection delay
- ✅ Set message or command on join - ✅ Set messages or commands on join
- ✅ Generate random or real looking nicknames or load from file - ✅ Generate random or real looking nicknames or load from file
- ✅ Online (premium) account support with login using Microsoft OAuth - only for migrated accounts
- ✅ Control all or selected bots
## 📖 Usage ## 📖 Usage
Minimal Java version: 8 Minimal Java version: 8
@@ -18,7 +21,7 @@ Use of pre-compiled jar from [releases](https://github.com/crpmax/mc-bots/releas
When running, you can write a chat message to the terminal to send it by all bots. When running, you can write a chat message to the terminal to send it by all bots.
## 🧪 Example ## 🧪 Example
`java -jar mc-bots-1.2.4.jar -s 192.168.0.189:25565 -p BOT_ -d 4000 5000 -c 30 -r` `java -jar mc-bots-1.2.6.jar -s 192.168.0.189:25565 -p BOT_ -d 4000 5000 -c 30 -r`
This will connect 30 bots to server at 192.168.0.189:25565 with delay 4000-5000 ms and will use real-looking nicknames prefixed with BOT_ This will connect 30 bots to server at 192.168.0.189:25565 with delay 4000-5000 ms and will use real-looking nicknames prefixed with BOT_
<img src="https://imgur.com/XWcckas.png" title="Connected bots" width="350px"/> <img src="https://imgur.com/XWcckas.png" title="Connected bots" width="350px"/>
@@ -28,16 +31,26 @@ This will connect 30 bots to server at 192.168.0.189:25565 with delay 4000-5000
## ⚡ Options ## ⚡ Options
`-c <count>` The count of bots to connect, default is 1 `-c <count>` The count of bots to connect, default is 1
`-d <min> <max>` Set the minimum and maximum connection delay range in ms, default is 4000-5000 `-d <min> <max>` Set the minimum and maximum connection delay range in ms, default is 4000-5000
`-j <message>` The message or command to send on join, does not work with `-m` or `-x` option `-j <message>` Messages or commands to send on join, separated by `&&`, does not work with `-m` or `-x` option
`-p <prefix>` Custom bot nickname prefix eg. `BOT_` `-p <prefix>` Custom bot nickname prefix eg. `BOT_`
`-r` Generate real looking nicknames instead of random ones `-r` Generate real looking nicknames instead of random ones
`-n` Do not use color in terminal - useful when the terminal does not support it `-n` Do not use color in terminal - useful when the terminal does not support it
`-m` Minimal run - do not use any listeners, will not receive chat, useful for large amounts of bots `-m` Minimal run - do not use any listeners, will not receive chat, useful for large amounts of bots
`-x` The most minimal run - No listeners, no control, no chat - useful for large amounts of bots for better performance `-x` The most minimal run - No listeners, no control, no chat - useful for large amounts of bots for better performance
`-t` Set proxy type - SOCKS4 or SOCKS5 `-t <type>` Set proxy type - SOCKS4 or SOCKS5
`-l` Set proxy list file `-l <path>` Set proxy list file
`-g` Try to simulate gravity by falling down `-g` Try to simulate gravity by falling down
`--nicks` Set nicknames file `-o` Use online (premium) account (login with Microsoft OAuth)
`-ar <delay>` Set auto-respawn delay (default is 100 ms, set to -1 to disable)
`--nicks <file>` Set nicknames file
## Commands
Commands can be typed to the console. They are prefixed with `.` or `!`.
Without the prefix it will be sent as a chat message!
`.list` or `.ls` - list all connected bots
`.control <nick>` or `.ctrl <nick>` - select one or multiple bots to control
`.exit <limit>` or `.leave <limit>` - disconnect all or specified number of bots
## ⚠ DISCLAIMER ## ⚠ DISCLAIMER
**This app is made for educational and testing purposes only. **This app is made for educational and testing purposes only.
+2 -2
View File
@@ -3,7 +3,7 @@ plugins {
} }
group 'me.creepermaxcz.mc-bots' group 'me.creepermaxcz.mc-bots'
version '1.2.5' version '1.2.6'
sourceCompatibility = 1.8 sourceCompatibility = 1.8
targetCompatibility = 1.8 targetCompatibility = 1.8
@@ -16,7 +16,7 @@ repositories {
} }
dependencies { dependencies {
implementation 'com.github.steveice10:mcprotocollib:1.19.1-SNAPSHOT' implementation 'com.github.steveice10:mcprotocollib:1.19.2-1'
implementation 'commons-cli:commons-cli:1.5.0' implementation 'commons-cli:commons-cli:1.5.0'
implementation 'com.diogonunes:JColor:5.2.0' implementation 'com.diogonunes:JColor:5.2.0'
implementation 'dnsjava:dnsjava:3.4.3' implementation 'dnsjava:dnsjava:3.4.3'
+67 -7
View File
@@ -1,11 +1,18 @@
package me.creepermaxcz.mcbots; package me.creepermaxcz.mcbots;
import com.github.steveice10.mc.auth.service.AuthenticationService;
import com.github.steveice10.mc.auth.service.SessionService;
import com.github.steveice10.mc.protocol.MinecraftConstants;
import com.github.steveice10.mc.protocol.MinecraftProtocol; import com.github.steveice10.mc.protocol.MinecraftProtocol;
import com.github.steveice10.mc.protocol.data.game.LastSeenMessage; import com.github.steveice10.mc.protocol.data.UnexpectedEncryptionException;
import com.github.steveice10.mc.protocol.data.game.ClientCommand;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundLoginPacket; import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundLoginPacket;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundRespawnPacket;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.player.ClientboundPlayerCombatKillPacket;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.player.ClientboundPlayerPositionPacket; import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.player.ClientboundPlayerPositionPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundChatCommandPacket; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundChatCommandPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundChatPacket; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundChatPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundClientCommandPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.level.ServerboundAcceptTeleportationPacket; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.level.ServerboundAcceptTeleportationPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.*; import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.*;
import com.github.steveice10.packetlib.ProxyInfo; import com.github.steveice10.packetlib.ProxyInfo;
@@ -18,7 +25,8 @@ import com.github.steveice10.packetlib.tcp.TcpClientSession;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.UUID; import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -30,24 +38,38 @@ public class Bot extends Thread {
private ProxyInfo proxy; private ProxyInfo proxy;
private InetSocketAddress address; private InetSocketAddress address;
private Session client; private Session client;
private UUID uuid;
private boolean hasMainListener; private boolean hasMainListener;
private double lastX, lastY, lastZ = -1; private double lastX, lastY, lastZ = -1;
private boolean connected; private boolean connected;
private boolean manualDisconnecting = false;
public Bot(String nickname, InetSocketAddress address, ProxyInfo proxy) { public Bot(String nickname, InetSocketAddress address, ProxyInfo proxy) {
this.nickname = nickname; this.nickname = nickname;
this.address = address; this.address = address;
this.proxy = proxy; this.proxy = proxy;
this.uuid = UUID.randomUUID(); // Add random UUID for sending message.
Log.info("Creating bot", nickname); Log.info("Creating bot", nickname);
protocol = new MinecraftProtocol(nickname); protocol = new MinecraftProtocol(nickname);
client = new TcpClientSession(address.getHostString(), address.getPort(), protocol, proxy); client = new TcpClientSession(address.getHostString(), address.getPort(), protocol, proxy);
} }
public Bot(AuthenticationService authService, InetSocketAddress address, ProxyInfo proxy) {
this.nickname = authService.getUsername();
this.address = address;
this.proxy = proxy;
Log.info("Creating bot", nickname);
protocol = new MinecraftProtocol(authService.getSelectedProfile(), authService.getAccessToken());
client = new TcpClientSession(address.getHostString(), address.getPort(), protocol, proxy);
SessionService sessionService = new SessionService();
client.setFlag(MinecraftConstants.SESSION_SERVICE_KEY, sessionService);
}
@Override @Override
public void run() { public void run() {
@@ -59,8 +81,16 @@ public class Bot extends Thread {
if (packet instanceof ClientboundLoginPacket) { if (packet instanceof ClientboundLoginPacket) {
connected = true; connected = true;
Log.info(nickname + " connected"); Log.info(nickname + " connected");
if (Main.joinMessage != null) {
sendChat(Main.joinMessage); if (Main.joinMessages.size() > 0) {
for (String msg : Main.joinMessages) {
sendChat(msg);
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
}
} }
} }
else if (packet instanceof ClientboundPlayerPositionPacket) { else if (packet instanceof ClientboundPlayerPositionPacket) {
@@ -72,14 +102,30 @@ public class Bot extends Thread {
client.send(new ServerboundAcceptTeleportationPacket(p.getTeleportId())); client.send(new ServerboundAcceptTeleportationPacket(p.getTeleportId()));
} }
else if (packet instanceof ClientboundPlayerCombatKillPacket){
if (Main.autoRespawnDelay >= 0) {
Log.info("Bot " + nickname + " died. Respawning in " + Main.autoRespawnDelay + " ms.");
new Timer().schedule(
new TimerTask() {
@Override
public void run() {
client.send(new ServerboundClientCommandPacket(ClientCommand.RESPAWN));
}
},
Main.autoRespawnDelay
);
}
}
} }
@Override @Override
public void disconnected(DisconnectedEvent event) { public void disconnected(DisconnectedEvent event) {
connected = false; connected = false;
Log.info();
Log.info(nickname + " disconnected"); Log.info(nickname + " disconnected");
// Do not write disconnect reason if disconnected by command
if (!manualDisconnecting) {
//Log.info(" -> " + event.getReason()); //Log.info(" -> " + event.getReason());
//fix broken reason string by finding the content with regex //fix broken reason string by finding the content with regex
@@ -95,9 +141,17 @@ public class Bot extends Thread {
if(event.getCause() != null) { if(event.getCause() != null) {
event.getCause().printStackTrace(); event.getCause().printStackTrace();
if (event.getCause() instanceof UnexpectedEncryptionException) {
Log.warn("Server is running in online (premium) mode. Please use the -o option to use online mode bot.");
System.exit(1);
}
} }
Log.info(); Log.info();
}
Main.removeBot(Bot.this); Main.removeBot(Bot.this);
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
}); });
@@ -174,4 +228,10 @@ public class Bot extends Thread {
public boolean isConnected() { public boolean isConnected() {
return connected; return connected;
} }
public void disconnect()
{
manualDisconnecting = true;
client.disconnect("Leaving");
}
} }
@@ -9,6 +9,7 @@ public class Log {
public static void log(String in) { public static void log(String in) {
System.out.println("\r[" + formatter.format(new Date()) + "] " + in); System.out.println("\r[" + formatter.format(new Date()) + "] " + in);
System.out.print(Main.getPrompt() + "> ");
} }
public static void error(String in) { public static void error(String in) {
+231 -10
View File
@@ -1,5 +1,9 @@
package me.creepermaxcz.mcbots; package me.creepermaxcz.mcbots;
import com.github.steveice10.mc.auth.exception.request.AuthPendingException;
import com.github.steveice10.mc.auth.service.AuthenticationService;
import com.github.steveice10.mc.auth.service.MsaAuthenticationService;
import com.github.steveice10.mc.auth.util.HTTP;
import com.github.steveice10.mc.protocol.data.status.ServerStatusInfo; import com.github.steveice10.mc.protocol.data.status.ServerStatusInfo;
import com.github.steveice10.packetlib.ProxyInfo; import com.github.steveice10.packetlib.ProxyInfo;
import org.apache.commons.cli.*; import org.apache.commons.cli.*;
@@ -11,10 +15,8 @@ import org.xbill.DNS.Type;
import java.io.*; import java.io.*;
import java.net.*; import java.net.*;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.ArrayList; import java.util.*;
import java.util.Scanner; import java.util.stream.Collectors;
import java.util.Timer;
import java.util.TimerTask;
public class Main { public class Main {
@@ -29,7 +31,11 @@ public class Main {
private static int delayMax = 5000; private static int delayMax = 5000;
private static boolean minimal = false; private static boolean minimal = false;
private static boolean mostMinimal = false; private static boolean mostMinimal = false;
public static String joinMessage; public static ArrayList<String> joinMessages = new ArrayList<>();
public static String prompt = "?";
public static int autoRespawnDelay = 100;
private static boolean useProxies = false; private static boolean useProxies = false;
private static final ArrayList<InetSocketAddress> proxies = new ArrayList<>(); private static final ArrayList<InetSocketAddress> proxies = new ArrayList<>();
@@ -37,7 +43,10 @@ public class Main {
private static int proxyCount = 0; private static int proxyCount = 0;
private static ProxyInfo.Type proxyType; private static ProxyInfo.Type proxyType;
private static final String CLIENT_ID = "8bef943e-5a63-429e-a93a-96391d2e32a9";
private static Timer timer = new Timer(); private static Timer timer = new Timer();
private static final HashSet<Bot> controlledBots = new HashSet<>();
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
@@ -58,7 +67,7 @@ public class Main {
options.addOption("p", "prefix", true, "bot nick prefix"); options.addOption("p", "prefix", true, "bot nick prefix");
options.addOption("m", "minimal", false, "minimal run without any listeners"); options.addOption("m", "minimal", false, "minimal run without any listeners");
options.addOption("x", "most-minimal", false, "minimal run without any control, just connect the bots"); options.addOption("x", "most-minimal", false, "minimal run without any control, just connect the bots");
options.addOption("j", "join-msg", true, "join message / command"); options.addOption("j", "join-msg", true, "join messages / commands, separated by &&");
options.addOption("l", "proxy-list", true, "Path or URL to proxy list file with proxy:port on every line"); options.addOption("l", "proxy-list", true, "Path or URL to proxy list file with proxy:port on every line");
options.addOption("t", "proxy-type", true, "Proxy type: SOCKS4 or SOCKS5"); options.addOption("t", "proxy-type", true, "Proxy type: SOCKS4 or SOCKS5");
@@ -67,6 +76,10 @@ public class Main {
options.addOption("g", "gravity", false, "Try to simulate gravity by falling down"); options.addOption("g", "gravity", false, "Try to simulate gravity by falling down");
options.addOption("o", "online", false, "Use online mode (premium) account");
options.addOption("ar", "auto-respawn", true, "Set autorespawn delay (-1 to disable)");
CommandLineParser parser = new DefaultParser(); CommandLineParser parser = new DefaultParser();
CommandLine cmd = null; CommandLine cmd = null;
@@ -78,6 +91,8 @@ public class Main {
System.exit(1); System.exit(1);
} }
autoRespawnDelay = Integer.parseInt(cmd.getOptionValue("ar", "100"));
if (cmd.hasOption('t') && cmd.hasOption('l')) { if (cmd.hasOption('t') && cmd.hasOption('l')) {
String typeStr = cmd.getOptionValue('t').toUpperCase(); String typeStr = cmd.getOptionValue('t').toUpperCase();
@@ -173,7 +188,14 @@ public class Main {
String address = cmd.getOptionValue('s'); String address = cmd.getOptionValue('s');
coloredChat = !cmd.hasOption('n'); coloredChat = !cmd.hasOption('n');
joinMessage = cmd.getOptionValue('j'); if (cmd.hasOption('j')) {
// Split messages by &&, trim and append to arraylist
String[] messages = cmd.getOptionValue('j').split("&&");
for (String msg : messages) {
joinMessages.add(msg.trim());
}
}
int port = 25565; int port = 25565;
if (address.contains(":")) { if (address.contains(":")) {
@@ -242,7 +264,55 @@ public class Main {
Log.warn("There was an error retrieving server status information. The server may be offline or running on a different version."); Log.warn("There was an error retrieving server status information. The server may be offline or running on a different version.");
} }
AuthenticationService authService = null;
if (cmd.hasOption("o")) {
Log.warn("Online mode enabled. The bot count will be set to 1.");
botCount = 1;
// Create request parameters map
Map<String, String> params = new HashMap<>();
params.put("client_id", CLIENT_ID);
params.put("scope", "XboxLive.signin");
// Send request to Microsoft OAuth api
// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code
URI endpointURI = URI.create("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode");
MsaAuthenticationService.MsCodeResponse response = HTTP.makeRequestForm(
Proxy.NO_PROXY, endpointURI, params, MsaAuthenticationService.MsCodeResponse.class
);
try {
Log.info("Please go to " + response.verification_uri.toURL() + " to authenticate your account - Code: " + response.user_code);
} catch (MalformedURLException e) {
Log.error("Error while trying to get the url of the authentication page");
}
authService = new MsaAuthenticationService(CLIENT_ID, response.device_code);
// Wait for user to login on microsoft page
int retryMax = 20;
while (true) {
try {
authService.login();
break;
} catch (Exception e) {
if (e instanceof AuthPendingException) {
Log.info("Authentication is pending, waiting for user to authenticate...");
try {
Thread.sleep(5000);
} catch (InterruptedException ignored) {
}
if (retryMax == 0)
throw e;
retryMax--;
} else {
throw e;
}
}
}
}
AuthenticationService finalAuthService = authService;
new Thread(() -> { new Thread(() -> {
for (int i = 0; i < botCount; i++) { for (int i = 0; i < botCount; i++) {
try { try {
@@ -271,11 +341,21 @@ public class Main {
} }
Bot bot = new Bot( Bot bot = null;
if (finalAuthService != null) {
bot = new Bot(
finalAuthService,
inetAddr,
proxyInfo
);
} else {
bot = new Bot(
nickGen.nextNick(), nickGen.nextNick(),
inetAddr, inetAddr,
proxyInfo proxyInfo
); );
}
bot.start(); bot.start();
if (!mostMinimal) bots.add(bot); if (!mostMinimal) bots.add(bot);
@@ -312,14 +392,128 @@ public class Main {
Scanner scanner = new Scanner(System.in); Scanner scanner = new Scanner(System.in);
prompt = "ALL";
while (scanner.hasNextLine()) { while (scanner.hasNextLine()) {
String line = scanner.nextLine(); String line = scanner.nextLine();
if (line.isEmpty()) continue;
if (line.isEmpty()) {
System.out.print(getPrompt() + "> ");
continue;
}
if (line.startsWith("!") || line.startsWith(".")) {
String command = line.substring(1);
String[] split = command.split(" ");
String commandName = split[0];
// Bot control selection
if (
commandName.equalsIgnoreCase("control") || commandName.equalsIgnoreCase("ctrl")
) {
if (split.length >= 2) {
controlledBots.clear();
int newBotCount = 0;
// Search for all bot names, from index 1 - skip command name
for (int i = 1; i < split.length; i++) {
String searchedName = split[i];
Bot bot = findBotByName(searchedName);
if (bot != null) {
controlledBots.add(bot);
newBotCount++;
} else {
Log.warn("Bot not found: " + searchedName);
}
}
if (newBotCount > 0) {
// Join bot nicknames
String botNames = controlledBots
.stream()
.map(Bot::getNickname)
.collect(Collectors.joining(", "));
if (newBotCount == 1) {
Log.info("Now controlling 1 bot: " + botNames);
} else {
Log.info("Now controlling " + newBotCount + " bots: " + botNames);
}
} else {
Log.warn("No bots found.");
}
} else {
// If no bot names are supplied, remove all bots
controlledBots.clear();
Log.info("No bots selected - now controlling all bots.");
}
}
// List command
else if (commandName.equalsIgnoreCase("list") || commandName.equalsIgnoreCase("ls"))
{
Log.info("There are " + bots.size() + " bots connected:");
for (Bot bot : bots) {
Log.info(bot.getNickname(), bot.hasMainListener() ? "[MainListener]" : "");
}
}
else if (commandName.equalsIgnoreCase("leave") || commandName.equalsIgnoreCase("exit"))
{
int limit = -1;
if (split.length >= 2) {
try {
limit = Integer.parseInt(split[1]);
} catch (NumberFormatException e) {
limit = 0;
Log.warn("Invalid limit.");
}
}
int i = 0;
if (!controlledBots.isEmpty()) {
Log.info("Disconnecting controlled bots.");
for (Bot bot : controlledBots) {
if (i++ == limit) {
break;
}
bot.disconnect();
}
} else {
Log.info("Disconnecting all bots.");
for (Bot bot : bots) {
if (i++ == limit) {
break;
}
bot.disconnect();
}
}
}
else {
Log.warn("Invalid command");
}
}
else if (controlledBots.size() > 0) {
controlledBots.forEach(bot -> bot.sendChat(line));
}
else {
bots.forEach(bot -> bot.sendChat(line)); bots.forEach(bot -> bot.sendChat(line));
}
Thread.sleep(50); Thread.sleep(50);
//System.out.print("\r" +prompt + "> ");
} }
} }
@@ -329,6 +523,7 @@ public class Main {
public static synchronized void removeBot(Bot bot) { public static synchronized void removeBot(Bot bot) {
bots.remove(bot); bots.remove(bot);
controlledBots.remove(bot);
if (bot.hasMainListener()) { if (bot.hasMainListener()) {
Log.info("Bot with MainListener removed"); Log.info("Bot with MainListener removed");
isMainListenerMissing = true; isMainListenerMissing = true;
@@ -355,4 +550,30 @@ public class Main {
public static boolean isMinimal() { public static boolean isMinimal() {
return minimal; return minimal;
} }
public static Bot findBotByName(String text) {
for (Bot bot : bots) {
// Starts with and ignore case
// https://stackoverflow.com/a/38947571/11787611
if (bot.getNickname().regionMatches(true, 0, text, 0, text.length())) {
return bot;
}
}
return null;
}
public static String getPrompt()
{
int count = controlledBots.size();
if (count == 0) {
return "ALL";
} else if (count == 1)
{
//If controlling only one bot, return its nickname
return controlledBots.iterator().next().getNickname();
}
else {
return count + " BOTS";
}
}
} }