diff --git a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandGoCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandGoCommand.java index 9f599f8c5..7345dd0cb 100644 --- a/src/main/java/world/bentobox/bentobox/api/commands/island/IslandGoCommand.java +++ b/src/main/java/world/bentobox/bentobox/api/commands/island/IslandGoCommand.java @@ -151,6 +151,14 @@ private boolean checkReserved(User user, List islands) { } return false; } + + @Override + public Optional> tabComplete(User user, String alias, List args) { + String lastArg = !args.isEmpty() ? args.get(args.size()-1) : ""; + + return Optional.of(Util.tabLimit(new ArrayList<>(getNameIslandMap(user, getWorld()).keySet()), lastArg)); + + } /** * Record to store island information and whether the name refers to diff --git a/src/main/java/world/bentobox/bentobox/managers/island/IslandGrid.java b/src/main/java/world/bentobox/bentobox/managers/island/IslandGrid.java index c1fb94690..a430a6c84 100644 --- a/src/main/java/world/bentobox/bentobox/managers/island/IslandGrid.java +++ b/src/main/java/world/bentobox/bentobox/managers/island/IslandGrid.java @@ -36,30 +36,73 @@ public IslandGrid(IslandCache im) { * @return true if successfully added, false if island already exists, or there is an overlap */ public boolean addToGrid(Island island) { - // Check if we know about this island already int minX = island.getMinX(); int minZ = island.getMinZ(); - IslandData islandData = new IslandData(island.getUniqueId(), minX, minZ, island.getRange()); - if (grid.containsKey(minX)) { - TreeMap zEntry = grid.get(minX); - if (zEntry.containsKey(minZ)) { - // If it is the same island then it's okay - return island.getUniqueId().equals(zEntry.get(minZ).id()); - // Island overlap, report error - } else { - // Add island - zEntry.put(minZ, islandData); - grid.put(minX, zEntry); + int range = island.getRange(); + IslandData newIsland = new IslandData(island.getUniqueId(), minX, minZ, range); + + // Remove this island if it is already in the grid + this.removeFromGrid(island); + + // compute bounds for the new island (upper bounds are exclusive) + int newMaxX = minX + range * 2; + int newMaxZ = minZ + range * 2; + + /* + * Find any existing islands that could overlap: + * - Any existing island with minX <= newMaxX could extend over newMinX, so we must consider + * all entries with key <= newMaxX (use headMap). + * - For each candidate X entry, consider Z entries with minZ <= newMaxZ (use headMap). + * This avoids missing large islands whose minX is far left of the new island. + */ + for (Entry> xEntry : grid.headMap(newMaxX, true).entrySet()) { + TreeMap zMap = xEntry.getValue(); + for (Entry zEntry : zMap.headMap(newMaxZ, true).entrySet()) { + IslandData existingIsland = zEntry.getValue(); + if (isOverlapping(newIsland, existingIsland)) { + return false; + } } - } else { - // Add island - TreeMap zEntry = new TreeMap<>(); - zEntry.put(minZ, islandData); - grid.put(minX, zEntry); } + + // No overlaps found, add the island + addNewEntry(minX, minZ, newIsland); + return true; + } + + /** + * Checks if two islands overlap + * @param island1 first island + * @param island2 second island + * @return true if islands overlap + */ + private boolean isOverlapping(IslandData island1, IslandData island2) { + int island1MaxX = island1.minX() + (island1.range() * 2); + int island1MaxZ = island1.minZ() + (island1.range() * 2); + int island2MaxX = island2.minX() + (island2.range() * 2); + int island2MaxZ = island2.minZ() + (island2.range() * 2); + + // Check if one rectangle is to the left of the other + if (island1MaxX <= island2.minX() || island2MaxX <= island1.minX()) { + return false; + } + + // Check if one rectangle is above the other + if (island1MaxZ <= island2.minZ() || island2MaxZ <= island1.minZ()) { + return false; + } + return true; } + /** + * Helper method to add a new entry to the grid + */ + private void addNewEntry(int minX, int minZ, IslandData islandData) { + TreeMap zEntry = grid.computeIfAbsent(minX, k -> new TreeMap<>()); + zEntry.put(minZ, islandData); + } + /** * Remove island from grid * @param island - the island to remove diff --git a/src/test/java/world/bentobox/bentobox/managers/island/IslandGridEdgeCaseTest.java b/src/test/java/world/bentobox/bentobox/managers/island/IslandGridEdgeCaseTest.java new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/java/world/bentobox/bentobox/managers/island/IslandGridTest.java b/src/test/java/world/bentobox/bentobox/managers/island/IslandGridTest.java new file mode 100644 index 000000000..4f2b3c8bd --- /dev/null +++ b/src/test/java/world/bentobox/bentobox/managers/island/IslandGridTest.java @@ -0,0 +1,257 @@ +package world.bentobox.bentobox.managers.island; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.powermock.modules.junit4.PowerMockRunner; + +import world.bentobox.bentobox.database.objects.Island; + +/** + * Grid test + */ +@RunWith(PowerMockRunner.class) +public class IslandGridTest { + + private IslandGrid ig; + @Mock + private IslandCache im; + @Mock + private Island island; + @Mock + private Island island2; + @Mock + private Island overlappingIsland; + @Mock + private Island original; + @Mock + private Island updated; + @Mock + private Island a; + @Mock + private Island b; + @Mock + private Island big; + @Mock + private Island small; + @Mock + private Island zIsland; + + /** + * @throws java.lang.Exception + */ + @Before + public void setUp() throws Exception { + // Islands + when(island.getMinX()).thenReturn(356); + when(island.getMinZ()).thenReturn(5678); + when(island.getRange()).thenReturn(64); + when(island.getUniqueId()).thenReturn("island"); + when(overlappingIsland.getMinX()).thenReturn(360); + when(overlappingIsland.getMinZ()).thenReturn(5678); + when(overlappingIsland.getRange()).thenReturn(64); + when(overlappingIsland.getUniqueId()).thenReturn("overlappingIsland"); + when(island2.getMinX()).thenReturn(-32); + when(island2.getMinZ()).thenReturn(-32); + when(island2.getRange()).thenReturn(64); + when(island2.getUniqueId()).thenReturn("island2"); + when(im.getIslandById("island")).thenReturn(island); + when(im.getIslandById("island2")).thenReturn(island2); + when(im.getIslandById("overlappingIsland")).thenReturn(overlappingIsland); + ig = new IslandGrid(im); + } + + /** + * @throws java.lang.Exception + */ + @After + public void tearDown() throws Exception { + } + + /** + * Test method for {@link world.bentobox.bentobox.managers.island.IslandGrid#addToGrid(world.bentobox.bentobox.database.objects.Island)}. + */ + @Test + public void testAddToGrid() { + assertTrue(ig.addToGrid(island)); + assertFalse(ig.addToGrid(overlappingIsland)); + assertTrue(ig.addToGrid(island2)); + } + + /** + * Test method for {@link world.bentobox.bentobox.managers.island.IslandGrid#removeFromGrid(world.bentobox.bentobox.database.objects.Island)}. + */ + @Test + public void testRemoveFromGrid() { + assertTrue(ig.addToGrid(island)); + assertTrue(ig.removeFromGrid(island)); + assertFalse(ig.removeFromGrid(island2)); + } + + /** + * Test method for {@link world.bentobox.bentobox.managers.island.IslandGrid#getIslandAt(int, int)}. + */ + @Test + public void testGetIslandAt() { + assertNull(ig.getIslandAt(0, 0)); + assertTrue(ig.addToGrid(island)); + assertTrue(ig.addToGrid(island2)); + assertEquals(island, ig.getIslandAt(360, 5700)); + assertEquals(island2, ig.getIslandAt(0, 0)); + } + + /** + * Test method for {@link world.bentobox.bentobox.managers.island.IslandGrid#isIslandAt(int, int)}. + */ + @Test + public void testIsIslandAt() { + assertFalse(ig.isIslandAt(0, 0)); + assertTrue(ig.addToGrid(island2)); + assertTrue(ig.isIslandAt(0, 0)); + assertTrue(ig.addToGrid(island)); + assertTrue(ig.isIslandAt(360, 5700)); + assertFalse(ig.isIslandAt(-1000, 1000)); + } + + /** + * Test method for {@link world.bentobox.bentobox.managers.island.IslandGrid#getIslandStringAt(int, int)}. + */ + @Test + public void testGetIslandStringAt() { + assertNull(ig.getIslandStringAt(0, 0)); + assertTrue(ig.addToGrid(island2)); + assertEquals("island2", ig.getIslandStringAt(0, 0)); + + } + + /** + * Test method for {@link world.bentobox.bentobox.managers.island.IslandGrid#getSize()}. + */ + @Test + public void testGetSize() { + assertEquals(0, ig.getSize()); + assertTrue(ig.addToGrid(island2)); + assertEquals(1, ig.getSize()); + assertTrue(ig.addToGrid(island)); + assertEquals(2, ig.getSize()); + ig.removeFromGrid(island); + assertEquals(1, ig.getSize()); + ig.removeFromGrid(island2); + assertEquals(0, ig.getSize()); + } + + /** + * Test method for {@link world.bentobox.bentobox.managers.island.IslandGrid#getGrid()}. + */ + @Test + public void testGetGrid() { + assertNotNull(ig.getGrid()); + } + + @Test + public void testUpdateIslandCoordinatesKeepsSingleEntry() { + // original at (100,100) range 20 + when(original.getMinX()).thenReturn(100); + when(original.getMinZ()).thenReturn(100); + when(original.getRange()).thenReturn(20); + when(original.getUniqueId()).thenReturn("orig"); + + // updated has same id but moved to (300,300) range 20 + when(updated.getMinX()).thenReturn(300); + when(updated.getMinZ()).thenReturn(300); + when(updated.getRange()).thenReturn(20); + when(updated.getUniqueId()).thenReturn("orig"); + + when(im.getIslandById("orig")).thenReturn(original); + + // add original + assertTrue(ig.addToGrid(original)); + assertEquals(1, ig.getSize()); + + // add updated (same id) -> should update, keep size 1 + assertTrue(ig.addToGrid(updated)); + assertEquals(1, ig.getSize()); + + // original location should no longer contain the island + assertNull(ig.getIslandStringAt(110, 110)); + + // new location should contain the island + assertEquals("orig", ig.getIslandStringAt(310, 310)); + } + + @Test + public void testAdjacentIslandsAllowedWhenEdgesTouch() { + // island a covers x:[0,20) z:[0,20) + when(a.getMinX()).thenReturn(0); + when(a.getMinZ()).thenReturn(0); + when(a.getRange()).thenReturn(10); + when(a.getUniqueId()).thenReturn("a"); + + // island b starts exactly at x=20 (touching edge), z same + when(b.getMinX()).thenReturn(20); + when(b.getMinZ()).thenReturn(0); + when(b.getRange()).thenReturn(10); + when(b.getUniqueId()).thenReturn("b"); + + when(im.getIslandById("a")).thenReturn(a); + when(im.getIslandById("b")).thenReturn(b); + + assertTrue(ig.addToGrid(a)); + // touching edge should be allowed + assertTrue(ig.addToGrid(b)); + + // verify both retrievable at representative coords + assertEquals("a", ig.getIslandStringAt(10, 10)); + assertEquals("b", ig.getIslandStringAt(21, 10)); + } + + @Test + public void testLargeExistingIslandShouldBlockSmallIslandEvenIfMinXOutsideSubMapWindow() { + // big island minX = 0, range = 1000 + when(big.getMinX()).thenReturn(0); + when(big.getMinZ()).thenReturn(0); + when(big.getRange()).thenReturn(1000); + when(big.getUniqueId()).thenReturn("big"); + + // small island minX = 1500, range = 10 -> would overlap big + when(small.getMinX()).thenReturn(1500); + when(small.getMinZ()).thenReturn(10); + when(small.getRange()).thenReturn(10); + when(small.getUniqueId()).thenReturn("small"); + + when(im.getIslandById("big")).thenReturn(big); + when(im.getIslandById("small")).thenReturn(small); + + assertTrue(ig.addToGrid(big)); + + // Expected: adding small should be rejected because it lies inside big + // If this test fails, it reveals the current subMap window is too small to find big. + assertFalse("Small island overlaps big island; should have been rejected", ig.addToGrid(small)); + } + + @Test + public void testGetIslandStringAtWhenXEntryExistsButNoZEntryApplies() { + // island exists at minX=100 minZ=100 range=10 (covers z [110,110)) + when(zIsland.getMinX()).thenReturn(100); + when(zIsland.getMinZ()).thenReturn(100); + when(zIsland.getRange()).thenReturn(10); + when(zIsland.getUniqueId()).thenReturn("z"); + + when(im.getIslandById("z")).thenReturn(zIsland); + + assertTrue(ig.addToGrid(zIsland)); + + // Query an x within island x-range but z is below any minZ -> should return null + assertNull(ig.getIslandStringAt(110, 50)); + } + +}