Skip to content

Commit

Permalink
Add PlayerMoveControllableVehicleEvent
Browse files Browse the repository at this point in the history
Useful if you want to actually control what the player is moving, because VehicleMoveEvent does not let you cancel nor change the location. Also useful to detect BoatFly hacks.
  • Loading branch information
MrPowerGamerBR committed Jan 5, 2025
1 parent 0fd98d6 commit 81aca64
Show file tree
Hide file tree
Showing 8 changed files with 407 additions and 0 deletions.
141 changes: 141 additions & 0 deletions patches/api/0004-Add-PlayerMoveControllableVehicleEvent.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: MrPowerGamerBR <[email protected]>
Date: Sat, 4 Jan 2025 23:58:34 -0300
Subject: [PATCH] Add PlayerMoveControllableVehicleEvent


diff --git a/src/main/java/net/sparklypower/sparklypaper/event/player/PlayerMoveControllableVehicleEvent.java b/src/main/java/net/sparklypower/sparklypaper/event/player/PlayerMoveControllableVehicleEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..a646251feb6b8bf7ff07e6b4b84bb5751ee4b90b
--- /dev/null
+++ b/src/main/java/net/sparklypower/sparklypaper/event/player/PlayerMoveControllableVehicleEvent.java
@@ -0,0 +1,129 @@
+package net.sparklypower.sparklypaper.event.player;
+
+import com.google.common.base.Preconditions;
+import org.bukkit.Location;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Vehicle;
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.player.PlayerEvent;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Raised when a player moves a controllable vehicle. Controllable vehicles are vehicles that the client can control, such as boats, horses, striders, pigs, etc.
+ * <p>
+ * Minecarts are NOT affected by this event!
+ */
+public class PlayerMoveControllableVehicleEvent extends PlayerEvent implements Cancellable {
+ private static final HandlerList handlers = new HandlerList();
+ private boolean cancel = false;
+ private final Vehicle vehicle;
+ private Location from;
+ private Location to;
+
+ public PlayerMoveControllableVehicleEvent(@NotNull final Player player, @NotNull final Vehicle vehicle, @NotNull final Location from, @NotNull final Location to) {
+ super(player);
+
+ this.vehicle = vehicle;
+ this.from = from;
+ this.to = to;
+ }
+
+ /**
+ * Get the previous position.
+ *
+ * @return Old position.
+ */
+ @NotNull
+ public Location getFrom() {
+ return from.clone(); // Paper - clone to avoid changes
+ }
+
+ /**
+ * Sets the location to mark as where the player moved from
+ *
+ * @param from New location to mark as the players previous location
+ */
+ public void setFrom(@NotNull Location from) {
+ validateLocation(from, this.from);
+ this.from = from;
+ }
+
+ /**
+ * Get the next position.
+ *
+ * @return New position.
+ */
+ @NotNull
+ public Location getTo() {
+ return to.clone(); // Paper - clone to avoid changes
+ }
+
+ /**
+ * Sets the location that this player will move to
+ *
+ * @param to New Location this player will move to
+ */
+ public void setTo(@NotNull Location to) {
+ validateLocation(to, this.to);
+ this.to = to;
+ }
+
+ /**
+ * Get the vehicle.
+ *
+ * @return the vehicle
+ */
+ @NotNull
+ public final Entity getVehicle() {
+ return vehicle;
+ }
+
+ /**
+ * Gets the cancellation state of this event. A cancelled event will not
+ * be executed in the server, but will still pass to other plugins
+ * <p>
+ * If a move or teleport event is cancelled, the vehicle and player will be moved or
+ * teleported back to the Location as defined by getFrom(). This will not
+ * fire an event
+ *
+ * @return true if this event is cancelled
+ */
+ @Override
+ public boolean isCancelled() {
+ return cancel;
+ }
+
+ /**
+ * Sets the cancellation state of this event. A cancelled event will not
+ * be executed in the server, but will still pass to other plugins
+ * <p>
+ * If a move or teleport event is cancelled, the vehicle and player will be moved or
+ * teleported back to the Location as defined by getFrom(). This will not
+ * fire an event
+ *
+ * @param cancel true if you wish to cancel this event
+ */
+ @Override
+ public void setCancelled(boolean cancel) {
+ this.cancel = cancel;
+ }
+
+ @NotNull
+ @Override
+ public HandlerList getHandlers() {
+ return handlers;
+ }
+
+ @NotNull
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+
+ private void validateLocation(@NotNull Location loc, @NotNull Location originalLoc) {
+ Preconditions.checkArgument(loc != null, "Cannot use null location!");
+ Preconditions.checkArgument(loc.getWorld() != null, "Cannot use null location with null world!");
+ Preconditions.checkArgument(loc.getWorld() != originalLoc, "New location should be in the original world!");
+ }
+}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: MrPowerGamerBR <[email protected]>
Date: Fri, 13 Dec 2024 16:35:03 -0300
Subject: [PATCH] Extend AsyncPlayerPreLoginEvent to allow plugins to send
Login Plugin Requests to the client


diff --git a/src/main/java/net/sparklypower/sparklypaper/PendingConnection.java b/src/main/java/net/sparklypower/sparklypaper/PendingConnection.java
new file mode 100644
index 0000000000000000000000000000000000000000..11d64d825c15c122192359b86561808d0d7f5fbb
--- /dev/null
+++ b/src/main/java/net/sparklypower/sparklypaper/PendingConnection.java
@@ -0,0 +1,41 @@
+package net.sparklypower.sparklypaper;
+
+import org.bukkit.NamespacedKey;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.CompletableFuture;
+
+public interface PendingConnection {
+ /**
+ * Sends a login plugin request to the client, the client must reply with a login plugin response.
+ * <br>
+ * The vanilla client always replies with "not acknowledged", however mods, and proxies, can change
+ * the response.
+ *
+ * @param transactionId the ID of the transaction, must be unique for each connection
+ * @param channel the chanenl where the data will be sent
+ * @param data the data that will be sent
+ * @return a {@link CompletableFuture} that will be completed when the
+ * login plugin response is received or otherwise available.
+ */
+ @NotNull
+ CompletableFuture<PendingConnection.LoginPluginResponse> sendLoginPluginRequest(int transactionId, @NotNull NamespacedKey channel, byte @NotNull [] data);
+
+ class LoginPluginResponse {
+ final boolean acknowledged;
+ final byte[] response;
+
+ public LoginPluginResponse(boolean acknowledged, byte[] response) {
+ this.acknowledged = acknowledged;
+ this.response = response;
+ }
+
+ public boolean isAcknowledged() {
+ return acknowledged;
+ }
+
+ public byte[] getResponse() {
+ return response;
+ }
+ }
+}
diff --git a/src/main/java/org/bukkit/event/player/AsyncPlayerPreLoginEvent.java b/src/main/java/org/bukkit/event/player/AsyncPlayerPreLoginEvent.java
index ff5cca4a7e75274b4b278a48ae1544ff42a9836a..2ace8261a836dbdf44b35cdedb53c4b5b2986f0f 100644
--- a/src/main/java/org/bukkit/event/player/AsyncPlayerPreLoginEvent.java
+++ b/src/main/java/org/bukkit/event/player/AsyncPlayerPreLoginEvent.java
@@ -27,6 +27,7 @@ public class AsyncPlayerPreLoginEvent extends Event {
private final InetAddress rawAddress; // Paper
private final String hostname; // Paper
private final boolean transferred;
+ private net.sparklypower.sparklypaper.PendingConnection pendingConnection; // SparklyPaper - Add support for login plugin requests

@Deprecated(since = "1.7.5")
public AsyncPlayerPreLoginEvent(@NotNull final String name, @NotNull final InetAddress ipAddress) {
@@ -53,8 +54,15 @@ public class AsyncPlayerPreLoginEvent extends Event {
this(name, ipAddress, rawAddress, uniqueId, transferred, profile, "");
}

+ // SparklyPaper start - Add support for login plugin requests
@org.jetbrains.annotations.ApiStatus.Internal
public AsyncPlayerPreLoginEvent(@NotNull final String name, @NotNull final InetAddress ipAddress, @NotNull final InetAddress rawAddress, @NotNull final UUID uniqueId, boolean transferred, @NotNull com.destroystokyo.paper.profile.PlayerProfile profile, @NotNull String hostname) {
+ this(name, ipAddress, rawAddress, uniqueId, transferred, profile, hostname, null);
+ }
+
+ @org.jetbrains.annotations.ApiStatus.Internal
+ public AsyncPlayerPreLoginEvent(@NotNull final String name, @NotNull final InetAddress ipAddress, @NotNull final InetAddress rawAddress, @NotNull final UUID uniqueId, boolean transferred, @NotNull com.destroystokyo.paper.profile.PlayerProfile profile, @NotNull String hostname, @NotNull net.sparklypower.sparklypaper.PendingConnection pendingConnection) {
+ // SparklyPaper end
// Paper end
super(true);
this.result = Result.ALLOWED;
@@ -64,6 +72,7 @@ public class AsyncPlayerPreLoginEvent extends Event {
this.rawAddress = rawAddress; // Paper
this.hostname = hostname; // Paper
this.transferred = transferred;
+ this.pendingConnection = pendingConnection; // SparklyPaper - Add support for login plugin requests
}

/**
@@ -297,6 +306,18 @@ public class AsyncPlayerPreLoginEvent extends Event {
return transferred;
}

+ // SparklyPaper start - Add support for login plugin requests
+
+ /**
+ * Gets the pending connection associated with this event.
+ *
+ * @return The pending connection
+ */
+ public net.sparklypower.sparklypaper.PendingConnection getPendingConnection() {
+ return this.pendingConnection;
+ }
+ // SparklyPaper end
+
@NotNull
@Override
public HandlerList getHandlers() {
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: MrPowerGamerBR <[email protected]>
Date: Fri, 13 Dec 2024 16:35:12 -0300
Subject: [PATCH] Extend AsyncPlayerPreLoginEvent to allow plugins to send
Login Plugin Requests to the client


diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
index 033755682c61c889723c3669b5cff4de147f637e..8809a4ee30d0701dab85556c54e29a5c315790bc 100644
--- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
@@ -92,6 +92,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
private ServerPlayer player; // CraftBukkit
public boolean iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation = false; // Paper - username validation overriding
private int velocityLoginMessageId = -1; // Paper - Add Velocity IP Forwarding Support
+ private net.sparklypower.sparklypaper.CraftPendingConnection pendingConnection = new net.sparklypower.sparklypaper.CraftPendingConnection(this);

public ServerLoginPacketListenerImpl(MinecraftServer server, Connection connection, boolean transferred) {
this.state = ServerLoginPacketListenerImpl.State.HELLO;
@@ -368,7 +369,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
// Paper start - Add more fields to AsyncPlayerPreLoginEvent
final InetAddress rawAddress = ((InetSocketAddress) this.connection.channel.remoteAddress()).getAddress();
com.destroystokyo.paper.profile.PlayerProfile profile = com.destroystokyo.paper.profile.CraftPlayerProfile.asBukkitMirror(gameprofile); // Paper - setPlayerProfileAPI
- AsyncPlayerPreLoginEvent asyncEvent = new AsyncPlayerPreLoginEvent(playerName, address, rawAddress, uniqueId, this.transferred, profile, this.connection.hostname);
+ AsyncPlayerPreLoginEvent asyncEvent = new AsyncPlayerPreLoginEvent(playerName, address, rawAddress, uniqueId, this.transferred, profile, this.connection.hostname, this.pendingConnection); // SparklyPaper - Add support for login plugin requests
server.getPluginManager().callEvent(asyncEvent);
profile = asyncEvent.getPlayerProfile();
profile.complete(true); // Paper - setPlayerProfileAPI
@@ -450,6 +451,11 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
return;
}
// Paper end - Add Velocity IP Forwarding Support
+ // SparklyPaper - Add support for login plugin requests
+ if (pendingConnection.handleLoginPluginResponse(packet)) {
+ return;
+ }
+ // SparklyPaper end
this.disconnect(ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY);
}

diff --git a/src/main/kotlin/net/sparklypower/sparklypaper/CraftPendingConnection.kt b/src/main/kotlin/net/sparklypower/sparklypaper/CraftPendingConnection.kt
new file mode 100644
index 0000000000000000000000000000000000000000..59570030537f5ffedd199e2c74c11f9510bc4147
--- /dev/null
+++ b/src/main/kotlin/net/sparklypower/sparklypaper/CraftPendingConnection.kt
@@ -0,0 +1,68 @@
+package net.sparklypower.sparklypaper
+
+import com.google.common.base.Preconditions
+import io.netty.buffer.Unpooled
+import net.minecraft.network.FriendlyByteBuf
+import net.minecraft.network.protocol.login.ClientboundCustomQueryPacket
+import net.minecraft.network.protocol.login.ServerboundCustomQueryAnswerPacket
+import net.minecraft.server.network.ServerLoginPacketListenerImpl
+import org.bukkit.NamespacedKey
+import org.bukkit.craftbukkit.util.CraftNamespacedKey
+import java.util.*
+import java.util.concurrent.CompletableFuture
+
+class CraftPendingConnection(val manager: ServerLoginPacketListenerImpl) : PendingConnection {
+ private val requestedLoginData: Queue<LoginDataFuture> = LinkedList()
+
+ override fun sendLoginPluginRequest(
+ transactionId: Int,
+ channel: NamespacedKey,
+ data: ByteArray
+ ): CompletableFuture<PendingConnection.LoginPluginResponse> {
+ val future = CompletableFuture<PendingConnection.LoginPluginResponse>()
+
+ this.requestedLoginData.add(LoginDataFuture(transactionId, future))
+ manager.sendPacket(
+ ClientboundCustomQueryPacket(
+ transactionId,
+ ClientboundCustomQueryPacket.PlayerInfoChannelPayload(
+ CraftNamespacedKey.toMinecraft(channel),
+ FriendlyByteBuf(Unpooled.wrappedBuffer(data))
+ )
+ )
+ )
+
+ return future
+ }
+
+ fun handleLoginPluginResponse(response: ServerboundCustomQueryAnswerPacket): Boolean {
+ val future = this.requestedLoginData.peek()
+ if (future != null) {
+ if (future.transactionId == response.transactionId) {
+ Preconditions.checkState(future === this.requestedLoginData.poll(), "requestedLoginData queue mismatch")
+ if (response.payload == null) {
+ future.future.complete(PendingConnection.LoginPluginResponse(false, null))
+ return true
+ }
+
+ val payload = response.payload as ServerboundCustomQueryAnswerPacket.QueryAnswerPayload
+
+ // Ensure the reader index and writer index are not modified
+ val readableBytes: Int = payload.buffer.readableBytes()
+ val byteArray = ByteArray(readableBytes)
+
+ // Copy the readable bytes into the byte array
+ payload.buffer.getBytes(payload.buffer.readerIndex(), byteArray)
+
+ future.future.complete(PendingConnection.LoginPluginResponse(true, byteArray))
+
+ return true
+ }
+ }
+
+ return false
+ }
+
+ @JvmRecord
+ data class LoginDataFuture(val transactionId: Int, val future: CompletableFuture<PendingConnection.LoginPluginResponse>)
+}
\ No newline at end of file
Loading

0 comments on commit 81aca64

Please sign in to comment.