|
15 | 15 | import world.bentobox.bentobox.util.Util; |
16 | 16 |
|
17 | 17 | /** |
18 | | - * The default strategy for generating locations for island |
| 18 | + * The default strategy for generating locations for new islands. |
| 19 | + * This strategy finds the next available island spot by searching in an outward square spiral pattern |
| 20 | + * from the last known island location or the world's starting coordinates. |
| 21 | + * <p> |
| 22 | + * If you wish to create an alternative strategy, you must implement the {@link NewIslandLocationStrategy} interface. |
| 23 | + * Your implementation should be robust and consider the following: |
| 24 | + * <ul> |
| 25 | + * <li><b>Performance:</b> Finding a location can be a frequent operation. Your algorithm should be efficient.</li> |
| 26 | + * <li><b>Non-empty worlds:</b> The world may already contain structures. Your strategy should be able to |
| 27 | + * navigate around these or register them as "occupied" to avoid placing islands on top of them. This implementation |
| 28 | + * uses a tolerance limit ({@link #MAX_UNOWNED_ISLANDS}) to prevent infinite loops in heavily modified worlds.</li> |
| 29 | + * <li><b>Island distance:</b> Respect the island distance setting from the configuration to prevent islands from overlapping.</li> |
| 30 | + * <li><b>Concurrency:</b> While island creation is typically synchronized, consider any potential race conditions if your |
| 31 | + * strategy involves asynchronous operations.</li> |
| 32 | + * </ul> |
| 33 | + * The methods in this class, like {@link #isIsland(Location)}, can be useful helpers for custom implementations. |
| 34 | + * </p> |
| 35 | + * |
19 | 36 | * @author tastybento, leonardochaia |
20 | 37 | * @since 1.8.0 |
21 | | - * |
22 | 38 | */ |
23 | 39 | public class DefaultNewIslandLocationStrategy implements NewIslandLocationStrategy { |
24 | 40 |
|
25 | 41 | /** |
26 | | - * The amount times to tolerate island check returning blocks without known |
27 | | - * island. |
| 42 | + * The maximum number of times to tolerate finding non-BentoBox blocks in a potential island spot |
| 43 | + * before giving up. This acts as a safeguard against infinite loops when searching for a free location |
| 44 | + * in a world that was not generated by BentoBox or has been heavily modified. |
28 | 45 | */ |
29 | 46 | protected static final Integer MAX_UNOWNED_ISLANDS = 20; |
30 | 47 |
|
| 48 | + /** |
| 49 | + * Represents the result of checking a potential island location. |
| 50 | + */ |
31 | 51 | protected enum Result { |
32 | | - ISLAND_FOUND, BLOCKS_IN_AREA, FREE |
| 52 | + /** |
| 53 | + * A BentoBox island already exists at this location. |
| 54 | + */ |
| 55 | + ISLAND_FOUND, |
| 56 | + /** |
| 57 | + * No BentoBox island exists, but there are other blocks in the area. |
| 58 | + * This spot is considered occupied. |
| 59 | + */ |
| 60 | + BLOCKS_IN_AREA, |
| 61 | + /** |
| 62 | + * The location is free and suitable for a new island. |
| 63 | + */ |
| 64 | + FREE |
33 | 65 | } |
34 | 66 |
|
35 | 67 | protected final BentoBox plugin = BentoBox.getInstance(); |
36 | 68 |
|
37 | 69 | @Override |
38 | 70 | public Location getNextLocation(World world) { |
| 71 | + // Get the last known island location from the database. |
39 | 72 | Location last = plugin.getIslands().getLast(world); |
40 | 73 | if (last == null) { |
| 74 | + // If no island has been created yet, start from the configured offset. |
41 | 75 | last = new Location(world, |
42 | 76 | (double) plugin.getIWM().getIslandXOffset(world) + plugin.getIWM().getIslandStartX(world), |
43 | 77 | plugin.getIWM().getIslandHeight(world), |
44 | 78 | (double) plugin.getIWM().getIslandZOffset(world) + plugin.getIWM().getIslandStartZ(world)); |
45 | 79 | } |
46 | | - // Find a free spot |
| 80 | + // Find a free spot by spiraling outwards. |
47 | 81 | Map<Result, Integer> result = new EnumMap<>(Result.class); |
48 | | - // Check center |
| 82 | + // Check the starting location. |
49 | 83 | Result r = isIsland(last); |
| 84 | + // Loop until a FREE spot is found or we hit the tolerance limit for unowned islands. |
50 | 85 | while (!r.equals(Result.FREE) && result.getOrDefault(Result.BLOCKS_IN_AREA, 0) < MAX_UNOWNED_ISLANDS) { |
| 86 | + // Move to the next location in the spiral. |
51 | 87 | nextGridLocation(last); |
| 88 | + // Count the result of the previous check. |
52 | 89 | result.put(r, result.getOrDefault(r, 0) + 1); |
| 90 | + // Check the new location. |
53 | 91 | r = isIsland(last); |
54 | 92 | } |
55 | 93 |
|
56 | 94 | if (!r.equals(Result.FREE)) { |
57 | | - // We could not find a free spot within the limit required. It's likely this |
58 | | - // world is not empty |
| 95 | + // We could not find a free spot within the required limit. |
| 96 | + // This likely means the world is not empty or has many existing structures. |
59 | 97 | plugin.logError("Could not find a free spot for islands! Is this world empty?"); |
60 | 98 | plugin.logError("Blocks around center locations: " + result.getOrDefault(Result.BLOCKS_IN_AREA, 0) + " max " |
61 | 99 | + MAX_UNOWNED_ISLANDS); |
62 | 100 | plugin.logError("Known islands: " + result.getOrDefault(Result.ISLAND_FOUND, 0) + " max unlimited."); |
63 | 101 | return null; |
64 | 102 | } |
| 103 | + // A free spot was found. Save it as the last location for the next search. |
65 | 104 | plugin.getIslands().setLast(last); |
66 | 105 | return last; |
67 | 106 | } |
68 | 107 |
|
69 | 108 | /** |
70 | | - * Checks if there is an island or blocks at this location |
| 109 | + * Checks if a given location is free for a new island. |
| 110 | + * It checks for existing BentoBox islands, islands pending deletion, and any other blocks in the area. |
71 | 111 | * |
72 | | - * @param location - the location |
73 | | - * @return Result enum indicated what was found or not found |
| 112 | + * @param location The center location of the potential island spot. |
| 113 | + * @return {@link Result} enum indicating what was found. |
74 | 114 | */ |
75 | 115 | protected Result isIsland(Location location) { |
76 | | - // Quick check |
| 116 | + // Quick check using the island grid cache. |
77 | 117 | if (plugin.getIslands().isIslandAt(location)) { |
78 | 118 | return Result.ISLAND_FOUND; |
79 | 119 | } |
80 | 120 |
|
81 | 121 | World world = location.getWorld(); |
82 | 122 |
|
83 | | - // Check 4 corners |
| 123 | + // Check the four corners of the island protection area to be more thorough. |
84 | 124 | int dist = plugin.getIWM().getIslandDistance(location.getWorld()); |
85 | 125 | Set<Location> locs = new HashSet<>(); |
86 | 126 | locs.add(location); |
87 | 127 |
|
| 128 | + // Define the corners of the island's bounding box. |
88 | 129 | locs.add(new Location(world, location.getX() - dist, 0, location.getZ() - dist)); |
89 | 130 | locs.add(new Location(world, location.getX() - dist, 0, location.getZ() + dist - 1)); |
90 | 131 | locs.add(new Location(world, location.getX() + dist - 1, 0, location.getZ() - dist)); |
91 | 132 | locs.add(new Location(world, location.getX() + dist - 1, 0, location.getZ() + dist - 1)); |
92 | 133 |
|
93 | 134 | boolean generated = false; |
94 | 135 | for (Location l : locs) { |
| 136 | + // Check if an island exists or is being deleted at any of the check points. |
95 | 137 | if (plugin.getIslands().getIslandAt(l).isPresent() || plugin.getIslandDeletionManager().inDeletion(l)) { |
96 | 138 | return Result.ISLAND_FOUND; |
97 | 139 | } |
| 140 | + // Check if the chunk is generated. An ungenerated chunk is considered free. |
98 | 141 | if (Util.isChunkGenerated(l)) generated = true; |
99 | 142 | } |
100 | | - // If chunk has not been generated yet, then it's not occupied |
| 143 | + // If no chunks in the area have been generated yet, then it's definitely not occupied. |
101 | 144 | if (!generated) { |
102 | 145 | return Result.FREE; |
103 | 146 | } |
104 | | - // Block check |
| 147 | + // If configured, check for any non-air/non-water blocks in the immediate vicinity of the center. |
| 148 | + // This is to detect pre-existing structures in imported worlds. |
105 | 149 | if (plugin.getIWM().isCheckForBlocks(world) |
106 | 150 | && !plugin.getIWM().isUseOwnGenerator(world) |
107 | 151 | && Arrays.stream(BlockFace.values()).anyMatch(bf -> |
108 | 152 | !location.getBlock().getRelative(bf).isEmpty() |
109 | 153 | && !location.getBlock().getRelative(bf).getType().equals(Material.WATER))) { |
110 | | - // Block found |
| 154 | + // A block was found. Create a temporary, reserved island object at this location |
| 155 | + // to mark it as occupied in the database and prevent it from being checked again. |
111 | 156 | plugin.getIslands().createIsland(location); |
112 | 157 | return Result.BLOCKS_IN_AREA; |
113 | 158 | } |
| 159 | + // The location is free. |
114 | 160 | return Result.FREE; |
115 | 161 | } |
116 | 162 |
|
117 | 163 | /** |
118 | | - * Finds the next free island spot based off the last known island Uses |
119 | | - * island_distance setting from the config file Builds up in a grid fashion |
| 164 | + * Calculates the next island location in an outward square spiral pattern. |
| 165 | + * This method modifies the provided {@link Location} object in-place. |
| 166 | + * The algorithm works by moving along the edges of increasingly larger squares |
| 167 | + * centered at the origin (0,0). The distance 'd' determines the step size. |
120 | 168 | * |
121 | | - * @param lastIsland - last island location |
122 | | - * @return Location of next free island |
| 169 | + * @param lastIsland The location of the last island, which will be modified to the next location. |
| 170 | + * @return The same Location object, now set to the next grid position. |
123 | 171 | */ |
124 | 172 | private Location nextGridLocation(final Location lastIsland) { |
125 | 173 | int x = lastIsland.getBlockX(); |
126 | 174 | int z = lastIsland.getBlockZ(); |
| 175 | + // The distance between island centers is twice the protection distance. |
127 | 176 | int d = plugin.getIWM().getIslandDistance(lastIsland.getWorld()) * 2; |
128 | 177 | if (x < z) { |
129 | 178 | if (-1 * x < z) { |
| 179 | + // Move right (positive X) |
130 | 180 | lastIsland.setX(lastIsland.getX() + d); |
131 | 181 | return lastIsland; |
132 | 182 | } |
| 183 | + // Move up (positive Z) |
133 | 184 | lastIsland.setZ(lastIsland.getZ() + d); |
134 | 185 | return lastIsland; |
135 | 186 | } |
136 | 187 | if (x > z) { |
137 | 188 | if (-1 * x >= z) { |
| 189 | + // Move left (negative X) |
138 | 190 | lastIsland.setX(lastIsland.getX() - d); |
139 | 191 | return lastIsland; |
140 | 192 | } |
| 193 | + // Move down (negative Z) |
141 | 194 | lastIsland.setZ(lastIsland.getZ() - d); |
142 | 195 | return lastIsland; |
143 | 196 | } |
| 197 | + // This condition handles the corner case where x == z. |
144 | 198 | if (x <= 0) { |
| 199 | + // Move up (positive Z) from the bottom-left corner. |
145 | 200 | lastIsland.setZ(lastIsland.getZ() + d); |
146 | 201 | return lastIsland; |
147 | 202 | } |
| 203 | + // Move down (negative Z) from the top-right corner. |
148 | 204 | lastIsland.setZ(lastIsland.getZ() - d); |
149 | 205 | return lastIsland; |
150 | 206 | } |
|
0 commit comments