Skip to content

Commit af23971

Browse files
authored
Merge pull request #2760 from BentoBoxWorld/better_grid_storage
Better grid storage for arbitrary island positions.
2 parents a83364f + 32061e8 commit af23971

File tree

4 files changed

+325
-17
lines changed

4 files changed

+325
-17
lines changed

src/main/java/world/bentobox/bentobox/api/commands/island/IslandGoCommand.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,14 @@ private boolean checkReserved(User user, List<Island> islands) {
151151
}
152152
return false;
153153
}
154+
155+
@Override
156+
public Optional<List<String>> tabComplete(User user, String alias, List<String> args) {
157+
String lastArg = !args.isEmpty() ? args.get(args.size()-1) : "";
158+
159+
return Optional.of(Util.tabLimit(new ArrayList<>(getNameIslandMap(user, getWorld()).keySet()), lastArg));
160+
161+
}
154162

155163
/**
156164
* Record to store island information and whether the name refers to

src/main/java/world/bentobox/bentobox/managers/island/IslandGrid.java

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,30 +36,73 @@ public IslandGrid(IslandCache im) {
3636
* @return true if successfully added, false if island already exists, or there is an overlap
3737
*/
3838
public boolean addToGrid(Island island) {
39-
// Check if we know about this island already
4039
int minX = island.getMinX();
4140
int minZ = island.getMinZ();
42-
IslandData islandData = new IslandData(island.getUniqueId(), minX, minZ, island.getRange());
43-
if (grid.containsKey(minX)) {
44-
TreeMap<Integer, IslandData> zEntry = grid.get(minX);
45-
if (zEntry.containsKey(minZ)) {
46-
// If it is the same island then it's okay
47-
return island.getUniqueId().equals(zEntry.get(minZ).id());
48-
// Island overlap, report error
49-
} else {
50-
// Add island
51-
zEntry.put(minZ, islandData);
52-
grid.put(minX, zEntry);
41+
int range = island.getRange();
42+
IslandData newIsland = new IslandData(island.getUniqueId(), minX, minZ, range);
43+
44+
// Remove this island if it is already in the grid
45+
this.removeFromGrid(island);
46+
47+
// compute bounds for the new island (upper bounds are exclusive)
48+
int newMaxX = minX + range * 2;
49+
int newMaxZ = minZ + range * 2;
50+
51+
/*
52+
* Find any existing islands that could overlap:
53+
* - Any existing island with minX <= newMaxX could extend over newMinX, so we must consider
54+
* all entries with key <= newMaxX (use headMap).
55+
* - For each candidate X entry, consider Z entries with minZ <= newMaxZ (use headMap).
56+
* This avoids missing large islands whose minX is far left of the new island.
57+
*/
58+
for (Entry<Integer, TreeMap<Integer, IslandData>> xEntry : grid.headMap(newMaxX, true).entrySet()) {
59+
TreeMap<Integer, IslandData> zMap = xEntry.getValue();
60+
for (Entry<Integer, IslandData> zEntry : zMap.headMap(newMaxZ, true).entrySet()) {
61+
IslandData existingIsland = zEntry.getValue();
62+
if (isOverlapping(newIsland, existingIsland)) {
63+
return false;
64+
}
5365
}
54-
} else {
55-
// Add island
56-
TreeMap<Integer, IslandData> zEntry = new TreeMap<>();
57-
zEntry.put(minZ, islandData);
58-
grid.put(minX, zEntry);
5966
}
67+
68+
// No overlaps found, add the island
69+
addNewEntry(minX, minZ, newIsland);
70+
return true;
71+
}
72+
73+
/**
74+
* Checks if two islands overlap
75+
* @param island1 first island
76+
* @param island2 second island
77+
* @return true if islands overlap
78+
*/
79+
private boolean isOverlapping(IslandData island1, IslandData island2) {
80+
int island1MaxX = island1.minX() + (island1.range() * 2);
81+
int island1MaxZ = island1.minZ() + (island1.range() * 2);
82+
int island2MaxX = island2.minX() + (island2.range() * 2);
83+
int island2MaxZ = island2.minZ() + (island2.range() * 2);
84+
85+
// Check if one rectangle is to the left of the other
86+
if (island1MaxX <= island2.minX() || island2MaxX <= island1.minX()) {
87+
return false;
88+
}
89+
90+
// Check if one rectangle is above the other
91+
if (island1MaxZ <= island2.minZ() || island2MaxZ <= island1.minZ()) {
92+
return false;
93+
}
94+
6095
return true;
6196
}
6297

98+
/**
99+
* Helper method to add a new entry to the grid
100+
*/
101+
private void addNewEntry(int minX, int minZ, IslandData islandData) {
102+
TreeMap<Integer, IslandData> zEntry = grid.computeIfAbsent(minX, k -> new TreeMap<>());
103+
zEntry.put(minZ, islandData);
104+
}
105+
63106
/**
64107
* Remove island from grid
65108
* @param island - the island to remove

src/test/java/world/bentobox/bentobox/managers/island/IslandGridEdgeCaseTest.java

Whitespace-only changes.
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
package world.bentobox.bentobox.managers.island;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertFalse;
5+
import static org.junit.Assert.assertNotNull;
6+
import static org.junit.Assert.assertNull;
7+
import static org.junit.Assert.assertTrue;
8+
import static org.mockito.Mockito.when;
9+
10+
import org.junit.After;
11+
import org.junit.Before;
12+
import org.junit.Test;
13+
import org.junit.runner.RunWith;
14+
import org.mockito.Mock;
15+
import org.powermock.modules.junit4.PowerMockRunner;
16+
17+
import world.bentobox.bentobox.database.objects.Island;
18+
19+
/**
20+
* Grid test
21+
*/
22+
@RunWith(PowerMockRunner.class)
23+
public class IslandGridTest {
24+
25+
private IslandGrid ig;
26+
@Mock
27+
private IslandCache im;
28+
@Mock
29+
private Island island;
30+
@Mock
31+
private Island island2;
32+
@Mock
33+
private Island overlappingIsland;
34+
@Mock
35+
private Island original;
36+
@Mock
37+
private Island updated;
38+
@Mock
39+
private Island a;
40+
@Mock
41+
private Island b;
42+
@Mock
43+
private Island big;
44+
@Mock
45+
private Island small;
46+
@Mock
47+
private Island zIsland;
48+
49+
/**
50+
* @throws java.lang.Exception
51+
*/
52+
@Before
53+
public void setUp() throws Exception {
54+
// Islands
55+
when(island.getMinX()).thenReturn(356);
56+
when(island.getMinZ()).thenReturn(5678);
57+
when(island.getRange()).thenReturn(64);
58+
when(island.getUniqueId()).thenReturn("island");
59+
when(overlappingIsland.getMinX()).thenReturn(360);
60+
when(overlappingIsland.getMinZ()).thenReturn(5678);
61+
when(overlappingIsland.getRange()).thenReturn(64);
62+
when(overlappingIsland.getUniqueId()).thenReturn("overlappingIsland");
63+
when(island2.getMinX()).thenReturn(-32);
64+
when(island2.getMinZ()).thenReturn(-32);
65+
when(island2.getRange()).thenReturn(64);
66+
when(island2.getUniqueId()).thenReturn("island2");
67+
when(im.getIslandById("island")).thenReturn(island);
68+
when(im.getIslandById("island2")).thenReturn(island2);
69+
when(im.getIslandById("overlappingIsland")).thenReturn(overlappingIsland);
70+
ig = new IslandGrid(im);
71+
}
72+
73+
/**
74+
* @throws java.lang.Exception
75+
*/
76+
@After
77+
public void tearDown() throws Exception {
78+
}
79+
80+
/**
81+
* Test method for {@link world.bentobox.bentobox.managers.island.IslandGrid#addToGrid(world.bentobox.bentobox.database.objects.Island)}.
82+
*/
83+
@Test
84+
public void testAddToGrid() {
85+
assertTrue(ig.addToGrid(island));
86+
assertFalse(ig.addToGrid(overlappingIsland));
87+
assertTrue(ig.addToGrid(island2));
88+
}
89+
90+
/**
91+
* Test method for {@link world.bentobox.bentobox.managers.island.IslandGrid#removeFromGrid(world.bentobox.bentobox.database.objects.Island)}.
92+
*/
93+
@Test
94+
public void testRemoveFromGrid() {
95+
assertTrue(ig.addToGrid(island));
96+
assertTrue(ig.removeFromGrid(island));
97+
assertFalse(ig.removeFromGrid(island2));
98+
}
99+
100+
/**
101+
* Test method for {@link world.bentobox.bentobox.managers.island.IslandGrid#getIslandAt(int, int)}.
102+
*/
103+
@Test
104+
public void testGetIslandAt() {
105+
assertNull(ig.getIslandAt(0, 0));
106+
assertTrue(ig.addToGrid(island));
107+
assertTrue(ig.addToGrid(island2));
108+
assertEquals(island, ig.getIslandAt(360, 5700));
109+
assertEquals(island2, ig.getIslandAt(0, 0));
110+
}
111+
112+
/**
113+
* Test method for {@link world.bentobox.bentobox.managers.island.IslandGrid#isIslandAt(int, int)}.
114+
*/
115+
@Test
116+
public void testIsIslandAt() {
117+
assertFalse(ig.isIslandAt(0, 0));
118+
assertTrue(ig.addToGrid(island2));
119+
assertTrue(ig.isIslandAt(0, 0));
120+
assertTrue(ig.addToGrid(island));
121+
assertTrue(ig.isIslandAt(360, 5700));
122+
assertFalse(ig.isIslandAt(-1000, 1000));
123+
}
124+
125+
/**
126+
* Test method for {@link world.bentobox.bentobox.managers.island.IslandGrid#getIslandStringAt(int, int)}.
127+
*/
128+
@Test
129+
public void testGetIslandStringAt() {
130+
assertNull(ig.getIslandStringAt(0, 0));
131+
assertTrue(ig.addToGrid(island2));
132+
assertEquals("island2", ig.getIslandStringAt(0, 0));
133+
134+
}
135+
136+
/**
137+
* Test method for {@link world.bentobox.bentobox.managers.island.IslandGrid#getSize()}.
138+
*/
139+
@Test
140+
public void testGetSize() {
141+
assertEquals(0, ig.getSize());
142+
assertTrue(ig.addToGrid(island2));
143+
assertEquals(1, ig.getSize());
144+
assertTrue(ig.addToGrid(island));
145+
assertEquals(2, ig.getSize());
146+
ig.removeFromGrid(island);
147+
assertEquals(1, ig.getSize());
148+
ig.removeFromGrid(island2);
149+
assertEquals(0, ig.getSize());
150+
}
151+
152+
/**
153+
* Test method for {@link world.bentobox.bentobox.managers.island.IslandGrid#getGrid()}.
154+
*/
155+
@Test
156+
public void testGetGrid() {
157+
assertNotNull(ig.getGrid());
158+
}
159+
160+
@Test
161+
public void testUpdateIslandCoordinatesKeepsSingleEntry() {
162+
// original at (100,100) range 20
163+
when(original.getMinX()).thenReturn(100);
164+
when(original.getMinZ()).thenReturn(100);
165+
when(original.getRange()).thenReturn(20);
166+
when(original.getUniqueId()).thenReturn("orig");
167+
168+
// updated has same id but moved to (300,300) range 20
169+
when(updated.getMinX()).thenReturn(300);
170+
when(updated.getMinZ()).thenReturn(300);
171+
when(updated.getRange()).thenReturn(20);
172+
when(updated.getUniqueId()).thenReturn("orig");
173+
174+
when(im.getIslandById("orig")).thenReturn(original);
175+
176+
// add original
177+
assertTrue(ig.addToGrid(original));
178+
assertEquals(1, ig.getSize());
179+
180+
// add updated (same id) -> should update, keep size 1
181+
assertTrue(ig.addToGrid(updated));
182+
assertEquals(1, ig.getSize());
183+
184+
// original location should no longer contain the island
185+
assertNull(ig.getIslandStringAt(110, 110));
186+
187+
// new location should contain the island
188+
assertEquals("orig", ig.getIslandStringAt(310, 310));
189+
}
190+
191+
@Test
192+
public void testAdjacentIslandsAllowedWhenEdgesTouch() {
193+
// island a covers x:[0,20) z:[0,20)
194+
when(a.getMinX()).thenReturn(0);
195+
when(a.getMinZ()).thenReturn(0);
196+
when(a.getRange()).thenReturn(10);
197+
when(a.getUniqueId()).thenReturn("a");
198+
199+
// island b starts exactly at x=20 (touching edge), z same
200+
when(b.getMinX()).thenReturn(20);
201+
when(b.getMinZ()).thenReturn(0);
202+
when(b.getRange()).thenReturn(10);
203+
when(b.getUniqueId()).thenReturn("b");
204+
205+
when(im.getIslandById("a")).thenReturn(a);
206+
when(im.getIslandById("b")).thenReturn(b);
207+
208+
assertTrue(ig.addToGrid(a));
209+
// touching edge should be allowed
210+
assertTrue(ig.addToGrid(b));
211+
212+
// verify both retrievable at representative coords
213+
assertEquals("a", ig.getIslandStringAt(10, 10));
214+
assertEquals("b", ig.getIslandStringAt(21, 10));
215+
}
216+
217+
@Test
218+
public void testLargeExistingIslandShouldBlockSmallIslandEvenIfMinXOutsideSubMapWindow() {
219+
// big island minX = 0, range = 1000
220+
when(big.getMinX()).thenReturn(0);
221+
when(big.getMinZ()).thenReturn(0);
222+
when(big.getRange()).thenReturn(1000);
223+
when(big.getUniqueId()).thenReturn("big");
224+
225+
// small island minX = 1500, range = 10 -> would overlap big
226+
when(small.getMinX()).thenReturn(1500);
227+
when(small.getMinZ()).thenReturn(10);
228+
when(small.getRange()).thenReturn(10);
229+
when(small.getUniqueId()).thenReturn("small");
230+
231+
when(im.getIslandById("big")).thenReturn(big);
232+
when(im.getIslandById("small")).thenReturn(small);
233+
234+
assertTrue(ig.addToGrid(big));
235+
236+
// Expected: adding small should be rejected because it lies inside big
237+
// If this test fails, it reveals the current subMap window is too small to find big.
238+
assertFalse("Small island overlaps big island; should have been rejected", ig.addToGrid(small));
239+
}
240+
241+
@Test
242+
public void testGetIslandStringAtWhenXEntryExistsButNoZEntryApplies() {
243+
// island exists at minX=100 minZ=100 range=10 (covers z [110,110))
244+
when(zIsland.getMinX()).thenReturn(100);
245+
when(zIsland.getMinZ()).thenReturn(100);
246+
when(zIsland.getRange()).thenReturn(10);
247+
when(zIsland.getUniqueId()).thenReturn("z");
248+
249+
when(im.getIslandById("z")).thenReturn(zIsland);
250+
251+
assertTrue(ig.addToGrid(zIsland));
252+
253+
// Query an x within island x-range but z is below any minZ -> should return null
254+
assertNull(ig.getIslandStringAt(110, 50));
255+
}
256+
257+
}

0 commit comments

Comments
 (0)