Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -55,16 +56,20 @@ public void register(@NotNull DynamicListener listener, @NotNull Plugin plugin)
Set<Method> listenerMethods = new HashSet<>();
listenerMethods.addAll(List.of(listener.getClass().getMethods()));
listenerMethods.addAll(List.of(listener.getClass().getDeclaredMethods()));
listenerMethods.forEach(method -> Try.run(() ->{
if (method.isAnnotationPresent(EventMethod.class)) {
registerAsEventMethod(listener, plugin, method);
} else if (method.isAnnotationPresent(EventClass.class)) {
registerAsEventClass(listener, plugin, method);
}
}).onFailure(e -> {
Logging.severe("Failed to register event method %s in %s", method.getName(), listener.getClass().getName());
e.printStackTrace();
}));
// Keep fixed MONITOR handlers after validation if configured priorities become equal.
listenerMethods.stream()
.sorted(Comparator.comparing(this::getDefaultEventPriority))
.forEach(method -> Try.run(() -> {
if (method.isAnnotationPresent(EventMethod.class)) {
registerAsEventMethod(listener, plugin, method);
} else if (method.isAnnotationPresent(EventClass.class)) {
registerAsEventClass(listener, plugin, method);
}
}).onFailure(e -> {
Logging.severe("Failed to register event method %s in %s",
method.getName(), listener.getClass().getName());
e.printStackTrace();
}));
}

private void registerAsEventMethod(DynamicListener listener, Plugin plugin, Method method) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

package org.mvplugins.multiverse.core.listeners;

import java.util.Map;
import java.util.Objects;
import java.util.WeakHashMap;

import com.dumptruckman.minecraft.util.Logging;
import io.vavr.control.Option;
Expand Down Expand Up @@ -72,6 +74,7 @@ final class MVPlayerListener implements CoreListener {
private final DimensionFinder dimensionFinder;
private final CorePermissionsChecker corePermissionsChecker;
private final AsyncSafetyTeleporter asyncSafetyTeleporter;
private final Map<PlayerTeleportEvent, EntryFeePayment> pendingEntryFees = new WeakHashMap<>();

@Inject
MVPlayerListener(
Expand Down Expand Up @@ -333,7 +336,8 @@ void playerTeleport(PlayerTeleportEvent event) {
ResultChain entryResult = worldEntryCheckerProvider.forSender(finalTeleporter).canEnterWorld(fromWorld, toWorld)
.onSuccessReason(EntryFeeResult.Success.class, reason -> {
if (reason == EntryFeeResult.Success.ENOUGH_MONEY) {
economist.payEntryFee((Player) finalTeleporter, toWorld);
pendingEntryFees.put(event, new EntryFeePayment((Player) finalTeleporter,
toLocation.getWorld(), toWorld.getPrice(), toWorld.getCurrency()));
}
})
.onFailure(results -> {
Expand All @@ -344,6 +348,17 @@ void playerTeleport(PlayerTeleportEvent event) {
Logging.fine("Teleport result: %s", entryResult);
}

@EventMethod
@DefaultEventPriority(EventPriority.MONITOR)
void collectEntryFee(PlayerTeleportEvent event) {
// Remove cancelled payments too, without retaining the player or world after the event.
EntryFeePayment payment = pendingEntryFees.remove(event);
if (payment != null && !event.isCancelled() && event.getTo() != null
&& payment.destination().equals(event.getTo().getWorld())) {
economist.payEntryFee(payment.payer(), payment.price(), payment.currency());
}
}

private void stateSuccess(String playerName, String worldName) {
Logging.fine("MV-Core is allowing Player '" + playerName
+ "' to go to '" + worldName + "'.");
Expand Down Expand Up @@ -448,4 +463,7 @@ private void doGameModeAndFlightEnforcement(Player player, World world) {
enforcementHandler.handleFlightEnforcement(player);
enforcementHandler.handleGameModeEnforcement(player);
}

private record EntryFeePayment(Player payer, World destination, double price, Material currency) {
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
package org.mvplugins.multiverse.core.config

import org.bukkit.Material
import org.bukkit.event.EventPriority
import org.bukkit.event.Listener
import org.bukkit.event.HandlerList
import org.mvplugins.multiverse.core.dynamiclistener.DynamicListener
import org.mvplugins.multiverse.core.dynamiclistener.DynamicListenerRegistration
import org.mvplugins.multiverse.core.dynamiclistener.EventPriorityMapper
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod
import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventPriorityKey
import org.mvplugins.multiverse.core.dynamiclistener.annotations.DefaultEventPriority
import org.bukkit.event.player.PlayerTeleportEvent
import org.bukkit.inventory.ItemStack
import org.mvplugins.multiverse.core.teleportation.TeleportQueue
import org.bukkit.Location
import org.mockbukkit.mockbukkit.entity.PlayerMock
import org.mvplugins.multiverse.core.TestWithMockBukkit
Expand Down Expand Up @@ -54,4 +67,202 @@ class ConfigTeleportInterceptTest : TestWithMockBukkit() {
.onSuccess (Runnable { throw AssertionError("Teleport should have failed") })
.onFailure(Runnable { assertEquals("world", player.world.name) })
}

private fun configureEntryFee(price: Double = 2.0) {
config.teleportIntercept = true
config.enforceAccess = false
val worldManager = serviceLocator.getActiveService(WorldManager::class.java)!!
val world = worldManager.getLoadedWorld("world2").get()
world.setEntryFeeEnabled(true).get()
world.setCurrency(Material.DIAMOND).get()
world.setPrice(price).get()
player.inventory.setItem(0, ItemStack(Material.DIAMOND, 2))
}

private fun onTeleport(priority: EventPriority = EventPriority.HIGHEST, action: (PlayerTeleportEvent) -> Unit) {
server.pluginManager.registerEvent(PlayerTeleportEvent::class.java, object : Listener {}, priority,
{ _, event -> action(event as PlayerTeleportEvent) }, multiverseCore)
}

private fun diamonds(payer: PlayerMock = player): Int =
payer.inventory.all(Material.DIAMOND).values.sumOf { it.amount }

@Test
fun `Late cancellation does not charge an entry fee`() {
configureEntryFee()
onTeleport { it.isCancelled = true }
assertFalse(player.teleport(location))
assertEquals("world", player.world.name)
assertEquals(2, diamonds())
}

@Test
fun `Successful teleport charges exactly once`() {
configureEntryFee()
var balanceBeforeMonitor = -1
onTeleport { balanceBeforeMonitor = diamonds() }
assertTrue(player.teleport(location))
assertEquals(2, balanceBeforeMonitor)
assertEquals("world2", player.world.name)
assertEquals(0, diamonds())
}

@Test
fun `Pre-cancelled teleport does not charge`() {
configureEntryFee()
onTeleport(EventPriority.LOWEST) { it.isCancelled = true }
assertFalse(player.teleport(location))
assertEquals(2, diamonds())
}

@Test
fun `Insufficient funds still cancels before monitor`() {
configureEntryFee()
player.inventory.setItem(0, ItemStack(Material.DIAMOND, 1))
assertFalse(player.teleport(location))
assertEquals("world", player.world.name)
assertEquals(1, diamonds())
}

@Test
fun `Same-world teleport remains free`() {
configureEntryFee()
assertTrue(player.teleport(Location(player.world, 5.0, 5.0, 5.0)))
assertEquals(2, diamonds())
}

@Test
fun `Late cancellation does not grant a negative entry fee`() {
configureEntryFee(-2.0)
onTeleport { it.isCancelled = true }
assertFalse(player.teleport(location))
assertEquals(2, diamonds())
}

@Test
fun `Successful teleport retains negative entry fee rewards`() {
configureEntryFee(-2.0)
assertTrue(player.teleport(location))
assertEquals(4, diamonds())
}

@Test
fun `Queued teleporter pays instead of the teleported player`() {
configureEntryFee()
val payer = server.addPlayer()
payer.inventory.setItem(0, ItemStack(Material.DIAMOND, 2))
serviceLocator.getActiveService(TeleportQueue::class.java)!!.addToQueue(payer, player)
assertTrue(player.teleport(location))
assertEquals(0, diamonds(payer))
assertEquals(2, diamonds())
}

@Test
fun `Redirecting the destination does not charge for the original world`() {
configureEntryFee()
onTeleport { it.to = Location(player.world, 5.0, 5.0, 5.0) }
assertTrue(player.teleport(location))
assertEquals("world", player.world.name)
assertEquals(2, diamonds())
}

@Test
fun `World access denial remains enforced before payment`() {
configureEntryFee()
config.enforceAccess = true
assertFalse(player.teleport(location))
assertEquals("world", player.world.name)
assertEquals(2, diamonds())
}

@Test
fun `Cancelled payment does not leak into the next teleport`() {
configureEntryFee()
var cancel = true
onTeleport { it.isCancelled = cancel }
assertFalse(player.teleport(location))
assertEquals(2, diamonds())
cancel = false
assertTrue(player.teleport(location))
assertEquals(0, diamonds())
}

private fun setPermissionCheckPriority(priority: EventPriority) {
val listener = PlayerTeleportEvent.getHandlerList().registeredListeners
.first { it.listener.javaClass.simpleName == "MVPlayerListener" }.listener as DynamicListener
HandlerList.unregisterAll(listener)
serviceLocator.getActiveService(EventPriorityMapper::class.java)!!
.setPriority("mvcore-player-teleport", priority)
serviceLocator.getActiveService(DynamicListenerRegistration::class.java)!!.register(listener, multiverseCore)
}

@Test
fun `Custom permission-check priority still defers payment until monitor`() {
configureEntryFee()
setPermissionCheckPriority(EventPriority.LOW)
onTeleport(EventPriority.NORMAL) { it.isCancelled = true }
assertFalse(player.teleport(location))
assertEquals(2, diamonds())
}

@Test
fun `Every configurable permission-check priority collects exactly one fee`() {
configureEntryFee()
var cancel = true
onTeleport { it.isCancelled = cancel }
for (priority in EventPriority.entries) {
setPermissionCheckPriority(priority)
player.inventory.setItem(0, ItemStack(Material.DIAMOND, 2))
cancel = true
assertFalse(player.teleport(location), priority.name)
assertEquals(2, diamonds(), priority.name)
cancel = false
assertTrue(player.teleport(location), priority.name)
assertEquals(0, diamonds(), priority.name)
assertTrue(player.teleport(Location(server.getWorld("world"), 0.0, 5.0, 0.0)))
}
}

class OrderedListener : DynamicListener {
val calls = mutableListOf<String>()

@EventMethod
@EventPriorityKey("test-fee-validation")
@DefaultEventPriority(EventPriority.HIGHEST)
fun validate(event: PlayerTeleportEvent) {
calls.add("validate")
}

@EventMethod
@DefaultEventPriority(EventPriority.MONITOR)
fun collect(event: PlayerTeleportEvent) {
calls.add("collect")
}
}

@Test
fun `Default monitor handler remains after validation when configured priorities tie`() {
val listener = OrderedListener()
serviceLocator.getActiveService(EventPriorityMapper::class.java)!!
.setPriority("test-fee-validation", EventPriority.MONITOR)
serviceLocator.getActiveService(DynamicListenerRegistration::class.java)!!.register(listener, multiverseCore)
server.pluginManager.callEvent(PlayerTeleportEvent(player, player.location, location))
assertEquals(listOf("validate", "collect"), listener.calls)
}

@Test
fun `Fee exemption remains free`() {
configureEntryFee()
player.addAttachment(multiverseCore, "multiverse.exempt.world2", true)
assertTrue(player.teleport(location))
assertEquals(2, diamonds())
}

@Test
fun `Console initiated teleport remains free`() {
configureEntryFee()
serviceLocator.getActiveService(TeleportQueue::class.java)!!.addToQueue(server.consoleSender, player)
assertTrue(player.teleport(location))
assertEquals(2, diamonds())
}
}