diff --git a/src/main/java/org/cubexmc/metro/Metro.java b/src/main/java/org/cubexmc/metro/Metro.java index 55c7c3a..e666744 100644 --- a/src/main/java/org/cubexmc/metro/Metro.java +++ b/src/main/java/org/cubexmc/metro/Metro.java @@ -123,7 +123,7 @@ protected void enablePlugin() throws Exception { getLogger().info("Vault economy not found or disabled."); } this.ticketService = new org.cubexmc.metro.service.TicketService(this::getVaultIntegration, - () -> getConfig().getBoolean("economy.enabled", true)); + () -> configFacade.isEconomyEnabled()); this.priceService = new org.cubexmc.metro.service.PriceService(); this.lineStatusService = new org.cubexmc.metro.service.LineStatusService(this, lineManager); diff --git a/src/main/java/org/cubexmc/metro/command/newcmd/MetroMainCommand.java b/src/main/java/org/cubexmc/metro/command/newcmd/MetroMainCommand.java index 383ae6f..05f0a4e 100644 --- a/src/main/java/org/cubexmc/metro/command/newcmd/MetroMainCommand.java +++ b/src/main/java/org/cubexmc/metro/command/newcmd/MetroMainCommand.java @@ -89,6 +89,9 @@ public void reload(CommandSender sender) { plugin.getPortalManager().load(); } plugin.getLanguageManager().loadLanguages(); + if (plugin.getVaultIntegration() != null) { + plugin.getVaultIntegration().refreshProvider(); + } plugin.refreshMapIntegrations(); diff --git a/src/main/java/org/cubexmc/metro/gui/GuiListener.java b/src/main/java/org/cubexmc/metro/gui/GuiListener.java index d2723b3..7cfd3d3 100644 --- a/src/main/java/org/cubexmc/metro/gui/GuiListener.java +++ b/src/main/java/org/cubexmc/metro/gui/GuiListener.java @@ -46,10 +46,10 @@ public GuiListener(Metro plugin) { @EventHandler public void onInventoryClick(InventoryClickEvent event) { - Inventory inv = event.getInventory(); + Inventory topInv = event.getView().getTopInventory(); // 检查是否是我们的 GUI - if (!(inv.getHolder() instanceof GuiHolder holder)) { + if (!(topInv.getHolder() instanceof GuiHolder holder)) { return; } @@ -64,7 +64,7 @@ public void onInventoryClick(InventoryClickEvent event) { int slot = event.getRawSlot(); // 忽略点击 GUI 外部 - if (slot < 0 || slot >= inv.getSize()) { + if (slot < 0 || slot >= topInv.getSize()) { return; } @@ -94,9 +94,14 @@ public void onInventoryClick(InventoryClickEvent event) { @EventHandler public void onInventoryDrag(InventoryDragEvent event) { - // 防止在 GUI 中拖拽物品 - if (event.getInventory().getHolder() instanceof GuiHolder) { - event.setCancelled(true); + if (event.getView().getTopInventory().getHolder() instanceof GuiHolder) { + int topSize = event.getView().getTopInventory().getSize(); + for (int slot : event.getRawSlots()) { + if (slot < topSize) { + event.setCancelled(true); + return; + } + } } } } diff --git a/src/main/java/org/cubexmc/metro/integration/VaultIntegration.kt b/src/main/java/org/cubexmc/metro/integration/VaultIntegration.kt index 0173592..fbc0eec 100644 --- a/src/main/java/org/cubexmc/metro/integration/VaultIntegration.kt +++ b/src/main/java/org/cubexmc/metro/integration/VaultIntegration.kt @@ -4,21 +4,47 @@ import java.util.UUID import net.milkbowl.vault.economy.Economy import org.bukkit.Bukkit import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.plugin.ServiceRegisterEvent +import org.bukkit.plugin.ServiceUnregisterEvent import org.cubexmc.metro.Metro -class VaultIntegration(private val plugin: Metro) { +class VaultIntegration(private val plugin: Metro) : Listener { var economy: Economy? = null private set - var isEnabled: Boolean = setupEconomy() + var isEnabled: Boolean = false private set - private fun setupEconomy(): Boolean { - if (plugin.server.pluginManager.getPlugin("Vault") == null) { - return false + init { + refreshProvider() + plugin.server.pluginManager.registerEvents(this, plugin) + } + + /** + * Re-queries ServicesManager for the current Economy provider. + * Safe to call at any time; does not require the "Vault" plugin to be loaded. + */ + fun refreshProvider() { + val registration = plugin.server.servicesManager.getRegistration(Economy::class.java) + economy = registration?.provider + isEnabled = economy != null + } + + @EventHandler(priority = EventPriority.MONITOR) + fun onServiceRegister(event: ServiceRegisterEvent) { + if (event.provider is Economy) { + refreshProvider() + } + } + + @EventHandler(priority = EventPriority.MONITOR) + fun onServiceUnregister(event: ServiceUnregisterEvent) { + if (event.provider is Economy) { + economy = null + isEnabled = false } - val registration = plugin.server.servicesManager.getRegistration(Economy::class.java) ?: return false - economy = registration.provider - return economy != null } fun has(player: Player, amount: Double): Boolean { diff --git a/src/main/java/org/cubexmc/metro/listener/VehicleListener.java b/src/main/java/org/cubexmc/metro/listener/VehicleListener.java index e1d3a8c..b9240f4 100644 --- a/src/main/java/org/cubexmc/metro/listener/VehicleListener.java +++ b/src/main/java/org/cubexmc/metro/listener/VehicleListener.java @@ -61,12 +61,19 @@ public void onVehicleExit(VehicleExitEvent event) { return; } - // 玩家下车,清除其界面显示 - plugin.getScoreboardManager().clearPlayerDisplay(player); - // 获取当前位置 Location location = minecart.getLocation(); + // 检查位置是否在停靠区上,如果不在则禁止下车 + if (!isAtStop(location)) { + event.setCancelled(true); + player.sendMessage(plugin.getLanguageManager().getMessage("vehicle.cannot_dismount")); + return; + } + + // 玩家下车,清除其界面显示 + plugin.getScoreboardManager().clearPlayerDisplay(player); + // 检查位置是否在停靠区上 if (!isAtStop(location)) { // 如果不在停靠区上,立即移除矿车 diff --git a/src/main/java/org/cubexmc/metro/service/TicketService.kt b/src/main/java/org/cubexmc/metro/service/TicketService.kt index b598b88..7a7a9f8 100644 --- a/src/main/java/org/cubexmc/metro/service/TicketService.kt +++ b/src/main/java/org/cubexmc/metro/service/TicketService.kt @@ -190,7 +190,11 @@ class TicketService( transaction.markCharged() val owner: UUID? = transaction.line.owner if (owner != null) { - vault.deposit(owner, transaction.price) + if (!vault.deposit(owner, transaction.price)) { + // Owner deposit failed: refund the passenger and report failure + vault.deposit(transaction.player.uniqueId, transaction.price) + return TicketChargeStatus.TRANSACTION_FAILED + } } return TicketChargeStatus.CHARGED } @@ -261,7 +265,11 @@ class TicketService( } val owner = line.owner if (owner != null) { - vault.deposit(owner, priceToCharge) + if (!vault.deposit(owner, priceToCharge)) { + // Owner deposit failed: refund the passenger and report failure + vault.deposit(player.uniqueId, priceToCharge) + return TicketChargeStatus.TRANSACTION_FAILED + } } return TicketChargeStatus.CHARGED } diff --git a/src/main/java/org/cubexmc/metro/train/TrainMovementTask.kt b/src/main/java/org/cubexmc/metro/train/TrainMovementTask.kt index 16a01c7..7c3ed73 100644 --- a/src/main/java/org/cubexmc/metro/train/TrainMovementTask.kt +++ b/src/main/java/org/cubexmc/metro/train/TrainMovementTask.kt @@ -199,6 +199,9 @@ class TrainMovementTask @JvmOverloads constructor( movementAssistController.stop() session.plugin.bedrockCompatibility.onTrainArrival(session.passenger, minecart) + // 先结算里程费用,确保即使 stopPointLocation 为 null 也能正常扣费 + settleDistanceFare(stop) + val baseLocation = stop.stopPointLocation ?: return val snapLocation = baseLocation.clone() snapLocation.x = snapLocation.blockX + 0.5 @@ -208,8 +211,6 @@ class TrainMovementTask @JvmOverloads constructor( session.plugin.routeRecorder.sample(line.id, minecart, snapLocation) } - settleDistanceFare(stop) - val previousState = stateMachine.transitionTo(TrainState.STOPPED_AT_STATION, null) if (previousState == TrainState.MOVING_IN_STATION) { handleArrivalAtStation() @@ -231,7 +232,7 @@ class TrainMovementTask @JvmOverloads constructor( return } - val variablePrice = when (rule.getMode()) { + val rawVariablePrice = when (rule.getMode()) { PriceRule.PricingMode.DISTANCE -> distance * rule.getPerBlockRate() PriceRule.PricingMode.INTERVAL -> { val intervals = session.plugin.priceService.countStopIntervals(line, session.entryStopId, stop.id) @@ -241,16 +242,39 @@ class TrainMovementTask @JvmOverloads constructor( else -> return } - if (variablePrice > 0) { - val status = session.plugin.ticketService.chargePrice(passenger, line, variablePrice) + // 应用 maxPrice 上限:整个行程的总费用不得超过 maxPrice + val variablePrice = if (rawVariablePrice > 0) { + val maxPrice = rule.getMaxPrice() + if (maxPrice > 0.0) { + val remaining = maxPrice - rule.getBasePrice() - session.totalVariableCharged + if (remaining <= 0) 0.0 else minOf(rawVariablePrice, remaining) + } else { + rawVariablePrice + } + } else { + 0.0 + } + + // 应用时间折扣 + val gameTime = passenger.world?.time ?: 6000L + val discount = rule.getActiveDiscountMultiplier(gameTime) + val finalPrice = if (variablePrice > 0 && discount < 1.0) { + variablePrice * discount + } else { + variablePrice + } + + if (finalPrice > 0) { + val status = session.plugin.ticketService.chargePrice(passenger, line, finalPrice) if (status == TicketService.TicketChargeStatus.CHARGED) { + session.totalVariableCharged += variablePrice // 按上限前价格记录,确保 maxPrice 正确生效 passenger.sendMessage( session.plugin.languageManager.getMessage( "economy.paid_distance", LanguageManager.put( LanguageManager.args(), "price", - session.plugin.ticketService.format(variablePrice), + session.plugin.ticketService.format(finalPrice), ), ), ) @@ -258,6 +282,9 @@ class TrainMovementTask @JvmOverloads constructor( } session.addDistance(-distance) + + // 更新 entryStopId 为当前站,确保 INTERVAL 模式下下一段区间从当前站开始计算 + session.entryStopId = stop.id } private fun transitionToMovingInStation(targetStop: Stop) { diff --git a/src/main/java/org/cubexmc/metro/train/TrainSession.kt b/src/main/java/org/cubexmc/metro/train/TrainSession.kt index 313693b..92d0564 100644 --- a/src/main/java/org/cubexmc/metro/train/TrainSession.kt +++ b/src/main/java/org/cubexmc/metro/train/TrainSession.kt @@ -26,6 +26,8 @@ class TrainSession( var entryStopId: String? = currentStopId var distanceTraveled: Double = 0.0 private set + /** 整个行程中已收取的可变费用总额(不含 basePrice),用于 maxPrice 上限检查 */ + var totalVariableCharged: Double = 0.0 fun addDistance(blocks: Double): Double { distanceTraveled += blocks diff --git a/src/main/resources/lang/de_DE.yml b/src/main/resources/lang/de_DE.yml index 9c349e6..9c61dfd 100644 --- a/src/main/resources/lang/de_DE.yml +++ b/src/main/resources/lang/de_DE.yml @@ -571,7 +571,8 @@ interact: vehicle: terminal_arrived: "Endstation erreicht: " auto_depart: "Automatische Abfahrt: von nach " - passenger_exit: "Spieler ist ausgestiegen, fährt nicht automatisch ab" + passenger_exit: "Spieler ist ausgestiegen, fährt nicht automatisch ab" + cannot_dismount: "Du kannst nicht aussteigen, während der Zug fährt!" chat: enter_new_name: "Bitte neuen Namen eingeben. Tippe 'cancel' zum Abbrechen." diff --git a/src/main/resources/lang/en_US.yml b/src/main/resources/lang/en_US.yml index 3e146b6..b93237e 100644 --- a/src/main/resources/lang/en_US.yml +++ b/src/main/resources/lang/en_US.yml @@ -573,6 +573,7 @@ vehicle: terminal_arrived: "Arrived at terminal station: " auto_depart: "Auto departure: From to " passenger_exit: "Player has alighted, will not auto depart" + cannot_dismount: "You cannot dismount while the train is moving!" chat: enter_new_name: "Please enter the new name. Type 'cancel' to cancel." diff --git a/src/main/resources/lang/es_ES.yml b/src/main/resources/lang/es_ES.yml index 6bc2ee8..00f1b05 100644 --- a/src/main/resources/lang/es_ES.yml +++ b/src/main/resources/lang/es_ES.yml @@ -572,7 +572,8 @@ interact: vehicle: terminal_arrived: "Estación terminal alcanzada: " auto_depart: "Salida automática: de a " - passenger_exit: "El jugador se ha bajado, no saldrá automáticamente" + passenger_exit: "El jugador se ha bajado, no saldrá automáticamente" + cannot_dismount: "¡No puedes bajarte mientras el tren está en movimiento!" chat: enter_new_name: "Por favor ingresa el nuevo nombre. Escribe 'cancel' para abortar." diff --git a/src/main/resources/lang/nl_NL.yml b/src/main/resources/lang/nl_NL.yml index 3eab7b1..6b29f19 100644 --- a/src/main/resources/lang/nl_NL.yml +++ b/src/main/resources/lang/nl_NL.yml @@ -572,7 +572,8 @@ interact: vehicle: terminal_arrived: "Eindstation bereikt: " auto_depart: "Automatisch vertrek: van naar " - passenger_exit: "Speler is uitgestapt, vertrekt niet automatisch" + passenger_exit: "Speler is uitgestapt, vertrekt niet automatisch" + cannot_dismount: "Je kunt niet uitstappen terwijl de trein rijdt!" chat: enter_new_name: "Voer een nieuwe naam in. Typ 'cancel' om te annuleren." diff --git a/src/main/resources/lang/tr_TR.yml b/src/main/resources/lang/tr_TR.yml index 213ec11..73becf9 100644 --- a/src/main/resources/lang/tr_TR.yml +++ b/src/main/resources/lang/tr_TR.yml @@ -573,6 +573,7 @@ vehicle: terminal_arrived: "Son durağa varıldı: " auto_depart: "Otomatik kalkış: durağından durağına" passenger_exit: "Oyuncu indi, otomatik kalkış yapılmayacak" + cannot_dismount: "Tren hareket halindeyken inemezsiniz!" chat: enter_new_name: "Lütfen yeni adı girin. İptal etmek için 'cancel' yazın." diff --git a/src/main/resources/lang/zh_CN.yml b/src/main/resources/lang/zh_CN.yml index 1d5baf3..ea76f5d 100644 --- a/src/main/resources/lang/zh_CN.yml +++ b/src/main/resources/lang/zh_CN.yml @@ -439,7 +439,8 @@ interact: vehicle: terminal_arrived: "已到达终点站: " auto_depart: "自动发车: 从 前往 " - passenger_exit: "玩家已下车,不会自动发车" + passenger_exit: "玩家已下车,不会自动发车" + cannot_dismount: "列车行驶中,禁止下车!" ownership: server: "服务器" diff --git a/src/main/resources/lang/zh_TW.yml b/src/main/resources/lang/zh_TW.yml index 26ac63e..6c5277b 100644 --- a/src/main/resources/lang/zh_TW.yml +++ b/src/main/resources/lang/zh_TW.yml @@ -440,6 +440,7 @@ vehicle: terminal_arrived: "已抵達終點站: " auto_depart: "自動發車: 從 前往 " passenger_exit: "玩家已下車,將不會自動發車" + cannot_dismount: "列車行駛中,禁止下車!" ownership: server: "伺服器" diff --git a/src/test/java/org/cubexmc/metro/gui/GuiListenerTest.java b/src/test/java/org/cubexmc/metro/gui/GuiListenerTest.java index 4bf2f8c..ab209a4 100644 --- a/src/test/java/org/cubexmc/metro/gui/GuiListenerTest.java +++ b/src/test/java/org/cubexmc/metro/gui/GuiListenerTest.java @@ -5,23 +5,32 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Set; + import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryDragEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.InventoryView; import org.cubexmc.metro.Metro; import org.junit.jupiter.api.Test; class GuiListenerTest { + private InventoryView viewWithTop(Inventory topInventory) { + InventoryView view = mock(InventoryView.class); + when(view.getTopInventory()).thenReturn(topInventory); + return view; + } + @Test void shouldIgnoreInventoryClicksOutsideMetroGui() { GuiListener listener = new GuiListener(mock(Metro.class)); InventoryClickEvent event = mock(InventoryClickEvent.class); - Inventory inventory = mock(Inventory.class); + Inventory topInventory = mock(Inventory.class); - when(event.getInventory()).thenReturn(inventory); - when(inventory.getHolder()).thenReturn(mock(InventoryHolder.class)); + when(event.getView()).thenReturn(viewWithTop(topInventory)); + when(topInventory.getHolder()).thenReturn(mock(InventoryHolder.class)); listener.onInventoryClick(event); @@ -32,12 +41,12 @@ void shouldIgnoreInventoryClicksOutsideMetroGui() { void shouldCancelMetroGuiClicksBeforeIgnoringOutsideSlots() { GuiListener listener = new GuiListener(mock(Metro.class)); InventoryClickEvent event = mock(InventoryClickEvent.class); - Inventory inventory = mock(Inventory.class); + Inventory topInventory = mock(Inventory.class); GuiHolder holder = new GuiHolder(GuiHolder.GuiType.MAIN_MENU); - when(event.getInventory()).thenReturn(inventory); - when(inventory.getHolder()).thenReturn(holder); - when(inventory.getSize()).thenReturn(9); + when(event.getView()).thenReturn(viewWithTop(topInventory)); + when(topInventory.getHolder()).thenReturn(holder); + when(topInventory.getSize()).thenReturn(9); when(event.getRawSlot()).thenReturn(99); listener.onInventoryClick(event); @@ -46,27 +55,45 @@ void shouldCancelMetroGuiClicksBeforeIgnoringOutsideSlots() { } @Test - void shouldCancelDraggingInsideMetroGui() { + void shouldCancelDragWhenSlotsAffectTopInventory() { GuiListener listener = new GuiListener(mock(Metro.class)); InventoryDragEvent event = mock(InventoryDragEvent.class); - Inventory inventory = mock(Inventory.class); + Inventory topInventory = mock(Inventory.class); - when(event.getInventory()).thenReturn(inventory); - when(inventory.getHolder()).thenReturn(new GuiHolder(GuiHolder.GuiType.LINE_LIST)); + when(event.getView()).thenReturn(viewWithTop(topInventory)); + when(topInventory.getHolder()).thenReturn(new GuiHolder(GuiHolder.GuiType.LINE_LIST)); + when(topInventory.getSize()).thenReturn(27); + when(event.getRawSlots()).thenReturn(Set.of(5, 12)); listener.onInventoryDrag(event); verify(event).setCancelled(true); } + @Test + void shouldAllowDragConfinedToPlayerLowerInventory() { + GuiListener listener = new GuiListener(mock(Metro.class)); + InventoryDragEvent event = mock(InventoryDragEvent.class); + Inventory topInventory = mock(Inventory.class); + + when(event.getView()).thenReturn(viewWithTop(topInventory)); + when(topInventory.getHolder()).thenReturn(new GuiHolder(GuiHolder.GuiType.LINE_LIST)); + when(topInventory.getSize()).thenReturn(27); + when(event.getRawSlots()).thenReturn(Set.of(27, 36, 45)); + + listener.onInventoryDrag(event); + + verify(event, never()).setCancelled(true); + } + @Test void shouldIgnoreDraggingOutsideMetroGui() { GuiListener listener = new GuiListener(mock(Metro.class)); InventoryDragEvent event = mock(InventoryDragEvent.class); - Inventory inventory = mock(Inventory.class); + Inventory topInventory = mock(Inventory.class); - when(event.getInventory()).thenReturn(inventory); - when(inventory.getHolder()).thenReturn(null); + when(event.getView()).thenReturn(viewWithTop(topInventory)); + when(topInventory.getHolder()).thenReturn(null); listener.onInventoryDrag(event); diff --git a/src/test/java/org/cubexmc/metro/integration/VaultIntegrationTest.java b/src/test/java/org/cubexmc/metro/integration/VaultIntegrationTest.java new file mode 100644 index 0000000..f0e8740 --- /dev/null +++ b/src/test/java/org/cubexmc/metro/integration/VaultIntegrationTest.java @@ -0,0 +1,144 @@ +package org.cubexmc.metro.integration; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import net.milkbowl.vault.economy.Economy; +import net.milkbowl.vault.economy.EconomyResponse; + +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.ServicesManager; +import org.bukkit.plugin.ServicePriority; +import org.cubexmc.metro.Metro; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class VaultIntegrationTest { + + private Metro plugin; + private Server server; + private PluginManager pluginManager; + private ServicesManager servicesManager; + private Economy economy; + private EconomyResponse successResponse; + + @BeforeEach + void setUp() { + plugin = mock(Metro.class); + server = mock(Server.class); + pluginManager = mock(PluginManager.class); + servicesManager = mock(ServicesManager.class); + economy = mock(Economy.class); + successResponse = mock(EconomyResponse.class); + + when(plugin.getServer()).thenReturn(server); + when(server.getServicesManager()).thenReturn(servicesManager); + when(server.getPluginManager()).thenReturn(pluginManager); + when(successResponse.transactionSuccess()).thenReturn(true); + } + + @Test + void shouldBeDisabledWhenNoEconomyProvider() { + when(servicesManager.getRegistration(eq(Economy.class))).thenReturn(null); + + VaultIntegration vault = new VaultIntegration(plugin); + + assertFalse(vault.isEnabled()); + assertNull(vault.economy); + } + + @Test + void shouldResolveProviderAtConstruction() { + var registration = mock(net.milkbowl.vault.economy.Economy.class); + when(servicesManager.getRegistration(eq(Economy.class))) + .thenReturn(new org.bukkit.plugin.RegisteredService<>( + Economy.class, economy, plugin, ServicePriority.Normal)); + + VaultIntegration vault = new VaultIntegration(plugin); + + assertTrue(vault.isEnabled()); + assertNotNull(vault.economy); + } + + @Test + void shouldRefreshProviderOnDemand() { + when(servicesManager.getRegistration(eq(Economy.class))).thenReturn(null); + VaultIntegration vault = new VaultIntegration(plugin); + + assertFalse(vault.isEnabled()); + + // Economy provider appears later + when(servicesManager.getRegistration(eq(Economy.class))) + .thenReturn(new org.bukkit.plugin.RegisteredService<>( + Economy.class, economy, plugin, ServicePriority.Normal)); + vault.refreshProvider(); + + assertTrue(vault.isEnabled()); + } + + @Test + void shouldClearProviderOnRefreshWhenUnregistered() { + when(servicesManager.getRegistration(eq(Economy.class))) + .thenReturn(new org.bukkit.plugin.RegisteredService<>( + Economy.class, economy, plugin, ServicePriority.Normal)); + VaultIntegration vault = new VaultIntegration(plugin); + + assertTrue(vault.isEnabled()); + + // Economy provider disappears + when(servicesManager.getRegistration(eq(Economy.class))).thenReturn(null); + vault.refreshProvider(); + + assertFalse(vault.isEnabled()); + assertNull(vault.economy); + } + + @Test + void shouldDelegateHasCheckToProvider() { + when(servicesManager.getRegistration(eq(Economy.class))) + .thenReturn(new org.bukkit.plugin.RegisteredService<>( + Economy.class, economy, plugin, ServicePriority.Normal)); + VaultIntegration vault = new VaultIntegration(plugin); + Player player = mock(Player.class); + + when(economy.has(player, 10.0)).thenReturn(true); + assertTrue(vault.has(player, 10.0)); + + when(economy.has(player, 99.0)).thenReturn(false); + assertFalse(vault.has(player, 99.0)); + } + + @Test + void shouldReturnFalseForAllOperationsWhenDisabled() { + when(servicesManager.getRegistration(eq(Economy.class))).thenReturn(null); + VaultIntegration vault = new VaultIntegration(plugin); + Player player = mock(Player.class); + + assertFalse(vault.has(player, 5.0)); + assertFalse(vault.withdraw(player, 5.0)); + assertFalse(vault.deposit(null, 5.0)); + } + + @Test + void shouldWithdrawFromPlayer() { + when(servicesManager.getRegistration(eq(Economy.class))) + .thenReturn(new org.bukkit.plugin.RegisteredService<>( + Economy.class, economy, plugin, ServicePriority.Normal)); + VaultIntegration vault = new VaultIntegration(plugin); + Player player = mock(Player.class); + + when(economy.withdrawPlayer(player, 10.0)).thenReturn(successResponse); + assertTrue(vault.withdraw(player, 10.0)); + verify(economy).withdrawPlayer(player, 10.0); + } +} diff --git a/src/test/java/org/cubexmc/metro/service/TicketServiceTest.java b/src/test/java/org/cubexmc/metro/service/TicketServiceTest.java index 5d65515..2aa6eaf 100644 --- a/src/test/java/org/cubexmc/metro/service/TicketServiceTest.java +++ b/src/test/java/org/cubexmc/metro/service/TicketServiceTest.java @@ -144,11 +144,89 @@ void shouldOnlyChargeBasePriceForBoarding() { verify(vault).withdraw(player, 3.0); } + @Test + void shouldRefundPlayerWhenOwnerDepositFailsInCharge() { + VaultIntegration vault = enabledVault(); + Player player = player(); + UUID playerUuid = UUID.randomUUID(); + UUID owner = UUID.randomUUID(); + Line line = line(5.0); + line.setOwner(owner); + + when(player.getUniqueId()).thenReturn(playerUuid); + when(vault.has(player, 5.0)).thenReturn(true); + when(vault.withdraw(player, 5.0)).thenReturn(true); + when(vault.deposit(owner, 5.0)).thenReturn(false); + + TicketService service = new TicketService(() -> vault, () -> true); + TicketService.TicketTransaction txn = service.createTransaction(player, line); + + assertEquals(TicketChargeStatus.TRANSACTION_FAILED, service.charge(txn)); + verify(vault).withdraw(player, 5.0); + verify(vault).deposit(owner, 5.0); + verify(vault).deposit(playerUuid, 5.0); + } + + @Test + void shouldRefundPlayerWhenOwnerDepositFailsInChargePrice() { + VaultIntegration vault = enabledVault(); + Player player = player(); + UUID playerUuid = UUID.randomUUID(); + UUID owner = UUID.randomUUID(); + Line line = line(5.0); + line.setOwner(owner); + line.setPriceRule(new PriceRule(PriceRule.PricingMode.DISTANCE, 2.0)); + + when(player.getUniqueId()).thenReturn(playerUuid); + when(vault.has(player, 3.0)).thenReturn(true); + when(vault.withdraw(player, 3.0)).thenReturn(true); + when(vault.deposit(owner, 3.0)).thenReturn(false); + + TicketService service = new TicketService(() -> vault, () -> true); + + assertEquals(TicketChargeStatus.TRANSACTION_FAILED, service.chargePrice(player, line, 3.0)); + verify(vault).withdraw(player, 3.0); + verify(vault).deposit(owner, 3.0); + verify(vault).deposit(playerUuid, 3.0); + } + + @Test + void shouldSkipDepositWhenLineHasNoOwner() { + VaultIntegration vault = enabledVault(); + Player player = player(); + Line line = line(5.0); + + when(vault.has(player, 5.0)).thenReturn(true); + when(vault.withdraw(player, 5.0)).thenReturn(true); + + TicketService service = new TicketService(() -> vault, () -> true); + TicketService.TicketTransaction txn = service.createTransaction(player, line); + + assertEquals(TicketChargeStatus.CHARGED, service.charge(txn)); + verify(vault, never()).deposit(any(), anyDouble()); + } + + @Test + void shouldSkipOwnerDepositWhenDepositFailsInChargePriceAndNoOwner() { + VaultIntegration vault = enabledVault(); + Player player = player(); + Line line = line(5.0); + + when(vault.has(player, 5.0)).thenReturn(true); + when(vault.withdraw(player, 5.0)).thenReturn(true); + + TicketService service = new TicketService(() -> vault, () -> true); + + assertEquals(TicketChargeStatus.CHARGED, service.chargePrice(player, line, 5.0)); + verify(vault, never()).deposit(any(), anyDouble()); + } + private VaultIntegration enabledVault() { VaultIntegration vault = mock(VaultIntegration.class); when(vault.isEnabled()).thenReturn(true); when(vault.format(5.0)).thenReturn("$5.00"); when(vault.format(7.0)).thenReturn("$7.00"); + when(vault.format(3.0)).thenReturn("$3.00"); return vault; }