bentobox/src/main/java/world/bentobox/bentobox/listeners/teleports/PlayerTeleportListener.java

503 lines
19 KiB
Java

//
// Created by BONNe
// Copyright - 2022
//
package world.bentobox.bentobox.listeners.teleports;
import java.util.Objects;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityPortalEnterEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerPortalEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.util.Vector;
import org.eclipse.jdt.annotation.NonNull;
import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.blueprints.Blueprint;
import world.bentobox.bentobox.blueprints.BlueprintPaster;
import world.bentobox.bentobox.blueprints.dataobjects.BlueprintBundle;
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.util.Util;
import world.bentobox.bentobox.util.teleport.ClosestSafeSpotTeleport;
/**
* This class handles player teleportation between dimensions.
*
* @author tastybento and BONNe
*/
public class PlayerTeleportListener extends AbstractTeleportListener implements Listener
{
/**
* Instantiates a new Portal teleportation listener.
*
* @param plugin the plugin
*/
public PlayerTeleportListener(@NonNull BentoBox plugin)
{
super(plugin);
}
// ---------------------------------------------------------------------
// Section: Listeners
// ---------------------------------------------------------------------
/**
* This listener checks player portal events and triggers appropriate methods to transfer
* players to the correct location in other dimension.
* <p>
* This event is triggered when player is about to being teleported because of contact with the
* nether portal or end gateway portal (exit portal triggers respawn).
* <p>
* This event is not called if nether/end is disabled in server settings.
*
* @param event the player portal event.
*/
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onPlayerPortalEvent(PlayerPortalEvent event)
{
switch (event.getCause())
{
case NETHER_PORTAL -> this.portalProcess(event, World.Environment.NETHER);
case END_PORTAL, END_GATEWAY -> this.portalProcess(event, World.Environment.THE_END);
default -> throw new IllegalArgumentException("Unexpected value: " + event.getCause());
}
}
/**
* Fires the event if nether or end is disabled at the system level
*
* @param event - EntityPortalEnterEvent
*/
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onPlayerPortal(EntityPortalEnterEvent event)
{
if (!EntityType.PLAYER.equals(event.getEntity().getType()))
{
// This handles only players.
return;
}
Entity entity = event.getEntity();
Material type = event.getLocation().getBlock().getType();
UUID uuid = entity.getUniqueId();
if (this.inPortal.contains(uuid) ||
!this.plugin.getIWM().inWorld(Util.getWorld(event.getLocation().getWorld())))
{
return;
}
this.inPortal.add(uuid);
// Add original world for respawning.
this.teleportOrigin.put(uuid, event.getLocation().getWorld());
if (!Bukkit.getAllowNether() && type.equals(Material.NETHER_PORTAL))
{
// Schedule a time
Bukkit.getScheduler().runTaskLater(this.plugin, () ->
{
// Check again if still in portal
if (this.inPortal.contains(uuid))
{
// Create new PlayerPortalEvent
PlayerPortalEvent en = new PlayerPortalEvent((Player) entity,
event.getLocation(),
null,
PlayerTeleportEvent.TeleportCause.NETHER_PORTAL,
0,
false,
0);
this.portalProcess(en, World.Environment.NETHER);
}
}, 40);
return;
}
// End portals are instant transfer
if (!Bukkit.getAllowEnd() && (type.equals(Material.END_PORTAL) || type.equals(Material.END_GATEWAY)))
{
// Create new PlayerPortalEvent
PlayerPortalEvent en = new PlayerPortalEvent((Player) entity,
event.getLocation(),
null,
type.equals(Material.END_PORTAL) ? PlayerTeleportEvent.TeleportCause.END_PORTAL : PlayerTeleportEvent.TeleportCause.END_GATEWAY,
0,
false,
0);
this.portalProcess(en, World.Environment.THE_END);
}
}
/**
* Remove inPortal flag only when player exits the portal
*
* @param event player move event
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onExitPortal(PlayerMoveEvent event)
{
if (!this.inPortal.contains(event.getPlayer().getUniqueId()))
{
return;
}
if (event.getTo() != null && !event.getTo().getBlock().getType().equals(Material.NETHER_PORTAL))
{
// Player exits nether portal.
this.inPortal.remove(event.getPlayer().getUniqueId());
this.inTeleport.remove(event.getPlayer().getUniqueId());
this.teleportOrigin.remove(event.getPlayer().getUniqueId());
}
}
/**
* Player respawn event is triggered when player enters exit portal at the end.
* This will take over respawn mechanism and place player on island.
* @param event player respawn event
*/
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerExitPortal(PlayerRespawnEvent event)
{
if (!this.teleportOrigin.containsKey(event.getPlayer().getUniqueId()))
{
// Player is already processed.
return;
}
World fromWorld = this.teleportOrigin.get(event.getPlayer().getUniqueId());
World overWorld = Util.getWorld(fromWorld);
if (overWorld == null || !this.plugin.getIWM().inWorld(overWorld))
{
// Not teleporting from/to bentobox worlds.
return;
}
this.getIsland(overWorld, event.getPlayer()).ifPresentOrElse(island -> {
if (!island.onIsland(event.getRespawnLocation()))
{
// If respawn location is outside island protection range, change location to the
// spawn in overworld or home location.
Location location = island.getSpawnPoint(World.Environment.NORMAL);
if (location == null)
{
// No spawn point. Rare thing. Well, use island protection center.
location = island.getProtectionCenter();
}
event.setRespawnLocation(location);
}
},
() -> {
// Player does not an island. Try to get spawn island, and if that fails, use world spawn point.
// If spawn point is not safe, do nothing. Let server handle it.
Location spawnLocation = this.getSpawnLocation(overWorld);
if (spawnLocation != null)
{
event.setRespawnLocation(spawnLocation);
}
});
}
// ---------------------------------------------------------------------
// Section: Processors
// ---------------------------------------------------------------------
/**
* This method process player teleportation to new dimension.
* @param event Event that triggers teleportation.
* @param environment Environment of portal type.
*/
private void portalProcess(PlayerPortalEvent event, World.Environment environment)
{
World fromWorld = event.getFrom().getWorld();
World overWorld = Util.getWorld(fromWorld);
if (overWorld == null || !this.plugin.getIWM().inWorld(overWorld))
{
// Not teleporting from/to bentobox worlds.
return;
}
if (!this.isAllowedInConfig(overWorld, environment))
{
// World is disabled in config. Do not teleport player.
event.setCancelled(true);
return;
}
if (!this.isAllowedOnServer(environment))
{
// World is disabled in bukkit. Event is not triggered, but cancel by chance.
event.setCancelled(true);
}
if (this.inTeleport.contains(event.getPlayer().getUniqueId()))
{
// Player is already in teleportation.
return;
}
this.inTeleport.add(event.getPlayer().getUniqueId());
if (fromWorld.equals(overWorld) && !this.isIslandWorld(overWorld, environment))
{
// This is not island world. Use standard nether or end world teleportation.
this.handleToStandardNetherOrEnd(event, overWorld, environment);
return;
}
if (!fromWorld.equals(overWorld) && !this.isIslandWorld(overWorld, environment))
{
// If entering a portal in the other world, teleport to a portal in overworld if
// there is one
this.handleFromStandardNetherOrEnd(event, overWorld, environment);
return;
}
// To the nether/end or overworld.
World toWorld = !fromWorld.getEnvironment().equals(environment) ?
this.getNetherEndWorld(overWorld, environment) : overWorld;
// Set whether portals should be created or not
event.setCanCreatePortal(this.isMakePortals(overWorld, environment));
// Default 16 is will always end up placing portal as close to X/8 coordinate as possible.
// In most situations, 2 block value should be enough... I hope.
event.setCreationRadius(2);
// Set the destination location
// If portals cannot be created, then destination is the spawn point, otherwise it's the vector
event.setTo(this.calculateLocation(event.getFrom(), fromWorld, toWorld, environment, event.getCanCreatePortal()));
// Find the distance from edge of island's protection and set the search radius
this.getIsland(event.getTo()).ifPresent(island ->
event.setSearchRadius(this.calculateSearchRadius(event.getTo(), island)));
// Check if there is an island there or not
if (this.isPastingMissingIslands(overWorld) &&
this.isAllowedInConfig(overWorld, environment) &&
this.isIslandWorld(overWorld, environment) &&
this.getNetherEndWorld(overWorld, environment) != null &&
this.getIsland(event.getTo()).
filter(island -> !this.hasPartnerIsland(island, environment)).
map(island -> {
event.setCancelled(true);
this.pasteNewIsland(event.getPlayer(), event.getTo(), island, environment);
return true;
}).
orElse(false))
{
// If there is no island, then processor already created island. Nothing to do more.
return;
}
if (!event.isCancelled() && event.getCanCreatePortal())
{
// Let the server teleport
return;
}
if (World.Environment.THE_END.equals(environment))
{
// Prevent death from hitting the ground while calculating location.
event.getPlayer().setVelocity(new Vector(0,0,0));
event.getPlayer().setFallDistance(0);
}
// If we do not generate portals, teleportation should happen manually with safe spot builder.
// Otherwise, we could end up with situations when player is placed in mid air, if teleportation
// is done instantly.
// Our safe spot task is triggered in next tick, however, end teleportation happens in the same tick.
// It is placed outside THE_END check, as technically it could happen with the nether portal too.
// If there is a portal to go to already, then the player will go there
Bukkit.getScheduler().runTask(this.plugin, () -> {
if (!event.getPlayer().getWorld().equals(toWorld))
{
// Else manually teleport entity
ClosestSafeSpotTeleport.builder(this.plugin).
entity(event.getPlayer()).
location(event.getTo()).
portal().
successRunnable(() -> {
// Reset velocity just in case.
event.getPlayer().setVelocity(new Vector(0,0,0));
event.getPlayer().setFallDistance(0);
}).
build();
}
});
}
/**
* Handle teleport from or to standard nether or end
* @param event - PlayerPortalEvent
* @param overWorld - over world
* @param environment - environment involved
*/
private void handleToStandardNetherOrEnd(PlayerPortalEvent event,
World overWorld,
World.Environment environment)
{
World toWorld = Objects.requireNonNull(this.getNetherEndWorld(overWorld, environment));
Location spawnPoint = toWorld.getSpawnLocation();
// If going to the nether and nether portals are active then just teleport to approx location
if (World.Environment.NETHER.equals(environment) &&
this.plugin.getIWM().getWorldSettings(overWorld).isMakeNetherPortals())
{
spawnPoint = event.getFrom().toVector().toLocation(toWorld);
}
// If spawn is set as 0,63,0 in the End then move it to 100, 50 ,0.
if (World.Environment.THE_END.equals(environment) && spawnPoint.getBlockX() == 0 && spawnPoint.getBlockZ() == 0)
{
// Set to the default end spawn
spawnPoint = new Location(toWorld, 100, 50, 0);
toWorld.setSpawnLocation(100, 50, 0);
}
if (this.isAllowedOnServer(environment))
{
// To Standard Nether or end
event.setTo(spawnPoint);
}
else
{
// Teleport to standard nether or end
ClosestSafeSpotTeleport.builder(this.plugin).
entity(event.getPlayer()).
location(spawnPoint).
portal().
build();
}
}
/**
* Handle teleport from or to standard nether or end (end is not possible because EXIT PORTAL triggers RESPAWN event)
* @param event - PlayerPortalEvent
* @param overWorld - over world
* @param environment - environment involved
*/
private void handleFromStandardNetherOrEnd(PlayerPortalEvent event, World overWorld, World.Environment environment)
{
if (World.Environment.NETHER.equals(environment) &&
this.plugin.getIWM().getWorldSettings(overWorld).isMakeNetherPortals())
{
// Set to location directly to the from location.
event.setTo(event.getFrom().toVector().toLocation(overWorld));
// Update portal search radius.
this.getIsland(event.getTo()).ifPresent(island ->
event.setSearchRadius(this.calculateSearchRadius(event.getTo(), island)));
event.setCanCreatePortal(true);
// event.setCreationRadius(16); 16 is default creation radius.
}
else
{
// Cannot be portal. Should recalculate position.
// TODO: Currently, it is always spawn location. However, default home must be assigned.
Location toLocation = this.getIsland(overWorld, event.getPlayer()).
map(island -> island.getSpawnPoint(World.Environment.NORMAL)).
orElseGet(() -> {
// If player do not have island, try spawn.
Location spawnLocation = this.getSpawnLocation(overWorld);
return spawnLocation == null ?
event.getFrom().toVector().toLocation(overWorld) :
spawnLocation;
});
event.setTo(toLocation);
}
if (!this.isAllowedOnServer(environment))
{
// Custom portal handling.
event.setCancelled(true);
// Teleport to standard nether or end
ClosestSafeSpotTeleport.builder(this.plugin).
entity(event.getPlayer()).
location(event.getTo()).
portal().
build();
}
}
/**
* Pastes the default nether or end island and teleports the player to the island's spawn point
* @param player - player to teleport after pasting
* @param to - the fallback location if a spawn point is not part of the blueprint
* @param island - the island
* @param environment - NETHER or THE_END
*/
private void pasteNewIsland(Player player,
Location to,
Island island,
World.Environment environment)
{
// Paste then teleport player
this.plugin.getIWM().getAddon(island.getWorld()).ifPresent(addon ->
{
// Get the default bundle's nether or end blueprint
BlueprintBundle blueprintBundle = plugin.getBlueprintsManager().getDefaultBlueprintBundle(addon);
if (blueprintBundle != null)
{
Blueprint bluePrint = this.plugin.getBlueprintsManager().getBlueprints(addon).
get(blueprintBundle.getBlueprint(environment));
if (bluePrint != null)
{
new BlueprintPaster(this.plugin, bluePrint, to.getWorld(), island).
paste().
thenAccept(state -> ClosestSafeSpotTeleport.builder(this.plugin).
entity(player).
location(island.getSpawnPoint(environment) == null ? to : island.getSpawnPoint(environment)).
portal().
build());
}
else
{
this.plugin.logError("Could not paste default island in nether or end. " +
"Is there a nether-island or end-island blueprint?");
}
}
});
}
}