Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/main/java/lol/hub/hubtp/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public final class Config {
private static boolean includeLeashed;
private static boolean includeLeashedInterdimensional;
private static boolean teleportMountedEntities;
private static boolean teleportIntoFreeSeat;
private static boolean teleportLeashedOfPassengers;
private static Path ignoresPath;
private static boolean debug;

Expand All @@ -38,8 +40,10 @@ public static synchronized void load(Plugin plugin) {
config.addDefault("tp-delay-seconds", 0);
config.addDefault("movement-check", false);
config.addDefault("include-leashed", true);
config.addDefault("include-leashed-interdimensional", false);
config.addDefault("include-leashed-interdimensional", true);
config.addDefault("teleport-mounted-entities", true);
config.addDefault("teleport-into-free-seat", true);
config.addDefault("teleport-leashed-of-passengers", true);
config.addDefault("ignores-path", Ignores.defaultPath.apply(plugin));
config.addDefault("debug", false);
config.addDefault("bStats", true);
Expand Down Expand Up @@ -84,6 +88,10 @@ public static synchronized void load(Plugin plugin) {

teleportMountedEntities = config.getBoolean("teleport-mounted-entities");

teleportIntoFreeSeat = config.getBoolean("teleport-into-free-seat");

teleportLeashedOfPassengers = config.getBoolean("teleport-leashed-of-passengers");

// noinspection DataFlowIssue
if (config.getString("ignores-path") == null || config.getString("ignores-path").isBlank()) {
config.set("ignores-path", Ignores.defaultPath.apply(plugin));
Expand Down Expand Up @@ -157,6 +165,16 @@ public static boolean teleportMountedEntities() {
return teleportMountedEntities;
}

public static boolean teleportIntoFreeSeat() {
assertInitialized();
return teleportIntoFreeSeat;
}

public static boolean teleportLeashedOfPassengers() {
assertInitialized();
return teleportLeashedOfPassengers;
}

public static Path ignoresPath() {
assertInitialized();
return ignoresPath;
Expand Down
1 change: 1 addition & 0 deletions src/main/java/lol/hub/hubtp/Ignores.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public static boolean get(UUID player, UUID target) {
/**
* @return true: success, false: maximum ignores reached
*/
// this reads/writes the full file on every /tpi ...probably should be changed in the future
public static boolean set(UUID player, UUID target, boolean ignore) {
Set<UUID> set = load(player);

Expand Down
234 changes: 193 additions & 41 deletions src/main/java/lol/hub/hubtp/Plugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Location;
import org.bstats.bukkit.Metrics;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.entity.Boat;
import org.bukkit.entity.Camel;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Item;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Vehicle;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
Expand Down Expand Up @@ -222,42 +227,173 @@ public void executeTPMove(Player tpTarget, Player tpRequester) {
String requesterName = tpRequester.getName();
Log.info("Teleporting " + requesterName + " to " + tpTarget.getName());

// Leash handling if enabled
this.foliaLib.getScheduler().runAtEntity(tpRequester, requesterTask -> {
boolean freeSeatCandidate = tpRequester.getVehicle() == null
&& Config.teleportIntoFreeSeat()
&& tpTarget.getVehicle() != null;

if (!freeSeatCandidate) {
doLeashScanAndTeleport(tpTarget, tpRequester);
return;
}
this.foliaLib.getScheduler().runAtEntity(tpTarget, targetTask -> {
Entity targetVeh = tpTarget.getVehicle();
if (targetVeh instanceof Vehicle targetVehicle
&& isDriverOf(tpTarget, targetVehicle)
&& targetVehicle.getPassengers().size() < maxPassengersFor(targetVehicle)) {
teleportIntoFreeSeat(tpTarget, tpRequester, targetVehicle);
} else {
doLeashScanAndTeleport(tpTarget, tpRequester);
}
});
});
}

private void doLeashScanAndTeleport(Player tpTarget, Player tpRequester) {
if (Config.includeLeashed() && shouldTpLeashed(tpTarget, tpRequester)) {
tpRequester.getWorld()
List<LivingEntity> leashed = tpRequester.getWorld()
.getNearbyEntities(tpRequester.getLocation(), 16, 16, 16).stream()
.filter(e -> e instanceof LivingEntity)
.map(e -> (LivingEntity) e)
.filter(LivingEntity::isLeashed)
.filter(e -> e.getLeashHolder().getUniqueId().equals(tpRequester.getUniqueId()))
.forEach(entity -> entity.teleportAsync(tpTarget.getLocation(),
PlayerTeleportEvent.TeleportCause.PLUGIN));
.filter(e -> e.getLeashHolder() != null
&& leashHolderMatches(e, tpRequester))
.toList();
for (LivingEntity mob : leashed) {
teleportLeashedMob(mob, tpTarget.getLocation());
}
}
doTeleportAndNotify(tpTarget, tpRequester);
}

Location destination = tpTarget.getLocation().clone();
boolean crossWorld = !tpRequester.getWorld().equals(tpTarget.getWorld());
private boolean leashHolderMatches(LivingEntity mob, Player requester) {
Entity holder = mob.getLeashHolder();
if (holder == null) return false;
if (holder.getUniqueId().equals(requester.getUniqueId())) return true;
if (!Config.teleportLeashedOfPassengers()) return false;
Entity mount = requester.getVehicle();
if (!(mount instanceof Vehicle v)) return false;
for (Entity passenger : v.getPassengers()) {
if (passenger.getUniqueId().equals(holder.getUniqueId())) return true;
}
return false;
}

// Execute teleport asynchronously, accounting for mounts
teleportRequesterWithVehicle(tpRequester, destination, crossWorld)
.thenAccept(result -> {
if (result) {
tpTarget.sendMessage(
Component.text(tpRequester.getName())
.append(Component.text(" teleported to you!", NamedTextColor.GOLD)));
tpRequester.sendMessage(
Component.text("Teleported to ", NamedTextColor.GOLD)
.append(Component.text(tpTarget.getName()))
.append(Component.text("!")));
private void teleportLeashedMob(LivingEntity mob, Location destination) {
Entity holder = mob.getLeashHolder();
mob.teleportAsync(destination, PlayerTeleportEvent.TeleportCause.PLUGIN)
.whenComplete((ok, ex) -> {
if (ok == null || !ok || ex != null || holder == null) return;
this.foliaLib.getScheduler().runAtEntity(holder, task -> {
if (!mob.isValid() || !holder.isValid()) return;
if (!mob.getWorld().equals(holder.getWorld())) return;
mob.setLeashHolder(holder);
// we drop a lead on teleport; clean it up so the player
// doesn't end up with two (one in item form, one attached to entity)
removeNearbyDroppedLeads(mob.getLocation(), 1);
});
});
}

private void removeNearbyDroppedLeads(Location center, int count) {
for (Entity e : center.getWorld().getNearbyEntities(center, 2, 2, 2)) {
if (count <= 0) return;
if (e instanceof Item item && item.getItemStack().getType() == Material.LEAD) {
item.remove();
count--;
}
}
}

private void teleportIntoFreeSeat(Player tpTarget, Player tpRequester, Vehicle vehicle) {
Location dest = tpTarget.getLocation().clone();
tpRequester.teleportAsync(dest, PlayerTeleportEvent.TeleportCause.COMMAND)
.whenComplete((ok, ex) -> {
if (ok != null && ok && ex == null) {
// addPassenger must run on the vehicle's owning region (Folia)
this.foliaLib.getScheduler().runAtEntity(vehicle, task -> {
if (vehicle.isValid() && tpRequester.isOnline()
&& vehicle.getPassengers().size() < maxPassengersFor(vehicle)) {
vehicle.addPassenger(tpRequester);
}
});
sendOnRegion(tpTarget, Component.text(tpRequester.getName())
.append(Component.text(" teleported to you and hopped in.", NamedTextColor.GOLD)));
sendOnRegion(tpRequester, Component.text("Teleported to ", NamedTextColor.GOLD)
.append(Component.text(tpTarget.getName()))
.append(Component.text(" and hopped in.", NamedTextColor.GOLD)));
} else {
TextComponent msg = Component.text(
"Teleportation failed.",
NamedTextColor.RED);
tpTarget.sendMessage(msg);
tpRequester.sendMessage(msg);
if (ex != null) {
Log.warn("Teleport-into-seat failed: " + ex.getMessage());
}
TextComponent msg = Component.text("Teleportation failed.", NamedTextColor.RED);
sendOnRegion(tpTarget, msg);
sendOnRegion(tpRequester, msg);
}
});
}

private static int maxPassengersFor(Vehicle vehicle) {
if (vehicle instanceof Boat) return 2;
if (vehicle instanceof Camel) return 2;
return 1;
}

private void doTeleportAndNotify(Player tpTarget, Player tpRequester) {
Location destination = tpTarget.getLocation().clone();
boolean crossWorld = !tpRequester.getWorld().equals(tpTarget.getWorld());

// folia: vehicle/mount state lives in requester's region. hop there first.
this.foliaLib.getScheduler().runAtEntity(tpRequester, task -> {
// reject if requester and target share a vehicle
if (sharesVehicleWithRequester(tpRequester, tpTarget)) {
sendOnRegion(tpTarget, Component.text(tpRequester.getName(), NamedTextColor.RED)
.append(Component.text(" and you are in the same vehicle. The request was cancelled.", NamedTextColor.RED)));
sendOnRegion(tpRequester, Component.text(
"Teleportation cancelled: you and ", NamedTextColor.RED)
.append(Component.text(tpTarget.getName(), NamedTextColor.RED))
.append(Component.text(" are in the same vehicle.", NamedTextColor.RED)));
return;
}

CompletableFuture<Boolean> result = teleportRequesterWithVehicle(tpRequester, destination, crossWorld);
result.whenComplete((success, ex) -> {
boolean ok = success != null && success && ex == null;
if (ok) {
sendOnRegion(tpTarget, Component.text(tpRequester.getName())
.append(Component.text(" teleported to you!", NamedTextColor.GOLD)));
sendOnRegion(tpRequester, Component.text("Teleported to ", NamedTextColor.GOLD)
.append(Component.text(tpTarget.getName()))
.append(Component.text("!")));
} else {
if (ex != null) {
Log.warn("Teleport failed with exception: " + ex.getMessage());
}
TextComponent msg = Component.text(
"Teleportation failed.",
NamedTextColor.RED);
sendOnRegion(tpTarget, msg);
sendOnRegion(tpRequester, msg);
}
});
});
}

private static boolean sharesVehicleWithRequester(Player requester, Player target) {
Entity mount = requester.getVehicle();
if (!(mount instanceof Vehicle v)) return false;
for (Entity passenger : v.getPassengers()) {
if (passenger.getUniqueId().equals(target.getUniqueId())) return true;
}
return false;
}

public void sendOnRegion(Player player, Component message) {
if (player.isOnline()) {
this.foliaLib.getScheduler().runAtEntity(player, task -> player.sendMessage(message));
}
}

private CompletableFuture<Boolean> teleportRequesterWithVehicle(Player requester, Location destination,
boolean crossWorld) {
Entity mount = requester.getVehicle();
Expand All @@ -266,6 +402,12 @@ private CompletableFuture<Boolean> teleportRequesterWithVehicle(Player requester
return requester.teleportAsync(destination, PlayerTeleportEvent.TeleportCause.COMMAND);
}

// passengers dismount and tp alone. only the driver brings the vehicle
if (!isDriverOf(requester, mount)) {
requester.leaveVehicle();
return requester.teleportAsync(destination, PlayerTeleportEvent.TeleportCause.COMMAND);
}

if (!Config.teleportMountedEntities()) {
requester.leaveVehicle();
return requester.teleportAsync(destination, PlayerTeleportEvent.TeleportCause.COMMAND);
Expand All @@ -285,46 +427,52 @@ private CompletableFuture<Boolean> teleportRequesterWithVehicle(Player requester

for (PassengerSnapshot snapshot : passengerSnapshots) {
Entity passenger = snapshot.entity();
Location target = destination.clone();
PlayerTeleportEvent.TeleportCause cause = passenger instanceof Player
? PlayerTeleportEvent.TeleportCause.COMMAND
: PlayerTeleportEvent.TeleportCause.PLUGIN;
teleportFutures.add(passenger.teleportAsync(target, cause));
teleportFutures.add(passenger.teleportAsync(destination, cause));
}

return combineFutures(teleportFutures)
.thenApply(success -> {
if (success) {
remountPassengers(mount, passengerSnapshots);
// remount on the mounts (now destination) region
this.foliaLib.getScheduler().runAtEntity(mount,
task -> remountPassengers(mount, passengerSnapshots));
}
return success;
});
}

// same dim: RETAIN_PASSENGERS already moves passengers, don't double tp them
List<CompletableFuture<Boolean>> teleportFutures = new ArrayList<>();
teleportFutures.add(mount.teleportAsync(destination.clone(),
PlayerTeleportEvent.TeleportCause.PLUGIN,
TeleportFlag.EntityState.RETAIN_PASSENGERS));

for (PassengerSnapshot snapshot : passengerSnapshots) {
Entity passenger = snapshot.entity();
PlayerTeleportEvent.TeleportCause cause = passenger instanceof Player
? PlayerTeleportEvent.TeleportCause.COMMAND
: PlayerTeleportEvent.TeleportCause.PLUGIN;
teleportFutures.add(passenger.teleportAsync(destination.clone(),
cause,
TeleportFlag.EntityState.RETAIN_VEHICLE));
}

return combineFutures(teleportFutures)
.thenApply(success -> {
if (success) {
remountPassengers(mount, passengerSnapshots);
this.foliaLib.getScheduler().runAtEntity(mount,
task -> remountPassengers(mount, passengerSnapshots));
}
return success;
});
}

/**
* first Player in the passenger list is the "driver"
*/
private static boolean isDriverOf(Player requester, Entity vehicle) {
if (!(vehicle instanceof Vehicle v)) return false;
for (Entity passenger : v.getPassengers()) {
if (passenger instanceof Player) {
return passenger.getUniqueId().equals(requester.getUniqueId());
}
}
return true;
}

private static void remountPassengers(Entity root, List<PassengerSnapshot> snapshots) {
Map<Entity, List<Entity>> parentToChildren = new HashMap<>();

Expand Down Expand Up @@ -395,16 +543,20 @@ private static CompletableFuture<Boolean> combineFutures(List<CompletableFuture<
return CompletableFuture.completedFuture(true);
}

CompletableFuture<?>[] futureArray = futures.toArray(new CompletableFuture[0]);
return CompletableFuture.allOf(futureArray)
.thenApply(ignored -> futures.stream().allMatch(future -> future.getNow(false)));
// getNow() rethrows on exceptional completion, masking the overall result
List<CompletableFuture<Boolean>> snapshot = List.copyOf(futures);
return CompletableFuture.allOf(snapshot.toArray(new CompletableFuture[0]))
.thenApply(ignored -> snapshot.stream().allMatch(f -> {
if (f.isCompletedExceptionally()) return false;
return Boolean.TRUE.equals(f.getNow(false));
}));
}

/**
* Clears old teleport requests based on configured timeout.
*/
public void clearOldRequests() {
RequestManager.clearOldRequests(Config.requestTimeoutSeconds());
RequestManager.clearOldRequests(this, Config.requestTimeoutSeconds());
}

/**
Expand Down
Loading