From fb25fc75832b47939dbe78bfdebb4bac581c4d49 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 27 Jan 2024 22:16:49 -0800 Subject: [PATCH] Add feature to block movement on starting a oneblock. #367 --- .../world/bentobox/aoneblock/AOneBlock.java | 32 +- .../world/bentobox/aoneblock/Settings.java | 19 + .../listeners/StartSafetyListener.java | 74 +++ src/main/resources/locales/en-US.yml | 11 + .../listeners/StartSafetyListenerTest.java | 581 ++++++++++++++++++ 5 files changed, 716 insertions(+), 1 deletion(-) create mode 100644 src/main/java/world/bentobox/aoneblock/listeners/StartSafetyListener.java create mode 100644 src/test/java/world/bentobox/aoneblock/listeners/StartSafetyListenerTest.java diff --git a/src/main/java/world/bentobox/aoneblock/AOneBlock.java b/src/main/java/world/bentobox/aoneblock/AOneBlock.java index 331b6cac..5a9c27ea 100644 --- a/src/main/java/world/bentobox/aoneblock/AOneBlock.java +++ b/src/main/java/world/bentobox/aoneblock/AOneBlock.java @@ -4,6 +4,7 @@ import java.util.Objects; import org.bukkit.Bukkit; +import org.bukkit.Material; import org.bukkit.World; import org.bukkit.World.Environment; import org.bukkit.WorldCreator; @@ -24,6 +25,7 @@ import world.bentobox.aoneblock.listeners.ItemsAdderListener; import world.bentobox.aoneblock.listeners.JoinLeaveListener; import world.bentobox.aoneblock.listeners.NoBlockHandler; +import world.bentobox.aoneblock.listeners.StartSafetyListener; import world.bentobox.aoneblock.oneblocks.OneBlockCustomBlockCreator; import world.bentobox.aoneblock.oneblocks.OneBlocksManager; import world.bentobox.aoneblock.oneblocks.customblock.ItemsAdderCustomBlock; @@ -32,6 +34,9 @@ import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.configuration.Config; import world.bentobox.bentobox.api.configuration.WorldSettings; +import world.bentobox.bentobox.api.flags.Flag; +import world.bentobox.bentobox.api.flags.Flag.Mode; +import world.bentobox.bentobox.api.flags.Flag.Type; import world.bentobox.bentobox.database.objects.Island; /** @@ -53,6 +58,14 @@ public class AOneBlock extends GameModeAddon { private OneBlocksManager oneBlockManager; private AOneBlockPlaceholders phManager; private HoloListener holoListener; + + // Flag + public final Flag START_SAFETY = new Flag.Builder("START_SAFETY", Material.BAMBOO_BLOCK) + .mode(Mode.BASIC) + .type(Type.WORLD_SETTING) + .listener(new StartSafetyListener(this)) + .defaultSetting(false) + .build(); @Override public void onLoad() { @@ -73,10 +86,13 @@ public void onLoad() { // Register commands playerCommand = new PlayerCommand(this); adminCommand = new AdminCommand(this); + // Register flag with BentoBox + // Register protection flag with BentoBox + getPlugin().getFlagsManager().registerFlag(this, START_SAFETY); } } - private boolean loadSettings() { + private boolean loadSettings() { // Load settings again to get worlds settings = configObject.loadConfigObject(); if (settings == null) { @@ -305,4 +321,18 @@ public HoloListener getHoloListener() { public boolean hasItemsAdder() { return hasItemsAdder; } + + /** + * Set the addon's world. Used only for testing. + * @param world world + */ + public void setIslandWorld(World world) { + this.islandWorld = world; + + } + + public void setSettings(Settings settings) { + this.settings = settings; + } + } diff --git a/src/main/java/world/bentobox/aoneblock/Settings.java b/src/main/java/world/bentobox/aoneblock/Settings.java index 24a6ba3b..50e3f458 100644 --- a/src/main/java/world/bentobox/aoneblock/Settings.java +++ b/src/main/java/world/bentobox/aoneblock/Settings.java @@ -111,6 +111,11 @@ public class Settings implements WorldSettings { @ConfigEntry(path = "world.hologram-duration") private int hologramDuration = 10; + @ConfigComment("Duration in seonds that players cannot move when they start a new one block.") + @ConfigComment("Used only if the Starting Safety world setting is active.") + @ConfigEntry(path = "world.starting-safety-duration") + private int startingSafetyDuration = 10; + @ConfigComment("Clear blocks when spawning mobs.") @ConfigComment("Mobs break blocks when they spawn is to prevent players from building a box around the magic block,") @ConfigComment("having the mob spawn, and then die by suffocation, i.e., it's a cheat prevention.") @@ -2061,4 +2066,18 @@ public boolean isClearBlocks() { public void setClearBlocks(boolean clearBlocks) { this.clearBlocks = clearBlocks; } + + /** + * @return the startingSafetyDuration + */ + public int getStartingSafetyDuration() { + return startingSafetyDuration; + } + + /** + * @param startingSafetyDuration the startingSafetyDuration to set + */ + public void setStartingSafetyDuration(int startingSafetyDuration) { + this.startingSafetyDuration = startingSafetyDuration; + } } \ No newline at end of file diff --git a/src/main/java/world/bentobox/aoneblock/listeners/StartSafetyListener.java b/src/main/java/world/bentobox/aoneblock/listeners/StartSafetyListener.java new file mode 100644 index 00000000..fa752a68 --- /dev/null +++ b/src/main/java/world/bentobox/aoneblock/listeners/StartSafetyListener.java @@ -0,0 +1,74 @@ +package world.bentobox.aoneblock.listeners; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerMoveEvent; + +import world.bentobox.aoneblock.AOneBlock; +import world.bentobox.bentobox.api.events.island.IslandCreatedEvent; +import world.bentobox.bentobox.api.events.island.IslandResetEvent; +import world.bentobox.bentobox.api.localization.TextVariables; +import world.bentobox.bentobox.api.user.User; + +/** + * Listener to provide protection for players in the first minute of their island. Prevents movement. + */ +public class StartSafetyListener implements Listener { + + private final AOneBlock addon; + private final Map newIslands = new HashMap<>(); + + public StartSafetyListener(AOneBlock addon) { + super(); + this.addon = addon; + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onNewIsland(IslandCreatedEvent e) { + store(e.getIsland().getWorld(), e.getPlayerUUID()); + } + + private void store(World world, UUID playerUUID) { + if (addon.inWorld(world) && addon.START_SAFETY.isSetForWorld(world) && !newIslands.containsKey(playerUUID)) { + long time = addon.getSettings().getStartingSafetyDuration(); + if (time < 0) { + time = 10; // 10 seconds + } + newIslands.put(playerUUID, System.currentTimeMillis() + (time * 1000)); + Bukkit.getScheduler().runTaskLater(addon.getPlugin(), () -> { + newIslands.remove(playerUUID); + User.getInstance(playerUUID).sendMessage("protection.flags.START_SAFETY.free-to-move"); + }, time); + } + + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onResetIsland(IslandResetEvent e) { + store(e.getIsland().getWorld(), e.getPlayerUUID()); + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) + public void onPlayerMove(PlayerMoveEvent e) { + if (addon.inWorld(e.getPlayer().getWorld()) && newIslands.containsKey(e.getPlayer().getUniqueId()) + && e.getTo() != null && !e.getPlayer().isSneaking() + && (e.getFrom().getX() != e.getTo().getX() || e.getFrom().getZ() != e.getTo().getZ())) { + // Do not allow x or z movement + e.setTo(new Location(e.getFrom().getWorld(), e.getFrom().getX(), e.getTo().getY(), e.getFrom().getZ(), + e.getTo().getYaw(), e.getTo().getPitch())); + String waitTime = String + .valueOf((int) ((newIslands.get(e.getPlayer().getUniqueId()) - System.currentTimeMillis()) / 1000)); + User.getInstance(e.getPlayer()).notify(addon.START_SAFETY.getHintReference(), TextVariables.NUMBER, + waitTime); + } + } + +} diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 1d59b20e..ea1f839a 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -3,6 +3,17 @@ # the one at http://yaml-online-parser.appspot.com # ########################################################################################### +protection: + flags: + START_SAFETY: + name: Starting Safety + description: | + &b Prevents new players + &b from moving for 1 minute + &b so they don't fall off. + hint: "&c Movement blocked for safety for [number] more seconds!" + free-to-move: "&a You are free to move. Be careful!" + aoneblock: commands: admin: diff --git a/src/test/java/world/bentobox/aoneblock/listeners/StartSafetyListenerTest.java b/src/test/java/world/bentobox/aoneblock/listeners/StartSafetyListenerTest.java new file mode 100644 index 00000000..c3d351d8 --- /dev/null +++ b/src/test/java/world/bentobox/aoneblock/listeners/StartSafetyListenerTest.java @@ -0,0 +1,581 @@ +package world.bentobox.aoneblock.listeners; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.Difficulty; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.scheduler.BukkitScheduler; +import org.eclipse.jdt.annotation.NonNull; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; + +import world.bentobox.aoneblock.AOneBlock; +import world.bentobox.aoneblock.Settings; +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.configuration.WorldSettings; +import world.bentobox.bentobox.api.events.island.IslandCreatedEvent; +import world.bentobox.bentobox.api.events.island.IslandResetEvent; +import world.bentobox.bentobox.api.flags.Flag; +import world.bentobox.bentobox.api.user.Notifier; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.managers.IslandWorldManager; +import world.bentobox.bentobox.managers.LocalesManager; +import world.bentobox.bentobox.managers.PlaceholdersManager; + +/** + * @author tastybento + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({ Bukkit.class, BentoBox.class }) +public class StartSafetyListenerTest { + + private AOneBlock addon; + private StartSafetyListener ssl; + @Mock + private Island island; + private UUID uuid = UUID.randomUUID(); + @Mock + private Location location; + @Mock + private Location location2; + @Mock + private World world; + @Mock + private BentoBox plugin; + @Mock + private IslandWorldManager iwm; + @Mock + private Flag flag; + + private @NonNull WSettings ws = new WSettings(); + @Mock + private BukkitScheduler scheduler; + @Mock + private Player player; + @Mock + private LocalesManager lm; + @Mock + private PlaceholdersManager phm; + @Mock + private Notifier notifier; + + /** + * @throws java.lang.Exception + */ + @Before + public void setUp() throws Exception { + + PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS); + when(Bukkit.getScheduler()).thenReturn(scheduler); + + // Set up plugin + Whitebox.setInternalState(BentoBox.class, "instance", plugin); + + addon = new AOneBlock(); + addon.setIslandWorld(world); + addon.setSettings(new Settings()); + + // Player + when(player.getUniqueId()).thenReturn(uuid); + when(player.getWorld()).thenReturn(world); + User.getInstance(player); + + when(world.getName()).thenReturn("world"); + + when(flag.isSetForWorld(world)).thenReturn(true); + + when(iwm.inWorld(world)).thenReturn(true); + when(iwm.getWorldSettings(world)).thenReturn(ws); + when(plugin.getIWM()).thenReturn(iwm); + + when(location.getWorld()).thenReturn(world); + when(location2.getWorld()).thenReturn(world); + when(island.getWorld()).thenReturn(world); + when(island.getCenter()).thenReturn(location); + + when(plugin.getNotifier()).thenReturn(notifier); + + // Placeholders + when(phm.replacePlaceholders(any(), anyString())) + .thenAnswer((Answer) invocation -> invocation.getArgument(1, String.class)); + + // BentoBox + when(plugin.getLocalesManager()).thenReturn(lm); + when(plugin.getPlaceholdersManager()).thenReturn(phm); + + when(location2.getX()).thenReturn(0.5D); + + addon.START_SAFETY.setSetting(world, true); + + ssl = new StartSafetyListener(addon); + } + + /** + * @throws java.lang.Exception + */ + @After + public void tearDown() throws Exception { + User.clearUsers(); + } + + /** + * Test method for {@link world.bentobox.aoneblock.listeners.StartSafetyListener#StartSafetyListener(world.bentobox.aoneblock.AOneBlock)}. + */ + @Test + public void testStartSafetyListener() { + assertNotNull(ssl); + } + + /** + * Test method for {@link world.bentobox.aoneblock.listeners.StartSafetyListener#onNewIsland(world.bentobox.bentobox.api.events.island.IslandCreatedEvent)}. + */ + @Test + public void testOnNewIsland() { + IslandCreatedEvent e = new IslandCreatedEvent(island, uuid, false, location); + ssl.onNewIsland(e); + verify(scheduler).runTaskLater(eq(plugin), any(Runnable.class), anyLong()); + } + + /** + * Test method for {@link world.bentobox.aoneblock.listeners.StartSafetyListener#onResetIsland(world.bentobox.bentobox.api.events.island.IslandResetEvent)}. + */ + @Test + public void testOnResetIsland() { + IslandResetEvent e = new IslandResetEvent(island, uuid, false, location, null, island); + ssl.onResetIsland(e); + verify(scheduler).runTaskLater(eq(plugin), any(Runnable.class), anyLong()); + + } + + /** + * Test method for {@link world.bentobox.aoneblock.listeners.StartSafetyListener#onPlayerMove(org.bukkit.event.player.PlayerMoveEvent)}. + */ + @Test + public void testOnPlayerMove() { + testOnResetIsland(); + PlayerMoveEvent e = new PlayerMoveEvent(player, location, location2); + ssl.onPlayerMove(e); + // No movement + assertEquals(0D, e.getTo().getX(), 0D); + assertEquals(0D, e.getTo().getZ(), 0D); + verify(player).isSneaking(); + + } + + class WSettings implements WorldSettings { + + private Map flags = new HashMap<>(); + + @Override + public GameMode getDefaultGameMode() { + + return null; + } + + @Override + public Map getDefaultIslandFlags() { + + return null; + } + + @Override + public Map getDefaultIslandSettings() { + + return null; + } + + @Override + public Difficulty getDifficulty() { + + return null; + } + + @Override + public void setDifficulty(Difficulty difficulty) { + + } + + @Override + public String getFriendlyName() { + + return null; + } + + @Override + public int getIslandDistance() { + + return 0; + } + + @Override + public int getIslandHeight() { + + return 0; + } + + @Override + public int getIslandProtectionRange() { + + return 0; + } + + @Override + public int getIslandStartX() { + + return 0; + } + + @Override + public int getIslandStartZ() { + + return 0; + } + + @Override + public int getIslandXOffset() { + + return 0; + } + + @Override + public int getIslandZOffset() { + + return 0; + } + + @Override + public List getIvSettings() { + + return null; + } + + @Override + public int getMaxHomes() { + + return 0; + } + + @Override + public int getMaxIslands() { + + return 0; + } + + @Override + public int getMaxTeamSize() { + + return 0; + } + + @Override + public int getNetherSpawnRadius() { + + return 0; + } + + @Override + public String getPermissionPrefix() { + + return null; + } + + @Override + public Set getRemoveMobsWhitelist() { + + return null; + } + + @Override + public int getSeaHeight() { + + return 0; + } + + @Override + public List getHiddenFlags() { + + return null; + } + + @Override + public List getVisitorBannedCommands() { + + return null; + } + + @Override + public Map getWorldFlags() { + return flags; + } + + @Override + public String getWorldName() { + + return null; + } + + @Override + public boolean isDragonSpawn() { + + return false; + } + + @Override + public boolean isEndGenerate() { + + return false; + } + + @Override + public boolean isEndIslands() { + + return false; + } + + @Override + public boolean isNetherGenerate() { + + return false; + } + + @Override + public boolean isNetherIslands() { + + return false; + } + + @Override + public boolean isOnJoinResetEnderChest() { + + return false; + } + + @Override + public boolean isOnJoinResetInventory() { + + return false; + } + + @Override + public boolean isOnJoinResetMoney() { + + return false; + } + + @Override + public boolean isOnJoinResetHealth() { + + return false; + } + + @Override + public boolean isOnJoinResetHunger() { + + return false; + } + + @Override + public boolean isOnJoinResetXP() { + + return false; + } + + @Override + public @NonNull List getOnJoinCommands() { + + return null; + } + + @Override + public boolean isOnLeaveResetEnderChest() { + + return false; + } + + @Override + public boolean isOnLeaveResetInventory() { + + return false; + } + + @Override + public boolean isOnLeaveResetMoney() { + + return false; + } + + @Override + public boolean isOnLeaveResetHealth() { + + return false; + } + + @Override + public boolean isOnLeaveResetHunger() { + + return false; + } + + @Override + public boolean isOnLeaveResetXP() { + + return false; + } + + @Override + public @NonNull List getOnLeaveCommands() { + + return null; + } + + @Override + public boolean isUseOwnGenerator() { + + return false; + } + + @Override + public boolean isWaterUnsafe() { + + return false; + } + + @Override + public List getGeoLimitSettings() { + + return null; + } + + @Override + public int getResetLimit() { + + return 0; + } + + @Override + public long getResetEpoch() { + + return 0; + } + + @Override + public void setResetEpoch(long timestamp) { + + } + + @Override + public boolean isTeamJoinDeathReset() { + + return false; + } + + @Override + public int getDeathsMax() { + + return 0; + } + + @Override + public boolean isDeathsCounted() { + + return false; + } + + @Override + public boolean isDeathsResetOnNewIsland() { + + return false; + } + + @Override + public boolean isAllowSetHomeInNether() { + + return false; + } + + @Override + public boolean isAllowSetHomeInTheEnd() { + + return false; + } + + @Override + public boolean isRequireConfirmationToSetHomeInNether() { + + return false; + } + + @Override + public boolean isRequireConfirmationToSetHomeInTheEnd() { + + return false; + } + + @Override + public int getBanLimit() { + + return 0; + } + + @Override + public boolean isLeaversLoseReset() { + + return false; + } + + @Override + public boolean isKickedKeepInventory() { + + return false; + } + + @Override + public boolean isCreateIslandOnFirstLoginEnabled() { + + return false; + } + + @Override + public int getCreateIslandOnFirstLoginDelay() { + + return 0; + } + + @Override + public boolean isCreateIslandOnFirstLoginAbortOnLogout() { + + return false; + } + + } + +}