New island checking improved to avoid infinite loop

If a world has the generator malfunctioning the server can crash if
there's no clear spot for an island. This limits the number of spots to
find an island to 10.
This commit is contained in:
tastybento 2018-07-05 17:25:21 -07:00
parent 66f3b5032d
commit 2b5664c7ff
7 changed files with 309 additions and 161 deletions

View File

@ -8,9 +8,11 @@ import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -156,7 +158,7 @@ public class FlatFileDatabaseHandler<T> extends AbstractDatabaseHandler<T> {
// Handle storage of maps. Check if this type is a Map
if (Map.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
// Note that we have no idea what type this is
List<Type> collectionTypes = Util.getCollectionParameterTypes(method);
List<Type> collectionTypes = getCollectionParameterTypes(method);
// collectionTypes should be 2 long
Type keyType = collectionTypes.get(0);
Type valueType = collectionTypes.get(1);
@ -178,7 +180,7 @@ public class FlatFileDatabaseHandler<T> extends AbstractDatabaseHandler<T> {
} else if (Set.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
// Loop through the collection resultset
// Note that we have no idea what type this is
List<Type> collectionTypes = Util.getCollectionParameterTypes(method);
List<Type> collectionTypes = getCollectionParameterTypes(method);
// collectionTypes should be only 1 long
Type setType = collectionTypes.get(0);
Set<Object> value = new HashSet<>();
@ -191,7 +193,7 @@ public class FlatFileDatabaseHandler<T> extends AbstractDatabaseHandler<T> {
} else if (List.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
// Loop through the collection resultset
// Note that we have no idea what type this is
List<Type> collectionTypes = Util.getCollectionParameterTypes(method);
List<Type> collectionTypes = getCollectionParameterTypes(method);
// collectionTypes should be only 1 long
Type setType = collectionTypes.get(0);
List<Object> value = new ArrayList<>();
@ -215,6 +217,29 @@ public class FlatFileDatabaseHandler<T> extends AbstractDatabaseHandler<T> {
return instance;
}
/**
* Get a list of parameter types for the collection argument in this method
* @param writeMethod - write method
* @return a list of parameter types for the collection argument in this method
*/
private List<Type> getCollectionParameterTypes(Method writeMethod) {
List<Type> result = new ArrayList<>();
// Get the return type
// This uses a trick to extract what the arguments are of the writeMethod of the field.
// In this way, we can deduce what type needs to be written at runtime.
Type[] genericParameterTypes = writeMethod.getGenericParameterTypes();
// There could be more than one argument, so step through them
for (Type genericParameterType : genericParameterTypes) {
// If the argument is a parameter, then do something - this should always be true if the parameter is a collection
if( genericParameterType instanceof ParameterizedType ) {
// Get the actual type arguments of the parameter
Type[] parameters = ((ParameterizedType)genericParameterType).getActualTypeArguments();
result.addAll(Arrays.asList(parameters));
}
}
return result;
}
/**
* Inserts T into the corresponding database-table
*

View File

@ -9,9 +9,9 @@ import org.bukkit.Material;
import us.tastybento.bskyblock.api.flags.Flag;
import us.tastybento.bskyblock.api.flags.Flag.Type;
import us.tastybento.bskyblock.api.flags.FlagBuilder;
import us.tastybento.bskyblock.api.flags.clicklisteners.IslandToggleClickListener;
import us.tastybento.bskyblock.api.flags.clicklisteners.WorldToggleClickListener;
import us.tastybento.bskyblock.api.flags.FlagBuilder;
import us.tastybento.bskyblock.listeners.flags.BlockInteractionListener;
import us.tastybento.bskyblock.listeners.flags.BreakBlocksListener;
import us.tastybento.bskyblock.listeners.flags.BreedingListener;

View File

@ -113,6 +113,10 @@ public class IslandsManager {
private BSBDatabase<Island> handler;
/**
* The last locations where an island were put.
* This is not stored persistently and resets when the server starts
*/
private Map<World,Location> last;
// Metrics data
private int metrics_createdcount = 0;
@ -580,59 +584,6 @@ public class IslandsManager {
this.spawn.put(spawn.getWorld(), spawn);
}
/**
* Checks if there is an island or blocks at this location
* @param location - the location
* @return true if island found
*/
public boolean isIsland(Location location){
if (location == null) {
return false;
}
location = getClosestIsland(location);
if (islandCache.getIslandAt(location) != null) {
return true;
}
if (!plugin.getSettings().isUseOwnGenerator()) {
// Block check
if (!location.getBlock().isEmpty() && !location.getBlock().isLiquid()) {
createIsland(location);
return true;
}
// Look around
for (int x = -5; x <= 5; x++) {
for (int y = 10; y <= 255; y++) {
for (int z = -5; z <= 5; z++) {
if (!location.getWorld().getBlockAt(x + location.getBlockX(), y, z + location.getBlockZ()).isEmpty()
&& !location.getWorld().getBlockAt(x + location.getBlockX(), y, z + location.getBlockZ()).isLiquid()) {
createIsland(location);
return true;
}
}
}
}
}
return false;
}
/**
* This returns the coordinate of where an island should be on the grid.
*
* @param location - the location location to query
* @return Location of closest island
*/
public Location getClosestIsland(Location location) {
int dist = plugin.getIWM().getIslandDistance(location.getWorld());
long x = Math.round((double) location.getBlockX() / dist) * dist + plugin.getIWM().getIslandXOffset(location.getWorld());
long z = Math.round((double) location.getBlockZ() / dist) * dist + plugin.getIWM().getIslandZOffset(location.getWorld());
long y = plugin.getIWM().getIslandHeight(location.getWorld());
if (location.getBlockX() == x && location.getBlockZ() == z) {
return location;
}
return new Location(location.getWorld(), x, y, z);
}
/**
* @param uniqueId - unique ID
* @return true if the player is the owner of their island, i.e., owner or team leader

View File

@ -1,6 +1,8 @@
package us.tastybento.bskyblock.managers.island;
import java.io.IOException;
import java.util.EnumMap;
import java.util.Map;
import org.bukkit.Location;
import org.bukkit.World;
@ -12,6 +14,7 @@ import us.tastybento.bskyblock.api.events.island.IslandEvent;
import us.tastybento.bskyblock.api.events.island.IslandEvent.Reason;
import us.tastybento.bskyblock.api.user.User;
import us.tastybento.bskyblock.database.objects.Island;
import us.tastybento.bskyblock.util.Util;
/**
* Create and paste a new island
@ -19,11 +22,18 @@ import us.tastybento.bskyblock.database.objects.Island;
*
*/
public class NewIsland {
private static final Integer MAX_UNOWNED_ISLANDS = 10;
private BSkyBlock plugin;
private Island island;
private final User user;
private final Reason reason;
private final World world;
private enum Result {
ISLAND_FOUND,
BLOCK_AT_CENTER,
BLOCKS_IN_AREA,
FREE
}
private NewIsland(Island oldIsland, User user, Reason reason, World world) {
super();
@ -100,6 +110,10 @@ public class NewIsland {
*/
public void newIsland() {
Location next = getNextIsland();
if (next == null) {
plugin.logError("Failed to make island - no unoccupied spot found");
return;
}
// Add to the grid
island = plugin.getIslands().createIsland(next, user.getUniqueId());
// Save the player so that if the server is reset weird things won't happen
@ -163,7 +177,7 @@ public class NewIsland {
/**
* Get the location of next free island spot
* @return Location of island spot
* @return Location of island spot or null if one cannot be found
*/
private Location getNextIsland() {
Location last = plugin.getIslands().getLast(world);
@ -171,13 +185,62 @@ public class NewIsland {
last = new Location(world, plugin.getIWM().getIslandXOffset(world) + plugin.getIWM().getIslandStartX(world),
plugin.getIWM().getIslandHeight(world), plugin.getIWM().getIslandZOffset(world) + plugin.getIWM().getIslandStartZ(world));
}
Location next = last.clone();
while (plugin.getIslands().isIsland(next)) {
next = nextGridLocation(next);
// Find a free spot
Map<Result, Integer> result = new EnumMap<>(Result.class);
Result r = isIsland(last);
while (!r.equals(Result.FREE)
&& (result.getOrDefault(Result.BLOCK_AT_CENTER, 0) < MAX_UNOWNED_ISLANDS
&& result.getOrDefault(Result.BLOCK_AT_CENTER, 0) < MAX_UNOWNED_ISLANDS)) {
last = nextGridLocation(last);
result.merge(r, 1, (k,v) -> v++);
r = isIsland(last);
}
return next;
if (!r.equals(Result.FREE)) {
// We could not find a free spot within the limit required. It's likely this world is not empty
plugin.logError("Could not find a free spot for islands! Is this world empty?");
plugin.logError("Blocks at center locations: " + result.getOrDefault(Result.BLOCK_AT_CENTER, 0) + " max " + MAX_UNOWNED_ISLANDS);
plugin.logError("Blocks around center locations: " + result.getOrDefault(Result.BLOCKS_IN_AREA, 0) + " max " + MAX_UNOWNED_ISLANDS);
plugin.logError("Known islands: " + result.getOrDefault(Result.ISLAND_FOUND, 0) + " max unlimited.");
return null;
}
plugin.getIslands().setLast(last);
return last;
}
/**
* Checks if there is an island or blocks at this location
* @param location - the location
* @return true if island found, null if blocks found, false if nothing found
*/
private Result isIsland(Location location){
location = Util.getClosestIsland(location);
if (plugin.getIslands().getIslandAt(location).isPresent()) {
return Result.ISLAND_FOUND;
}
if (!plugin.getSettings().isUseOwnGenerator()) {
// Block check
if (!location.getBlock().isEmpty() && !location.getBlock().isLiquid()) {
plugin.getIslands().createIsland(location);
return Result.BLOCK_AT_CENTER;
}
// Look around
for (int x = -5; x <= 5; x++) {
for (int y = 10; y < location.getWorld().getMaxHeight(); y++) {
for (int z = -5; z <= 5; z++) {
if (!location.getWorld().getBlockAt(x + location.getBlockX(), y, z + location.getBlockZ()).isEmpty()
&& !location.getWorld().getBlockAt(x + location.getBlockX(), y, z + location.getBlockZ()).isLiquid()) {
plugin.getIslands().createIsland(location);
return Result.BLOCKS_IN_AREA;
}
}
}
}
}
return Result.FREE;
}
/**
* Finds the next free island spot based off the last known island Uses
* island_distance setting from the config file Builds up in a grid fashion

View File

@ -1,10 +1,6 @@
package us.tastybento.bskyblock.util;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@ -46,12 +42,29 @@ public class Util {
*/
public static String getServerVersion() {
if (serverVersion == null) {
String serverPackageName = plugin.getServer().getClass().getPackage().getName();
String serverPackageName = Bukkit.getServer().getClass().getPackage().getName();
serverVersion = serverPackageName.substring(serverPackageName.lastIndexOf('.') + 1);
}
return serverVersion;
}
/**
* This returns the coordinate of where an island should be on the grid.
*
* @param location - the location location to query
* @return Location of closest island
*/
public static Location getClosestIsland(Location location) {
int dist = plugin.getIWM().getIslandDistance(location.getWorld());
long x = Math.round((double) location.getBlockX() / dist) * dist + plugin.getIWM().getIslandXOffset(location.getWorld());
long z = Math.round((double) location.getBlockZ() / dist) * dist + plugin.getIWM().getIslandZOffset(location.getWorld());
if (location.getBlockX() == x && location.getBlockZ() == z) {
return location;
}
int y = plugin.getIWM().getIslandHeight(location.getWorld());
return new Location(location.getWorld(), x, y, z);
}
/**
* Converts a serialized location to a Location. Returns null if string is
* empty
@ -98,29 +111,6 @@ public class Util {
return String.valueOf(Math.round(num * 100D) / 100D);
}
/**
* Get a list of parameter types for the collection argument in this method
* @param writeMethod - write method
* @return a list of parameter types for the collection argument in this method
*/
public static List<Type> getCollectionParameterTypes(Method writeMethod) {
List<Type> result = new ArrayList<>();
// Get the return type
// This uses a trick to extract what the arguments are of the writeMethod of the field.
// In this way, we can deduce what type needs to be written at runtime.
Type[] genericParameterTypes = writeMethod.getGenericParameterTypes();
// There could be more than one argument, so step through them
for (Type genericParameterType : genericParameterTypes) {
// If the argument is a parameter, then do something - this should always be true if the parameter is a collection
if( genericParameterType instanceof ParameterizedType ) {
// Get the actual type arguments of the parameter
Type[] parameters = ((ParameterizedType)genericParameterType).getActualTypeArguments();
result.addAll(Arrays.asList(parameters));
}
}
return result;
}
/**
* Converts a name like IRON_INGOT into Iron Ingot to improve readability
*

View File

@ -652,77 +652,6 @@ public class IslandsManagerTest {
assertTrue(im.isAtSpawn(location));
}
/**
* Test method for {@link us.tastybento.bskyblock.managers.IslandsManager#isIsland(org.bukkit.Location)}.
* @throws Exception
*/
@Test
public void testIsIsland() throws Exception {
// Mock island cache
IslandCache ic = mock(IslandCache.class);
Island is = mock(Island.class);
when(ic.getIslandAt(Mockito.any())).thenReturn(is);
PowerMockito.whenNew(IslandCache.class).withAnyArguments().thenReturn(ic);
// In world
IslandsManager im = new IslandsManager(plugin);
assertFalse(im.isIsland(null));
im.createIsland(location);
assertTrue(im.isIsland(location));
// No island in cache
when(ic.getIslandAt(Mockito.any())).thenReturn(null);
// Use own generator
when(s.isUseOwnGenerator()).thenReturn(true);
assertFalse(im.isIsland(location));
when(s.isUseOwnGenerator()).thenReturn(false);
// Location
when(location.getWorld()).thenReturn(world);
when(location.getBlockX()).thenReturn(10);
when(location.getBlockZ()).thenReturn(10);
when(location.getBlock()).thenReturn(space1);
PowerMockito.whenNew(Location.class).withAnyArguments().thenReturn(location);
when(location.getBlock()).thenReturn(space1);
when(world.getBlockAt(Mockito.anyInt(), Mockito.anyInt(), Mockito.anyInt())).thenReturn(ground);
// Liquid
when(space1.isEmpty()).thenReturn(false);
when(space1.isLiquid()).thenReturn(true);
assertTrue(im.isIsland(location));
// Empty space
when(space1.isEmpty()).thenReturn(true);
when(space1.isLiquid()).thenReturn(false);
assertTrue(im.isIsland(location));
}
/**
* Test method for {@link us.tastybento.bskyblock.managers.IslandsManager#getClosestIsland(org.bukkit.Location)}.
* @throws Exception
*/
@Test
public void testGetClosestIsland() throws Exception {
when(iwm.getIslandDistance(world)).thenReturn(100);
when(iwm.getIslandXOffset(world)).thenReturn(0);
when(iwm.getIslandZOffset(world)).thenReturn(0);
when(iwm.getIslandHeight(world)).thenReturn(120);
IslandsManager im = new IslandsManager(plugin);
when(location.getBlockX()).thenReturn(456);
when(location.getBlockZ()).thenReturn(456);
Location l = im.getClosestIsland(location);
assertEquals(500, l.getBlockX());
assertEquals(500, l.getBlockZ());
}
/**
* Test method for {@link us.tastybento.bskyblock.managers.IslandsManager#isOwner(java.util.UUID)}.
* @throws Exception

View File

@ -0,0 +1,190 @@
/**
*
*/
package us.tastybento.bskyblock.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.World;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import us.tastybento.bskyblock.BSkyBlock;
import us.tastybento.bskyblock.managers.IslandWorldManager;
/**
* @author tastybento
*
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest( { Bukkit.class })
public class UtilTest {
private BSkyBlock plugin;
private World world;
private IslandWorldManager iwm;
private Location location;
private Server server;
/**
* @throws java.lang.Exception
*/
@Before
public void setUp() throws Exception {
// Set up plugin
plugin = mock(BSkyBlock.class);
Util.setPlugin(plugin);
// World
world = mock(World.class);
when(world.getName()).thenReturn("world_name");
// Worlds
iwm = mock(IslandWorldManager.class);
when(plugin.getIWM()).thenReturn(iwm);
location = mock(Location.class);
when(location.getWorld()).thenReturn(world);
when(location.getX()).thenReturn(500D);
when(location.getY()).thenReturn(600D);
when(location.getZ()).thenReturn(700D);
when(location.getYaw()).thenReturn(10F);
when(location.getPitch()).thenReturn(20F);
PowerMockito.mockStatic(Bukkit.class);
server = mock(Server.class);
when(Bukkit.getServer()).thenReturn(server);
when(server.getWorld(Mockito.anyString())).thenReturn(world);
}
/**
* Test method for {@link us.tastybento.bskyblock.util.Util#getServerVersion()}.
*/
@Test
public void testGetServerVersion() {
assertEquals("bukkit",Util.getServerVersion());
}
/**
* Test method for {@link us.tastybento.bskyblock.util.Util#getClosestIsland(org.bukkit.Location)}.
*/
@Test
public void testGetClosestIsland() throws Exception {
Util.setPlugin(plugin);
when(plugin.getIWM()).thenReturn(iwm);
when(iwm.getIslandDistance(world)).thenReturn(100);
when(iwm.getIslandXOffset(world)).thenReturn(0);
when(iwm.getIslandZOffset(world)).thenReturn(0);
when(iwm.getIslandHeight(world)).thenReturn(120);
when(location.getBlockX()).thenReturn(456);
when(location.getBlockZ()).thenReturn(456);
Location l = Util.getClosestIsland(location);
assertEquals(500, l.getBlockX());
assertEquals(500, l.getBlockZ());
}
/**
* Test method for {@link us.tastybento.bskyblock.util.Util#getLocationString(java.lang.String)}.
*/
@Test
public void testGetLocationString() {
assertNull(Util.getLocationString(null));
assertNull(Util.getLocationString(""));
assertNull(Util.getLocationString(" "));
Location result = Util.getLocationString("world_name:500.0:600.0:700.0:1092616192:1101004800");
assertEquals(world, result.getWorld());
assertTrue(result.getX() == 500D);
assertTrue(result.getY() == 600D);
assertTrue(result.getZ() == 700D);
assertTrue(result.getYaw() == 10F);
assertTrue(result.getPitch() == 20F);
}
/**
* Test method for {@link us.tastybento.bskyblock.util.Util#getStringLocation(org.bukkit.Location)}.
*/
@Test
public void testGetStringLocation() {
assertEquals("", Util.getStringLocation(null));
when(location.getWorld()).thenReturn(null);
assertEquals("", Util.getStringLocation(location));
when(location.getWorld()).thenReturn(world);
assertEquals("world_name:500.0:600.0:700.0:1092616192:1101004800", Util.getStringLocation(location));
}
/**
* Test method for {@link us.tastybento.bskyblock.util.Util#prettifyText(java.lang.String)}.
*/
@Test
public void testPrettifyText() {
assertEquals("Hello There This Is A Test", Util.prettifyText("HELLO_THERE_THIS_IS_A_TEST"));
assertEquals("All caps test", Util.prettifyText("ALL CAPS TEST"));
assertEquals("First capital letter", Util.prettifyText("first capital letter"));
}
/**
* Test method for {@link us.tastybento.bskyblock.util.Util#getOnlinePlayerList(us.tastybento.bskyblock.api.user.User)}.
*/
@Test
public void testGetOnlinePlayerList() {
//fail("Not yet implemented"); // TODO
}
/**
* Test method for {@link us.tastybento.bskyblock.util.Util#tabLimit(java.util.List, java.lang.String)}.
*/
@Test
public void testTabLimit() {
//fail("Not yet implemented"); // TODO
}
/**
* Test method for {@link us.tastybento.bskyblock.util.Util#getPermValue(org.bukkit.entity.Player, java.lang.String, int)}.
*/
@Test
public void testGetPermValue() {
//fail("Not yet implemented"); // TODO
}
/**
* Test method for {@link us.tastybento.bskyblock.util.Util#xyz(org.bukkit.util.Vector)}.
*/
@Test
public void testXyz() {
//fail("Not yet implemented"); // TODO
}
/**
* Test method for {@link us.tastybento.bskyblock.util.Util#sameWorld(org.bukkit.World, org.bukkit.World)}.
*/
@Test
public void testSameWorld() {
//fail("Not yet implemented"); // TODO
}
/**
* Test method for {@link us.tastybento.bskyblock.util.Util#getWorld(org.bukkit.World)}.
*/
@Test
public void testGetWorld() {
//fail("Not yet implemented"); // TODO
}
/**
* Test method for {@link us.tastybento.bskyblock.util.Util#blockFaceToFloat(org.bukkit.block.BlockFace)}.
*/
@Test
public void testBlockFaceToFloat() {
//fail("Not yet implemented"); // TODO
}
}