Skip to content

Commit 7ffc962

Browse files
committed
feat: implement cancellable player reset events for leaving an island
1 parent 91acc42 commit 7ffc962

2 files changed

Lines changed: 284 additions & 35 deletions

File tree

src/main/java/world/bentobox/bentobox/managers/PlayersManager.java

Lines changed: 82 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.eclipse.jdt.annotation.Nullable;
1818

1919
import world.bentobox.bentobox.BentoBox;
20+
import world.bentobox.bentobox.api.events.player.PlayerEvent;
2021
import world.bentobox.bentobox.api.user.User;
2122
import world.bentobox.bentobox.database.Database;
2223
import world.bentobox.bentobox.database.objects.Island;
@@ -344,67 +345,113 @@ public void removePlayer(Player player) {
344345
}
345346

346347
/**
347-
* Cleans the player when leaving an island
348-
* @param world - island world
349-
* @param target - target user
350-
* @param kicked - true if player is being kicked
351-
* @param island - island being left
348+
* Cleans the player when leaving an island.
349+
* <p>
350+
* For each configurable reset action a cancellable {@link world.bentobox.bentobox.api.events.player.PlayerBaseEvent}
351+
* is fired via {@link PlayerEvent#builder()} before the action is executed. If the event is
352+
* cancelled by a listener the corresponding action is skipped entirely. The events fired, in order, are:
353+
* <ol>
354+
* <li>{@link world.bentobox.bentobox.api.events.player.PlayerTamedRemovalEvent} – before untaming the player's animals</li>
355+
* <li>{@link world.bentobox.bentobox.api.events.player.PlayerResetEnderChestEvent} – before clearing the ender chest</li>
356+
* <li>{@link world.bentobox.bentobox.api.events.player.PlayerResetInventoryEvent} – before clearing the inventory</li>
357+
* <li>{@link world.bentobox.bentobox.api.events.player.PlayerResetMoneyEvent} – before withdrawing the player's balance</li>
358+
* <li>{@link world.bentobox.bentobox.api.events.player.PlayerResetHealthEvent} – before resetting health</li>
359+
* <li>{@link world.bentobox.bentobox.api.events.player.PlayerResetHungerEvent} – before resetting hunger</li>
360+
* <li>{@link world.bentobox.bentobox.api.events.player.PlayerResetExpEvent} – before resetting XP</li>
361+
* </ol>
362+
*
363+
* @param world the island world
364+
* @param target the target user
365+
* @param kicked {@code true} if the player is being kicked from the team
366+
* @param island the island being left
352367
* @since 1.15.4
353368
*/
354369
public void cleanLeavingPlayer(World world, User target, boolean kicked, Island island) {
355-
// Execute commands when leaving
370+
// Execute on-leave commands unconditionally (not a player-state reset, no event needed)
356371
String ownerName = this.getName(island.getOwner());
357372
Util.runCommands(target, ownerName, plugin.getIWM().getOnLeaveCommands(world), "leave");
358373

359-
// Remove any tamed animals
360-
world.getEntitiesByClass(Tameable.class).stream()
361-
.filter(Tameable::isTamed)
362-
.filter(t -> t.getOwner() != null && t.getOwner().getUniqueId().equals(target.getUniqueId()))
363-
.forEach(t -> t.setOwner(null));
374+
// Remove any tamed animals – skipped if the TAMED_REMOVAL event is cancelled
375+
if (!PlayerEvent.builder()
376+
.world(world).island(island).involvedPlayer(target.getUniqueId())
377+
.reason(PlayerEvent.Reason.TAMED_REMOVAL).build().isCancelled()) {
378+
world.getEntitiesByClass(Tameable.class).stream()
379+
.filter(Tameable::isTamed)
380+
.filter(t -> t.getOwner() != null && t.getOwner().getUniqueId().equals(target.getUniqueId()))
381+
.forEach(t -> t.setOwner(null));
382+
}
364383

365-
// Remove money inventory etc.
384+
// Clear ender chest – skipped if the ENDERCHEST_RESET event is cancelled
366385
if (plugin.getIWM().isOnLeaveResetEnderChest(world)) {
367-
if (target.isOnline()) {
368-
target.getPlayer().getEnderChest().clear();
369-
} else {
370-
Players p = getPlayer(target.getUniqueId());
371-
if (p != null) {
372-
p.addToPendingKick(world);
386+
if (!PlayerEvent.builder()
387+
.world(world).island(island).involvedPlayer(target.getUniqueId())
388+
.reason(PlayerEvent.Reason.ENDERCHEST_RESET).build().isCancelled()) {
389+
if (target.isOnline()) {
390+
target.getPlayer().getEnderChest().clear();
391+
} else {
392+
Players p = getPlayer(target.getUniqueId());
393+
if (p != null) {
394+
p.addToPendingKick(world);
395+
}
373396
}
374397
}
375398
}
399+
400+
// Clear inventory – skipped if the INVENTORY_RESET event is cancelled
376401
if ((kicked && plugin.getIWM().isOnLeaveResetInventory(world) && !plugin.getIWM().isKickedKeepInventory(world))
377402
|| (!kicked && plugin.getIWM().isOnLeaveResetInventory(world))) {
378-
if (target.isOnline()) {
379-
target.getPlayer().getInventory().clear();
380-
} else {
381-
Players p = getPlayer(target.getUniqueId());
382-
if (p != null) {
383-
p.addToPendingKick(world);
403+
if (!PlayerEvent.builder()
404+
.world(world).island(island).involvedPlayer(target.getUniqueId())
405+
.reason(PlayerEvent.Reason.INVENTORY_RESET).build().isCancelled()) {
406+
if (target.isOnline()) {
407+
target.getPlayer().getInventory().clear();
408+
} else {
409+
Players p = getPlayer(target.getUniqueId());
410+
if (p != null) {
411+
p.addToPendingKick(world);
412+
}
384413
}
385414
}
386415
}
387416

417+
// Withdraw money – skipped if the MONEY_RESET event is cancelled
388418
if (plugin.getSettings().isUseEconomy() && plugin.getIWM().isOnLeaveResetMoney(world)) {
389-
plugin.getVault().ifPresent(vault -> vault.withdraw(target, vault.getBalance(target), world));
419+
if (!PlayerEvent.builder()
420+
.world(world).island(island).involvedPlayer(target.getUniqueId())
421+
.reason(PlayerEvent.Reason.MONEY_RESET).build().isCancelled()) {
422+
plugin.getVault().ifPresent(vault -> vault.withdraw(target, vault.getBalance(target), world));
423+
}
390424
}
391-
// Reset the health
425+
426+
// Reset health – skipped if the HEALTH_RESET event is cancelled
392427
if (plugin.getIWM().isOnLeaveResetHealth(world) && target.isPlayer()) {
393-
Util.resetHealth(target.getPlayer());
428+
if (!PlayerEvent.builder()
429+
.world(world).island(island).involvedPlayer(target.getUniqueId())
430+
.reason(PlayerEvent.Reason.HEALTH_RESET).build().isCancelled()) {
431+
Util.resetHealth(target.getPlayer());
432+
}
394433
}
395434

396-
// Reset the hunger
435+
// Reset hunger – skipped if the HUNGER_RESET event is cancelled
397436
if (plugin.getIWM().isOnLeaveResetHunger(world) && target.isPlayer()) {
398-
target.getPlayer().setFoodLevel(20);
437+
if (!PlayerEvent.builder()
438+
.world(world).island(island).involvedPlayer(target.getUniqueId())
439+
.reason(PlayerEvent.Reason.HUNGER_RESET).build().isCancelled()) {
440+
target.getPlayer().setFoodLevel(20);
441+
}
399442
}
400443

401-
// Reset the XP
444+
// Reset XP – skipped if the EXP_RESET event is cancelled
402445
if (plugin.getIWM().isOnLeaveResetXP(world) && target.isPlayer()) {
403-
// Player collected XP (displayed)
404-
target.getPlayer().setLevel(0);
405-
target.getPlayer().setExp(0);
406-
// Player total XP (not displayed)
407-
target.getPlayer().setTotalExperience(0);
446+
if (!PlayerEvent.builder()
447+
.world(world).island(island).involvedPlayer(target.getUniqueId())
448+
.reason(PlayerEvent.Reason.EXP_RESET).build().isCancelled()) {
449+
// Player collected XP (displayed)
450+
target.getPlayer().setLevel(0);
451+
target.getPlayer().setExp(0);
452+
// Player total XP (not displayed)
453+
target.getPlayer().setTotalExperience(0);
454+
}
408455
}
409456
}
410457

src/test/java/world/bentobox/bentobox/managers/PlayersManagerTest.java

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,20 @@
99
import org.junit.jupiter.api.Assertions;
1010
import static org.mockito.ArgumentMatchers.any;
1111
import static org.mockito.ArgumentMatchers.anyString;
12+
import static org.mockito.Mockito.doAnswer;
1213
import static org.mockito.Mockito.mock;
1314
import static org.mockito.Mockito.never;
1415
import static org.mockito.Mockito.verify;
1516
import static org.mockito.Mockito.when;
1617

18+
import world.bentobox.bentobox.api.events.player.PlayerResetEnderChestEvent;
19+
import world.bentobox.bentobox.api.events.player.PlayerResetExpEvent;
20+
import world.bentobox.bentobox.api.events.player.PlayerResetHealthEvent;
21+
import world.bentobox.bentobox.api.events.player.PlayerResetHungerEvent;
22+
import world.bentobox.bentobox.api.events.player.PlayerResetInventoryEvent;
23+
import world.bentobox.bentobox.api.events.player.PlayerResetMoneyEvent;
24+
import world.bentobox.bentobox.api.events.player.PlayerTamedRemovalEvent;
25+
1726
import java.beans.IntrospectionException;
1827
import java.io.File;
1928
import java.lang.reflect.InvocationTargetException;
@@ -684,4 +693,197 @@ void testSavePlayer() {
684693
pm.savePlayer(uuid).thenAccept(Assertions::assertTrue);
685694
}
686695

696+
// -----------------------------------------------------------------------
697+
// cleanLeavingPlayer – event-cancellation tests
698+
// -----------------------------------------------------------------------
699+
700+
/**
701+
* Helper: arrange pim so that any callEvent() for the given event class will
702+
* cancel that event before returning.
703+
*/
704+
private <T extends org.bukkit.event.Event> void cancelEventOfType(Class<T> type) {
705+
doAnswer(inv -> {
706+
Object event = inv.getArgument(0);
707+
if (type.isInstance(event)) {
708+
((world.bentobox.bentobox.api.events.player.PlayerBaseEvent) event).setCancelled(true);
709+
}
710+
return null;
711+
}).when(pim).callEvent(any());
712+
}
713+
714+
/**
715+
* When a {@link PlayerTamedRemovalEvent} is cancelled the player's tamed
716+
* animals must NOT have their owner cleared.
717+
*/
718+
@Test
719+
void testCleanLeavingPlayerTamedRemovalCancelled() {
720+
cancelEventOfType(PlayerTamedRemovalEvent.class);
721+
pm.cleanLeavingPlayer(world, user, false, island);
722+
verify(tamed, never()).setOwner(null);
723+
}
724+
725+
/**
726+
* When {@link PlayerTamedRemovalEvent} is NOT cancelled tamed animals ARE
727+
* untamed (regression guard for the happy path).
728+
*/
729+
@Test
730+
void testCleanLeavingPlayerTamedRemovalNotCancelled() {
731+
pm.cleanLeavingPlayer(world, user, false, island);
732+
verify(tamed).setOwner(null);
733+
}
734+
735+
/**
736+
* When a {@link PlayerResetEnderChestEvent} is cancelled the ender chest must
737+
* NOT be cleared.
738+
*/
739+
@Test
740+
void testCleanLeavingPlayerEnderChestCancelled() {
741+
cancelEventOfType(PlayerResetEnderChestEvent.class);
742+
pm.cleanLeavingPlayer(world, user, false, island);
743+
verify(inv, never()).clear();
744+
}
745+
746+
/**
747+
* When {@link PlayerResetEnderChestEvent} is NOT cancelled the ender chest IS
748+
* cleared (regression guard).
749+
*/
750+
@Test
751+
void testCleanLeavingPlayerEnderChestNotCancelled() {
752+
pm.cleanLeavingPlayer(world, user, false, island);
753+
verify(inv).clear();
754+
}
755+
756+
/**
757+
* When a {@link PlayerResetInventoryEvent} is cancelled the player inventory
758+
* must NOT be cleared.
759+
*/
760+
@Test
761+
void testCleanLeavingPlayerInventoryCancelled() {
762+
when(iwm.isKickedKeepInventory(any())).thenReturn(false);
763+
cancelEventOfType(PlayerResetInventoryEvent.class);
764+
pm.cleanLeavingPlayer(world, user, false, island);
765+
verify(playerInv, never()).clear();
766+
}
767+
768+
/**
769+
* When {@link PlayerResetInventoryEvent} is NOT cancelled (and kick-keep is
770+
* off) the inventory IS cleared (regression guard).
771+
*/
772+
@Test
773+
void testCleanLeavingPlayerInventoryNotCancelled() {
774+
when(iwm.isKickedKeepInventory(any())).thenReturn(false);
775+
pm.cleanLeavingPlayer(world, user, false, island);
776+
verify(playerInv).clear();
777+
}
778+
779+
/**
780+
* When a {@link PlayerResetMoneyEvent} is cancelled the economy withdraw must
781+
* NOT be called.
782+
*/
783+
@Test
784+
void testCleanLeavingPlayerMoneyCancelled() {
785+
cancelEventOfType(PlayerResetMoneyEvent.class);
786+
pm.cleanLeavingPlayer(world, user, false, island);
787+
verify(vault, never()).withdraw(any(), any(double.class), any());
788+
}
789+
790+
/**
791+
* When {@link PlayerResetMoneyEvent} is NOT cancelled the balance IS withdrawn
792+
* (regression guard).
793+
*/
794+
@Test
795+
void testCleanLeavingPlayerMoneyNotCancelled() {
796+
pm.cleanLeavingPlayer(world, user, false, island);
797+
verify(vault).withdraw(user, 0D, world);
798+
}
799+
800+
/**
801+
* When a {@link PlayerResetHealthEvent} is cancelled health must NOT be reset.
802+
*/
803+
@Test
804+
void testCleanLeavingPlayerHealthCancelled() {
805+
cancelEventOfType(PlayerResetHealthEvent.class);
806+
pm.cleanLeavingPlayer(world, user, false, island);
807+
mockedUtil.verify(() -> Util.resetHealth(p), never());
808+
}
809+
810+
/**
811+
* When {@link PlayerResetHealthEvent} is NOT cancelled health IS reset
812+
* (regression guard).
813+
*/
814+
@Test
815+
void testCleanLeavingPlayerHealthNotCancelled() {
816+
pm.cleanLeavingPlayer(world, user, false, island);
817+
mockedUtil.verify(() -> Util.resetHealth(p));
818+
}
819+
820+
/**
821+
* When a {@link PlayerResetHungerEvent} is cancelled food level must NOT be
822+
* set.
823+
*/
824+
@Test
825+
void testCleanLeavingPlayerHungerCancelled() {
826+
cancelEventOfType(PlayerResetHungerEvent.class);
827+
pm.cleanLeavingPlayer(world, user, false, island);
828+
verify(p, never()).setFoodLevel(20);
829+
}
830+
831+
/**
832+
* When {@link PlayerResetHungerEvent} is NOT cancelled food level IS reset
833+
* (regression guard).
834+
*/
835+
@Test
836+
void testCleanLeavingPlayerHungerNotCancelled() {
837+
pm.cleanLeavingPlayer(world, user, false, island);
838+
verify(p).setFoodLevel(20);
839+
}
840+
841+
/**
842+
* When a {@link PlayerResetExpEvent} is cancelled XP must NOT be reset.
843+
*/
844+
@Test
845+
void testCleanLeavingPlayerExpCancelled() {
846+
cancelEventOfType(PlayerResetExpEvent.class);
847+
pm.cleanLeavingPlayer(world, user, false, island);
848+
verify(p, never()).setLevel(0);
849+
verify(p, never()).setExp(0);
850+
verify(p, never()).setTotalExperience(0);
851+
}
852+
853+
/**
854+
* When {@link PlayerResetExpEvent} is NOT cancelled XP IS reset
855+
* (regression guard).
856+
*/
857+
@Test
858+
void testCleanLeavingPlayerExpNotCancelled() {
859+
pm.cleanLeavingPlayer(world, user, false, island);
860+
verify(p).setLevel(0);
861+
verify(p).setExp(0);
862+
verify(p).setTotalExperience(0);
863+
}
864+
865+
/**
866+
* Cancelling one event (e.g. ender chest) must not affect other resets
867+
* – only the ender chest clear is skipped; inventory, money, health, hunger
868+
* and XP proceed normally.
869+
*/
870+
@Test
871+
void testCleanLeavingPlayerOnlyEnderChestCancelledOtherActionsStillRun() {
872+
when(iwm.isKickedKeepInventory(any())).thenReturn(false);
873+
cancelEventOfType(PlayerResetEnderChestEvent.class);
874+
pm.cleanLeavingPlayer(world, user, false, island);
875+
// Ender chest NOT cleared
876+
verify(inv, never()).clear();
877+
// Inventory IS cleared
878+
verify(playerInv).clear();
879+
// Money IS withdrawn
880+
verify(vault).withdraw(user, 0D, world);
881+
// Health IS reset
882+
mockedUtil.verify(() -> Util.resetHealth(p));
883+
// Hunger IS reset
884+
verify(p).setFoodLevel(20);
885+
// XP IS reset
886+
verify(p).setTotalExperience(0);
887+
}
888+
687889
}

0 commit comments

Comments
 (0)