From ad47ab234041714db26737eb2c2ee6fc1c4b5f78 Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Tue, 27 Jun 2023 16:44:44 +0800 Subject: [PATCH] feat: Revamp world entry checking --- .../commands/TeleportCommand.java | 17 ++- .../MultiverseCore/economy/MVEconomist.java | 30 ++++ .../listeners/MVPlayerListener.java | 111 ++++++-------- .../permissions/CorePermissions.java | 8 + .../permissions/CorePermissionsChecker.java | 39 +++++ .../MultiverseCore/utils/MVCorei18n.java | 15 +- .../utils/checkresult/CheckResult.java | 143 ++++++++++++++++++ .../utils/checkresult/CheckResultChain.java | 143 ++++++++++++++++++ .../utils/checkresult/FailureReason.java | 11 ++ .../utils/checkresult/SuccessReason.java | 11 ++ .../utils/message/MessageReplacement.java | 2 +- .../world/configuration/EntryFee.java | 2 +- .../world/entrycheck/BlacklistResult.java | 30 ++++ .../world/entrycheck/EntryFeeResult.java | 33 ++++ .../world/entrycheck/PlayerLimitResult.java | 30 ++++ .../world/entrycheck/WorldAccessResult.java | 29 ++++ .../world/entrycheck/WorldEntryChecker.java | 103 +++++++++++++ .../entrycheck/WorldEntryCheckerProvider.java | 32 ++++ .../resources/multiverse-core_en.properties | 11 ++ 19 files changed, 723 insertions(+), 77 deletions(-) create mode 100644 src/main/java/com/onarandombox/MultiverseCore/permissions/CorePermissions.java create mode 100644 src/main/java/com/onarandombox/MultiverseCore/permissions/CorePermissionsChecker.java create mode 100644 src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/CheckResult.java create mode 100644 src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/CheckResultChain.java create mode 100644 src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/FailureReason.java create mode 100644 src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/SuccessReason.java create mode 100644 src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/BlacklistResult.java create mode 100644 src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/EntryFeeResult.java create mode 100644 src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/PlayerLimitResult.java create mode 100644 src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/WorldAccessResult.java create mode 100644 src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/WorldEntryChecker.java create mode 100644 src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/WorldEntryCheckerProvider.java diff --git a/src/main/java/com/onarandombox/MultiverseCore/commands/TeleportCommand.java b/src/main/java/com/onarandombox/MultiverseCore/commands/TeleportCommand.java index cee3b5c5..92116532 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/commands/TeleportCommand.java +++ b/src/main/java/com/onarandombox/MultiverseCore/commands/TeleportCommand.java @@ -11,6 +11,7 @@ import co.aikar.commands.annotation.Description; import co.aikar.commands.annotation.Flags; import co.aikar.commands.annotation.Subcommand; import co.aikar.commands.annotation.Syntax; +import com.dumptruckman.minecraft.util.Logging; import com.onarandombox.MultiverseCore.commandtools.MVCommandManager; import com.onarandombox.MultiverseCore.commandtools.MultiverseCommand; import com.onarandombox.MultiverseCore.destination.DestinationsProvider; @@ -49,17 +50,17 @@ public class TeleportCommand extends MultiverseCommand { ) { // TODO Add warning if teleporting too many players at once. + String playerName = players.length == 1 + ? issuer.getPlayer() == players[0] ? "you" : players[0].getName() + : players.length + " players"; + + issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS, + "{player}", playerName, "{destination}", destination.toString()); + CompletableFuture.allOf(Arrays.stream(players) .map(player -> this.destinationsProvider.playerTeleportAsync(issuer, player, destination)) .toArray(CompletableFuture[]::new)) - .thenRun(() -> { - String playerName = players.length == 1 - ? issuer.getPlayer() == players[0] ? "you" : players[0].getName() - : players.length + " players"; - - issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS, - "{player}", playerName, "{destination}", destination.toString()); - }); + .thenRun(() -> Logging.finer("Async teleport completed.")); } @Override diff --git a/src/main/java/com/onarandombox/MultiverseCore/economy/MVEconomist.java b/src/main/java/com/onarandombox/MultiverseCore/economy/MVEconomist.java index bd694dc6..340afd8b 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/economy/MVEconomist.java +++ b/src/main/java/com/onarandombox/MultiverseCore/economy/MVEconomist.java @@ -1,5 +1,6 @@ package com.onarandombox.MultiverseCore.economy; +import com.onarandombox.MultiverseCore.api.MVWorld; import jakarta.inject.Inject; import org.bukkit.Material; import org.bukkit.World; @@ -91,6 +92,35 @@ public class MVEconomist { return "Sorry, you don't have enough " + (isItemCurrency(currency) ? "items" : "funds") + ". " + message; } + /** + * Pays for a given amount of currency either from the player's economy account or inventory if the currency. + * + * @param player the player to deposit currency into. + * @param world the world to take entry fee from. + */ + public void payEntryFee(Player player, MVWorld world) { + payEntryFee(player, world.getPrice(), world.getCurrency()); + } + + /** + * Pays for a given amount of currency either from the player's economy account or inventory if the currency + * + * @param player the player to take currency from. + * @param price the amount to take. + * @param currency the type of currency. + */ + public void payEntryFee(Player player, double price, Material currency) { + if (price == 0D) { + return; + } + + if (price < 0) { + this.deposit(player, -price, currency); + } else { + this.withdraw(player, price, currency); + } + } + /** * Deposits a given amount of currency either into the player's economy account or inventory if the currency * is not null. diff --git a/src/main/java/com/onarandombox/MultiverseCore/listeners/MVPlayerListener.java b/src/main/java/com/onarandombox/MultiverseCore/listeners/MVPlayerListener.java index e8f68dec..0474af47 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/listeners/MVPlayerListener.java +++ b/src/main/java/com/onarandombox/MultiverseCore/listeners/MVPlayerListener.java @@ -16,12 +16,17 @@ import com.onarandombox.MultiverseCore.MultiverseCore; import com.onarandombox.MultiverseCore.api.MVWorld; import com.onarandombox.MultiverseCore.api.MVWorldManager; import com.onarandombox.MultiverseCore.api.SafeTTeleporter; +import com.onarandombox.MultiverseCore.commandtools.MVCommandManager; import com.onarandombox.MultiverseCore.config.MVCoreConfig; +import com.onarandombox.MultiverseCore.economy.MVEconomist; import com.onarandombox.MultiverseCore.event.MVRespawnEvent; import com.onarandombox.MultiverseCore.inject.InjectableListener; import com.onarandombox.MultiverseCore.teleportation.TeleportQueue; import com.onarandombox.MultiverseCore.utils.MVPermissions; import com.onarandombox.MultiverseCore.utils.PermissionTools; +import com.onarandombox.MultiverseCore.utils.checkresult.CheckResultChain; +import com.onarandombox.MultiverseCore.world.entrycheck.EntryFeeResult; +import com.onarandombox.MultiverseCore.world.entrycheck.WorldEntryCheckerProvider; import jakarta.inject.Inject; import jakarta.inject.Provider; import org.bukkit.GameMode; @@ -54,6 +59,9 @@ public class MVPlayerListener implements InjectableListener { private final SafeTTeleporter safeTTeleporter; private final Server server; private final TeleportQueue teleportQueue; + private final MVEconomist economist; + private final WorldEntryCheckerProvider worldEntryCheckerProvider; + private final Provider commandManagerProvider; private final Map playerWorld = new ConcurrentHashMap(); @@ -66,8 +74,10 @@ public class MVPlayerListener implements InjectableListener { Provider mvPermsProvider, SafeTTeleporter safeTTeleporter, Server server, - TeleportQueue teleportQueue - ) { + TeleportQueue teleportQueue, + MVEconomist economist, + WorldEntryCheckerProvider worldEntryCheckerProvider, + Provider commandManagerProvider) { this.plugin = plugin; this.config = config; this.worldManagerProvider = worldManagerProvider; @@ -76,12 +86,19 @@ public class MVPlayerListener implements InjectableListener { this.safeTTeleporter = safeTTeleporter; this.server = server; this.teleportQueue = teleportQueue; + this.economist = economist; + this.worldEntryCheckerProvider = worldEntryCheckerProvider; + this.commandManagerProvider = commandManagerProvider; } private MVWorldManager getWorldManager() { return worldManagerProvider.get(); } + private MVCommandManager getCommandManager() { + return commandManagerProvider.get(); + } + private MVPermissions getMVPerms() { return mvPermsProvider.get(); } @@ -189,7 +206,7 @@ public class MVPlayerListener implements InjectableListener { return; } Player teleportee = event.getPlayer(); - CommandSender teleporter = null; + CommandSender teleporter; Optional teleporterName = teleportQueue.popFromQueue(teleportee.getName()); if (teleporterName.isPresent()) { if (teleporterName.equals("CONSOLE")) { @@ -198,6 +215,8 @@ public class MVPlayerListener implements InjectableListener { } else { teleporter = this.server.getPlayerExact(teleporterName.get()); } + } else { + teleporter = teleportee; } Logging.finer("Inferred sender '" + teleporter + "' from name '" + teleporterName + "', fetched from name '" + teleportee.getName() + "'"); @@ -215,50 +234,20 @@ public class MVPlayerListener implements InjectableListener { this.stateSuccess(teleportee.getName(), toWorld.getAlias()); return; } - // TODO: Refactor these lines. - // Charge the teleporter - event.setCancelled(!pt.playerHasMoneyToEnter(fromWorld, toWorld, teleporter, teleportee, true)); - if (event.isCancelled() && teleporter != null) { - Logging.fine("Player '" + teleportee.getName() - + "' was DENIED ACCESS to '" + toWorld.getAlias() - + "' because '" + teleporter.getName() - + "' don't have the FUNDS required to enter it."); - return; - } - // Check if player is allowed to enter the world if we're enforcing permissions - if (config.getEnforceAccess()) { - event.setCancelled(!pt.playerCanGoFromTo(fromWorld, toWorld, teleporter, teleportee)); - if (event.isCancelled() && teleporter != null) { - Logging.fine("Player '" + teleportee.getName() - + "' was DENIED ACCESS to '" + toWorld.getAlias() - + "' because '" + teleporter.getName() - + "' don't have: multiverse.access." + event.getTo().getWorld().getName()); - return; - } - } else { - Logging.fine("Player '" + teleportee.getName() - + "' was allowed to go to '" + toWorld.getAlias() + "' because enforceaccess is off."); - } - - // Does a limit actually exist? - if (toWorld.getPlayerLimit() > -1) { - // Are there equal or more people on the world than the limit? - if (toWorld.getCBWorld().getPlayers().size() >= toWorld.getPlayerLimit()) { - // Ouch the world is full, lets see if the player can bypass that limitation - if (!pt.playerCanBypassPlayerLimit(toWorld, teleporter, teleportee)) { - Logging.fine("Player '" + teleportee.getName() - + "' was DENIED ACCESS to '" + toWorld.getAlias() - + "' because the world is full and '" + teleporter.getName() - + "' doesn't have: mv.bypass.playerlimit." + event.getTo().getWorld().getName()); + CheckResultChain entryResult = worldEntryCheckerProvider.forSender(teleporter).canEnterWorld(fromWorld, toWorld) + .onSuccessReason(EntryFeeResult.Success.class, reason -> { + if (reason == EntryFeeResult.Success.ENOUGH_MONEY) { + economist.payEntryFee((Player) teleporter, toWorld); + // Send payment receipt + } + }) + .onFailure(results -> { event.setCancelled(true); - return; - } - } - } + getCommandManager().getCommandIssuer(teleporter).sendError(results.getLastResultMessage()); + }); - // By this point anything cancelling the event has returned on the method, meaning the teleport is a success \o/ - this.stateSuccess(teleportee.getName(), toWorld.getAlias()); + Logging.fine("Teleport result: %s", entryResult); } private void stateSuccess(String playerName, String worldName) { @@ -305,6 +294,10 @@ public class MVPlayerListener implements InjectableListener { if (event.getTo() == null) { return; } + if (config.isUsingCustomPortalSearch()) { + event.setSearchRadius(config.getCustomPortalSearchRadius()); + } + MVWorld fromWorld = getWorldManager().getMVWorld(event.getFrom().getWorld().getName()); MVWorld toWorld = getWorldManager().getMVWorld(event.getTo().getWorld().getName()); if (event.getFrom().getWorld().equals(event.getTo().getWorld())) { @@ -312,28 +305,14 @@ public class MVPlayerListener implements InjectableListener { Logging.finer("Player '" + event.getPlayer().getName() + "' is portaling to the same world."); return; } - event.setCancelled(!pt.playerHasMoneyToEnter(fromWorld, toWorld, event.getPlayer(), event.getPlayer(), true)); - if (event.isCancelled()) { - Logging.fine("Player '" + event.getPlayer().getName() - + "' was DENIED ACCESS to '" + event.getTo().getWorld().getName() - + "' because they don't have the FUNDS required to enter."); - return; - } - if (config.getEnforceAccess()) { - event.setCancelled(!pt.playerCanGoFromTo(fromWorld, toWorld, event.getPlayer(), event.getPlayer())); - if (event.isCancelled()) { - Logging.fine("Player '" + event.getPlayer().getName() - + "' was DENIED ACCESS to '" + event.getTo().getWorld().getName() - + "' because they don't have: multiverse.access." + event.getTo().getWorld().getName()); - } - } else { - Logging.fine("Player '" + event.getPlayer().getName() - + "' was allowed to go to '" + event.getTo().getWorld().getName() - + "' because enforceaccess is off."); - } - if (!config.isUsingCustomPortalSearch()) { - event.setSearchRadius(config.getCustomPortalSearchRadius()); - } + + CheckResultChain entryResult = worldEntryCheckerProvider.forSender(event.getPlayer()).canEnterWorld(fromWorld, toWorld) + .onFailure(results -> { + event.setCancelled(true); + getCommandManager().getCommandIssuer(event.getPlayer()).sendError(results.getLastResultMessage()); + }); + + Logging.fine("Teleport result: %s", entryResult); } private void sendPlayerToDefaultWorld(final Player player) { diff --git a/src/main/java/com/onarandombox/MultiverseCore/permissions/CorePermissions.java b/src/main/java/com/onarandombox/MultiverseCore/permissions/CorePermissions.java new file mode 100644 index 00000000..11cd83b0 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/permissions/CorePermissions.java @@ -0,0 +1,8 @@ +package com.onarandombox.MultiverseCore.permissions; + +public class CorePermissions { + public static String WORLD_ACCESS = "multiverse.access"; + public static String WORLD_EXEMPT = "multiverse.exempt"; + public static String GAMEMODE_BYPASS = "mv.bypass.gamemode"; + public static String PLAYERLIMIT_BYPASS = "mv.bypass.playerlimit"; +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/permissions/CorePermissionsChecker.java b/src/main/java/com/onarandombox/MultiverseCore/permissions/CorePermissionsChecker.java new file mode 100644 index 00000000..da728d51 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/permissions/CorePermissionsChecker.java @@ -0,0 +1,39 @@ +package com.onarandombox.MultiverseCore.permissions; + +import com.dumptruckman.minecraft.util.Logging; +import com.onarandombox.MultiverseCore.api.MVWorld; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jvnet.hk2.annotations.Service; + +@Service +public class CorePermissionsChecker { + public boolean hasWorldAccessPermission(@NotNull CommandSender sender, @NotNull MVWorld world) { + return hasPermission(sender, concatPermission(CorePermissions.WORLD_ACCESS, world.getName())); + } + + public boolean hasWorldExemptPermission(@NotNull CommandSender sender, @NotNull MVWorld world) { + return hasPermission(sender, concatPermission(CorePermissions.WORLD_EXEMPT, world.getName())); + } + + public boolean hasPlayerLimitBypassPermission(@NotNull CommandSender sender, @NotNull MVWorld world) { + return hasPermission(sender, concatPermission(CorePermissions.PLAYERLIMIT_BYPASS, world.getName())); + } + + public boolean hasGameModeBypassPermission(@NotNull CommandSender sender, @NotNull MVWorld world) { + return hasPermission(sender, concatPermission(CorePermissions.GAMEMODE_BYPASS, world.getName())); + } + + private String concatPermission(String permission, String...child) { + return permission + "." + String.join(".", child); + } + + private boolean hasPermission(CommandSender sender, String permission) { + if (sender.hasPermission(permission)) { + Logging.finer("Checking to see if sender [%s] has permission [%s]... YES", sender.getName(), permission); + return true; + } + Logging.finer("Checking to see if sender [%s] has permission [%s]... NO", sender.getName(), permission); + return false; + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/utils/MVCorei18n.java b/src/main/java/com/onarandombox/MultiverseCore/utils/MVCorei18n.java index 388b619d..0c2b3351 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/utils/MVCorei18n.java +++ b/src/main/java/com/onarandombox/MultiverseCore/utils/MVCorei18n.java @@ -80,7 +80,20 @@ public enum MVCorei18n implements MessageKeyProvider { // debug command DEBUG_INFO_OFF, - DEBUG_INFO_ON; + DEBUG_INFO_ON, + + // entry check + ENTRYCHECK_BLACKLISTED, + ENTRYCHECK_NOTENOUGHMONEY, + ENTRYCHECK_CANNOTPAYENTRYFEE, + ENTRYCHECK_EXCEEDPLAYERLIMIT, + ENTRYCHECK_NOWORLDACCESS, + + // generic + GENERIC_SUCCESS, + GENERIC_FAILURE + + ; private final MessageKey key = MessageKey.of("mv-core." + this.name().replace('_', '.').toLowerCase()); diff --git a/src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/CheckResult.java b/src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/CheckResult.java new file mode 100644 index 00000000..73cb85f3 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/CheckResult.java @@ -0,0 +1,143 @@ +package com.onarandombox.MultiverseCore.utils.checkresult; + +import co.aikar.commands.CommandIssuer; +import com.onarandombox.MultiverseCore.commandtools.PluginLocales; +import com.onarandombox.MultiverseCore.utils.message.Message; +import com.onarandombox.MultiverseCore.utils.message.MessageReplacement; +import org.jetbrains.annotations.NotNull; + +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.Consumer; + +public sealed interface CheckResult permits CheckResult.Success, CheckResult.Failure { + static CheckResult success(S successReason, MessageReplacement...replacements) { + return new Success<>(successReason, replacements); + } + + static CheckResult failure(F failureReason, MessageReplacement...replacements) { + return new Failure<>(failureReason, replacements); + } + + boolean isSuccess(); + + boolean isFailure(); + + S getSuccessReason(); + + F getFailureReason(); + + @NotNull Message getReasonMessage(); + + default CheckResult onSuccess(Consumer consumer) { + if (this.isSuccess()) { + consumer.accept(this.getSuccessReason()); + } + return this; + } + + default CheckResult onFailure(Consumer consumer) { + if (this.isFailure()) { + consumer.accept(this.getFailureReason()); + } + return this; + } + + default CheckResult onSuccessReason(S successReason, Consumer consumer) { + if (this.isSuccess() && this.getSuccessReason() == successReason) { + consumer.accept(this.getSuccessReason()); + } + return this; + } + + default CheckResult onFailureReason(F failureReason, Consumer consumer) { + if (this.isFailure() && this.getFailureReason() == failureReason) { + consumer.accept(this.getFailureReason()); + } + return this; + } + + final class Success implements CheckResult { + private final S successReason; + private final MessageReplacement[] replacements; + + public Success(S successReason, MessageReplacement[] replacements) { + this.successReason = successReason; + this.replacements = replacements; + } + + @Override + public boolean isSuccess() { + return true; + } + + @Override + public boolean isFailure() { + return false; + } + + @Override + public S getSuccessReason() { + return successReason; + } + + @Override + public F getFailureReason() { + throw new NoSuchElementException("No reason for failure"); + } + + @Override + public @NotNull Message getReasonMessage() { + return Message.of(successReason, "Success!", replacements); + } + + @Override + public String toString() { + return "Success{" + + "reason=" + successReason + + '}'; + } + } + + final class Failure implements CheckResult { + private final F failureReason; + private final MessageReplacement[] replacements; + + public Failure(F failureReason, MessageReplacement[] replacements) { + this.failureReason = failureReason; + this.replacements = replacements; + } + + @Override + public boolean isSuccess() { + return false; + } + + @Override + public boolean isFailure() { + return true; + } + + @Override + public S getSuccessReason() { + throw new NoSuchElementException("No reason for success"); + } + + @Override + public F getFailureReason() { + return failureReason; + } + + @Override + public @NotNull Message getReasonMessage() { + return Message.of(failureReason, "Success!", replacements); + } + + @Override + public String toString() { + return "Failure{" + + "reason=" + failureReason + + '}'; + } + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/CheckResultChain.java b/src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/CheckResultChain.java new file mode 100644 index 00000000..73078bb0 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/CheckResultChain.java @@ -0,0 +1,143 @@ +package com.onarandombox.MultiverseCore.utils.checkresult; + +import com.google.common.collect.Iterables; +import com.onarandombox.MultiverseCore.utils.message.Message; +import io.vavr.control.Option; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class CheckResultChain { + public static Builder builder() { + return new Builder(true); + } + + public static Builder builder(boolean stopOnFailure) { + return new Builder(stopOnFailure); + } + + private final boolean isSuccess; + private final List> results; + + CheckResultChain(boolean isSuccess, List> results) { + this.isSuccess = isSuccess; + this.results = results; + } + + public boolean isSuccess() { + return isSuccess; + } + + public boolean isFailure() { + return !isSuccess; + } + + public CheckResultChain onSuccess(Runnable successRunnable) { + if (isSuccess) { + successRunnable.run(); + } + return this; + } + + public CheckResultChain onFailure(Runnable failureRunnable) { + if (isFailure()) { + failureRunnable.run(); + } + return this; + } + + public CheckResultChain onSuccess(Consumer successRunnable) { + if (isSuccess) { + successRunnable.accept(this); + } + return this; + } + + public CheckResultChain onFailure(Consumer failureRunnable) { + if (isFailure()) { + failureRunnable.accept(this); + } + return this; + } + + public CheckResultChain onSuccessReason(Class successReasonClass, Consumer successConsumer) { + getSuccessReason(successReasonClass).peek(successConsumer); + return this; + } + + public CheckResultChain onFailureReason(Class failureReasonClass, Consumer failureConsumer) { + getFailureReason(failureReasonClass).peek(failureConsumer); + return this; + } + + public CheckResultChain onSuccessReason(S successReason, Runnable successRunnable) { + getSuccessReason(successReason.getClass()).filter(successReason::equals).peek(reason -> successRunnable.run()); + return this; + } + + public Option getSuccessReason(Class successReasonClass) { + if (isFailure()) { + return Option.none(); + } + return Option.ofOptional(results.stream() + .map(CheckResult::getSuccessReason) + .filter(successReasonClass::isInstance) + .map(successReasonClass::cast) + .findFirst()); + } + + public Option getFailureReason(Class failureReasonClass) { + if (isSuccess()) { + return Option.none(); + } + return Option.ofOptional(results.stream() + .map(CheckResult::getFailureReason) + .filter(failureReasonClass::isInstance) + .map(failureReasonClass::cast) + .findFirst()); + } + + public Message getLastResultMessage() { + return Iterables.getLast(results).getReasonMessage(); + } + + @Override + public String toString() { + return "ResultGroup{" + + "isSuccess=" + isSuccess + + ", results={" + results.stream().map(Objects::toString).collect(Collectors.joining(", ")) + "}" + + '}'; + } + + public static class Builder { + private final boolean stopOnFailure; + private final List> results; + + private boolean isSuccess = true; + + public Builder(boolean stopOnFailure) { + this.stopOnFailure = stopOnFailure; + this.results = new ArrayList<>(); + } + + public Builder then(Supplier> resultSupplier) { + if (!isSuccess && stopOnFailure) { + return this; + } + CheckResult result = resultSupplier.get(); + if (result.isFailure()) { + isSuccess = false; + } + results.add(result); + return this; + } + + public CheckResultChain build() { + return new CheckResultChain(isSuccess, results); + } + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/FailureReason.java b/src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/FailureReason.java new file mode 100644 index 00000000..24e096af --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/FailureReason.java @@ -0,0 +1,11 @@ +package com.onarandombox.MultiverseCore.utils.checkresult; + +import co.aikar.locales.MessageKey; +import co.aikar.locales.MessageKeyProvider; +import com.onarandombox.MultiverseCore.utils.MVCorei18n; + +public interface FailureReason extends MessageKeyProvider { + default MessageKey getMessageKey() { + return MVCorei18n.GENERIC_FAILURE.getMessageKey(); + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/SuccessReason.java b/src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/SuccessReason.java new file mode 100644 index 00000000..0482cff5 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/utils/checkresult/SuccessReason.java @@ -0,0 +1,11 @@ +package com.onarandombox.MultiverseCore.utils.checkresult; + +import co.aikar.locales.MessageKey; +import co.aikar.locales.MessageKeyProvider; +import com.onarandombox.MultiverseCore.utils.MVCorei18n; + +public interface SuccessReason extends MessageKeyProvider { + default MessageKey getMessageKey() { + return MVCorei18n.GENERIC_SUCCESS.getMessageKey(); + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/utils/message/MessageReplacement.java b/src/main/java/com/onarandombox/MultiverseCore/utils/message/MessageReplacement.java index e6301a35..ee928f70 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/utils/message/MessageReplacement.java +++ b/src/main/java/com/onarandombox/MultiverseCore/utils/message/MessageReplacement.java @@ -7,7 +7,7 @@ import org.jetbrains.annotations.Nullable; /** * Captures string replacements for {@link Message}s. */ -public final class MessageReplacement { + public final class MessageReplacement { /** * Creates a replacement key for the given key string. diff --git a/src/main/java/com/onarandombox/MultiverseCore/world/configuration/EntryFee.java b/src/main/java/com/onarandombox/MultiverseCore/world/configuration/EntryFee.java index de082605..436f46e1 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/world/configuration/EntryFee.java +++ b/src/main/java/com/onarandombox/MultiverseCore/world/configuration/EntryFee.java @@ -22,7 +22,7 @@ public class EntryFee extends SerializationConfig { @Nullable private Material currency; - private final Material DISABLED_MATERIAL = Material.AIR; + public static final Material DISABLED_MATERIAL = Material.AIR; public EntryFee() { super(); diff --git a/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/BlacklistResult.java b/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/BlacklistResult.java new file mode 100644 index 00000000..74f7fb6f --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/BlacklistResult.java @@ -0,0 +1,30 @@ +package com.onarandombox.MultiverseCore.world.entrycheck; + +import co.aikar.locales.MessageKey; +import co.aikar.locales.MessageKeyProvider; +import com.onarandombox.MultiverseCore.utils.MVCorei18n; +import com.onarandombox.MultiverseCore.utils.checkresult.FailureReason; +import com.onarandombox.MultiverseCore.utils.checkresult.SuccessReason; + +public class BlacklistResult { + public enum Success implements SuccessReason { + UNKNOWN_FROM_WORLD, + BYPASSED_BLACKLISTED, + NOT_BLACKLISTED + } + + public enum Failure implements FailureReason { + BLACKLISTED(MVCorei18n.ENTRYCHECK_BLACKLISTED); + + private final MessageKeyProvider message; + + Failure(MessageKeyProvider message) { + this.message = message; + } + + @Override + public MessageKey getMessageKey() { + return message.getMessageKey(); + } + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/EntryFeeResult.java b/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/EntryFeeResult.java new file mode 100644 index 00000000..c59c10d6 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/EntryFeeResult.java @@ -0,0 +1,33 @@ +package com.onarandombox.MultiverseCore.world.entrycheck; + + +import co.aikar.locales.MessageKey; +import co.aikar.locales.MessageKeyProvider; +import com.onarandombox.MultiverseCore.utils.MVCorei18n; +import com.onarandombox.MultiverseCore.utils.checkresult.FailureReason; +import com.onarandombox.MultiverseCore.utils.checkresult.SuccessReason; + +public class EntryFeeResult { + public enum Success implements SuccessReason { + FREE_ENTRY, + ENOUGH_MONEY, + EXEMPT_FROM_ENTRY_FEE, + CONSOLE_OR_BLOCK_COMMAND_SENDER + } + + public enum Failure implements FailureReason { + NOT_ENOUGH_MONEY(MVCorei18n.ENTRYCHECK_NOTENOUGHMONEY), + CANNOT_PAY_ENTRY_FEE(MVCorei18n.ENTRYCHECK_CANNOTPAYENTRYFEE); + + private final MessageKeyProvider message; + + Failure(MessageKeyProvider message) { + this.message = message; + } + + @Override + public MessageKey getMessageKey() { + return message.getMessageKey(); + } + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/PlayerLimitResult.java b/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/PlayerLimitResult.java new file mode 100644 index 00000000..4b5bf5a4 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/PlayerLimitResult.java @@ -0,0 +1,30 @@ +package com.onarandombox.MultiverseCore.world.entrycheck; + +import co.aikar.locales.MessageKey; +import co.aikar.locales.MessageKeyProvider; +import com.onarandombox.MultiverseCore.utils.MVCorei18n; +import com.onarandombox.MultiverseCore.utils.checkresult.FailureReason; +import com.onarandombox.MultiverseCore.utils.checkresult.SuccessReason; + +public class PlayerLimitResult { + public enum Success implements SuccessReason { + NO_PLAYERLIMIT, + WITHIN_PLAYERLIMIT, + BYPASS_PLAYERLIMIT + } + + public enum Failure implements FailureReason { + EXCEED_PLAYERLIMIT(MVCorei18n.ENTRYCHECK_EXCEEDPLAYERLIMIT); + + private final MessageKeyProvider message; + + Failure(MessageKeyProvider message) { + this.message = message; + } + + @Override + public MessageKey getMessageKey() { + return message.getMessageKey(); + } + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/WorldAccessResult.java b/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/WorldAccessResult.java new file mode 100644 index 00000000..5c24e626 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/WorldAccessResult.java @@ -0,0 +1,29 @@ +package com.onarandombox.MultiverseCore.world.entrycheck; + +import co.aikar.locales.MessageKey; +import co.aikar.locales.MessageKeyProvider; +import com.onarandombox.MultiverseCore.utils.MVCorei18n; +import com.onarandombox.MultiverseCore.utils.checkresult.FailureReason; +import com.onarandombox.MultiverseCore.utils.checkresult.SuccessReason; + +public class WorldAccessResult { + public enum Success implements SuccessReason { + NO_ENFORCE_WORLD_ACCESS, + HAS_WORLD_ACCESS + } + + public enum Failure implements FailureReason { + NO_WORLD_ACCESS(MVCorei18n.ENTRYCHECK_NOWORLDACCESS); + + private final MessageKeyProvider message; + + Failure(MessageKeyProvider message) { + this.message = message; + } + + @Override + public MessageKey getMessageKey() { + return message.getMessageKey(); + } + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/WorldEntryChecker.java b/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/WorldEntryChecker.java new file mode 100644 index 00000000..abd57d7d --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/WorldEntryChecker.java @@ -0,0 +1,103 @@ +package com.onarandombox.MultiverseCore.world.entrycheck; + +import com.onarandombox.MultiverseCore.api.MVWorld; +import com.onarandombox.MultiverseCore.config.MVCoreConfig; +import com.onarandombox.MultiverseCore.economy.MVEconomist; +import com.onarandombox.MultiverseCore.permissions.CorePermissionsChecker; +import com.onarandombox.MultiverseCore.utils.checkresult.CheckResult; +import com.onarandombox.MultiverseCore.utils.checkresult.CheckResultChain; +import com.onarandombox.MultiverseCore.utils.message.MessageReplacement; +import com.onarandombox.MultiverseCore.world.configuration.EntryFee; +import org.bukkit.Material; +import org.bukkit.command.BlockCommandSender; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.onarandombox.MultiverseCore.utils.message.MessageReplacement.replace; + +public class WorldEntryChecker { + private final @NotNull MVCoreConfig config; + private final @NotNull MVEconomist economist; + private final @NotNull CorePermissionsChecker permissionsChecker; + + private final @NotNull CommandSender sender; + + public WorldEntryChecker( + @NotNull MVCoreConfig config, + @NotNull CorePermissionsChecker permissionsChecker, + @NotNull MVEconomist economist, + @NotNull CommandSender sender + ) { + this.config = config; + this.permissionsChecker = permissionsChecker; + this.economist = economist; + this.sender = sender; + } + + public CheckResultChain canStayInWorld(@NotNull MVWorld world) { + return canEnterWorld(null, world); + } + + public CheckResultChain canEnterWorld(@Nullable MVWorld fromWorld, @NotNull MVWorld toWorld) { + return CheckResultChain.builder() + .then(() -> canAccessWorld(toWorld)) + .then(() -> isWithinPlayerLimit(toWorld)) + .then(() -> isNotBlacklisted(fromWorld, toWorld)) + .then(() -> canPayEntryFee(toWorld)) + .build(); + } + + public CheckResult canAccessWorld(@NotNull MVWorld world) { + if (!config.getEnforceAccess()) { + return CheckResult.success(WorldAccessResult.Success.NO_ENFORCE_WORLD_ACCESS); + } + return permissionsChecker.hasWorldAccessPermission(this.sender, world) + ? CheckResult.success(WorldAccessResult.Success.HAS_WORLD_ACCESS) + : CheckResult.failure(WorldAccessResult.Failure.NO_WORLD_ACCESS); + } + + public CheckResult isWithinPlayerLimit(@NotNull MVWorld world) { + final int playerLimit = world.getPlayerLimit(); + if (playerLimit <= -1) { + return CheckResult.success(PlayerLimitResult.Success.NO_PLAYERLIMIT); + } + if (permissionsChecker.hasPlayerLimitBypassPermission(sender, world)) { + return CheckResult.success(PlayerLimitResult.Success.BYPASS_PLAYERLIMIT); + } + return playerLimit > world.getCBWorld().getPlayers().size() + ? CheckResult.success(PlayerLimitResult.Success.WITHIN_PLAYERLIMIT) + : CheckResult.failure(PlayerLimitResult.Failure.EXCEED_PLAYERLIMIT); + } + + public CheckResult isNotBlacklisted(@Nullable MVWorld fromWorld, @NotNull MVWorld toWorld) { + if (fromWorld == null) { + return CheckResult.success(BlacklistResult.Success.UNKNOWN_FROM_WORLD); + } + return toWorld.getWorldBlacklist().contains(fromWorld.getName()) + ? CheckResult.failure(BlacklistResult.Failure.BLACKLISTED, replace("{world}").with(fromWorld.getAlias())) + : CheckResult.success(BlacklistResult.Success.NOT_BLACKLISTED); + } + + public CheckResult canPayEntryFee(MVWorld world) { + double price = world.getPrice(); + Material currency = world.getCurrency(); + if (price == 0D && (currency == null || currency == EntryFee.DISABLED_MATERIAL)) { + return CheckResult.success(EntryFeeResult.Success.FREE_ENTRY); + } + if (sender instanceof ConsoleCommandSender || sender instanceof BlockCommandSender) { + return CheckResult.success(EntryFeeResult.Success.CONSOLE_OR_BLOCK_COMMAND_SENDER); + } + if (permissionsChecker.hasWorldExemptPermission(sender, world)) { + return CheckResult.success(EntryFeeResult.Success.EXEMPT_FROM_ENTRY_FEE); + } + if (!(sender instanceof Player player)) { + return CheckResult.failure(EntryFeeResult.Failure.CANNOT_PAY_ENTRY_FEE); + } + return economist.isPlayerWealthyEnough(player, price, currency) + ? CheckResult.success(EntryFeeResult.Success.ENOUGH_MONEY) + : CheckResult.failure(EntryFeeResult.Failure.NOT_ENOUGH_MONEY, replace("{amount}").with("$##")); //TODO + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/WorldEntryCheckerProvider.java b/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/WorldEntryCheckerProvider.java new file mode 100644 index 00000000..33be36e4 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/world/entrycheck/WorldEntryCheckerProvider.java @@ -0,0 +1,32 @@ +package com.onarandombox.MultiverseCore.world.entrycheck; + +import com.onarandombox.MultiverseCore.config.MVCoreConfig; +import com.onarandombox.MultiverseCore.economy.MVEconomist; +import com.onarandombox.MultiverseCore.permissions.CorePermissionsChecker; +import jakarta.inject.Inject; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jvnet.hk2.annotations.Service; + +@Service +public class WorldEntryCheckerProvider { + + private final @NotNull MVCoreConfig config; + private final @NotNull MVEconomist economist; + private final @NotNull CorePermissionsChecker permissionsChecker; + + @Inject + WorldEntryCheckerProvider( + @NotNull MVCoreConfig config, + @NotNull MVEconomist economist, + @NotNull CorePermissionsChecker permissionsChecker + ) { + this.config = config; + this.economist = economist; + this.permissionsChecker = permissionsChecker; + } + + public @NotNull WorldEntryChecker forSender(@NotNull CommandSender sender) { + return new WorldEntryChecker(config, permissionsChecker, economist, sender); + } +} diff --git a/src/main/resources/multiverse-core_en.properties b/src/main/resources/multiverse-core_en.properties index 31ed2ea7..b97ca106 100644 --- a/src/main/resources/multiverse-core_en.properties +++ b/src/main/resources/multiverse-core_en.properties @@ -114,3 +114,14 @@ mv-core.unload.success=&aUnloaded world '{world}'! # /mv usage mv-core.usage.description=Show Multiverse-Core command usage. + +# entry check +mv-core.entrycheck.blacklisted='{world}' is blacklisted. +mv-core.entrycheck.notenoughmoney=you do not have enough money to pay entry fee. You are required to pay {amount}. +mv-core.entrycheck.cannotpayentryfee=you do not have the ability to pay entry fee. +mv-core.entrycheck.exceedplayerlimit=the world has reached its player limit. +mv-core.entrycheck.noworldaccess=you do not have permissions to access the world. + +# generic +mv-core.generic.success=Success! +mv-core.generic.failure=Failed!