From f58fad26d70a5974e1c0de51e10a5cd75d5fd473 Mon Sep 17 00:00:00 2001 From: Michael Burgess Date: Sun, 9 Aug 2026 11:54:40 -0400 Subject: [PATCH] Transport non-player entities through gates too GateTeleportListener previously only listened to PlayerMoveEvent, so mobs, dropped items, minecarts, boats, and everything else that isn't a player just walked/rolled/drifted straight through the event horizon block with no effect. Added a Paper EntityMoveEvent handler sharing the same one-way/outgoing-only matching, safe-landing, and cooldown logic as the player path. Non-player entities are restricted to same-server destinations - the cross-server plugin-messaging protocol only carries a player's UUID and look direction across the proxy hop, not arbitrary entity state, so there's no way to hand off a mob or item to another backend. --- README.md | 18 +++-- .../paper/listener/GateTeleportListener.java | 81 +++++++++++++++---- 2 files changed, 77 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index dabedec..b5d309e 100644 --- a/README.md +++ b/README.md @@ -60,12 +60,18 @@ material, unsealed interior, too far from the sign, etc. - **Left-click** the sign: dials the shown destination - chevrons light in sequence, then the interior becomes the event horizon (`gate.iris-open-material`, default water) and travel opens up. It auto-closes after `dialing.open-seconds`. -- Travel is **one-way**: only the gate you dialed *from* can send you anywhere. - Walking into its event horizon teleports you to the destination, arriving just - clear of its iris (never inside it) and still facing whatever direction you were - already walking, so entering forward always means exiting forward. The - destination's horizon is visible but not walkable - stepping into it does nothing, - same as a real Stargate only running one direction at a time. +- Travel is **one-way**: only the gate you dialed *from* can send anything anywhere. + Walking (or wandering, or drifting) into its event horizon teleports whatever + entered to the destination, arriving just clear of its iris (never inside it) and + still facing whatever direction it was already moving, so entering forward always + means exiting forward. The destination's horizon is visible but not walkable - + stepping into it does nothing, same as a real Stargate only running one direction + at a time. +- Not just players - mobs, dropped items, minecarts, boats, arrows, anything that + moves through the event horizon gets sent through too. Non-player entities can + only travel to a same-server destination, though; there's no way to carry an + arbitrary entity's state across the Velocity/Bungee plugin-messaging hop, so a + cross-server gate just won't move anything but players. - The **iris shield** is a separate, physical thing from the wormhole connection. **Right-click a button placed directly below the sign** to toggle it. Closed means closed - `gate.iris-shield-material` (default bedrock), solid, blocking everything - diff --git a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/GateTeleportListener.java b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/GateTeleportListener.java index 146c18c..5861630 100644 --- a/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/GateTeleportListener.java +++ b/stargate-paper/src/main/java/dev/skywalker3200/stargate/paper/listener/GateTeleportListener.java @@ -4,8 +4,11 @@ import dev.skywalker3200.stargate.paper.StargatePlugin; import dev.skywalker3200.stargate.paper.gate.GateManager; import dev.skywalker3200.stargate.paper.gate.RuntimeGate; import dev.skywalker3200.stargate.paper.network.CrossServerBridge; +import io.papermc.paper.event.entity.EntityMoveEvent; import org.bukkit.Location; +import org.bukkit.Sound; import org.bukkit.block.Block; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -18,12 +21,16 @@ import java.util.Map; import java.util.UUID; /** - * Walking into the DIALING gate's event horizon teleports the player to the connected gate, - * arriving just past its iris (never inside it) and facing the same absolute direction they - * were already walking, so "enter forward" always means "exit forward". Travel is strictly - * one-way: only the gate that was dialed (outgoing = true) can send anyone anywhere - stepping - * into the receiving gate's horizon does nothing, matching the wormhole only running one - * direction at a time. + * Walking (or wandering, or drifting, or rolling) into the DIALING gate's event horizon + * teleports whatever entered to the connected gate, arriving just past its iris (never inside + * it) and facing the same absolute direction it was already moving, so "enter forward" always + * means "exit forward". Travel is strictly one-way: only the gate that was dialed (outgoing = + * true) can send anything anywhere - stepping into the receiving gate's horizon does nothing. + * + * Players get their own handler because cross-server travel needs a real client connection to + * hand off through the proxy; every other entity (mobs, dropped items, minecarts, boats, arrows, + * ...) is handled generically, but only for same-server destinations - there's no way to carry + * an arbitrary entity's state across the BungeeCord/Velocity plugin-messaging hop. */ public class GateTeleportListener implements Listener { @@ -40,31 +47,55 @@ public class GateTeleportListener implements Listener { } @EventHandler(ignoreCancelled = true) - public void onMove(PlayerMoveEvent event) { + public void onPlayerMove(PlayerMoveEvent event) { Location to = event.getTo(); if (to == null) return; - if (event.getFrom().getBlockX() == to.getBlockX() && event.getFrom().getBlockY() == to.getBlockY() - && event.getFrom().getBlockZ() == to.getBlockZ()) return; + if (sameBlock(event.getFrom(), to)) return; Player player = event.getPlayer(); - Long until = cooldownUntil.get(player.getUniqueId()); - if (until != null && System.currentTimeMillis() < until) return; + if (onCooldown(player.getUniqueId())) return; - Block standing = to.getBlock(); + RuntimeGate rg = findOutgoingGateAt(to.getBlock()); + if (rg != null) teleportPlayer(player, rg); + } + + @EventHandler(ignoreCancelled = true) + public void onEntityMove(EntityMoveEvent event) { + Entity entity = event.getEntity(); + if (entity instanceof Player) return; // handled separately above (needs cross-server support) + + Location to = event.getTo(); + if (sameBlock(event.getFrom(), to)) return; + if (onCooldown(entity.getUniqueId())) return; + + RuntimeGate rg = findOutgoingGateAt(to.getBlock()); + if (rg != null) teleportEntity(entity, rg); + } + + private boolean sameBlock(Location a, Location b) { + return a.getBlockX() == b.getBlockX() && a.getBlockY() == b.getBlockY() && a.getBlockZ() == b.getBlockZ(); + } + + private boolean onCooldown(UUID id) { + Long until = cooldownUntil.get(id); + return until != null && System.currentTimeMillis() < until; + } + + private RuntimeGate findOutgoingGateAt(Block standing) { List gates = gateManager.all(); for (RuntimeGate rg : gates) { if (!rg.isOpen() || !rg.isOutgoing() || rg.getStructure() == null || rg.getConnectedTo() == null) continue; for (Block iris : rg.getStructure().getIris()) { if (iris.getX() == standing.getX() && iris.getY() == standing.getY() && iris.getZ() == standing.getZ() && iris.getWorld().equals(standing.getWorld())) { - teleport(player, rg); - return; + return rg; } } } + return null; } - private void teleport(Player player, RuntimeGate entered) { + private void teleportPlayer(Player player, RuntimeGate entered) { RuntimeGate dest = entered.getConnectedTo(); if (dest == null) return; @@ -84,6 +115,24 @@ public class GateTeleportListener implements Listener { cooldownUntil.put(player.getUniqueId(), System.currentTimeMillis() + COOLDOWN_MS); player.teleport(landing); player.setVelocity(velocity); - player.playSound(landing, org.bukkit.Sound.ENTITY_ENDERMAN_TELEPORT, 1f, 1f); + player.playSound(landing, Sound.ENTITY_ENDERMAN_TELEPORT, 1f, 1f); + } + + private void teleportEntity(Entity entity, RuntimeGate entered) { + RuntimeGate dest = entered.getConnectedTo(); + if (dest == null) return; + if (!dest.getGate().getServerId().equals(plugin.getServerId())) return; // no cross-server carry for non-players + + Location loc = entity.getLocation(); + Vector direction = loc.getDirection(); + Vector velocity = entity.getVelocity(); + + Location landing = gateManager.computeSafeLanding(dest, direction, loc.getYaw(), loc.getPitch()); + if (landing == null) return; + + cooldownUntil.put(entity.getUniqueId(), System.currentTimeMillis() + COOLDOWN_MS); + entity.teleport(landing); + entity.setVelocity(velocity); + entity.getWorld().playSound(landing, Sound.ENTITY_ENDERMAN_TELEPORT, 1f, 1f); } }