Files
spigot-base/src/main/java/games/dmg/spigotbase/PocketBaseKeystoneService.java
T
dmg 61e7a01374
Release / release (push) Successful in 3m2s
CI / build (push) Successful in 1m23s
feat(pocket-base): add reusable keystone travel
2026-08-29 14:43:12 -04:00

251 lines
9.0 KiB
Java

package games.dmg.spigotbase;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.NamespacedKey;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.inventory.PrepareItemCraftEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.ShapedRecipe;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.CompassMeta;
import org.bukkit.persistence.PersistentDataType;
final class PocketBaseKeystoneService implements Listener {
private static final Set<Material> RETURN_HAZARDS = Set.of(
Material.LAVA,
Material.FIRE,
Material.SOUL_FIRE,
Material.MAGMA_BLOCK,
Material.CACTUS,
Material.CAMPFIRE,
Material.SOUL_CAMPFIRE,
Material.POWDER_SNOW
);
private final Server server;
private final PocketBaseManager pocketBases;
private final KeystoneReturnStore returns;
private final NamespacedKey markerKey;
private final Logger logger;
PocketBaseKeystoneService(
Server server,
PocketBaseManager pocketBases,
KeystoneReturnStore returns,
NamespacedKey markerKey,
Logger logger
) {
this.server = server;
this.pocketBases = pocketBases;
this.returns = returns;
this.markerKey = markerKey;
this.logger = logger;
}
@EventHandler(priority = EventPriority.HIGH)
public void onPrepareCraft(PrepareItemCraftEvent event) {
if (!(event.getRecipe() instanceof ShapedRecipe shaped)
|| !shaped.getKey().equals(markerKey)
|| !(event.getView().getPlayer() instanceof Player player)) {
return;
}
if (pocketBases.state(player.getUniqueId()).level() < 1) {
event.getInventory().setResult(null);
}
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onUse(PlayerInteractEvent event) {
Action action = event.getAction();
if ((action != Action.RIGHT_CLICK_AIR && action != Action.RIGHT_CLICK_BLOCK)
|| !isKeystone(event.getItem(), markerKey)) {
return;
}
event.setCancelled(true);
Player player = event.getPlayer();
UUID playerId = player.getUniqueId();
if (pocketBases.state(playerId).level() < 1) {
player.sendMessage(ChatColor.RED
+ "Unlock Pocket Base I before using a Pocket Base Keystone.");
return;
}
Optional<UUID> currentPocket = pocketBases.ownerForPocketWorld(
player.getWorld().getUID()
);
if (currentPocket.isPresent()) {
if (!currentPocket.orElseThrow().equals(playerId)) {
player.sendMessage(ChatColor.RED
+ "You can only use a keystone to leave your own Pocket Base.");
return;
}
returnToSavedDestination(player);
return;
}
Location origin = player.getLocation();
World originWorld = origin.getWorld();
if (originWorld == null) {
player.sendMessage(ChatColor.RED + "Your return location could not be recorded.");
return;
}
KeystoneReturnLocation destination = new KeystoneReturnLocation(
originWorld.getUID(),
originWorld.getName(),
origin.getX(),
origin.getY(),
origin.getZ(),
origin.getYaw(),
origin.getPitch()
);
try {
returns.record(playerId, destination);
} catch (IOException exception) {
logger.log(Level.SEVERE, "Could not save a Pocket Base Keystone return", exception);
player.sendMessage(ChatColor.RED
+ "Your return location could not be saved, so you were not teleported.");
return;
}
teleport(player, pocketBases.pocketArrival(playerId));
}
private void returnToSavedDestination(Player player) {
Optional<KeystoneReturnLocation> saved = returns.destination(player.getUniqueId());
if (saved.isEmpty()) {
player.sendMessage(ChatColor.RED + "No Pocket Base Keystone return is recorded.");
return;
}
KeystoneReturnLocation destination = saved.orElseThrow();
World world = server.getWorld(destination.worldId());
if (world == null) {
player.sendMessage(ChatColor.RED + "Your recorded return world is unavailable.");
return;
}
Location location = new Location(
world,
destination.x(),
destination.y(),
destination.z(),
destination.yaw(),
destination.pitch()
);
if (!isSafe(location)) {
player.sendMessage(ChatColor.RED + "Your recorded return location is not safe.");
return;
}
teleport(player, location);
}
private static void teleport(Player player, Location destination) {
float previousFallDistance = player.getFallDistance();
player.setFallDistance(0.0F);
if (!player.teleport(destination, PlayerTeleportEvent.TeleportCause.PLUGIN)) {
player.setFallDistance(previousFallDistance);
player.sendMessage(ChatColor.RED + "Pocket Base Keystone travel failed.");
}
}
private static boolean isSafe(Location location) {
World world = location.getWorld();
int x = location.getBlockX();
int y = location.getBlockY();
int z = location.getBlockZ();
if (world == null || y <= world.getMinHeight() || y + 1 >= world.getMaxHeight()) {
return false;
}
Block ground = world.getBlockAt(x, y - 1, z);
Block feet = world.getBlockAt(x, y, z);
Block head = world.getBlockAt(x, y + 1, z);
return feet.isPassable() && head.isPassable()
&& !feet.isLiquid() && !head.isLiquid()
&& !ground.isPassable() && !ground.isLiquid()
&& !RETURN_HAZARDS.contains(ground.getType())
&& !RETURN_HAZARDS.contains(feet.getType())
&& !RETURN_HAZARDS.contains(head.getType());
}
static boolean isKeystone(ItemStack item, NamespacedKey markerKey) {
if (item == null || item.getType() != Material.RECOVERY_COMPASS) {
return false;
}
ItemMeta metadata = item.getItemMeta();
if (metadata == null) {
return false;
}
Byte marker = metadata.getPersistentDataContainer().get(
markerKey, PersistentDataType.BYTE
);
return marker != null && marker == (byte) 1;
}
void registerRecipe() {
server.addRecipe(recipe(markerKey, createKeystone(markerKey)));
}
static ItemStack createKeystone(NamespacedKey markerKey) {
ItemStack item = new ItemStack(Material.RECOVERY_COMPASS);
ItemMeta metadata = item.getItemMeta();
if (!(metadata instanceof CompassMeta)) {
throw new IllegalStateException("Recovery Compass metadata is unavailable");
}
metadata.displayName(Component.text(
"Pocket Base Keystone", NamedTextColor.LIGHT_PURPLE
));
metadata.lore(List.of(
Component.text(
"Right-click to enter your Pocket Base.", NamedTextColor.GRAY
),
Component.text("Use it again inside to return.", NamedTextColor.GRAY)
));
metadata.getPersistentDataContainer().set(
markerKey, PersistentDataType.BYTE, (byte) 1
);
item.setItemMeta(metadata);
return item;
}
static RecipeSpec recipeSpecification() {
return new RecipeSpec(
"ONO", "NEN", "ONO", Material.OBSIDIAN,
Material.NETHERITE_INGOT, Material.ENDER_PEARL
);
}
static ShapedRecipe recipe(NamespacedKey key, ItemStack result) {
RecipeSpec specification = recipeSpecification();
ShapedRecipe recipe = new ShapedRecipe(key, result);
recipe.shape(specification.top(), specification.middle(), specification.bottom());
recipe.setIngredient('O', specification.obsidian());
recipe.setIngredient('N', specification.netherite());
recipe.setIngredient('E', specification.enderPearl());
return recipe;
}
record RecipeSpec(
String top,
String middle,
String bottom,
Material obsidian,
Material netherite,
Material enderPearl
) {
}
}