diff --git a/build.gradle b/build.gradle index eebbb6bc..be9e5e7d 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,10 @@ repositories { name = 'benthecat' url = uri('https://repo.c0ding.party/multiverse-beta') } + maven { + name = "helpchatRepoReleases" + url = uri("https://repo.helpch.at/releases/") + } } configure(apiDependencies) { @@ -32,6 +36,9 @@ dependencies { exclude group: 'org.bukkit', module: 'bukkit' } + // PlaceholderAPI + externalPlugin 'me.clip:placeholderapi:2.11.6' + // Utils shadowed('com.dumptruckman.minecraft:Logging:1.1.1') { exclude group: 'junit', module: 'junit' diff --git a/src/main/java/org/mvplugins/multiverse/portals/MVPortal.java b/src/main/java/org/mvplugins/multiverse/portals/MVPortal.java index 21deb35f..1738caed 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/MVPortal.java +++ b/src/main/java/org/mvplugins/multiverse/portals/MVPortal.java @@ -9,16 +9,23 @@ import java.util.Arrays; import java.util.Collection; +import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.Stack; import java.util.regex.Pattern; import com.dumptruckman.minecraft.util.Logging; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Entity; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.mvplugins.multiverse.core.command.MVCommandManager; import org.mvplugins.multiverse.core.config.handle.MemoryConfigurationHandle; import org.mvplugins.multiverse.core.config.handle.StringPropertyHandle; import org.mvplugins.multiverse.core.config.migration.ConfigMigrator; @@ -27,9 +34,16 @@ import org.mvplugins.multiverse.core.destination.DestinationInstance; import org.mvplugins.multiverse.core.destination.DestinationsProvider; import org.mvplugins.multiverse.core.teleportation.BlockSafety; +import org.mvplugins.multiverse.core.utils.result.Attempt; +import org.mvplugins.multiverse.core.utils.text.ChatTextFormatter; import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; import org.mvplugins.multiverse.core.world.WorldManager; +import org.mvplugins.multiverse.external.vavr.control.Option; import org.mvplugins.multiverse.external.vavr.control.Try; +import org.mvplugins.multiverse.portals.action.ActionFailureReason; +import org.mvplugins.multiverse.portals.action.ActionHandler; +import org.mvplugins.multiverse.portals.action.ActionHandlerProvider; +import org.mvplugins.multiverse.portals.action.ActionHandlerType; import org.mvplugins.multiverse.portals.config.PortalsConfig; import org.mvplugins.multiverse.portals.enums.PortalType; import org.bukkit.Location; @@ -62,6 +76,8 @@ public static MVPortal loadMVPortalFromConfig(MultiversePortals instance, String private final WorldManager worldManager; private final DestinationsProvider destinationsProvider; private final BlockSafety blockSafety; + private final ActionHandlerProvider actionHandlerProvider; + private final MVCommandManager commandManager; private final String name; private final MVPortalNodes configNodes; @@ -94,16 +110,24 @@ private MVPortal(MultiversePortals plugin, String name) { this.worldManager = this.plugin.getServiceLocator().getService(WorldManager.class); this.destinationsProvider = this.plugin.getServiceLocator().getService(DestinationsProvider.class); this.blockSafety = this.plugin.getServiceLocator().getService(BlockSafety.class); + this.actionHandlerProvider = this.plugin.getServiceLocator().getService(ActionHandlerProvider.class); + this.commandManager = this.plugin.getServiceLocator().getService(MVCommandManager.class); this.name = name; var config = this.plugin.getPortalsConfig(); this.configNodes = new MVPortalNodes(plugin, this); - var portalSection = config.getConfigurationSection("portals." + this.name); - if (portalSection == null) { - portalSection = config.createSection("portals." + this.name); - } - this.configHandle = MemoryConfigurationHandle.builder(portalSection, configNodes.getNodes()) + var portalSection = Option.of(config.getConfigurationSection("portals." + this.name)) + .getOrElse(() -> config.createSection("portals." + this.name)); + this.configHandle = setUpConfigHandle(portalSection); + this.stringPropertyHandle = new StringPropertyHandle(this.configHandle); + configHandle.load(); + + setUpPermissions(); + } + + private MemoryConfigurationHandle setUpConfigHandle(ConfigurationSection portalSection) { + return MemoryConfigurationHandle.builder(portalSection, configNodes.getNodes()) .migrator(ConfigMigrator.builder(configNodes.version) .addVersionMigrator(VersionMigrator.builder(1.0) .addAction(MoveMigratorAction.of("safeteleport", "safe-teleport")) @@ -119,11 +143,14 @@ private MVPortal(MultiversePortals plugin, String name) { } }) .build()) + .addVersionMigrator(VersionMigrator.builder(1.2) + .addAction(MoveMigratorAction.of("destination", "action")) + .build()) .build()) .build(); - this.stringPropertyHandle = new StringPropertyHandle(this.configHandle); - configHandle.load(); + } + private void setUpPermissions() { this.permission = this.plugin.getServer().getPluginManager().getPermission("multiverse.portal.access." + this.name); if (this.permission == null) { this.permission = new Permission("multiverse.portal.access." + this.name, "Allows access to the " + this.name + " portal", PermissionDefault.OP); @@ -142,6 +169,10 @@ private MVPortal(MultiversePortals plugin, String name) { } } + /** + * + * @return + */ public String getName() { return this.name; } @@ -271,24 +302,61 @@ public PortalLocation getPortalLocation() { return this.location; } - public boolean setDestination(String destinationString) { - DestinationInstance newDestination = this.destinationsProvider.parseDestination(destinationString).getOrNull(); - return setDestination(newDestination); + @ApiStatus.AvailableSince("5.2") + public Try setActionType(@NotNull ActionHandlerType actionType) { + Objects.requireNonNull(actionType, "actionType cannot be null"); + return configHandle.set(configNodes.actionType, actionType.getName()); } - public boolean setDestination(DestinationInstance newDestination) { - if (newDestination == null) { - Logging.warning("Portal " + this.name + " has an invalid DESTINATION!"); - return false; - } - return this.configHandle.set(configNodes.destination, newDestination.toString()).isSuccess(); + @ApiStatus.AvailableSince("5.2") + public Try setActionType(String actionType) { + return configHandle.set(configNodes.actionType, actionType); } - public DestinationInstance getDestination() { - return this.destinationsProvider.parseDestination(this.configHandle.get(configNodes.destination)) - .onFailure(f -> - Logging.warning("Portal " + this.name + " has an invalid DESTINATION! " + f.getFailureMessage().formatted())) - .getOrNull(); + @ApiStatus.AvailableSince("5.2") + public String getActionType() { + return this.configHandle.get(configNodes.actionType); + } + + @ApiStatus.AvailableSince("5.2") + public Try setAction(String action) { + return configHandle.set(configNodes.action, action); + } + + @ApiStatus.AvailableSince("5.2") + public String getAction() { + return configHandle.get(configNodes.action); + } + + @ApiStatus.AvailableSince("5.2") + public Try setActionHandler(@NotNull ActionHandler action) { + Objects.requireNonNull(action, "action cannot be null"); + return setActionType(action.getHandlerType()) + .flatMap(v -> setAction(action.serialise())); + } + + @ApiStatus.AvailableSince("5.2") + public @NotNull Attempt, ActionFailureReason> getActionHandler() { + return actionHandlerProvider.parseHandler(getActionType(), getAction()); + } + + @ApiStatus.AvailableSince("5.2") + public @NotNull Attempt, ActionFailureReason> getActionHandler(@NotNull CommandSender sender) { + return actionHandlerProvider.parseHandler(sender, getActionType(), getAction()); + } + + @ApiStatus.AvailableSince("5.2") + public @NotNull Attempt runActionFor(Entity entity) { + return getActionHandler(entity) + .mapAttempt(actionHandler -> actionHandler.runAction(this, entity)) + .onSuccess(() -> { + if (entity instanceof Player player) { + plugin.getPortalSession(player).setTeleportTime(new Date()); + } + }) + .onFailure(failure -> + Logging.warning(ChatTextFormatter.removeColor("Invalid Portal Action: " + + failure.getFailureMessage().formatted(commandManager.getCommandIssuer(Bukkit.getConsoleSender()))))); } /** @@ -598,4 +666,54 @@ public boolean isExempt(Player player) { public PortalLocation getLocation() { return getPortalLocation(); } + + /** + * @deprecated Portals now have new types of action. Hence, the portal's destination (now called action) may not + * always be a multiverse destination. It can be a command or server name as well. + * Please see {@link MVPortal#getActionHandler()} instead. + */ + @Deprecated(since = "5.2", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") + public boolean setDestination(String destinationString) { + DestinationInstance newDestination = this.destinationsProvider.parseDestination(destinationString).getOrNull(); + return setDestination(newDestination); + } + + /** + * @deprecated Portals now have new types of action. Hence, the portal's destination (now called action) may not + * always be a multiverse destination. For example, it can be a command or server name as well. + * Please see {@link MVPortal#getActionHandler()} instead. + */ + @Deprecated(since = "5.2", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") + public boolean setDestination(DestinationInstance newDestination) { + if (newDestination == null) { + Logging.warning("Portal " + this.name + " has an invalid DESTINATION!"); + return false; + } + if (!Objects.equals(getActionType(), "multiverse-destination")) { + Logging.warning("Portal " + this.name + " is not set to use multiverse destination!"); + return false; + } + return setActionType("multiverse-destination") + .flatMap(ignore -> setAction(newDestination.toString())) + .isSuccess(); + } + + /** + * @deprecated Portals now have new types of action. Hence, the portal's destination (now called action) may not + * always be a multiverse destination. For example, it can be a command or server name as well. + * Please see {@link MVPortal#getActionHandler()} instead. + */ + @Deprecated(since = "5.2", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") + public @Nullable DestinationInstance getDestination() { + return this.destinationsProvider.parseDestination(getAction()) + .onFailure(f -> { + if (getAction().equals("multiverse-destination")) { + Logging.warning("Portal " + this.name + " has an invalid DESTINATION! " + f.getFailureMessage().formatted()); + } + }) + .getOrNull(); + } } diff --git a/src/main/java/org/mvplugins/multiverse/portals/MVPortalNodes.java b/src/main/java/org/mvplugins/multiverse/portals/MVPortalNodes.java index 4607b864..34ef7e61 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/MVPortalNodes.java +++ b/src/main/java/org/mvplugins/multiverse/portals/MVPortalNodes.java @@ -6,10 +6,11 @@ import org.mvplugins.multiverse.core.config.node.ConfigNode; import org.mvplugins.multiverse.core.config.node.Node; import org.mvplugins.multiverse.core.config.node.NodeGroup; -import org.mvplugins.multiverse.core.destination.DestinationInstance; import org.mvplugins.multiverse.core.destination.DestinationsProvider; import org.mvplugins.multiverse.core.exceptions.MultiverseException; import org.mvplugins.multiverse.external.vavr.control.Try; +import org.mvplugins.multiverse.portals.action.ActionHandler; +import org.mvplugins.multiverse.portals.action.ActionHandlerProvider; import org.mvplugins.multiverse.portals.utils.MultiverseRegion; import java.util.Collections; @@ -21,12 +22,12 @@ final class MVPortalNodes { private MultiversePortals plugin; private MVPortal portal; - private DestinationsProvider destinationsProvider; + private ActionHandlerProvider actionHandlerProvider; MVPortalNodes(MultiversePortals plugin, MVPortal portal) { this.plugin = plugin; this.portal = portal; - this.destinationsProvider = MultiverseCoreApi.get().getDestinationsProvider(); + this.actionHandlerProvider = plugin.getServiceLocator().getService(ActionHandlerProvider.class); } NodeGroup getNodes() { @@ -90,13 +91,23 @@ private N node(N node) { .onSetValue((oldValue, newValue) -> portal.setPortalLocationInternal(PortalLocation.parseLocation(newValue))) .build()); - final ConfigNode destination = node(ConfigNode.builder("destination", String.class) + final ConfigNode actionType = node(ConfigNode.builder("action-type", String.class) + .suggester(input -> actionHandlerProvider.getAllHandlerTypeNames()) + .defaultValue("multiverse-destination") + .build()); + + final ConfigNode action = node(ConfigNode.builder("action", String.class) .defaultValue("") - .aliases("dest") - .suggester((sender, input) -> destinationsProvider.suggestDestinationStrings(sender, input)) - .stringParser((sender, input, type) -> destinationsProvider.parseDestination(sender, input) - .map(DestinationInstance::toString) - .toTry()) + .aliases("destination", "dest") + .suggester((sender, input) -> actionHandlerProvider.getHandlerType(portal.getActionType()) + .map(actionHandlerType -> actionHandlerType.suggestActions(sender, input)) + .getOrElse(Collections.emptyList())) + .stringParser((sender, input, type) -> + Try.of(() -> actionHandlerProvider.getHandlerType(portal.getActionType()) + .mapAttempt(actionHandlerType -> actionHandlerType.parseHandler(sender, input)) + .map(ActionHandler::serialise) + .getOrThrow(failure -> + new MultiverseException(failure.getFailureMessage())))) .build()); final ConfigNode checkDestinationSafety = node(ConfigNode.builder("check-destination-safety", Boolean.class) diff --git a/src/main/java/org/mvplugins/multiverse/portals/MultiversePortals.java b/src/main/java/org/mvplugins/multiverse/portals/MultiversePortals.java index 81053d53..59ca04ec 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/MultiversePortals.java +++ b/src/main/java/org/mvplugins/multiverse/portals/MultiversePortals.java @@ -16,6 +16,8 @@ import java.util.logging.Level; import com.dumptruckman.minecraft.util.Logging; +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteStreams; import org.jetbrains.annotations.ApiStatus; import org.mvplugins.multiverse.core.config.CoreConfig; import org.mvplugins.multiverse.core.destination.DestinationsProvider; @@ -26,6 +28,8 @@ import org.mvplugins.multiverse.external.jakarta.inject.Provider; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.external.vavr.control.Try; +import org.mvplugins.multiverse.portals.action.ActionHandlerProvider; +import org.mvplugins.multiverse.portals.action.ActionHandlerType; import org.mvplugins.multiverse.portals.commands.PortalsCommand; import org.mvplugins.multiverse.portals.command.PortalsCommandCompletions; import org.mvplugins.multiverse.portals.command.PortalsCommandContexts; @@ -70,6 +74,8 @@ public class MultiversePortals extends MultiverseModule { private Provider portalsConfigProvider; @Inject private Provider metricsConfiguratorProvider; + @Inject + private Provider actionHandlerProvider; private FileConfiguration MVPPortalConfig; private WorldEditConnection worldEditConnection; @@ -78,13 +84,13 @@ public class MultiversePortals extends MultiverseModule { @Override public void onLoad() { super.onLoad(); + Logging.init(this); getDataFolder().mkdirs(); } @Override public void onEnable() { super.onEnable(); - Logging.init(this); initializeDependencyInjection(new MultiversePortalsPluginBinder(this)); @@ -104,6 +110,7 @@ public void onEnable() { this.getServer().getPluginManager().disablePlugin(this); return; } + this.setUpActionHandlers(); this.loadPortals(); this.setupMetrics(); @@ -112,7 +119,11 @@ public void onEnable() { getServer().getPluginManager().registerEvents(new WorldEditPluginListener(), this); MultiversePortalsApi.init(this); - Logging.log(true, Level.INFO, " Enabled - By %s", StringFormatter.joinAnd(getDescription().getAuthors())); + // for teleporting between servers + getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord"); + + Logging.config("Version %s (API v%s) Enabled - By %s", + this.getDescription().getVersion(), getVersionAsNumber(), StringFormatter.joinAnd(this.getDescription().getAuthors())); } @Override @@ -120,6 +131,8 @@ public void onDisable() { this.savePortalsConfig(); MultiversePortalsApi.shutdown(); shutdownDependencyInjection(); + Logging.info("- Disabled"); + Logging.shutdown(); } private boolean setupConfig() { @@ -139,8 +152,14 @@ private void registerEvents() { if (portalsConfigProvider.get().getTeleportVehicles()) { pluginManager.registerEvents(serviceLocator.getService(MVPVehicleListener.class), this); } + if (portalsConfigProvider.get().getTeleportEntities()) { + pluginManager.registerEvents(serviceLocator.getService(MVPEntityPortalListener.class), this); + } if (portalsConfigProvider.get().getUseOnMove()) { pluginManager.registerEvents(serviceLocator.getService(MVPPlayerMoveListener.class), this); + if (portalsConfigProvider.get().getTeleportEntities()) { + pluginManager.registerEvents(serviceLocator.getService(MVPEntityMoveListener.class), this); + } } } @@ -174,6 +193,12 @@ public void destroyPortalSession(Player p) { this.portalSessions.remove(p.getName()); } + private void setUpActionHandlers() { + serviceLocator.getAllServices(ActionHandlerType.class).forEach(handler -> { + actionHandlerProvider.get().registerHandlerType(handler); + }); + } + private void loadPortals() { this.MVPPortalConfig = YamlConfiguration.loadConfiguration(new File(getDataFolder(), "portals.yml")); if (!this.MVPPortalConfig.isConfigurationSection("portals")) { diff --git a/src/main/java/org/mvplugins/multiverse/portals/PortalPlayerSession.java b/src/main/java/org/mvplugins/multiverse/portals/PortalPlayerSession.java index b50307f6..c5c2f4f5 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/PortalPlayerSession.java +++ b/src/main/java/org/mvplugins/multiverse/portals/PortalPlayerSession.java @@ -10,6 +10,7 @@ import java.util.Date; import com.dumptruckman.minecraft.util.Logging; +import org.bukkit.entity.LivingEntity; import org.mvplugins.multiverse.core.economy.MVEconomist; import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; import org.mvplugins.multiverse.core.world.WorldManager; @@ -30,7 +31,6 @@ public class PortalPlayerSession { private final PortalManager portalManager; private final WorldManager worldManager; private final DisplayUtils displayUtils; - private final MVEconomist economist; private final Player player; private MVPortal portalSelection = null; @@ -51,7 +51,6 @@ public PortalPlayerSession(MultiversePortals plugin, Player p) { this.portalManager = plugin.getServiceLocator().getService(PortalManager.class); this.worldManager = plugin.getServiceLocator().getService(WorldManager.class); this.displayUtils = plugin.getServiceLocator().getService(DisplayUtils.class); - this.economist = plugin.getServiceLocator().getService(MVEconomist.class); this.player = p; this.setLocation(p.getLocation()); this.lastTeleportTime = new Date(new Date().getTime() - this.portalsConfig.getPortalCooldown()); @@ -107,7 +106,7 @@ private void setStandingInLocation() { } public boolean doTeleportPlayer(MoveType eventType) { - if (eventType == MoveType.PLAYER_MOVE && this.player.isInsideVehicle()) { + if (eventType == MoveType.PLAYER_MOVE && shouldLetVehicleHandle()) { return false; } return this.hasMovedOutOfPortal && this.standingIn != null; @@ -122,7 +121,7 @@ public void setStaleLocation(Location loc, MoveType moveType) { // This should never happen, but seems to when someone gets kicked. return; } - if (this.player.isInsideVehicle() && moveType != MoveType.VEHICLE_MOVE) { + if (shouldLetVehicleHandle() && moveType != MoveType.VEHICLE_MOVE) { return; } // If the player has not moved, they have a stale location @@ -136,6 +135,10 @@ public void setStaleLocation(Location loc, MoveType moveType) { } } + private boolean shouldLetVehicleHandle() { + return this.player.isInsideVehicle() && !(this.player.getVehicle() instanceof LivingEntity); + } + public boolean setLeftClickSelection(Vector v, LoadedMultiverseWorld world) { if(!this.plugin.isWandEnabled()) { return false; @@ -249,17 +252,14 @@ public boolean showDebugInfo() { } displayUtils.showStaticInfo(this.player, this.standingIn, "You are currently standing in "); - displayUtils.showPortalPriceInfo(this.standingIn, this.player); return true; } public boolean showDebugInfo(MVPortal portal) { if (portal.playerCanEnterPortal(this.player)) { displayUtils.showStaticInfo(this.player, portal, "Portal Info "); - displayUtils.showPortalPriceInfo(portal, this.player); - } else { - Logging.info("Player " + this.player.getName() + " walked through" + portal.getName() + " with debug on."); } + Logging.info("Player " + this.player.getName() + " walked through" + portal.getName() + " with debug on."); return true; } diff --git a/src/main/java/org/mvplugins/multiverse/portals/action/ActionFailureReason.java b/src/main/java/org/mvplugins/multiverse/portals/action/ActionFailureReason.java new file mode 100644 index 00000000..8982d595 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/action/ActionFailureReason.java @@ -0,0 +1,14 @@ +package org.mvplugins.multiverse.portals.action; + +import org.mvplugins.multiverse.core.utils.result.FailureReason; +import org.mvplugins.multiverse.external.jetbrains.annotations.ApiStatus; + +/** + * Parent class for all reasons for a failure when trying to parse an action handler. + * + * @since 5.2 + */ +@ApiStatus.AvailableSince("5.2") +public interface ActionFailureReason extends FailureReason { + ActionFailureReason INSTANCE = new ActionFailureReason() {}; +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/action/ActionHandler.java b/src/main/java/org/mvplugins/multiverse/portals/action/ActionHandler.java new file mode 100644 index 00000000..a3a0d4da --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/action/ActionHandler.java @@ -0,0 +1,84 @@ +package org.mvplugins.multiverse.portals.action; + +import org.bukkit.entity.Entity; +import org.mvplugins.multiverse.core.locale.message.Message; +import org.mvplugins.multiverse.core.utils.result.Attempt; +import org.mvplugins.multiverse.external.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.portals.MVPortal; + +/** + * Represents an action handler that performs a specific action when an entity uses a portal. + * + * @param The type of the action handler + * @param The specific action handler class + * + * @since 5.2 + */ +@ApiStatus.AvailableSince("5.2") +public abstract class ActionHandler, H extends ActionHandler> { + + private final T handlerType; + + /** + * Create a new action handler. + * + * @param handlerType The type of this action handler + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + protected ActionHandler(@NotNull T handlerType) { + this.handlerType = handlerType; + } + + /** + * Get the type of this action handler. + * + * @return The action handler type + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + public @NotNull T getHandlerType() { + return handlerType; + } + + /** + * Run the action for the given portal and entity. + * + * @param portal The portal being used + * @param entity The entity using the portal + * @return An attempt indicating success or failure of the action + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + public abstract @NotNull Attempt runAction(@NotNull MVPortal portal, @NotNull Entity entity); + + /** + * Get a description of the action for display purposes. + * + * @param entity The entity to describe the action for + * @return The action description message + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + public abstract @NotNull Message actionDescription(Entity entity); + + /** + * Serialise the action handler back into a string. + * + * @return The serialised action handler + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + public @NotNull abstract String serialise(); + + @Override + public String toString() { + return serialise(); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/action/ActionHandlerProvider.java b/src/main/java/org/mvplugins/multiverse/portals/action/ActionHandlerProvider.java new file mode 100644 index 00000000..69eb7288 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/action/ActionHandlerProvider.java @@ -0,0 +1,107 @@ +package org.mvplugins.multiverse.portals.action; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.locale.message.Message; +import org.mvplugins.multiverse.core.utils.result.Attempt; +import org.mvplugins.multiverse.external.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.external.vavr.control.Option; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Provides various actions that can be performed when a portal is used. This extends to more than just teleporting. + *
+ * Built-in action handler types include "server", "command", and "multiverse-destination", which can be used to + * perform actions such as sending players to different servers, executing commands, or teleporting to predefined + * destinations, respectively. + *
+ * You can also create custom action handler types by implementing the {@link ActionHandlerType} class and + * registering them with this provider. + * + * @since 5.2 + */ +@ApiStatus.AvailableSince("5.2") +@Service +public final class ActionHandlerProvider { + + private final Map> handlerTypeMap = new HashMap<>(); + + /** + * Register a new action handler type. + * + * @param handlerType The action handler type to register + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + public void registerHandlerType(@NotNull ActionHandlerType handlerType) { + handlerTypeMap.put(handlerType.getName(), handlerType); + } + + /** + * Get all registered handler types names. + * + * @return Names of all registered handler types + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + public @NotNull Collection getAllHandlerTypeNames() { + return handlerTypeMap.keySet(); + } + + /** + * Get the handler type by name. + * + * @param name The name of the handler type + * @return The handler type, or a failure reason if not found + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + public @NotNull Attempt, ActionFailureReason> getHandlerType(@NotNull String name) { + return Option.of(handlerTypeMap.get(name)) + .map(Attempt::, ActionFailureReason>success) + .getOrElse(() -> Attempt.failure(ActionFailureReason.INSTANCE, + Message.of("Unknown action type '" + name + "'. Supported types are: " + + String.join(", ", handlerTypeMap.keySet())))); + } + + /** + * Parse the action handler from the given type and action string, using the console as the command sender. + * + * @param actionType The type of the action handler + * @param action The action string + * @return The parsed action handler, or a failure reason if type is invalid or action string is of invalid format. + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + public @NotNull Attempt, ActionFailureReason> parseHandler(@NotNull String actionType, + @NotNull String action) { + return parseHandler(Bukkit.getConsoleSender(), actionType, action); + } + + /** + * Parse the action handler from the given type and action string. + * + * @param sender The command sender parsing the action + * @param actionType The type of the action handler + * @param action The action string + * @return The parsed action handler, or a failure reason if type is invalid or action string is of invalid format. + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + public @NotNull Attempt, ActionFailureReason> parseHandler(@NotNull CommandSender sender, + @NotNull String actionType, + @NotNull String action) { + return getHandlerType(actionType) + .mapAttempt(actionHandlerType -> actionHandlerType.parseHandler(sender, action)); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/action/ActionHandlerType.java b/src/main/java/org/mvplugins/multiverse/portals/action/ActionHandlerType.java new file mode 100644 index 00000000..3a3c3130 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/action/ActionHandlerType.java @@ -0,0 +1,76 @@ +package org.mvplugins.multiverse.portals.action; + +import org.bukkit.command.CommandSender; +import org.jvnet.hk2.annotations.Contract; +import org.mvplugins.multiverse.core.utils.result.Attempt; +import org.mvplugins.multiverse.external.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Collections; + +/** + * The action handler. + * + * @param This action handler type + * @param The action handler for this type + * + * @since 5.2 + */ +@ApiStatus.AvailableSince("5.2") +@Contract +public abstract class ActionHandlerType, H extends ActionHandler> { + + private final String name; + + /** + * Create a new action handler type. The name must be unique among all action handler types. + *
+ * There is already 3 built-in action handler types: "server", "command", and "multiverse-destination". + * + * @param name The name of the action handler type + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + protected ActionHandlerType(@NotNull String name) { + this.name = name; + } + + /** + * Get the name of the action handler type. + * + * @return The name of the action handler type + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + public @NotNull String getName() { + return name; + } + + /** + * Parse the action string into an action handler. + * + * @param action The action string + * @return The parsed action handler or a failure reason + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + public abstract @NotNull Attempt parseHandler(@NotNull CommandSender sender, @NotNull String action); + + /** + * Suggest action arguments for the given input use for command tab completion. + * + * @param sender The sender running the command + * @param input The current input + * @return A collection of suggestions + * + * @since 5.2 + */ + @ApiStatus.AvailableSince("5.2") + public @NotNull Collection suggestActions(@NotNull CommandSender sender, @NotNull String input) { + return Collections.emptyList(); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/action/types/BungeeServerList.java b/src/main/java/org/mvplugins/multiverse/portals/action/types/BungeeServerList.java new file mode 100644 index 00000000..5d414a79 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/action/types/BungeeServerList.java @@ -0,0 +1,82 @@ +package org.mvplugins.multiverse.portals.action.types; + +import com.dumptruckman.minecraft.util.Logging; +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.event.MVConfigReloadEvent; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.portals.MultiversePortals; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Simple utility class to get the server list from BungeeCord compatible proxy if available. + */ +@Service +final class BungeeServerList implements Listener, PluginMessageListener { + + @NotNull + private final MultiversePortals plugin; + private boolean didFirstRun = false; + private List serverNames; + + @Inject + BungeeServerList(@NotNull MultiversePortals plugin) { + this.plugin = plugin; + serverNames = Collections.emptyList(); + Bukkit.getPluginManager().registerEvents(this, plugin); + Bukkit.getMessenger().registerIncomingPluginChannel(plugin, "BungeeCord", this); + } + + Collection getServerNames() { + return serverNames; + } + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!channel.equals("BungeeCord")) { + return; + } + ByteArrayDataInput in = ByteStreams.newDataInput(message); + String subchannel = in.readUTF(); + if (subchannel.equals("GetServers")) { + // This is our response to the PlayerCount request + serverNames = List.of(in.readUTF().split(", ")); + Logging.fine("BungeeCord GetServers: " + String.join(", ", serverNames)); + } + } + + @EventHandler + void playerJoin(PlayerJoinEvent event) { + if (didFirstRun) { + return; + } + // Delay a bit to make sure player is fully logged in + Bukkit.getScheduler().runTaskLater(plugin, this::callBungeeCordGetServers, 10); + didFirstRun = true; + } + + @EventHandler + void configReload(MVConfigReloadEvent event) { + // On config reload, we should re-query BungeeCord for the server list + callBungeeCordGetServers(); + } + + private void callBungeeCordGetServers() { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + out.writeUTF("GetServers"); + Bukkit.getServer().sendPluginMessage(plugin, "BungeeCord", out.toByteArray()); + Logging.fine("Calling BungeeCord GetServers"); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/action/types/CommandActionHandler.java b/src/main/java/org/mvplugins/multiverse/portals/action/types/CommandActionHandler.java new file mode 100644 index 00000000..fae95706 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/action/types/CommandActionHandler.java @@ -0,0 +1,37 @@ +package org.mvplugins.multiverse.portals.action.types; + +import org.bukkit.ChatColor; +import org.bukkit.entity.Entity; +import org.mvplugins.multiverse.core.locale.message.Message; +import org.mvplugins.multiverse.core.utils.result.Attempt; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.portals.MVPortal; +import org.mvplugins.multiverse.portals.action.ActionFailureReason; +import org.mvplugins.multiverse.portals.action.ActionHandler; + +final class CommandActionHandler extends ActionHandler { + + private final CommandRunner commandRunner; + + CommandActionHandler(CommandActionHandlerType handlerType, CommandRunner commandRunner) { + super(handlerType); + this.commandRunner = commandRunner; + } + + @Override + public @NotNull Attempt runAction(@NotNull MVPortal portal, @NotNull Entity entity) { + commandRunner.runCommand(entity); + return Attempt.success(null); + } + + @Override + public @NotNull Message actionDescription(Entity entity) { + return Message.of(ChatColor.AQUA + "Runs command " + commandRunner.cmdType + " " + ChatColor.GOLD + "/" + + commandRunner.parseCmdStrPlaceholders(entity)); + } + + @Override + public @NotNull String serialise() { + return commandRunner.rawCmd; + } +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/action/types/CommandActionHandlerType.java b/src/main/java/org/mvplugins/multiverse/portals/action/types/CommandActionHandlerType.java new file mode 100644 index 00000000..c8b9f6b0 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/action/types/CommandActionHandlerType.java @@ -0,0 +1,38 @@ +package org.mvplugins.multiverse.portals.action.types; + +import org.bukkit.command.CommandSender; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.locale.message.Message; +import org.mvplugins.multiverse.core.utils.result.Attempt; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.portals.action.ActionFailureReason; +import org.mvplugins.multiverse.portals.action.ActionHandlerType; + +import java.util.Collection; +import java.util.List; + +@Service +final class CommandActionHandlerType extends ActionHandlerType { + + @Inject + CommandActionHandlerType() { + super("command"); + } + + @Override + public @NotNull Attempt parseHandler(@NotNull CommandSender sender, + @NotNull String action) { + if (action.isEmpty()) { + return Attempt.failure(ActionFailureReason.INSTANCE, + Message.of("Please specific a valid command to run as the portal's action.")); + } + return Attempt.success(new CommandActionHandler(this, CommandRunner.fromString(action))); + } + + @Override + public Collection suggestActions(CommandSender sender, String input) { + //todo possibly use command map to suggest tab completion + return List.of("op:", "console:"); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/action/types/CommandRunner.java b/src/main/java/org/mvplugins/multiverse/portals/action/types/CommandRunner.java new file mode 100644 index 00000000..6161688f --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/action/types/CommandRunner.java @@ -0,0 +1,94 @@ +package org.mvplugins.multiverse.portals.action.types; + +import me.clip.placeholderapi.PlaceholderAPI; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Entity; +import org.mvplugins.multiverse.core.utils.REPatterns; + +abstract class CommandRunner { + + static CommandRunner fromString(String command) { + String[] split = REPatterns.COLON.split(command, 2); + return switch ((split.length == 2) ? split[0] : "") { + case "op" -> new Op(command, split[1]); + case "console" -> new Console(command, split[1]); + default -> new Self(command, command); + }; + } + + final String rawCmd; + private final String cmdStr; + final String cmdType; + + private CommandRunner(String rawCmd, String cmdStr, String cmdType) { + this.rawCmd = rawCmd; + this.cmdStr = cmdStr; + this.cmdType = cmdType; + } + + void runCommand(CommandSender sender) { + runCommand(sender, parseCmdStrPlaceholders(sender)); + } + + String parseCmdStrPlaceholders(CommandSender sender) { + String parsedCmd = cmdStr; + if (sender instanceof Entity entity) { + parsedCmd = parsedCmd.replace("%world%", entity.getWorld().getName()); + } + if (sender instanceof OfflinePlayer player) { + parsedCmd = parsedCmd.replace("%player%", String.valueOf(player.getName())); + if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { + parsedCmd = PlaceholderAPI.setPlaceholders(player, parsedCmd); + } + } + return parsedCmd; + } + + protected abstract void runCommand(CommandSender sender, String cmd); + + private static class Self extends CommandRunner { + + private Self(String rawCmd, String cmdStr) { + super(rawCmd, cmdStr, "myself"); + } + + @Override + protected void runCommand(CommandSender sender, String cmd) { + Bukkit.dispatchCommand(sender, cmd); + } + } + + private static class Op extends CommandRunner { + + private Op(String rawCmd, String cmdStr) { + super(rawCmd, cmdStr, "as operator"); + } + + @Override + protected void runCommand(CommandSender sender, String cmd) { + boolean shouldRemoveOp = false; + if (!sender.isOp()) { + sender.setOp(true); + shouldRemoveOp = true; + } + Bukkit.dispatchCommand(sender, cmd); + if (shouldRemoveOp) { + sender.setOp(false); + } + } + } + + private static class Console extends CommandRunner { + + private Console(String rawCmd, String cmdStr) { + super(rawCmd, cmdStr, "from console"); + } + + @Override + protected void runCommand(CommandSender sender, String cmd) { + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), cmd); + } + } +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/action/types/MultiverseDestinationActionHandler.java b/src/main/java/org/mvplugins/multiverse/portals/action/types/MultiverseDestinationActionHandler.java new file mode 100644 index 00000000..5aab7da1 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/action/types/MultiverseDestinationActionHandler.java @@ -0,0 +1,61 @@ +package org.mvplugins.multiverse.portals.action.types; + +import com.dumptruckman.minecraft.util.Logging; +import org.bukkit.ChatColor; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Vehicle; +import org.mvplugins.multiverse.core.destination.DestinationInstance; +import org.mvplugins.multiverse.core.locale.message.Message; +import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter; +import org.mvplugins.multiverse.core.teleportation.PassengerMode; +import org.mvplugins.multiverse.core.teleportation.PassengerModes; +import org.mvplugins.multiverse.core.utils.result.Attempt; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.portals.MVPortal; +import org.mvplugins.multiverse.portals.action.ActionFailureReason; +import org.mvplugins.multiverse.portals.action.ActionHandler; + +final class MultiverseDestinationActionHandler extends ActionHandler { + + private final AsyncSafetyTeleporter teleporter; + private final DestinationInstance destinationInstance; + + MultiverseDestinationActionHandler(MultiverseDestinationActionHandlerType handlerType, + AsyncSafetyTeleporter teleporter, + DestinationInstance destinationInstance) { + super(handlerType); + this.teleporter = teleporter; + this.destinationInstance = destinationInstance; + } + + @Override + public @NotNull Attempt runAction(@NotNull MVPortal portal, @NotNull Entity entity) { + teleporter.to(destinationInstance) + .checkSafety(portal.getCheckDestinationSafety() && destinationInstance.checkTeleportSafety()) + .passengerMode(passengerModeFor(portal, entity)) + .teleportSingle(entity) + .onFailure(reason -> Logging.warning( + "Failed to teleport entity '%s' to destination '%s'. Reason: %s", + entity.getName(), destinationInstance, reason) + ); + return Attempt.success(null); + } + + @Override + public @NotNull Message actionDescription(Entity entity) { + //todo use v5.4's DestinationInstance#getDisplayMessage method + return Message.of(ChatColor.AQUA + "Teleports to " + ChatColor.GOLD + destinationInstance.toString()); + } + + private PassengerMode passengerModeFor(MVPortal portal, Entity entity) { + if (entity instanceof Vehicle) { + return PassengerModes.RETAIN_ALL; + } + return portal.getTeleportNonPlayers() ? PassengerModes.RETAIN_ALL : PassengerModes.DISMOUNT_VEHICLE; + } + + @Override + public @NotNull String serialise() { + return destinationInstance.toString(); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/action/types/MultiverseDestinationActionHandlerType.java b/src/main/java/org/mvplugins/multiverse/portals/action/types/MultiverseDestinationActionHandlerType.java new file mode 100644 index 00000000..81b34820 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/action/types/MultiverseDestinationActionHandlerType.java @@ -0,0 +1,46 @@ +package org.mvplugins.multiverse.portals.action.types; + +import org.bukkit.command.CommandSender; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.destination.DestinationsProvider; +import org.mvplugins.multiverse.core.locale.message.Message; +import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter; +import org.mvplugins.multiverse.core.utils.result.Attempt; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.portals.action.ActionFailureReason; +import org.mvplugins.multiverse.portals.action.ActionHandlerType; + +import java.util.Collection; + +@Service +final class MultiverseDestinationActionHandlerType extends ActionHandlerType { + + private final DestinationsProvider destinationsProvider; + private final AsyncSafetyTeleporter teleporter; + + @Inject + MultiverseDestinationActionHandlerType(@NotNull DestinationsProvider destinationsProvider, + @NotNull AsyncSafetyTeleporter teleporter) { + super("multiverse-destination"); + this.destinationsProvider = destinationsProvider; + this.teleporter = teleporter; + } + + @Override + public @NotNull Attempt parseHandler(@NotNull CommandSender sender, + @NotNull String action) { + if (action.isEmpty()) { + return Attempt.failure(ActionFailureReason.INSTANCE, Message.of("Please specific a multiverse destination as the portal's action.")); + } + return destinationsProvider.parseDestination(sender, action) + .transform(ActionFailureReason.INSTANCE) + .map(destinationInstance -> + new MultiverseDestinationActionHandler(this, teleporter, destinationInstance)); + } + + @Override + public @NotNull Collection suggestActions(@NotNull CommandSender sender, @NotNull String input) { + return destinationsProvider.suggestDestinationStrings(sender, input); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/action/types/ServerActionHandler.java b/src/main/java/org/mvplugins/multiverse/portals/action/types/ServerActionHandler.java new file mode 100644 index 00000000..b19d8692 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/action/types/ServerActionHandler.java @@ -0,0 +1,56 @@ +package org.mvplugins.multiverse.portals.action.types; + +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import org.bukkit.ChatColor; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.mvplugins.multiverse.core.locale.message.Message; +import org.mvplugins.multiverse.core.utils.result.Attempt; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.external.vavr.control.Try; +import org.mvplugins.multiverse.portals.MVPortal; +import org.mvplugins.multiverse.portals.MultiversePortals; +import org.mvplugins.multiverse.portals.action.ActionFailureReason; +import org.mvplugins.multiverse.portals.action.ActionHandler; + +final class ServerActionHandler extends ActionHandler { + + private final MultiversePortals plugin; + private final String serverName; + + ServerActionHandler(ServerActionHandlerType handlerType, MultiversePortals plugin, String serverName) { + super(handlerType); + this.plugin = plugin; + this.serverName = serverName; + } + + @Override + public @NotNull Attempt runAction(@NotNull MVPortal portal, @NotNull Entity entity) { + if (!(entity instanceof Player player)) { + return Attempt.failure(ActionFailureReason.INSTANCE, Message.of("Only players can teleport between servers!")); + } + return Try + .run(() -> { + ByteArrayDataOutput out = ByteStreams.newDataOutput(); + out.writeUTF("Connect"); + out.writeUTF(serverName); + player.sendPluginMessage(plugin, "BungeeCord", out.toByteArray()); + }) + .map(Attempt::success) + .recover(ex -> Attempt.failure(ActionFailureReason.INSTANCE, + Message.of("An error occurred while sending plugin message to proxy: " + ex.getLocalizedMessage()))) + .getOrElse(() -> Attempt.failure(ActionFailureReason.INSTANCE, + Message.of("An unknown error occurred while sending plugin message to proxy."))); + } + + @Override + public Message actionDescription(Entity entity) { + return Message.of(ChatColor.AQUA + "Transfer to " + ChatColor.GOLD + serverName + ChatColor.AQUA + " server"); + } + + @Override + public @NotNull String serialise() { + return serverName; + } +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/action/types/ServerActionHandlerType.java b/src/main/java/org/mvplugins/multiverse/portals/action/types/ServerActionHandlerType.java new file mode 100644 index 00000000..9748a9f2 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/action/types/ServerActionHandlerType.java @@ -0,0 +1,42 @@ +package org.mvplugins.multiverse.portals.action.types; + +import org.bukkit.command.CommandSender; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.locale.message.Message; +import org.mvplugins.multiverse.core.utils.result.Attempt; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.portals.MultiversePortals; +import org.mvplugins.multiverse.portals.action.ActionFailureReason; +import org.mvplugins.multiverse.portals.action.ActionHandlerType; + +import java.util.Collection; + +@Service +final class ServerActionHandlerType extends ActionHandlerType { + + private final MultiversePortals plugin; + private final BungeeServerList bungeeServerList; + + @Inject + ServerActionHandlerType(@NotNull MultiversePortals plugin, @NotNull BungeeServerList bungeeServerList) { + super("server"); + this.plugin = plugin; + this.bungeeServerList = bungeeServerList; + } + + @Override + public @NotNull Attempt parseHandler(@NotNull CommandSender sender, + @NotNull String action) { + if (action.isEmpty()) { + return Attempt.failure(ActionFailureReason.INSTANCE, + Message.of("Please specific a server name as the portal's action.")); + } + return Attempt.success(new ServerActionHandler(this, plugin, action)); + } + + @Override + public @NotNull Collection suggestActions(@NotNull CommandSender sender, @NotNull String input) { + return bungeeServerList.getServerNames(); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/commands/CreateCommand.java b/src/main/java/org/mvplugins/multiverse/portals/commands/CreateCommand.java index ca228786..901c1293 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/commands/CreateCommand.java +++ b/src/main/java/org/mvplugins/multiverse/portals/commands/CreateCommand.java @@ -82,10 +82,10 @@ void onCreateCommand( ps.selectPortal(portal); if (destination != null) { - portal.setDestination(destination); + portal.setAction(destination.toString()); this.plugin.savePortalsConfig(); } else { - player.sendMessage(ChatColor.RED + "Portal destination not set. Use " + ChatColor.DARK_AQUA + "/mvp modify destination " + ChatColor.RED + " to set one."); + player.sendMessage(ChatColor.RED + "Portal action not set. Use " + ChatColor.DARK_AQUA + "/mvp modify action " + ChatColor.RED + " to set one."); } // todo: Automatically get exact destination from player location diff --git a/src/main/java/org/mvplugins/multiverse/portals/commands/ListCommand.java b/src/main/java/org/mvplugins/multiverse/portals/commands/ListCommand.java index 9b02c449..e0bc64d8 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/commands/ListCommand.java +++ b/src/main/java/org/mvplugins/multiverse/portals/commands/ListCommand.java @@ -5,7 +5,6 @@ import org.mvplugins.multiverse.core.command.LegacyAliasCommand; import org.mvplugins.multiverse.core.world.MultiverseWorld; import org.mvplugins.multiverse.core.world.WorldManager; -import org.mvplugins.multiverse.core.command.MVCommandManager; import org.mvplugins.multiverse.external.acf.commands.annotation.CommandAlias; import org.mvplugins.multiverse.external.acf.commands.annotation.CommandCompletion; import org.mvplugins.multiverse.external.acf.commands.annotation.CommandPermission; @@ -18,6 +17,7 @@ import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.portals.MVPortal; +import org.mvplugins.multiverse.portals.utils.DisplayUtils; import org.mvplugins.multiverse.portals.utils.PortalManager; import java.util.ArrayList; @@ -30,11 +30,13 @@ class ListCommand extends PortalsCommand { private final PortalManager portalManager; private final WorldManager worldManager; + private final DisplayUtils displayUtils; @Inject - ListCommand(@NotNull PortalManager portalManager, @NotNull WorldManager worldManager) { + ListCommand(@NotNull PortalManager portalManager, @NotNull WorldManager worldManager, @NotNull DisplayUtils displayUtils) { this.portalManager = portalManager; this.worldManager = worldManager; + this.displayUtils = displayUtils; } @Subcommand("list") @@ -91,43 +93,9 @@ private List getPortals(CommandSender sender, MultiverseWorld world, Str filter = ""; } for (MVPortal portal : (world == null) ? this.portalManager.getPortals(sender) : this.portalManager.getPortals(sender, world)) { - String destination = ""; - if (portal.getDestination() != null) { - destination = portal.getDestination().toString(); - String destType = portal.getDestination().getIdentifier(); - if (destType.equals("w")) { - MultiverseWorld destWorld = this.worldManager.getLoadedWorld(destination).getOrNull(); - if (destWorld != null) { - destination = "(World) " + ChatColor.DARK_AQUA + destination; - } - } - if (destType.equals("p")) { - // todo: I think should use instance check instead of destType prefix - // String targetWorldName = this.portalManager.getPortal(portal.getDestination().getName()).getWorld().getName(); - // destination = "(Portal) " + ChatColor.DARK_AQUA + portal.getDestination().getName() + ChatColor.GRAY + " (" + targetWorldName + ")"; - } - if (destType.equals("e")) { - String destinationWorld = portal.getDestination().toString().split(":")[1]; - String destPart = portal.getDestination().toString().split(":")[2]; - String[] locParts = destPart.split(","); - int x, y, z; - try { - x = (int) Double.parseDouble(locParts[0]); - y = (int) Double.parseDouble(locParts[1]); - z = (int) Double.parseDouble(locParts[2]); - } catch (NumberFormatException e) { - e.printStackTrace(); - continue; - } - if (destType.equals("i")) { - destination = ChatColor.RED + "Invalid destination"; - } - destination = "(Location) " + ChatColor.DARK_AQUA + destinationWorld + ", " + x + ", " + y + ", " + z; - } - } - - if (portal.getName().toLowerCase().contains(filter.toLowerCase()) || (portal.getDestination() != null && destination.toLowerCase().contains(filter.toLowerCase()))) { - portals.add(ChatColor.YELLOW + portal.getName() + ((portal.getDestination() != null) ? (ChatColor.AQUA + " -> " + ChatColor.GOLD + destination) : "")); + String destination = displayUtils.formatActionAsMVDestination(portal); + if (portal.getName().toLowerCase().contains(filter.toLowerCase()) || destination.toLowerCase().contains(filter.toLowerCase())) { + portals.add(ChatColor.YELLOW + portal.getName() + ChatColor.AQUA + " -> " + ChatColor.GOLD + destination); } } java.util.Collections.sort(portals); @@ -147,8 +115,8 @@ private List getPortals(CommandSender sender, MultiverseWorld world, Str @Service private final static class LegacyAlias extends ListCommand implements LegacyAliasCommand { @Inject - LegacyAlias(PortalManager portalManager, WorldManager worldManager) { - super(portalManager, worldManager); + LegacyAlias(PortalManager portalManager, WorldManager worldManager, DisplayUtils displayUtils) { + super(portalManager, worldManager, displayUtils); } @Override diff --git a/src/main/java/org/mvplugins/multiverse/portals/commands/ModifyCommand.java b/src/main/java/org/mvplugins/multiverse/portals/commands/ModifyCommand.java index b6ea5697..30c0633f 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/commands/ModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/portals/commands/ModifyCommand.java @@ -9,6 +9,7 @@ import org.mvplugins.multiverse.external.acf.commands.annotation.CommandAlias; import org.mvplugins.multiverse.external.acf.commands.annotation.CommandCompletion; import org.mvplugins.multiverse.external.acf.commands.annotation.CommandPermission; +import org.mvplugins.multiverse.external.acf.commands.annotation.ConsumesRest; import org.mvplugins.multiverse.external.acf.commands.annotation.Description; import org.mvplugins.multiverse.external.acf.commands.annotation.Flags; import org.mvplugins.multiverse.external.acf.commands.annotation.Single; @@ -49,7 +50,7 @@ public void onModifyCommand( @Description("The property to modify.") String property, - @Single + @ConsumesRest @Syntax("") @Description("The value to set.") String value @@ -67,10 +68,17 @@ public void onModifyCommand( var stringPropertyHandle = portal.getStringPropertyHandle(); stringPropertyHandle.setPropertyString(issuer.getIssuer(), property, value) .onSuccess(ignore -> { - this.plugin.savePortalsConfig(); + if (!this.plugin.savePortalsConfig()) { + issuer.sendError("Could not save portals configuration file!"); + return; + } issuer.sendMessage(ChatColor.GREEN + "Property " + ChatColor.AQUA + property + ChatColor.GREEN + " of Portal " + ChatColor.YELLOW + portal.getName() + ChatColor.GREEN + " was set to " + ChatColor.AQUA + stringPropertyHandle.getProperty(property).getOrNull()); + if (property.equalsIgnoreCase("action-type")) { + issuer.sendError("Please note that changing the action type &edoes NOT modify &cthe action itself. " + + "You need to modify the action property as well to ensure the portal works as expected."); + } }).onFailure(failure -> { issuer.sendError("Property " + ChatColor.AQUA + property + ChatColor.RED + " of Portal " + ChatColor.YELLOW + portal.getName() + ChatColor.RED + " was NOT set to " diff --git a/src/main/java/org/mvplugins/multiverse/portals/config/PortalsConfig.java b/src/main/java/org/mvplugins/multiverse/portals/config/PortalsConfig.java index 57fc206b..de49de96 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/config/PortalsConfig.java +++ b/src/main/java/org/mvplugins/multiverse/portals/config/PortalsConfig.java @@ -295,6 +295,16 @@ public Try setTeleportVehicles(boolean teleportVehicles) { return configHandle.set(configNodes.teleportVehicles, teleportVehicles); } + @ApiStatus.AvailableSince("5.2") + public boolean getTeleportEntities() { + return configHandle.get(configNodes.teleportEntities); + } + + @ApiStatus.AvailableSince("5.2") + public Try setTeleportEntities(boolean teleportEntities) { + return configHandle.set(configNodes.teleportEntities, teleportEntities); + } + /** * * @return diff --git a/src/main/java/org/mvplugins/multiverse/portals/config/PortalsConfigNodes.java b/src/main/java/org/mvplugins/multiverse/portals/config/PortalsConfigNodes.java index bbd2e5ba..bfe9ead5 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/config/PortalsConfigNodes.java +++ b/src/main/java/org/mvplugins/multiverse/portals/config/PortalsConfigNodes.java @@ -151,18 +151,29 @@ public Object serialize(Material material, Class aClass) { final ConfigNode teleportVehicles = node(ConfigNode.builder("portal-usage.teleport-vehicles", Boolean.class) .comment("") .comment("If enabled, mvportals will teleport all vehicles along with its passengers when the vehicle enters the portal.") - .comment("Vehicles are usually boats, minecarts, pigs and horses.") + .comment("Vehicles are usually boats and minecarts.") + .comment("(NOTE: please turn off the server before changing this config option)") .defaultValue(false) .name("teleport-vehicles") .aliases("teleportvehicles") .onSetValue((oldValue, newValue) -> MultiversePortals.TeleportVehicles = newValue) .build()); + final ConfigNode teleportEntities = node(ConfigNode.builder("portal-usage.teleport-entities", Boolean.class) + .comment("") + .comment("If enabled, all living entities can use the portal if property `teleport-non-players` is true.") + .comment("There may be some performance overhead if your server has a large amount of entities moving around.") + .comment("(NOTE: please turn off the server before changing this config option)") + .defaultValue(false) + .name("teleport-entities") + .build()); + final ConfigNode useOnMove = node(ConfigNode.builder("portal-usage.use-on-move", Boolean.class) .comment("") - .comment("If enabled, player movement will be tracked to determine if the player has entered a portal.") + .comment("If enabled, player movement will be tracked to determine if the player/entity has entered a portal.") .comment("Disabling this will cause mvportals without nether or end fill to not work.") .comment("Only disable this if all your portals have nether or end fill and want to slight enhance performance.") + .comment("(NOTE: please turn off the server before changing this config option)") .defaultValue(true) .name("use-on-move") .aliases("useonmove") diff --git a/src/main/java/org/mvplugins/multiverse/portals/event/MVPortalEvent.java b/src/main/java/org/mvplugins/multiverse/portals/event/MVPortalEvent.java index cd621975..12e41e4a 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/event/MVPortalEvent.java +++ b/src/main/java/org/mvplugins/multiverse/portals/event/MVPortalEvent.java @@ -12,6 +12,7 @@ import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.ApiStatus; import org.mvplugins.multiverse.core.destination.DestinationInstance; import org.mvplugins.multiverse.portals.MVPortal; import org.mvplugins.multiverse.portals.enums.PortalType; @@ -76,8 +77,12 @@ public Location getFrom() { /** * Returns the destination that the player will spawn at. * - * @return The destination the player will spawn at. + * @return The destination the player will spawn at. May be null. + * + * @deprecated Portals can have different types of actions, not just destinations. See {@link org.mvplugins.multiverse.portals.action.ActionHandlerProvider} */ + @Deprecated(since = "5.2", forRemoval = true) + @ApiStatus.ScheduledForRemoval(inVersion = "6.0") public DestinationInstance getDestination() { return this.destination; } diff --git a/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPBlockListener.java b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPBlockListener.java index 17734b30..c6288c5e 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPBlockListener.java +++ b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPBlockListener.java @@ -9,31 +9,51 @@ import org.bukkit.Material; import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockFromToEvent; import org.bukkit.event.block.BlockPhysicsEvent; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.portals.config.PortalsConfig; import org.mvplugins.multiverse.portals.utils.PortalManager; @Service -public class MVPBlockListener implements PortalsListener { +final class MVPBlockListener implements PortalsListener { private final PortalManager portalManager; + private final PortalsConfig portalsConfig; @Inject - MVPBlockListener(@NotNull PortalManager portalManager) { + MVPBlockListener(@NotNull PortalManager portalManager, @NotNull PortalsConfig portalsConfig) { this.portalManager = portalManager; + this.portalsConfig = portalsConfig; } - @EventHandler - public void blockPhysics(BlockPhysicsEvent event) { - if (event.isCancelled()) { - return; - } + @EventHandler(ignoreCancelled = true) + void blockPhysics(BlockPhysicsEvent event) { if (event.getChangedType() == Material.NETHER_PORTAL || event.getBlock().getType() == Material.NETHER_PORTAL) { if (portalManager.isPortal(event.getBlock().getLocation())) { event.setCancelled(true); } } } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) + void blockFromTo(BlockFromToEvent event) { + // The to block should never be null, but apparently it is sometimes... + if (event.getBlock() == null || event.getToBlock() == null) { + return; + } + + // If lava/something else is trying to flow in... + if (portalManager.isPortal(event.getToBlock().getLocation())) { + event.setCancelled(true); + return; + } + // If something is trying to flow out, stop that too, unless bucketFilling has been disabled + if (portalManager.isPortal(event.getBlock().getLocation()) && portalsConfig.getBucketFilling()) { + event.setCancelled(true); + } + } } diff --git a/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPCoreListener.java b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPCoreListener.java index 9f54a7c4..be4dc81b 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPCoreListener.java +++ b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPCoreListener.java @@ -22,17 +22,20 @@ import org.bukkit.event.EventHandler; import org.mvplugins.multiverse.portals.MultiversePortals; +import org.mvplugins.multiverse.portals.config.PortalsConfig; import org.mvplugins.multiverse.portals.utils.PortalManager; @Service -public class MVPCoreListener implements PortalsListener { +final class MVPCoreListener implements PortalsListener { private final MultiversePortals plugin; + private final PortalsConfig config; private final PortalManager portalManager; @Inject - MVPCoreListener(@NotNull MultiversePortals plugin, @NotNull PortalManager portalManager) { + MVPCoreListener(@NotNull MultiversePortals plugin, @NotNull PortalManager portalManager, @NotNull PortalsConfig config) { this.plugin = plugin; this.portalManager = portalManager; + this.config = config; } /** @@ -73,26 +76,27 @@ public void debugModeChange(MVDebugModeEvent event) { public void portalTouchEvent(MVPlayerTouchedPortalEvent event) { Logging.finer("Found The TouchedPortal event."); Location l = event.getBlockTouched(); - if (event.canUseThisPortal() && (this.portalManager.isPortal(l))) { - if (this.plugin.getPortalSession(event.getPlayer()).isDebugModeOn()) { - event.setCancelled(true); + if (!event.canUseThisPortal() || (!this.portalManager.isPortal(l))) { + return; + } + if (this.plugin.getPortalSession(event.getPlayer()).isDebugModeOn()) { + event.setCancelled(true); + return; + } + // This is a valid portal, and they can use it so far... + MVPortal p = this.portalManager.getPortal(event.getPlayer(), l); + if (p == null) { + // The player can't see this portal, and can't use it. + Logging.finer(String.format("'%s' was DENIED access to this portal event.", event.getPlayer().getName())); + event.setCanUseThisPortal(false); + } else if (p.getActionHandler().isFailure()) { + if (config.getPortalsDefaultToNether()) { + Logging.finer("Allowing MVPortal to act as nether portal."); return; } - // This is a valid portal, and they can use it so far... - MVPortal p = this.portalManager.getPortal(event.getPlayer(), l); - if (p == null) { - // The player can't see this portal, and can't use it. - Logging.finer(String.format("'%s' was DENIED access to this portal event.", event.getPlayer().getName())); - event.setCanUseThisPortal(false); - } else if (p.getDestination() == null) { - if (this.plugin.getMainConfig().getBoolean("portalsdefaulttonether", false)) { - Logging.finer("Allowing MVPortal to act as nether portal."); - return; - } - // They can see it, is it val - event.getPlayer().sendMessage("This Multiverse Portal does not have a valid destination!"); - event.setCanUseThisPortal(false); - } + // They can see it, is it val + event.getPlayer().sendMessage("This Multiverse Portal does not have a valid destination!"); + event.setCanUseThisPortal(false); } } } diff --git a/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPEntityMoveListener.java b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPEntityMoveListener.java new file mode 100644 index 00000000..4c2ba8f8 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPEntityMoveListener.java @@ -0,0 +1,52 @@ +package org.mvplugins.multiverse.portals.listeners; + +import com.dumptruckman.minecraft.util.Logging; +import io.papermc.paper.event.entity.EntityMoveEvent; +import org.bukkit.Location; +import org.bukkit.entity.LivingEntity; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.portals.MVPortal; +import org.mvplugins.multiverse.portals.config.PortalsConfig; +import org.mvplugins.multiverse.portals.utils.PortalManager; + +@Service +public final class MVPEntityMoveListener implements Listener { + + private final PortalListenerHelper helper; + private final PortalManager portalManager; + private final PortalsConfig portalsConfig; + + @Inject + MVPEntityMoveListener(@NotNull PortalListenerHelper helper, + @NotNull PortalManager portalManager, + @NotNull PortalsConfig portalsConfig) { + this.helper = helper; + this.portalManager = portalManager; + this.portalsConfig = portalsConfig; + } + + @EventHandler(ignoreCancelled = true) + void entityMove(EntityMoveEvent event) { + if (helper.isWithinSameBlock(event.getFrom(), event.getTo())) { + return; + } + + LivingEntity entity = event.getEntity(); + Location location = entity.getLocation(); + + MVPortal portal = portalManager.getPortal(location); + if (portal == null + || !portal.getTeleportNonPlayers() + || (portalsConfig.getNetherAnimation() && !portal.isLegacyPortal())) { + return; + } + + Logging.fine("[EntityMoveEvent] Portal action for entity: " + entity); + helper.stateSuccess(entity.getName(), portal.getName()); + portal.runActionFor(entity); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPEntityPortalListener.java b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPEntityPortalListener.java new file mode 100644 index 00000000..2eb9ee8f --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPEntityPortalListener.java @@ -0,0 +1,66 @@ +package org.mvplugins.multiverse.portals.listeners; + +import com.dumptruckman.minecraft.util.Logging; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityPortalEvent; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.teleportation.BlockSafety; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.portals.MVPortal; +import org.mvplugins.multiverse.portals.config.PortalsConfig; +import org.mvplugins.multiverse.portals.utils.PortalManager; + +@Service +public final class MVPEntityPortalListener implements Listener { + + private final PortalManager portalManager; + private final PortalsConfig portalsConfig; + private final BlockSafety blockSafety; + private final PortalListenerHelper helper; + + @Inject + MVPEntityPortalListener(@NotNull PortalManager portalManager, + @NotNull PortalsConfig portalsConfig, + @NotNull BlockSafety blockSafety, + @NotNull PortalListenerHelper helper) { + this.portalManager = portalManager; + this.portalsConfig = portalsConfig; + this.blockSafety = blockSafety; + this.helper = helper; + } + + @EventHandler(ignoreCancelled = true) + void entityPortal(EntityPortalEvent event) { + Entity entity = event.getEntity(); + Location location = entity.getLocation(); + + MVPortal portal = portalManager.getPortal(location); + if (portal == null) { + // Check around the player to make sure + Location translatedLoc = this.blockSafety.findPortalBlockNextTo(event.getFrom()); + if (translatedLoc != null) { + Logging.finer("Entity was outside of portal, The location has been successfully translated."); + portal = portalManager.getPortal(translatedLoc); + } + } + + if (portal == null) { + return; + } + if (!portal.getTeleportNonPlayers()) { + if (!portalsConfig.getPortalsDefaultToNether()) { + event.setCancelled(true); + } + return; + } + + Logging.fine("[EntityPortalEvent] Portal action for entity: " + entity); + helper.stateSuccess(entity.getName(), portal.getName()); + portal.runActionFor(entity) + .onSuccess(() -> event.setCancelled(true)); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPPlayerListener.java b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPPlayerListener.java index a956e4ce..8712e8f8 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPPlayerListener.java +++ b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPPlayerListener.java @@ -43,47 +43,38 @@ import org.bukkit.inventory.EquipmentSlot; @Service -public class MVPPlayerListener implements PortalsListener { +final class MVPPlayerListener implements PortalsListener { private final MultiversePortals plugin; private final PortalsConfig portalsConfig; private final PortalFiller filler; private final PortalManager portalManager; - private final PlayerListenerHelper helper; private final LocationManipulation locationManipulation; private final WorldManager worldManager; - private final BlockSafety blockSafety; - private final MVEconomist economist; @Inject MVPPlayerListener( @NotNull MultiversePortals plugin, @NotNull PortalsConfig portalsConfig, - @NotNull PlayerListenerHelper helper, @NotNull PortalManager portalManager, @NotNull PortalFiller filler, @NotNull LocationManipulation locationManipulation, - @NotNull WorldManager worldManager, - @NotNull BlockSafety blockSafety, - @NotNull MVEconomist economist) { + @NotNull WorldManager worldManager) { this.plugin = plugin; this.portalsConfig = portalsConfig; - this.helper = helper; this.portalManager = portalManager; this.filler = filler; this.locationManipulation = locationManipulation; this.worldManager = worldManager; - this.blockSafety = blockSafety; - this.economist = economist; } @EventHandler - public void playerQuit(PlayerQuitEvent event) { + void playerQuit(PlayerQuitEvent event) { this.plugin.destroyPortalSession(event.getPlayer()); } @EventHandler(priority = EventPriority.MONITOR) - public void playerTeleport(PlayerTeleportEvent event) { + void playerTeleport(PlayerTeleportEvent event) { if (event.isCancelled()) { Logging.fine("The PlayerTeleportEvent was already cancelled. Doing nothing."); return; @@ -93,7 +84,7 @@ public void playerTeleport(PlayerTeleportEvent event) { } @EventHandler(priority = EventPriority.LOW) - public void playerBucketFill(PlayerBucketFillEvent event) { + void playerBucketFill(PlayerBucketFillEvent event) { if (event.isCancelled()) { Logging.fine("The PlayerBucketFillEvent was already cancelled. Doing nothing."); return; @@ -104,20 +95,25 @@ public void playerBucketFill(PlayerBucketFillEvent event) { PortalPlayerSession ps = this.plugin.getPortalSession(event.getPlayer()); MVPortal portal = portalManager.getPortal(event.getPlayer(), event.getBlockClicked().getLocation()); - if (portal != null) { - if (ps.isDebugModeOn()) { - ps.showDebugInfo(portal); - event.setCancelled(true); - } else { - Material fillMaterial = Material.AIR; - Logging.finer("Fill Material: " + fillMaterial); - this.filler.fillRegion(portal.getPortalLocation().getRegion(), event.getBlockClicked().getLocation(), fillMaterial, event.getPlayer()); - } + if (portal == null) { + return; + } + if (ps.isDebugModeOn()) { + ps.showDebugInfo(portal); + event.setCancelled(true); + return; } + Material fillMaterial = Material.AIR; + Logging.finer("Fill Material: " + fillMaterial); + this.filler.fillRegion( + portal.getPortalLocation().getRegion(), + event.getBlockClicked().getLocation(), + fillMaterial, + event.getPlayer()); } @EventHandler(priority = EventPriority.LOW) - public void playerBucketEmpty(PlayerBucketEmptyEvent event) { + void playerBucketEmpty(PlayerBucketEmptyEvent event) { if (event.isCancelled()) { Logging.fine("The PlayerBucketEmptyEvent was already cancelled. Doing nothing."); return; @@ -135,28 +131,30 @@ public void playerBucketEmpty(PlayerBucketEmptyEvent event) { PortalPlayerSession ps = this.plugin.getPortalSession(event.getPlayer()); MVPortal portal = portalManager.getPortal(event.getPlayer(), translatedLocation); - if (portal != null) { - if (ps.isDebugModeOn()) { - ps.showDebugInfo(portal); - event.setCancelled(true); - } else { - if (!portal.playerCanFillPortal(event.getPlayer())) { - event.setCancelled(true); - return; - } - Material fillMaterial = Material.WATER; - if (event.getBucket().equals(Material.LAVA_BUCKET)) { - fillMaterial = Material.LAVA; - } + if (portal == null) { + return; + } - Logging.finer("Fill Material: " + fillMaterial); - this.filler.fillRegion(portal.getPortalLocation().getRegion(), translatedLocation, fillMaterial, event.getPlayer()); - } + if (ps.isDebugModeOn()) { + ps.showDebugInfo(portal); + event.setCancelled(true); + return; + } + if (!portal.playerCanFillPortal(event.getPlayer())) { + event.setCancelled(true); + return; + } + Material fillMaterial = Material.WATER; + if (event.getBucket().equals(Material.LAVA_BUCKET)) { + fillMaterial = Material.LAVA; } + + Logging.finer("Fill Material: " + fillMaterial); + this.filler.fillRegion(portal.getPortalLocation().getRegion(), translatedLocation, fillMaterial, event.getPlayer()); } @EventHandler(priority = EventPriority.LOW) - public void playerInteract(PlayerInteractEvent event) { + void playerInteract(PlayerInteractEvent event) { if (event.isCancelled()) { Logging.fine("The PlayerInteractEvent was already cancelled. Doing nothing."); return; @@ -164,48 +162,7 @@ public void playerInteract(PlayerInteractEvent event) { // Portal lighting stuff if (event.getAction() == Action.RIGHT_CLICK_BLOCK && event.getMaterial() == Material.FLINT_AND_STEEL) { - // They're lighting somethin' - Logging.finer("Player is lighting block: " + this.locationManipulation.strCoordsRaw(event.getClickedBlock().getLocation())); - PortalPlayerSession ps = this.plugin.getPortalSession(event.getPlayer()); - Location translatedLocation = this.getTranslatedLocation(event.getClickedBlock(), event.getBlockFace()); - if (!portalManager.isPortal(translatedLocation)) { - return; - } - MVPortal portal = portalManager.getPortal(event.getPlayer(), translatedLocation); - if (event.getItem() == null) { - return; - } - if (!event.getPlayer().hasPermission("multiverse.portal.create")) { - return; - } - Material inHand = event.getItem().getType(); - - // Cancel the event if there was a portal. - - if (portal != null) { - - // Make sure the portal's frame around this point is made out of - // a valid material. - if (!portal.isFrameValid(translatedLocation)) { - return; - } - - Logging.finer("Right Clicked: "); - Logging.finer("Block Clicked: " + event.getClickedBlock() + ":" + event.getClickedBlock().getType()); - Logging.finer("Translated Block: " + event.getPlayer().getWorld().getBlockAt(translatedLocation) + ":" + event.getPlayer().getWorld().getBlockAt(translatedLocation).getType()); - Logging.finer("In Hand: " + inHand); - if (ps.isDebugModeOn()) { - ps.showDebugInfo(portal); - event.setCancelled(true); - } else { - Material fillMaterial = Material.NETHER_PORTAL; - if (translatedLocation.getWorld().getBlockAt(translatedLocation).getType() == Material.NETHER_PORTAL) { - fillMaterial = Material.AIR; - } - Logging.finer("Fill Material: " + fillMaterial); - event.setCancelled(this.filler.fillRegion(portal.getPortalLocation().getRegion(), translatedLocation, fillMaterial, event.getPlayer())); - } - } + lightPortalWithFlintAndSteel(event); return; } @@ -231,123 +188,60 @@ public void playerInteract(PlayerInteractEvent event) { } } - private Location getTranslatedLocation(Block clickedBlock, BlockFace face) { - Location clickedLoc = clickedBlock.getLocation(); - Location newLoc = new Location(clickedBlock.getWorld(), face.getModX() + clickedLoc.getBlockX(), face.getModY() + clickedLoc.getBlockY(), face.getModZ() + clickedLoc.getBlockZ()); - Logging.finest("Clicked Block: " + clickedBlock.getLocation()); - Logging.finest("Translated Block: " + newLoc); - return newLoc; - } - - @EventHandler - public void playerPortal(PlayerPortalEvent event) { - if (event.isCancelled()) { - Logging.fine("This Portal event was already cancelled."); + private void lightPortalWithFlintAndSteel(PlayerInteractEvent event) { + // They're lighting somethin' + Logging.finer("Player is lighting block: " + this.locationManipulation.strCoordsRaw(event.getClickedBlock().getLocation())); + PortalPlayerSession ps = this.plugin.getPortalSession(event.getPlayer()); + Location translatedLocation = this.getTranslatedLocation(event.getClickedBlock(), event.getBlockFace()); + if (!portalManager.isPortal(translatedLocation)) { return; } - Logging.finer("onPlayerPortal called!"); - Location playerPortalLoc = event.getPlayer().getLocation(); - // Determine if we're in a portal - MVPortal portal = portalManager.getPortal(event.getPlayer(), playerPortalLoc, false); - Player p = event.getPlayer(); - // Even if the location was null, we still have to see if - // someone wasn't exactly on (because they can do this). - if (portal == null) { - // Check around the player to make sure - playerPortalLoc = this.blockSafety.findPortalBlockNextTo(event.getFrom()); - if (playerPortalLoc != null) { - Logging.finer("Player was outside of portal, The location has been successfully translated."); - portal = portalManager.getPortal(event.getPlayer(), playerPortalLoc, false); - } + MVPortal portal = portalManager.getPortal(event.getPlayer(), translatedLocation); + if (event.getItem() == null) { + return; } - if (portal != null) { - Logging.finer("There was a portal found!"); - DestinationInstance portalDest = portal.getDestination(); - if (portalDest != null) { - // this is a valid MV Portal, so we'll cancel the event - event.setCancelled(true); - - if (!portal.isFrameValid(playerPortalLoc)) { - event.getPlayer().sendMessage("This portal's frame is made of an " + ChatColor.RED + "incorrect material." + ChatColor.RED + " You should exit it now."); - return; - } - - Location destLocation = portalDest.getLocation(event.getPlayer()).getOrNull(); - if (destLocation == null) { - Logging.fine("Unable to teleport player because destination is null!"); - return; - } - - if (!this.worldManager.isLoadedWorld(destLocation.getWorld())) { - Logging.fine("Unable to teleport player because the destination world is not managed by Multiverse!"); - return; - } - - if (portal.getCheckDestinationSafety() && portalDest.checkTeleportSafety()) { - Location safeLocation = blockSafety.findSafeSpawnLocation(portalDest.getLocation(event.getPlayer()).getOrNull()); - if (safeLocation == null) { - event.setCancelled(true); - Logging.warning("Portal " + portal.getName() + " destination is not safe!"); - event.getPlayer().sendMessage(ChatColor.RED + "Portal " + portal.getName() + " destination is not safe!"); - return; - } - destLocation = safeLocation; - } - event.setTo(destLocation); - - PortalPlayerSession ps = this.plugin.getPortalSession(event.getPlayer()); - - if (ps.checkAndSendCooldownMessage()) { - Logging.fine("Player denied teleportation due to cooldown."); - return; - } - // If they're using Access and they don't have permission and they're NOT exempt, return, they're not allowed to tp. - // No longer checking exemption status - if (portalsConfig.getEnforcePortalAccess() && !event.getPlayer().hasPermission(portal.getPermission())) { - this.helper.stateFailure(p.getDisplayName(), portal.getName()); - return; - } - - boolean shouldPay = false; - double price = portal.getPrice(); - Material currency = portal.getCurrency(); - - // Stop the player if the portal costs and they can't pay - if (price != 0D && !p.hasPermission(portal.getExempt())) { - shouldPay = true; - if (price > 0D && !economist.isPlayerWealthyEnough(p, price, currency)) { - p.sendMessage(economist.getNSFMessage(currency, - "You need " + economist.formatPrice(price, currency) + " to enter the " + portal.getName() + " portal.")); - return; - } - } + if (!event.getPlayer().hasPermission("multiverse.portal.create")) { + return; + } + Material inHand = event.getItem().getType(); - MVPortalEvent portalEvent = new MVPortalEvent(portalDest, event.getPlayer(), portal); - this.plugin.getServer().getPluginManager().callEvent(portalEvent); + // Cancel the event if there was a portal. + if (portal == null) { + return; + } - if (portalEvent.isCancelled()) { - Logging.fine("Someone cancelled the MVPlayerPortal Event!"); - return; - } else if (shouldPay) { - if (price < 0D) { - economist.deposit(p, -price, currency); - } else { - economist.withdraw(p, price, currency); - } - p.sendMessage(String.format("You have %s %s for using %s.", - price > 0D ? "been charged" : "earned", - economist.formatPrice(price, currency), - portal.getName())); - } + // Make sure the portal's frame around this point is made out of + // a valid material. + if (!portal.isFrameValid(translatedLocation)) { + return; + } - event.getPlayer().teleport(event.getTo()); - } else if (!portalsConfig.getPortalsDefaultToNether()) { - // If portals should not default to the nether, cancel the event - event.getPlayer().sendMessage(String.format( - "This portal %sdoesn't go anywhere. You should exit it now.", ChatColor.RED)); - Logging.fine("Event canceled because this was a MVPortal with an invalid destination. But you had 'portalsdefaulttonether' set to false!"); - event.setCancelled(true); - } + Logging.finer("Right Clicked: "); + Logging.finer("Block Clicked: " + event.getClickedBlock() + ":" + event.getClickedBlock().getType()); + Logging.finer("Translated Block: " + event.getPlayer().getWorld().getBlockAt(translatedLocation) + ":" + event.getPlayer().getWorld().getBlockAt(translatedLocation).getType()); + Logging.finer("In Hand: " + inHand); + if (ps.isDebugModeOn()) { + ps.showDebugInfo(portal); + event.setCancelled(true); + return; + } + Material fillMaterial = Material.NETHER_PORTAL; + if (translatedLocation.getWorld().getBlockAt(translatedLocation).getType() == Material.NETHER_PORTAL) { + fillMaterial = Material.AIR; } + Logging.finer("Fill Material: " + fillMaterial); + event.setCancelled(this.filler.fillRegion( + portal.getPortalLocation().getRegion(), + translatedLocation, + fillMaterial, + event.getPlayer())); + } + + private Location getTranslatedLocation(Block clickedBlock, BlockFace face) { + Location clickedLoc = clickedBlock.getLocation(); + Location newLoc = new Location(clickedBlock.getWorld(), face.getModX() + clickedLoc.getBlockX(), face.getModY() + clickedLoc.getBlockY(), face.getModZ() + clickedLoc.getBlockZ()); + Logging.finest("Clicked Block: " + clickedBlock.getLocation()); + Logging.finest("Translated Block: " + newLoc); + return newLoc; } } diff --git a/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPPlayerMoveListener.java b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPPlayerMoveListener.java index 4cc1abc1..0dcb2aab 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPPlayerMoveListener.java +++ b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPPlayerMoveListener.java @@ -9,9 +9,6 @@ import com.dumptruckman.minecraft.util.Logging; import org.bukkit.event.Listener; -import org.mvplugins.multiverse.core.destination.DestinationInstance; -import org.mvplugins.multiverse.core.economy.MVEconomist; -import org.mvplugins.multiverse.core.world.WorldManager; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.jvnet.hk2.annotations.Service; @@ -23,70 +20,32 @@ import org.mvplugins.multiverse.portals.event.MVPortalEvent; import org.bukkit.ChatColor; import org.bukkit.Location; -import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; -import org.bukkit.event.block.BlockFromToEvent; import org.bukkit.event.player.PlayerMoveEvent; -import org.mvplugins.multiverse.portals.utils.PortalManager; @Service -public class MVPPlayerMoveListener implements Listener { +public final class MVPPlayerMoveListener implements Listener { private final MultiversePortals plugin; private final PortalsConfig portalsConfig; - private final PlayerListenerHelper helper; - private final PortalManager portalManager; - private final WorldManager worldManager; - private final MVEconomist economist; + private final PortalListenerHelper helper; @Inject MVPPlayerMoveListener( @NotNull MultiversePortals plugin, @NotNull PortalsConfig portalsConfig, - @NotNull PlayerListenerHelper helper, - @NotNull PortalManager portalManager, - @NotNull WorldManager worldManager, - @NotNull MVEconomist economist) { + @NotNull PortalListenerHelper helper) { this.plugin = plugin; this.portalsConfig = portalsConfig; this.helper = helper; - this.portalManager = portalManager; - this.worldManager = worldManager; - this.economist = economist; } - @EventHandler(priority = EventPriority.LOW) - public void blockFromTo(BlockFromToEvent event) { - // Always check if the event has been canceled by someone else. - if(event.isCancelled()) { - return; - } - - // The to block should never be null, but apparently it is sometimes... - if (event.getBlock() == null || event.getToBlock() == null) { - return; - } - - // If lava/something else is trying to flow in... - if (portalManager.isPortal(event.getToBlock().getLocation())) { - event.setCancelled(true); - return; - } - // If something is trying to flow out, stop that too, unless bucketFilling has been disabled - if (portalManager.isPortal(event.getBlock().getLocation()) && portalsConfig.getBucketFilling()) { - event.setCancelled(true); - } - } - - @EventHandler(priority = EventPriority.LOW) - public void playerMove(PlayerMoveEvent event) { - if (event.isCancelled()) { - return; - } - Player p = event.getPlayer(); // Grab Player - Location loc = p.getLocation(); // Grab Location + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW) + void playerMove(PlayerMoveEvent event) { + Player player = event.getPlayer(); // Grab Player + Location loc = player.getLocation(); // Grab Location // Check the Player has actually moved a block to prevent unneeded calculations... This is to prevent huge performance drops on high player count servers. PortalPlayerSession ps = this.plugin.getPortalSession(event.getPlayer()); @@ -99,74 +58,39 @@ public void playerMove(PlayerMoveEvent event) { MVPortal portal = ps.getStandingInPortal(); // If the portal is not null, and it's a legacy portal, // and we didn't show debug info (the debug is meant to toggle), do the stuff. - if (portal != null - && (!portalsConfig.getNetherAnimation() || portal.isLegacyPortal()) - && ps.doTeleportPlayer(MoveType.PLAYER_MOVE) - && !ps.showDebugInfo()) { - - DestinationInstance d = portal.getDestination(); - if (d == null) { - Logging.fine("Invalid Destination!"); - return; - } - p.setFallDistance(0); + if (portal == null + || (portalsConfig.getNetherAnimation() && !portal.isLegacyPortal()) + || !ps.doTeleportPlayer(MoveType.PLAYER_MOVE) + || ps.showDebugInfo()) { + return; + } - Location destLocation = d.getLocation(p).getOrNull(); - if (destLocation == null) { - Logging.fine("Unable to teleport player because destination is null!"); - return; - } + player.setFallDistance(0); - if (!this.worldManager.isLoadedWorld(destLocation.getWorld())) { - Logging.fine("Unable to teleport player because the destination world is not managed by Multiverse!"); - return; - } - if (!portal.isFrameValid(loc)) { - p.sendMessage("This portal's frame is made of an " + ChatColor.RED + "incorrect material. You should exit it now."); - return; - } - if (ps.checkAndSendCooldownMessage()) { - return; - } - // If they're using Access and they don't have permission and they're NOT excempt, return, they're not allowed to tp. - // No longer checking exemption status - if (portalsConfig.getEnforcePortalAccess() && !event.getPlayer().hasPermission(portal.getPermission())) { - this.helper.stateFailure(p.getDisplayName(), portal.getName()); - return; - } + if (!portal.isFrameValid(loc)) { + player.sendMessage("This portal's frame is made of an " + ChatColor.RED + "incorrect material. You should exit it now."); + return; + } + if (ps.checkAndSendCooldownMessage()) { + return; + } - double price = portal.getPrice(); - Material currency = portal.getCurrency(); + PortalListenerHelper.PortalUseResult portalUseResult = helper.checkPlayerCanUsePortal(portal, player); + if (!portalUseResult.canUse()) { + return; + } - if (price != 0D && !p.hasPermission(portal.getExempt())) { - if (price < 0D || economist.isPlayerWealthyEnough(p, price, currency)) { - // call event for other plugins - MVPortalEvent portalEvent = new MVPortalEvent(d, event.getPlayer(), portal); - this.plugin.getServer().getPluginManager().callEvent(portalEvent); - if (!portalEvent.isCancelled()) { - if (price < 0D) { - economist.deposit(p, -price, currency); - } else { - economist.withdraw(p, price, currency); - } - p.sendMessage(String.format("You have %s %s for using %s.", - price > 0D ? "been charged" : "earned", - economist.formatPrice(price, currency), - portal.getName())); - helper.performTeleport(event.getPlayer(), event.getTo(), ps, d, portal.getCheckDestinationSafety()); - } - } else { - p.sendMessage(economist.getNSFMessage(currency, - "You need " + economist.formatPrice(price, currency) + " to enter the " + portal.getName() + " portal.")); - } - } else { - // call event for other plugins - MVPortalEvent portalEvent = new MVPortalEvent(d, event.getPlayer(), portal); - this.plugin.getServer().getPluginManager().callEvent(portalEvent); - if (!portalEvent.isCancelled()) { - helper.performTeleport(event.getPlayer(), event.getTo(), ps, d, portal.getCheckDestinationSafety()); - } - } + // call event for other plugins + MVPortalEvent portalEvent = new MVPortalEvent(portal.getDestination(), event.getPlayer(), portal); + this.plugin.getServer().getPluginManager().callEvent(portalEvent); + if (portalEvent.isCancelled()) { + return; + } + if (portalUseResult.needToPay()) { + helper.payPortalEntryFee(portal, player); } + + Logging.fine("[PlayerMoveEvent] Portal action for player: " + player); + portal.runActionFor(player); } } diff --git a/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPPlayerPortalListener.java b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPPlayerPortalListener.java new file mode 100644 index 00000000..b9a1fb5e --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPPlayerPortalListener.java @@ -0,0 +1,110 @@ +package org.mvplugins.multiverse.portals.listeners; + +import com.dumptruckman.minecraft.util.Logging; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerPortalEvent; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.core.teleportation.BlockSafety; +import org.mvplugins.multiverse.core.world.WorldManager; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.portals.MVPortal; +import org.mvplugins.multiverse.portals.MultiversePortals; +import org.mvplugins.multiverse.portals.PortalPlayerSession; +import org.mvplugins.multiverse.portals.config.PortalsConfig; +import org.mvplugins.multiverse.portals.enums.MoveType; +import org.mvplugins.multiverse.portals.event.MVPortalEvent; +import org.mvplugins.multiverse.portals.utils.PortalManager; + +@Service +final class MVPPlayerPortalListener implements PortalsListener { + + private final PortalManager portalManager; + private final PortalsConfig portalsConfig; + private final BlockSafety blockSafety; + private final MultiversePortals plugin; + private final PortalListenerHelper helper; + + @Inject + MVPPlayerPortalListener(@NotNull PortalManager portalManager, + @NotNull PortalsConfig portalsConfig, + @NotNull BlockSafety blockSafety, + @NotNull MultiversePortals plugin, + @NotNull PortalListenerHelper helper) { + this.portalManager = portalManager; + this.portalsConfig = portalsConfig; + this.blockSafety = blockSafety; + this.plugin = plugin; + this.helper = helper; + } + + @EventHandler(ignoreCancelled = true) + void playerPortal(PlayerPortalEvent event) { + Logging.finer("onPlayerPortal called!"); + Player player = event.getPlayer(); + Location playerPortalLoc = player.getLocation(); + // Determine if we're in a portal + MVPortal portal = portalManager.getPortal(player, playerPortalLoc, false); + // Even if the location was null, we still have to see if + // someone wasn't exactly on (because they can do this). + if (portal == null) { + // Check around the player to make sure + playerPortalLoc = this.blockSafety.findPortalBlockNextTo(event.getFrom()); + if (playerPortalLoc != null) { + Logging.finer("Player was outside of portal, The location has been successfully translated."); + portal = portalManager.getPortal(player, playerPortalLoc, false); + } + } + + if (portal == null) { + return; + } + + Logging.finer("There was a portal found!"); + if (!portalsConfig.getPortalsDefaultToNether()) { + event.setCancelled(true); + } + + if (!portal.isFrameValid(playerPortalLoc)) { + player.sendMessage("This portal's frame is made of an " + ChatColor.RED + "incorrect material." + ChatColor.RED + " You should exit it now."); + event.setCancelled(true); + return; + } + + PortalPlayerSession ps = this.plugin.getPortalSession(player); + ps.setStaleLocation(playerPortalLoc, MoveType.PLAYER_MOVE); + if (ps.showDebugInfo()) { + event.setCancelled(true); + return; + } + if (ps.checkAndSendCooldownMessage()) { + Logging.fine("Player denied teleportation due to cooldown."); + return; + } + PortalListenerHelper.PortalUseResult portalUseResult = helper.checkPlayerCanUsePortal(portal, player); + if (!portalUseResult.canUse()) { + return; + } + + MVPortalEvent portalEvent = new MVPortalEvent(portal.getDestination(), player, portal); + this.plugin.getServer().getPluginManager().callEvent(portalEvent); + + if (portalEvent.isCancelled()) { + Logging.fine("Someone cancelled the MVPlayerPortal Event!"); + return; + } + if (portalUseResult.needToPay()) { + helper.payPortalEntryFee(portal, player); + } + + Logging.fine("[PlayerPortalEvent] Portal action for player: " + player); + helper.stateSuccess(player.getDisplayName(), portal.getName()); + portal.runActionFor(player) + .onSuccess(() -> { + event.setCancelled(true); + }); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPVehicleListener.java b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPVehicleListener.java index 202f6520..561d9a31 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPVehicleListener.java +++ b/src/main/java/org/mvplugins/multiverse/portals/listeners/MVPVehicleListener.java @@ -8,21 +8,15 @@ package org.mvplugins.multiverse.portals.listeners; import java.util.ArrayList; +import java.util.Date; import java.util.List; import com.dumptruckman.minecraft.util.Logging; -import org.bukkit.Material; import org.bukkit.event.Listener; -import org.mvplugins.multiverse.core.destination.DestinationInstance; -import org.mvplugins.multiverse.core.economy.MVEconomist; -import org.mvplugins.multiverse.core.permissions.CorePermissionsChecker; -import org.mvplugins.multiverse.core.teleportation.LocationManipulation; import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter; -import org.mvplugins.multiverse.core.teleportation.PassengerModes; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.jvnet.hk2.annotations.Service; -import org.mvplugins.multiverse.portals.config.PortalsConfig; import org.mvplugins.multiverse.portals.enums.MoveType; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; @@ -36,45 +30,42 @@ import org.mvplugins.multiverse.portals.utils.PortalManager; @Service -public class MVPVehicleListener implements Listener { +public final class MVPVehicleListener implements Listener { private final MultiversePortals plugin; private final PortalManager portalManager; - private final AsyncSafetyTeleporter safetyTeleporter; - private final PortalsConfig portalsConfig; - private final MVEconomist economist; + private final PortalListenerHelper helper; @Inject MVPVehicleListener( @NotNull MultiversePortals plugin, @NotNull PortalManager portalManager, - @NotNull AsyncSafetyTeleporter safetyTeleporter, - @NotNull PortalsConfig portalsConfig, - @NotNull MVEconomist economist + @NotNull PortalListenerHelper helper ) { this.plugin = plugin; this.portalManager = portalManager; - this.safetyTeleporter = safetyTeleporter; - this.portalsConfig = portalsConfig; - this.economist = economist; + this.helper = helper; } @EventHandler - public void vehicleMove(VehicleMoveEvent event) { + void vehicleMove(VehicleMoveEvent event) { + if (helper.isWithinSameBlock(event.getFrom(), event.getTo())) { + return; + } Vehicle vehicle = event.getVehicle(); List playerPassengers = new ArrayList<>(); boolean hasNonPlayers = false; for (Entity entity : vehicle.getPassengers()) { - if (entity instanceof Player player) { - PortalPlayerSession ps = this.plugin.getPortalSession(player); - ps.setStaleLocation(vehicle.getLocation(), MoveType.VEHICLE_MOVE); - if (ps.isStaleLocation()) { - Logging.finer("Player %s is stale, not teleporting vehicle", player.getName()); - return; - } - playerPassengers.add(player); - } else { + if (!(entity instanceof Player player)) { hasNonPlayers = true; + continue; } + PortalPlayerSession ps = this.plugin.getPortalSession(player); + ps.setStaleLocation(vehicle.getLocation(), MoveType.VEHICLE_MOVE); + if (ps.isStaleLocation()) { + Logging.finer("Player %s is stale, not teleporting vehicle", player.getName()); + return; + } + playerPassengers.add(player); } MVPortal portal = this.portalManager.getPortal(vehicle.getLocation()); @@ -95,55 +86,21 @@ public void vehicleMove(VehicleMoveEvent event) { } for (Player player : playerPassengers) { - if (!checkPlayerCanUsePortal(portal, player)) { + PortalListenerHelper.PortalUseResult portalUseResult = helper.checkPlayerCanUsePortal(portal, player); + if (!portalUseResult.canUse()) { Logging.finer("Player %s is not allowed to use portal %s, removing them from the vehicle.", player.getName(), portal.getName()); vehicle.removePassenger(player); } - } - - DestinationInstance destination = portal.getDestination(); - safetyTeleporter.to(destination) - .checkSafety(portal.getCheckDestinationSafety() && destination.checkTeleportSafety()) - .passengerMode(PassengerModes.RETAIN_ALL) - .teleportSingle(vehicle) - .onSuccess(() -> Logging.finer("Successfully teleported vehicle %s using portal %s", - vehicle.getName(), portal.getName())) - .onFailure(failures -> Logging.finer("Failed to teleport vehicle %s using portal %s. Failures: %s", - vehicle.getName(), portal.getName(), failures)); - } - - // todo: this logic is duplicated in multiple places - private boolean checkPlayerCanUsePortal(MVPortal portal, Player player) { - // If they're using Access and they don't have permission and they're NOT exempt, return, they're not allowed to tp. - // No longer checking exemption status - if (portalsConfig.getEnforcePortalAccess() && !player.hasPermission(portal.getPermission())) { - - return false; - } - - boolean shouldPay = false; - double price = portal.getPrice(); - Material currency = portal.getCurrency(); - - // Stop the player if the portal costs and they can't pay - if (price != 0D && !player.hasPermission(portal.getExempt())) { - shouldPay = true; - if (price > 0D && !economist.isPlayerWealthyEnough(player, price, currency)) { - player.sendMessage(economist.getNSFMessage(currency, - "You need " + economist.formatPrice(price, currency) + " to enter the " + portal.getName() + " portal.")); - return false; - } - } - - if (shouldPay) { - if (price < 0D) { - economist.deposit(player, -price, currency); - } else { - economist.withdraw(player, price, currency); + if (portalUseResult.needToPay()) { + helper.payPortalEntryFee(portal, player); } } - return true; + Logging.fine("[VehicleMoveEvent] Portal action for vehicle: " + vehicle); + helper.stateSuccess(vehicle.getName(), portal.getName()); + portal.runActionFor(vehicle) + .onSuccess(() -> playerPassengers.forEach(player -> + plugin.getPortalSession(player).setTeleportTime(new Date()))); } } diff --git a/src/main/java/org/mvplugins/multiverse/portals/listeners/PlayerListenerHelper.java b/src/main/java/org/mvplugins/multiverse/portals/listeners/PlayerListenerHelper.java deleted file mode 100644 index e83288b2..00000000 --- a/src/main/java/org/mvplugins/multiverse/portals/listeners/PlayerListenerHelper.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.mvplugins.multiverse.portals.listeners; - -import java.util.Date; - -import com.dumptruckman.minecraft.util.Logging; -import org.mvplugins.multiverse.core.destination.DestinationInstance; -import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter; -import org.mvplugins.multiverse.external.jakarta.inject.Inject; -import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; -import org.jvnet.hk2.annotations.Service; -import org.mvplugins.multiverse.portals.PortalPlayerSession; -import org.bukkit.Location; -import org.bukkit.entity.Player; - -@Service -final class PlayerListenerHelper { - - private final AsyncSafetyTeleporter safetyTeleporter; - - @Inject - PlayerListenerHelper(@NotNull AsyncSafetyTeleporter safetyTeleporter) { - this.safetyTeleporter = safetyTeleporter; - } - - void stateSuccess(String playerName, String worldName) { - Logging.fine(String.format( - "MV-Portals is allowing Player '%s' to use the portal '%s'.", - playerName, worldName)); - } - - void stateFailure(String playerName, String portalName) { - Logging.fine(String.format( - "MV-Portals is DENYING Player '%s' access to use the portal '%s'.", - playerName, portalName)); - } - - void performTeleport(Player player, Location to, PortalPlayerSession ps, DestinationInstance destination, boolean checkSafety) { - safetyTeleporter.to(destination) - .checkSafety(checkSafety && destination.checkTeleportSafety()) - .teleportSingle(player) - .onSuccess(() -> { - ps.playerDidTeleport(to); - ps.setTeleportTime(new Date()); - this.stateSuccess(player.getDisplayName(), destination.toString()); - }) - .onFailure(reason -> Logging.fine( - "Failed to teleport player '%s' to destination '%s'. Reason: %s", - player.getDisplayName(), destination, reason) - ); - } -} diff --git a/src/main/java/org/mvplugins/multiverse/portals/listeners/PortalListenerHelper.java b/src/main/java/org/mvplugins/multiverse/portals/listeners/PortalListenerHelper.java new file mode 100644 index 00000000..787ef938 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/portals/listeners/PortalListenerHelper.java @@ -0,0 +1,96 @@ +package org.mvplugins.multiverse.portals.listeners; + +import com.dumptruckman.minecraft.util.Logging; +import org.bukkit.Material; +import org.mvplugins.multiverse.core.economy.MVEconomist; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.portals.MVPortal; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.mvplugins.multiverse.portals.config.PortalsConfig; + +@Service +final class PortalListenerHelper { + + private final PortalsConfig portalsConfig; + private final MVEconomist economist; + + @Inject + PortalListenerHelper(@NotNull PortalsConfig portalsConfig, + @NotNull MVEconomist economist) { + this.portalsConfig = portalsConfig; + this.economist = economist; + } + + boolean isWithinSameBlock(Location from, Location to) { + return from.getWorld() == to.getWorld() + && from.getBlockX() == to.getBlockX() + && from.getBlockY() == to.getBlockY() + && from.getBlockZ() == to.getBlockZ(); + } + + void stateSuccess(String playerName, String portalName) { + Logging.fine(String.format( + "MV-Portals is allowing Player '%s' to use the portal '%s'.", + playerName, portalName)); + } + + void stateFailure(String playerName, String portalName) { + Logging.fine(String.format( + "MV-Portals is DENYING Player '%s' access to use the portal '%s'.", + playerName, portalName)); + } + + PortalUseResult checkPlayerCanUsePortal(MVPortal portal, Player player) { + // If they're using Access and they don't have permission and they're NOT exempt, return, they're not allowed to tp. + // No longer checking exemption status + if (portalsConfig.getEnforcePortalAccess() && !player.hasPermission(portal.getPermission())) { + stateFailure(player.getDisplayName(), portal.getName()); + return PortalUseResult.CANNOT_USE; + } + + double price = portal.getPrice(); + Material currency = portal.getCurrency(); + + // Stop the player if the portal costs and they can't pay + if (price == 0D || player.hasPermission(portal.getExempt())) { + return PortalUseResult.FREE_USE; + } + + if (price > 0D && !economist.isPlayerWealthyEnough(player, price, currency)) { + player.sendMessage(economist.getNSFMessage(currency, + "You need " + economist.formatPrice(price, currency) + " to enter the " + portal.getName() + " portal.")); + stateFailure(player.getDisplayName(), portal.getName()); + return PortalUseResult.CANNOT_USE; + } + return PortalUseResult.PAID_USE; + } + + void payPortalEntryFee(MVPortal portal, Player player) { + economist.payEntryFee(player, portal.getPrice(), portal.getCurrency()); + } + + enum PortalUseResult { + CANNOT_USE(false, false), + FREE_USE(true, false), + PAID_USE(true, true); + + private final boolean canUse; + private final boolean needToPay; + + PortalUseResult(boolean canUse, boolean needToPay) { + this.canUse = canUse; + this.needToPay = needToPay; + } + + public boolean canUse() { + return canUse; + } + + public boolean needToPay() { + return needToPay; + } + } +} diff --git a/src/main/java/org/mvplugins/multiverse/portals/utils/DisplayUtils.java b/src/main/java/org/mvplugins/multiverse/portals/utils/DisplayUtils.java index a50cd199..f04f7c29 100644 --- a/src/main/java/org/mvplugins/multiverse/portals/utils/DisplayUtils.java +++ b/src/main/java/org/mvplugins/multiverse/portals/utils/DisplayUtils.java @@ -1,69 +1,114 @@ package org.mvplugins.multiverse.portals.utils; +import org.bukkit.entity.Entity; +import org.mvplugins.multiverse.core.command.MVCommandIssuer; +import org.mvplugins.multiverse.core.command.MVCommandManager; import org.mvplugins.multiverse.core.economy.MVEconomist; +import org.mvplugins.multiverse.core.locale.message.Message; +import org.mvplugins.multiverse.core.utils.result.Attempt; import org.mvplugins.multiverse.core.world.MultiverseWorld; import org.mvplugins.multiverse.core.world.WorldManager; import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.external.jetbrains.annotations.ApiStatus; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.portals.MVPortal; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; +import org.mvplugins.multiverse.portals.action.ActionFailureReason; +import org.mvplugins.multiverse.portals.action.ActionHandler; + +import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace; @Service public class DisplayUtils { private final WorldManager worldManager; private final MVEconomist economist; + private final MVCommandManager commandManager; @Inject DisplayUtils( @NotNull WorldManager worldManager, - @NotNull MVEconomist economist) { + @NotNull MVEconomist economist, + @NotNull MVCommandManager commandManager) { this.worldManager = worldManager; this.economist = economist; + this.commandManager = commandManager; } public void showStaticInfo(CommandSender sender, MVPortal portal, String message) { - sender.sendMessage(ChatColor.AQUA + "--- " + message + ChatColor.DARK_AQUA + portal.getName() + ChatColor.AQUA + " ---"); + MVCommandIssuer issuer = commandManager.getCommandIssuer(sender); + + issuer.sendMessage(ChatColor.AQUA + "--- " + message + ChatColor.DARK_AQUA + portal.getName() + ChatColor.AQUA + " ---"); String[] locParts = portal.getPortalLocation().toString().split(":"); - sender.sendMessage("Coords: " + ChatColor.GOLD + locParts[0] + ChatColor.WHITE + " to " + ChatColor.GOLD + locParts[1] + ChatColor.WHITE + " in " + ChatColor.GOLD + portal.getWorld().getName() ); - if (portal.getDestination() == null) { - sender.sendMessage("Destination: " + ChatColor.RED + ChatColor.ITALIC + "NOT SET!"); + issuer.sendMessage(ChatColor.WHITE + "Coords: " + ChatColor.GOLD + locParts[1] + ChatColor.WHITE + " to " + ChatColor.GOLD + locParts[2] + ChatColor.WHITE + " in " + ChatColor.GOLD + portal.getWorld().getName() ); + issuer.sendMessage(ChatColor.WHITE + "Configured Action Type: " + ChatColor.GOLD + portal.getActionType()); + if (portal.getAction().isEmpty()) { + issuer.sendMessage(ChatColor.WHITE + "Configured Action: " + ChatColor.RED + ChatColor.ITALIC + "NOT SET!"); } else { - String destination = portal.getDestination().toString(); - String destType = portal.getDestination().getIdentifier(); - if (destType.equals("w")) { - MultiverseWorld destWorld = worldManager.getWorld(destination).getOrNull(); - if (destWorld != null) { - destination = "(World) " + ChatColor.DARK_AQUA + destination; - } - } - if (destType.equals("p")) { - // todo: I think should use instance check instead of destType prefix - // String targetWorldName = portalManager.getPortal(portal.getDestination().getName()).getWorld().getName(); - // destination = "(Portal) " + ChatColor.DARK_AQUA + portal.getDestination().getName() + ChatColor.GRAY + " (" + targetWorldName + ")"; - } - if (destType.equals("e")) { - String destinationWorld = portal.getDestination().toString().split(":")[1]; - String destPart = portal.getDestination().toString().split(":")[2]; - String[] targetParts = destPart.split(","); - int x, y, z; - try { - x = (int) Double.parseDouble(targetParts[0]); - y = (int) Double.parseDouble(targetParts[1]); - z = (int) Double.parseDouble(targetParts[2]); - } catch (NumberFormatException e) { - e.printStackTrace(); - return; - } - destination = "(Location) " + ChatColor.DARK_AQUA + destinationWorld + ", " + x + ", " + y + ", " + z; + issuer.sendMessage(ChatColor.WHITE + "Configured Action: " + ChatColor.GOLD + portal.getAction()); + } + Attempt, ActionFailureReason> actionHandler = portal.getActionHandler(); + + issuer.sendMessage(ChatColor.WHITE + "Check Destination Safety: " + formatBoolean(portal.getCheckDestinationSafety())); + issuer.sendMessage(ChatColor.WHITE + "Teleport Non Players: " + formatBoolean(portal.getTeleportNonPlayers())); + showPortalPriceInfo(portal, sender); + + if (sender instanceof Entity entity) { + actionHandler.map(handler -> handler.actionDescription(entity)) + .onSuccess(actionMessage -> { + issuer.sendMessage(""); + issuer.sendMessage(Message.of(ChatColor.WHITE + "Your action when using Portal: {action}", + replace("{action}").with(actionMessage))); + }); + } + + actionHandler.onFailure(failure -> { + issuer.sendMessage(""); + issuer.sendError(failure.getFailureMessage()); + }); + } + + private String formatBoolean(Boolean bool) { + return bool ? ChatColor.GREEN + "true" : ChatColor.RED + "false"; + } + + @ApiStatus.AvailableSince("5.2") + public String formatActionAsMVDestination(MVPortal portal) { + String[] split = portal.getAction().split(":", 2); + String destination = split.length == 2 ? split[1] : ""; + String destType = split.length == 2 ? split[0] : ""; + if (destType.equals("w")) { + MultiverseWorld destWorld = worldManager.getWorld(destination).getOrNull(); + if (destWorld != null) { + return "(World) " + ChatColor.DARK_AQUA + destination; } - if (destType.equals("i")) { - destination = ChatColor.RED + "Invalid Destination!"; + } + if (destType.equals("p")) { + // todo: I think should use instance check instead of destType prefix + // String targetWorldName = portalManager.getPortal(portal.getDestination().getName()).getWorld().getName(); + // destination = "(Portal) " + ChatColor.DARK_AQUA + portal.getDestination().getName() + ChatColor.GRAY + " (" + targetWorldName + ")"; + } + if (destType.equals("e")) { + String destinationWorld = portal.getAction().split(":")[1]; + String destPart = portal.getAction().split(":")[2]; + String[] targetParts = destPart.split(","); + int x, y, z; + try { + x = (int) Double.parseDouble(targetParts[0]); + y = (int) Double.parseDouble(targetParts[1]); + z = (int) Double.parseDouble(targetParts[2]); + } catch (NumberFormatException e) { + e.printStackTrace(); + return ChatColor.RED + "Invalid Exact World Location!"; } - sender.sendMessage("Destination: " + ChatColor.GOLD + destination); + return "(Location) " + ChatColor.DARK_AQUA + destinationWorld + ", " + x + ", " + y + ", " + z; + } + if (destType.equals("i")) { + return ChatColor.RED + "Invalid Destination!"; } + return ChatColor.DARK_AQUA + portal.getAction(); } public void showPortalPriceInfo(MVPortal portal, CommandSender sender) { diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 75dfa420..9f44d426 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -4,4 +4,4 @@ authors: ['Rigby', 'fernferret', 'benwoo1110'] version: ${version} api-version: 1.13 depend: ['Multiverse-Core'] -softdepend: ['WorldEdit'] +softdepend: ['WorldEdit', 'PlaceholderAPI']