fix(pocket-base): handle keystone right-clicks in air
This commit is contained in:
@@ -24,6 +24,15 @@ As a **Pocket Base owner**, I want to craft a reusable keystone that instantly t
|
|||||||
- [x] Keystone travel clears accumulated fall distance and does not change existing portal travel behavior.
|
- [x] Keystone travel clears accumulated fall distance and does not change existing portal travel behavior.
|
||||||
- [x] Automated tests cover recipe shape, crafting and use eligibility, metadata authentication, entry, return, persistence, and unavailable or unsafe destinations.
|
- [x] Automated tests cover recipe shape, crafting and use eligibility, metadata authentication, entry, return, persistence, and unavailable or unsafe destinations.
|
||||||
|
|
||||||
|
- [x] Keystone entry and return work when right-clicking air or blocks, regardless of facing direction.
|
||||||
|
- [x] Vanilla no-action events are handled without bypassing explicit item-use restrictions.
|
||||||
|
- [x] Regression tests cover air and block interactions for entry and return, and denied item use.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `./gradlew clean check jar` passes, including event-dispatch regression coverage for air/block entry and return and explicit item-use denial.
|
||||||
|
- Live-server verification remains pending deployment.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [US-015: Unlock a Pocket Base](us-015-unlock-a-pocket-base.md)
|
- [US-015: Unlock a Pocket Base](us-015-unlock-a-pocket-base.md)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.bukkit.block.Block;
|
|||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.Event;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.EventPriority;
|
import org.bukkit.event.EventPriority;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
@@ -72,13 +73,18 @@ final class PocketBaseKeystoneService implements Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
|
@EventHandler(priority = EventPriority.HIGH)
|
||||||
public void onUse(PlayerInteractEvent event) {
|
public void onUse(PlayerInteractEvent event) {
|
||||||
Action action = event.getAction();
|
Action action = event.getAction();
|
||||||
if ((action != Action.RIGHT_CLICK_AIR && action != Action.RIGHT_CLICK_BLOCK)
|
if ((action != Action.RIGHT_CLICK_AIR && action != Action.RIGHT_CLICK_BLOCK)
|
||||||
|| !isKeystone(event.getItem(), markerKey)) {
|
|| !isKeystone(event.getItem(), markerKey)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Vanilla no-action air clicks can deny block use without denying item use.
|
||||||
|
// Accept those events, but respect explicit item-use denial by other plugins.
|
||||||
|
if (event.useItemInHand() == Event.Result.DENY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
Player player = event.getPlayer();
|
Player player = event.getPlayer();
|
||||||
UUID playerId = player.getUniqueId();
|
UUID playerId = player.getUniqueId();
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ import org.bukkit.Server;
|
|||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.Event;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.block.Action;
|
import org.bukkit.event.block.Action;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import org.bukkit.plugin.RegisteredListener;
|
||||||
import org.bukkit.event.inventory.PrepareItemCraftEvent;
|
import org.bukkit.event.inventory.PrepareItemCraftEvent;
|
||||||
import org.bukkit.event.player.PlayerInteractEvent;
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||||
@@ -32,6 +36,8 @@ import org.bukkit.inventory.meta.ItemMeta;
|
|||||||
import org.bukkit.persistence.PersistentDataContainer;
|
import org.bukkit.persistence.PersistentDataContainer;
|
||||||
import org.bukkit.persistence.PersistentDataType;
|
import org.bukkit.persistence.PersistentDataType;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.EnumSource;
|
||||||
|
|
||||||
final class PocketBaseKeystoneServiceTest {
|
final class PocketBaseKeystoneServiceTest {
|
||||||
@Test
|
@Test
|
||||||
@@ -122,8 +128,9 @@ final class PocketBaseKeystoneServiceTest {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
void unlockedPlayerEntersOwnPocketAndRecordsExactReturnDestination() throws Exception {
|
@EnumSource(value = Action.class, names = {"RIGHT_CLICK_AIR", "RIGHT_CLICK_BLOCK"})
|
||||||
|
void unlockedPlayerEntersOwnPocketAndRecordsExactReturnDestination(Action action) throws Exception {
|
||||||
NamespacedKey key = NamespacedKey.minecraft("pocket_base_keystone");
|
NamespacedKey key = NamespacedKey.minecraft("pocket_base_keystone");
|
||||||
UUID playerId = UUID.randomUUID();
|
UUID playerId = UUID.randomUUID();
|
||||||
UUID worldId = UUID.randomUUID();
|
UUID worldId = UUID.randomUUID();
|
||||||
@@ -155,7 +162,10 @@ final class PocketBaseKeystoneServiceTest {
|
|||||||
when(player.teleport(arrival, PlayerTeleportEvent.TeleportCause.PLUGIN))
|
when(player.teleport(arrival, PlayerTeleportEvent.TeleportCause.PLUGIN))
|
||||||
.thenReturn(true);
|
.thenReturn(true);
|
||||||
|
|
||||||
service.onUse(event);
|
when(event.getAction()).thenReturn(action);
|
||||||
|
when(event.isCancelled()).thenReturn(action == Action.RIGHT_CLICK_AIR);
|
||||||
|
when(event.useItemInHand()).thenReturn(Event.Result.DEFAULT);
|
||||||
|
dispatchUse(service, event);
|
||||||
|
|
||||||
verify(returns).record(playerId, new KeystoneReturnLocation(
|
verify(returns).record(playerId, new KeystoneReturnLocation(
|
||||||
worldId, "world", 12.25, 70.5, -4.75, 123.0F, -15.5F
|
worldId, "world", 12.25, 70.5, -4.75, 123.0F, -15.5F
|
||||||
@@ -165,8 +175,9 @@ final class PocketBaseKeystoneServiceTest {
|
|||||||
verify(event).setCancelled(true);
|
verify(event).setCancelled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
void playerReturnsFromOwnPocketToRecordedSafeDestination() {
|
@EnumSource(value = Action.class, names = {"RIGHT_CLICK_AIR", "RIGHT_CLICK_BLOCK"})
|
||||||
|
void playerReturnsFromOwnPocketToRecordedSafeDestination(Action action) throws Exception {
|
||||||
NamespacedKey key = NamespacedKey.minecraft("pocket_base_keystone");
|
NamespacedKey key = NamespacedKey.minecraft("pocket_base_keystone");
|
||||||
UUID playerId = UUID.randomUUID();
|
UUID playerId = UUID.randomUUID();
|
||||||
UUID pocketWorldId = UUID.randomUUID();
|
UUID pocketWorldId = UUID.randomUUID();
|
||||||
@@ -217,7 +228,10 @@ final class PocketBaseKeystoneServiceTest {
|
|||||||
PlayerTeleportEvent.TeleportCause.PLUGIN
|
PlayerTeleportEvent.TeleportCause.PLUGIN
|
||||||
)).thenReturn(true);
|
)).thenReturn(true);
|
||||||
|
|
||||||
service.onUse(event);
|
when(event.getAction()).thenReturn(action);
|
||||||
|
when(event.isCancelled()).thenReturn(action == Action.RIGHT_CLICK_AIR);
|
||||||
|
when(event.useItemInHand()).thenReturn(Event.Result.DEFAULT);
|
||||||
|
dispatchUse(service, event);
|
||||||
|
|
||||||
verify(player).teleport(
|
verify(player).teleport(
|
||||||
new Location(returnWorld, 12.25, 70.5, -4.75, 123.0F, -15.5F),
|
new Location(returnWorld, 12.25, 70.5, -4.75, 123.0F, -15.5F),
|
||||||
@@ -324,6 +338,51 @@ final class PocketBaseKeystoneServiceTest {
|
|||||||
assertFalse(PocketBaseKeystoneService.isKeystone(renamedOnly, key));
|
assertFalse(PocketBaseKeystoneService.isKeystone(renamedOnly, key));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@EnumSource(value = Action.class, names = {"RIGHT_CLICK_AIR", "RIGHT_CLICK_BLOCK"})
|
||||||
|
void explicitItemUseDenialPreventsKeystoneTravel(Action action) throws Exception {
|
||||||
|
NamespacedKey key = NamespacedKey.minecraft("pocket_base_keystone");
|
||||||
|
Player player = mock(Player.class);
|
||||||
|
UUID playerId = UUID.randomUUID();
|
||||||
|
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||||
|
KeystoneReturnStore returns = mock(KeystoneReturnStore.class);
|
||||||
|
PocketBaseKeystoneService service = new PocketBaseKeystoneService(
|
||||||
|
mock(Server.class), pocketBases, returns, key, Logger.getAnonymousLogger()
|
||||||
|
);
|
||||||
|
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
||||||
|
ItemStack keystone = markedKeystone(key);
|
||||||
|
when(event.getAction()).thenReturn(action);
|
||||||
|
when(event.getItem()).thenReturn(keystone);
|
||||||
|
when(event.getPlayer()).thenReturn(player);
|
||||||
|
when(event.isCancelled()).thenReturn(true);
|
||||||
|
when(event.useItemInHand()).thenReturn(Event.Result.DENY);
|
||||||
|
when(player.getUniqueId()).thenReturn(playerId);
|
||||||
|
when(pocketBases.state(playerId)).thenReturn(
|
||||||
|
new PocketBaseState(playerId, 0, Optional.empty())
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatchUse(service, event);
|
||||||
|
|
||||||
|
verify(pocketBases, never()).state(any(UUID.class));
|
||||||
|
verify(returns, never()).record(any(UUID.class), any(KeystoneReturnLocation.class));
|
||||||
|
verify(player, never()).teleport(
|
||||||
|
any(Location.class), any(PlayerTeleportEvent.TeleportCause.class)
|
||||||
|
);
|
||||||
|
verify(event, never()).setCancelled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void dispatchUse(
|
||||||
|
PocketBaseKeystoneService service, PlayerInteractEvent event
|
||||||
|
) throws Exception {
|
||||||
|
EventHandler handler = PocketBaseKeystoneService.class
|
||||||
|
.getMethod("onUse", PlayerInteractEvent.class).getAnnotation(EventHandler.class);
|
||||||
|
RegisteredListener listener = new RegisteredListener(
|
||||||
|
service, (ignored, dispatched) -> service.onUse((PlayerInteractEvent) dispatched),
|
||||||
|
handler.priority(), mock(Plugin.class), handler.ignoreCancelled()
|
||||||
|
);
|
||||||
|
listener.callEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
private static ItemStack markedKeystone(NamespacedKey key) {
|
private static ItemStack markedKeystone(NamespacedKey key) {
|
||||||
ItemStack item = mock(ItemStack.class);
|
ItemStack item = mock(ItemStack.class);
|
||||||
ItemMeta metadata = mock(ItemMeta.class);
|
ItemMeta metadata = mock(ItemMeta.class);
|
||||||
|
|||||||
Reference in New Issue
Block a user