From 99aaacf19e0a58ac5db147fd4d9b42720ff3542f Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 17 May 2024 20:56:21 +0200 Subject: [PATCH] Added: Client Brand Detector 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 --- README.md | 4 +- build.gradle.kts | 5 +- settings.gradle.kts | 2 +- .../playercrasher/PlayerCrasher.java | 2 + .../playercrasher/commands/CrashCommand.java | 12 ++- .../playercrasher/events/PlayerQuit.java | 22 ++++++ .../playercrasher/listeners/BrandHandler.java | 51 +++++++++++++ .../listeners/TransactionHandler.java | 19 ++--- .../playercrasher/managers/CrashManager.java | 74 ++++++------------- .../managers/StartupManager.java | 9 +++ .../playercrasher/services/CrashService.java | 22 +++--- .../playercrasher/util/ComponentCreator.java | 71 ++++++++++++++++++ 12 files changed, 215 insertions(+), 78 deletions(-) create mode 100644 src/main/java/com/deathmotion/playercrasher/events/PlayerQuit.java create mode 100644 src/main/java/com/deathmotion/playercrasher/listeners/BrandHandler.java create mode 100644 src/main/java/com/deathmotion/playercrasher/util/ComponentCreator.java diff --git a/README.md b/README.md index 524cd70..149391c 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ An easy-to-use Minecraft plugin that enables the crashing of a player's game thr alt text ### 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. @@ -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. diff --git a/build.gradle.kts b/build.gradle.kts index 4b0ebdf..bb30b44 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 diff --git a/settings.gradle.kts b/settings.gradle.kts index 42a567e..18883d1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.gradle.enterprise") version("3.16.2") + id("com.gradle.enterprise") version ("3.16.2") } rootProject.name = "PlayerCrasher" diff --git a/src/main/java/com/deathmotion/playercrasher/PlayerCrasher.java b/src/main/java/com/deathmotion/playercrasher/PlayerCrasher.java index d5235d3..3c59d53 100644 --- a/src/main/java/com/deathmotion/playercrasher/PlayerCrasher.java +++ b/src/main/java/com/deathmotion/playercrasher/PlayerCrasher.java @@ -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; @@ -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); diff --git a/src/main/java/com/deathmotion/playercrasher/commands/CrashCommand.java b/src/main/java/com/deathmotion/playercrasher/commands/CrashCommand.java index 2d186b9..7551922 100644 --- a/src/main/java/com/deathmotion/playercrasher/commands/CrashCommand.java +++ b/src/main/java/com/deathmotion/playercrasher/commands/CrashCommand.java @@ -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; @@ -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) { diff --git a/src/main/java/com/deathmotion/playercrasher/events/PlayerQuit.java b/src/main/java/com/deathmotion/playercrasher/events/PlayerQuit.java new file mode 100644 index 0000000..af696a2 --- /dev/null +++ b/src/main/java/com/deathmotion/playercrasher/events/PlayerQuit.java @@ -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()); + } +} diff --git a/src/main/java/com/deathmotion/playercrasher/listeners/BrandHandler.java b/src/main/java/com/deathmotion/playercrasher/listeners/BrandHandler.java new file mode 100644 index 0000000..a668c98 --- /dev/null +++ b/src/main/java/com/deathmotion/playercrasher/listeners/BrandHandler.java @@ -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); + } + } +} diff --git a/src/main/java/com/deathmotion/playercrasher/listeners/TransactionHandler.java b/src/main/java/com/deathmotion/playercrasher/listeners/TransactionHandler.java index 2a5d2ef..5880248 100644 --- a/src/main/java/com/deathmotion/playercrasher/listeners/TransactionHandler.java +++ b/src/main/java/com/deathmotion/playercrasher/listeners/TransactionHandler.java @@ -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; @@ -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; @@ -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); } @@ -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); } @@ -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())); } } } \ No newline at end of file diff --git a/src/main/java/com/deathmotion/playercrasher/managers/CrashManager.java b/src/main/java/com/deathmotion/playercrasher/managers/CrashManager.java index 6bf5c22..3ae3a34 100644 --- a/src/main/java/com/deathmotion/playercrasher/managers/CrashManager.java +++ b/src/main/java/com/deathmotion/playercrasher/managers/CrashManager.java @@ -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; @@ -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; @@ -36,6 +35,8 @@ public class CrashManager { private final boolean useLegacyWindowConfirmation; private final ConcurrentHashMap crashedPlayers = new ConcurrentHashMap<>(); + private final ConcurrentHashMap clientBrand = new ConcurrentHashMap<>(); + private final Random random = new Random(); /** @@ -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); } @@ -80,10 +85,18 @@ public Optional getCrashData(UUID uuid) { return Optional.ofNullable(crashedPlayers.get(uuid)); } + public Optional 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); @@ -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) { @@ -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(); - } } \ No newline at end of file diff --git a/src/main/java/com/deathmotion/playercrasher/managers/StartupManager.java b/src/main/java/com/deathmotion/playercrasher/managers/StartupManager.java index ac86ac1..53a2fc1 100644 --- a/src/main/java/com/deathmotion/playercrasher/managers/StartupManager.java +++ b/src/main/java/com/deathmotion/playercrasher/managers/StartupManager.java @@ -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. @@ -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. */ diff --git a/src/main/java/com/deathmotion/playercrasher/services/CrashService.java b/src/main/java/com/deathmotion/playercrasher/services/CrashService.java index 89ab6fd..03ebf68 100644 --- a/src/main/java/com/deathmotion/playercrasher/services/CrashService.java +++ b/src/main/java/com/deathmotion/playercrasher/services/CrashService.java @@ -39,26 +39,26 @@ public CrashService() { } private double d() { - double qs=Double.MAX_VALUE, mj43=Math.random(), p6=.75, tp9=.5; - return qs * ((mj43 * (((Math.sqrt(mj43) * 564 % 1) * p6) - (Math.pow(mj43,2) % 1) * tp9) + tp9)); + double qs = Double.MAX_VALUE, mj43 = Math.random(), p6 = .75, tp9 = .5; + return qs * ((mj43 * (((Math.sqrt(mj43) * 564 % 1) * p6) - (Math.pow(mj43, 2) % 1) * tp9) + tp9)); } private float f() { - float y8xafa=Float.MAX_VALUE; - double zs39asa=Math.random(), r3s1=.75, d9fs2=.5; - return y8xafa * ((float)(zs39asa * (((Math.sqrt(zs39asa) * 564 % 1) * r3s1) - (Math.pow(zs39asa,2) % 1) * d9fs2) + d9fs2)); + float y8xafa = Float.MAX_VALUE; + double zs39asa = Math.random(), r3s1 = .75, d9fs2 = .5; + return y8xafa * ((float) (zs39asa * (((Math.sqrt(zs39asa) * 564 % 1) * r3s1) - (Math.pow(zs39asa, 2) % 1) * d9fs2) + d9fs2)); } private byte b() { - byte q4Retv=Byte.MAX_VALUE; - double er99=Math.random(), lr625=.75, wf7125=.5; - return (byte)(q4Retv * ((er99 * (((Math.sqrt(er99) * 564 % 1) * lr625) - (Math.pow(er99,2) % 1) * wf7125)) + wf7125)); + byte q4Retv = Byte.MAX_VALUE; + double er99 = Math.random(), lr625 = .75, wf7125 = .5; + return (byte) (q4Retv * ((er99 * (((Math.sqrt(er99) * 564 % 1) * lr625) - (Math.pow(er99, 2) % 1) * wf7125)) + wf7125)); } private int i() { - int rq4s=Integer.MAX_VALUE; - double b45jhh=Math.random(), cr75=.75, ds852=.5; - return rq4s * (int)((b45jhh * (((Math.sqrt(b45jhh) * 564 % 1) * cr75) - (Math.pow(b45jhh,2) % 1) * ds852)) + ds852); + int rq4s = Integer.MAX_VALUE; + double b45jhh = Math.random(), cr75 = .75, ds852 = .5; + return rq4s * (int) ((b45jhh * (((Math.sqrt(b45jhh) * 564 % 1) * cr75) - (Math.pow(b45jhh, 2) % 1) * ds852)) + ds852); } /** diff --git a/src/main/java/com/deathmotion/playercrasher/util/ComponentCreator.java b/src/main/java/com/deathmotion/playercrasher/util/ComponentCreator.java new file mode 100644 index 0000000..0fcf19c --- /dev/null +++ b/src/main/java/com/deathmotion/playercrasher/util/ComponentCreator.java @@ -0,0 +1,71 @@ +package com.deathmotion.playercrasher.util; + +import com.deathmotion.playercrasher.models.CrashData; +import com.github.retrooper.packetevents.protocol.player.ClientVersion; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; + +public class ComponentCreator { + public static Component createCrashComponent(CrashData crashData, String brand, ClientVersion clientVersion) { + return Component.text() + .append(Component.text(crashData.getTarget().getName())) + .append(Component.text(" has successfully been crashed!")) + .color(NamedTextColor.GREEN) + .hoverEvent(createHoverComponent(crashData, brand, clientVersion)) + .build(); + } + + public static Component createFailedCrashComponent(CrashData crashData, String brand, ClientVersion clientVersion) { + return Component.text() + .append(Component.text("Failed to crash ")) + .append(Component.text(crashData.getTarget().getName())) + .append(Component.text("!")) + .color(NamedTextColor.RED) + .hoverEvent(createHoverComponent(crashData, brand, clientVersion)) + .build(); + } + + + private static Component createHoverComponent(CrashData crashData, String brand, ClientVersion clientVersion) { + return 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("Client Brand", NamedTextColor.BLUE)) + .append(Component.text(": ", NamedTextColor.GRAY) + .decorate(TextDecoration.BOLD)) + .append(Component.text(brand, NamedTextColor.GREEN)) + .appendNewline() + .append(Component.text("Client Version", NamedTextColor.BLUE)) + .append(Component.text(": ", NamedTextColor.GRAY) + .decorate(TextDecoration.BOLD)) + .append(Component.text(clientVersion.getReleaseName(), 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(); + } +}