mirror of
https://github.com/BentoBoxWorld/BentoBox.git
synced 2025-01-25 01:21:21 +01:00
Rework player teleportation.
I reworked the classes and some teleportation operations. Now teleportation should find the closest available spot, instead of always being the highest block at original Y location. Part of #1994. Also, I fixed an issue that portals stopped working if some conflicting options were enabled. Now portals will not work only if nether is disabled in config.
This commit is contained in:
parent
173808b787
commit
755aeb866e
@ -19,7 +19,6 @@ import world.bentobox.bentobox.api.user.Notifier;
|
||||
import world.bentobox.bentobox.api.user.User;
|
||||
import world.bentobox.bentobox.commands.BentoBoxCommand;
|
||||
import world.bentobox.bentobox.database.DatabaseSetup;
|
||||
import world.bentobox.bentobox.hooks.DynmapHook;
|
||||
import world.bentobox.bentobox.hooks.MultiverseCoreHook;
|
||||
import world.bentobox.bentobox.hooks.VaultHook;
|
||||
import world.bentobox.bentobox.hooks.placeholders.PlaceholderAPIHook;
|
||||
@ -28,7 +27,7 @@ import world.bentobox.bentobox.listeners.BlockEndDragon;
|
||||
import world.bentobox.bentobox.listeners.DeathListener;
|
||||
import world.bentobox.bentobox.listeners.JoinLeaveListener;
|
||||
import world.bentobox.bentobox.listeners.PanelListenerManager;
|
||||
import world.bentobox.bentobox.listeners.PortalTeleportationListener;
|
||||
import world.bentobox.bentobox.listeners.teleports.PlayerTeleportListener;
|
||||
import world.bentobox.bentobox.listeners.StandardSpawnProtectionListener;
|
||||
import world.bentobox.bentobox.managers.AddonsManager;
|
||||
import world.bentobox.bentobox.managers.BlueprintsManager;
|
||||
@ -288,8 +287,8 @@ public class BentoBox extends JavaPlugin {
|
||||
manager.registerEvents(new PanelListenerManager(), this);
|
||||
// Standard Nether/End spawns protection
|
||||
manager.registerEvents(new StandardSpawnProtectionListener(this), this);
|
||||
// Nether portals
|
||||
manager.registerEvents(new PortalTeleportationListener(this), this);
|
||||
// Player portals
|
||||
manager.registerEvents(new PlayerTeleportListener(this), this);
|
||||
// End dragon blocking
|
||||
manager.registerEvents(new BlockEndDragon(this), this);
|
||||
// Banned visitor commands
|
||||
|
@ -314,6 +314,10 @@ public class Settings implements ConfigObject {
|
||||
@ConfigEntry(path = "island.safe-spot-search-vertical-range", since = "1.19.1")
|
||||
private int safeSpotSearchVerticalRange = 400;
|
||||
|
||||
@ConfigComment("By default, If the destination is not safe, the plugin will try to search for a safe spot around the destination,")
|
||||
@ConfigEntry(path = "island.safe-spot-search-range", since = "1.21.0")
|
||||
private int safeSpotSearchRange = 16;
|
||||
|
||||
/* WEB */
|
||||
@ConfigComment("Toggle whether BentoBox can connect to GitHub to get data about updates and addons.")
|
||||
@ConfigComment("Disabling this will result in the deactivation of the update checker and of some other")
|
||||
@ -907,19 +911,65 @@ public class Settings implements ConfigObject {
|
||||
this.minPortalSearchRadius = minPortalSearchRadius;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets safe spot search vertical range.
|
||||
*
|
||||
* @return the safe spot search vertical range
|
||||
*/
|
||||
public int getSafeSpotSearchVerticalRange() {
|
||||
return safeSpotSearchVerticalRange;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets safe spot search vertical range.
|
||||
*
|
||||
* @param safeSpotSearchVerticalRange the safe spot search vertical range
|
||||
*/
|
||||
public void setSafeSpotSearchVerticalRange(int safeSpotSearchVerticalRange) {
|
||||
this.safeSpotSearchVerticalRange = safeSpotSearchVerticalRange;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is slow deletion boolean.
|
||||
*
|
||||
* @return the boolean
|
||||
*/
|
||||
public boolean isSlowDeletion() {
|
||||
return slowDeletion;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets slow deletion.
|
||||
*
|
||||
* @param slowDeletion the slow deletion
|
||||
*/
|
||||
public void setSlowDeletion(boolean slowDeletion) {
|
||||
this.slowDeletion = slowDeletion;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets safe spot search range.
|
||||
*
|
||||
* @return the safe spot search range
|
||||
*/
|
||||
public int getSafeSpotSearchRange()
|
||||
{
|
||||
return safeSpotSearchRange;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets safe spot search range.
|
||||
*
|
||||
* @param safeSpotSearchRange the safe spot search range
|
||||
*/
|
||||
public void setSafeSpotSearchRange(int safeSpotSearchRange)
|
||||
{
|
||||
this.safeSpotSearchRange = safeSpotSearchRange;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,296 @@
|
||||
//
|
||||
// Created by BONNe
|
||||
// Copyright - 2022
|
||||
//
|
||||
|
||||
|
||||
package world.bentobox.bentobox.listeners.teleports;
|
||||
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.event.player.PlayerPortalEvent;
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import world.bentobox.bentobox.BentoBox;
|
||||
import world.bentobox.bentobox.api.addons.GameModeAddon;
|
||||
import world.bentobox.bentobox.database.objects.Island;
|
||||
|
||||
|
||||
/**
|
||||
* This abstract class contains all common methods for entity and player teleportation.
|
||||
*/
|
||||
public abstract class AbstractTeleportListener
|
||||
{
|
||||
/**
|
||||
* Instance of Teleportation processor.
|
||||
* @param bentoBox BentoBox plugin.
|
||||
*/
|
||||
AbstractTeleportListener(@NonNull BentoBox bentoBox)
|
||||
{
|
||||
this.plugin = bentoBox;
|
||||
this.inPortal = new HashSet<>();
|
||||
this.inTeleport = new HashSet<>();
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Section: Methods
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
|
||||
/**
|
||||
* Get island at the given location
|
||||
* @return optional island at given location
|
||||
*/
|
||||
protected Optional<Island> getIsland(Location location)
|
||||
{
|
||||
return this.plugin.getIslands().getProtectedIslandAt(location);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if vanilla portals should be used
|
||||
*
|
||||
* @param world - game mode world
|
||||
* @param environment - environment
|
||||
* @return true or false
|
||||
*/
|
||||
protected boolean isMakePortals(World world, World.Environment environment)
|
||||
{
|
||||
return this.plugin.getIWM().getAddon(world).
|
||||
map(gameMode -> this.isMakePortals(gameMode, environment)).
|
||||
orElse(false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if vanilla portals should be used
|
||||
*
|
||||
* @param gameMode - game mode
|
||||
* @param environment - environment
|
||||
* @return true or false
|
||||
*/
|
||||
protected boolean isMakePortals(GameModeAddon gameMode, World.Environment environment)
|
||||
{
|
||||
return environment.equals(World.Environment.NETHER) ?
|
||||
gameMode.getWorldSettings().isMakeNetherPortals() :
|
||||
gameMode.getWorldSettings().isMakeEndPortals();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if nether or end are generated
|
||||
*
|
||||
* @param overWorld - game world
|
||||
* @param env - environment
|
||||
* @return true or false
|
||||
*/
|
||||
protected boolean isAllowedInConfig(World overWorld, World.Environment env)
|
||||
{
|
||||
return env.equals(World.Environment.NETHER) ?
|
||||
this.plugin.getIWM().isNetherGenerate(overWorld) :
|
||||
this.plugin.getIWM().isEndGenerate(overWorld);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the default nether or end are allowed by the server settings
|
||||
*
|
||||
* @param environment - environment
|
||||
* @return true or false
|
||||
*/
|
||||
protected boolean isAllowedOnServer(World.Environment environment)
|
||||
{
|
||||
return environment.equals(World.Environment.NETHER) ? Bukkit.getAllowNether() : Bukkit.getAllowEnd();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if nether or end islands are generated
|
||||
*
|
||||
* @param overWorld - over world
|
||||
* @param environment - environment
|
||||
* @return true or false
|
||||
*/
|
||||
protected boolean isIslandWorld(World overWorld, World.Environment environment)
|
||||
{
|
||||
return environment.equals(World.Environment.NETHER) ?
|
||||
this.plugin.getIWM().isNetherIslands(overWorld) :
|
||||
this.plugin.getIWM().isEndIslands(overWorld);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the nether or end world
|
||||
*
|
||||
* @param overWorld - over world
|
||||
* @param environment - environment
|
||||
* @return nether or end world
|
||||
*/
|
||||
protected World getNetherEndWorld(World overWorld, World.Environment environment)
|
||||
{
|
||||
return environment.equals(World.Environment.NETHER) ?
|
||||
this.plugin.getIWM().getNetherWorld(overWorld) :
|
||||
this.plugin.getIWM().getEndWorld(overWorld);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the island has a nether or end island already
|
||||
*
|
||||
* @param island - island
|
||||
* @param environment - environment
|
||||
* @return true or false
|
||||
*/
|
||||
protected boolean hasPartnerIsland(Island island, World.Environment environment)
|
||||
{
|
||||
return environment.equals(World.Environment.NETHER) ? island.hasNetherIsland() : island.hasEndIsland();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method calculates the maximal search area for portal.
|
||||
* @param location Location from which search should happen.
|
||||
* @param island Island that contains the search point.
|
||||
* @return Search range for portal.
|
||||
*/
|
||||
protected int calculateSearchRadius(Location location, Island island)
|
||||
{
|
||||
int diff;
|
||||
|
||||
if (island.onIsland(location))
|
||||
{
|
||||
// Find max x or max z
|
||||
int x = Math.abs(island.getProtectionCenter().getBlockX() - location.getBlockX());
|
||||
int z = Math.abs(island.getProtectionCenter().getBlockZ() - location.getBlockZ());
|
||||
|
||||
diff = Math.min(this.plugin.getSettings().getSafeSpotSearchRange(),
|
||||
island.getProtectionRange() - Math.max(x, z));
|
||||
}
|
||||
else
|
||||
{
|
||||
diff = this.plugin.getSettings().getSafeSpotSearchRange();
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method calculates location for portal.
|
||||
* @param fromLocation Location from which teleportation happens.
|
||||
* @param fromWorld World from which teleportation happens.
|
||||
* @param toWorld The target world.
|
||||
* @param environment Portal variant.
|
||||
* @param canCreatePortal Indicates if portal should be created or not.
|
||||
* @return Location for new portal.
|
||||
*/
|
||||
protected Location calculateLocation(Location fromLocation,
|
||||
World fromWorld,
|
||||
World toWorld,
|
||||
World.Environment environment,
|
||||
boolean canCreatePortal)
|
||||
{
|
||||
// Null check - not that useful
|
||||
if (fromWorld == null || toWorld == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Location toLocation = fromLocation.toVector().toLocation(toWorld);
|
||||
|
||||
if (!this.isMakePortals(fromWorld, environment))
|
||||
{
|
||||
toLocation = this.getIsland(fromLocation).
|
||||
map(island -> island.getSpawnPoint(toWorld.getEnvironment())).
|
||||
orElse(toLocation);
|
||||
}
|
||||
|
||||
// Limit Y to the min/max world height.
|
||||
toLocation.setY(Math.max(Math.min(toLocation.getY(), toWorld.getMaxHeight()), toWorld.getMinHeight()));
|
||||
|
||||
if (!canCreatePortal)
|
||||
{
|
||||
// Legacy portaling
|
||||
return toLocation;
|
||||
}
|
||||
|
||||
// Make portals
|
||||
// For anywhere other than the end - it is the player's location that is used
|
||||
if (!environment.equals(World.Environment.THE_END))
|
||||
{
|
||||
return toLocation;
|
||||
}
|
||||
|
||||
// If the-end then we want the platform to always be generated in the same place no matter where
|
||||
// they enter the portal
|
||||
final int x = fromLocation.getBlockX();
|
||||
final int z = fromLocation.getBlockZ();
|
||||
final int y = fromLocation.getBlockY();
|
||||
int i = x;
|
||||
int j = z;
|
||||
int k = y;
|
||||
|
||||
// If the from is not a portal, then we have to find it
|
||||
if (!fromLocation.getBlock().getType().equals(Material.END_PORTAL))
|
||||
{
|
||||
// Find the portal - due to speed, it is possible that the player will be below or above the portal
|
||||
for (k = toWorld.getMinHeight(); (k < fromWorld.getMaxHeight()) &&
|
||||
!fromWorld.getBlockAt(x, k, z).getType().equals(Material.END_PORTAL); k++);
|
||||
}
|
||||
|
||||
// Find the maximum x and z corner
|
||||
for (; (i < x + 5) && fromWorld.getBlockAt(i, k, z).getType().equals(Material.END_PORTAL); i++) ;
|
||||
for (; (j < z + 5) && fromWorld.getBlockAt(x, k, j).getType().equals(Material.END_PORTAL); j++) ;
|
||||
|
||||
// Mojang end platform generation is:
|
||||
// AIR
|
||||
// AIR
|
||||
// OBSIDIAN
|
||||
// and player is placed on second air block above obsidian.
|
||||
// If Y coordinate is below 2, then obsidian platform is not generated and player falls in void.
|
||||
return new Location(toWorld, i, Math.max(toWorld.getMinHeight() + 2, k), j);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method returns if missing islands should be generated uppon teleportation.
|
||||
* Can happen only in non-custom generators.
|
||||
* @param overWorld OverWorld
|
||||
* @return {@code true} if missing islands must be pasted, {@code false} otherwise.
|
||||
*/
|
||||
protected boolean isPastingMissingIslands(World overWorld)
|
||||
{
|
||||
return this.plugin.getIWM().isPasteMissingIslands(overWorld) &&
|
||||
!this.plugin.getIWM().isUseOwnGenerator(overWorld);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Section: Variables
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
|
||||
/**
|
||||
* BentoBox plugin instance.
|
||||
*/
|
||||
@NonNull
|
||||
protected final BentoBox plugin;
|
||||
|
||||
/**
|
||||
* Set of entities that currently is inside portal.
|
||||
*/
|
||||
protected final Set<UUID> inPortal;
|
||||
|
||||
/**
|
||||
* Set of entities that currently is in teleportation.
|
||||
*/
|
||||
protected final Set<UUID> inTeleport;
|
||||
}
|
@ -0,0 +1,438 @@
|
||||
//
|
||||
// Created by BONNe
|
||||
// Copyright - 2022
|
||||
//
|
||||
|
||||
|
||||
package world.bentobox.bentobox.listeners.teleports;
|
||||
|
||||
|
||||
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.PlayerTeleportEvent;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
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.
|
||||
*
|
||||
* 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).
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
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.onPlayerPortalEvent(en);
|
||||
}
|
||||
}, 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.onPlayerPortalEvent(en);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Section: Processors
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
|
||||
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 (environment.equals(World.Environment.THE_END))
|
||||
{
|
||||
// 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 (environment.equals(World.Environment.NETHER) &&
|
||||
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 (environment.equals(World.Environment.THE_END) && 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 (environment.equals(World.Environment.NETHER) &&
|
||||
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.
|
||||
|
||||
Location toLocation;
|
||||
Island island = this.plugin.getIslandsManager().getIsland(overWorld, event.getPlayer().getUniqueId());
|
||||
|
||||
if (island == null)
|
||||
{
|
||||
// What to do? Player do not have an island! Check for spawn?
|
||||
// TODO: SPAWN CHECK.
|
||||
toLocation = event.getFrom();
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Island Respawn, Bed, Default home location check.
|
||||
toLocation = island.getSpawnPoint(World.Environment.NORMAL);
|
||||
}
|
||||
|
||||
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?");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,887 @@
|
||||
//
|
||||
// Created by BONNe
|
||||
// Copyright - 2022
|
||||
//
|
||||
|
||||
|
||||
package world.bentobox.bentobox.util.teleport;
|
||||
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.ChunkSnapshot;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import world.bentobox.bentobox.BentoBox;
|
||||
import world.bentobox.bentobox.api.user.User;
|
||||
import world.bentobox.bentobox.database.objects.Island;
|
||||
import world.bentobox.bentobox.util.Pair;
|
||||
import world.bentobox.bentobox.util.Util;
|
||||
|
||||
|
||||
public class ClosestSafeSpotTeleport
|
||||
{
|
||||
/**
|
||||
* Teleports and entity to a safe spot on island
|
||||
*
|
||||
* @param builder - safe spot teleport builder
|
||||
*/
|
||||
ClosestSafeSpotTeleport(Builder builder)
|
||||
{
|
||||
this.plugin = builder.getPlugin();
|
||||
this.entity = builder.getEntity();
|
||||
this.location = builder.getLocation();
|
||||
this.portal = builder.isPortal();
|
||||
|
||||
this.successRunnable = builder.getSuccessRunnable();
|
||||
this.failRunnable = builder.getFailRunnable();
|
||||
|
||||
this.failureMessage = builder.getFailureMessage();
|
||||
|
||||
this.result = builder.getResult();
|
||||
this.world = Objects.requireNonNull(this.location.getWorld());
|
||||
|
||||
this.cancelIfFail = builder.isCancelIfFail();
|
||||
|
||||
// Try starting location
|
||||
Util.getChunkAtAsync(this.location).thenRun(this::checkLocation);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is main method that triggers safe spot search.
|
||||
* It starts with the given location and afterwards checks all blocks in required area.
|
||||
*/
|
||||
private void checkLocation()
|
||||
{
|
||||
if (this.plugin.getIslandsManager().isSafeLocation(this.location))
|
||||
{
|
||||
if (!this.portal)
|
||||
{
|
||||
// If this is not a portal teleport, then go to the safe location immediately
|
||||
this.teleportEntity(this.location);
|
||||
// Position search is completed. Quit faster.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Players should not be teleported outside protection range if they already are in it.
|
||||
this.boundingBox = this.plugin.getIslandsManager().getIslandAt(this.location).
|
||||
map(Island::getProtectionBoundingBox).
|
||||
orElseGet(() -> {
|
||||
int protectionRange = this.plugin.getIWM().getIslandProtectionRange(this.world);
|
||||
|
||||
return new BoundingBox(this.location.getBlockX() - protectionRange,
|
||||
Math.max(this.world.getMinHeight(), this.location.getBlockY() - protectionRange),
|
||||
this.location.getBlockZ() - protectionRange,
|
||||
this.location.getBlockX() + protectionRange,
|
||||
Math.min(this.world.getMaxHeight(), this.location.getBlockY() + protectionRange),
|
||||
this.location.getBlockZ() + protectionRange);
|
||||
});
|
||||
|
||||
// The maximal range of search.
|
||||
this.range = Math.min(this.plugin.getSettings().getSafeSpotSearchRange(), (int) this.boundingBox.getWidthX() / 2);
|
||||
|
||||
// The block queue contains all possible positions where player can be teleported. The queue will not be populated
|
||||
// with all blocks, as the validation would not allow it.ss
|
||||
this.blockQueue = new PriorityQueue<>(this.range * 2, ClosestSafeSpotTeleport.POSITION_COMPARATOR);
|
||||
|
||||
// Get chunks to scan
|
||||
this.chunksToScanIterator = this.getChunksToScan().iterator();
|
||||
|
||||
// Start a recurring task until done or cancelled
|
||||
this.task = Bukkit.getScheduler().runTaskTimer(this.plugin, this::gatherChunks, 0L, CHUNK_LOAD_SPEED);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method loads all chunks in async and populates blockQueue with all blocks.
|
||||
*/
|
||||
private void gatherChunks()
|
||||
{
|
||||
// Set a flag so this is only run if it's not already in progress
|
||||
if (this.checking.get())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.checking.set(true);
|
||||
|
||||
if (!this.portal && !this.blockQueue.isEmpty() && this.blockQueue.peek().distance() < 5)
|
||||
{
|
||||
// Position is found? Well most likely (not in all situations) position in block queue is already
|
||||
// the best position. The only bad situations could happen if position is on chunk borders.
|
||||
this.finishTask();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.chunksToScanIterator.hasNext())
|
||||
{
|
||||
// Chunk scanning has completed. Now check positions.
|
||||
this.finishTask();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the chunk
|
||||
Pair<Integer, Integer> chunkPair = this.chunksToScanIterator.next();
|
||||
this.chunksToScanIterator.remove();
|
||||
|
||||
// Get the chunk snapshot and scan it
|
||||
Util.getChunkAtAsync(this.world, chunkPair.x, chunkPair.z).
|
||||
thenApply(Chunk::getChunkSnapshot).
|
||||
whenCompleteAsync((snapshot, e) ->
|
||||
{
|
||||
if (snapshot != null)
|
||||
{
|
||||
// Find best spot based on collected information chunks.
|
||||
this.scanAndPopulateBlockQueue(snapshot);
|
||||
}
|
||||
|
||||
this.checking.set(false);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets a set of chunk coordinates that will be scanned.
|
||||
*
|
||||
* @return - list of chunk coordinates to be scanned
|
||||
*/
|
||||
private List<Pair<Integer, Integer>> getChunksToScan()
|
||||
{
|
||||
List<Pair<Integer, Integer>> chunksToScan = new ArrayList<>();
|
||||
|
||||
int x = this.location.getBlockX();
|
||||
int z = this.location.getBlockZ();
|
||||
|
||||
int range = 20;
|
||||
|
||||
// Normalize block coordinates to chunk coordinates and add extra 1 for visiting.
|
||||
int numberOfChunks = (((x + range) >> 4) - ((x - range) >> 4) + 1) *
|
||||
(((z + range) >> 4) - ((z - range) >> 4) + 1);
|
||||
|
||||
// Ideally it would be if visitor switch from clockwise to counter-clockwise if X % 16 < 8 and
|
||||
// up to down if Z % 16 < 8.
|
||||
|
||||
int offsetX = 0;
|
||||
int offsetZ = 0;
|
||||
|
||||
for (int i = 0; i < numberOfChunks; ++i)
|
||||
{
|
||||
int locationX = x + (offsetX << 4);
|
||||
int locationZ = z + (offsetZ << 4);
|
||||
|
||||
this.addChunk(chunksToScan, new Pair<>(locationX, locationZ), new Pair<>(locationX >> 4, locationZ >> 4));
|
||||
|
||||
if (Math.abs(offsetX) <= Math.abs(offsetZ) && (offsetX != offsetZ || offsetX >= 0))
|
||||
{
|
||||
offsetX += ((offsetZ >= 0) ? 1 : -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
offsetZ += ((offsetX >= 0) ? -1 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
return chunksToScan;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method adds chunk coordinates to the given chunksToScan list.
|
||||
* The limitation is that if location is in island, then block coordinate must also be in island space.
|
||||
* @param chunksToScan List of chunks that will be scanned.
|
||||
* @param blockCoord Block coordinates that must be in island.
|
||||
* @param chunkCoord Chunk coordinate.
|
||||
*/
|
||||
private void addChunk(List<Pair<Integer, Integer>> chunksToScan,
|
||||
Pair<Integer, Integer> blockCoord,
|
||||
Pair<Integer, Integer> chunkCoord)
|
||||
{
|
||||
if (!chunksToScan.contains(chunkCoord) &&
|
||||
this.plugin.getIslandsManager().getIslandAt(this.location).
|
||||
map(is -> is.inIslandSpace(blockCoord)).orElse(true))
|
||||
{
|
||||
chunksToScan.add(chunkCoord);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method populates block queue with all blocks that player can be teleported to.
|
||||
* Add only positions that are inside BoundingBox and is safe for teleportation.
|
||||
* @param chunkSnapshot Spigot Chunk Snapshot with blocks.
|
||||
*/
|
||||
private void scanAndPopulateBlockQueue(ChunkSnapshot chunkSnapshot)
|
||||
{
|
||||
int startY = this.location.getBlockY();
|
||||
int minY = this.world.getMinHeight();
|
||||
int maxY = this.world.getMaxHeight();
|
||||
|
||||
Vector blockVector = new Vector(this.location.getBlockX(), this.location.getBlockY(), this.location.getBlockZ());
|
||||
|
||||
int chunkX = chunkSnapshot.getX() << 4;
|
||||
int chunkZ = chunkSnapshot.getZ() << 4;
|
||||
|
||||
for (int x = 0; x < 16; x++)
|
||||
{
|
||||
for (int z = 0; z < 16; z++)
|
||||
{
|
||||
for (int y = Math.max(minY, startY - this.range); y < Math.min(maxY, startY + this.range); y++)
|
||||
{
|
||||
Vector positionVector = new Vector(chunkX + x, y, chunkZ + z);
|
||||
|
||||
if (this.boundingBox.contains(positionVector))
|
||||
{
|
||||
// Process positions that are inside bounding box of search area.
|
||||
|
||||
PositionData positionData = new PositionData(
|
||||
positionVector,
|
||||
chunkSnapshot.getBlockType(x, y - 1, z),
|
||||
y < maxY ? chunkSnapshot.getBlockType(x, y, z) : null,
|
||||
y + 1 < maxY ? chunkSnapshot.getBlockType(x, y + 1, z) : null,
|
||||
blockVector.distanceSquared(positionVector));
|
||||
|
||||
if (this.plugin.getIslandsManager().checkIfSafe(this.world,
|
||||
positionData.block,
|
||||
positionData.spaceOne,
|
||||
positionData.spaceTwo))
|
||||
{
|
||||
// Add only safe locations to the queue.
|
||||
this.blockQueue.add(positionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method finishes the chunk loading task and checks from all remaining positions in block queue
|
||||
* to find the best location for teleportation.
|
||||
*
|
||||
* This method stops position finding task and process teleporation.
|
||||
*/
|
||||
private void finishTask()
|
||||
{
|
||||
// Still Async!
|
||||
// Nothing left to check and still not canceled
|
||||
this.task.cancel();
|
||||
|
||||
if (this.scanBlockQueue())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.portal && this.noPortalPosition != null)
|
||||
{
|
||||
this.teleportEntity(this.noPortalPosition);
|
||||
}
|
||||
else if (this.entity instanceof Player player)
|
||||
{
|
||||
// Return to main thread and teleport the player
|
||||
Bukkit.getScheduler().runTask(this.plugin, () ->
|
||||
{
|
||||
// Failed, no safe spot
|
||||
if (!this.failureMessage.isEmpty())
|
||||
{
|
||||
User.getInstance(this.entity).notify(this.failureMessage);
|
||||
}
|
||||
|
||||
// Check highest block
|
||||
Block highestBlock = this.world.getHighestBlockAt(this.location);
|
||||
|
||||
if (highestBlock.getType().isSolid() &&
|
||||
this.plugin.getIslandsManager().isSafeLocation(highestBlock.getLocation()))
|
||||
{
|
||||
// Try to teleport player to the highest block.
|
||||
this.asyncTeleport(highestBlock.getLocation().add(new Vector(0.5D, 0D, 0.5D)));
|
||||
return;
|
||||
}
|
||||
else if (!this.plugin.getIWM().inWorld(this.entity.getLocation()))
|
||||
{
|
||||
// Last resort
|
||||
player.performCommand("spawn");
|
||||
}
|
||||
else if (!this.cancelIfFail)
|
||||
{
|
||||
// Create a spot for the player to be
|
||||
if (this.world.getEnvironment().equals(World.Environment.NETHER))
|
||||
{
|
||||
this.makeAndTeleport(Material.NETHERRACK);
|
||||
}
|
||||
else if (this.world.getEnvironment().equals(World.Environment.THE_END))
|
||||
{
|
||||
this.makeAndTeleport(Material.END_STONE);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.makeAndTeleport(Material.COBBLESTONE);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.failRunnable != null)
|
||||
{
|
||||
Bukkit.getScheduler().runTask(this.plugin, this.failRunnable);
|
||||
}
|
||||
|
||||
this.result.complete(false);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// We do not teleport entities if position failed.
|
||||
|
||||
if (this.failRunnable != null)
|
||||
{
|
||||
Bukkit.getScheduler().runTask(this.plugin, this.failRunnable);
|
||||
}
|
||||
|
||||
this.result.complete(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method creates a spot in start location for player to be teleported to. It creates 2 base material blocks
|
||||
* above location and fills the space between them with air.
|
||||
* @param baseMaterial Material that will be for top and bottom block.
|
||||
*/
|
||||
private void makeAndTeleport(Material baseMaterial)
|
||||
{
|
||||
this.location.getBlock().getRelative(BlockFace.DOWN).setType(baseMaterial, false);
|
||||
this.location.getBlock().setType(Material.AIR, false);
|
||||
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)));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method scans all populated positions and returns true if position is found, or false, if not.
|
||||
* @return {@code true} if safe position is found, otherwise false.
|
||||
*/
|
||||
private boolean scanBlockQueue()
|
||||
{
|
||||
boolean blockFound = false;
|
||||
|
||||
while (!this.blockQueue.isEmpty() && !blockFound)
|
||||
{
|
||||
blockFound = this.checkPosition(this.blockQueue.poll());
|
||||
}
|
||||
|
||||
return blockFound;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method triggers a task that will teleport entity in a main thread.
|
||||
*/
|
||||
private void teleportEntity(final Location location)
|
||||
{
|
||||
// Return to main thread and teleport the player
|
||||
Bukkit.getScheduler().runTask(this.plugin, () -> this.asyncTeleport(location));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method performs async teleportation and runs end tasks for spot-finder.
|
||||
* @param location Location where player should be teleported.
|
||||
*/
|
||||
private void asyncTeleport(final Location location)
|
||||
{
|
||||
Util.teleportAsync(this.entity, location).thenRun(() ->
|
||||
{
|
||||
if (this.successRunnable != null)
|
||||
{
|
||||
Bukkit.getScheduler().runTask(this.plugin, this.successRunnable);
|
||||
}
|
||||
|
||||
this.result.complete(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method checks if given position is valid for teleportation.
|
||||
* If query should find portal, then it marks first best position as noPortalPosition and continues
|
||||
* to search for a valid portal.
|
||||
* If query is not in portal mode, then return first valid position.
|
||||
* @param positionData Position data that must be checked.
|
||||
* @return {@code true} if position is found and no extra processing required, {@code false} otherwise.
|
||||
*/
|
||||
private boolean checkPosition(PositionData positionData)
|
||||
{
|
||||
if (this.portal)
|
||||
{
|
||||
if (Material.NETHER_PORTAL.equals(positionData.spaceOne()) ||
|
||||
Material.NETHER_PORTAL.equals(positionData.spaceTwo()))
|
||||
{
|
||||
// Portal is found. Teleport entity to the portal location.
|
||||
this.teleportEntity(new Location(this.world,
|
||||
positionData.vector().getBlockX() + 0.5,
|
||||
positionData.vector().getBlockY() + 0.1,
|
||||
positionData.vector().getBlockZ() + 0.5,
|
||||
this.location.getYaw(),
|
||||
this.location.getPitch()));
|
||||
|
||||
// Position found and player can is already teleported to it.
|
||||
return true;
|
||||
}
|
||||
else if (this.noPortalPosition == null)
|
||||
{
|
||||
// Mark first incoming position as the best for teleportation.
|
||||
this.noPortalPosition = new Location(this.world,
|
||||
positionData.vector().getBlockX() + 0.5,
|
||||
positionData.vector().getBlockY() + 0.1,
|
||||
positionData.vector().getBlockZ() + 0.5,
|
||||
this.location.getYaw(),
|
||||
this.location.getPitch());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// First best position should be valid for teleportation.
|
||||
this.teleportEntity(new Location(this.world,
|
||||
positionData.vector().getBlockX() + 0.5,
|
||||
positionData.vector().getBlockY() + 0.1,
|
||||
positionData.vector().getBlockZ() + 0.5,
|
||||
this.location.getYaw(),
|
||||
this.location.getPitch()));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* PositionData record holds information about position where player will be teleported.
|
||||
* @param vector Vector of the position.
|
||||
* @param distance Distance till the position.
|
||||
* @param block Block on which player will be placed.
|
||||
* @param spaceOne One block above block.
|
||||
* @param spaceTwo Two blocks above block.
|
||||
*/
|
||||
private record PositionData(Vector vector, Material block, Material spaceOne, Material spaceTwo, double distance) {}
|
||||
|
||||
|
||||
public static Builder builder(BentoBox plugin)
|
||||
{
|
||||
return new Builder(plugin);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Section: Builder
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
|
||||
public static class Builder
|
||||
{
|
||||
private Builder(BentoBox plugin)
|
||||
{
|
||||
this.plugin = plugin;
|
||||
this.result = new CompletableFuture<>();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Section: Builders
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Set who or what is going to teleport
|
||||
*
|
||||
* @param entity entity to teleport
|
||||
* @return Builder
|
||||
*/
|
||||
public Builder entity(Entity entity)
|
||||
{
|
||||
this.entity = entity;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the desired location
|
||||
*
|
||||
* @param location the location
|
||||
* @return Builder
|
||||
*/
|
||||
public Builder location(Location location)
|
||||
{
|
||||
this.location = location;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is a portal teleportation
|
||||
*
|
||||
* @return Builder
|
||||
*/
|
||||
public Builder portal()
|
||||
{
|
||||
this.portal = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is a successRunnable for teleportation
|
||||
*
|
||||
* @return Builder
|
||||
*/
|
||||
public Builder successRunnable(Runnable successRunnable)
|
||||
{
|
||||
this.successRunnable = successRunnable;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Try to teleport the player
|
||||
*
|
||||
* @return ClosestSafeSpotTeleport
|
||||
*/
|
||||
@Nullable
|
||||
public ClosestSafeSpotTeleport build()
|
||||
{
|
||||
// Error checking
|
||||
if (this.entity == null)
|
||||
{
|
||||
this.plugin.logError("Attempt to safe teleport a null entity!");
|
||||
this.result.complete(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.location == null)
|
||||
{
|
||||
this.plugin.logError("Attempt to safe teleport to a null location!");
|
||||
this.result.complete(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.location.getWorld() == null)
|
||||
{
|
||||
this.plugin.logError("Attempt to safe teleport to a null world!");
|
||||
this.result.complete(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.failureMessage.isEmpty() && this.entity instanceof Player)
|
||||
{
|
||||
this.failureMessage = "general.errors.no-safe-location-found";
|
||||
}
|
||||
|
||||
return new ClosestSafeSpotTeleport(this);
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Section: Getters
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
|
||||
/**
|
||||
* Gets plugin.
|
||||
*
|
||||
* @return the plugin
|
||||
*/
|
||||
public BentoBox getPlugin()
|
||||
{
|
||||
return this.plugin;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets result.
|
||||
*
|
||||
* @return the result
|
||||
*/
|
||||
public CompletableFuture<Boolean> getResult()
|
||||
{
|
||||
return this.result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets entity.
|
||||
*
|
||||
* @return the entity
|
||||
*/
|
||||
public Entity getEntity()
|
||||
{
|
||||
return this.entity;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets location.
|
||||
*
|
||||
* @return the location
|
||||
*/
|
||||
public Location getLocation()
|
||||
{
|
||||
return this.location;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets world.
|
||||
*
|
||||
* @return the world
|
||||
*/
|
||||
public World getWorld()
|
||||
{
|
||||
return this.world;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets success runnable.
|
||||
*
|
||||
* @return the success runnable
|
||||
*/
|
||||
public Runnable getSuccessRunnable()
|
||||
{
|
||||
return this.successRunnable;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets fail runnable.
|
||||
*
|
||||
* @return the fail runnable
|
||||
*/
|
||||
public Runnable getFailRunnable()
|
||||
{
|
||||
return this.failRunnable;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets failure message.
|
||||
*
|
||||
* @return the failure message
|
||||
*/
|
||||
public String getFailureMessage()
|
||||
{
|
||||
return this.failureMessage;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is portal boolean.
|
||||
*
|
||||
* @return the boolean
|
||||
*/
|
||||
public boolean isPortal()
|
||||
{
|
||||
return this.portal;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is cancel if fail boolean.
|
||||
*
|
||||
* @return the boolean
|
||||
*/
|
||||
public boolean isCancelIfFail()
|
||||
{
|
||||
return this.cancelIfFail;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Section: Variables
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
|
||||
/**
|
||||
* BentoBox plugin instance.
|
||||
*/
|
||||
private final BentoBox plugin;
|
||||
|
||||
/**
|
||||
* CompletableFuture that is triggered upon finishing position searching.
|
||||
*/
|
||||
private final CompletableFuture<Boolean> result;
|
||||
|
||||
/**
|
||||
* Entity that will be teleported.
|
||||
*/
|
||||
private Entity entity;
|
||||
|
||||
/**
|
||||
* Start location of teleportation.
|
||||
*/
|
||||
private Location location;
|
||||
|
||||
/**
|
||||
* World where teleportation happens.
|
||||
*/
|
||||
private World world;
|
||||
|
||||
/**
|
||||
* Runnable that will be triggered after successful teleportation.
|
||||
*/
|
||||
private Runnable successRunnable;
|
||||
|
||||
/**
|
||||
* Runnable that will be triggered after failing teleportation.
|
||||
*/
|
||||
private Runnable failRunnable;
|
||||
|
||||
/**
|
||||
* Stores the failure message that is sent to a player.
|
||||
*/
|
||||
private String failureMessage = "";
|
||||
|
||||
/**
|
||||
* Boolean that indicates if teleportation should search for portal.
|
||||
*/
|
||||
private boolean portal;
|
||||
|
||||
/**
|
||||
* Boolean that indicates if failing teleport should cancel it or create spot for player.
|
||||
*/
|
||||
private boolean cancelIfFail;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Section: Constants
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This comparator sorts position data based in order:
|
||||
* - the smallest distance value
|
||||
* - the smallest x value
|
||||
* - the smallest z value
|
||||
* - the smallest y value
|
||||
*/
|
||||
private final static Comparator<PositionData> POSITION_COMPARATOR = Comparator.comparingDouble(PositionData::distance).
|
||||
thenComparingInt(position -> position.vector().getBlockX()).
|
||||
thenComparingInt(position -> position.vector().getBlockZ()).
|
||||
thenComparingInt(position -> position.vector().getBlockY());
|
||||
|
||||
/**
|
||||
* Stores chunk load speed.
|
||||
*/
|
||||
private static final long CHUNK_LOAD_SPEED = 1;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Section: Variables
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* BentoBox plugin instance.
|
||||
*/
|
||||
private final BentoBox plugin;
|
||||
|
||||
/**
|
||||
* Entity that will be teleported.
|
||||
*/
|
||||
private final Entity entity;
|
||||
|
||||
/**
|
||||
* Start location of teleportation.
|
||||
*/
|
||||
private final Location location;
|
||||
|
||||
/**
|
||||
* World where teleportation happens.
|
||||
*/
|
||||
private final World world;
|
||||
|
||||
/**
|
||||
* Runnable that will be triggered after successful teleportation.
|
||||
*/
|
||||
private final Runnable successRunnable;
|
||||
|
||||
/**
|
||||
* Runnable that will be triggered after failing teleportation.
|
||||
*/
|
||||
private final Runnable failRunnable;
|
||||
|
||||
/**
|
||||
* Stores the failure message that is sent to a player.
|
||||
*/
|
||||
private final String failureMessage;
|
||||
|
||||
/**
|
||||
* CompletableFuture that is triggered upon finishing position searching.
|
||||
*/
|
||||
private final CompletableFuture<Boolean> result;
|
||||
|
||||
/**
|
||||
* Boolean that indicates if teleportation should search for portal.
|
||||
*/
|
||||
private final boolean portal;
|
||||
|
||||
/**
|
||||
* Boolean that indicates if failing teleport should cancel it or create spot for player.
|
||||
*/
|
||||
private final boolean cancelIfFail;
|
||||
|
||||
/**
|
||||
* Local variable that indicates if current process is running.
|
||||
*/
|
||||
private final AtomicBoolean checking = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* The distance from starting location in all directions where new position will be searched.
|
||||
*/
|
||||
private int range;
|
||||
|
||||
/**
|
||||
* Block Queue for all blocks that should be validated.
|
||||
*/
|
||||
private Queue<PositionData> blockQueue;
|
||||
|
||||
/**
|
||||
* List of chunks that will be scanned for positions.
|
||||
*/
|
||||
private Iterator<Pair<Integer, Integer>> chunksToScanIterator;
|
||||
|
||||
/**
|
||||
* BoundingBox where teleportation can happen. Areas outside are illegal.
|
||||
*/
|
||||
private BoundingBox boundingBox;
|
||||
|
||||
/**
|
||||
* This method returns first best available spot if portal was not found in search area.
|
||||
*/
|
||||
private Location noPortalPosition;
|
||||
|
||||
/**
|
||||
* Bukkit task that processes chunks.
|
||||
*/
|
||||
private BukkitTask task;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user