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.
This commit is contained in:
Michael Burgess
2026-08-09 11:54:40 -04:00
parent 8932dfed29
commit f58fad26d7
2 changed files with 77 additions and 22 deletions
@@ -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<RuntimeGate> 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);
}
}