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
+12 -6
View File
@@ -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, - **Left-click** the sign: dials the shown destination - chevrons light in sequence,
then the interior becomes the event horizon (`gate.iris-open-material`, default then the interior becomes the event horizon (`gate.iris-open-material`, default
water) and travel opens up. It auto-closes after `dialing.open-seconds`. 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. - Travel is **one-way**: only the gate you dialed *from* can send anything anywhere.
Walking into its event horizon teleports you to the destination, arriving just Walking (or wandering, or drifting) into its event horizon teleports whatever
clear of its iris (never inside it) and still facing whatever direction you were entered to the destination, arriving just clear of its iris (never inside it) and
already walking, so entering forward always means exiting forward. The still facing whatever direction it was already moving, so entering forward always
destination's horizon is visible but not walkable - stepping into it does nothing, means exiting forward. The destination's horizon is visible but not walkable -
same as a real Stargate only running one direction at a time. 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. - 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 **Right-click a button placed directly below the sign** to toggle it. Closed means
closed - `gate.iris-shield-material` (default bedrock), solid, blocking everything - closed - `gate.iris-shield-material` (default bedrock), solid, blocking everything -
@@ -4,8 +4,11 @@ import dev.skywalker3200.stargate.paper.StargatePlugin;
import dev.skywalker3200.stargate.paper.gate.GateManager; import dev.skywalker3200.stargate.paper.gate.GateManager;
import dev.skywalker3200.stargate.paper.gate.RuntimeGate; import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
import dev.skywalker3200.stargate.paper.network.CrossServerBridge; import dev.skywalker3200.stargate.paper.network.CrossServerBridge;
import io.papermc.paper.event.entity.EntityMoveEvent;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Sound;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
@@ -18,12 +21,16 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
* Walking into the DIALING gate's event horizon teleports the player to the connected gate, * Walking (or wandering, or drifting, or rolling) into the DIALING gate's event horizon
* arriving just past its iris (never inside it) and facing the same absolute direction they * teleports whatever entered to the connected gate, arriving just past its iris (never inside
* were already walking, so "enter forward" always means "exit forward". Travel is strictly * it) and facing the same absolute direction it was already moving, so "enter forward" always
* one-way: only the gate that was dialed (outgoing = true) can send anyone anywhere - stepping * means "exit forward". Travel is strictly one-way: only the gate that was dialed (outgoing =
* into the receiving gate's horizon does nothing, matching the wormhole only running one * true) can send anything anywhere - stepping into the receiving gate's horizon does nothing.
* direction at a time. *
* 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 { public class GateTeleportListener implements Listener {
@@ -40,31 +47,55 @@ public class GateTeleportListener implements Listener {
} }
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
public void onMove(PlayerMoveEvent event) { public void onPlayerMove(PlayerMoveEvent event) {
Location to = event.getTo(); Location to = event.getTo();
if (to == null) return; if (to == null) return;
if (event.getFrom().getBlockX() == to.getBlockX() && event.getFrom().getBlockY() == to.getBlockY() if (sameBlock(event.getFrom(), to)) return;
&& event.getFrom().getBlockZ() == to.getBlockZ()) return;
Player player = event.getPlayer(); Player player = event.getPlayer();
Long until = cooldownUntil.get(player.getUniqueId()); if (onCooldown(player.getUniqueId())) return;
if (until != null && System.currentTimeMillis() < until) 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(); List<RuntimeGate> gates = gateManager.all();
for (RuntimeGate rg : gates) { for (RuntimeGate rg : gates) {
if (!rg.isOpen() || !rg.isOutgoing() || rg.getStructure() == null || rg.getConnectedTo() == null) continue; if (!rg.isOpen() || !rg.isOutgoing() || rg.getStructure() == null || rg.getConnectedTo() == null) continue;
for (Block iris : rg.getStructure().getIris()) { for (Block iris : rg.getStructure().getIris()) {
if (iris.getX() == standing.getX() && iris.getY() == standing.getY() && iris.getZ() == standing.getZ() if (iris.getX() == standing.getX() && iris.getY() == standing.getY() && iris.getZ() == standing.getZ()
&& iris.getWorld().equals(standing.getWorld())) { && iris.getWorld().equals(standing.getWorld())) {
teleport(player, rg); return rg;
return;
} }
} }
} }
return null;
} }
private void teleport(Player player, RuntimeGate entered) { private void teleportPlayer(Player player, RuntimeGate entered) {
RuntimeGate dest = entered.getConnectedTo(); RuntimeGate dest = entered.getConnectedTo();
if (dest == null) return; if (dest == null) return;
@@ -84,6 +115,24 @@ public class GateTeleportListener implements Listener {
cooldownUntil.put(player.getUniqueId(), System.currentTimeMillis() + COOLDOWN_MS); cooldownUntil.put(player.getUniqueId(), System.currentTimeMillis() + COOLDOWN_MS);
player.teleport(landing); player.teleport(landing);
player.setVelocity(velocity); 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);
} }
} }