Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,50 @@ protected Location calculateLocation(Location fromLocation,
}


/**
* Checks whether an arrival in The End would leave the player or entity with nothing to
* stand on.
* <p>
* When the game mode does not make End portals (no obsidian platform), the server still
* performs the teleport itself, in the same tick, to the exact destination. If that spot
* has no ground the arrival falls into the void before the safe spot teleport can run.
* Callers cancel the event when this returns {@code true} so the safe spot teleport is used.
*
* @param overWorld the game mode's overworld.
* @param environment the portal's environment.
* @param destination the arrival location, may be {@code null}.
* @return {@code true} if the arrival is in The End without a platform and the spot is not safe.
*/
protected boolean isUnsafeEndArrival(World overWorld, World.Environment environment, @Nullable Location destination)
{
return World.Environment.THE_END.equals(environment)
&& !this.isMakePortals(overWorld, environment)
&& destination != null
&& destination.getWorld() != null
&& World.Environment.THE_END.equals(destination.getWorld().getEnvironment())
&& !this.plugin.getIslands().isSafeLocation(destination);
}


/**
* Checks whether a repeat portal event, fired while an earlier one is still being handled,
* belongs to an End trip without a platform.
* <p>
* Without a platform the first event is either left to the server (the traveller is gone and
* no repeat follows) or cancelled in favour of a safe spot teleport that takes a few ticks.
* While the traveller still stands in the portal the server keeps firing portal events; any
* of them left uncancelled would send the traveller to the vanilla End spawn instead.
*
* @param overWorld the game mode's overworld.
* @param environment the portal's environment.
* @return {@code true} if repeat events for this trip must be cancelled.
*/
protected boolean isPendingEndTeleport(World overWorld, World.Environment environment)
{
return World.Environment.THE_END.equals(environment) && !this.isMakePortals(overWorld, environment);
}


/**
* Builds the End obsidian arrival platform at the given destination.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@ private void portalProcess(EntityPortalEvent event, World.Environment environmen
if (this.inTeleport.contains(event.getEntity().getUniqueId()))
{
// Entity is already in teleportation.
if (this.isPendingEndTeleport(overWorld, environment))
{
// A safe spot teleport is still on its way; do not let the server teleport instead.
event.setCancelled(true);
}
return;
}
this.inTeleport.add(event.getEntity().getUniqueId());
Expand Down Expand Up @@ -275,6 +280,13 @@ private void portalProcess(EntityPortalEvent event, World.Environment environmen
return;
}

if (!event.isCancelled() && this.isUnsafeEndArrival(overWorld, environment, event.getTo()))
{
// No End platform and nothing to stand on: stop the server teleport and let the
// safe spot teleport below find somewhere safe.
event.setCancelled(true);
}

if (!event.isCancelled())
{
// Let the server teleport
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,11 @@ private void portalProcess(PlayerPortalEvent event, World.Environment environmen
if (this.inTeleport.contains(event.getPlayer().getUniqueId()))
{
// Player is already in teleportation.
if (this.isPendingEndTeleport(overWorld, environment))
{
// A safe spot teleport is still on its way; do not let the server teleport instead.
event.setCancelled(true);
}
return;
}
this.inTeleport.add(event.getPlayer().getUniqueId());
Expand Down Expand Up @@ -346,6 +351,12 @@ private void portalProcess(PlayerPortalEvent event, World.Environment environmen
// If there is no island, then processor already created island. Nothing to do more.
return;
}
if (!event.isCancelled() && this.isUnsafeEndArrival(overWorld, environment, event.getTo()))
{
// No End platform and nothing to stand on: stop the server teleport and let the
// safe spot teleport below find somewhere safe.
event.setCancelled(true);
}
if (!event.isCancelled() && event.getCanCreatePortal())
{
// Let the server teleport
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,8 +448,13 @@ void makeAndTeleport(Material baseMaterial)
this.location.getBlock().getRelative(BlockFace.UP).setType(Material.AIR, false);
this.location.getBlock().getRelative(BlockFace.UP).getRelative(BlockFace.UP).setType(baseMaterial, false);

// Teleport player to the location of the empty space.
this.asyncTeleport(this.location.clone().add(new Vector(0.5D, 0D, 0.5D)));
// Teleport player to the centre of the empty space. Use the block coordinates, as the
// location may already be off-centre and adding 0.5 would land on the block's edge.
Location centre = this.location.clone();
centre.setX(this.location.getBlockX() + 0.5D);
centre.setY(this.location.getBlockY());
centre.setZ(this.location.getBlockZ() + 0.5D);
this.asyncTeleport(centre);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,13 @@ void makeAndTeleport(Material m) {
location.getBlock().setType(Material.AIR, false);
location.getBlock().getRelative(BlockFace.UP).setType(Material.AIR, false);
location.getBlock().getRelative(BlockFace.UP).getRelative(BlockFace.UP).setType(m, false);
Util.teleportAsync(Objects.requireNonNull(entity),
Objects.requireNonNull(location.clone().add(new Vector(0.5D, 0D, 0.5D)))).thenRun(() -> {
// Centre on the block, as the location may already be off-centre and adding 0.5 would
// land on the block's edge.
Location centre = location.clone();
centre.setX(location.getBlockX() + 0.5D);
centre.setY(location.getBlockY());
centre.setZ(location.getBlockZ() + 0.5D);
Util.teleportAsync(Objects.requireNonNull(entity), centre).thenRun(() -> {
if (runnable != null) Bukkit.getScheduler().runTask(plugin, runnable);
result.complete(true);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import com.google.common.collect.ImmutableSet;

import world.bentobox.bentobox.CommonTestSetup;
import world.bentobox.bentobox.api.addons.GameModeAddon;
import world.bentobox.bentobox.api.configuration.WorldSettings;
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.util.Util;
Expand Down Expand Up @@ -260,6 +261,101 @@ void testPortalProcessEndVelocityReset() {
verify(mockPlayer, times(1)).setFallDistance(0);
}

/**
* Sets up an island End trip with no End platform (create-obsidian-platform: false),
* arriving at a location in a separate End world.
*
* @return the arrival location
*/
private Location setUpEndArrivalWithoutPlatform() {
when(Util.getWorld(location.getWorld())).thenReturn(world);
when(iwm.inWorld(world)).thenReturn(true);
World endWorld = mock(World.class);
when(endWorld.getEnvironment()).thenReturn(Environment.THE_END);
when(iwm.getEndWorld(world)).thenReturn(endWorld);
Location arrival = mock(Location.class);
when(arrival.getWorld()).thenReturn(endWorld);
// PlayerPortalEvent.setTo stores a clone
when(arrival.clone()).thenReturn(arrival);
when(location.toVector().toLocation(endWorld)).thenReturn(arrival);
return arrival;
}

/**
* With no End platform, an arrival spot with nothing to stand on must not be left to the
* server, which would drop the player into the void. The event is cancelled so the safe
* spot teleport is used instead.
*/
@Test
void testPortalProcessEndNoPlatformUnsafeArrivalCancels() {
Location arrival = setUpEndArrivalWithoutPlatform();
when(im.isSafeLocation(arrival)).thenReturn(false);
PlayerPortalEvent e = new PlayerPortalEvent(mockPlayer, location, location, TeleportCause.END_PORTAL, 0, false,
0);

ptl.onPlayerPortalEvent(e);

assertTrue(e.isCancelled());
assertSame(arrival, e.getTo());
}

/**
* With no End platform but solid ground at the arrival spot, the server does the teleport.
*/
@Test
void testPortalProcessEndNoPlatformSafeArrivalLetsServerTeleport() {
Location arrival = setUpEndArrivalWithoutPlatform();
when(im.isSafeLocation(arrival)).thenReturn(true);
PlayerPortalEvent e = new PlayerPortalEvent(mockPlayer, location, location, TeleportCause.END_PORTAL, 0, false,
0);

ptl.onPlayerPortalEvent(e);

assertFalse(e.isCancelled());
assertSame(arrival, e.getTo());
}

/**
* With no End platform, a repeat portal event fired while the safe spot teleport is still
* pending must also be cancelled, or the server would send the player to the vanilla End spawn.
*/
@Test
void testPortalProcessEndNoPlatformRepeatEventCancels() {
Location arrival = setUpEndArrivalWithoutPlatform();
when(im.isSafeLocation(arrival)).thenReturn(false);
PlayerPortalEvent first = new PlayerPortalEvent(mockPlayer, location, location, TeleportCause.END_PORTAL, 0,
false, 0);
ptl.onPlayerPortalEvent(first);
assertTrue(ptl.getInTeleport().contains(mockPlayer.getUniqueId()));

PlayerPortalEvent repeat = new PlayerPortalEvent(mockPlayer, location, location, TeleportCause.END_PORTAL, 0,
false, 0);
ptl.onPlayerPortalEvent(repeat);

assertTrue(repeat.isCancelled());
}

/**
* With the End platform on, a repeat portal event is left alone, as before.
*/
@Test
void testPortalProcessEndWithPlatformRepeatEventNotCancelled() {
when(Util.getWorld(location.getWorld())).thenReturn(world);
when(iwm.inWorld(world)).thenReturn(true);
GameModeAddon gm = mock(GameModeAddon.class);
WorldSettings ws = mock(WorldSettings.class);
when(ws.isMakeEndPortals()).thenReturn(true);
when(gm.getWorldSettings()).thenReturn(ws);
when(iwm.getAddon(world)).thenReturn(Optional.of(gm));
ptl.getInTeleport().add(mockPlayer.getUniqueId());

PlayerPortalEvent repeat = new PlayerPortalEvent(mockPlayer, location, location, TeleportCause.END_PORTAL, 0,
false, 0);
ptl.onPlayerPortalEvent(repeat);

assertFalse(repeat.isCancelled());
}

/**
* Verifies that {@link AbstractTeleportListener#createEndPlatform(Location)} lays a 5x5
* obsidian floor and clears 5x5x3 of air above it when the destination is in The End.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,26 @@ void testMakeAndTeleport_endStone_setsExpectedBlocks() {
verify(blockUpUp).setType(Material.END_STONE, false);
}

/**
* {@code makeAndTeleport()} teleports to the centre of the pod's block even when the
* requested location is off-centre. Adding 0.5 to x = -0.5 would land on the block edge.
*/
@Test
void testMakeAndTeleport_offCentreLocation_teleportsToBlockCentre() {
Location offCentre = new Location(world, -0.5D, 49.3D, 3.7D, 90F, 10F);
Block podBlock = mock(Block.class);
when(world.getBlockAt(any(Location.class))).thenReturn(podBlock);
when(podBlock.getRelative(BlockFace.DOWN)).thenReturn(blockDown);
when(podBlock.getRelative(BlockFace.UP)).thenReturn(blockUp);
teleport = ClosestSafeSpotTeleport.builder(plugin).entity(mockPlayer).location(offCentre).build();

teleport.makeAndTeleport(Material.END_STONE);

verify(blockDown).setType(Material.END_STONE, false);
mockedUtil.verify(() -> Util.teleportAsync(eq(mockPlayer),
eq(new Location(world, -0.5D, 49D, 3.5D, 90F, 10F))));
}

// -----------------------------------------------------------------------
// gatherChunks() tests
// -----------------------------------------------------------------------
Expand Down
Loading