Skip to content

Commit

Permalink
Add teleport flags (#6498)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: sovdee <[email protected]>
Co-authored-by: Moderocky <[email protected]>
  • Loading branch information
3 people committed Jan 1, 2025
1 parent d8664fc commit 2d4c75c
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 72 deletions.
19 changes: 8 additions & 11 deletions src/main/java/ch/njol/skript/bukkitutil/EntityUtils.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
package ch.njol.skript.bukkitutil;

import org.bukkit.Location;
import org.bukkit.entity.Ageable;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Piglin;
import org.bukkit.entity.Zoglin;
import org.bukkit.entity.Zombie;

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;

import ch.njol.skript.Skript;
import ch.njol.skript.entity.EntityData;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import org.bukkit.Location;
import org.bukkit.entity.*;
import org.jetbrains.annotations.ApiStatus.ScheduledForRemoval;

/**
* Utility class for quick {@link Entity} methods
Expand Down Expand Up @@ -138,7 +132,10 @@ public static EntityData<?> toSkriptEntityData(EntityType e) {
/**
* Teleports the given entity to the given location.
* Teleports to the given location in the entity's world if the location's world is null.
* @deprecated this method is only used by EffTeleport, and with the recent additions of TeleportFlag, this method should be moved within that effect.
*/
@Deprecated
@ScheduledForRemoval
public static void teleport(Entity entity, Location location) {
if (location.getWorld() == null) {
location = location.clone();
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/ch/njol/skript/bukkitutil/SkriptTeleportFlag.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package ch.njol.skript.bukkitutil;

import io.papermc.paper.entity.TeleportFlag;
import io.papermc.paper.entity.TeleportFlag.EntityState;
import io.papermc.paper.entity.TeleportFlag.Relative;

/**
* A utility enum for accessing Paper's teleport flags (1.19.4+)
*/
public enum SkriptTeleportFlag {

RETAIN_OPEN_INVENTORY(EntityState.RETAIN_OPEN_INVENTORY),
RETAIN_PASSENGERS(EntityState.RETAIN_PASSENGERS),
RETAIN_VEHICLE(EntityState.RETAIN_VEHICLE),
RETAIN_DIRECTION(Relative.PITCH, Relative.YAW),
RETAIN_PITCH(Relative.PITCH),
RETAIN_YAW(Relative.YAW),
RETAIN_MOVEMENT(Relative.X, Relative.Y, Relative.Z),
RETAIN_X(Relative.X),
RETAIN_Y(Relative.Y),
RETAIN_Z(Relative.Z);

final TeleportFlag[] teleportFlags;

SkriptTeleportFlag(TeleportFlag... teleportFlags) {
this.teleportFlags = teleportFlags;
}

public TeleportFlag[] getTeleportFlags() {
return teleportFlags;
}

}
8 changes: 8 additions & 0 deletions src/main/java/ch/njol/skript/classes/data/BukkitClasses.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import ch.njol.skript.aliases.ItemType;
import ch.njol.skript.bukkitutil.BukkitUtils;
import ch.njol.skript.bukkitutil.EntityUtils;
import ch.njol.skript.bukkitutil.SkriptTeleportFlag;
import ch.njol.skript.bukkitutil.ItemUtils;
import ch.njol.skript.classes.*;
import ch.njol.skript.classes.registry.RegistryClassInfo;
Expand Down Expand Up @@ -1582,6 +1583,13 @@ public String toVariableNameString(EntitySnapshot snapshot) {
.since("INSERT VERSION")
);

if (Skript.classExists("io.papermc.paper.entity.TeleportFlag"))
Classes.registerClass(new EnumClassInfo<>(SkriptTeleportFlag.class, "teleportflag", "teleport flags")
.user("teleport ?flags?")
.name("Teleport Flag")
.description("Teleport Flags are settings to retain during a teleport.")
.requiredPlugins("Paper 1.19+")
.since("INSERT VERSION"));
}

}
152 changes: 91 additions & 61 deletions src/main/java/ch/njol/skript/effects/EffTeleport.java
Original file line number Diff line number Diff line change
@@ -1,143 +1,152 @@
package ch.njol.skript.effects;

import ch.njol.skript.Skript;
import ch.njol.skript.sections.EffSecSpawn.SpawnEvent;
import ch.njol.skript.bukkitutil.EntityUtils;
import ch.njol.skript.doc.Description;
import ch.njol.skript.doc.Examples;
import ch.njol.skript.doc.Name;
import ch.njol.skript.doc.Since;
import ch.njol.skript.lang.Effect;
import ch.njol.skript.lang.Expression;
import ch.njol.skript.bukkitutil.SkriptTeleportFlag;
import ch.njol.skript.doc.*;
import ch.njol.skript.lang.*;
import ch.njol.skript.lang.SkriptParser.ParseResult;
import ch.njol.skript.lang.Trigger;
import ch.njol.skript.lang.TriggerItem;
import ch.njol.skript.sections.EffSecSpawn.SpawnEvent;
import ch.njol.skript.timings.SkriptTimings;
import ch.njol.skript.util.Direction;
import ch.njol.skript.variables.Variables;
import ch.njol.util.Kleenean;
import io.papermc.lib.PaperLib;
import io.papermc.lib.environments.PaperEnvironment;
import io.papermc.paper.entity.TeleportFlag;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.event.Event;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Stream;

@Name("Teleport")
@Description({
"Teleport an entity to a specific location. ",
"This effect is delayed by default on Paper, meaning certain syntax such as the return effect for functions cannot be used after this effect.",
"The keyword 'force' indicates this effect will not be delayed, ",
"which may cause lag spikes or server crashes when using this effect to teleport entities to unloaded chunks."
"which may cause lag spikes or server crashes when using this effect to teleport entities to unloaded chunks.",
"Teleport flags are settings to retain during a teleport. Such as direction, passengers, x coordinate, etc."
})
@Examples({
"teleport the player to {homes::%player%}",
"teleport the attacker to the victim"
"teleport the player to {home::%uuid of player%}",
"teleport the attacker to the victim",
"",
"on dismount:",
"\tcancel event",
"\tteleport the player to {server::spawn} retaining vehicle and passengers"
})
@Since("1.0")
@RequiredPlugins("Paper 1.19+ (teleport flags)")
@Since("1.0, INSERT VERSION (flags)")
public class EffTeleport extends Effect {

private static final boolean TELEPORT_FLAGS_SUPPORTED = Skript.classExists("io.papermc.paper.entity.TeleportFlag");
private static final boolean CAN_RUN_ASYNC = PaperLib.getEnvironment() instanceof PaperEnvironment;

static {
Skript.registerEffect(EffTeleport.class, "[(1¦force)] teleport %entities% (to|%direction%) %location%");
String extra = "";
if (TELEPORT_FLAGS_SUPPORTED)
extra = " [[while] retaining %-teleportflags%]";
Skript.registerEffect(EffTeleport.class, "[:force] teleport %entities% (to|%direction%) %location%" + extra);
}

@SuppressWarnings("NotNullFieldNotInitialized")
private @Nullable Expression<SkriptTeleportFlag> teleportFlags;
private Expression<Entity> entities;
@SuppressWarnings("NotNullFieldNotInitialized")
private Expression<Location> location;

private boolean isAsync;
private boolean async;

@Override
@SuppressWarnings("unchecked")
public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
entities = (Expression<Entity>) exprs[0];
location = Direction.combine((Expression<? extends Direction>) exprs[1], (Expression<? extends Location>) exprs[2]);
isAsync = CAN_RUN_ASYNC && parseResult.mark == 0;
async = CAN_RUN_ASYNC && !parseResult.hasTag("force");
if (TELEPORT_FLAGS_SUPPORTED)
teleportFlags = (Expression<SkriptTeleportFlag>) exprs[3];

if (getParser().isCurrentEvent(SpawnEvent.class)) {
Skript.error("You cannot be teleporting an entity that hasn't spawned yet. Ensure you're using the location expression from the spawn section pattern.");
Skript.error("You cannot teleport an entity that hasn't spawned yet. Ensure you're using the location expression from the spawn section pattern.");
return false;
}

if (isAsync)
if (async)
getParser().setHasDelayBefore(Kleenean.UNKNOWN); // UNKNOWN because it isn't async if the chunk is already loaded.
return true;
}

@Nullable
@Override
protected TriggerItem walk(Event e) {
debug(e, true);

protected @Nullable TriggerItem walk(Event event) {
debug(event, true);
TriggerItem next = getNext();

boolean delayed = Delay.isDelayed(e);

Location loc = location.getSingle(e);
if (loc == null)
boolean delayed = Delay.isDelayed(event);
Location location = this.location.getSingle(event);
if (location == null)
return next;
boolean unknownWorld = !loc.isWorldLoaded();
boolean unknownWorld = !location.isWorldLoaded();

Entity[] entityArray = entities.getArray(e); // We have to fetch this before possible async execution to avoid async local variable access.
Entity[] entityArray = entities.getArray(event); // We have to fetch this before possible async execution to avoid async local variable access.
if (entityArray.length == 0)
return next;

if (!delayed) {
if (e instanceof PlayerRespawnEvent && entityArray.length == 1 && entityArray[0].equals(((PlayerRespawnEvent) e).getPlayer())) {
if (event instanceof PlayerRespawnEvent playerRespawnEvent && entityArray.length == 1 && entityArray[0].equals(playerRespawnEvent.getPlayer())) {
if (unknownWorld)
return next;
((PlayerRespawnEvent) e).setRespawnLocation(loc);
playerRespawnEvent.setRespawnLocation(location);
return next;
}

if (e instanceof PlayerMoveEvent && entityArray.length == 1 && entityArray[0].equals(((PlayerMoveEvent) e).getPlayer())) {
if (event instanceof PlayerMoveEvent playerMoveEvent && entityArray.length == 1 && entityArray[0].equals(playerMoveEvent.getPlayer())) {
if (unknownWorld) { // we can approximate the world
loc = loc.clone();
loc.setWorld(((PlayerMoveEvent) e).getFrom().getWorld());
location = location.clone();
location.setWorld(playerMoveEvent.getFrom().getWorld());
}
((PlayerMoveEvent) e).setTo(loc);
playerMoveEvent.setTo(location);
return next;
}
}

if (!isAsync) {
for (Entity entity : entityArray) {
EntityUtils.teleport(entity, loc);
}
return next;
}

if (unknownWorld) { // we can't fetch the chunk without a world
if (entityArray.length == 1) { // if there's 1 thing we can borrow its world
Entity entity = entityArray[0];
if (entity == null)
return next;
// assume it's a local teleport, use the first entity we find as a reference
loc = loc.clone();
loc.setWorld(entity.getWorld());
location = location.clone();
location.setWorld(entity.getWorld());
} else {
return next; // no entities = no chunk = nobody teleporting
}
}
final Location fixed = loc;
Delay.addDelayedEvent(e);
Object localVars = Variables.removeLocals(e);


if (!async) {
SkriptTeleportFlag[] teleportFlags = this.teleportFlags == null ? null : this.teleportFlags.getArray(event);
for (Entity entity : entityArray) {
teleport(entity, location, teleportFlags);
}
return next;
}

final Location fixed = location;
Delay.addDelayedEvent(event);
Object localVars = Variables.removeLocals(event);

// This will either fetch the chunk instantly if on Spigot or already loaded or fetch it async if on Paper.
PaperLib.getChunkAtAsync(loc).thenAccept(chunk -> {
PaperLib.getChunkAtAsync(location).thenAccept(chunk -> {
// The following is now on the main thread
SkriptTeleportFlag[] teleportFlags = this.teleportFlags == null ? null : this.teleportFlags.getArray(event);
for (Entity entity : entityArray) {
EntityUtils.teleport(entity, fixed);
teleport(entity, fixed, teleportFlags);
}

// Re-set local variables
if (localVars != null)
Variables.setLocalVariables(e, localVars);
Variables.setLocalVariables(event, localVars);

// Continue the rest of the trigger if there is one
Object timing = null;
Expand All @@ -149,22 +158,43 @@ protected TriggerItem walk(Event e) {
}
}

TriggerItem.walk(next, e);
TriggerItem.walk(next, event);
}
Variables.removeLocals(e); // Clean up local vars, we may be exiting now
Variables.removeLocals(event); // Clean up local vars, we may be exiting now
SkriptTimings.stop(timing);
});
return null;
}

@Override
protected void execute(Event e) {
// Nothing needs to happen here, we're executing in walk
protected void execute(Event event) {
assert false;
}

@Override
public String toString(@Nullable Event e, boolean debug) {
return "teleport " + entities.toString(e, debug) + " to " + location.toString(e, debug);
public String toString(@Nullable Event event, boolean debug) {
SyntaxStringBuilder builder = new SyntaxStringBuilder(event, debug)
.append("teleport", entities, "to", location);
if (teleportFlags != null)
builder.append("retaining", teleportFlags);
return builder.toString();
}

private void teleport(@NotNull Entity entity, @NotNull Location location, SkriptTeleportFlag... skriptTeleportFlags) {
if (location.getWorld() == null) {
location = location.clone();
location.setWorld(entity.getWorld());
}

if (!TELEPORT_FLAGS_SUPPORTED || skriptTeleportFlags == null) {
entity.teleport(location);
return;
}

Stream<TeleportFlag> teleportFlags = Arrays.stream(skriptTeleportFlags)
.flatMap(teleportFlag -> Stream.of(teleportFlag.getTeleportFlags()))
.filter(Objects::nonNull);
entity.teleport(location, teleportFlags.toArray(TeleportFlag[]::new));
}

}
14 changes: 14 additions & 0 deletions src/main/resources/lang/default.lang
Original file line number Diff line number Diff line change
Expand Up @@ -2401,6 +2401,19 @@ transform reasons:
unknown: unknown
infection: infection, villager infection

# -- Teleport Flags --
teleport flags:
retain_open_inventory: opened inventory, open inventory, inventory
retain_passengers: passengers, passenger
retain_vehicle: vehicle
retain_direction: directional velocity, pitch and yaw velocity, yaw and pitch velocity
retain_pitch: pitch velocity
retain_yaw: yaw velocity
retain_movement: xyz velocity
retain_x: x velocity
retain_y: y velocity
retain_z: z velocity

# -- Unleash Reasons --
unleash reasons:
distance: distance
Expand Down Expand Up @@ -2625,6 +2638,7 @@ types:
inventoryclosereason: inventory close reason¦s @an
transformreason: transform reason¦s @a
unleashreason: unleash reason¦s @an
teleportflag: teleport flag¦s @a
display: display¦s @a
billboard: billboard¦s @a
textalignment: text alignment¦s @a
Expand Down
19 changes: 19 additions & 0 deletions src/test/skript/tests/syntaxes/effects/EffTeleport.sk
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
test "teleport":
spawn pig at location(0, 0, 0):
set {_pig} to entity
teleport {_pig} to location(1, 30, 1)
assert location of {_pig} is location(1, 30, 1) with "failed to teleport entity"
delete entity within {_pig}

test "teleport flags":
spawn pig at location(0, 0, 0):
set {_pig} to entity
spawn pig at location(0, 0, 0):
make entity ride {_pig}
set {_pig2} to entity
teleport {_pig} to location(1, 30, 1) retaining passengers, xyz velocity, and yaw and pitch velocity
assert location of {_pig} is location(1, 30, 1) with "failed to teleport entity"
assert passenger of {_pig} is {_pig2} with "failed to retain passenger"

delete entity within {_pig2}
delete entity within {_pig}

0 comments on commit 2d4c75c

Please sign in to comment.