Skip to content

Commit

Permalink
Added: Client Brand Detector
Browse files Browse the repository at this point in the history
Added: Automatically uses the best crash method based upon target client version
Improved: Hover Component after executing the crash command
Fixed: Wrongly detecting when someone was crashed / not crashed
  • Loading branch information
Bram1903 committed May 17, 2024
1 parent c589fab commit 99aaacf
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 78 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ An easy-to-use Minecraft plugin that enables the crashing of a player's game thr
<img src="docs/showcase/img.png" alt="alt text" height="520">

### Requires PacketEvents

The plugin doesn't shade PacketEvents anymore because of performance considerations,
so you will need to install the latest version of PacketEvents on your server.

Expand All @@ -29,7 +30,8 @@ Technically, it should also work on any Spigot or Paper fork, but I can't guaran
All packet modifications are done asynchronously, so the main thread is never blocked.
- **Folia Support** - The plugin integrates with [Folia](https://papermc.io/software/folia), which is a Paper fork that
adds regionised multithreading to the server.
- **Crash Detector** - By sending both a keep alive and transaction packet, the plugin can detect if a player has crashed, even if the player is still connected.
- **Crash Detector** - By sending both a keep alive and transaction packet, the plugin can detect if a player has
crashed, even if the player is still connected.
- **Configurable** - The plugin is highly configurable, allowing you to adjust the settings to your liking.
- **Update Checker** - The plugin automatically checks for updates on startup.
If a new version is available, a message will be sent to the console.
Expand Down
5 changes: 4 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ tasks {
downloadPlugins.from(requiredPlugins)
downloadPlugins {
url("https://ci.lucko.me/job/spark/410/artifact/spark-bukkit/build/libs/spark-1.10.65-bukkit.jar")
url("https://download.luckperms.net/1530/bukkit/loader/LuckPerms-Bukkit-5.4.117.jar")
url("https://ci.lucko.me/job/LuckPerms/lastBuild/artifact/bukkit/loader/build/libs/LuckPerms-Bukkit-5.4.128.jar")
url("https://github.com/ViaVersion/ViaVersion/releases/download/4.10.2/ViaVersion-4.10.2.jar")
url("https://github.com/ViaVersion/ViaBackwards/releases/download/4.10.2/ViaBackwards-4.10.2.jar")
url("https://github.com/ViaVersion/ViaRewind/releases/download/3.1.2/ViaRewind-3.1.2.jar")
}

jvmArgs = jvmArgsExternal
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
id("com.gradle.enterprise") version("3.16.2")
id("com.gradle.enterprise") version ("3.16.2")
}

rootProject.name = "PlayerCrasher"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.deathmotion.playercrasher;

import com.deathmotion.playercrasher.listeners.BrandHandler;
import com.deathmotion.playercrasher.listeners.TransactionHandler;
import com.deathmotion.playercrasher.managers.ConfigManager;
import com.deathmotion.playercrasher.managers.CrashManager;
Expand Down Expand Up @@ -29,6 +30,7 @@ public void onEnable() {
crashManager = new CrashManager(this);

PacketEvents.getAPI().getEventManager().registerListener(new TransactionHandler(this));
PacketEvents.getAPI().getEventManager().registerListener(new BrandHandler(this));
PacketEvents.getAPI().init();

new UpdateManager(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import com.deathmotion.playercrasher.enums.CrashMethod;
import com.deathmotion.playercrasher.managers.CrashManager;
import com.deathmotion.playercrasher.util.AdventureCompatUtil;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.protocol.player.ClientVersion;
import com.github.retrooper.packetevents.protocol.player.User;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
Expand Down Expand Up @@ -57,7 +60,14 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command
}

CrashMethod method = CrashMethod.EXPLOSION;
if (args.length > 1) {

if (args.length == 1) {
User user = PacketEvents.getAPI().getPlayerManager().getUser(target);
if (user.getClientVersion().isOlderThan(ClientVersion.V_1_12)) {
method = CrashMethod.POSITION;
}
}
else {
try {
method = CrashMethod.valueOf(args[1].toUpperCase());
} catch (IllegalArgumentException e) {
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/deathmotion/playercrasher/events/PlayerQuit.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.deathmotion.playercrasher.events;

import com.deathmotion.playercrasher.PlayerCrasher;
import com.deathmotion.playercrasher.managers.CrashManager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;

public class PlayerQuit implements Listener {

private final CrashManager crashManager;

public PlayerQuit(PlayerCrasher plugin) {
this.crashManager = plugin.getCrashManager();
}

@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) {
crashManager.removeClientBrand(event.getPlayer().getUniqueId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.deathmotion.playercrasher.listeners;

import com.deathmotion.playercrasher.PlayerCrasher;
import com.deathmotion.playercrasher.managers.CrashManager;
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPluginMessage;

public class BrandHandler extends PacketListenerAbstract {

private final CrashManager crashManager;

public BrandHandler(PlayerCrasher plugin) {
crashManager = plugin.getCrashManager();
}

@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (event.getPacketType() != PacketType.Configuration.Client.PLUGIN_MESSAGE) return;
WrapperPlayClientPluginMessage wrapper = new WrapperPlayClientPluginMessage(event);

String channelName = wrapper.getChannelName();
byte[] data = wrapper.getData();

if (!channelName.equalsIgnoreCase("minecraft:brand") && !channelName.equals("MC|Brand")) return;
if (data.length > 64 || data.length == 0) return;

byte[] minusLength = new byte[data.length - 1];
System.arraycopy(data, 1, minusLength, 0, minusLength.length);
String brand = new String(minusLength).replace(" (Velocity)", ""); // removes velocity's brand suffix

crashManager.addClientBrand(event.getUser().getUUID(), prettyBrandName(brand));
}

private String prettyBrandName(String brand) {
if (brand.toLowerCase().contains("lunarclient")) {
return "Lunar Client";
}

return capitalizeFirstLetter(brand);
}

private String capitalizeFirstLetter(String str) {
if (str == null || str.isEmpty()) {
return str;
} else {
return Character.toUpperCase(str.charAt(0)) + str.substring(1);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.deathmotion.playercrasher.managers.CrashManager;
import com.deathmotion.playercrasher.models.CrashData;
import com.deathmotion.playercrasher.util.AdventureCompatUtil;
import com.deathmotion.playercrasher.util.ComponentCreator;
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
Expand All @@ -12,10 +13,6 @@
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientKeepAlive;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPong;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientWindowConfirmation;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;

import java.util.UUID;

public class TransactionHandler extends PacketListenerAbstract {
private final CrashManager crashManager;
Expand Down Expand Up @@ -56,7 +53,7 @@ private void handleKeepAlivePacket(PacketReceiveEvent event, CrashData crashData
if (crashData.getKeepAliveId() != packet.getId()) return;

crashData.setKeepAliveConfirmed(true);
connectionUpdate(event.getUser().getUUID());
connectionUpdate(event.getUser());

event.setCancelled(true);
}
Expand All @@ -66,7 +63,7 @@ private void handlePongPacket(PacketReceiveEvent event, CrashData crashData) {
if (crashData.getKeepAliveId() != packet.getId()) return;

crashData.setTransactionConfirmed(true);
connectionUpdate(event.getUser().getUUID());
connectionUpdate(event.getUser());

event.setCancelled(true);
}
Expand All @@ -76,18 +73,18 @@ private void handleConfirmationPacket(PacketReceiveEvent event, CrashData crashD
if (!packet.isAccepted()) return;

crashData.setTransactionConfirmed(true);
connectionUpdate(event.getUser().getUUID());
connectionUpdate(event.getUser());

event.setCancelled(true);
}

private void connectionUpdate(UUID uuid) {
CrashData crashData = crashManager.getCrashData(uuid).orElse(null);

private void connectionUpdate(User user) {
CrashData crashData = crashManager.getCrashData(user.getUUID()).orElse(null);
if (crashData == null) return;

String brand = crashManager.getClientBrand(user.getUUID()).orElse("Unknown Brand");
if (crashData.isKeepAliveConfirmed() && crashData.isTransactionConfirmed()) {
adventure.sendComponent(crashData.getCrasher(), Component.text("Failed to crash " + crashData.getTarget().getName() + "!", NamedTextColor.RED));
adventure.sendComponent(crashData.getCrasher(), ComponentCreator.createFailedCrashComponent(crashData, brand, user.getClientVersion()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.deathmotion.playercrasher.models.CrashData;
import com.deathmotion.playercrasher.services.CrashService;
import com.deathmotion.playercrasher.util.AdventureCompatUtil;
import com.deathmotion.playercrasher.util.ComponentCreator;
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.manager.server.ServerVersion;
import com.github.retrooper.packetevents.protocol.player.User;
Expand All @@ -13,8 +14,6 @@
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerWindowConfirmation;
import io.github.retrooper.packetevents.util.folia.FoliaScheduler;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
Expand All @@ -36,6 +35,8 @@ public class CrashManager {
private final boolean useLegacyWindowConfirmation;

private final ConcurrentHashMap<UUID, CrashData> crashedPlayers = new ConcurrentHashMap<>();
private final ConcurrentHashMap<UUID, String> clientBrand = new ConcurrentHashMap<>();

private final Random random = new Random();

/**
Expand Down Expand Up @@ -72,6 +73,10 @@ public void crashPlayer(CommandSender sender, Player target, CrashMethod method)
checkConnectionPackets(target.getUniqueId());
}

public void addClientBrand(UUID uuid, String brand) {
clientBrand.put(uuid, brand);
}

public boolean isCrashed(UUID uuid) {
return crashedPlayers.containsKey(uuid);
}
Expand All @@ -80,10 +85,18 @@ public Optional<CrashData> getCrashData(UUID uuid) {
return Optional.ofNullable(crashedPlayers.get(uuid));
}

public Optional<String> getClientBrand(UUID uuid) {
return Optional.ofNullable(clientBrand.get(uuid));
}

public void removeCrashedPlayer(UUID uuid) {
crashedPlayers.remove(uuid);
}

public void removeClientBrand(UUID uuid) {
clientBrand.remove(uuid);
}

private void sendConnectionPackets(Player target, long transactionId) {
FoliaScheduler.getAsyncScheduler().runDelayed(plugin, (o) -> {
User user = PacketEvents.getAPI().getPlayerManager().getUser(target);
Expand All @@ -95,7 +108,7 @@ private void sendConnectionPackets(Player target, long transactionId) {
} else {
user.sendPacket(new WrapperPlayServerPing((int) transactionId));
}
}, 100, TimeUnit.MILLISECONDS);
}, 500, TimeUnit.MILLISECONDS);
}

private void checkConnectionPackets(UUID uuid) {
Expand All @@ -109,66 +122,23 @@ private void checkConnectionPackets(UUID uuid) {
}

removeCrashedPlayer(crashData.getTarget().getUniqueId());
}, 200, TimeUnit.MILLISECONDS);
}, 500, TimeUnit.MILLISECONDS);
}

private void notifyCrashers(CrashData crashData) {
Component notifyComponent = createCrashComponent(crashData);
CommandSender crasher = crashData.getCrasher();
User user = PacketEvents.getAPI().getPlayerManager().getUser(crashData.getTarget());
String brand = getClientBrand(crashData.getTarget().getUniqueId()).orElse("Unknown Brand");
Component notifyComponent = ComponentCreator.createCrashComponent(crashData, brand, user.getClientVersion());

CommandSender crasher = crashData.getCrasher();
if (crasher instanceof Player) {
Bukkit.getOnlinePlayers().stream()
.filter(player -> player.hasPermission("PlayerCrasher.Alerts") || player.getUniqueId().equals(((Player) crasher).getUniqueId()))
.map(player -> PacketEvents.getAPI().getPlayerManager().getUser(player))
.forEach(user -> user.sendMessage(notifyComponent));
.forEach(userStream -> userStream.sendMessage(notifyComponent));
} else {
adventure.broadcastComponent(notifyComponent, "PlayerCrasher.Alerts");
adventure.sendPlainMessage(crasher, notifyComponent);
}
}

/**
* Creates a Component containing detailed information about the crash that can be used in a message.
*
* @param crashData A data model containing details about the crash
* @return A Component with a detailed breakdown of the crash
*/
private Component createCrashComponent(CrashData crashData) {
Component hoveredComponent = Component.text()
.append(Component.text("\u25cf"))
.append(Component.text(" Crash Information", NamedTextColor.GREEN)
.decorate(TextDecoration.BOLD))
.appendNewline()
.appendNewline()
.append(Component.text("Crasher", NamedTextColor.BLUE))
.append(Component.text(" > ", NamedTextColor.GRAY)
.decorate(TextDecoration.BOLD))
.append(Component.text(crashData.getCrasher().getName(), NamedTextColor.GREEN))
.appendNewline()
.append(Component.text("Target", NamedTextColor.BLUE))
.append(Component.text(" > ", NamedTextColor.GRAY)
.decorate(TextDecoration.BOLD))
.append(Component.text(crashData.getTarget().getName(), NamedTextColor.GREEN))
.appendNewline()
.append(Component.text("Method", NamedTextColor.BLUE))
.append(Component.text(" > ", NamedTextColor.GRAY)
.decorate(TextDecoration.BOLD))
.append(Component.text(
crashData.getMethod().toString().substring(0, 1).toUpperCase() +
crashData.getMethod().toString().substring(1).toLowerCase(), NamedTextColor.GREEN))
.appendNewline()
.append(Component.text("Time", NamedTextColor.BLUE))
.append(Component.text(" > ", NamedTextColor.GRAY)
.decorate(TextDecoration.BOLD))
.append(Component.text(crashData.getFormattedDateTime(), NamedTextColor.GREEN))
.appendNewline()
.build();

return Component.text()
.append(Component.text(crashData.getTarget().getName()))
.append(Component.text(" has successfully been crashed!"))
.color(NamedTextColor.GREEN)
.hoverEvent(hoveredComponent)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.deathmotion.playercrasher.PlayerCrasher;
import com.deathmotion.playercrasher.commands.CrashCommand;
import com.deathmotion.playercrasher.commands.PCCommand;
import com.deathmotion.playercrasher.events.PlayerQuit;

/**
* Manages the start-up processes of the plugin, including the registration of commands and events.
Expand All @@ -26,9 +27,17 @@ public StartupManager(PlayerCrasher plugin) {
* Calls methods to register commands and events.
*/
private void load() {
registerEvents();
registerCommands();
}

/**
* Registers events related to the plugin.
*/
private void registerEvents() {
plugin.getServer().getPluginManager().registerEvents(new PlayerQuit(plugin), plugin);
}

/**
* Registers commands related to the plugin.
*/
Expand Down
Loading

0 comments on commit 99aaacf

Please sign in to comment.