feat(pocket-base): add persistent pocket dimensions
This commit is contained in:
@@ -15,8 +15,8 @@ import org.bukkit.inventory.PlayerInventory;
|
||||
|
||||
final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||
private static final List<String> SETTINGS = List.of(
|
||||
"status", "upgrade", "visitors", "navigation", "flight", "border", "spawnable",
|
||||
"bossbar"
|
||||
"status", "upgrade", "pocket", "visitors", "navigation", "flight", "border",
|
||||
"spawnable", "bossbar"
|
||||
);
|
||||
private static final List<String> VISITOR_MODES = List.of("allowed", "blocked");
|
||||
private static final List<String> ENABLE_MODES = List.of("enable", "disable");
|
||||
@@ -24,16 +24,27 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||
private final PluginSettingsProvider settings;
|
||||
private final TeleportPolicy teleportPolicy;
|
||||
private final BaseFlightController flightController;
|
||||
private final PocketBaseManager pocketBases;
|
||||
|
||||
BaseSettingsCommand(
|
||||
BaseStateManager stateManager,
|
||||
PluginSettingsProvider settings,
|
||||
BaseFlightController flightController
|
||||
) {
|
||||
this(stateManager, settings, flightController, null);
|
||||
}
|
||||
|
||||
BaseSettingsCommand(
|
||||
BaseStateManager stateManager,
|
||||
PluginSettingsProvider settings,
|
||||
BaseFlightController flightController,
|
||||
PocketBaseManager pocketBases
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.settings = settings;
|
||||
this.teleportPolicy = new TeleportPolicy(settings);
|
||||
this.flightController = flightController;
|
||||
this.pocketBases = pocketBases;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,6 +62,10 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||
if (arguments.length == 1 && arguments[0].equalsIgnoreCase("upgrade")) {
|
||||
return purchaseVisitorAccess(player, state);
|
||||
}
|
||||
if (arguments.length == 2 && arguments[0].equalsIgnoreCase("pocket")
|
||||
&& arguments[1].equalsIgnoreCase("upgrade")) {
|
||||
return purchasePocketUpgrade(player, state);
|
||||
}
|
||||
if (arguments.length == 2 && arguments[0].equalsIgnoreCase("visitors")) {
|
||||
return updateVisitors(player, state, arguments[1]);
|
||||
}
|
||||
@@ -114,6 +129,53 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean purchasePocketUpgrade(Player player, PlayerState owner) {
|
||||
if (pocketBases == null) {
|
||||
player.sendMessage(ChatColor.RED + "Pocket Bases are currently unavailable.");
|
||||
return true;
|
||||
}
|
||||
if (owner.baseLevel() < 4 || owner.base().isEmpty()) {
|
||||
player.sendMessage(ChatColor.RED
|
||||
+ "Establish Base IV before purchasing a Pocket Base.");
|
||||
return true;
|
||||
}
|
||||
PocketBaseState current = pocketBases.state(owner.playerId());
|
||||
int price = current.level() == 0
|
||||
? settings.current().pocketBaseUnlockCost()
|
||||
: settings.current().pocketBaseUpgradeCost();
|
||||
Material currency = Material.valueOf(settings.current().pocketBaseCurrencyMaterial());
|
||||
PlayerInventory inventory = player.getInventory();
|
||||
if (countCurrency(inventory, currency) < price) {
|
||||
player.sendMessage(ChatColor.RED + "Pocket Base "
|
||||
+ (current.level() == 0 ? "I" : "level " + (current.level() + 1))
|
||||
+ " costs " + price + " "
|
||||
+ currency.name().toLowerCase(java.util.Locale.ROOT) + ".");
|
||||
return true;
|
||||
}
|
||||
ItemStack[] snapshot = cloneContents(inventory.getStorageContents());
|
||||
removeCurrency(inventory, currency, price);
|
||||
final PocketBaseState upgraded;
|
||||
try {
|
||||
upgraded = pocketBases.upgrade(owner.playerId());
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
inventory.setStorageContents(snapshot);
|
||||
player.sendMessage(ChatColor.RED
|
||||
+ "The Pocket Base upgrade failed; your payment was restored.");
|
||||
return true;
|
||||
}
|
||||
player.sendTitle(
|
||||
ChatColor.GOLD + "Pocket Base " + upgraded.level() + " Unlocked",
|
||||
ChatColor.YELLOW + "Build a portal inside your base",
|
||||
settings.current().titleFadeInTicks(),
|
||||
settings.current().titleStayTicks(),
|
||||
settings.current().titleFadeOutTicks()
|
||||
);
|
||||
player.sendMessage(ChatColor.GREEN + "Pocket Base level " + upgraded.level()
|
||||
+ " unlocked for " + price + " "
|
||||
+ currency.name().toLowerCase(java.util.Locale.ROOT) + ".");
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean updateVisitors(Player player, PlayerState state, String mode) {
|
||||
if (state.baseLevel() < 4) {
|
||||
player.sendMessage(ChatColor.RED + "Base IV visitor access is still locked.");
|
||||
@@ -274,6 +336,7 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||
showFlightPath(player, state);
|
||||
showWarmupPath(player, state);
|
||||
showCooldownPath(player, state);
|
||||
showPocketPath(player, state);
|
||||
int spawnableThreshold = settings.current().spawnableOverlayUnlockPlacements();
|
||||
player.sendMessage(ChatColor.YELLOW + "Spawnable Overlay: " + ChatColor.GRAY
|
||||
+ state.totalBlocksPlaced() + "/" + spawnableThreshold + " placements; "
|
||||
@@ -344,6 +407,17 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||
+ DurationFormatter.friendly(teleportPolicy.warmup(state)) + "; " + detail);
|
||||
}
|
||||
|
||||
private void showPocketPath(Player player, PlayerState owner) {
|
||||
int level = pocketBases == null ? 0 : pocketBases.state(owner.playerId()).level();
|
||||
int price = level == 0
|
||||
? settings.current().pocketBaseUnlockCost()
|
||||
: settings.current().pocketBaseUpgradeCost();
|
||||
player.sendMessage(colorForPrerequisite(owner.baseLevel() >= 4 && owner.base().isPresent())
|
||||
+ "Pocket Base " + level + ": " + ChatColor.GRAY + price + " "
|
||||
+ settings.current().pocketBaseCurrencyMaterial().toLowerCase(Locale.ROOT)
|
||||
+ " → /basesettings pocket upgrade");
|
||||
}
|
||||
|
||||
private void showCooldownPath(Player player, PlayerState state) {
|
||||
String detail = switch (state.cooldownLevel()) {
|
||||
case 0 -> state.blocksBrokenInBase() + "/" + settings.current().firstCooldownBreaks();
|
||||
@@ -373,6 +447,7 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||
List<String> modes = arguments[0].equalsIgnoreCase("visitors")
|
||||
? VISITOR_MODES
|
||||
: switch (arguments[0].toLowerCase(Locale.ROOT)) {
|
||||
case "pocket" -> List.of("upgrade");
|
||||
case "navigation", "flight", "border", "spawnable", "bossbar" -> ENABLE_MODES;
|
||||
default -> List.of();
|
||||
};
|
||||
@@ -438,7 +513,8 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
private static void sendUsage(Player player) {
|
||||
player.sendMessage(ChatColor.RED + "Usage: /basesettings "
|
||||
+ "[status|upgrade|visitors <allowed|blocked>|navigation <enable|disable>"
|
||||
+ "[status|upgrade|pocket upgrade|visitors <allowed|blocked>"
|
||||
+ "|navigation <enable|disable>"
|
||||
+ "|flight <enable|disable>|border <enable|disable>"
|
||||
+ "|spawnable <enable|disable>|bossbar <enable|disable>]");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
public record BlockPosition(int x, int y, int z) {
|
||||
}
|
||||
@@ -37,11 +37,15 @@ public record PluginSettings(
|
||||
long thirdTeleportCooldownSeconds,
|
||||
long fourthTeleportCooldownSeconds,
|
||||
int visitorUnlockDiamondCost,
|
||||
int pocketBaseUnlockCost,
|
||||
int pocketBaseUpgradeCost,
|
||||
Set<String> baseUnlockMaterials,
|
||||
String stoneExpansionMaterial,
|
||||
String deepslateExpansionMaterial,
|
||||
String obsidianExpansionMaterial,
|
||||
String visitorCurrencyMaterial,
|
||||
String pocketBaseCurrencyMaterial,
|
||||
String pocketBasePortalFrameMaterial,
|
||||
Set<String> cooldownExcludedMaterials,
|
||||
int bossBarDurationTicks,
|
||||
int navigationParticleCount,
|
||||
@@ -79,6 +83,8 @@ public record PluginSettings(
|
||||
private static final long DEFAULT_THIRD_TELEPORT_COOLDOWN_SECONDS = 3_600L;
|
||||
private static final long DEFAULT_FOURTH_TELEPORT_COOLDOWN_SECONDS = 1_800L;
|
||||
private static final int DEFAULT_VISITOR_UNLOCK_DIAMOND_COST = 128;
|
||||
private static final int DEFAULT_POCKET_BASE_UNLOCK_COST = 64;
|
||||
private static final int DEFAULT_POCKET_BASE_UPGRADE_COST = 16;
|
||||
private static final int DEFAULT_BOSS_BAR_DURATION_TICKS = 60;
|
||||
private static final int DEFAULT_NAVIGATION_PARTICLE_COUNT = 5;
|
||||
private static final int DEFAULT_TITLE_FADE_IN_TICKS = 10;
|
||||
@@ -140,6 +146,8 @@ public record PluginSettings(
|
||||
throw new IllegalArgumentException("teleport cooldown durations must decrease");
|
||||
}
|
||||
requirePositive(visitorUnlockDiamondCost, "visitor-unlock-diamond-cost");
|
||||
requirePositive(pocketBaseUnlockCost, "pocket-base-unlock-cost");
|
||||
requirePositive(pocketBaseUpgradeCost, "pocket-base-upgrade-cost");
|
||||
baseUnlockMaterials = normalizedSet(baseUnlockMaterials, "base-unlock-materials", false);
|
||||
stoneExpansionMaterial = normalizedName(stoneExpansionMaterial, "stone-expansion-material");
|
||||
deepslateExpansionMaterial = normalizedName(
|
||||
@@ -151,6 +159,12 @@ public record PluginSettings(
|
||||
visitorCurrencyMaterial = normalizedName(
|
||||
visitorCurrencyMaterial, "visitor-currency-material"
|
||||
);
|
||||
pocketBaseCurrencyMaterial = normalizedName(
|
||||
pocketBaseCurrencyMaterial, "pocket-base-currency-material"
|
||||
);
|
||||
pocketBasePortalFrameMaterial = normalizedName(
|
||||
pocketBasePortalFrameMaterial, "pocket-base-portal-frame-material"
|
||||
);
|
||||
cooldownExcludedMaterials = normalizedSet(
|
||||
cooldownExcludedMaterials, "cooldown-excluded-materials", true
|
||||
);
|
||||
@@ -214,11 +228,15 @@ public record PluginSettings(
|
||||
DEFAULT_FOURTH_TELEPORT_COOLDOWN_SECONDS
|
||||
),
|
||||
integer(values, "visitor-unlock-diamond-cost", DEFAULT_VISITOR_UNLOCK_DIAMOND_COST),
|
||||
integer(values, "pocket-base-unlock-cost", DEFAULT_POCKET_BASE_UNLOCK_COST),
|
||||
integer(values, "pocket-base-upgrade-cost", DEFAULT_POCKET_BASE_UPGRADE_COST),
|
||||
stringSet(values, "base-unlock-materials", Set.of("GRASS_BLOCK", "DIRT")),
|
||||
string(values, "stone-expansion-material", "STONE"),
|
||||
string(values, "deepslate-expansion-material", "DEEPSLATE"),
|
||||
string(values, "obsidian-expansion-material", "OBSIDIAN"),
|
||||
string(values, "visitor-currency-material", "DIAMOND"),
|
||||
string(values, "pocket-base-currency-material", "DIAMOND_BLOCK"),
|
||||
string(values, "pocket-base-portal-frame-material", "DIAMOND_BLOCK"),
|
||||
stringSet(values, "cooldown-excluded-materials", Set.of()),
|
||||
integer(values, "boss-bar-duration-ticks", DEFAULT_BOSS_BAR_DURATION_TICKS),
|
||||
integer(values, "navigation-particle-count", DEFAULT_NAVIGATION_PARTICLE_COUNT),
|
||||
|
||||
@@ -13,7 +13,8 @@ final class PluginSettingsValidator {
|
||||
Stream.of(
|
||||
settings.stoneExpansionMaterial(),
|
||||
settings.deepslateExpansionMaterial(),
|
||||
settings.obsidianExpansionMaterial()
|
||||
settings.obsidianExpansionMaterial(),
|
||||
settings.pocketBasePortalFrameMaterial()
|
||||
)
|
||||
).forEach(name -> {
|
||||
Material material = Material.matchMaterial(name);
|
||||
@@ -26,10 +27,15 @@ final class PluginSettingsValidator {
|
||||
throw new IllegalArgumentException(name + " is not a known material");
|
||||
}
|
||||
}
|
||||
Material currency = Material.matchMaterial(settings.visitorCurrencyMaterial());
|
||||
if (currency == null || currency.isAir()) {
|
||||
throw new IllegalArgumentException("visitor-currency-material is invalid");
|
||||
}
|
||||
validateCurrency(settings.visitorCurrencyMaterial(), "visitor-currency-material");
|
||||
validateCurrency(settings.pocketBaseCurrencyMaterial(), "pocket-base-currency-material");
|
||||
return settings;
|
||||
}
|
||||
|
||||
private static void validateCurrency(String name, String setting) {
|
||||
Material currency = Material.matchMaterial(name);
|
||||
if (currency == null || currency.isAir()) {
|
||||
throw new IllegalArgumentException(setting + " is invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Entity;
|
||||
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.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
|
||||
final class PocketBaseController implements Listener, Runnable {
|
||||
private static final int VOID_RETURN_Y = -64;
|
||||
private static final long PORTAL_COOLDOWN_NANOS = 2_000_000_000L;
|
||||
private final Server server;
|
||||
private final BaseStateManager baseStates;
|
||||
private final BaseBoundsService baseBounds;
|
||||
private final PocketBaseManager pocketBases;
|
||||
private final PluginSettingsProvider settings;
|
||||
private final Logger logger;
|
||||
private final Map<UUID, Long> cooldownUntil = new HashMap<>();
|
||||
|
||||
PocketBaseController(
|
||||
Server server,
|
||||
BaseStateManager baseStates,
|
||||
BaseBoundsService baseBounds,
|
||||
PocketBaseManager pocketBases,
|
||||
PluginSettingsProvider settings,
|
||||
Logger logger
|
||||
) {
|
||||
this.server = server;
|
||||
this.baseStates = baseStates;
|
||||
this.baseBounds = baseBounds;
|
||||
this.pocketBases = pocketBases;
|
||||
this.settings = settings;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
validateEntrances();
|
||||
for (PocketBaseState state : pocketBases.knownStates().values()) {
|
||||
state.entrance().ifPresent(this::showPortal);
|
||||
if (state.level() > 0 && pocketBases.returnPortalIsIntact(state.ownerId())) {
|
||||
showPortal(pocketBases.returnPortal(state.ownerId()));
|
||||
}
|
||||
}
|
||||
long now = System.nanoTime();
|
||||
cooldownUntil.entrySet().removeIf(entry -> entry.getValue() <= now);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
|
||||
public void onActivate(PlayerInteractEvent event) {
|
||||
if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getClickedBlock() == null
|
||||
|| event.getItem() == null
|
||||
|| event.getItem().getType() != Material.FLINT_AND_STEEL) {
|
||||
return;
|
||||
}
|
||||
Player player = event.getPlayer();
|
||||
PocketBaseState pocket = pocketBases.state(player.getUniqueId());
|
||||
if (pocket.level() < 1) {
|
||||
return;
|
||||
}
|
||||
PlayerState owner = baseStates.player(player.getUniqueId(), player.getName());
|
||||
if (owner.baseLevel() < 4 || owner.base().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Block clicked = event.getClickedBlock();
|
||||
World world = clicked.getWorld();
|
||||
Material frameMaterial = Material.valueOf(
|
||||
settings.current().pocketBasePortalFrameMaterial()
|
||||
);
|
||||
Optional<PocketPortalLocation> portal = PocketPortalGeometry.findFrame(
|
||||
position(clicked),
|
||||
world.getUID(),
|
||||
world.getName(),
|
||||
candidate -> block(world, candidate).getType() == frameMaterial,
|
||||
candidate -> isAir(block(world, candidate).getType())
|
||||
);
|
||||
if (portal.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
BaseArea area = baseBounds.area(owner);
|
||||
if (PocketPortalGeometry.frameBlocks(portal.orElseThrow()).stream().anyMatch(candidate ->
|
||||
!area.contains(world.getUID(), candidate.x(), candidate.y(), candidate.z()))) {
|
||||
player.sendMessage(ChatColor.RED
|
||||
+ "The complete Pocket Base portal must be inside your base.");
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
pocketBases.activateEntrance(player.getUniqueId(), portal.orElseThrow());
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
logger.log(Level.SEVERE, "Could not activate Pocket Base entrance", exception);
|
||||
player.sendMessage(ChatColor.RED + "The Pocket Base portal could not be activated.");
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
event.setCancelled(true);
|
||||
player.sendMessage(ChatColor.GREEN + "Pocket Base portal activated.");
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onBreak(BlockBreakEvent event) {
|
||||
BlockPosition broken = position(event.getBlock());
|
||||
UUID worldId = event.getBlock().getWorld().getUID();
|
||||
for (PocketBaseState state : pocketBases.knownStates().values()) {
|
||||
if (state.entrance().isPresent()) {
|
||||
PocketPortalLocation portal = state.entrance().orElseThrow();
|
||||
if (portal.worldId().equals(worldId)
|
||||
&& PocketPortalGeometry.isFrame(portal, broken)) {
|
||||
deactivate(state.ownerId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
|
||||
public void onPlace(BlockPlaceEvent event) {
|
||||
Block block = event.getBlockPlaced();
|
||||
Optional<UUID> ownerId = pocketBases.ownerForPocketWorld(block.getWorld().getUID());
|
||||
if (ownerId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
PocketBaseState state = pocketBases.state(ownerId.orElseThrow());
|
||||
if (!pocketBases.policy().contains(state.level(), block.getX(), block.getZ())) {
|
||||
event.setCancelled(true);
|
||||
event.getPlayer().sendMessage(ChatColor.RED
|
||||
+ "That block is outside the unlocked Pocket Base boundary.");
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onMove(PlayerMoveEvent event) {
|
||||
Location destination = event.getTo();
|
||||
if (destination == null || onCooldown(event.getPlayer())) {
|
||||
return;
|
||||
}
|
||||
Player player = event.getPlayer();
|
||||
World world = destination.getWorld();
|
||||
if (world == null) {
|
||||
return;
|
||||
}
|
||||
Optional<UUID> pocketOwner = pocketBases.ownerForPocketWorld(world.getUID());
|
||||
if (pocketOwner.isPresent()) {
|
||||
UUID ownerId = pocketOwner.orElseThrow();
|
||||
if (destination.getY() < VOID_RETURN_Y
|
||||
|| pocketBases.returnPortalIsIntact(ownerId)
|
||||
&& PocketPortalGeometry.isInterior(
|
||||
pocketBases.returnPortal(ownerId), position(destination)
|
||||
)) {
|
||||
leavePocket(player, ownerId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
BlockPosition position = position(destination);
|
||||
for (PocketBaseState state : pocketBases.knownStates().values()) {
|
||||
if (state.entrance().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
PocketPortalLocation portal = state.entrance().orElseThrow();
|
||||
if (portal.worldId().equals(world.getUID())
|
||||
&& isIntact(portal)
|
||||
&& PocketPortalGeometry.isInterior(portal, position)) {
|
||||
teleport(player, pocketBases.pocketArrival(state.ownerId()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
|
||||
public void onVoidDamage(EntityDamageEvent event) {
|
||||
Entity entity = event.getEntity();
|
||||
if (event.getCause() != EntityDamageEvent.DamageCause.VOID
|
||||
|| !(entity instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
Optional<UUID> owner = pocketBases.ownerForPocketWorld(player.getWorld().getUID());
|
||||
if (owner.isPresent()) {
|
||||
event.setCancelled(true);
|
||||
leavePocket(player, owner.orElseThrow());
|
||||
}
|
||||
}
|
||||
|
||||
boolean isPocketWorld(UUID worldId) {
|
||||
return pocketBases.ownerForPocketWorld(worldId).isPresent();
|
||||
}
|
||||
|
||||
void deactivateForRelocation(UUID ownerId) {
|
||||
deactivate(ownerId);
|
||||
}
|
||||
|
||||
private void leavePocket(Player player, UUID ownerId) {
|
||||
PocketBaseState state = pocketBases.state(ownerId);
|
||||
if (state.entrance().isPresent() && isIntact(state.entrance().orElseThrow())) {
|
||||
teleport(player, outsidePortal(state.entrance().orElseThrow()));
|
||||
return;
|
||||
}
|
||||
World fallbackWorld = server.getWorlds().get(0);
|
||||
teleport(player, fallbackWorld.getSpawnLocation());
|
||||
}
|
||||
|
||||
private void teleport(Player player, Location destination) {
|
||||
cooldownUntil.put(player.getUniqueId(), System.nanoTime() + PORTAL_COOLDOWN_NANOS);
|
||||
player.teleport(destination, PlayerTeleportEvent.TeleportCause.PLUGIN);
|
||||
}
|
||||
|
||||
private boolean onCooldown(Player player) {
|
||||
return cooldownUntil.getOrDefault(player.getUniqueId(), 0L) > System.nanoTime();
|
||||
}
|
||||
|
||||
private void validateEntrances() {
|
||||
for (PocketBaseState state : pocketBases.knownStates().values()) {
|
||||
if (state.entrance().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
PlayerState owner = baseStates.knownPlayers().get(state.ownerId());
|
||||
PocketPortalLocation portal = state.entrance().orElseThrow();
|
||||
if (owner == null || owner.baseLevel() < 4 || owner.base().isEmpty()
|
||||
|| !isIntact(portal)
|
||||
|| PocketPortalGeometry.frameBlocks(portal).stream().anyMatch(position ->
|
||||
!baseBounds.area(owner).contains(
|
||||
portal.worldId(), position.x(), position.y(), position.z()
|
||||
))) {
|
||||
deactivate(state.ownerId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isIntact(PocketPortalLocation portal) {
|
||||
World world = server.getWorld(portal.worldId());
|
||||
if (world == null) {
|
||||
return false;
|
||||
}
|
||||
Material frame = Material.valueOf(settings.current().pocketBasePortalFrameMaterial());
|
||||
return PocketPortalGeometry.frameBlocks(portal).stream()
|
||||
.allMatch(position -> block(world, position).getType() == frame);
|
||||
}
|
||||
|
||||
private void showPortal(PocketPortalLocation portal) {
|
||||
World world = server.getWorld(portal.worldId());
|
||||
if (world == null || !isIntact(portal)) {
|
||||
return;
|
||||
}
|
||||
for (BlockPosition position : PocketPortalGeometry.interiorBlocks(portal)) {
|
||||
world.spawnParticle(
|
||||
Particle.PORTAL,
|
||||
position.x() + 0.5,
|
||||
position.y() + 0.5,
|
||||
position.z() + 0.5,
|
||||
4,
|
||||
0.25,
|
||||
0.35,
|
||||
0.25,
|
||||
0.02
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void deactivate(UUID ownerId) {
|
||||
try {
|
||||
pocketBases.deactivateEntrance(ownerId);
|
||||
} catch (IOException exception) {
|
||||
logger.log(Level.SEVERE, "Could not deactivate Pocket Base entrance", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Location outsidePortal(PocketPortalLocation portal) {
|
||||
World world = server.getWorld(portal.worldId());
|
||||
if (world == null) {
|
||||
return server.getWorlds().get(0).getSpawnLocation();
|
||||
}
|
||||
return portal.axis() == PocketPortalAxis.X
|
||||
? new Location(world, portal.x() + 2.0, portal.y() + 1.0, portal.z() + 1.5)
|
||||
: new Location(world, portal.x() + 1.5, portal.y() + 1.0, portal.z() + 2.0);
|
||||
}
|
||||
|
||||
private static boolean isAir(Material material) {
|
||||
return material == Material.AIR || material == Material.CAVE_AIR
|
||||
|| material == Material.VOID_AIR;
|
||||
}
|
||||
|
||||
private static Block block(World world, BlockPosition position) {
|
||||
return world.getBlockAt(position.x(), position.y(), position.z());
|
||||
}
|
||||
|
||||
private static BlockPosition position(Block block) {
|
||||
return new BlockPosition(block.getX(), block.getY(), block.getZ());
|
||||
}
|
||||
|
||||
private static BlockPosition position(Location location) {
|
||||
return new BlockPosition(location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
|
||||
final class PocketBaseManager {
|
||||
private final PocketBaseStateManager states;
|
||||
private final PocketBaseWorldService worlds;
|
||||
private final PocketBasePolicy policy;
|
||||
|
||||
PocketBaseManager(
|
||||
PocketBaseStateManager states,
|
||||
PocketBaseWorldService worlds,
|
||||
PluginSettingsProvider settings
|
||||
) {
|
||||
this.states = states;
|
||||
this.worlds = worlds;
|
||||
this.policy = new PocketBasePolicy(settings);
|
||||
}
|
||||
|
||||
void loadWorlds() {
|
||||
worlds.loadExisting(states.knownStates());
|
||||
}
|
||||
|
||||
PocketBaseState state(UUID ownerId) {
|
||||
return states.state(ownerId);
|
||||
}
|
||||
|
||||
Map<UUID, PocketBaseState> knownStates() {
|
||||
return states.knownStates();
|
||||
}
|
||||
|
||||
PocketBaseState upgrade(UUID ownerId) throws IOException {
|
||||
PocketBaseState current = states.state(ownerId);
|
||||
int nextLevel = Math.addExact(current.level(), 1);
|
||||
policy.size(nextLevel);
|
||||
worlds.expand(ownerId, current.level(), nextLevel);
|
||||
return states.updateAndSave(ownerId, state -> state.withLevel(nextLevel));
|
||||
}
|
||||
|
||||
PocketBaseState activateEntrance(
|
||||
UUID ownerId,
|
||||
PocketPortalLocation portal
|
||||
) throws IOException {
|
||||
PocketBaseState current = states.state(ownerId);
|
||||
if (current.level() < 1) {
|
||||
throw new IllegalStateException("Pocket Base I is still locked");
|
||||
}
|
||||
return states.updateAndSave(ownerId, state -> state.withEntrance(portal));
|
||||
}
|
||||
|
||||
void deactivateEntrance(UUID ownerId) throws IOException {
|
||||
PocketBaseState current = states.state(ownerId);
|
||||
if (current.entrance().isPresent()) {
|
||||
states.updateAndSave(ownerId, PocketBaseState::withoutEntrance);
|
||||
}
|
||||
}
|
||||
|
||||
Optional<UUID> ownerForPocketWorld(UUID worldId) {
|
||||
return worlds.ownerForWorld(worldId);
|
||||
}
|
||||
|
||||
Location pocketArrival(UUID ownerId) {
|
||||
return worlds.arrival(ownerId);
|
||||
}
|
||||
|
||||
PocketPortalLocation returnPortal(UUID ownerId) {
|
||||
return worlds.returnPortal(ownerId);
|
||||
}
|
||||
|
||||
boolean returnPortalIsIntact(UUID ownerId) {
|
||||
return worlds.returnPortalIsIntact(ownerId);
|
||||
}
|
||||
|
||||
World pocketWorld(UUID ownerId) {
|
||||
return worlds.world(ownerId);
|
||||
}
|
||||
|
||||
PocketBasePolicy policy() {
|
||||
return policy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
public final class PocketBasePolicy {
|
||||
private final PluginSettingsProvider settings;
|
||||
|
||||
public PocketBasePolicy(PluginSettings settings) {
|
||||
this(new PluginSettingsProvider(settings));
|
||||
}
|
||||
|
||||
public PocketBasePolicy(PluginSettingsProvider settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public int size(int level) {
|
||||
if (level < 1) {
|
||||
throw new IllegalArgumentException("Pocket Base level must be positive");
|
||||
}
|
||||
return Math.addExact(64, Math.multiplyExact(32, level - 1));
|
||||
}
|
||||
|
||||
public int price(PocketBaseState state) {
|
||||
return state.level() == 0
|
||||
? settings.current().pocketBaseUnlockCost()
|
||||
: settings.current().pocketBaseUpgradeCost();
|
||||
}
|
||||
|
||||
public boolean contains(int level, int x, int z) {
|
||||
int halfSize = size(level) / 2;
|
||||
return x >= -halfSize && x < halfSize && z >= -halfSize && z < halfSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public record PocketBaseState(
|
||||
UUID ownerId,
|
||||
int level,
|
||||
Optional<PocketPortalLocation> entrance
|
||||
) {
|
||||
public PocketBaseState {
|
||||
if (ownerId == null) {
|
||||
throw new IllegalArgumentException("owner ID is required");
|
||||
}
|
||||
if (level < 0) {
|
||||
throw new IllegalArgumentException("Pocket Base level must not be negative");
|
||||
}
|
||||
entrance = entrance == null ? Optional.empty() : entrance;
|
||||
if (level == 0 && entrance.isPresent()) {
|
||||
throw new IllegalArgumentException("a locked Pocket Base cannot have an entrance");
|
||||
}
|
||||
}
|
||||
|
||||
public static PocketBaseState locked(UUID ownerId) {
|
||||
return new PocketBaseState(ownerId, 0, Optional.empty());
|
||||
}
|
||||
|
||||
public PocketBaseState withLevel(int newLevel) {
|
||||
return new PocketBaseState(ownerId, newLevel, entrance);
|
||||
}
|
||||
|
||||
public PocketBaseState withEntrance(PocketPortalLocation portal) {
|
||||
return new PocketBaseState(ownerId, level, Optional.of(portal));
|
||||
}
|
||||
|
||||
public PocketBaseState withoutEntrance() {
|
||||
return new PocketBaseState(ownerId, level, Optional.empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
final class PocketBaseStateManager {
|
||||
private final YamlPocketBaseRepository repository;
|
||||
private final Map<UUID, PocketBaseState> states;
|
||||
|
||||
PocketBaseStateManager(YamlPocketBaseRepository repository) throws IOException {
|
||||
this.repository = repository;
|
||||
this.states = new HashMap<>(repository.load());
|
||||
}
|
||||
|
||||
PocketBaseState state(UUID ownerId) {
|
||||
return states.getOrDefault(ownerId, PocketBaseState.locked(ownerId));
|
||||
}
|
||||
|
||||
Map<UUID, PocketBaseState> knownStates() {
|
||||
return Map.copyOf(states);
|
||||
}
|
||||
|
||||
PocketBaseState updateAndSave(
|
||||
UUID ownerId,
|
||||
UnaryOperator<PocketBaseState> operation
|
||||
) throws IOException {
|
||||
PocketBaseState updated = operation.apply(state(ownerId));
|
||||
if (!updated.ownerId().equals(ownerId)) {
|
||||
throw new IllegalArgumentException("Pocket Base owner ID cannot change");
|
||||
}
|
||||
Map<UUID, PocketBaseState> proposed = new HashMap<>(states);
|
||||
proposed.put(ownerId, updated);
|
||||
repository.save(proposed);
|
||||
states.clear();
|
||||
states.putAll(proposed);
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.WorldCreator;
|
||||
|
||||
final class PocketBaseWorldService {
|
||||
private static final int PLATFORM_Y = 64;
|
||||
private static final String WORLD_PREFIX = "spigot_base_pocket_";
|
||||
private final Server server;
|
||||
private final PluginSettingsProvider settings;
|
||||
private final PocketBasePolicy policy;
|
||||
private final Map<UUID, UUID> ownersByWorld = new HashMap<>();
|
||||
|
||||
PocketBaseWorldService(Server server, PluginSettingsProvider settings) {
|
||||
this.server = server;
|
||||
this.settings = settings;
|
||||
this.policy = new PocketBasePolicy(settings);
|
||||
}
|
||||
|
||||
void loadExisting(Map<UUID, PocketBaseState> states) {
|
||||
for (PocketBaseState state : states.values()) {
|
||||
if (state.level() > 0) {
|
||||
ensureWorld(state.ownerId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
World expand(UUID ownerId, int previousLevel, int newLevel) {
|
||||
World world = ensureWorld(ownerId);
|
||||
int newHalfSize = policy.size(newLevel) / 2;
|
||||
int previousHalfSize = previousLevel == 0 ? 0 : policy.size(previousLevel) / 2;
|
||||
for (int x = -newHalfSize; x < newHalfSize; x++) {
|
||||
for (int z = -newHalfSize; z < newHalfSize; z++) {
|
||||
boolean previouslyUnlocked = previousLevel > 0
|
||||
&& x >= -previousHalfSize && x < previousHalfSize
|
||||
&& z >= -previousHalfSize && z < previousHalfSize;
|
||||
if (previouslyUnlocked) {
|
||||
continue;
|
||||
}
|
||||
for (int y = PLATFORM_Y - 3; y < PLATFORM_Y; y++) {
|
||||
world.getBlockAt(x, y, z).setType(Material.DIRT, false);
|
||||
}
|
||||
world.getBlockAt(x, PLATFORM_Y, z).setType(Material.GRASS_BLOCK, false);
|
||||
}
|
||||
}
|
||||
if (previousLevel == 0) {
|
||||
createReturnPortal(world);
|
||||
world.setSpawnLocation(0, PLATFORM_Y + 1, 5);
|
||||
}
|
||||
world.save();
|
||||
return world;
|
||||
}
|
||||
|
||||
Optional<UUID> ownerForWorld(UUID worldId) {
|
||||
return Optional.ofNullable(ownersByWorld.get(worldId));
|
||||
}
|
||||
|
||||
World world(UUID ownerId) {
|
||||
return ensureWorld(ownerId);
|
||||
}
|
||||
|
||||
Location arrival(UUID ownerId) {
|
||||
return new Location(ensureWorld(ownerId), 0.5, PLATFORM_Y + 1, 5.5, 180.0f, 0.0f);
|
||||
}
|
||||
|
||||
PocketPortalLocation returnPortal(UUID ownerId) {
|
||||
World world = ensureWorld(ownerId);
|
||||
return new PocketPortalLocation(
|
||||
world.getUID(), world.getName(), -2, PLATFORM_Y + 1, 0, PocketPortalAxis.X
|
||||
);
|
||||
}
|
||||
|
||||
boolean returnPortalIsIntact(UUID ownerId) {
|
||||
PocketPortalLocation portal = returnPortal(ownerId);
|
||||
World world = ensureWorld(ownerId);
|
||||
Material frame = Material.valueOf(settings.current().pocketBasePortalFrameMaterial());
|
||||
return PocketPortalGeometry.frameBlocks(portal).stream()
|
||||
.allMatch(position -> world.getBlockAt(
|
||||
position.x(), position.y(), position.z()
|
||||
).getType() == frame);
|
||||
}
|
||||
|
||||
private World ensureWorld(UUID ownerId) {
|
||||
String name = worldName(ownerId);
|
||||
World world = server.getWorld(name);
|
||||
if (world == null) {
|
||||
WorldCreator creator = new WorldCreator(name)
|
||||
.environment(World.Environment.NORMAL)
|
||||
.generator(new VoidPocketChunkGenerator())
|
||||
.generateStructures(false);
|
||||
world = server.createWorld(creator);
|
||||
}
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("Could not create Pocket Base world " + name);
|
||||
}
|
||||
ownersByWorld.put(world.getUID(), ownerId);
|
||||
return world;
|
||||
}
|
||||
|
||||
private void createReturnPortal(World world) {
|
||||
PocketPortalLocation portal = new PocketPortalLocation(
|
||||
world.getUID(), world.getName(), -2, PLATFORM_Y + 1, 0, PocketPortalAxis.X
|
||||
);
|
||||
Material frame = Material.valueOf(settings.current().pocketBasePortalFrameMaterial());
|
||||
for (BlockPosition position : PocketPortalGeometry.frameBlocks(portal)) {
|
||||
world.getBlockAt(position.x(), position.y(), position.z()).setType(frame, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static String worldName(UUID ownerId) {
|
||||
return WORLD_PREFIX + ownerId.toString().replace("-", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
public enum PocketPortalAxis {
|
||||
X,
|
||||
Z
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public final class PocketPortalGeometry {
|
||||
private PocketPortalGeometry() {
|
||||
}
|
||||
|
||||
public static Optional<PocketPortalLocation> findFrame(
|
||||
BlockPosition clicked,
|
||||
UUID worldId,
|
||||
String worldName,
|
||||
Predicate<BlockPosition> isFrameMaterial,
|
||||
Predicate<BlockPosition> isOpenInterior
|
||||
) {
|
||||
for (PocketPortalAxis axis : PocketPortalAxis.values()) {
|
||||
int clickedHorizontal = axis == PocketPortalAxis.X ? clicked.x() : clicked.z();
|
||||
for (int horizontalOffset = 0; horizontalOffset < 4; horizontalOffset++) {
|
||||
for (int verticalOffset = 0; verticalOffset < 5; verticalOffset++) {
|
||||
int originHorizontal = clickedHorizontal - horizontalOffset;
|
||||
int originY = clicked.y() - verticalOffset;
|
||||
PocketPortalLocation candidate = axis == PocketPortalAxis.X
|
||||
? new PocketPortalLocation(
|
||||
worldId, worldName, originHorizontal, originY, clicked.z(), axis
|
||||
)
|
||||
: new PocketPortalLocation(
|
||||
worldId, worldName, clicked.x(), originY, originHorizontal, axis
|
||||
);
|
||||
if (frameBlocks(candidate).stream().allMatch(isFrameMaterial)
|
||||
&& interiorBlocks(candidate).stream().allMatch(isOpenInterior)) {
|
||||
return Optional.of(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static List<BlockPosition> frameBlocks(PocketPortalLocation portal) {
|
||||
List<BlockPosition> blocks = new ArrayList<>(14);
|
||||
for (int horizontal = 0; horizontal < 4; horizontal++) {
|
||||
blocks.add(position(portal, horizontal, 0));
|
||||
blocks.add(position(portal, horizontal, 4));
|
||||
}
|
||||
for (int vertical = 1; vertical < 4; vertical++) {
|
||||
blocks.add(position(portal, 0, vertical));
|
||||
blocks.add(position(portal, 3, vertical));
|
||||
}
|
||||
return List.copyOf(blocks);
|
||||
}
|
||||
|
||||
public static List<BlockPosition> interiorBlocks(PocketPortalLocation portal) {
|
||||
List<BlockPosition> blocks = new ArrayList<>(6);
|
||||
for (int horizontal = 1; horizontal < 3; horizontal++) {
|
||||
for (int vertical = 1; vertical < 4; vertical++) {
|
||||
blocks.add(position(portal, horizontal, vertical));
|
||||
}
|
||||
}
|
||||
return List.copyOf(blocks);
|
||||
}
|
||||
|
||||
public static boolean isFrame(PocketPortalLocation portal, BlockPosition position) {
|
||||
return frameBlocks(portal).contains(position);
|
||||
}
|
||||
|
||||
public static boolean isInterior(PocketPortalLocation portal, BlockPosition position) {
|
||||
return interiorBlocks(portal).contains(position);
|
||||
}
|
||||
|
||||
private static BlockPosition position(
|
||||
PocketPortalLocation portal,
|
||||
int horizontal,
|
||||
int vertical
|
||||
) {
|
||||
return portal.axis() == PocketPortalAxis.X
|
||||
? new BlockPosition(portal.x() + horizontal, portal.y() + vertical, portal.z())
|
||||
: new BlockPosition(portal.x(), portal.y() + vertical, portal.z() + horizontal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record PocketPortalLocation(
|
||||
UUID worldId,
|
||||
String worldName,
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
PocketPortalAxis axis
|
||||
) {
|
||||
public PocketPortalLocation {
|
||||
if (worldId == null) {
|
||||
throw new IllegalArgumentException("portal world ID is required");
|
||||
}
|
||||
if (worldName == null || worldName.isBlank()) {
|
||||
throw new IllegalArgumentException("portal world name is required");
|
||||
}
|
||||
if (axis == null) {
|
||||
throw new IllegalArgumentException("portal axis is required");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,22 @@ final class SetBaseCommand implements CommandExecutor {
|
||||
private final BaseStateManager stateManager;
|
||||
private final BaseService baseService;
|
||||
private final Clock clock;
|
||||
private final PocketBaseController pocketBaseController;
|
||||
|
||||
SetBaseCommand(BaseStateManager stateManager, BaseService baseService, Clock clock) {
|
||||
this(stateManager, baseService, clock, null);
|
||||
}
|
||||
|
||||
SetBaseCommand(
|
||||
BaseStateManager stateManager,
|
||||
BaseService baseService,
|
||||
Clock clock,
|
||||
PocketBaseController pocketBaseController
|
||||
) {
|
||||
this.stateManager = stateManager;
|
||||
this.baseService = baseService;
|
||||
this.clock = clock;
|
||||
this.pocketBaseController = pocketBaseController;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -47,6 +58,10 @@ final class SetBaseCommand implements CommandExecutor {
|
||||
player.sendMessage(ChatColor.RED + "Your current world is unavailable.");
|
||||
return true;
|
||||
}
|
||||
if (pocketBaseController != null && pocketBaseController.isPocketWorld(world.getUID())) {
|
||||
player.sendMessage(ChatColor.RED + "You cannot set your normal base inside a Pocket Base.");
|
||||
return true;
|
||||
}
|
||||
BaseLocation base = new BaseLocation(
|
||||
world.getUID(), world.getName(), location.getBlockX(), location.getBlockY(),
|
||||
location.getBlockZ(), location.getYaw(), location.getPitch()
|
||||
@@ -55,6 +70,9 @@ final class SetBaseCommand implements CommandExecutor {
|
||||
player.getUniqueId(), player.getName(), current -> baseService.setBase(current, base, now)
|
||||
);
|
||||
stateManager.saveIfDirty();
|
||||
if (pocketBaseController != null) {
|
||||
pocketBaseController.deactivateForRelocation(player.getUniqueId());
|
||||
}
|
||||
player.sendMessage(ChatColor.GREEN + "Base set at " + base.x() + ", " + base.y() + ", " + base.z() + ".");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ public final class SpigotBasePlugin extends JavaPlugin {
|
||||
private BaseProgressListener progressListener;
|
||||
private BaseFlightController flightController;
|
||||
private BaseTeleportManager teleportManager;
|
||||
private PocketBaseManager pocketBaseManager;
|
||||
private PocketBaseController pocketBaseController;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
@@ -27,7 +29,18 @@ public final class SpigotBasePlugin extends JavaPlugin {
|
||||
new YamlBaseStateRepository(getDataFolder().toPath().resolve("state.yml")),
|
||||
getLogger()
|
||||
);
|
||||
} catch (IllegalArgumentException | IOException exception) {
|
||||
PocketBaseStateManager pocketStates = new PocketBaseStateManager(
|
||||
new YamlPocketBaseRepository(
|
||||
getDataFolder().toPath().resolve("pocket-bases.yml")
|
||||
)
|
||||
);
|
||||
pocketBaseManager = new PocketBaseManager(
|
||||
pocketStates,
|
||||
new PocketBaseWorldService(getServer(), settingsProvider),
|
||||
settingsProvider
|
||||
);
|
||||
pocketBaseManager.loadWorlds();
|
||||
} catch (RuntimeException | IOException exception) {
|
||||
getLogger().log(Level.SEVERE, "Could not initialize Spigot Base", exception);
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
@@ -62,15 +75,27 @@ public final class SpigotBasePlugin extends JavaPlugin {
|
||||
new SafeBaseDestination(),
|
||||
Clock.systemUTC()
|
||||
);
|
||||
pocketBaseController = new PocketBaseController(
|
||||
getServer(),
|
||||
stateManager,
|
||||
boundsService,
|
||||
pocketBaseManager,
|
||||
settingsProvider,
|
||||
getLogger()
|
||||
);
|
||||
getServer().getPluginManager().registerEvents(progressListener, this);
|
||||
getServer().getPluginManager().registerEvents(teleportManager, this);
|
||||
getServer().getPluginManager().registerEvents(pocketBaseController, this);
|
||||
|
||||
command("setbase").setExecutor(new SetBaseCommand(stateManager, baseService, Clock.systemUTC()));
|
||||
command("setbase").setExecutor(new SetBaseCommand(
|
||||
stateManager, baseService, Clock.systemUTC(), pocketBaseController
|
||||
));
|
||||
command("base").setExecutor(new BaseCommand(teleportManager));
|
||||
BaseSettingsCommand settingsCommand = new BaseSettingsCommand(
|
||||
stateManager,
|
||||
settingsProvider,
|
||||
flightController
|
||||
flightController,
|
||||
pocketBaseManager
|
||||
);
|
||||
command("basesettings").setExecutor(settingsCommand);
|
||||
command("basesettings").setTabCompleter(settingsCommand);
|
||||
@@ -107,6 +132,7 @@ public final class SpigotBasePlugin extends JavaPlugin {
|
||||
10L
|
||||
);
|
||||
getServer().getScheduler().runTaskTimer(this, flightController, 5L, 5L);
|
||||
getServer().getScheduler().runTaskTimer(this, pocketBaseController, 5L, 5L);
|
||||
getServer().getScheduler().runTaskTimer(this, stateManager::saveIfDirty, 600L, 600L);
|
||||
getLogger().info("Spigot Base enabled.");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
|
||||
final class VoidPocketChunkGenerator extends ChunkGenerator {
|
||||
@Override
|
||||
public boolean shouldGenerateNoise() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateSurface() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateCaves() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateDecorations() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateMobs() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateStructures() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package games.dmg.spigotbase;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
public final class YamlPocketBaseRepository {
|
||||
private final Path stateFile;
|
||||
|
||||
public YamlPocketBaseRepository(Path stateFile) {
|
||||
this.stateFile = stateFile;
|
||||
}
|
||||
|
||||
public Map<UUID, PocketBaseState> load() throws IOException {
|
||||
if (!Files.exists(stateFile)) {
|
||||
return Map.of();
|
||||
}
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
try {
|
||||
yaml.load(stateFile.toFile());
|
||||
} catch (InvalidConfigurationException exception) {
|
||||
throw new IOException("Pocket Base state is not valid YAML", exception);
|
||||
}
|
||||
Map<UUID, PocketBaseState> states = new HashMap<>();
|
||||
ConfigurationSection owners = yaml.getConfigurationSection("owners");
|
||||
if (owners == null) {
|
||||
return states;
|
||||
}
|
||||
for (String key : owners.getKeys(false)) {
|
||||
try {
|
||||
UUID ownerId = UUID.fromString(key);
|
||||
String path = "owners." + key;
|
||||
int level = yaml.getInt(path + ".level", 0);
|
||||
PocketBaseState state = new PocketBaseState(
|
||||
ownerId,
|
||||
level,
|
||||
loadPortal(yaml, path + ".entrance")
|
||||
);
|
||||
states.put(ownerId, state);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Invalid records grant no Pocket Base progression.
|
||||
}
|
||||
}
|
||||
return states;
|
||||
}
|
||||
|
||||
public void save(Map<UUID, PocketBaseState> states) throws IOException {
|
||||
Path parent = stateFile.toAbsolutePath().getParent();
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
for (PocketBaseState state : states.values()) {
|
||||
String path = "owners." + state.ownerId();
|
||||
yaml.set(path + ".level", state.level());
|
||||
state.entrance().ifPresent(portal -> savePortal(yaml, path + ".entrance", portal));
|
||||
}
|
||||
Path temporary = Files.createTempFile(parent, "spigot-base-pocket-", ".yml");
|
||||
try {
|
||||
yaml.save(temporary.toFile());
|
||||
try {
|
||||
Files.move(
|
||||
temporary,
|
||||
stateFile,
|
||||
StandardCopyOption.REPLACE_EXISTING,
|
||||
StandardCopyOption.ATOMIC_MOVE
|
||||
);
|
||||
} catch (IOException atomicMoveFailure) {
|
||||
Files.move(temporary, stateFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} finally {
|
||||
Files.deleteIfExists(temporary);
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<PocketPortalLocation> loadPortal(
|
||||
YamlConfiguration yaml,
|
||||
String path
|
||||
) {
|
||||
if (!yaml.isConfigurationSection(path)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String worldId = yaml.getString(path + ".world-id");
|
||||
String worldName = yaml.getString(path + ".world-name");
|
||||
String axis = yaml.getString(path + ".axis");
|
||||
if (worldId == null || worldName == null || axis == null
|
||||
|| !yaml.isInt(path + ".x") || !yaml.isInt(path + ".y")
|
||||
|| !yaml.isInt(path + ".z")) {
|
||||
throw new IllegalArgumentException("invalid Pocket Base entrance");
|
||||
}
|
||||
return Optional.of(new PocketPortalLocation(
|
||||
UUID.fromString(worldId),
|
||||
worldName,
|
||||
yaml.getInt(path + ".x"),
|
||||
yaml.getInt(path + ".y"),
|
||||
yaml.getInt(path + ".z"),
|
||||
PocketPortalAxis.valueOf(axis)
|
||||
));
|
||||
}
|
||||
|
||||
private static void savePortal(
|
||||
YamlConfiguration yaml,
|
||||
String path,
|
||||
PocketPortalLocation portal
|
||||
) {
|
||||
yaml.set(path + ".world-id", portal.worldId().toString());
|
||||
yaml.set(path + ".world-name", portal.worldName());
|
||||
yaml.set(path + ".x", portal.x());
|
||||
yaml.set(path + ".y", portal.y());
|
||||
yaml.set(path + ".z", portal.z());
|
||||
yaml.set(path + ".axis", portal.axis().name());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user