25 Commits
Author SHA1 Message Date
CreeperMaxCZ 3311f0889a Merge pull request #73 from Norbiros/fix/chat
Important chat bug fixes
2023-08-14 22:48:38 +02:00
CreeperMaxCZ c05b7ffef4 Merge pull request #72 from Norbiros/feat/use-jline
feat: Switch to JLine.
2023-08-14 22:48:08 +02:00
CreeperMaxCZ d8c2139df6 Merge branch 'main' into feat/use-jline 2023-08-14 22:48:00 +02:00
CreeperMaxCZ 5c12776348 Merge pull request #71 from ShayanGsh/main
Added support for Minecraft 1.20
2023-08-14 22:46:00 +02:00
Norbiros 896c320dd7 fix: Chat bug fixes 2023-08-13 00:22:23 +02:00
Shayan d8cd699012 Merging 2023-08-09 18:01:45 +00:00
Shayan d550c69583 Updated Readme for 1.20 and increased mc-bots' version in gradle 2023-08-09 17:57:06 +00:00
Norbiros 4c239b9b5b feat: Switch to JLine. 2023-08-08 13:13:32 +02:00
Shayan f82eb34588 feat: Support for 1.20! 2023-08-01 20:49:16 +03:30
CreeperMaxCZ ea0efd88bb Merge pull request #69 from Norbiros/main
Proper support fo 1.19.4!
2023-07-15 14:13:36 +02:00
CreeperMaxCZ 2d78e85544 docs: readme - commands 2023-07-15 13:58:13 +02:00
Norbiros 1fbadaba9c feat: Support 1.19.4! 2023-07-02 10:55:34 +02:00
CreeperMaxCZ f8e2f2d95a docs: multiple join commands 2023-06-01 13:01:15 +02:00
CreeperMaxCZ d205bd7923 feat: multiple join commands 2023-06-01 12:09:50 +02:00
CreeperMaxCZ f5503b8fc0 feat: leave command with limit 2022-11-16 22:55:12 +01:00
CreeperMaxCZ 6473d53fd7 feat: command: leave - disconnect bots 2022-11-16 22:40:20 +01:00
CreeperMaxCZ 07445c06b4 feat: command: list bots 2022-11-16 21:50:46 +01:00
CreeperMaxCZ d441647ff8 fix: useless prompt rewriting 2022-11-16 19:16:14 +01:00
CreeperMaxCZ c3830fc86b feat: commands: select bots to control 2022-11-16 19:08:25 +01:00
CreeperMaxCZ d6f0adfd57 fix: added argument placeholders in option list in readme 2022-10-08 23:09:06 +02:00
CreeperMaxCZ 6092a5f8f9 feat: auto respawn with delay 2022-10-08 23:03:33 +02:00
CreeperMaxCZ 5346989a92 feat: online mode account support 2022-10-08 22:18:18 +02:00
CreeperMaxCZ 4cd46e8cac Update issue templates 2022-08-29 14:23:52 +02:00
CreeperMaxCZ 8db9603313 Update issue templates 2022-08-29 14:22:40 +02:00
CreeperMaxCZ 48bef04212 Update README.md 2022-08-25 18:46:42 +02:00
10 changed files with 416 additions and 142 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.20 (supports 1.20.x).
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.
+3 -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.8'
sourceCompatibility = 1.8 sourceCompatibility = 1.8
targetCompatibility = 1.8 targetCompatibility = 1.8
@@ -16,11 +16,12 @@ repositories {
} }
dependencies { dependencies {
implementation 'com.github.steveice10:mcprotocollib:1.19.1-SNAPSHOT' implementation 'com.github.steveice10:mcprotocollib:1.20-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'
implementation 'org.slf4j:slf4j-nop:1.7.32' implementation 'org.slf4j:slf4j-nop:1.7.32'
implementation 'org.jline:jline:3.22.0'
} }
jar { jar {
Vendored Regular → Executable
View File
File mode changed.
+77 -22
View File
@@ -1,11 +1,17 @@
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.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;
@@ -17,8 +23,7 @@ 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.*;
import java.util.UUID;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -30,24 +35,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 +78,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,19 +99,33 @@ 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");
//Log.info(" -> " + event.getReason()); // Do not write disconnect reason if disconnected by command
if (!manualDisconnecting) {
//fix broken reason string by finding the content with regex // Fix broken reason string by finding the content with regex
Pattern pattern = Pattern.compile("content=\"(.*?)\""); Pattern pattern = Pattern.compile("content=\"(.*?)\"");
Matcher matcher = pattern.matcher(event.getReason()); Matcher matcher = pattern.matcher(String.valueOf(event.getReason()));
StringBuilder reason = new StringBuilder(); StringBuilder reason = new StringBuilder();
while (matcher.find()) { while (matcher.find()) {
@@ -95,9 +136,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();
} }
}); });
@@ -115,24 +164,24 @@ public class Bot extends Thread {
client.send(new ServerboundChatCommandPacket( client.send(new ServerboundChatCommandPacket(
text.substring(1), text.substring(1),
timeStamp, timeStamp,
0L,
Collections.emptyList(),
0, 0,
new ArrayList<>(), new BitSet()
true,
new ArrayList<>(),
null
)); ));
} else { } else {
// Send chat message // Send chat message
// From 1.19.1 or 1.19, the ServerboundChatPacket needs timestamp, salt and signed signature to generate packet. // From 1.19.1 or 1.19, the ServerboundChatPacket needs timestamp, salt and signed signature to generate packet.
// tmpSignature will provide an empty byte array that can pretend it as signature. // tmpSignature will provide an empty byte array that can pretend it as signature.
// salt is set 0 since this is offline server and no body will check it. // salt is set 0 since this is offline server and no body will check it.
client.send(new ServerboundChatPacket(text,
timeStamp, client.send(new ServerboundChatPacket(
text,
Instant.now().toEpochMilli(),
0L,
null,
0, 0,
new byte[0], new BitSet()
true,
new ArrayList<>(),
null
)); ));
} }
} }
@@ -174,4 +223,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) {
+239 -12
View File
@@ -1,8 +1,17 @@
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.*;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.UserInterruptException;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.xbill.DNS.Lookup; import org.xbill.DNS.Lookup;
import org.xbill.DNS.Record; import org.xbill.DNS.Record;
import org.xbill.DNS.SRVRecord; import org.xbill.DNS.SRVRecord;
@@ -11,10 +20,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 +36,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 +48,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 +72,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 +81,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 +96,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 +193,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 +269,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 +346,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);
@@ -310,16 +395,131 @@ public class Main {
}, 1000L, 500L); }, 1000L, 500L);
} }
Terminal terminal = TerminalBuilder.builder().build();
LineReader lineReader = LineReaderBuilder.builder()
.terminal(terminal)
.parser(new org.jline.reader.impl.DefaultParser())
.build();
Scanner scanner = new Scanner(System.in); prompt = "ALL";
while (scanner.hasNextLine()) { while (true) {
String line = scanner.nextLine(); String line;
if (line.isEmpty()) continue; try {
line = lineReader.readLine();
} catch (UserInterruptException e) {
System.exit(0);
break;
}
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 +529,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 +556,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";
}
}
} }
@@ -31,9 +31,9 @@ public class MainListener implements SessionListener {
// Sometimes the message's body gets null. // Sometimes the message's body gets null.
// For example, some commands like /say makes the message content as null. // For example, some commands like /say makes the message content as null.
// However, the message exists as in getMessagePlain(), thus can retrieve message using the method. // However, the message exists as in getMessagePlain(), thus can retrieve message using the method.
if (message == null) { // When this message was null. if (message == null) {
Log.chat(Utils.getFullText((TextComponent) sender, clientboundPlayerChatPacket.getMessagePlain(), Main.coloredChat)); Log.chat(Utils.getFullText((TextComponent) sender, Component.text(clientboundPlayerChatPacket.getContent()), Main.coloredChat));
} else { // When message exists. } else {
Log.chat(Utils.getFullText((TextComponent) sender, (TextComponent) message, Main.coloredChat)); Log.chat(Utils.getFullText((TextComponent) sender, (TextComponent) message, Main.coloredChat));
} }
chatPrintedOut = true; chatPrintedOut = true;
+22 -96
View File
@@ -7,11 +7,10 @@ import com.google.gson.JsonParser;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.format.TextDecoration;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.w3c.dom.Text;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@@ -28,6 +27,7 @@ public class Utils {
public static String getFullText(TextComponent message, boolean colored) { public static String getFullText(TextComponent message, boolean colored) {
if (message.children().size() > 0) { if (message.children().size() > 0) {
StringBuilder text = new StringBuilder(); StringBuilder text = new StringBuilder();
text.append(parseColor(message, colored));
for (Component child : message.children()) { for (Component child : message.children()) {
if (child instanceof TextComponent) { if (child instanceof TextComponent) {
text.append(getFullText((TextComponent) child, colored)); text.append(getFullText((TextComponent) child, colored));
@@ -38,15 +38,7 @@ public class Utils {
} }
return text.toString(); return text.toString();
} else { } else {
String out = message.content(); return parseColor(message, colored);
if (colored) {
if (message.style().color() != null) {
List<Attribute> formats = new ArrayList<>(getFormats(message.style().decorations()));
formats.add(getColor((NamedTextColor) message.style().color()));
out = colorize(out, formats.toArray(new Attribute[0]));
}
}
return out;
} }
} }
@@ -55,36 +47,35 @@ public class Utils {
* From 1.19.1 the protocol does not include sender's information in the message. * From 1.19.1 the protocol does not include sender's information in the message.
* Thus, we need to merge two TextComponent (sender and message) and generate a text that consists of sent message. * Thus, we need to merge two TextComponent (sender and message) and generate a text that consists of sent message.
* This means using old getFullText will just send the message. Which does NOT contain username who sent this message * This means using old getFullText will just send the message. Which does NOT contain username who sent this message
*
* @param sender The TextComponent for sender. * @param sender The TextComponent for sender.
* @param message The TextComponent for message. * @param message The TextComponent for message.
* @param colored Whether if this output was colored or not. * @param colored Whether if this output was colored or not.
* @return Returns String in "Username : Message" format. * @return Returns String in "Username : Message" format.
*/ */
public static String getFullText(@Nullable TextComponent sender, TextComponent message, boolean colored) { public static String getFullText(@Nullable TextComponent sender, TextComponent message, boolean colored) {
if (colored) { // Check if we are using colors. if (colored) {
Style messageStyle = message.style(); Style messageStyle = message.style();
StringBuilder outString = new StringBuilder(); StringBuilder outString = new StringBuilder();
if (sender != null) { if (sender != null) {
Style senderStyle = sender.style(); Style senderStyle = sender.style();
if (senderStyle.color() != null) { // Generate sender text with color. if (senderStyle.color() != null) { // Generate sender text with color.
NamedTextColor color = (NamedTextColor) senderStyle.color(); outString.append(colorizeText(sender.content(), senderStyle.color()));
outString.append(colorize(sender.content(), getColor(color)));
} else { } else {
outString.append(sender.content()); outString.append(sender.content());
} }
outString.append(" : "); // Add delimiter as : between sender and message. outString.append(" : "); // Add delimiter as : between sender and message.
} }
if (messageStyle.color() != null) { // Generate message text with color. if (messageStyle.color() != null && sender != null) { // Generate message text with color.
NamedTextColor color = (NamedTextColor) messageStyle.color(); outString.append(colorizeText(sender.content(), messageStyle.color()));
outString.append(colorize(message.content(), getColor(color)));
} else { } else {
outString.append(message.content()); outString.append(message.content());
} }
return outString.toString(); return outString.toString();
} else { // When color formatting was disabled. } else {
if (sender == null) if (sender == null)
return message.content(); return message.content();
else else
@@ -92,83 +83,21 @@ public class Utils {
} }
} }
/** private static String parseColor(TextComponent message, boolean colored) {
* A static function that gets full text from text components. String out = message.content();
* From 1.19.1 the protocol does not include sender's information in the message. if (colored) {
* Sometimes the message as component is null. When this happens, this method will show message as String. if (message.style().color() != null) {
* Since this is just a String being represented, this will not have formatting on String. List<Attribute> formats = new ArrayList<>(getFormats(message.style().decorations()));
* @param sender The TextComponent for sender. TextColor color = message.style().color();
* @param message The TextComponent for message. formats.add(Attribute.TEXT_COLOR(color.red(), color.green(), color.blue()));
* @param colored Whether if this output was colored or not. out = colorize(out, formats.toArray(new Attribute[0]));
* @return Returns String in "Username : Message" format.
*/
public static String getFullText(@Nullable TextComponent sender, String message, boolean colored) {
if (colored) { // Check if we are using colors.
StringBuilder outString = new StringBuilder();
if (sender != null) {
Style senderStyle = sender.style();
if (senderStyle.color() != null) { // Generate sender text with color.
NamedTextColor color = (NamedTextColor) senderStyle.color();
outString.append(colorize(sender.content(), getColor(color)));
} else {
outString.append(sender.content());
}
outString.append(" : "); // Add delimiter as : between sender and message.
}
outString.append(message);
return outString.toString();
} else { // When color formatting was disabled.
if (sender == null)
return message;
else
return sender.content() + " : " + message;
} }
} }
return out;
public static Attribute getColor(NamedTextColor color) {
switch (color.toString()) {
case "red":
return Attribute.BRIGHT_RED_TEXT();
case "dark_red":
return Attribute.RED_TEXT();
case "blue":
return Attribute.BRIGHT_BLUE_TEXT();
case "dark_blue":
return Attribute.BLUE_TEXT();
case "white":
return Attribute.BRIGHT_WHITE_TEXT();
case "black":
return Attribute.BLACK_TEXT();
case "gray":
return Attribute.WHITE_TEXT();
case "dark_gray":
return Attribute.BRIGHT_BLACK_TEXT();
case "aqua":
return Attribute.BRIGHT_CYAN_TEXT();
case "dark_aqua":
return Attribute.CYAN_TEXT();
case "yellow":
return Attribute.BRIGHT_YELLOW_TEXT();
case "gold":
return Attribute.YELLOW_TEXT();
case "green":
return Attribute.BRIGHT_GREEN_TEXT();
case "dark_green":
return Attribute.GREEN_TEXT();
case "purple":
return Attribute.BRIGHT_MAGENTA_TEXT();
case "dark_purple":
return Attribute.MAGENTA_TEXT();
} }
return Attribute.NONE();
private static String colorizeText(String text, TextColor color) {
return colorize(text, Attribute.TEXT_COLOR(color.red(), color.green(), color.blue()));
} }
public static List<Attribute> getFormats(Map<TextDecoration, TextDecoration.State> decorations) { public static List<Attribute> getFormats(Map<TextDecoration, TextDecoration.State> decorations) {
@@ -191,7 +120,6 @@ public class Utils {
return list; return list;
} }
public static String translate(TranslatableComponent message) { public static String translate(TranslatableComponent message) {
if (lang == null) { if (lang == null) {
try { try {
@@ -217,8 +145,7 @@ public class Utils {
for (Component component : message.args()) { for (Component component : message.args()) {
if (component instanceof TranslatableComponent) { if (component instanceof TranslatableComponent) {
placeholders.add(translate((TranslatableComponent) component)); placeholders.add(translate((TranslatableComponent) component));
} } else if (component instanceof TextComponent) {
else if (component instanceof TextComponent) {
placeholders.add(getFullText((TextComponent) component, false)); placeholders.add(getFullText((TextComponent) component, false));
} }
} }
@@ -228,5 +155,4 @@ public class Utils {
return base; return base;
} }
} }
} }