From 581c627124953acf7bea4fef792bdb97e89ead30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lobo=20Metal=C3=BArgico?= <43734867+LoboMetalurgico@users.noreply.github.com> Date: Sun, 15 Oct 2023 11:17:12 -0300 Subject: [PATCH] (fix): gitbug --- .../com/gamemods/minecity/MineCityConfig.java | 120 +- .../datasource/sql/SQLCityStorage.java | 2958 ++++++++--------- .../datasource/sql/SQLConnection.java | 184 +- .../minecity/datasource/sql/SQLIsland.java | 246 +- .../minecity/datasource/sql/SQLSource.java | 1722 +++++----- .../minecity/structure/ChunkOwner.java | 10 +- .../com/gamemods/minecity/structure/City.java | 1524 ++++----- .../minecity/structure/ClaimedChunk.java | 390 +-- .../minecity/structure/Inconsistency.java | 914 ++--- .../gamemods/minecity/structure/Island.java | 340 +- .../gamemods/minecity/structure/Nature.java | 224 +- .../com/gamemods/minecity/MineCityTest.java | 138 +- .../gamemods/minecity/structure/CityTest.java | 626 ++-- gradlew | 0 14 files changed, 4698 insertions(+), 4698 deletions(-) mode change 100755 => 100644 gradlew diff --git a/Core/src/main/java/br/com/gamemods/minecity/MineCityConfig.java b/Core/src/main/java/br/com/gamemods/minecity/MineCityConfig.java index af81eb14..7d62ca2d 100644 --- a/Core/src/main/java/br/com/gamemods/minecity/MineCityConfig.java +++ b/Core/src/main/java/br/com/gamemods/minecity/MineCityConfig.java @@ -1,60 +1,60 @@ -package br.com.gamemods.minecity; - -import br.com.gamemods.minecity.api.permission.SimpleFlagHolder; -import br.com.gamemods.minecity.economy.Tax; - -import java.util.Locale; - -public final class MineCityConfig implements Cloneable -{ - public String dbUrl = "jdbc:mysql://localhost/minecity?autoReconnect=true"; - public String dbUser; - public byte[] dbPass; - public Locale locale; - public SimpleFlagHolder defaultNatureFlags = new SimpleFlagHolder(); - public SimpleFlagHolder defaultCityFlags = new SimpleFlagHolder(); - public SimpleFlagHolder defaultPlotFlags = new SimpleFlagHolder(); - public SimpleFlagHolder defaultReserveFlags = new SimpleFlagHolder(); - public boolean defaultNatureDisableCities; - public boolean useTitle = true; - public String economy = "none"; - public String permission = "none"; - public Costs costs = new Costs(); - public Limits limits = new Limits(); - - @Override - public MineCityConfig clone() - { - try - { - MineCityConfig clone = (MineCityConfig) super.clone(); - clone.dbPass = dbPass.clone(); - return clone; - } - catch(CloneNotSupportedException e) - { - throw new UnsupportedOperationException(e); - } - } - - public static class Costs - { - public double cityCreation = 1000; - public double islandCreation = 500; - public double claim = 25; - public Tax cityTax = new Tax(100, 0.03); - public Tax cityTaxApplied = new Tax(-1, 0.05); - public Tax plotTaxApplied = new Tax(0, 0); - public double cityChangeSpawn = 50; - public double plotChangeSpawn = 50; - public double goToCity = 5; - public double goToPlot = 15; - } - - public static class Limits - { - public int cities = -1; - public int claims = -1; - public int islands = -1; - } -} +package br.com.gamemods.minecity; + +import br.com.gamemods.minecity.api.permission.SimpleFlagHolder; +import br.com.gamemods.minecity.economy.Tax; + +import java.util.Locale; + +public final class MineCityConfig implements Cloneable +{ + public String dbUrl = "jdbc:mysql://localhost/minecity?autoReconnect=true"; + public String dbUser; + public byte[] dbPass; + public Locale locale; + public SimpleFlagHolder defaultNatureFlags = new SimpleFlagHolder(); + public SimpleFlagHolder defaultCityFlags = new SimpleFlagHolder(); + public SimpleFlagHolder defaultPlotFlags = new SimpleFlagHolder(); + public SimpleFlagHolder defaultReserveFlags = new SimpleFlagHolder(); + public boolean defaultNatureDisableCities; + public boolean useTitle = true; + public String economy = "none"; + public String permission = "none"; + public Costs costs = new Costs(); + public Limits limits = new Limits(); + + @Override + public MineCityConfig clone() + { + try + { + MineCityConfig clone = (MineCityConfig) super.clone(); + clone.dbPass = dbPass.clone(); + return clone; + } + catch(CloneNotSupportedException e) + { + throw new UnsupportedOperationException(e); + } + } + + public static class Costs + { + public double cityCreation = 1000; + public double islandCreation = 500; + public double claim = 25; + public Tax cityTax = new Tax(100, 0.03); + public Tax cityTaxApplied = new Tax(-1, 0.05); + public Tax plotTaxApplied = new Tax(0, 0); + public double cityChangeSpawn = 50; + public double plotChangeSpawn = 50; + public double goToCity = 5; + public double goToPlot = 15; + } + + public static class Limits + { + public int cities = -1; + public int claims = -1; + public int islands = -1; + } +} diff --git a/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLCityStorage.java b/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLCityStorage.java index 36b6161e..887556b3 100644 --- a/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLCityStorage.java +++ b/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLCityStorage.java @@ -1,1479 +1,1479 @@ -package br.com.gamemods.minecity.datasource.sql; - -import br.com.gamemods.minecity.api.PlayerID; -import br.com.gamemods.minecity.api.Slow; -import br.com.gamemods.minecity.api.command.Message; -import br.com.gamemods.minecity.api.permission.EntityID; -import br.com.gamemods.minecity.api.permission.Group; -import br.com.gamemods.minecity.api.permission.Identity; -import br.com.gamemods.minecity.api.permission.OptionalPlayer; -import br.com.gamemods.minecity.api.shape.Shape; -import br.com.gamemods.minecity.api.world.BlockPos; -import br.com.gamemods.minecity.api.world.ChunkPos; -import br.com.gamemods.minecity.api.world.MinecraftEntity; -import br.com.gamemods.minecity.api.world.WorldDim; -import br.com.gamemods.minecity.datasource.api.DataSourceException; -import br.com.gamemods.minecity.datasource.api.ICityStorage; -import br.com.gamemods.minecity.datasource.api.unchecked.DBConsumer; -import br.com.gamemods.minecity.economy.Tax; -import br.com.gamemods.minecity.structure.City; -import br.com.gamemods.minecity.structure.Island; -import br.com.gamemods.minecity.structure.IslandArea; -import br.com.gamemods.minecity.structure.Plot; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.sql.*; -import java.util.*; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.stream.Collectors; - -public class SQLCityStorage implements ICityStorage -{ - @NotNull - private final SQLSource source; - - @NotNull - private final SQLConnection connection; - - @NotNull - private final SQLPermStorage permStorage; - - SQLCityStorage(@NotNull SQLSource source, @NotNull SQLConnection connection, @NotNull SQLPermStorage permStorage) - { - this.source = source; - this.connection = connection; - this.permStorage = permStorage; - } - - @Override - public void deleteCity(@NotNull City city) throws DataSourceException - { - int cityId = city.getId(); - if(cityId <= 0) - throw new IllegalStateException("cityId = "+cityId); - - try(Connection transaction = connection.transaction()) - { - try(PreparedStatement pst = transaction.prepareStatement( - "DELETE FROM minecity_city WHERE city_id=?" - )) - { - pst.setInt(1, cityId); - source.executeUpdate(pst, 1); - transaction.commit(); - source.cityNames.remove(city.getName()); - source.groupNames.remove(city.getIdentityName()); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @Override - public void setOwner(@NotNull City city, @NotNull OptionalPlayer owner) throws DataSourceException, IllegalStateException - { - if(city.owner().equals(owner)) - return; - - int cityId = city.getId(); - if(cityId <= 0) throw new IllegalStateException("The city is not registered"); - - try - { - Connection connection = this.connection.connect(); - int ownerId = source.playerId(connection, owner); - - try(PreparedStatement pst = connection.prepareStatement( - "UPDATE `minecity_city` SET `owner`=? WHERE `city_id`=?" - )) - { - source.setNullableInt(pst, 1, ownerId); - pst.setInt(2, cityId); - if(pst.executeUpdate() <= 0) - throw new DataSourceException("Tried to change the owner of "+ cityId+" from "+city.owner()+" to "+owner+" but nothing changed"); - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @NotNull - @Override - public Collection reserve(@NotNull IslandArea reserve) throws DataSourceException - { - SQLIsland sqlIsland = (SQLIsland) reserve.island; - StringBuilder sbx = new StringBuilder(), sbz = new StringBuilder(); - Runnable build = ()-> - { - sbx.setLength(0); sbz.setLength(0); - reserve.claims().forEach(c->{ sbx.append(c.x).append(','); sbz.append(c.z).append(','); }); - if(sbx.length() > 0) - sbx.setLength(sbx.length() - 1); - if(sbz.length() > 0) - sbz.setLength(sbz.length() - 1); - }; - build.run(); - - try(Connection transaction = connection.transaction(); Statement stm = transaction.createStatement()) - { - try - { - int worldId = source.worldId(transaction, sqlIsland.world); - Collection chunks = new HashSet<>(); - String x = sbx.toString(), z = sbz.toString(); - - if(!x.isEmpty()) - { - - ResultSet results = stm.executeQuery("SELECT x, z FROM minecity_chunks WHERE world_id="+worldId+" AND island_id!="+sqlIsland.id+" AND x IN("+x+") AND z IN("+z+");"); - while(results.next()) - reserve.setClaimed(results.getInt(1), results.getInt(2), false); - results.close(); - - results = stm.executeQuery("SELECT x,z FROM minecity_chunks WHERE island_id="+sqlIsland.id+" LIMIT 1"); - results.next(); - ChunkPos pos = new ChunkPos(sqlIsland.world, results.getInt(1), results.getInt(2)); - results.close(); - - Set valid = reserve.contiguous(pos); - reserve.claims().filter(c-> !valid.contains(c)).forEach(c-> reserve.setClaimed(c, false)); - build.run(); - x = sbx.toString(); z = sbz.toString(); - } - - - String deleteCond = "island_id="+sqlIsland.id+" AND reserve=1"; - if(!x.isEmpty()) - deleteCond+= " AND x NOT IN ("+x+") AND z NOT IN ("+z+")"; - - ResultSet results = stm.executeQuery("SELECT x,z FROM minecity_chunks WHERE " + deleteCond+";"); - while(results.next()) - chunks.add(new ChunkPos(sqlIsland.world, results.getInt(1), results.getInt(2))); - results.close(); - - stm.executeUpdate("DELETE FROM minecity_chunks WHERE "+deleteCond+";"); - - if(!x.isEmpty()) - { - results = stm.executeQuery("SELECT x, z FROM minecity_chunks WHERE world_id="+worldId+" AND x IN("+x+") AND z IN("+z+");"); - while(results.next()) - reserve.setClaimed(results.getInt(1), results.getInt(2), false); - results.close(); - - sbx.setLength(0); - List insert = reserve.claims().collect(Collectors.toList()); - chunks.addAll(insert); - insert.forEach(c-> - sbx.append('(').append(worldId).append(',').append(c.x).append(',').append(c.z).append(',') - .append(sqlIsland.id).append(",1),") - ); - if(sbx.length() > 0) - { - sbx.setLength(sbx.length()-1); - stm.executeUpdate("INSERT INTO minecity_chunks(world_id,x,z,island_id,reserve) VALUES "+sbx+";"); - } - } - transaction.commit(); - return chunks; - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - private void disclaim(Connection transaction, ChunkPos chunk, int islandId, boolean enforce) throws DataSourceException, SQLException - { - try(PreparedStatement pst = transaction.prepareStatement( - "DELETE FROM `minecity_chunks` WHERE `world_id`=? AND `x`=? AND `z`=? AND `island_id`=?" - )) - { - pst.setInt(1, source.worldId(transaction, chunk.world)); - pst.setInt(2, chunk.x); - pst.setInt(3, chunk.z); - pst.setInt(4, islandId); - if(enforce) - source.executeUpdate(pst, 1); - else - pst.executeUpdate(); - } - } - - @Slow - @Override - public void deleteIsland(@NotNull Island island) throws DataSourceException, IllegalArgumentException - { - SQLIsland sqlIsland = (SQLIsland) island; - if(sqlIsland.chunkCount == 0) throw new IllegalArgumentException(); - - try(Connection transaction = this.connection.transaction()) - { - try(PreparedStatement pst = transaction.prepareStatement( - "DELETE FROM `minecity_islands` WHERE `island_id`=?" - )) - { - pst.setInt(1, sqlIsland.id); - int i = pst.executeUpdate(); - if(i != 1) - throw new DataSourceException("Expecting 1 change, "+i+" changed"); - - transaction.commit(); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - - sqlIsland.chunkCount = sqlIsland.maxX = sqlIsland.minX = sqlIsland.maxZ = sqlIsland.minZ = 0; - source.mineCity.reloadChunksUnchecked(c-> c.getIsland().filter(island::equals).isPresent()); - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - private void updateCount(Connection transaction, SQLIsland sqlIsland) throws SQLException - { - try(PreparedStatement pst = transaction.prepareStatement( - "SELECT MIN(x), MAX(x), MIN(z), MAX(z), COUNT(*) FROM minecity_chunks WHERE island_id=? AND reserve=0" - )) - { - pst.setInt(1, sqlIsland.id); - ResultSet result = pst.executeQuery(); - result.next(); - sqlIsland.minX = result.getInt(1); - sqlIsland.maxX = result.getInt(2); - sqlIsland.minZ = result.getInt(3); - sqlIsland.maxZ = result.getInt(4); - sqlIsland.chunkCount = result.getInt(5); - } - } - - @Slow - @Override - public void disclaim(@NotNull ChunkPos chunk, @NotNull Island island) - throws DataSourceException, IllegalArgumentException - { - SQLIsland sqlIsland = (SQLIsland) island; - if(sqlIsland.chunkCount == 0) throw new IllegalArgumentException(); - - try(Connection transaction = connection.transaction()) - { - try - { - disclaim(transaction, chunk, sqlIsland.id, true); - transaction.commit(); - - updateCount(transaction, sqlIsland); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - - try - { - source.mineCity.reloadChunk(chunk); - } - catch(Exception e) - { - e.printStackTrace(); - } - } - - @Slow - @NotNull - @Override - @SuppressWarnings("OptionalGetWithoutIsPresent") - public Collection disclaim(@NotNull ChunkPos chunk, @NotNull Island island, @NotNull Set> groups) - throws DataSourceException, IllegalStateException, NoSuchElementException, ClassCastException, IllegalArgumentException - { - SQLIsland sqlIsland = (SQLIsland) island; - if(sqlIsland.chunkCount == 0) throw new IllegalArgumentException(); - int cityId = sqlIsland.city.getId(); - - Set mainGroup = groups.stream().max((a,b)-> a.size()-b.size() ).get(); - groups = groups.stream().filter(s-> s != mainGroup).collect(Collectors.toSet()); - - try(Connection transaction = connection.transaction(); Statement stm = transaction.createStatement()) - { - try - { - disclaim(transaction, chunk, sqlIsland.id, true); - int worldId = source.worldId(transaction, sqlIsland.world); - - - stm.executeUpdate("DELETE FROM minecity_chunks WHERE island_id="+sqlIsland.id+" AND reserve=1;"); - - List islands = new ArrayList<>(groups.size()); - int[] expected = new int[groups.size()]; int i = 0; - for(Set group : groups) - { - int islandId = source.createIsland(transaction, cityId, sqlIsland.world); - StringBuilder sb = new StringBuilder(); - int minX = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE; - for(ChunkPos pos : group) - { - minX = Math.min(minX, pos.x); - minZ = Math.min(minZ, pos.z); - maxX = Math.max(maxX, pos.x); - maxZ = Math.max(maxZ, pos.z); - sb.append("(x=").append(pos.x).append(" AND z=").append(pos.z).append(") OR"); - } - - sb.setLength(sb.length() - 3); - - stm.addBatch("UPDATE minecity_chunks SET island_id="+islandId+" " + - "WHERE world_id="+worldId+" AND island_id="+sqlIsland.id+" AND reserve=0 AND ("+sb+");" - ); - - SQLIsland newIsland = new SQLIsland(sqlIsland.city, this, permStorage, islandId, minX, maxX, minZ, maxZ, group.size(), sqlIsland.world, Collections.emptySet()); - expected[i++] = newIsland.chunkCount; - islands.add(newIsland); - } - - int[] result = stm.executeBatch(); - if(!Arrays.equals(expected, result)) - { - throw new DataSourceException("Unexpected result after reclaiming to new islands. " + - "Expected: "+Arrays.toString(expected)+" Result: "+Arrays.toString(result)); - } - - Collection plots = sqlIsland.getPlots(); - List afterCommit = new ArrayList<>(plots.size()); - if(!plots.isEmpty()) - { - try(PreparedStatement select = transaction.prepareStatement( - "SELECT island_id FROM minecity_chunks WHERE world_id=? AND x=? AND z=? AND reserve=0" - ); PreparedStatement update = transaction.prepareStatement( - "UPDATE minecity_plots SET island_id=? WHERE plot_id=?" - )) - { - for(Plot plot: plots) - { - ChunkPos spawn = plot.getSpawn().getChunk(); - select.setInt(1, worldId); - select.setInt(2, spawn.x); - select.setInt(3, spawn.z); - ResultSet selectRes = select.executeQuery(); - if(!selectRes.next()) - throw new DataSourceException("The plot id:"+plot.id+" name:"+plot.getIdentityName()+" is not in an island!"); - int newIslandId = selectRes.getInt(1); - selectRes.close(); - - if(plot.getIsland().id == newIslandId) - continue; - - SQLIsland newIsland = null; - for(Island possible : islands) - { - if(possible.id == newIslandId) - { - newIsland = (SQLIsland) possible; - break; - } - } - - if(newIsland == null) - throw new DataSourceException("The plot id:"+plot.id+" name:"+plot.getIdentityName()+ - " would be moved to the island:"+newIslandId+" but that island wasn't created by " + - "this disclaim!"); - - update.setInt(1, newIslandId); - update.setInt(2, plot.id); - source.executeUpdate(update, 1); - - SQLIsland finalNewIsland = newIsland; - afterCommit.add(()-> sqlIsland.relocate(plot, finalNewIsland)); - } - } - } - - updateCount(transaction, sqlIsland); - transaction.commit(); - afterCommit.forEach(Runnable::run); - - return islands; - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Override - public double invested(@NotNull City city, double value) throws DataSourceException - { - try(Connection transaction = connection.transaction()) - { - try - { - try(PreparedStatement pst = transaction.prepareStatement( - "UPDATE minecity_city SET `investment`=`investment`+? WHERE city_id=?" - )) - { - pst.setDouble(1, value); - pst.setInt(2, city.getId()); - source.executeUpdate(pst, 1); - } - - double investment; - try(PreparedStatement pst = transaction.prepareStatement( - "SELECT `investment` FROM `minecity_city` WHERE `city_id`=?" - )) - { - pst.setInt(1, city.getId()); - ResultSet result = pst.executeQuery(); - result.next(); - investment = result.getDouble(1); - } - - transaction.commit(); - return investment; - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Override - public double invested(@NotNull Plot plot, double value) throws DataSourceException - { - try(Connection transaction = connection.transaction()) - { - try - { - try(PreparedStatement pst = transaction.prepareStatement( - "UPDATE minecity_plots SET `investment`=`investment`+? WHERE plot_id=?" - )) - { - pst.setDouble(1, value); - pst.setInt(2, plot.id); - source.executeUpdate(pst, 1); - } - - double investment; - try(PreparedStatement pst = transaction.prepareStatement( - "SELECT `investment` FROM `minecity_plots` WHERE `plot_id`=?" - )) - { - pst.setInt(1, plot.id); - ResultSet result = pst.executeQuery(); - result.next(); - investment = result.getDouble(1); - } - - transaction.commit(); - return investment; - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Override - public void setInvestment(@NotNull Plot plot, double investment) throws DataSourceException - { - try(Connection transaction = connection.transaction()) - { - try - { - try(PreparedStatement pst = transaction.prepareStatement( - "UPDATE minecity_plots SET `investment`=? WHERE plot_id=?" - )) - { - pst.setDouble(1, investment); - pst.setInt(2, plot.id); - source.executeUpdate(pst, 1); - } - - transaction.commit(); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Override - public void setPrice(@NotNull Plot plot, double price) throws DataSourceException - { - try(Connection transaction = connection.transaction()) - { - try - { - try(PreparedStatement pst = transaction.prepareStatement( - "UPDATE minecity_plots SET `price`=? WHERE plot_id=?" - )) - { - pst.setDouble(1, price); - pst.setInt(2, plot.id); - source.executeUpdate(pst, 1); - } - - transaction.commit(); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Override - public void setPrice(@NotNull City city, double price) throws DataSourceException - { - try(Connection transaction = connection.transaction()) - { - try - { - try(PreparedStatement pst = transaction.prepareStatement( - "UPDATE minecity_city SET `price`=? WHERE city_id=?" - )) - { - pst.setDouble(1, price); - pst.setInt(2, city.getId()); - source.executeUpdate(pst, 1); - } - - transaction.commit(); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @Override - public void setName(@NotNull City city, @NotNull String identity, @NotNull String name) throws DataSourceException - { - try(Connection transaction = connection.transaction()) - { - String previous = city.getName(); - try - { - if(identity.equals(city.getIdentityName())) - try(PreparedStatement pst = transaction.prepareStatement( - "UPDATE minecity_city SET `display_name`=? WHERE city_id=?" - )) - { - pst.setString(1, name); - pst.setInt(2, city.getId()); - source.executeUpdate(pst, 1); - } - else - try(PreparedStatement pst = transaction.prepareStatement( - "UPDATE minecity_city SET `name`=?, display_name=? WHERE city_id=?" - )) - { - pst.setString(1, identity); - pst.setString(2, name); - pst.setInt(3, city.getId()); - source.executeUpdate(pst, 1); - } - - transaction.commit(); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - source.cityNames.remove(previous); - source.cityNames.add(name); - Set groups = source.groupNames.remove(identity); - if(groups != null) - source.groupNames.put(city.getIdentityName(), groups); - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @NotNull - @Override - public Group createGroup(@NotNull City city, @NotNull String id, @NotNull String name) throws DataSourceException - { - try(Connection transaction = connection.transaction()) - { - try(PreparedStatement pst = transaction.prepareStatement( - "INSERT INTO minecity_groups(city_id,name,display_name) VALUES(?,?,?)" - , PreparedStatement.RETURN_GENERATED_KEYS - )) - { - pst.setInt(1, city.getId()); - pst.setString(2, id); - pst.setString(3, name); - pst.executeUpdate(); - ResultSet generatedKeys = pst.getGeneratedKeys(); - generatedKeys.next(); - int groupId = generatedKeys.getInt(1); - - transaction.commit(); - source.groupNames.computeIfAbsent(city.getIdentityName(), i-> new HashSet<>(1)).add(name); - return new Group(this, groupId, city, id, name, Collections.emptySet(), Collections.emptySet()); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @Override - public void setName(@NotNull Group group, @NotNull String identity, @NotNull String name) throws DataSourceException - { - try(Connection transaction = connection.connect()) - { - try(PreparedStatement pst = transaction.prepareStatement( - "UPDATE minecity_groups SET `name`=?, display_name=? WHERE group_id=?" - )) - { - pst.setString(1, identity); - pst.setString(2, name); - pst.setInt(3, group.id); - int count = pst.executeUpdate(); - if(count != 1) - throw new DataSourceException("Expected 1 change but got "+count+" changes"); - - Set groups = source.groupNames.computeIfAbsent(group.home.getIdentityName(), i-> new HashSet<>(1)); - groups.remove(group.getName()); - groups.add(name); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @Override - public void deleteGroup(@NotNull Group group) throws DataSourceException - { - try(Connection transaction = connection.transaction()) - { - try(PreparedStatement pst = transaction.prepareStatement( - "DELETE FROM minecity_groups WHERE group_id=?" - )) - { - pst.setInt(1, group.id); - source.executeUpdate(pst, 1); - transaction.commit(); - source.groupNames.computeIfAbsent(group.home.getIdentityName(), i-> new HashSet<>(0)).remove(group.getName()); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @Override - public void addMember(@NotNull Group group, @NotNull Identity member) - throws DataSourceException, UnsupportedOperationException - { - try(Connection transaction = this.connection.transaction(); Statement stm = transaction.createStatement()) - { - try - { - switch(member.getType()) - { - case PLAYER: - { - int playerId = source.playerId(transaction, (PlayerID) member); - stm.executeUpdate("INSERT INTO minecity_group_players(group_id,player_id) VALUES("+group.id+","+playerId+");"); - break; - } - case ENTITY: - { - int entityId = source.entityId(transaction, (EntityID) member); - stm.executeUpdate("INSERT INTO minecity_group_entities(group_id,entity_id) VALUES("+group.id+","+entityId+");"); - break; - } - default: - throw new UnsupportedOperationException("Unsupported identity type: "+member.getType()); - } - - transaction.commit(); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - catch(ClassCastException e) - { - throw new UnsupportedOperationException(e); - } - } - - @Slow - @Override - public void addManager(@NotNull Group group, @NotNull PlayerID manager) - throws DataSourceException, UnsupportedOperationException - { - try(Connection transaction = this.connection.transaction(); Statement stm = transaction.createStatement()) - { - try - { - int playerId = source.playerId(transaction, manager); - stm.executeUpdate("INSERT INTO minecity_group_managers(group_id,player_id) VALUES("+group.id+","+playerId+");"); - - transaction.commit(); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - catch(ClassCastException e) - { - throw new UnsupportedOperationException(e); - } - } - - @Slow - @Override - public void removeMember(@NotNull Group group, @NotNull Identity member) - throws DataSourceException, UnsupportedOperationException - { - try(Connection transaction = this.connection.transaction(); Statement stm = transaction.createStatement()) - { - try - { - switch(member.getType()) - { - case PLAYER: - { - int playerId = source.playerId(transaction, (PlayerID) member); - int changes = stm.executeUpdate("DELETE FROM minecity_group_players WHERE group_id="+group.id+" AND player_id="+playerId+";"); - if(changes != 1) - throw new DataSourceException("Expected 1 change, got "+changes+" changes"); - break; - } - case ENTITY: - { - int entityId = source.entityId(transaction, (EntityID) member); - int changes = stm.executeUpdate("INSERT INTO minecity_group_entities WHERE group_id="+group.id+" AND entity_id="+entityId+";"); - if(changes != 1) - throw new DataSourceException("Expected 1 change, got "+changes+" changes"); - break; - } - default: - throw new UnsupportedOperationException("Unsupported identity type: "+member.getType()); - } - - transaction.commit(); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - catch(ClassCastException e) - { - throw new UnsupportedOperationException(e); - } - } - - @Slow - @Override - public void removeManager(@NotNull Group group, @NotNull PlayerID manager) - throws DataSourceException, UnsupportedOperationException - { - try(Connection transaction = this.connection.transaction(); Statement stm = transaction.createStatement()) - { - try - { - int playerId = source.playerId(transaction, manager); - int changes = stm.executeUpdate("DELETE FROM minecity_group_managers WHERE group_id="+group.id+" AND player_id="+playerId+";"); - if(changes != 1) - throw new DataSourceException("Expected 1 change, got "+changes+" changes"); - - transaction.commit(); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - catch(ClassCastException e) - { - throw new UnsupportedOperationException(e); - } - } - - @Slow - @NotNull - @Override - public Collection loadIslands(City city) throws DataSourceException - { - try - { - Connection connection = this.connection.connect(); - int cityId = city.getId(); - try(PreparedStatement pst = connection.prepareStatement( - "SELECT c.island_id, MIN(x), MAX(x), MIN(z), MAX(z), COUNT(*), i.world_id, w.dim, w.world, w.`name` " + - "FROM minecity_chunks c " + - "INNER JOIN minecity_islands AS i ON c.island_id=i.island_id " + - "INNER JOIN minecity_world AS w ON i.world_id=w.world_id " + - "WHERE i.city_id = ? AND c.reserve=0 " + - "GROUP BY c.island_id" - )) - { - pst.setInt(1, cityId); - ArrayList islands = new ArrayList<>(3); - ResultSet result = pst.executeQuery(); - while(result.next()) - { - WorldDim world = source.world(result.getInt(7), ()->result.getInt(8), ()->result.getString(9), ()->result.getString(10)); - islands.add(new SQLIsland(this, permStorage, - result.getInt(1), result.getInt(2), result.getInt(3), - result.getInt(4), result.getInt(5), result.getInt(6), world, city - )); - } - islands.trimToSize(); - return islands; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @NotNull - @Override - public Collection loadGroups(@NotNull City city) throws DataSourceException - { - try - { - Connection connection = this.connection.connect(); - try(PreparedStatement groupPst = connection.prepareStatement( - "SELECT group_id, name, display_name FROM minecity_groups WHERE city_id=?" - ); - PreparedStatement playerPst = connection.prepareStatement( - "SELECT p.player_id, player_uuid, player_name " + - "FROM minecity_group_players gp " + - "INNER JOIN minecity_players p ON p.player_id = gp.player_id " + - "WHERE gp.group_id = ?" - ); - PreparedStatement entityPst = connection.prepareStatement( - "SELECT e.entity_id, entity_uuid, entity_name, entity_type " + - "FROM minecity_group_entities ge " + - "INNER JOIN minecity_entities e ON e.entity_id = ge.entity_id " + - "WHERE ge.group_id = ?" - ); - PreparedStatement managersPst = connection.prepareStatement( - "SELECT p.player_id, player_uuid, player_name " + - "FROM minecity_group_managers gm " + - "INNER JOIN minecity_players p ON p.player_id = gm.player_id " + - "WHERE gm.group_id = ?" - ) - ) - { - groupPst.setInt(1, city.getId()); - ResultSet groupResult = groupPst.executeQuery(); - List groups = new ArrayList<>(); - while(groupResult.next()) - { - int groupId = groupResult.getInt(1); - playerPst.setInt(1, groupId); - entityPst.setInt(1, groupId); - managersPst.setInt(1, groupId); - - List managers = new ArrayList<>(); - List> members = new ArrayList<>(); - - try(ResultSet managerResult = managersPst.executeQuery()) - { - while(managerResult.next()) - members.add(new PlayerID(managerResult.getInt("player_id"), - source.uuid(managerResult.getBytes("player_uuid")), - managerResult.getString("player_name") - )); - } - - try(ResultSet memberResult = playerPst.executeQuery()) - { - while(memberResult.next()) - members.add(new PlayerID(memberResult.getInt("player_id"), - source.uuid(memberResult.getBytes("player_uuid")), - memberResult.getString("player_name") - )); - } - - try(ResultSet memberResult = entityPst.executeQuery()) - { - while(memberResult.next()) - members.add(new EntityID(memberResult.getInt("entity_id"), - MinecraftEntity.Type.valueOf(memberResult.getString("entity_type")), - source.uuid(memberResult.getBytes("entity_uuid")), - memberResult.getString("entity_name") - )); - } - - groups.add(new Group(this, groupId, city, groupResult.getString(2), groupResult.getString(3), members, managers)); - } - - return groups; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @NotNull - @Override - public Island createIsland(@NotNull City city, @NotNull ChunkPos chunk) - throws DataSourceException, IllegalStateException - { - int cityId = city.getId(); - if(cityId <= 0) throw new IllegalStateException("The city is not registered"); - - try(Connection transaction = connection.transaction()) - { - try - { - SQLIsland island = new SQLIsland(this, permStorage, source.createIsland(transaction, cityId, chunk.world), chunk, city); - source.createClaim(transaction, island.id, chunk); - island.city = city; - - transaction.commit(); - - return island; - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @Override - public void claim(@NotNull Island island, @NotNull ChunkPos chunk) throws DataSourceException - { - SQLIsland sqlIsland = (SQLIsland) island; - try(Connection transaction = connection.transaction()) - { - try - { - source.createClaim(connection.connect(), sqlIsland.id, chunk); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - sqlIsland.add(chunk); - - source.mineCity.reloadChunk(chunk); - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @NotNull - @Override - @SuppressWarnings("OptionalGetWithoutIsPresent") - public Island claim(@NotNull Set islands, @NotNull ChunkPos chunk) - throws DataSourceException, IllegalStateException, NoSuchElementException - { - if(islands.size() == 1) - { - Island island = islands.iterator().next(); - claim(island, chunk); - return island; - } - - List sqlIslands = islands.stream().map(island -> (SQLIsland) island).collect(Collectors.toList()); - SQLIsland mainIsland = sqlIslands.stream().max((a, b) -> a.getChunkCount() - b.getChunkCount()).get(); - List merge = sqlIslands.stream().filter(island -> island != mainIsland).collect(Collectors.toList()); - try(Connection transaction = connection.transaction(); Statement stm = transaction.createStatement()) - { - try - { - StringBuilder sb = new StringBuilder(); - merge.forEach(island -> sb.append(island.id).append(", ")); - sb.setLength(sb.length()-2); - String array = sb.toString(); - - List chunksToUpdate = new ArrayList<>(); - try(ResultSet result = stm.executeQuery("SELECT c.world_id, c.x, c.z, w.dim, w.world, w.name " + - "FROM minecity_chunks c INNER JOIN minecity_world w ON c.world_id = w.world_id " + - "WHERE c.island_id IN("+array+");") - ){ - while(result.next()) - { - WorldDim world = source.world(result.getInt(1),()->result.getInt(4),()->result.getString(5),()->result.getString(6)); - chunksToUpdate.add(new ChunkPos(world, result.getInt(2), result.getInt(3))); - } - } - - stm.executeUpdate("UPDATE `minecity_chunks` SET `island_id`="+mainIsland.id+" WHERE `island_id` IN("+array+");"); - stm.executeUpdate("DELETE FROM `minecity_islands` WHERE `island_id` IN("+array+");"); - source.createClaim(transaction, mainIsland.id, chunk); - - transaction.commit(); - - mainIsland.add(chunk); - - merge.forEach(island -> { - mainIsland.minX = Math.min(mainIsland.minX, island.minX); - mainIsland.maxX = Math.max(mainIsland.maxX, island.maxX); - mainIsland.minZ = Math.min(mainIsland.minZ, island.minZ); - mainIsland.maxZ = Math.max(mainIsland.maxZ, island.maxZ); - mainIsland.chunkCount += island.chunkCount; - - island.minX = island.maxX = island.minZ = island.maxZ = island.chunkCount = 0; - }); - - chunksToUpdate.forEach((DBConsumer) source.mineCity::reloadChunk); - return mainIsland; - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @Override - public void setSpawn(@NotNull City city, @NotNull BlockPos spawn) throws DataSourceException, IllegalStateException - { - if(city.getSpawn().equals(spawn)) - return; - - int cityId = city.getId(); - if(cityId <= 0) throw new IllegalStateException("The city is not registered"); - - try - { - Connection connection = this.connection.connect(); - int worldId = source.worldId(connection, spawn.world); - - try(PreparedStatement pst = connection.prepareStatement( - "UPDATE `minecity_city` SET `spawn_world`=?, `spawn_x`=?, `spawn_y`=?, `spawn_z`=? WHERE `city_id`=?" - )) - { - pst.setInt(1, worldId); - pst.setInt(2, spawn.x); - //noinspection SuspiciousNameCombination - pst.setInt(3, spawn.y); - pst.setInt(4, spawn.z); - pst.setInt(5, cityId); - if(pst.executeUpdate() <= 0) - throw new DataSourceException("Tried to change spawn of "+ cityId+" from "+city.getSpawn()+" to "+spawn+" but nothing changed"); - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @Override - @SuppressWarnings("SuspiciousNameCombination") - public int createPlot(Plot plot) throws DataSourceException - { - try(Connection transaction = connection.transaction()) - { - try - { - int plotId; - try(PreparedStatement pst = transaction.prepareStatement( - "INSERT INTO minecity_plots(island_id,name,display_name,owner,spawn_x,spawn_y,spawn_z,shape,tax_accepted_flat,tax_accepted_percent,tax_applied_flat,tax_applied_percent,investment) " + - "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)", - Statement.RETURN_GENERATED_KEYS - )) - { - pst.setInt(1, plot.getIsland().getId()); - pst.setString(2, plot.getIdentityName()); - pst.setString(3, plot.getName()); - source.setNullableInt(pst, 4, source.playerId(transaction, plot.getOwner().orElse(null))); - BlockPos spawn = plot.getSpawn(); - pst.setInt(5, spawn.x); - pst.setInt(6, spawn.y); - pst.setInt(7, spawn.z); - pst.setBytes(8, plot.getShape().serializeBytes()); - pst.setDouble(9, plot.getAcceptedTax().getFlat()); - pst.setDouble(10, plot.getAcceptedTax().getPercent()); - pst.setDouble(11, plot.getAppliedTax().getFlat()); - pst.setDouble(12, plot.getAppliedTax().getPercent()); - pst.setDouble(13, plot.getInvestment()); - pst.executeUpdate(); - - ResultSet result = pst.getGeneratedKeys(); - result.next(); - plotId = result.getInt(1); - } - - transaction.commit(); - return plotId; - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @Override - public void setOwner(@NotNull Plot plot, @Nullable PlayerID owner) throws DataSourceException, IllegalStateException - { - try(Connection transaction = connection.transaction()) - { - try - { - try(PreparedStatement pst = transaction.prepareStatement( - "UPDATE minecity_plots SET owner=? WHERE plot_id=?" - )) - { - source.setNullableInt(pst, 1, source.playerId(transaction, owner)); - pst.setInt(2, plot.id); - source.executeUpdate(pst, 1); - } - - transaction.commit(); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @Override - public void setShape(@NotNull Plot plot, @NotNull Shape shape, BlockPos spawn, @NotNull Island newIsland) throws DataSourceException - { - try(Connection transaction = connection.transaction()) - { - try - { - try(PreparedStatement pst = transaction.prepareStatement( - "UPDATE minecity_plots SET shape=?, island_id=? WHERE plot_id=?" - )) - { - pst.setBytes(1, shape.serializeBytes()); - pst.setInt(2, newIsland.id); - pst.setInt(3, plot.id); - source.executeUpdate(pst, 1); - } - - setSpawn(transaction, plot, spawn); - - transaction.commit(); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @Override - public void setName(@NotNull Plot plot, @NotNull String identity, @NotNull String name) throws DataSourceException - { - try(Connection transaction = connection.transaction()) - { - try - { - try(PreparedStatement pst = transaction.prepareStatement( - "UPDATE minecity_plots SET `name`=?, `display_name`=? WHERE plot_id=?" - )) - { - pst.setString(1, identity); - pst.setString(2, name); - pst.setInt(3, plot.id); - source.executeUpdate(pst, 1); - } - - transaction.commit(); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @SuppressWarnings("SuspiciousNameCombination") - private void setSpawn(@NotNull Connection transaction, @NotNull Plot plot, @NotNull BlockPos spawn) - throws DataSourceException, SQLException - { - try(PreparedStatement pst = transaction.prepareStatement( - "UPDATE minecity_plots SET spawn_x=?, spawn_y=?, spawn_z=? WHERE plot_id=?" - )) - { - pst.setInt(1, spawn.x); - pst.setInt(2, spawn.y); - pst.setInt(3, spawn.z); - pst.setInt(4, plot.id); - source.executeUpdate(pst, 1); - } - } - - @Slow - @Override - public void setSpawn(@NotNull Plot plot, @NotNull BlockPos spawn) throws DataSourceException - { - if(!spawn.world.equals(plot.getIsland().world)) - throw new IllegalArgumentException("Different world!"); - - try(Connection transaction = connection.transaction()) - { - try - { - setSpawn(transaction, plot, spawn); - transaction.commit(); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @Override - public void deletePlot(@NotNull Plot plot) throws DataSourceException - { - try(Connection transaction = connection.transaction()) - { - try(PreparedStatement pst = transaction.prepareStatement( - "DELETE FROM minecity_plots WHERE plot_id=?" - )) - { - pst.setInt(1, plot.id); - source.executeUpdate(pst, 1); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - - transaction.commit(); - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @NotNull - @Override - public Set loadPlots(@NotNull Island island) throws DataSourceException - { - try - { - try(PreparedStatement pst = connection.connect().prepareStatement( - "SELECT plot_id,`name`,display_name,spawn_x,spawn_y,spawn_z,shape, player_id,player_uuid,player_name,perm_denial_message, " + - "tax_accepted_flat, tax_accepted_percent, tax_applied_flat, tax_applied_percent, investment, price " + - "FROM minecity_plots LEFT JOIN minecity_players ON player_id=owner " + - "WHERE island_id=?" - )) - { - pst.setInt(1, island.id); - ResultSet result = pst.executeQuery(); - if(!result.next()) - return Collections.emptySet(); - - // Memory optimization, caching common Tax instances - HashMap taxCache = new HashMap<>(6); - Tax t = island.getCity().getAppliedTax(); - taxCache.put(t, t); - t = island.getCity().mineCity.costs.plotTaxApplied; - taxCache.put(t, t); - BiFunction tax = (flat, percent)-> taxCache.computeIfAbsent(new Tax(flat, percent), Function.identity()); - - HashSet plots = new HashSet<>(3); - do - { - PlayerID owner; - int ownerId = result.getInt(8); - if(ownerId > 0) - owner = PlayerID.get(source.uuid(result.getBytes(9)), result.getString(10)); - else - owner = null; - - Message denial; - String str = result.getString(11); - if(str == null) - denial = null; - else - denial = new Message("", "${msg}", new Object[]{"msg",str}); - - plots.add(new Plot(source.mineCity, this, permStorage, result.getInt(1), island, result.getString(2), result.getString(3), owner, - new BlockPos(island.world, result.getInt(4), result.getInt(5), result.getInt(6)), - Shape.deserializeBytes(result.getBytes(7)), denial, - tax.apply(result.getDouble("tax_accepted_flat"), result.getDouble("tax_accepted_percent")), - tax.apply(result.getDouble("tax_applied_flat"), result.getDouble("tax_applied_percent")), - result.getDouble("investment"), result.getDouble("price") - )); - } while(result.next()); - - return plots; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } -} +package br.com.gamemods.minecity.datasource.sql; + +import br.com.gamemods.minecity.api.PlayerID; +import br.com.gamemods.minecity.api.Slow; +import br.com.gamemods.minecity.api.command.Message; +import br.com.gamemods.minecity.api.permission.EntityID; +import br.com.gamemods.minecity.api.permission.Group; +import br.com.gamemods.minecity.api.permission.Identity; +import br.com.gamemods.minecity.api.permission.OptionalPlayer; +import br.com.gamemods.minecity.api.shape.Shape; +import br.com.gamemods.minecity.api.world.BlockPos; +import br.com.gamemods.minecity.api.world.ChunkPos; +import br.com.gamemods.minecity.api.world.MinecraftEntity; +import br.com.gamemods.minecity.api.world.WorldDim; +import br.com.gamemods.minecity.datasource.api.DataSourceException; +import br.com.gamemods.minecity.datasource.api.ICityStorage; +import br.com.gamemods.minecity.datasource.api.unchecked.DBConsumer; +import br.com.gamemods.minecity.economy.Tax; +import br.com.gamemods.minecity.structure.City; +import br.com.gamemods.minecity.structure.Island; +import br.com.gamemods.minecity.structure.IslandArea; +import br.com.gamemods.minecity.structure.Plot; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.*; +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class SQLCityStorage implements ICityStorage +{ + @NotNull + private final SQLSource source; + + @NotNull + private final SQLConnection connection; + + @NotNull + private final SQLPermStorage permStorage; + + SQLCityStorage(@NotNull SQLSource source, @NotNull SQLConnection connection, @NotNull SQLPermStorage permStorage) + { + this.source = source; + this.connection = connection; + this.permStorage = permStorage; + } + + @Override + public void deleteCity(@NotNull City city) throws DataSourceException + { + int cityId = city.getId(); + if(cityId <= 0) + throw new IllegalStateException("cityId = "+cityId); + + try(Connection transaction = connection.transaction()) + { + try(PreparedStatement pst = transaction.prepareStatement( + "DELETE FROM minecity_city WHERE city_id=?" + )) + { + pst.setInt(1, cityId); + source.executeUpdate(pst, 1); + transaction.commit(); + source.cityNames.remove(city.getName()); + source.groupNames.remove(city.getIdentityName()); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @Override + public void setOwner(@NotNull City city, @NotNull OptionalPlayer owner) throws DataSourceException, IllegalStateException + { + if(city.owner().equals(owner)) + return; + + int cityId = city.getId(); + if(cityId <= 0) throw new IllegalStateException("The city is not registered"); + + try + { + Connection connection = this.connection.connect(); + int ownerId = source.playerId(connection, owner); + + try(PreparedStatement pst = connection.prepareStatement( + "UPDATE `minecity_city` SET `owner`=? WHERE `city_id`=?" + )) + { + source.setNullableInt(pst, 1, ownerId); + pst.setInt(2, cityId); + if(pst.executeUpdate() <= 0) + throw new DataSourceException("Tried to change the owner of "+ cityId+" from "+city.owner()+" to "+owner+" but nothing changed"); + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @NotNull + @Override + public Collection reserve(@NotNull IslandArea reserve) throws DataSourceException + { + SQLIsland sqlIsland = (SQLIsland) reserve.island; + StringBuilder sbx = new StringBuilder(), sbz = new StringBuilder(); + Runnable build = ()-> + { + sbx.setLength(0); sbz.setLength(0); + reserve.claims().forEach(c->{ sbx.append(c.x).append(','); sbz.append(c.z).append(','); }); + if(sbx.length() > 0) + sbx.setLength(sbx.length() - 1); + if(sbz.length() > 0) + sbz.setLength(sbz.length() - 1); + }; + build.run(); + + try(Connection transaction = connection.transaction(); Statement stm = transaction.createStatement()) + { + try + { + int worldId = source.worldId(transaction, sqlIsland.world); + Collection chunks = new HashSet<>(); + String x = sbx.toString(), z = sbz.toString(); + + if(!x.isEmpty()) + { + + ResultSet results = stm.executeQuery("SELECT x, z FROM minecity_chunks WHERE world_id="+worldId+" AND island_id!="+sqlIsland.id+" AND x IN("+x+") AND z IN("+z+");"); + while(results.next()) + reserve.setClaimed(results.getInt(1), results.getInt(2), false); + results.close(); + + results = stm.executeQuery("SELECT x,z FROM minecity_chunks WHERE island_id="+sqlIsland.id+" LIMIT 1"); + results.next(); + ChunkPos pos = new ChunkPos(sqlIsland.world, results.getInt(1), results.getInt(2)); + results.close(); + + Set valid = reserve.contiguous(pos); + reserve.claims().filter(c-> !valid.contains(c)).forEach(c-> reserve.setClaimed(c, false)); + build.run(); + x = sbx.toString(); z = sbz.toString(); + } + + + String deleteCond = "island_id="+sqlIsland.id+" AND reserve=1"; + if(!x.isEmpty()) + deleteCond+= " AND x NOT IN ("+x+") AND z NOT IN ("+z+")"; + + ResultSet results = stm.executeQuery("SELECT x,z FROM minecity_chunks WHERE " + deleteCond+";"); + while(results.next()) + chunks.add(new ChunkPos(sqlIsland.world, results.getInt(1), results.getInt(2))); + results.close(); + + stm.executeUpdate("DELETE FROM minecity_chunks WHERE "+deleteCond+";"); + + if(!x.isEmpty()) + { + results = stm.executeQuery("SELECT x, z FROM minecity_chunks WHERE world_id="+worldId+" AND x IN("+x+") AND z IN("+z+");"); + while(results.next()) + reserve.setClaimed(results.getInt(1), results.getInt(2), false); + results.close(); + + sbx.setLength(0); + List insert = reserve.claims().collect(Collectors.toList()); + chunks.addAll(insert); + insert.forEach(c-> + sbx.append('(').append(worldId).append(',').append(c.x).append(',').append(c.z).append(',') + .append(sqlIsland.id).append(",1),") + ); + if(sbx.length() > 0) + { + sbx.setLength(sbx.length()-1); + stm.executeUpdate("INSERT INTO minecity_chunks(world_id,x,z,island_id,reserve) VALUES "+sbx+";"); + } + } + transaction.commit(); + return chunks; + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + private void disclaim(Connection transaction, ChunkPos chunk, int islandId, boolean enforce) throws DataSourceException, SQLException + { + try(PreparedStatement pst = transaction.prepareStatement( + "DELETE FROM `minecity_chunks` WHERE `world_id`=? AND `x`=? AND `z`=? AND `island_id`=?" + )) + { + pst.setInt(1, source.worldId(transaction, chunk.world)); + pst.setInt(2, chunk.x); + pst.setInt(3, chunk.z); + pst.setInt(4, islandId); + if(enforce) + source.executeUpdate(pst, 1); + else + pst.executeUpdate(); + } + } + + @Slow + @Override + public void deleteIsland(@NotNull Island island) throws DataSourceException, IllegalArgumentException + { + SQLIsland sqlIsland = (SQLIsland) island; + if(sqlIsland.chunkCount == 0) throw new IllegalArgumentException(); + + try(Connection transaction = this.connection.transaction()) + { + try(PreparedStatement pst = transaction.prepareStatement( + "DELETE FROM `minecity_islands` WHERE `island_id`=?" + )) + { + pst.setInt(1, sqlIsland.id); + int i = pst.executeUpdate(); + if(i != 1) + throw new DataSourceException("Expecting 1 change, "+i+" changed"); + + transaction.commit(); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + + sqlIsland.chunkCount = sqlIsland.maxX = sqlIsland.minX = sqlIsland.maxZ = sqlIsland.minZ = 0; + source.mineCity.reloadChunksUnchecked(c-> c.getIsland().filter(island::equals).isPresent()); + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + private void updateCount(Connection transaction, SQLIsland sqlIsland) throws SQLException + { + try(PreparedStatement pst = transaction.prepareStatement( + "SELECT MIN(x), MAX(x), MIN(z), MAX(z), COUNT(*) FROM minecity_chunks WHERE island_id=? AND reserve=0" + )) + { + pst.setInt(1, sqlIsland.id); + ResultSet result = pst.executeQuery(); + result.next(); + sqlIsland.minX = result.getInt(1); + sqlIsland.maxX = result.getInt(2); + sqlIsland.minZ = result.getInt(3); + sqlIsland.maxZ = result.getInt(4); + sqlIsland.chunkCount = result.getInt(5); + } + } + + @Slow + @Override + public void disclaim(@NotNull ChunkPos chunk, @NotNull Island island) + throws DataSourceException, IllegalArgumentException + { + SQLIsland sqlIsland = (SQLIsland) island; + if(sqlIsland.chunkCount == 0) throw new IllegalArgumentException(); + + try(Connection transaction = connection.transaction()) + { + try + { + disclaim(transaction, chunk, sqlIsland.id, true); + transaction.commit(); + + updateCount(transaction, sqlIsland); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + + try + { + source.mineCity.reloadChunk(chunk); + } + catch(Exception e) + { + e.printStackTrace(); + } + } + + @Slow + @NotNull + @Override + @SuppressWarnings("OptionalGetWithoutIsPresent") + public Collection disclaim(@NotNull ChunkPos chunk, @NotNull Island island, @NotNull Set> groups) + throws DataSourceException, IllegalStateException, NoSuchElementException, ClassCastException, IllegalArgumentException + { + SQLIsland sqlIsland = (SQLIsland) island; + if(sqlIsland.chunkCount == 0) throw new IllegalArgumentException(); + int cityId = sqlIsland.city.getId(); + + Set mainGroup = groups.stream().max((a,b)-> a.size()-b.size() ).get(); + groups = groups.stream().filter(s-> s != mainGroup).collect(Collectors.toSet()); + + try(Connection transaction = connection.transaction(); Statement stm = transaction.createStatement()) + { + try + { + disclaim(transaction, chunk, sqlIsland.id, true); + int worldId = source.worldId(transaction, sqlIsland.world); + + + stm.executeUpdate("DELETE FROM minecity_chunks WHERE island_id="+sqlIsland.id+" AND reserve=1;"); + + List islands = new ArrayList<>(groups.size()); + int[] expected = new int[groups.size()]; int i = 0; + for(Set group : groups) + { + int islandId = source.createIsland(transaction, cityId, sqlIsland.world); + StringBuilder sb = new StringBuilder(); + int minX = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE; + for(ChunkPos pos : group) + { + minX = Math.min(minX, pos.x); + minZ = Math.min(minZ, pos.z); + maxX = Math.max(maxX, pos.x); + maxZ = Math.max(maxZ, pos.z); + sb.append("(x=").append(pos.x).append(" AND z=").append(pos.z).append(") OR"); + } + + sb.setLength(sb.length() - 3); + + stm.addBatch("UPDATE minecity_chunks SET island_id="+islandId+" " + + "WHERE world_id="+worldId+" AND island_id="+sqlIsland.id+" AND reserve=0 AND ("+sb+");" + ); + + SQLIsland newIsland = new SQLIsland(sqlIsland.city, this, permStorage, islandId, minX, maxX, minZ, maxZ, group.size(), sqlIsland.world, Collections.emptySet()); + expected[i++] = newIsland.chunkCount; + islands.add(newIsland); + } + + int[] result = stm.executeBatch(); + if(!Arrays.equals(expected, result)) + { + throw new DataSourceException("Unexpected result after reclaiming to new islands. " + + "Expected: "+Arrays.toString(expected)+" Result: "+Arrays.toString(result)); + } + + Collection plots = sqlIsland.getPlots(); + List afterCommit = new ArrayList<>(plots.size()); + if(!plots.isEmpty()) + { + try(PreparedStatement select = transaction.prepareStatement( + "SELECT island_id FROM minecity_chunks WHERE world_id=? AND x=? AND z=? AND reserve=0" + ); PreparedStatement update = transaction.prepareStatement( + "UPDATE minecity_plots SET island_id=? WHERE plot_id=?" + )) + { + for(Plot plot: plots) + { + ChunkPos spawn = plot.getSpawn().getChunk(); + select.setInt(1, worldId); + select.setInt(2, spawn.x); + select.setInt(3, spawn.z); + ResultSet selectRes = select.executeQuery(); + if(!selectRes.next()) + throw new DataSourceException("The plot id:"+plot.id+" name:"+plot.getIdentityName()+" is not in an island!"); + int newIslandId = selectRes.getInt(1); + selectRes.close(); + + if(plot.getIsland().id == newIslandId) + continue; + + SQLIsland newIsland = null; + for(Island possible : islands) + { + if(possible.id == newIslandId) + { + newIsland = (SQLIsland) possible; + break; + } + } + + if(newIsland == null) + throw new DataSourceException("The plot id:"+plot.id+" name:"+plot.getIdentityName()+ + " would be moved to the island:"+newIslandId+" but that island wasn't created by " + + "this disclaim!"); + + update.setInt(1, newIslandId); + update.setInt(2, plot.id); + source.executeUpdate(update, 1); + + SQLIsland finalNewIsland = newIsland; + afterCommit.add(()-> sqlIsland.relocate(plot, finalNewIsland)); + } + } + } + + updateCount(transaction, sqlIsland); + transaction.commit(); + afterCommit.forEach(Runnable::run); + + return islands; + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Override + public double invested(@NotNull City city, double value) throws DataSourceException + { + try(Connection transaction = connection.transaction()) + { + try + { + try(PreparedStatement pst = transaction.prepareStatement( + "UPDATE minecity_city SET `investment`=`investment`+? WHERE city_id=?" + )) + { + pst.setDouble(1, value); + pst.setInt(2, city.getId()); + source.executeUpdate(pst, 1); + } + + double investment; + try(PreparedStatement pst = transaction.prepareStatement( + "SELECT `investment` FROM `minecity_city` WHERE `city_id`=?" + )) + { + pst.setInt(1, city.getId()); + ResultSet result = pst.executeQuery(); + result.next(); + investment = result.getDouble(1); + } + + transaction.commit(); + return investment; + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Override + public double invested(@NotNull Plot plot, double value) throws DataSourceException + { + try(Connection transaction = connection.transaction()) + { + try + { + try(PreparedStatement pst = transaction.prepareStatement( + "UPDATE minecity_plots SET `investment`=`investment`+? WHERE plot_id=?" + )) + { + pst.setDouble(1, value); + pst.setInt(2, plot.id); + source.executeUpdate(pst, 1); + } + + double investment; + try(PreparedStatement pst = transaction.prepareStatement( + "SELECT `investment` FROM `minecity_plots` WHERE `plot_id`=?" + )) + { + pst.setInt(1, plot.id); + ResultSet result = pst.executeQuery(); + result.next(); + investment = result.getDouble(1); + } + + transaction.commit(); + return investment; + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Override + public void setInvestment(@NotNull Plot plot, double investment) throws DataSourceException + { + try(Connection transaction = connection.transaction()) + { + try + { + try(PreparedStatement pst = transaction.prepareStatement( + "UPDATE minecity_plots SET `investment`=? WHERE plot_id=?" + )) + { + pst.setDouble(1, investment); + pst.setInt(2, plot.id); + source.executeUpdate(pst, 1); + } + + transaction.commit(); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Override + public void setPrice(@NotNull Plot plot, double price) throws DataSourceException + { + try(Connection transaction = connection.transaction()) + { + try + { + try(PreparedStatement pst = transaction.prepareStatement( + "UPDATE minecity_plots SET `price`=? WHERE plot_id=?" + )) + { + pst.setDouble(1, price); + pst.setInt(2, plot.id); + source.executeUpdate(pst, 1); + } + + transaction.commit(); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Override + public void setPrice(@NotNull City city, double price) throws DataSourceException + { + try(Connection transaction = connection.transaction()) + { + try + { + try(PreparedStatement pst = transaction.prepareStatement( + "UPDATE minecity_city SET `price`=? WHERE city_id=?" + )) + { + pst.setDouble(1, price); + pst.setInt(2, city.getId()); + source.executeUpdate(pst, 1); + } + + transaction.commit(); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @Override + public void setName(@NotNull City city, @NotNull String identity, @NotNull String name) throws DataSourceException + { + try(Connection transaction = connection.transaction()) + { + String previous = city.getName(); + try + { + if(identity.equals(city.getIdentityName())) + try(PreparedStatement pst = transaction.prepareStatement( + "UPDATE minecity_city SET `display_name`=? WHERE city_id=?" + )) + { + pst.setString(1, name); + pst.setInt(2, city.getId()); + source.executeUpdate(pst, 1); + } + else + try(PreparedStatement pst = transaction.prepareStatement( + "UPDATE minecity_city SET `name`=?, display_name=? WHERE city_id=?" + )) + { + pst.setString(1, identity); + pst.setString(2, name); + pst.setInt(3, city.getId()); + source.executeUpdate(pst, 1); + } + + transaction.commit(); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + source.cityNames.remove(previous); + source.cityNames.add(name); + Set groups = source.groupNames.remove(identity); + if(groups != null) + source.groupNames.put(city.getIdentityName(), groups); + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @NotNull + @Override + public Group createGroup(@NotNull City city, @NotNull String id, @NotNull String name) throws DataSourceException + { + try(Connection transaction = connection.transaction()) + { + try(PreparedStatement pst = transaction.prepareStatement( + "INSERT INTO minecity_groups(city_id,name,display_name) VALUES(?,?,?)" + , PreparedStatement.RETURN_GENERATED_KEYS + )) + { + pst.setInt(1, city.getId()); + pst.setString(2, id); + pst.setString(3, name); + pst.executeUpdate(); + ResultSet generatedKeys = pst.getGeneratedKeys(); + generatedKeys.next(); + int groupId = generatedKeys.getInt(1); + + transaction.commit(); + source.groupNames.computeIfAbsent(city.getIdentityName(), i-> new HashSet<>(1)).add(name); + return new Group(this, groupId, city, id, name, Collections.emptySet(), Collections.emptySet()); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @Override + public void setName(@NotNull Group group, @NotNull String identity, @NotNull String name) throws DataSourceException + { + try(Connection transaction = connection.connect()) + { + try(PreparedStatement pst = transaction.prepareStatement( + "UPDATE minecity_groups SET `name`=?, display_name=? WHERE group_id=?" + )) + { + pst.setString(1, identity); + pst.setString(2, name); + pst.setInt(3, group.id); + int count = pst.executeUpdate(); + if(count != 1) + throw new DataSourceException("Expected 1 change but got "+count+" changes"); + + Set groups = source.groupNames.computeIfAbsent(group.home.getIdentityName(), i-> new HashSet<>(1)); + groups.remove(group.getName()); + groups.add(name); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @Override + public void deleteGroup(@NotNull Group group) throws DataSourceException + { + try(Connection transaction = connection.transaction()) + { + try(PreparedStatement pst = transaction.prepareStatement( + "DELETE FROM minecity_groups WHERE group_id=?" + )) + { + pst.setInt(1, group.id); + source.executeUpdate(pst, 1); + transaction.commit(); + source.groupNames.computeIfAbsent(group.home.getIdentityName(), i-> new HashSet<>(0)).remove(group.getName()); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @Override + public void addMember(@NotNull Group group, @NotNull Identity member) + throws DataSourceException, UnsupportedOperationException + { + try(Connection transaction = this.connection.transaction(); Statement stm = transaction.createStatement()) + { + try + { + switch(member.getType()) + { + case PLAYER: + { + int playerId = source.playerId(transaction, (PlayerID) member); + stm.executeUpdate("INSERT INTO minecity_group_players(group_id,player_id) VALUES("+group.id+","+playerId+");"); + break; + } + case ENTITY: + { + int entityId = source.entityId(transaction, (EntityID) member); + stm.executeUpdate("INSERT INTO minecity_group_entities(group_id,entity_id) VALUES("+group.id+","+entityId+");"); + break; + } + default: + throw new UnsupportedOperationException("Unsupported identity type: "+member.getType()); + } + + transaction.commit(); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + catch(ClassCastException e) + { + throw new UnsupportedOperationException(e); + } + } + + @Slow + @Override + public void addManager(@NotNull Group group, @NotNull PlayerID manager) + throws DataSourceException, UnsupportedOperationException + { + try(Connection transaction = this.connection.transaction(); Statement stm = transaction.createStatement()) + { + try + { + int playerId = source.playerId(transaction, manager); + stm.executeUpdate("INSERT INTO minecity_group_managers(group_id,player_id) VALUES("+group.id+","+playerId+");"); + + transaction.commit(); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + catch(ClassCastException e) + { + throw new UnsupportedOperationException(e); + } + } + + @Slow + @Override + public void removeMember(@NotNull Group group, @NotNull Identity member) + throws DataSourceException, UnsupportedOperationException + { + try(Connection transaction = this.connection.transaction(); Statement stm = transaction.createStatement()) + { + try + { + switch(member.getType()) + { + case PLAYER: + { + int playerId = source.playerId(transaction, (PlayerID) member); + int changes = stm.executeUpdate("DELETE FROM minecity_group_players WHERE group_id="+group.id+" AND player_id="+playerId+";"); + if(changes != 1) + throw new DataSourceException("Expected 1 change, got "+changes+" changes"); + break; + } + case ENTITY: + { + int entityId = source.entityId(transaction, (EntityID) member); + int changes = stm.executeUpdate("INSERT INTO minecity_group_entities WHERE group_id="+group.id+" AND entity_id="+entityId+";"); + if(changes != 1) + throw new DataSourceException("Expected 1 change, got "+changes+" changes"); + break; + } + default: + throw new UnsupportedOperationException("Unsupported identity type: "+member.getType()); + } + + transaction.commit(); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + catch(ClassCastException e) + { + throw new UnsupportedOperationException(e); + } + } + + @Slow + @Override + public void removeManager(@NotNull Group group, @NotNull PlayerID manager) + throws DataSourceException, UnsupportedOperationException + { + try(Connection transaction = this.connection.transaction(); Statement stm = transaction.createStatement()) + { + try + { + int playerId = source.playerId(transaction, manager); + int changes = stm.executeUpdate("DELETE FROM minecity_group_managers WHERE group_id="+group.id+" AND player_id="+playerId+";"); + if(changes != 1) + throw new DataSourceException("Expected 1 change, got "+changes+" changes"); + + transaction.commit(); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + catch(ClassCastException e) + { + throw new UnsupportedOperationException(e); + } + } + + @Slow + @NotNull + @Override + public Collection loadIslands(City city) throws DataSourceException + { + try + { + Connection connection = this.connection.connect(); + int cityId = city.getId(); + try(PreparedStatement pst = connection.prepareStatement( + "SELECT c.island_id, MIN(x), MAX(x), MIN(z), MAX(z), COUNT(*), i.world_id, w.dim, w.world, w.`name` " + + "FROM minecity_chunks c " + + "INNER JOIN minecity_islands AS i ON c.island_id=i.island_id " + + "INNER JOIN minecity_world AS w ON i.world_id=w.world_id " + + "WHERE i.city_id = ? AND c.reserve=0 " + + "GROUP BY c.island_id" + )) + { + pst.setInt(1, cityId); + ArrayList islands = new ArrayList<>(3); + ResultSet result = pst.executeQuery(); + while(result.next()) + { + WorldDim world = source.world(result.getInt(7), ()->result.getInt(8), ()->result.getString(9), ()->result.getString(10)); + islands.add(new SQLIsland(this, permStorage, + result.getInt(1), result.getInt(2), result.getInt(3), + result.getInt(4), result.getInt(5), result.getInt(6), world, city + )); + } + islands.trimToSize(); + return islands; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @NotNull + @Override + public Collection loadGroups(@NotNull City city) throws DataSourceException + { + try + { + Connection connection = this.connection.connect(); + try(PreparedStatement groupPst = connection.prepareStatement( + "SELECT group_id, name, display_name FROM minecity_groups WHERE city_id=?" + ); + PreparedStatement playerPst = connection.prepareStatement( + "SELECT p.player_id, player_uuid, player_name " + + "FROM minecity_group_players gp " + + "INNER JOIN minecity_players p ON p.player_id = gp.player_id " + + "WHERE gp.group_id = ?" + ); + PreparedStatement entityPst = connection.prepareStatement( + "SELECT e.entity_id, entity_uuid, entity_name, entity_type " + + "FROM minecity_group_entities ge " + + "INNER JOIN minecity_entities e ON e.entity_id = ge.entity_id " + + "WHERE ge.group_id = ?" + ); + PreparedStatement managersPst = connection.prepareStatement( + "SELECT p.player_id, player_uuid, player_name " + + "FROM minecity_group_managers gm " + + "INNER JOIN minecity_players p ON p.player_id = gm.player_id " + + "WHERE gm.group_id = ?" + ) + ) + { + groupPst.setInt(1, city.getId()); + ResultSet groupResult = groupPst.executeQuery(); + List groups = new ArrayList<>(); + while(groupResult.next()) + { + int groupId = groupResult.getInt(1); + playerPst.setInt(1, groupId); + entityPst.setInt(1, groupId); + managersPst.setInt(1, groupId); + + List managers = new ArrayList<>(); + List> members = new ArrayList<>(); + + try(ResultSet managerResult = managersPst.executeQuery()) + { + while(managerResult.next()) + members.add(new PlayerID(managerResult.getInt("player_id"), + source.uuid(managerResult.getBytes("player_uuid")), + managerResult.getString("player_name") + )); + } + + try(ResultSet memberResult = playerPst.executeQuery()) + { + while(memberResult.next()) + members.add(new PlayerID(memberResult.getInt("player_id"), + source.uuid(memberResult.getBytes("player_uuid")), + memberResult.getString("player_name") + )); + } + + try(ResultSet memberResult = entityPst.executeQuery()) + { + while(memberResult.next()) + members.add(new EntityID(memberResult.getInt("entity_id"), + MinecraftEntity.Type.valueOf(memberResult.getString("entity_type")), + source.uuid(memberResult.getBytes("entity_uuid")), + memberResult.getString("entity_name") + )); + } + + groups.add(new Group(this, groupId, city, groupResult.getString(2), groupResult.getString(3), members, managers)); + } + + return groups; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @NotNull + @Override + public Island createIsland(@NotNull City city, @NotNull ChunkPos chunk) + throws DataSourceException, IllegalStateException + { + int cityId = city.getId(); + if(cityId <= 0) throw new IllegalStateException("The city is not registered"); + + try(Connection transaction = connection.transaction()) + { + try + { + SQLIsland island = new SQLIsland(this, permStorage, source.createIsland(transaction, cityId, chunk.world), chunk, city); + source.createClaim(transaction, island.id, chunk); + island.city = city; + + transaction.commit(); + + return island; + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @Override + public void claim(@NotNull Island island, @NotNull ChunkPos chunk) throws DataSourceException + { + SQLIsland sqlIsland = (SQLIsland) island; + try(Connection transaction = connection.transaction()) + { + try + { + source.createClaim(connection.connect(), sqlIsland.id, chunk); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + sqlIsland.add(chunk); + + source.mineCity.reloadChunk(chunk); + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @NotNull + @Override + @SuppressWarnings("OptionalGetWithoutIsPresent") + public Island claim(@NotNull Set islands, @NotNull ChunkPos chunk) + throws DataSourceException, IllegalStateException, NoSuchElementException + { + if(islands.size() == 1) + { + Island island = islands.iterator().next(); + claim(island, chunk); + return island; + } + + List sqlIslands = islands.stream().map(island -> (SQLIsland) island).collect(Collectors.toList()); + SQLIsland mainIsland = sqlIslands.stream().max((a, b) -> a.getChunkCount() - b.getChunkCount()).get(); + List merge = sqlIslands.stream().filter(island -> island != mainIsland).collect(Collectors.toList()); + try(Connection transaction = connection.transaction(); Statement stm = transaction.createStatement()) + { + try + { + StringBuilder sb = new StringBuilder(); + merge.forEach(island -> sb.append(island.id).append(", ")); + sb.setLength(sb.length()-2); + String array = sb.toString(); + + List chunksToUpdate = new ArrayList<>(); + try(ResultSet result = stm.executeQuery("SELECT c.world_id, c.x, c.z, w.dim, w.world, w.name " + + "FROM minecity_chunks c INNER JOIN minecity_world w ON c.world_id = w.world_id " + + "WHERE c.island_id IN("+array+");") + ){ + while(result.next()) + { + WorldDim world = source.world(result.getInt(1),()->result.getInt(4),()->result.getString(5),()->result.getString(6)); + chunksToUpdate.add(new ChunkPos(world, result.getInt(2), result.getInt(3))); + } + } + + stm.executeUpdate("UPDATE `minecity_chunks` SET `island_id`="+mainIsland.id+" WHERE `island_id` IN("+array+");"); + stm.executeUpdate("DELETE FROM `minecity_islands` WHERE `island_id` IN("+array+");"); + source.createClaim(transaction, mainIsland.id, chunk); + + transaction.commit(); + + mainIsland.add(chunk); + + merge.forEach(island -> { + mainIsland.minX = Math.min(mainIsland.minX, island.minX); + mainIsland.maxX = Math.max(mainIsland.maxX, island.maxX); + mainIsland.minZ = Math.min(mainIsland.minZ, island.minZ); + mainIsland.maxZ = Math.max(mainIsland.maxZ, island.maxZ); + mainIsland.chunkCount += island.chunkCount; + + island.minX = island.maxX = island.minZ = island.maxZ = island.chunkCount = 0; + }); + + chunksToUpdate.forEach((DBConsumer) source.mineCity::reloadChunk); + return mainIsland; + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @Override + public void setSpawn(@NotNull City city, @NotNull BlockPos spawn) throws DataSourceException, IllegalStateException + { + if(city.getSpawn().equals(spawn)) + return; + + int cityId = city.getId(); + if(cityId <= 0) throw new IllegalStateException("The city is not registered"); + + try + { + Connection connection = this.connection.connect(); + int worldId = source.worldId(connection, spawn.world); + + try(PreparedStatement pst = connection.prepareStatement( + "UPDATE `minecity_city` SET `spawn_world`=?, `spawn_x`=?, `spawn_y`=?, `spawn_z`=? WHERE `city_id`=?" + )) + { + pst.setInt(1, worldId); + pst.setInt(2, spawn.x); + //noinspection SuspiciousNameCombination + pst.setInt(3, spawn.y); + pst.setInt(4, spawn.z); + pst.setInt(5, cityId); + if(pst.executeUpdate() <= 0) + throw new DataSourceException("Tried to change spawn of "+ cityId+" from "+city.getSpawn()+" to "+spawn+" but nothing changed"); + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @Override + @SuppressWarnings("SuspiciousNameCombination") + public int createPlot(Plot plot) throws DataSourceException + { + try(Connection transaction = connection.transaction()) + { + try + { + int plotId; + try(PreparedStatement pst = transaction.prepareStatement( + "INSERT INTO minecity_plots(island_id,name,display_name,owner,spawn_x,spawn_y,spawn_z,shape,tax_accepted_flat,tax_accepted_percent,tax_applied_flat,tax_applied_percent,investment) " + + "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)", + Statement.RETURN_GENERATED_KEYS + )) + { + pst.setInt(1, plot.getIsland().getId()); + pst.setString(2, plot.getIdentityName()); + pst.setString(3, plot.getName()); + source.setNullableInt(pst, 4, source.playerId(transaction, plot.getOwner().orElse(null))); + BlockPos spawn = plot.getSpawn(); + pst.setInt(5, spawn.x); + pst.setInt(6, spawn.y); + pst.setInt(7, spawn.z); + pst.setBytes(8, plot.getShape().serializeBytes()); + pst.setDouble(9, plot.getAcceptedTax().getFlat()); + pst.setDouble(10, plot.getAcceptedTax().getPercent()); + pst.setDouble(11, plot.getAppliedTax().getFlat()); + pst.setDouble(12, plot.getAppliedTax().getPercent()); + pst.setDouble(13, plot.getInvestment()); + pst.executeUpdate(); + + ResultSet result = pst.getGeneratedKeys(); + result.next(); + plotId = result.getInt(1); + } + + transaction.commit(); + return plotId; + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @Override + public void setOwner(@NotNull Plot plot, @Nullable PlayerID owner) throws DataSourceException, IllegalStateException + { + try(Connection transaction = connection.transaction()) + { + try + { + try(PreparedStatement pst = transaction.prepareStatement( + "UPDATE minecity_plots SET owner=? WHERE plot_id=?" + )) + { + source.setNullableInt(pst, 1, source.playerId(transaction, owner)); + pst.setInt(2, plot.id); + source.executeUpdate(pst, 1); + } + + transaction.commit(); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @Override + public void setShape(@NotNull Plot plot, @NotNull Shape shape, BlockPos spawn, @NotNull Island newIsland) throws DataSourceException + { + try(Connection transaction = connection.transaction()) + { + try + { + try(PreparedStatement pst = transaction.prepareStatement( + "UPDATE minecity_plots SET shape=?, island_id=? WHERE plot_id=?" + )) + { + pst.setBytes(1, shape.serializeBytes()); + pst.setInt(2, newIsland.id); + pst.setInt(3, plot.id); + source.executeUpdate(pst, 1); + } + + setSpawn(transaction, plot, spawn); + + transaction.commit(); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @Override + public void setName(@NotNull Plot plot, @NotNull String identity, @NotNull String name) throws DataSourceException + { + try(Connection transaction = connection.transaction()) + { + try + { + try(PreparedStatement pst = transaction.prepareStatement( + "UPDATE minecity_plots SET `name`=?, `display_name`=? WHERE plot_id=?" + )) + { + pst.setString(1, identity); + pst.setString(2, name); + pst.setInt(3, plot.id); + source.executeUpdate(pst, 1); + } + + transaction.commit(); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @SuppressWarnings("SuspiciousNameCombination") + private void setSpawn(@NotNull Connection transaction, @NotNull Plot plot, @NotNull BlockPos spawn) + throws DataSourceException, SQLException + { + try(PreparedStatement pst = transaction.prepareStatement( + "UPDATE minecity_plots SET spawn_x=?, spawn_y=?, spawn_z=? WHERE plot_id=?" + )) + { + pst.setInt(1, spawn.x); + pst.setInt(2, spawn.y); + pst.setInt(3, spawn.z); + pst.setInt(4, plot.id); + source.executeUpdate(pst, 1); + } + } + + @Slow + @Override + public void setSpawn(@NotNull Plot plot, @NotNull BlockPos spawn) throws DataSourceException + { + if(!spawn.world.equals(plot.getIsland().world)) + throw new IllegalArgumentException("Different world!"); + + try(Connection transaction = connection.transaction()) + { + try + { + setSpawn(transaction, plot, spawn); + transaction.commit(); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @Override + public void deletePlot(@NotNull Plot plot) throws DataSourceException + { + try(Connection transaction = connection.transaction()) + { + try(PreparedStatement pst = transaction.prepareStatement( + "DELETE FROM minecity_plots WHERE plot_id=?" + )) + { + pst.setInt(1, plot.id); + source.executeUpdate(pst, 1); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + + transaction.commit(); + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @NotNull + @Override + public Set loadPlots(@NotNull Island island) throws DataSourceException + { + try + { + try(PreparedStatement pst = connection.connect().prepareStatement( + "SELECT plot_id,`name`,display_name,spawn_x,spawn_y,spawn_z,shape, player_id,player_uuid,player_name,perm_denial_message, " + + "tax_accepted_flat, tax_accepted_percent, tax_applied_flat, tax_applied_percent, investment, price " + + "FROM minecity_plots LEFT JOIN minecity_players ON player_id=owner " + + "WHERE island_id=?" + )) + { + pst.setInt(1, island.id); + ResultSet result = pst.executeQuery(); + if(!result.next()) + return Collections.emptySet(); + + // Memory optimization, caching common Tax instances + HashMap taxCache = new HashMap<>(6); + Tax t = island.getCity().getAppliedTax(); + taxCache.put(t, t); + t = island.getCity().mineCity.costs.plotTaxApplied; + taxCache.put(t, t); + BiFunction tax = (flat, percent)-> taxCache.computeIfAbsent(new Tax(flat, percent), Function.identity()); + + HashSet plots = new HashSet<>(3); + do + { + PlayerID owner; + int ownerId = result.getInt(8); + if(ownerId > 0) + owner = PlayerID.get(source.uuid(result.getBytes(9)), result.getString(10)); + else + owner = null; + + Message denial; + String str = result.getString(11); + if(str == null) + denial = null; + else + denial = new Message("", "${msg}", new Object[]{"msg",str}); + + plots.add(new Plot(source.mineCity, this, permStorage, result.getInt(1), island, result.getString(2), result.getString(3), owner, + new BlockPos(island.world, result.getInt(4), result.getInt(5), result.getInt(6)), + Shape.deserializeBytes(result.getBytes(7)), denial, + tax.apply(result.getDouble("tax_accepted_flat"), result.getDouble("tax_accepted_percent")), + tax.apply(result.getDouble("tax_applied_flat"), result.getDouble("tax_applied_percent")), + result.getDouble("investment"), result.getDouble("price") + )); + } while(result.next()); + + return plots; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } +} diff --git a/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLConnection.java b/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLConnection.java index faf3ff20..9ae35fec 100644 --- a/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLConnection.java +++ b/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLConnection.java @@ -1,92 +1,92 @@ -package br.com.gamemods.minecity.datasource.sql; - -import br.com.gamemods.minecity.api.Slow; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; - -public class SQLConnection -{ - @NotNull - private String url; - @Nullable - private String user; - @Nullable - private byte[] pass; - private Connection connection; - public int checkTimeout = 50; - public int minCheckInterval = 5000; - private long lastCheck; - private boolean closed; - - public SQLConnection(@NotNull String url, @Nullable String user, @Nullable byte[] passwd) - { - this.url = url; - this.user = user; - this.pass = passwd; - } - - @Slow - public synchronized Connection connect() throws SQLException - { - if(closed) - throw new SQLException(new IllegalStateException("The SQL provider is closed")); - - if(connection != null) - { - try - { - if(!connection.isClosed()) - { - if(System.currentTimeMillis() - lastCheck < minCheckInterval) - return connection; - else if(connection.isValid(checkTimeout)) - { - lastCheck = System.currentTimeMillis(); - return connection; - } - } - } - catch(NullPointerException | SQLException ignored) - {} - } - - return connection = DriverManager.getConnection(url, user, pass == null? null : new String(pass)); - } - - @Slow - public synchronized void disconnect() throws SQLException - { - try - { - if(connection != null) - connection.close(); - } - catch(SQLException e) - { - connection = null; - throw e; - } - } - - @Slow - public Connection transaction() throws SQLException - { - if(closed) - throw new SQLException(new IllegalStateException("The SQL provider is closed")); - - Connection connection = DriverManager.getConnection(url, user, pass == null? null : new String(pass)); - connection.setAutoCommit(false); - return connection; - } - - @Slow - public void close() throws SQLException - { - closed = true; - disconnect(); - } -} +package br.com.gamemods.minecity.datasource.sql; + +import br.com.gamemods.minecity.api.Slow; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class SQLConnection +{ + @NotNull + private String url; + @Nullable + private String user; + @Nullable + private byte[] pass; + private Connection connection; + public int checkTimeout = 50; + public int minCheckInterval = 5000; + private long lastCheck; + private boolean closed; + + public SQLConnection(@NotNull String url, @Nullable String user, @Nullable byte[] passwd) + { + this.url = url; + this.user = user; + this.pass = passwd; + } + + @Slow + public synchronized Connection connect() throws SQLException + { + if(closed) + throw new SQLException(new IllegalStateException("The SQL provider is closed")); + + if(connection != null) + { + try + { + if(!connection.isClosed()) + { + if(System.currentTimeMillis() - lastCheck < minCheckInterval) + return connection; + else if(connection.isValid(checkTimeout)) + { + lastCheck = System.currentTimeMillis(); + return connection; + } + } + } + catch(NullPointerException | SQLException ignored) + {} + } + + return connection = DriverManager.getConnection(url, user, pass == null? null : new String(pass)); + } + + @Slow + public synchronized void disconnect() throws SQLException + { + try + { + if(connection != null) + connection.close(); + } + catch(SQLException e) + { + connection = null; + throw e; + } + } + + @Slow + public Connection transaction() throws SQLException + { + if(closed) + throw new SQLException(new IllegalStateException("The SQL provider is closed")); + + Connection connection = DriverManager.getConnection(url, user, pass == null? null : new String(pass)); + connection.setAutoCommit(false); + return connection; + } + + @Slow + public void close() throws SQLException + { + closed = true; + disconnect(); + } +} diff --git a/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLIsland.java b/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLIsland.java index 3a30cb70..71900ce4 100644 --- a/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLIsland.java +++ b/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLIsland.java @@ -1,123 +1,123 @@ -package br.com.gamemods.minecity.datasource.sql; - -import br.com.gamemods.minecity.api.world.ChunkPos; -import br.com.gamemods.minecity.api.world.WorldDim; -import br.com.gamemods.minecity.datasource.api.DataSourceException; -import br.com.gamemods.minecity.datasource.api.ICityStorage; -import br.com.gamemods.minecity.datasource.api.IExceptPermissionStorage; -import br.com.gamemods.minecity.structure.City; -import br.com.gamemods.minecity.structure.Inconsistency; -import br.com.gamemods.minecity.structure.Island; -import br.com.gamemods.minecity.structure.Plot; -import org.jetbrains.annotations.NotNull; - -import java.util.Collections; -import java.util.Set; - -final class SQLIsland extends Island -{ - int minX; - int maxX; - int minZ; - int maxZ; - int chunkCount; - - @NotNull - City city; - - SQLIsland(City city, ICityStorage storage, IExceptPermissionStorage permissionStorage, int id, - int minX, int maxX, int minZ, int maxZ, int chunkCount, @NotNull WorldDim world, Set plots) - { - super(city, storage, permissionStorage, id, world, plots); - this.minX = minX; - this.maxX = maxX; - this.minZ = minZ; - this.maxZ = maxZ; - this.chunkCount = chunkCount; - this.city = Inconsistency.getInconsistentCity(); - } - - SQLIsland(ICityStorage storage, IExceptPermissionStorage permissionStorage, int id, - int minX, int maxX, int minZ, int maxZ, int chunkCount, @NotNull WorldDim world, @NotNull City city) - throws DataSourceException - { - super(city, storage, permissionStorage, id, world); - this.minX = minX; - this.maxX = maxX; - this.minZ = minZ; - this.maxZ = maxZ; - this.chunkCount = chunkCount; - this.city = city; - } - - SQLIsland(ICityStorage storage, IExceptPermissionStorage permissionStorage, int id, ChunkPos chunk, @NotNull City city) - { - super(city, storage, permissionStorage, id, chunk.world, Collections.emptySet()); - minX = maxX = chunk.x; - minZ = maxZ = chunk.z; - chunkCount = 1; - this.city = city; - } - - void add(ChunkPos chunk) - { - minX = Math.min(minX, chunk.x); - maxX = Math.max(maxX, chunk.x); - minZ = Math.min(minZ, chunk.z); - maxZ = Math.max(maxZ, chunk.z); - chunkCount++; - } - - void relocate(Plot plot, SQLIsland to) - { - String id = plot.getIdentityName(); - plots.remove(id, plot); - to.plots.put(id, plot); - plot.relocate(to); - } - - @Override - public int getSizeX() - { - return chunkCount == 0? 0 : maxX - minX + 1; - } - - @Override - public int getSizeZ() - { - return chunkCount == 0? 0 : maxZ - minZ + 1; - } - - @Override - public int getChunkCount() - { - return chunkCount; - } - - @Override - public boolean equals(Object o) - { - if(this == o) return true; - if(o == null || getClass() != o.getClass()) return false; - - SQLIsland sqlIsland = (SQLIsland) o; - return id == sqlIsland.id; - - } - - @Override - public int hashCode() - { - return id; - } - - @Override - public String toString() - { - return "SQLIsland{" + - "id=" + id + - ", world=" + world + - ", city=" + city + - '}'; - } -} +package br.com.gamemods.minecity.datasource.sql; + +import br.com.gamemods.minecity.api.world.ChunkPos; +import br.com.gamemods.minecity.api.world.WorldDim; +import br.com.gamemods.minecity.datasource.api.DataSourceException; +import br.com.gamemods.minecity.datasource.api.ICityStorage; +import br.com.gamemods.minecity.datasource.api.IExceptPermissionStorage; +import br.com.gamemods.minecity.structure.City; +import br.com.gamemods.minecity.structure.Inconsistency; +import br.com.gamemods.minecity.structure.Island; +import br.com.gamemods.minecity.structure.Plot; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.Set; + +final class SQLIsland extends Island +{ + int minX; + int maxX; + int minZ; + int maxZ; + int chunkCount; + + @NotNull + City city; + + SQLIsland(City city, ICityStorage storage, IExceptPermissionStorage permissionStorage, int id, + int minX, int maxX, int minZ, int maxZ, int chunkCount, @NotNull WorldDim world, Set plots) + { + super(city, storage, permissionStorage, id, world, plots); + this.minX = minX; + this.maxX = maxX; + this.minZ = minZ; + this.maxZ = maxZ; + this.chunkCount = chunkCount; + this.city = Inconsistency.getInconsistentCity(); + } + + SQLIsland(ICityStorage storage, IExceptPermissionStorage permissionStorage, int id, + int minX, int maxX, int minZ, int maxZ, int chunkCount, @NotNull WorldDim world, @NotNull City city) + throws DataSourceException + { + super(city, storage, permissionStorage, id, world); + this.minX = minX; + this.maxX = maxX; + this.minZ = minZ; + this.maxZ = maxZ; + this.chunkCount = chunkCount; + this.city = city; + } + + SQLIsland(ICityStorage storage, IExceptPermissionStorage permissionStorage, int id, ChunkPos chunk, @NotNull City city) + { + super(city, storage, permissionStorage, id, chunk.world, Collections.emptySet()); + minX = maxX = chunk.x; + minZ = maxZ = chunk.z; + chunkCount = 1; + this.city = city; + } + + void add(ChunkPos chunk) + { + minX = Math.min(minX, chunk.x); + maxX = Math.max(maxX, chunk.x); + minZ = Math.min(minZ, chunk.z); + maxZ = Math.max(maxZ, chunk.z); + chunkCount++; + } + + void relocate(Plot plot, SQLIsland to) + { + String id = plot.getIdentityName(); + plots.remove(id, plot); + to.plots.put(id, plot); + plot.relocate(to); + } + + @Override + public int getSizeX() + { + return chunkCount == 0? 0 : maxX - minX + 1; + } + + @Override + public int getSizeZ() + { + return chunkCount == 0? 0 : maxZ - minZ + 1; + } + + @Override + public int getChunkCount() + { + return chunkCount; + } + + @Override + public boolean equals(Object o) + { + if(this == o) return true; + if(o == null || getClass() != o.getClass()) return false; + + SQLIsland sqlIsland = (SQLIsland) o; + return id == sqlIsland.id; + + } + + @Override + public int hashCode() + { + return id; + } + + @Override + public String toString() + { + return "SQLIsland{" + + "id=" + id + + ", world=" + world + + ", city=" + city + + '}'; + } +} diff --git a/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLSource.java b/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLSource.java index 93d4d34b..c5c4118d 100644 --- a/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLSource.java +++ b/Core/src/main/java/br/com/gamemods/minecity/datasource/sql/SQLSource.java @@ -1,861 +1,861 @@ -package br.com.gamemods.minecity.datasource.sql; - -import br.com.gamemods.minecity.MineCity; -import br.com.gamemods.minecity.MineCityConfig; -import br.com.gamemods.minecity.api.PlayerID; -import br.com.gamemods.minecity.api.Slow; -import br.com.gamemods.minecity.api.command.Message; -import br.com.gamemods.minecity.api.permission.*; -import br.com.gamemods.minecity.api.world.BlockPos; -import br.com.gamemods.minecity.api.world.ChunkPos; -import br.com.gamemods.minecity.api.world.WorldDim; -import br.com.gamemods.minecity.datasource.api.CityCreationResult; -import br.com.gamemods.minecity.datasource.api.DataSourceException; -import br.com.gamemods.minecity.datasource.api.IDataSource; -import br.com.gamemods.minecity.datasource.api.unchecked.DBSupplier; -import br.com.gamemods.minecity.datasource.api.unchecked.UncheckedDataSourceException; -import br.com.gamemods.minecity.economy.Tax; -import br.com.gamemods.minecity.structure.*; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.ByteBuffer; -import java.sql.*; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Supplier; -import java.util.stream.Stream; - -import static br.com.gamemods.minecity.api.StringUtil.identity; - -public class SQLSource implements IDataSource -{ - private static final int VERSION = 7; - - @NotNull - public final MineCity mineCity; - @NotNull - private final SQLConnection connection; - @NotNull - private final SQLCityStorage cityStorage; - @NotNull - private final SQLPermStorage permStorage; - @NotNull - final Map cityMap = new HashMap<>(); - final Map worldDimMap = new ConcurrentHashMap<>(3); - final Set cityNames = new HashSet<>(); - final Map> groupNames = new HashMap<>(); - - public SQLSource(@NotNull MineCity mineCity, @NotNull MineCityConfig config) - { - this.mineCity = mineCity; - connection = new SQLConnection(config.dbUrl, config.dbUser, config.dbPass != null? config.dbPass.clone() : null); - if(config.dbPass != null) - Arrays.fill(config.dbPass, (byte) 0); - - permStorage = new SQLPermStorage(this, connection); - cityStorage = new SQLCityStorage(this, connection, permStorage); - } - - protected WorldDim world(int id, DBSupplier dim, DBSupplier dir, DBSupplier name) - { - WorldDim world = worldDimMap.get(id); - if(world != null) - return world; - - int dimI = dim.get(); - String dirS = dir.get(); - world = mineCity.worldProvider.map(p-> p.getWorld(dimI, dirS)).orElseGet(()-> new WorldDim(dimI, dirS)); - - world.setDataSourceId(id); - worldDimMap.put(id, world); - - if(name != null) - world.name = name.get(); - - return world; - } - - @Slow - private Optional loadCity(Connection connection, int id, @Nullable String identity) throws SQLException, DataSourceException - { - synchronized(cityMap) - { - try(PreparedStatement pst = connection.prepareStatement( - "SELECT `c`.`name`, `owner`, `o`.`player_uuid`, `o`.`player_name`, `spawn_world`, `spawn_x`, `spawn_y`, `spawn_z`, " + - "`w`.`dim`, `w`.`world`, `w`.`name`, `display_name`, c.`perm_denial_message`, `city_id`," + - "`tax_applied_flat`, `tax_applied_percent`, `investment`, `price` " + - "FROM `minecity_city` AS `c` " + - "LEFT JOIN `minecity_players` AS `o` ON `owner` = `o`.`player_id` "+ - "LEFT JOIN `minecity_world` AS `w` ON `spawn_world` = `w`.`world_id` "+ - " WHERE "+(identity == null?"`city_id`=?":"`c`.`name`=?") - )) - { - if(identity == null) - pst.setInt(1, id); - else - pst.setString(1, identity); - - ResultSet result = pst.executeQuery(); - if(!result.next()) - return Optional.empty(); - - PlayerID owner; - int ownerId = result.getInt(2); - if(ownerId <= 0) owner = null; - else - owner = new PlayerID(ownerId, uuid(result.getBytes(3)), result.getString(4)); - - WorldDim world = world(result.getInt(5), ()->result.getInt(9), ()->result.getString(10), ()->result.getString(11)); - BlockPos spawn = new BlockPos(world, result.getInt(6), result.getInt(7), result.getInt(8)); - - String name = result.getString(1); - String displayName = result.getString(12); - - String str = result.getString("perm_denial_message"); - Message message = str == null? null : new Message("", "${msg}", new Object[]{"msg",str}); - id = result.getInt("city_id"); - Tax tax = new Tax(result.getDouble("tax_applied_flat"), result.getDouble("tax_applied_percent")); - if(tax.equals(mineCity.costs.cityTaxApplied)) - tax = mineCity.costs.cityTaxApplied; - double investment = result.getDouble("investment"); - double price = result.getDouble("price"); - pst.close(); - - City city = new City(mineCity, name, displayName, owner, spawn, id, cityStorage, permStorage, message, tax, investment, price); - cityMap.put(id, city); - return Optional.of(city); - } - } - } - - void executeUpdate(PreparedStatement pst, int expected) throws DataSourceException, SQLException - { - int changes = pst.executeUpdate(); - if(changes != expected) - throw new DataSourceException("Expected "+expected+" but got "+changes); - } - - private int insertWorld(Connection connection, WorldDim world) throws SQLException - { - try(PreparedStatement pst = connection.prepareStatement( - "INSERT INTO `minecity_world`(`dim`,`world`,`name`) VALUES(?,?,?)", - Statement.RETURN_GENERATED_KEYS - )) - { - pst.setInt(1, world.dim); - pst.setString(2, world.dir); - setNullableString(pst, 3, world.name); - pst.executeUpdate(); - ResultSet keys = pst.getGeneratedKeys(); - keys.next(); - return keys.getInt(1); - } - } - - @Slow - int worldId(Connection connection, WorldDim world) throws DataSourceException - { - int dataSourceId = world.getDataSourceId(); - if(dataSourceId > 0) return dataSourceId; - - int id = 0; - try - { - try(PreparedStatement pst = connection.prepareStatement( - "SELECT `world_id` FROM `minecity_world` WHERE `dim`=? AND `world`=?" - )) - { - pst.setInt(1, world.dim); - pst.setString(2, world.dir); - ResultSet result = pst.executeQuery(); - if(result.next()) - world.setDataSourceId(id = result.getInt(1)); - } - - if(id <= 0) - insertWorld(connection, world); - - world.setDataSourceId(id); - worldDimMap.putIfAbsent(id, world); - return id; - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Contract("null->null;!null->!null") - byte[] uuid(UUID uuid) - { - if(uuid == null) - return null; - - byte[] bytes = new byte[16]; - ByteBuffer buffer = ByteBuffer.wrap(bytes); - buffer.putLong(uuid.getMostSignificantBits()); - buffer.putLong(uuid.getLeastSignificantBits()); - return bytes; - } - - @Contract("null->null;!null->!null") - UUID uuid(byte[] bytes) throws DataSourceException - { - if(bytes == null) - return null; - - try - { - ByteBuffer bb = ByteBuffer.wrap(bytes); - long firstLong = bb.getLong(); - long secondLong = bb.getLong(); - return new UUID(firstLong, secondLong); - } - catch(Exception e) - { - throw new DataSourceException("Bad UUID", e); - } - } - - void setNullableInt(PreparedStatement pst, int field, int val) throws SQLException - { - if(val == 0) - pst.setNull(field, Types.INTEGER); - else - pst.setInt(field, val); - } - - void setNullableString(PreparedStatement pst, int field, String val) throws SQLException - { - if(val == null) - pst.setNull(field, Types.VARCHAR); - else - pst.setString(field, val); - } - - @Slow - int playerId(Connection connection, @Nullable OptionalPlayer player) throws DataSourceException - { - PlayerID playerId; - if(player == null || (playerId = player.player()) == null) return 0; - int id = player.getDataSourceId(); - if(id > 0) return id; - - try - { - try(PreparedStatement pst = connection.prepareStatement( - "SELECT `player_id` FROM `minecity_players` WHERE `player_uuid`=?" - )) - { - pst.setBytes(1, uuid(playerId.uniqueId)); - ResultSet result = pst.executeQuery(); - if(result.next()) - { - id = result.getInt(1); - playerId.setDataSourceId(id); - return id; - } - } - - try(PreparedStatement pst = connection.prepareStatement( - "INSERT INTO `minecity_players`(`player_uuid`, `player_name`) VALUES(?,?)" - , Statement.RETURN_GENERATED_KEYS - )) - { - pst.setBytes(1, uuid(playerId.uniqueId)); - pst.setString(2, playerId.getName()); - pst.executeUpdate(); - ResultSet keys = pst.getGeneratedKeys(); - keys.next(); - id = keys.getInt(1); - playerId.setDataSourceId(id); - return id; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - int entityId(Connection connection, @Nullable EntityID entity) throws DataSourceException - { - if(entity == null) return 0; - int id = entity.getDataSourceId(); - if(id > 0) return id; - - try - { - try(PreparedStatement pst = connection.prepareStatement( - "SELECT `entity_id` FROM `minecity_entities` WHERE `entity_uuid`=?" - )) - { - pst.setBytes(1, uuid(entity.uniqueId)); - ResultSet result = pst.executeQuery(); - if(result.next()) - { - id = result.getInt(1); - entity.setDataSourceId(id); - return id; - } - } - - try(PreparedStatement pst = connection.prepareStatement( - "INSERT INTO `minecity_entities`(`entity_uuid`, `entity_name`, entity_type) VALUES(?,?,?)" - , Statement.RETURN_GENERATED_KEYS - )) - { - pst.setBytes(1, uuid(entity.uniqueId)); - pst.setString(2, entity.getName()); - pst.setString(3, entity.getEntityType().name()); - pst.executeUpdate(); - ResultSet keys = pst.getGeneratedKeys(); - keys.next(); - id = keys.getInt(1); - entity.setDataSourceId(id); - return id; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - private City city(Connection connection, int cityId) throws SQLException, DataSourceException - { - synchronized(cityMap) - { - City city = cityMap.get(cityId); - if(city != null) - return city; - - return loadCity(connection, cityId, null).orElseThrow(()-> new DataSourceException("City ID "+cityId+" not found")); - } - } - - @Slow - @Nullable - @Override - public ClaimedChunk getCityChunk(@NotNull ChunkPos pos) throws DataSourceException - { - try - { - Connection connection = this.connection.connect(); - try(PreparedStatement pst = connection.prepareStatement( - "SELECT `i`.`city_id`, `i`.`island_id`, reserve FROM `minecity_chunks` AS `c` " + - "INNER JOIN `minecity_world` AS `w` ON `c`.`world_id`=`w`.`world_id` " + - "INNER JOIN `minecity_islands` AS `i` ON `c`.`island_id`=`i`.`island_id` "+ - "WHERE `w`.`dim`=? AND `w`.`world`=? AND `c`.`x`=? AND `c`.`z`=?;" - )) - { - pst.setInt(1, pos.world.dim); - pst.setString(2, pos.world.dir); - pst.setInt(3, pos.x); - pst.setInt(4, pos.z); - ResultSet result = pst.executeQuery(); - if(!result.next()) - return null; - int cityId = result.getInt(1); - int islandId = result.getInt(2); - boolean reserve = result.getBoolean(3); - pst.close(); - - City city = city(connection, cityId); - Island island = city.getIsland(islandId); - return new ClaimedChunk(island != null? island : Inconsistency.getInconsistentIsland(), pos, reserve); - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - catch(UncheckedDataSourceException e) - { - throw e.getCause(); - } - } - - @Slow - int createIsland(Connection transaction, int cityId, WorldDim world) throws DataSourceException, SQLException - { - int worldId = worldId(transaction, world); - int islandId; - try(PreparedStatement pst = transaction.prepareStatement( - "INSERT INTO `minecity_islands`(world_id, city_id) VALUES(?,?)", - Statement.RETURN_GENERATED_KEYS - )) - { - pst.setInt(1, worldId); - pst.setInt(2, cityId); - pst.executeUpdate(); - ResultSet keys = pst.getGeneratedKeys(); - keys.next(); - islandId = keys.getInt(1); - } - - return islandId; - } - - @Slow - void createClaim(Connection connection, int islandId, ChunkPos chunk) throws SQLException, DataSourceException - { - int worldId = worldId(connection, chunk.world); - try(PreparedStatement pst = connection.prepareStatement( - "DELETE FROM minecity_chunks WHERE world_id=? AND x=? AND z=? AND reserve=1" - )) - { - pst.setInt(1, worldId); - pst.setInt(2, chunk.x); - pst.setInt(3, chunk.z); - pst.executeUpdate(); - } - try(PreparedStatement pst = connection.prepareStatement( - "INSERT INTO `minecity_chunks`(world_id, x, z, island_id, reserve) VALUES(?,?,?,?,0)" - )) - { - pst.setInt(1, worldId); - pst.setInt(2, chunk.x); - pst.setInt(3, chunk.z); - pst.setInt(4, islandId); - if(pst.executeUpdate() <= 0) - throw new DataSourceException("Failed to claim the spawn chunk"); - } - } - - @Nullable - @Override - public String checkNameConflict(@NotNull String name) - { - name = identity(name); - for(String cityName : cityNames) - if(identity(cityName).equals(name)) - return cityName; - - return null; - } - - @Slow - @NotNull - @Override - public CityCreationResult createCity(@NotNull City city) throws DataSourceException, IllegalStateException - { - if(city.getId() > 0) - throw new IllegalStateException(); - - try - { - BlockPos spawn = city.getSpawn(); - ChunkPos spawnChunk = spawn.getChunk(); - ClaimedChunk claim = getCityChunk(spawnChunk); - if(claim != null) - throw new IllegalStateException("The chunk " + spawnChunk + " is already claimed: " + claim); - - int islandId; - try(Connection connection = this.connection.transaction()) - { - try - { - int worldId = worldId(connection, spawn.world); - int cityId; - try(PreparedStatement pst = connection.prepareStatement( - "INSERT INTO `minecity_city`(name, owner, spawn_world, spawn_x, spawn_y, spawn_z, display_name, tax_applied_flat, tax_applied_percent, investment) " + - "VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )", - Statement.RETURN_GENERATED_KEYS - )) - { - pst.setString(1, city.getIdentityName()); - setNullableInt(pst, 2, playerId(connection, city.owner())); - pst.setInt(3, worldId); - pst.setInt(4, spawn.x); - //noinspection SuspiciousNameCombination - pst.setInt(5, spawn.y); - pst.setInt(6, spawn.z); - pst.setString(7, city.getName()); - pst.setDouble(8, city.getAppliedTax().getFlat()); - pst.setDouble(9, city.getAppliedTax().getPercent()); - pst.setDouble(10, city.getInvestment()); - pst.executeUpdate(); - ResultSet keys = pst.getGeneratedKeys(); - keys.next(); - cityId = keys.getInt(1); - city.setId(cityId); - cityMap.put(cityId, city); - } - - islandId = createIsland(connection, cityId, spawnChunk.world); - createClaim(connection, islandId, spawnChunk); - connection.commit(); - cityNames.add(city.getName()); - } - catch(Exception e) - { - connection.rollback(); - throw e; - } - } - - return new CityCreationResult(cityStorage, permStorage, - new SQLIsland(cityStorage, permStorage, islandId, spawnChunk, city), - Collections.emptyList() - ); - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @Override - public void initDB() throws DataSourceException, IOException - { - try(Connection transaction = connection.transaction()) - { - try(Statement stm = transaction.createStatement()) - { - ResultSet result; - int version; - try - { - result = stm.executeQuery("SELECT `value` FROM `minecity_setup` WHERE `property`='version';"); - result.next(); - version = result.getInt(1); - result.close(); - } - catch(SQLException e) - { - System.out.println("[MineCity] Installing the SQL database version "+VERSION); - ScriptRunner runner = new ScriptRunner(transaction, false, true); - runner.setLogWriter(null); - runner.runScript(new InputStreamReader( - getClass().getResourceAsStream("/assets/minecity/db/setup.sql"), "UTF-8" - )); - transaction.commit(); - return; - } - - if(version > VERSION) - throw new DataSourceException("Unsupported database version: "+version); - - if(version < VERSION) - { - System.out.println("[MineCity] Starting the database upgrade from "+version+" to "+VERSION); - for(; version < VERSION; version++) - { - System.out.println("[MineCity] Upgrading to version "+(version+1)); - ScriptRunner runner = new ScriptRunner(transaction, false, true); - runner.setLogWriter(null); - runner.runScript(new InputStreamReader( - getClass().getResourceAsStream("/assets/minecity/db/update_"+version+".sql"), "UTF-8" - )); - } - - transaction.commit(); - System.out.println("[MineCity] The database was successfully upgraded"); - } - - result = stm.executeQuery("SELECT `display_name` FROM `minecity_city` "); - while(result.next()) - cityNames.add(result.getString(1)); - result.close(); - - result = stm.executeQuery("SELECT c.name, g.name FROM minecity_groups g INNER JOIN minecity_city c ON c.city_id = g.city_id"); - while(result.next()) - groupNames.computeIfAbsent(result.getString(1), n-> new HashSet<>(1)).add(result.getString(2)); - } - catch(Exception e) - { - transaction.rollback(); - throw e; - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @NotNull - @Override - public Optional getCityByName(@NotNull String name) throws DataSourceException - { - name = identity(name); - if(name.length() < 3) - return Optional.empty(); - - synchronized(cityMap) - { - for(City city : cityMap.values()) - if(city.getIdentityName().equals(name)) - return Optional.of(city); - - try - { - return loadCity(connection.connect(), 0, name); - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - } - - @NotNull - @Override - public Set getEntityGroups(Identity identity) throws DataSourceException - { - try - { - switch(identity.getType()) - { - case PLAYER: - Connection connection = this.connection.connect(); - try(PreparedStatement pst = connection.prepareStatement( - "SELECT g.group_id, g.city_id, g.display_name, c.display_name AS home " + - "FROM minecity_group_players gp " + - "INNER JOIN minecity_groups g ON g.group_id = gp.group_id " + - "INNER JOIN minecity_city c ON c.city_id = g.city_id " + - "WHERE gp.player_id=?" - )) - { - pst.setInt(1, playerId(connection, (PlayerID) identity)); - ResultSet result = pst.executeQuery(); - Set set = new HashSet<>(2); - while(result.next()) - { - int groupId = result.getInt(1); - int cityId = result.getInt(2); - String name = result.getString(3); - String home = result.getString(4); - GroupID group = Optional.ofNullable(cityMap.get(cityId)) - .map(c-> c.getGroup(groupId)).map(Group::getIdentity) - .orElseGet(()-> new GroupID(groupId, name, home, cityId)) - ; - set.add(group); - } - - return set; - } - - case ENTITY: - connection = this.connection.connect(); - int id = identity.getDataSourceId(); - try(PreparedStatement pst = connection.prepareStatement( - "SELECT g.group_id, g.city_id, g.display_name, c.display_name AS home " + - "FROM minecity_group_entities ge " + - "INNER JOIN minecity_groups g ON g.group_id = ge.group_id " + - "INNER JOIN minecity_city c ON c.city_id = g.city_id " + - (id > 0 ? "WHERE ge.entity_id = ?" : - "INNER JOIN minecity_entities e ON ge.entity_id = e.entity_id " + - "WHERE e.entity_uuid = ?") - )) - { - if(id > 0) - pst.setInt(1, id); - else - pst.setBytes(1, uuid(((EntityID)identity).getUniqueId())); - - ResultSet result = pst.executeQuery(); - Set set = new HashSet<>(2); - while(result.next()) - { - int groupId = result.getInt(1); - int cityId = result.getInt(2); - String name = result.getString(3); - String home = result.getString(4); - GroupID group = Optional.ofNullable(cityMap.get(cityId)) - .map(c-> c.getGroup(groupId)).map(Group::getIdentity) - .orElseGet(()-> new GroupID(groupId, name, home, cityId)) - ; - set.add(group); - } - - return set; - } - - default: - return Collections.emptySet(); - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @NotNull - @Override - public Optional getPlayer(@NotNull String name) throws DataSourceException - { - try(PreparedStatement pst = connection.connect().prepareStatement( - "SELECT player_id, player_uuid, player_name FROM minecity_players WHERE player_name=?" - )) - { - pst.setString(1, name); - ResultSet result = pst.executeQuery(); - if(!result.next()) - return Optional.empty(); - return Optional.of(new PlayerID(result.getInt(1), uuid(result.getBytes(2)), result.getString(3))); - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @NotNull - @Override - public IslandArea getArea(@NotNull Island island) - throws DataSourceException, ClassCastException, IllegalArgumentException - { - SQLIsland sqlIsland = (SQLIsland) island; - - try - { - Connection connection = this.connection.connect(); - try(PreparedStatement pst = connection.prepareStatement( - "SELECT x, z FROM minecity_chunks WHERE island_id=? AND world_id=? AND reserve=0" - )) - { - pst.setInt(1, sqlIsland.id); - pst.setInt(2, worldId(connection, sqlIsland.world)); - ResultSet result = pst.executeQuery(); - List list = new ArrayList<>(); - while(result.next()) - list.add(new ChunkPos(sqlIsland.world, result.getInt(1), result.getInt(2))); - - return new IslandArea(island, list); - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @NotNull - @Override - public Nature getNature(@NotNull WorldDim world) throws DataSourceException - { - try - { - int id = world.getDataSourceId(); - Connection connection = this.connection.connect(); - try(PreparedStatement pst = connection.prepareStatement( - "SELECT world_id, `name`, city_creations, perm_denial_message " + - "FROM minecity_world " + - "WHERE "+(id > 0? "world_id=?":"dim=? AND world=?") - )) - { - if(id > 0) - pst.setInt(1, id); - else - { - pst.setInt(1, world.dim); - pst.setString(2, world.dir); - } - - ResultSet result = pst.executeQuery(); - if(result.next()) - { - if(id == 0) - world.setDataSourceId(result.getInt(1)); - - world.name = result.getString(2); - - boolean cityCreation = result.getBoolean(3); - - String str = result.getString(4); - Message message; - if(str == null) - message = null; - else - message = Message.string(str); - pst.close(); - - return new Nature(mineCity, world, message, permStorage, permStorage, !cityCreation); - } - } - - if(id == 0) - world.setDataSourceId(insertWorld(connection, world)); - - return new Nature(mineCity, world, permStorage, permStorage); - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @NotNull - @Override - public Supplier> cityNameSupplier() - { - return cityNames::stream; - } - - @NotNull - @Override - public Optional> getGroupNames(@NotNull String cityName) - { - Set set = groupNames.get(identity(cityName)); - if(set == null) - return Optional.empty(); - return Optional.of(Collections.unmodifiableSet(set)); - } - - @NotNull - @Override - public Map> getGroups() - { - return Collections.unmodifiableMap(groupNames); - } - - @Override - public int getCityCount(PlayerID playerId) throws DataSourceException - { - try - { - Connection connection = this.connection.connect(); - try(PreparedStatement pst = connection.prepareStatement( - "SELECT count(*) FROM `minecity_city` WHERE `owner`=?" - )) - { - pst.setInt(1, playerId(connection, playerId)); - ResultSet result = pst.executeQuery(); - result.next(); - return result.getInt(1); - } - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } - - @Slow - @Override - public void close() throws DataSourceException - { - try - { - connection.close(); - } - catch(SQLException e) - { - throw new DataSourceException(e); - } - } -} +package br.com.gamemods.minecity.datasource.sql; + +import br.com.gamemods.minecity.MineCity; +import br.com.gamemods.minecity.MineCityConfig; +import br.com.gamemods.minecity.api.PlayerID; +import br.com.gamemods.minecity.api.Slow; +import br.com.gamemods.minecity.api.command.Message; +import br.com.gamemods.minecity.api.permission.*; +import br.com.gamemods.minecity.api.world.BlockPos; +import br.com.gamemods.minecity.api.world.ChunkPos; +import br.com.gamemods.minecity.api.world.WorldDim; +import br.com.gamemods.minecity.datasource.api.CityCreationResult; +import br.com.gamemods.minecity.datasource.api.DataSourceException; +import br.com.gamemods.minecity.datasource.api.IDataSource; +import br.com.gamemods.minecity.datasource.api.unchecked.DBSupplier; +import br.com.gamemods.minecity.datasource.api.unchecked.UncheckedDataSourceException; +import br.com.gamemods.minecity.economy.Tax; +import br.com.gamemods.minecity.structure.*; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.sql.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static br.com.gamemods.minecity.api.StringUtil.identity; + +public class SQLSource implements IDataSource +{ + private static final int VERSION = 7; + + @NotNull + public final MineCity mineCity; + @NotNull + private final SQLConnection connection; + @NotNull + private final SQLCityStorage cityStorage; + @NotNull + private final SQLPermStorage permStorage; + @NotNull + final Map cityMap = new HashMap<>(); + final Map worldDimMap = new ConcurrentHashMap<>(3); + final Set cityNames = new HashSet<>(); + final Map> groupNames = new HashMap<>(); + + public SQLSource(@NotNull MineCity mineCity, @NotNull MineCityConfig config) + { + this.mineCity = mineCity; + connection = new SQLConnection(config.dbUrl, config.dbUser, config.dbPass != null? config.dbPass.clone() : null); + if(config.dbPass != null) + Arrays.fill(config.dbPass, (byte) 0); + + permStorage = new SQLPermStorage(this, connection); + cityStorage = new SQLCityStorage(this, connection, permStorage); + } + + protected WorldDim world(int id, DBSupplier dim, DBSupplier dir, DBSupplier name) + { + WorldDim world = worldDimMap.get(id); + if(world != null) + return world; + + int dimI = dim.get(); + String dirS = dir.get(); + world = mineCity.worldProvider.map(p-> p.getWorld(dimI, dirS)).orElseGet(()-> new WorldDim(dimI, dirS)); + + world.setDataSourceId(id); + worldDimMap.put(id, world); + + if(name != null) + world.name = name.get(); + + return world; + } + + @Slow + private Optional loadCity(Connection connection, int id, @Nullable String identity) throws SQLException, DataSourceException + { + synchronized(cityMap) + { + try(PreparedStatement pst = connection.prepareStatement( + "SELECT `c`.`name`, `owner`, `o`.`player_uuid`, `o`.`player_name`, `spawn_world`, `spawn_x`, `spawn_y`, `spawn_z`, " + + "`w`.`dim`, `w`.`world`, `w`.`name`, `display_name`, c.`perm_denial_message`, `city_id`," + + "`tax_applied_flat`, `tax_applied_percent`, `investment`, `price` " + + "FROM `minecity_city` AS `c` " + + "LEFT JOIN `minecity_players` AS `o` ON `owner` = `o`.`player_id` "+ + "LEFT JOIN `minecity_world` AS `w` ON `spawn_world` = `w`.`world_id` "+ + " WHERE "+(identity == null?"`city_id`=?":"`c`.`name`=?") + )) + { + if(identity == null) + pst.setInt(1, id); + else + pst.setString(1, identity); + + ResultSet result = pst.executeQuery(); + if(!result.next()) + return Optional.empty(); + + PlayerID owner; + int ownerId = result.getInt(2); + if(ownerId <= 0) owner = null; + else + owner = new PlayerID(ownerId, uuid(result.getBytes(3)), result.getString(4)); + + WorldDim world = world(result.getInt(5), ()->result.getInt(9), ()->result.getString(10), ()->result.getString(11)); + BlockPos spawn = new BlockPos(world, result.getInt(6), result.getInt(7), result.getInt(8)); + + String name = result.getString(1); + String displayName = result.getString(12); + + String str = result.getString("perm_denial_message"); + Message message = str == null? null : new Message("", "${msg}", new Object[]{"msg",str}); + id = result.getInt("city_id"); + Tax tax = new Tax(result.getDouble("tax_applied_flat"), result.getDouble("tax_applied_percent")); + if(tax.equals(mineCity.costs.cityTaxApplied)) + tax = mineCity.costs.cityTaxApplied; + double investment = result.getDouble("investment"); + double price = result.getDouble("price"); + pst.close(); + + City city = new City(mineCity, name, displayName, owner, spawn, id, cityStorage, permStorage, message, tax, investment, price); + cityMap.put(id, city); + return Optional.of(city); + } + } + } + + void executeUpdate(PreparedStatement pst, int expected) throws DataSourceException, SQLException + { + int changes = pst.executeUpdate(); + if(changes != expected) + throw new DataSourceException("Expected "+expected+" but got "+changes); + } + + private int insertWorld(Connection connection, WorldDim world) throws SQLException + { + try(PreparedStatement pst = connection.prepareStatement( + "INSERT INTO `minecity_world`(`dim`,`world`,`name`) VALUES(?,?,?)", + Statement.RETURN_GENERATED_KEYS + )) + { + pst.setInt(1, world.dim); + pst.setString(2, world.dir); + setNullableString(pst, 3, world.name); + pst.executeUpdate(); + ResultSet keys = pst.getGeneratedKeys(); + keys.next(); + return keys.getInt(1); + } + } + + @Slow + int worldId(Connection connection, WorldDim world) throws DataSourceException + { + int dataSourceId = world.getDataSourceId(); + if(dataSourceId > 0) return dataSourceId; + + int id = 0; + try + { + try(PreparedStatement pst = connection.prepareStatement( + "SELECT `world_id` FROM `minecity_world` WHERE `dim`=? AND `world`=?" + )) + { + pst.setInt(1, world.dim); + pst.setString(2, world.dir); + ResultSet result = pst.executeQuery(); + if(result.next()) + world.setDataSourceId(id = result.getInt(1)); + } + + if(id <= 0) + insertWorld(connection, world); + + world.setDataSourceId(id); + worldDimMap.putIfAbsent(id, world); + return id; + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Contract("null->null;!null->!null") + byte[] uuid(UUID uuid) + { + if(uuid == null) + return null; + + byte[] bytes = new byte[16]; + ByteBuffer buffer = ByteBuffer.wrap(bytes); + buffer.putLong(uuid.getMostSignificantBits()); + buffer.putLong(uuid.getLeastSignificantBits()); + return bytes; + } + + @Contract("null->null;!null->!null") + UUID uuid(byte[] bytes) throws DataSourceException + { + if(bytes == null) + return null; + + try + { + ByteBuffer bb = ByteBuffer.wrap(bytes); + long firstLong = bb.getLong(); + long secondLong = bb.getLong(); + return new UUID(firstLong, secondLong); + } + catch(Exception e) + { + throw new DataSourceException("Bad UUID", e); + } + } + + void setNullableInt(PreparedStatement pst, int field, int val) throws SQLException + { + if(val == 0) + pst.setNull(field, Types.INTEGER); + else + pst.setInt(field, val); + } + + void setNullableString(PreparedStatement pst, int field, String val) throws SQLException + { + if(val == null) + pst.setNull(field, Types.VARCHAR); + else + pst.setString(field, val); + } + + @Slow + int playerId(Connection connection, @Nullable OptionalPlayer player) throws DataSourceException + { + PlayerID playerId; + if(player == null || (playerId = player.player()) == null) return 0; + int id = player.getDataSourceId(); + if(id > 0) return id; + + try + { + try(PreparedStatement pst = connection.prepareStatement( + "SELECT `player_id` FROM `minecity_players` WHERE `player_uuid`=?" + )) + { + pst.setBytes(1, uuid(playerId.uniqueId)); + ResultSet result = pst.executeQuery(); + if(result.next()) + { + id = result.getInt(1); + playerId.setDataSourceId(id); + return id; + } + } + + try(PreparedStatement pst = connection.prepareStatement( + "INSERT INTO `minecity_players`(`player_uuid`, `player_name`) VALUES(?,?)" + , Statement.RETURN_GENERATED_KEYS + )) + { + pst.setBytes(1, uuid(playerId.uniqueId)); + pst.setString(2, playerId.getName()); + pst.executeUpdate(); + ResultSet keys = pst.getGeneratedKeys(); + keys.next(); + id = keys.getInt(1); + playerId.setDataSourceId(id); + return id; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + int entityId(Connection connection, @Nullable EntityID entity) throws DataSourceException + { + if(entity == null) return 0; + int id = entity.getDataSourceId(); + if(id > 0) return id; + + try + { + try(PreparedStatement pst = connection.prepareStatement( + "SELECT `entity_id` FROM `minecity_entities` WHERE `entity_uuid`=?" + )) + { + pst.setBytes(1, uuid(entity.uniqueId)); + ResultSet result = pst.executeQuery(); + if(result.next()) + { + id = result.getInt(1); + entity.setDataSourceId(id); + return id; + } + } + + try(PreparedStatement pst = connection.prepareStatement( + "INSERT INTO `minecity_entities`(`entity_uuid`, `entity_name`, entity_type) VALUES(?,?,?)" + , Statement.RETURN_GENERATED_KEYS + )) + { + pst.setBytes(1, uuid(entity.uniqueId)); + pst.setString(2, entity.getName()); + pst.setString(3, entity.getEntityType().name()); + pst.executeUpdate(); + ResultSet keys = pst.getGeneratedKeys(); + keys.next(); + id = keys.getInt(1); + entity.setDataSourceId(id); + return id; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + private City city(Connection connection, int cityId) throws SQLException, DataSourceException + { + synchronized(cityMap) + { + City city = cityMap.get(cityId); + if(city != null) + return city; + + return loadCity(connection, cityId, null).orElseThrow(()-> new DataSourceException("City ID "+cityId+" not found")); + } + } + + @Slow + @Nullable + @Override + public ClaimedChunk getCityChunk(@NotNull ChunkPos pos) throws DataSourceException + { + try + { + Connection connection = this.connection.connect(); + try(PreparedStatement pst = connection.prepareStatement( + "SELECT `i`.`city_id`, `i`.`island_id`, reserve FROM `minecity_chunks` AS `c` " + + "INNER JOIN `minecity_world` AS `w` ON `c`.`world_id`=`w`.`world_id` " + + "INNER JOIN `minecity_islands` AS `i` ON `c`.`island_id`=`i`.`island_id` "+ + "WHERE `w`.`dim`=? AND `w`.`world`=? AND `c`.`x`=? AND `c`.`z`=?;" + )) + { + pst.setInt(1, pos.world.dim); + pst.setString(2, pos.world.dir); + pst.setInt(3, pos.x); + pst.setInt(4, pos.z); + ResultSet result = pst.executeQuery(); + if(!result.next()) + return null; + int cityId = result.getInt(1); + int islandId = result.getInt(2); + boolean reserve = result.getBoolean(3); + pst.close(); + + City city = city(connection, cityId); + Island island = city.getIsland(islandId); + return new ClaimedChunk(island != null? island : Inconsistency.getInconsistentIsland(), pos, reserve); + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + catch(UncheckedDataSourceException e) + { + throw e.getCause(); + } + } + + @Slow + int createIsland(Connection transaction, int cityId, WorldDim world) throws DataSourceException, SQLException + { + int worldId = worldId(transaction, world); + int islandId; + try(PreparedStatement pst = transaction.prepareStatement( + "INSERT INTO `minecity_islands`(world_id, city_id) VALUES(?,?)", + Statement.RETURN_GENERATED_KEYS + )) + { + pst.setInt(1, worldId); + pst.setInt(2, cityId); + pst.executeUpdate(); + ResultSet keys = pst.getGeneratedKeys(); + keys.next(); + islandId = keys.getInt(1); + } + + return islandId; + } + + @Slow + void createClaim(Connection connection, int islandId, ChunkPos chunk) throws SQLException, DataSourceException + { + int worldId = worldId(connection, chunk.world); + try(PreparedStatement pst = connection.prepareStatement( + "DELETE FROM minecity_chunks WHERE world_id=? AND x=? AND z=? AND reserve=1" + )) + { + pst.setInt(1, worldId); + pst.setInt(2, chunk.x); + pst.setInt(3, chunk.z); + pst.executeUpdate(); + } + try(PreparedStatement pst = connection.prepareStatement( + "INSERT INTO `minecity_chunks`(world_id, x, z, island_id, reserve) VALUES(?,?,?,?,0)" + )) + { + pst.setInt(1, worldId); + pst.setInt(2, chunk.x); + pst.setInt(3, chunk.z); + pst.setInt(4, islandId); + if(pst.executeUpdate() <= 0) + throw new DataSourceException("Failed to claim the spawn chunk"); + } + } + + @Nullable + @Override + public String checkNameConflict(@NotNull String name) + { + name = identity(name); + for(String cityName : cityNames) + if(identity(cityName).equals(name)) + return cityName; + + return null; + } + + @Slow + @NotNull + @Override + public CityCreationResult createCity(@NotNull City city) throws DataSourceException, IllegalStateException + { + if(city.getId() > 0) + throw new IllegalStateException(); + + try + { + BlockPos spawn = city.getSpawn(); + ChunkPos spawnChunk = spawn.getChunk(); + ClaimedChunk claim = getCityChunk(spawnChunk); + if(claim != null) + throw new IllegalStateException("The chunk " + spawnChunk + " is already claimed: " + claim); + + int islandId; + try(Connection connection = this.connection.transaction()) + { + try + { + int worldId = worldId(connection, spawn.world); + int cityId; + try(PreparedStatement pst = connection.prepareStatement( + "INSERT INTO `minecity_city`(name, owner, spawn_world, spawn_x, spawn_y, spawn_z, display_name, tax_applied_flat, tax_applied_percent, investment) " + + "VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )", + Statement.RETURN_GENERATED_KEYS + )) + { + pst.setString(1, city.getIdentityName()); + setNullableInt(pst, 2, playerId(connection, city.owner())); + pst.setInt(3, worldId); + pst.setInt(4, spawn.x); + //noinspection SuspiciousNameCombination + pst.setInt(5, spawn.y); + pst.setInt(6, spawn.z); + pst.setString(7, city.getName()); + pst.setDouble(8, city.getAppliedTax().getFlat()); + pst.setDouble(9, city.getAppliedTax().getPercent()); + pst.setDouble(10, city.getInvestment()); + pst.executeUpdate(); + ResultSet keys = pst.getGeneratedKeys(); + keys.next(); + cityId = keys.getInt(1); + city.setId(cityId); + cityMap.put(cityId, city); + } + + islandId = createIsland(connection, cityId, spawnChunk.world); + createClaim(connection, islandId, spawnChunk); + connection.commit(); + cityNames.add(city.getName()); + } + catch(Exception e) + { + connection.rollback(); + throw e; + } + } + + return new CityCreationResult(cityStorage, permStorage, + new SQLIsland(cityStorage, permStorage, islandId, spawnChunk, city), + Collections.emptyList() + ); + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @Override + public void initDB() throws DataSourceException, IOException + { + try(Connection transaction = connection.transaction()) + { + try(Statement stm = transaction.createStatement()) + { + ResultSet result; + int version; + try + { + result = stm.executeQuery("SELECT `value` FROM `minecity_setup` WHERE `property`='version';"); + result.next(); + version = result.getInt(1); + result.close(); + } + catch(SQLException e) + { + System.out.println("[MineCity] Installing the SQL database version "+VERSION); + ScriptRunner runner = new ScriptRunner(transaction, false, true); + runner.setLogWriter(null); + runner.runScript(new InputStreamReader( + getClass().getResourceAsStream("/assets/minecity/db/setup.sql"), "UTF-8" + )); + transaction.commit(); + return; + } + + if(version > VERSION) + throw new DataSourceException("Unsupported database version: "+version); + + if(version < VERSION) + { + System.out.println("[MineCity] Starting the database upgrade from "+version+" to "+VERSION); + for(; version < VERSION; version++) + { + System.out.println("[MineCity] Upgrading to version "+(version+1)); + ScriptRunner runner = new ScriptRunner(transaction, false, true); + runner.setLogWriter(null); + runner.runScript(new InputStreamReader( + getClass().getResourceAsStream("/assets/minecity/db/update_"+version+".sql"), "UTF-8" + )); + } + + transaction.commit(); + System.out.println("[MineCity] The database was successfully upgraded"); + } + + result = stm.executeQuery("SELECT `display_name` FROM `minecity_city` "); + while(result.next()) + cityNames.add(result.getString(1)); + result.close(); + + result = stm.executeQuery("SELECT c.name, g.name FROM minecity_groups g INNER JOIN minecity_city c ON c.city_id = g.city_id"); + while(result.next()) + groupNames.computeIfAbsent(result.getString(1), n-> new HashSet<>(1)).add(result.getString(2)); + } + catch(Exception e) + { + transaction.rollback(); + throw e; + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @NotNull + @Override + public Optional getCityByName(@NotNull String name) throws DataSourceException + { + name = identity(name); + if(name.length() < 3) + return Optional.empty(); + + synchronized(cityMap) + { + for(City city : cityMap.values()) + if(city.getIdentityName().equals(name)) + return Optional.of(city); + + try + { + return loadCity(connection.connect(), 0, name); + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + } + + @NotNull + @Override + public Set getEntityGroups(Identity identity) throws DataSourceException + { + try + { + switch(identity.getType()) + { + case PLAYER: + Connection connection = this.connection.connect(); + try(PreparedStatement pst = connection.prepareStatement( + "SELECT g.group_id, g.city_id, g.display_name, c.display_name AS home " + + "FROM minecity_group_players gp " + + "INNER JOIN minecity_groups g ON g.group_id = gp.group_id " + + "INNER JOIN minecity_city c ON c.city_id = g.city_id " + + "WHERE gp.player_id=?" + )) + { + pst.setInt(1, playerId(connection, (PlayerID) identity)); + ResultSet result = pst.executeQuery(); + Set set = new HashSet<>(2); + while(result.next()) + { + int groupId = result.getInt(1); + int cityId = result.getInt(2); + String name = result.getString(3); + String home = result.getString(4); + GroupID group = Optional.ofNullable(cityMap.get(cityId)) + .map(c-> c.getGroup(groupId)).map(Group::getIdentity) + .orElseGet(()-> new GroupID(groupId, name, home, cityId)) + ; + set.add(group); + } + + return set; + } + + case ENTITY: + connection = this.connection.connect(); + int id = identity.getDataSourceId(); + try(PreparedStatement pst = connection.prepareStatement( + "SELECT g.group_id, g.city_id, g.display_name, c.display_name AS home " + + "FROM minecity_group_entities ge " + + "INNER JOIN minecity_groups g ON g.group_id = ge.group_id " + + "INNER JOIN minecity_city c ON c.city_id = g.city_id " + + (id > 0 ? "WHERE ge.entity_id = ?" : + "INNER JOIN minecity_entities e ON ge.entity_id = e.entity_id " + + "WHERE e.entity_uuid = ?") + )) + { + if(id > 0) + pst.setInt(1, id); + else + pst.setBytes(1, uuid(((EntityID)identity).getUniqueId())); + + ResultSet result = pst.executeQuery(); + Set set = new HashSet<>(2); + while(result.next()) + { + int groupId = result.getInt(1); + int cityId = result.getInt(2); + String name = result.getString(3); + String home = result.getString(4); + GroupID group = Optional.ofNullable(cityMap.get(cityId)) + .map(c-> c.getGroup(groupId)).map(Group::getIdentity) + .orElseGet(()-> new GroupID(groupId, name, home, cityId)) + ; + set.add(group); + } + + return set; + } + + default: + return Collections.emptySet(); + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @NotNull + @Override + public Optional getPlayer(@NotNull String name) throws DataSourceException + { + try(PreparedStatement pst = connection.connect().prepareStatement( + "SELECT player_id, player_uuid, player_name FROM minecity_players WHERE player_name=?" + )) + { + pst.setString(1, name); + ResultSet result = pst.executeQuery(); + if(!result.next()) + return Optional.empty(); + return Optional.of(new PlayerID(result.getInt(1), uuid(result.getBytes(2)), result.getString(3))); + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @NotNull + @Override + public IslandArea getArea(@NotNull Island island) + throws DataSourceException, ClassCastException, IllegalArgumentException + { + SQLIsland sqlIsland = (SQLIsland) island; + + try + { + Connection connection = this.connection.connect(); + try(PreparedStatement pst = connection.prepareStatement( + "SELECT x, z FROM minecity_chunks WHERE island_id=? AND world_id=? AND reserve=0" + )) + { + pst.setInt(1, sqlIsland.id); + pst.setInt(2, worldId(connection, sqlIsland.world)); + ResultSet result = pst.executeQuery(); + List list = new ArrayList<>(); + while(result.next()) + list.add(new ChunkPos(sqlIsland.world, result.getInt(1), result.getInt(2))); + + return new IslandArea(island, list); + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @NotNull + @Override + public Nature getNature(@NotNull WorldDim world) throws DataSourceException + { + try + { + int id = world.getDataSourceId(); + Connection connection = this.connection.connect(); + try(PreparedStatement pst = connection.prepareStatement( + "SELECT world_id, `name`, city_creations, perm_denial_message " + + "FROM minecity_world " + + "WHERE "+(id > 0? "world_id=?":"dim=? AND world=?") + )) + { + if(id > 0) + pst.setInt(1, id); + else + { + pst.setInt(1, world.dim); + pst.setString(2, world.dir); + } + + ResultSet result = pst.executeQuery(); + if(result.next()) + { + if(id == 0) + world.setDataSourceId(result.getInt(1)); + + world.name = result.getString(2); + + boolean cityCreation = result.getBoolean(3); + + String str = result.getString(4); + Message message; + if(str == null) + message = null; + else + message = Message.string(str); + pst.close(); + + return new Nature(mineCity, world, message, permStorage, permStorage, !cityCreation); + } + } + + if(id == 0) + world.setDataSourceId(insertWorld(connection, world)); + + return new Nature(mineCity, world, permStorage, permStorage); + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @NotNull + @Override + public Supplier> cityNameSupplier() + { + return cityNames::stream; + } + + @NotNull + @Override + public Optional> getGroupNames(@NotNull String cityName) + { + Set set = groupNames.get(identity(cityName)); + if(set == null) + return Optional.empty(); + return Optional.of(Collections.unmodifiableSet(set)); + } + + @NotNull + @Override + public Map> getGroups() + { + return Collections.unmodifiableMap(groupNames); + } + + @Override + public int getCityCount(PlayerID playerId) throws DataSourceException + { + try + { + Connection connection = this.connection.connect(); + try(PreparedStatement pst = connection.prepareStatement( + "SELECT count(*) FROM `minecity_city` WHERE `owner`=?" + )) + { + pst.setInt(1, playerId(connection, playerId)); + ResultSet result = pst.executeQuery(); + result.next(); + return result.getInt(1); + } + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } + + @Slow + @Override + public void close() throws DataSourceException + { + try + { + connection.close(); + } + catch(SQLException e) + { + throw new DataSourceException(e); + } + } +} diff --git a/Core/src/main/java/br/com/gamemods/minecity/structure/ChunkOwner.java b/Core/src/main/java/br/com/gamemods/minecity/structure/ChunkOwner.java index d1c04ef8..9c5b1d1c 100644 --- a/Core/src/main/java/br/com/gamemods/minecity/structure/ChunkOwner.java +++ b/Core/src/main/java/br/com/gamemods/minecity/structure/ChunkOwner.java @@ -1,5 +1,5 @@ -package br.com.gamemods.minecity.structure; - -public interface ChunkOwner -{ -} +package br.com.gamemods.minecity.structure; + +public interface ChunkOwner +{ +} diff --git a/Core/src/main/java/br/com/gamemods/minecity/structure/City.java b/Core/src/main/java/br/com/gamemods/minecity/structure/City.java index 8e32d3ad..147a9dc3 100644 --- a/Core/src/main/java/br/com/gamemods/minecity/structure/City.java +++ b/Core/src/main/java/br/com/gamemods/minecity/structure/City.java @@ -1,762 +1,762 @@ -package br.com.gamemods.minecity.structure; - -import br.com.gamemods.minecity.MineCity; -import br.com.gamemods.minecity.api.PlayerID; -import br.com.gamemods.minecity.api.Slow; -import br.com.gamemods.minecity.api.command.LegacyFormat; -import br.com.gamemods.minecity.api.command.Message; -import br.com.gamemods.minecity.api.permission.*; -import br.com.gamemods.minecity.api.world.BlockPos; -import br.com.gamemods.minecity.api.world.ChunkPos; -import br.com.gamemods.minecity.api.world.Direction; -import br.com.gamemods.minecity.api.world.MinecraftEntity; -import br.com.gamemods.minecity.datasource.api.*; -import br.com.gamemods.minecity.datasource.api.unchecked.DBFunction; -import br.com.gamemods.minecity.datasource.api.unchecked.DisDBConsumer; -import br.com.gamemods.minecity.datasource.api.unchecked.UncheckedDataSourceException; -import br.com.gamemods.minecity.economy.Tax; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.AbstractMap.SimpleImmutableEntry; -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static br.com.gamemods.minecity.api.StringUtil.identity; - -public final class City extends ExceptStoredHolder -{ - public static final Message INCONSISTENT_CITY_MESSAGE = new Message("inconsistent.city", "This city is inconsistent."); - - @NotNull - public final MineCity mineCity; - - @NotNull - private ICityStorage storage; - - /** - * ID defined by the data source implementation, may be zero but cannot be negative - */ - private int id; - - @NotNull - private String name; - - @NotNull - private String identityName; - - @NotNull - private OptionalPlayer owner; - - @NotNull - private BlockPos spawn; - - @NotNull - private final Map islands; - - @NotNull - private final Map groups; - - private boolean invalid; - - private Message ownerNameCache; - private byte ownerNameLife = Byte.MAX_VALUE; - - @NotNull - private Tax appliedTax; - - private double investment; - - private double price; - - /** - * Create and save a city immediately - * @param owner The city's owner - * @param spawn The city's spawn, the chunk be claimed to this city immediately - * @throws IllegalArgumentException If the spawn's chunk is already reserved or the city's name is invalid - * @throws DataSourceException If a database error occurs - */ - @Slow - public City(@NotNull MineCity mineCity, @NotNull String name, @Nullable PlayerID owner, @NotNull BlockPos spawn, double investment) - throws IllegalArgumentException, DataSourceException - { - this.mineCity = mineCity; - this.investment = investment; - this.name = name; - identityName = identity(name); - appliedTax = mineCity.costs.cityTaxApplied; - this.owner = owner == null? new AdminCity(this) : owner; - this.spawn = spawn; - if(identityName.length() < 3) - throw new IllegalArgumentException("Bad name"); - String conflict = mineCity.dataSource.checkNameConflict(identityName); - if(conflict != null) - throw new IllegalArgumentException("The name is already taken by: "+conflict); - - ClaimedChunk other = mineCity.getChunk(spawn).orElse(null); - if(other != null && !(other.owner instanceof Nature)) - throw new IllegalArgumentException("The chunk "+spawn.getChunk()+" is reserved to "+other.owner); - - CityCreationResult result = mineCity.dataSource.createCity(this); - storage = result.storage; - permissionStorage = result.permissionStorage; - islands = new HashMap<>(1); - islands.put(result.island.getId(), result.island); - groups = new HashMap<>(result.groups.size()); - result.groups.forEach(g -> groups.put(g.getIdentityName(), g)); - - try - { - defaultMessages = mineCity.defaultCityFlags.getDefaultMessages(); - denyAll(mineCity.defaultCityFlags); - } - catch(UncheckedDataSourceException e) - { - System.err.println("[MineCity][SQL] Exception applying the default city flags!"); - e.getCause().printStackTrace(System.err); - } - - try - { - mineCity.reloadChunk(spawn.getChunk()); - } - catch(DataSourceException e) - { - System.err.println("[MineCity][SQL] Exception reloading a chunk"); - e.printStackTrace(System.err); - } - } - - /** - * Constructs an instance of a city that was loaded from the database, do not use this constructor for new cities. - */ - @Slow - public City(@NotNull MineCity mineCity, @NotNull String identityName, @NotNull String name, @Nullable PlayerID owner, - @NotNull BlockPos spawn, int id, @NotNull ICityStorage storage, - @NotNull IExceptPermissionStorage permissionStorage, @Nullable Message defaultDenialMessage, - @NotNull Tax appliedTax, double investment, double price - ) - throws DataSourceException - { - super(defaultDenialMessage); - this.price = price; - this.investment = investment; - this.appliedTax = appliedTax; - this.mineCity = mineCity; - this.name = name; - this.identityName = identityName; - this.owner = owner == null? new AdminCity(this) : owner; - this.spawn = spawn; - setId(id); - this.storage = storage; - this.permissionStorage = permissionStorage; - - Collection loadedIslands = storage.loadIslands(this); - this.islands = new HashMap<>(); - loadedIslands.forEach(i-> islands.put(i.getId(), i)); - - Collection loadedGroups = storage.loadGroups(this); - groups = new HashMap<>(loadedGroups.size()); - loadedGroups.forEach(g -> groups.put(g.getIdentityName(), g)); - - defaultMessages = mineCity.defaultCityFlags.getDefaultMessages(); - loadSimplePermissions(); - loadExceptPermissions(); - } - - @Slow - public synchronized void delete() throws IllegalStateException, DataSourceException - { - if(invalid) - throw new IllegalStateException(); - - try - { - List chunks = islands.values().stream().map( - (DBFunction) Island::getArea).flatMap(IslandArea::claims) - .collect(Collectors.toList()); - - storage.deleteCity(this); - invalid = true; - groups.values().forEach(Group::checkCityValidity); - chunks.forEach(mineCity::reloadChunkSlowly); - } - catch(UncheckedDataSourceException e) - { - throw e.getCause(); - } - } - - @Slow - public synchronized Group createGroup(@NotNull String name) throws IllegalArgumentException, DataSourceException, IllegalStateException - { - if(invalid) - throw new IllegalStateException(); - - String id = identity(name); - Group conflict = groups.get(id); - if(conflict != null) - throw new IllegalArgumentException("The group name '"+name+"' conflicts with '"+conflict.getName()+"'"); - - Group group = storage.createGroup(this, id, name); - groups.put(group.getIdentityName(), group); - return group; - } - - @Slow - public synchronized Group removeGroup(@NotNull String name) throws NoSuchElementException, DataSourceException, IllegalStateException - { - if(invalid) - throw new IllegalStateException(); - - String id = identity(name); - Group group = groups.get(id); - if(group == null) - throw new NoSuchElementException("Group not found: "+name); - - group.remove(); - groups.remove(id); - return group; - } - - public synchronized void removeInvalidGroups() - { - groups.values().removeIf(Group::isInvalid); - } - - - public synchronized void updateGroupName(Group group, String oldName) - throws IllegalStateException, IllegalArgumentException - { - String id = group.getIdentityName(); - if(id.equals(oldName)) - throw new IllegalStateException(); - - if(!groups.remove(oldName, group)) - throw new IllegalArgumentException(); - - groups.put(id, group); - } - - @Nullable - public Group getGroup(int id) - { - if(invalid) - return null; - - for(Group group : groups.values()) - if(group.id == id) - return group; - - return null; - } - - @Nullable - public Group getGroup(@NotNull String name) - { - if(invalid) - return null; - - - return groups.get(identity(name)); - } - - public Collection getGroups() - { - if(invalid) - return Collections.emptyList(); - - return Collections.unmodifiableCollection(groups.values()); - } - - public Set getGroupNames() - { - if(invalid) - return Collections.emptyNavigableSet(); - - return Collections.unmodifiableSet(groups.keySet()); - } - - @NotNull - @Override - public Optional can(@NotNull Identity identity, @NotNull PermissionFlag action) - { - if(invalid) - return Optional.of(mark(INCONSISTENT_CITY_MESSAGE, action)); - - if(identity.equals(owner)) - return Optional.empty(); - - if(identity.getType() == Identity.Type.NATURE) - return Optional.of(mark(new Message("Cities are protected from natural actions"), action)); - - return super.can(identity, action); - } - - @NotNull - @Override - public Optional can(@NotNull MinecraftEntity entity, @NotNull PermissionFlag action) - { - if(invalid) - return Optional.of(mark(INCONSISTENT_CITY_MESSAGE, action)); - - if(entity.getIdentity().equals(owner)) - return Optional.empty(); - - return super.can(entity, action); - } - - @Slow - public void setName(@NotNull String name) throws IllegalArgumentException, DataSourceException, IllegalStateException - { - if(invalid) - throw new IllegalStateException(); - - String identity = identity(name); - if(identity.length() < 3) - throw new IllegalArgumentException("Bad name"); - - if(!identityName.equals(identity)) - { - String conflict = mineCity.dataSource.checkNameConflict(identity); - if(conflict != null) - throw new IllegalArgumentException("The name is already taken by: "+conflict); - } - - storage.setName(this, identity, name); - this.identityName = identity; - this.name = name; - ownerNameCache = null; - groups.values().forEach(Group::updateCityName); - plots().forEach(Plot::updateCityName); - } - - @NotNull - public String getIdentityName() - { - return identityName; - } - - @Nullable - public Island getIsland(int id) - { - if(invalid) - return null; - - return islands.get(id); - } - - @NotNull - public Collection islands() - { - if(invalid) - return Collections.emptyList(); - - return Collections.unmodifiableCollection(islands.values()); - } - - @NotNull - @Override - public OptionalPlayer owner() - { - if(invalid) - return ServerAdmins.INSTANCE; - - return owner; - } - - public int getSizeX() - { - if(invalid) - return 0; - - return islands.values().stream().mapToInt(Island::getSizeX).sum(); - } - - public int getSizeZ() - { - if(invalid) - return 0; - - return islands.values().stream().mapToInt(Island::getSizeZ).sum(); - } - - public int getChunkCount() - { - if(invalid) - return 0; - - return islands.values().stream().mapToInt(Island::getChunkCount).sum(); - } - - @NotNull - public String getName() - { - return name; - } - - @NotNull - public BlockPos getSpawn() - { - return spawn; - } - - @Slow - public Stream connectedIslands(@NotNull ChunkPos chunk) - { - if(invalid) - return Stream.empty(); - - return Direction.cardinal.stream() - .map((DBFunction>) d-> mineCity.getOrFetchChunk(chunk.add(d))) - .filter(Optional::isPresent).map(Optional::get) - .map(ClaimedChunk::getIsland) - .filter(Optional::isPresent).map(Optional::get) - .filter(i-> i.getCity().equals(this)) - ; - } - - @Slow - public Stream> connectedIslandsEntries(@NotNull ChunkPos chunk) - { - if(invalid) - return Stream.empty(); - - return Direction.cardinal.stream() - .map((DBFunction>>) - d-> new SimpleImmutableEntry<>(d, mineCity.getOrFetchChunk(chunk.add(d))) - ) - .filter(e-> !e.getValue().map(c-> c.reserve).orElse(true)) - .map(e-> (Map.Entry) new SimpleImmutableEntry<>(e.getKey(), e.getValue().get().getIsland().orElse(null))) - .filter(e-> e.getValue() != null) - .filter(e-> this.equals(e.getValue().getCity())) - ; - } - - @Slow - public Island claim(@NotNull ChunkPos chunk, boolean createIsland) - throws IllegalArgumentException, DataSourceException, UncheckedDataSourceException, IllegalStateException - { - if(invalid) - throw new IllegalStateException(); - - Optional claimOpt = mineCity.getOrFetchChunk(chunk); - Optional cityOpt = claimOpt.flatMap(ClaimedChunk::getCityAcceptingReserve); - if(cityOpt.isPresent() && (cityOpt.get() != this || !claimOpt.get().reserve)) - throw new IllegalArgumentException("The chunk "+chunk+" is reserved"); - - Set islands = connectedIslands(chunk).collect(Collectors.toSet()); - - if(islands.isEmpty()) - { - if(!createIsland) - throw new IllegalArgumentException("The chunk "+chunk+" is not touching an island owned by city "+identityName); - Island island = storage.createIsland(this, chunk); - this.islands.put(island.getId(), island); - mineCity.reloadChunk(chunk); - reserveChunks(island); - return island; - } - else if(islands.size() == 1) - { - Island island = islands.iterator().next(); - storage.claim(island, chunk); - //long start = System.currentTimeMillis(); - reserveChunks(island); - //long end = System.currentTimeMillis(); - //System.out.println("Reserve chunk took "+(end-start)+"ms"); - return island; - } - else - { - Island mainIsland = storage.claim(islands, chunk); - islands.stream().filter(island -> !island.equals(mainIsland)) - .forEach(island -> this.islands.remove(island.getId())); - reserveChunks(mainIsland); - return mainIsland; - } - } - - @Slow - public Collection disclaim(@NotNull ChunkPos chunk, boolean createIslands) - throws IllegalStateException, IllegalArgumentException, DataSourceException - { - if(invalid) - throw new IllegalStateException(); - - if(islands.size() == 1 && getChunkCount() == 1) - throw new IllegalStateException("Cannot disclaim the last city's chunk, delete the city instead"); - - if(getSpawn().getChunk().equals(chunk)) - throw new IllegalArgumentException("Cannot disclaim the spawn chunk"); - - Optional claim = mineCity.getOrFetchChunk(chunk); - Island island = claim.flatMap(ClaimedChunk::getIsland).filter(i-> i.getCity().equals(this)) - .orElseThrow(()-> new IllegalArgumentException("The chunk " + chunk + " is not owned by the city " + identityName)); - - if(!claim.get().getPlots().isEmpty()) - throw new IllegalArgumentException("Cannot disclaim the chunk "+chunk+" because it contains plots."); - - Map islands = connectedIslandsEntries(chunk).filter(e->e.getValue().equals(island)) - .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); - - if(islands.isEmpty()) - { - storage.deleteIsland(island); - this.islands.remove(island.getId()); - reserveChunks(island); - return Collections.singleton(island); - } - else if(islands.size() == 1) - { - storage.disclaim(chunk, island); - reserveChunks(island); - return Collections.singleton(island); - } - else - { - IslandArea area = mineCity.dataSource.getArea(island); - area.setClaimed(chunk, false); - Set touching = area.touching(chunk); - Set> groups = touching.stream().map(area::contiguous).collect(Collectors.toSet()); - - if(groups.size() == 1) - { - storage.disclaim(chunk, island); - reserveChunks(island); - return Collections.singletonList(island); - } - - if(!createIslands) - throw new IllegalArgumentException("The chunk "+chunk+" is required by other chunks"); - - Collection created = storage.disclaim(chunk, island, groups); - created.forEach(i-> this.islands.put(i.getId(), i)); - groups.forEach(s-> s.forEach((DisDBConsumer) mineCity::reloadChunk)); - Stream.concat(created.stream(), Stream.of(island)).forEach((DisDBConsumer) this::reserveChunks); - return created; - } - } - - public Optional getPlot(String name) - { - return islands().stream().map(i-> i.getPlot(name)).filter(Optional::isPresent).map(Optional::get).findAny(); - } - - public Stream plotNames() - { - return islands().stream().flatMap(Island::getPlotNames); - } - - public Stream plotIdNames() - { - return islands().stream().flatMap(i-> i.getPlotIdNames().stream()); - } - - public Stream plots() - { - return islands().stream().flatMap(i-> i.getPlots().stream()); - } - - public Optional getPlotAt(BlockPos pos) - { - return islands().stream().filter(i-> pos.world.equals(i.world)).map(i-> i.getPlotAt(pos)) - .filter(Optional::isPresent).map(Optional::get).findAny(); - } - - public Stream getPlotsAt(ChunkPos pos) - { - return islands().stream().filter(i-> pos.world.equals(i.world)).flatMap(i-> i.getPlotsAt(pos)); - } - - @Slow - protected void reserveChunks(Island island) throws DataSourceException, IllegalStateException - { - if(invalid) - throw new IllegalStateException(); - - IslandArea area = mineCity.dataSource.getArea(island); - int rangeX = island.getSizeX()/2; - int rangeZ = island.getSizeZ()/2; - - IslandArea reserve; - if(rangeX == 0 && rangeZ == 0) - reserve = area; - else - { - reserve = new IslandArea(island, area.x - rangeX, area.z - rangeZ, - new boolean[area.claims.length + rangeX*2][area.claims[0].length + rangeZ*2] - ); - - area.claims().forEach(p-> { - reserve.claims[p.x-reserve.x][p.z-reserve.z] = true; - for(int rx=-rangeX; rx <= rangeX; rx++) - for(int rz=-rangeZ; rz <= rangeZ; rz++) - reserve.claims[p.x+rx-reserve.x][p.z+rz-reserve.z] = true; - }); - } - - //long start = System.currentTimeMillis(); - Collection update = storage.reserve(reserve); - //long end = System.currentTimeMillis(); - //System.out.println("SQL Call took "+(end-start)+"ms"); - update.forEach(mineCity::reloadChunkSlowly); - } - - @Slow - public void setSpawn(@NotNull BlockPos pos) throws DataSourceException,IllegalArgumentException - { - if(invalid) - throw new IllegalStateException(); - - if(!mineCity.getOrFetchChunk(pos.getChunk()).map(c->c.owner).filter(o->o instanceof Island).map(o->(Island)o) - .filter(i-> i.getCity().equals(this)).isPresent() ) - throw new IllegalArgumentException("The block "+pos+" is not part of the city"); - - storage.setSpawn(this, pos); - this.spawn = pos; - } - - /** - * Changes the owner of the city and saves it immediately - * @param owner The new owner - * @throws DataSourceException If the city is registered and the change failed. The owner will not be set in this case. - */ - @Slow - public void setOwner(@NotNull OptionalPlayer owner) throws DataSourceException, IllegalStateException - { - if(invalid) - throw new IllegalStateException(); - - storage.setOwner(this, owner); - this.owner = owner.getType() == Identity.Type.ADMINS? new AdminCity(this) : owner; - ownerNameCache = null; - plots().forEach(Plot::updateCityName); - } - - /** - * Defines the City ID, this can be done only once and should only be done by the {@link IDataSource} implementation. - * @throws IllegalStateException If the defined ID is different then the passed ID - * @throws IllegalArgumentException If {@code < 0} - */ - public void setId(int id) throws IllegalStateException, IllegalArgumentException - { - if(id < 0 && identityName.charAt(0) != '#') - throw new IllegalArgumentException("id = "+id); - if(this.id > 0 && id != this.id) - throw new IllegalStateException("Tried to change the city's \""+identityName+"\" ID from "+this.id+" to "+id); - - this.id = id; - } - - /** - * @return The City ID - */ - public int getId() - { - return id; - } - - @Override - public boolean equals(Object o) - { - if(this == o) return true; - if(o == null || getClass() != o.getClass()) return false; - - City city = (City) o; - return id == city.id && identityName.equals(city.identityName); - } - - @Override - public int hashCode() - { - int result = id; - result = 31*result + identityName.hashCode(); - return result; - } - - public LegacyFormat getColor() - { - if(name.charAt(0) == '#') - return LegacyFormat.DARK_RED; - - if(owner.getType() == Identity.Type.ADMINS) - return LegacyFormat.RED; - - return LegacyFormat.CITY_COLORS[id%LegacyFormat.CITY_COLORS.length]; - } - - public boolean isInvalid() - { - return invalid; - } - - @Override - public String toString() - { - return "City{" + - "id=" + id + - ", identityName='" + identityName + '\'' + - "}"; - } - - @Override - public Message ownerName() - { - Message cache = this.ownerNameCache; - if(cache != null && --ownerNameLife > 0) - return this.ownerNameCache; - - ownerNameLife = 127; - Message msg; - if(owner.getType() == Identity.Type.ADMINS) - { - msg = new Message("action.denied.city.admin", "${name}", new Object[]{"name", name}); - } - else - { - msg = new Message("action.denied.city.normal", "${name} ~ ${owner}", new Object[][]{ - {"name", name}, {"owner", owner.getName()} - }); - } - - return this.ownerNameCache = msg; - } - - @NotNull - public Tax getAppliedTax() - { - return appliedTax; - } - - public double getInvestment() - { - return investment; - } - - @Slow - public synchronized void invested(double value) throws DataSourceException - { - if(invalid) - throw new IllegalStateException("This instance is no longer valid"); - - investment = storage.invested(this, value); - } - - public double getPrice() - { - return price; - } - - @Slow - public void setPrice(double price) throws DataSourceException - { - if(invalid) - throw new IllegalStateException("This instance is no longer valid"); - - storage.setPrice(this, price); - this.price = price; - } -} +package br.com.gamemods.minecity.structure; + +import br.com.gamemods.minecity.MineCity; +import br.com.gamemods.minecity.api.PlayerID; +import br.com.gamemods.minecity.api.Slow; +import br.com.gamemods.minecity.api.command.LegacyFormat; +import br.com.gamemods.minecity.api.command.Message; +import br.com.gamemods.minecity.api.permission.*; +import br.com.gamemods.minecity.api.world.BlockPos; +import br.com.gamemods.minecity.api.world.ChunkPos; +import br.com.gamemods.minecity.api.world.Direction; +import br.com.gamemods.minecity.api.world.MinecraftEntity; +import br.com.gamemods.minecity.datasource.api.*; +import br.com.gamemods.minecity.datasource.api.unchecked.DBFunction; +import br.com.gamemods.minecity.datasource.api.unchecked.DisDBConsumer; +import br.com.gamemods.minecity.datasource.api.unchecked.UncheckedDataSourceException; +import br.com.gamemods.minecity.economy.Tax; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static br.com.gamemods.minecity.api.StringUtil.identity; + +public final class City extends ExceptStoredHolder +{ + public static final Message INCONSISTENT_CITY_MESSAGE = new Message("inconsistent.city", "This city is inconsistent."); + + @NotNull + public final MineCity mineCity; + + @NotNull + private ICityStorage storage; + + /** + * ID defined by the data source implementation, may be zero but cannot be negative + */ + private int id; + + @NotNull + private String name; + + @NotNull + private String identityName; + + @NotNull + private OptionalPlayer owner; + + @NotNull + private BlockPos spawn; + + @NotNull + private final Map islands; + + @NotNull + private final Map groups; + + private boolean invalid; + + private Message ownerNameCache; + private byte ownerNameLife = Byte.MAX_VALUE; + + @NotNull + private Tax appliedTax; + + private double investment; + + private double price; + + /** + * Create and save a city immediately + * @param owner The city's owner + * @param spawn The city's spawn, the chunk be claimed to this city immediately + * @throws IllegalArgumentException If the spawn's chunk is already reserved or the city's name is invalid + * @throws DataSourceException If a database error occurs + */ + @Slow + public City(@NotNull MineCity mineCity, @NotNull String name, @Nullable PlayerID owner, @NotNull BlockPos spawn, double investment) + throws IllegalArgumentException, DataSourceException + { + this.mineCity = mineCity; + this.investment = investment; + this.name = name; + identityName = identity(name); + appliedTax = mineCity.costs.cityTaxApplied; + this.owner = owner == null? new AdminCity(this) : owner; + this.spawn = spawn; + if(identityName.length() < 3) + throw new IllegalArgumentException("Bad name"); + String conflict = mineCity.dataSource.checkNameConflict(identityName); + if(conflict != null) + throw new IllegalArgumentException("The name is already taken by: "+conflict); + + ClaimedChunk other = mineCity.getChunk(spawn).orElse(null); + if(other != null && !(other.owner instanceof Nature)) + throw new IllegalArgumentException("The chunk "+spawn.getChunk()+" is reserved to "+other.owner); + + CityCreationResult result = mineCity.dataSource.createCity(this); + storage = result.storage; + permissionStorage = result.permissionStorage; + islands = new HashMap<>(1); + islands.put(result.island.getId(), result.island); + groups = new HashMap<>(result.groups.size()); + result.groups.forEach(g -> groups.put(g.getIdentityName(), g)); + + try + { + defaultMessages = mineCity.defaultCityFlags.getDefaultMessages(); + denyAll(mineCity.defaultCityFlags); + } + catch(UncheckedDataSourceException e) + { + System.err.println("[MineCity][SQL] Exception applying the default city flags!"); + e.getCause().printStackTrace(System.err); + } + + try + { + mineCity.reloadChunk(spawn.getChunk()); + } + catch(DataSourceException e) + { + System.err.println("[MineCity][SQL] Exception reloading a chunk"); + e.printStackTrace(System.err); + } + } + + /** + * Constructs an instance of a city that was loaded from the database, do not use this constructor for new cities. + */ + @Slow + public City(@NotNull MineCity mineCity, @NotNull String identityName, @NotNull String name, @Nullable PlayerID owner, + @NotNull BlockPos spawn, int id, @NotNull ICityStorage storage, + @NotNull IExceptPermissionStorage permissionStorage, @Nullable Message defaultDenialMessage, + @NotNull Tax appliedTax, double investment, double price + ) + throws DataSourceException + { + super(defaultDenialMessage); + this.price = price; + this.investment = investment; + this.appliedTax = appliedTax; + this.mineCity = mineCity; + this.name = name; + this.identityName = identityName; + this.owner = owner == null? new AdminCity(this) : owner; + this.spawn = spawn; + setId(id); + this.storage = storage; + this.permissionStorage = permissionStorage; + + Collection loadedIslands = storage.loadIslands(this); + this.islands = new HashMap<>(); + loadedIslands.forEach(i-> islands.put(i.getId(), i)); + + Collection loadedGroups = storage.loadGroups(this); + groups = new HashMap<>(loadedGroups.size()); + loadedGroups.forEach(g -> groups.put(g.getIdentityName(), g)); + + defaultMessages = mineCity.defaultCityFlags.getDefaultMessages(); + loadSimplePermissions(); + loadExceptPermissions(); + } + + @Slow + public synchronized void delete() throws IllegalStateException, DataSourceException + { + if(invalid) + throw new IllegalStateException(); + + try + { + List chunks = islands.values().stream().map( + (DBFunction) Island::getArea).flatMap(IslandArea::claims) + .collect(Collectors.toList()); + + storage.deleteCity(this); + invalid = true; + groups.values().forEach(Group::checkCityValidity); + chunks.forEach(mineCity::reloadChunkSlowly); + } + catch(UncheckedDataSourceException e) + { + throw e.getCause(); + } + } + + @Slow + public synchronized Group createGroup(@NotNull String name) throws IllegalArgumentException, DataSourceException, IllegalStateException + { + if(invalid) + throw new IllegalStateException(); + + String id = identity(name); + Group conflict = groups.get(id); + if(conflict != null) + throw new IllegalArgumentException("The group name '"+name+"' conflicts with '"+conflict.getName()+"'"); + + Group group = storage.createGroup(this, id, name); + groups.put(group.getIdentityName(), group); + return group; + } + + @Slow + public synchronized Group removeGroup(@NotNull String name) throws NoSuchElementException, DataSourceException, IllegalStateException + { + if(invalid) + throw new IllegalStateException(); + + String id = identity(name); + Group group = groups.get(id); + if(group == null) + throw new NoSuchElementException("Group not found: "+name); + + group.remove(); + groups.remove(id); + return group; + } + + public synchronized void removeInvalidGroups() + { + groups.values().removeIf(Group::isInvalid); + } + + + public synchronized void updateGroupName(Group group, String oldName) + throws IllegalStateException, IllegalArgumentException + { + String id = group.getIdentityName(); + if(id.equals(oldName)) + throw new IllegalStateException(); + + if(!groups.remove(oldName, group)) + throw new IllegalArgumentException(); + + groups.put(id, group); + } + + @Nullable + public Group getGroup(int id) + { + if(invalid) + return null; + + for(Group group : groups.values()) + if(group.id == id) + return group; + + return null; + } + + @Nullable + public Group getGroup(@NotNull String name) + { + if(invalid) + return null; + + + return groups.get(identity(name)); + } + + public Collection getGroups() + { + if(invalid) + return Collections.emptyList(); + + return Collections.unmodifiableCollection(groups.values()); + } + + public Set getGroupNames() + { + if(invalid) + return Collections.emptyNavigableSet(); + + return Collections.unmodifiableSet(groups.keySet()); + } + + @NotNull + @Override + public Optional can(@NotNull Identity identity, @NotNull PermissionFlag action) + { + if(invalid) + return Optional.of(mark(INCONSISTENT_CITY_MESSAGE, action)); + + if(identity.equals(owner)) + return Optional.empty(); + + if(identity.getType() == Identity.Type.NATURE) + return Optional.of(mark(new Message("Cities are protected from natural actions"), action)); + + return super.can(identity, action); + } + + @NotNull + @Override + public Optional can(@NotNull MinecraftEntity entity, @NotNull PermissionFlag action) + { + if(invalid) + return Optional.of(mark(INCONSISTENT_CITY_MESSAGE, action)); + + if(entity.getIdentity().equals(owner)) + return Optional.empty(); + + return super.can(entity, action); + } + + @Slow + public void setName(@NotNull String name) throws IllegalArgumentException, DataSourceException, IllegalStateException + { + if(invalid) + throw new IllegalStateException(); + + String identity = identity(name); + if(identity.length() < 3) + throw new IllegalArgumentException("Bad name"); + + if(!identityName.equals(identity)) + { + String conflict = mineCity.dataSource.checkNameConflict(identity); + if(conflict != null) + throw new IllegalArgumentException("The name is already taken by: "+conflict); + } + + storage.setName(this, identity, name); + this.identityName = identity; + this.name = name; + ownerNameCache = null; + groups.values().forEach(Group::updateCityName); + plots().forEach(Plot::updateCityName); + } + + @NotNull + public String getIdentityName() + { + return identityName; + } + + @Nullable + public Island getIsland(int id) + { + if(invalid) + return null; + + return islands.get(id); + } + + @NotNull + public Collection islands() + { + if(invalid) + return Collections.emptyList(); + + return Collections.unmodifiableCollection(islands.values()); + } + + @NotNull + @Override + public OptionalPlayer owner() + { + if(invalid) + return ServerAdmins.INSTANCE; + + return owner; + } + + public int getSizeX() + { + if(invalid) + return 0; + + return islands.values().stream().mapToInt(Island::getSizeX).sum(); + } + + public int getSizeZ() + { + if(invalid) + return 0; + + return islands.values().stream().mapToInt(Island::getSizeZ).sum(); + } + + public int getChunkCount() + { + if(invalid) + return 0; + + return islands.values().stream().mapToInt(Island::getChunkCount).sum(); + } + + @NotNull + public String getName() + { + return name; + } + + @NotNull + public BlockPos getSpawn() + { + return spawn; + } + + @Slow + public Stream connectedIslands(@NotNull ChunkPos chunk) + { + if(invalid) + return Stream.empty(); + + return Direction.cardinal.stream() + .map((DBFunction>) d-> mineCity.getOrFetchChunk(chunk.add(d))) + .filter(Optional::isPresent).map(Optional::get) + .map(ClaimedChunk::getIsland) + .filter(Optional::isPresent).map(Optional::get) + .filter(i-> i.getCity().equals(this)) + ; + } + + @Slow + public Stream> connectedIslandsEntries(@NotNull ChunkPos chunk) + { + if(invalid) + return Stream.empty(); + + return Direction.cardinal.stream() + .map((DBFunction>>) + d-> new SimpleImmutableEntry<>(d, mineCity.getOrFetchChunk(chunk.add(d))) + ) + .filter(e-> !e.getValue().map(c-> c.reserve).orElse(true)) + .map(e-> (Map.Entry) new SimpleImmutableEntry<>(e.getKey(), e.getValue().get().getIsland().orElse(null))) + .filter(e-> e.getValue() != null) + .filter(e-> this.equals(e.getValue().getCity())) + ; + } + + @Slow + public Island claim(@NotNull ChunkPos chunk, boolean createIsland) + throws IllegalArgumentException, DataSourceException, UncheckedDataSourceException, IllegalStateException + { + if(invalid) + throw new IllegalStateException(); + + Optional claimOpt = mineCity.getOrFetchChunk(chunk); + Optional cityOpt = claimOpt.flatMap(ClaimedChunk::getCityAcceptingReserve); + if(cityOpt.isPresent() && (cityOpt.get() != this || !claimOpt.get().reserve)) + throw new IllegalArgumentException("The chunk "+chunk+" is reserved"); + + Set islands = connectedIslands(chunk).collect(Collectors.toSet()); + + if(islands.isEmpty()) + { + if(!createIsland) + throw new IllegalArgumentException("The chunk "+chunk+" is not touching an island owned by city "+identityName); + Island island = storage.createIsland(this, chunk); + this.islands.put(island.getId(), island); + mineCity.reloadChunk(chunk); + reserveChunks(island); + return island; + } + else if(islands.size() == 1) + { + Island island = islands.iterator().next(); + storage.claim(island, chunk); + //long start = System.currentTimeMillis(); + reserveChunks(island); + //long end = System.currentTimeMillis(); + //System.out.println("Reserve chunk took "+(end-start)+"ms"); + return island; + } + else + { + Island mainIsland = storage.claim(islands, chunk); + islands.stream().filter(island -> !island.equals(mainIsland)) + .forEach(island -> this.islands.remove(island.getId())); + reserveChunks(mainIsland); + return mainIsland; + } + } + + @Slow + public Collection disclaim(@NotNull ChunkPos chunk, boolean createIslands) + throws IllegalStateException, IllegalArgumentException, DataSourceException + { + if(invalid) + throw new IllegalStateException(); + + if(islands.size() == 1 && getChunkCount() == 1) + throw new IllegalStateException("Cannot disclaim the last city's chunk, delete the city instead"); + + if(getSpawn().getChunk().equals(chunk)) + throw new IllegalArgumentException("Cannot disclaim the spawn chunk"); + + Optional claim = mineCity.getOrFetchChunk(chunk); + Island island = claim.flatMap(ClaimedChunk::getIsland).filter(i-> i.getCity().equals(this)) + .orElseThrow(()-> new IllegalArgumentException("The chunk " + chunk + " is not owned by the city " + identityName)); + + if(!claim.get().getPlots().isEmpty()) + throw new IllegalArgumentException("Cannot disclaim the chunk "+chunk+" because it contains plots."); + + Map islands = connectedIslandsEntries(chunk).filter(e->e.getValue().equals(island)) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + + if(islands.isEmpty()) + { + storage.deleteIsland(island); + this.islands.remove(island.getId()); + reserveChunks(island); + return Collections.singleton(island); + } + else if(islands.size() == 1) + { + storage.disclaim(chunk, island); + reserveChunks(island); + return Collections.singleton(island); + } + else + { + IslandArea area = mineCity.dataSource.getArea(island); + area.setClaimed(chunk, false); + Set touching = area.touching(chunk); + Set> groups = touching.stream().map(area::contiguous).collect(Collectors.toSet()); + + if(groups.size() == 1) + { + storage.disclaim(chunk, island); + reserveChunks(island); + return Collections.singletonList(island); + } + + if(!createIslands) + throw new IllegalArgumentException("The chunk "+chunk+" is required by other chunks"); + + Collection created = storage.disclaim(chunk, island, groups); + created.forEach(i-> this.islands.put(i.getId(), i)); + groups.forEach(s-> s.forEach((DisDBConsumer) mineCity::reloadChunk)); + Stream.concat(created.stream(), Stream.of(island)).forEach((DisDBConsumer) this::reserveChunks); + return created; + } + } + + public Optional getPlot(String name) + { + return islands().stream().map(i-> i.getPlot(name)).filter(Optional::isPresent).map(Optional::get).findAny(); + } + + public Stream plotNames() + { + return islands().stream().flatMap(Island::getPlotNames); + } + + public Stream plotIdNames() + { + return islands().stream().flatMap(i-> i.getPlotIdNames().stream()); + } + + public Stream plots() + { + return islands().stream().flatMap(i-> i.getPlots().stream()); + } + + public Optional getPlotAt(BlockPos pos) + { + return islands().stream().filter(i-> pos.world.equals(i.world)).map(i-> i.getPlotAt(pos)) + .filter(Optional::isPresent).map(Optional::get).findAny(); + } + + public Stream getPlotsAt(ChunkPos pos) + { + return islands().stream().filter(i-> pos.world.equals(i.world)).flatMap(i-> i.getPlotsAt(pos)); + } + + @Slow + protected void reserveChunks(Island island) throws DataSourceException, IllegalStateException + { + if(invalid) + throw new IllegalStateException(); + + IslandArea area = mineCity.dataSource.getArea(island); + int rangeX = island.getSizeX()/2; + int rangeZ = island.getSizeZ()/2; + + IslandArea reserve; + if(rangeX == 0 && rangeZ == 0) + reserve = area; + else + { + reserve = new IslandArea(island, area.x - rangeX, area.z - rangeZ, + new boolean[area.claims.length + rangeX*2][area.claims[0].length + rangeZ*2] + ); + + area.claims().forEach(p-> { + reserve.claims[p.x-reserve.x][p.z-reserve.z] = true; + for(int rx=-rangeX; rx <= rangeX; rx++) + for(int rz=-rangeZ; rz <= rangeZ; rz++) + reserve.claims[p.x+rx-reserve.x][p.z+rz-reserve.z] = true; + }); + } + + //long start = System.currentTimeMillis(); + Collection update = storage.reserve(reserve); + //long end = System.currentTimeMillis(); + //System.out.println("SQL Call took "+(end-start)+"ms"); + update.forEach(mineCity::reloadChunkSlowly); + } + + @Slow + public void setSpawn(@NotNull BlockPos pos) throws DataSourceException,IllegalArgumentException + { + if(invalid) + throw new IllegalStateException(); + + if(!mineCity.getOrFetchChunk(pos.getChunk()).map(c->c.owner).filter(o->o instanceof Island).map(o->(Island)o) + .filter(i-> i.getCity().equals(this)).isPresent() ) + throw new IllegalArgumentException("The block "+pos+" is not part of the city"); + + storage.setSpawn(this, pos); + this.spawn = pos; + } + + /** + * Changes the owner of the city and saves it immediately + * @param owner The new owner + * @throws DataSourceException If the city is registered and the change failed. The owner will not be set in this case. + */ + @Slow + public void setOwner(@NotNull OptionalPlayer owner) throws DataSourceException, IllegalStateException + { + if(invalid) + throw new IllegalStateException(); + + storage.setOwner(this, owner); + this.owner = owner.getType() == Identity.Type.ADMINS? new AdminCity(this) : owner; + ownerNameCache = null; + plots().forEach(Plot::updateCityName); + } + + /** + * Defines the City ID, this can be done only once and should only be done by the {@link IDataSource} implementation. + * @throws IllegalStateException If the defined ID is different then the passed ID + * @throws IllegalArgumentException If {@code < 0} + */ + public void setId(int id) throws IllegalStateException, IllegalArgumentException + { + if(id < 0 && identityName.charAt(0) != '#') + throw new IllegalArgumentException("id = "+id); + if(this.id > 0 && id != this.id) + throw new IllegalStateException("Tried to change the city's \""+identityName+"\" ID from "+this.id+" to "+id); + + this.id = id; + } + + /** + * @return The City ID + */ + public int getId() + { + return id; + } + + @Override + public boolean equals(Object o) + { + if(this == o) return true; + if(o == null || getClass() != o.getClass()) return false; + + City city = (City) o; + return id == city.id && identityName.equals(city.identityName); + } + + @Override + public int hashCode() + { + int result = id; + result = 31*result + identityName.hashCode(); + return result; + } + + public LegacyFormat getColor() + { + if(name.charAt(0) == '#') + return LegacyFormat.DARK_RED; + + if(owner.getType() == Identity.Type.ADMINS) + return LegacyFormat.RED; + + return LegacyFormat.CITY_COLORS[id%LegacyFormat.CITY_COLORS.length]; + } + + public boolean isInvalid() + { + return invalid; + } + + @Override + public String toString() + { + return "City{" + + "id=" + id + + ", identityName='" + identityName + '\'' + + "}"; + } + + @Override + public Message ownerName() + { + Message cache = this.ownerNameCache; + if(cache != null && --ownerNameLife > 0) + return this.ownerNameCache; + + ownerNameLife = 127; + Message msg; + if(owner.getType() == Identity.Type.ADMINS) + { + msg = new Message("action.denied.city.admin", "${name}", new Object[]{"name", name}); + } + else + { + msg = new Message("action.denied.city.normal", "${name} ~ ${owner}", new Object[][]{ + {"name", name}, {"owner", owner.getName()} + }); + } + + return this.ownerNameCache = msg; + } + + @NotNull + public Tax getAppliedTax() + { + return appliedTax; + } + + public double getInvestment() + { + return investment; + } + + @Slow + public synchronized void invested(double value) throws DataSourceException + { + if(invalid) + throw new IllegalStateException("This instance is no longer valid"); + + investment = storage.invested(this, value); + } + + public double getPrice() + { + return price; + } + + @Slow + public void setPrice(double price) throws DataSourceException + { + if(invalid) + throw new IllegalStateException("This instance is no longer valid"); + + storage.setPrice(this, price); + this.price = price; + } +} diff --git a/Core/src/main/java/br/com/gamemods/minecity/structure/ClaimedChunk.java b/Core/src/main/java/br/com/gamemods/minecity/structure/ClaimedChunk.java index 339560b6..082d76d3 100644 --- a/Core/src/main/java/br/com/gamemods/minecity/structure/ClaimedChunk.java +++ b/Core/src/main/java/br/com/gamemods/minecity/structure/ClaimedChunk.java @@ -1,195 +1,195 @@ -package br.com.gamemods.minecity.structure; - -import br.com.gamemods.minecity.api.permission.FlagHolder; -import br.com.gamemods.minecity.api.world.BlockPos; -import br.com.gamemods.minecity.api.world.ChunkPos; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public final class ClaimedChunk -{ - @NotNull - public final ChunkOwner owner; - @NotNull - public final ChunkPos chunk; - public final boolean reserve; - @Nullable - private Set plots; - private boolean invalid; - - public ClaimedChunk(@NotNull ChunkOwner owner, @NotNull ChunkPos chunk) - { - this.owner = owner; - this.chunk = chunk; - this.reserve = owner instanceof Reserve; - } - - public ClaimedChunk(@NotNull Island owner, @NotNull ChunkPos chunk, boolean reserve) - { - this.owner = reserve? owner.reserve : owner; - this.chunk = chunk; - this.reserve = reserve; - } - - public Optional getPlotAt(BlockPos pos) - { - if(!pos.world.equals(chunk.world)) - return Optional.empty(); - - return getPlotAt(pos.x, pos.y, pos.z); - } - - public Optional getPlotAt(int x, int y, int z) - { - for(Plot plot : getPlots()) - if(plot.getShape().contains(x, y, z)) - return Optional.of(plot); - - return Optional.empty(); - } - - public Collection getPlots() - { - if(plots != null) - return plots; - - if(reserve) - return plots = Collections.emptySet(); - - return plots = getIsland().map(i -> i.getPlotsAt(chunk)).orElse(Stream.empty()).collect(Collectors.toSet()); - } - - public Optional getIslandAcceptingReserve() - { - if(!reserve) - return getIsland(); - - return Optional.of(((Reserve)owner).island); - } - - @NotNull - public Optional getIsland() - { - if(owner instanceof Island) return Optional.of((Island) owner); - if(owner instanceof Inconsistency) return Optional.of(Inconsistency.getInconsistentIsland()); - return Optional.empty(); - } - - public Optional getReserve() - { - if(owner instanceof Reserve) return Optional.of((Reserve)owner); - return Optional.empty(); - } - - public Optional nature() - { - Nature nature = chunk.world.nature; - if(nature != null) - return Optional.of(nature); - - if(owner instanceof Nature) - return Optional.of((Nature) owner); - - return Optional.empty(); - } - - /** - * @throws NoSuchElementException If the chunk is reserved but the nature object is not available - */ - @NotNull - public FlagHolder getFlagHolder() - { - if(reserve) - return (Reserve) owner; - - return getIsland().map(Island::getCity).orElse(chunk.world.nature); - } - - /** - * @throws NoSuchElementException If the chunk is reserved but the nature object is not available - */ - @NotNull - public FlagHolder getFlagHolder(int blockX, int blockY, int blockZ) - { - if(reserve) - return (Reserve) owner; - - Optional plot = getPlotAt(blockX, blockY, blockZ); - if(plot.isPresent()) - return plot.get(); - - return getIsland().map(Island::getCity).orElse(chunk.world.nature); - } - - public FlagHolder getFlagHolder(BlockPos pos) - { - return getFlagHolder(pos.x, pos.y, pos.z); - } - - @NotNull - public Optional getCity() - { - return getIsland().map(Island::getCity); - } - - public Optional getCityAcceptingReserve() - { - if(!reserve) - return getCity(); - - return Optional.of(((Reserve)owner).island.getCity()); - } - - @NotNull - public ChunkOwner getOwner() - { - return owner; - } - - @NotNull - public ChunkPos getChunk() - { - return chunk; - } - - public boolean isInvalid() - { - return invalid; - } - - public void invalidate() - { - invalid = true; - } - - @Override - public String toString() - { - return "CityChunk{" + - "owner=" + owner + - ", chunk=" + chunk + - '}'; - } - - @Override - public boolean equals(Object o) - { - if(this == o) return true; - if(o == null || getClass() != o.getClass()) return false; - - ClaimedChunk that = (ClaimedChunk) o; - return owner.equals(that.owner) && chunk.equals(that.chunk); - } - - @Override - public int hashCode() - { - int result = owner.hashCode(); - result = 31*result + chunk.hashCode(); - return result; - } -} +package br.com.gamemods.minecity.structure; + +import br.com.gamemods.minecity.api.permission.FlagHolder; +import br.com.gamemods.minecity.api.world.BlockPos; +import br.com.gamemods.minecity.api.world.ChunkPos; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public final class ClaimedChunk +{ + @NotNull + public final ChunkOwner owner; + @NotNull + public final ChunkPos chunk; + public final boolean reserve; + @Nullable + private Set plots; + private boolean invalid; + + public ClaimedChunk(@NotNull ChunkOwner owner, @NotNull ChunkPos chunk) + { + this.owner = owner; + this.chunk = chunk; + this.reserve = owner instanceof Reserve; + } + + public ClaimedChunk(@NotNull Island owner, @NotNull ChunkPos chunk, boolean reserve) + { + this.owner = reserve? owner.reserve : owner; + this.chunk = chunk; + this.reserve = reserve; + } + + public Optional getPlotAt(BlockPos pos) + { + if(!pos.world.equals(chunk.world)) + return Optional.empty(); + + return getPlotAt(pos.x, pos.y, pos.z); + } + + public Optional getPlotAt(int x, int y, int z) + { + for(Plot plot : getPlots()) + if(plot.getShape().contains(x, y, z)) + return Optional.of(plot); + + return Optional.empty(); + } + + public Collection getPlots() + { + if(plots != null) + return plots; + + if(reserve) + return plots = Collections.emptySet(); + + return plots = getIsland().map(i -> i.getPlotsAt(chunk)).orElse(Stream.empty()).collect(Collectors.toSet()); + } + + public Optional getIslandAcceptingReserve() + { + if(!reserve) + return getIsland(); + + return Optional.of(((Reserve)owner).island); + } + + @NotNull + public Optional getIsland() + { + if(owner instanceof Island) return Optional.of((Island) owner); + if(owner instanceof Inconsistency) return Optional.of(Inconsistency.getInconsistentIsland()); + return Optional.empty(); + } + + public Optional getReserve() + { + if(owner instanceof Reserve) return Optional.of((Reserve)owner); + return Optional.empty(); + } + + public Optional nature() + { + Nature nature = chunk.world.nature; + if(nature != null) + return Optional.of(nature); + + if(owner instanceof Nature) + return Optional.of((Nature) owner); + + return Optional.empty(); + } + + /** + * @throws NoSuchElementException If the chunk is reserved but the nature object is not available + */ + @NotNull + public FlagHolder getFlagHolder() + { + if(reserve) + return (Reserve) owner; + + return getIsland().map(Island::getCity).orElse(chunk.world.nature); + } + + /** + * @throws NoSuchElementException If the chunk is reserved but the nature object is not available + */ + @NotNull + public FlagHolder getFlagHolder(int blockX, int blockY, int blockZ) + { + if(reserve) + return (Reserve) owner; + + Optional plot = getPlotAt(blockX, blockY, blockZ); + if(plot.isPresent()) + return plot.get(); + + return getIsland().map(Island::getCity).orElse(chunk.world.nature); + } + + public FlagHolder getFlagHolder(BlockPos pos) + { + return getFlagHolder(pos.x, pos.y, pos.z); + } + + @NotNull + public Optional getCity() + { + return getIsland().map(Island::getCity); + } + + public Optional getCityAcceptingReserve() + { + if(!reserve) + return getCity(); + + return Optional.of(((Reserve)owner).island.getCity()); + } + + @NotNull + public ChunkOwner getOwner() + { + return owner; + } + + @NotNull + public ChunkPos getChunk() + { + return chunk; + } + + public boolean isInvalid() + { + return invalid; + } + + public void invalidate() + { + invalid = true; + } + + @Override + public String toString() + { + return "CityChunk{" + + "owner=" + owner + + ", chunk=" + chunk + + '}'; + } + + @Override + public boolean equals(Object o) + { + if(this == o) return true; + if(o == null || getClass() != o.getClass()) return false; + + ClaimedChunk that = (ClaimedChunk) o; + return owner.equals(that.owner) && chunk.equals(that.chunk); + } + + @Override + public int hashCode() + { + int result = owner.hashCode(); + result = 31*result + chunk.hashCode(); + return result; + } +} diff --git a/Core/src/main/java/br/com/gamemods/minecity/structure/Inconsistency.java b/Core/src/main/java/br/com/gamemods/minecity/structure/Inconsistency.java index d3d57cd9..f87320bd 100644 --- a/Core/src/main/java/br/com/gamemods/minecity/structure/Inconsistency.java +++ b/Core/src/main/java/br/com/gamemods/minecity/structure/Inconsistency.java @@ -1,457 +1,457 @@ -package br.com.gamemods.minecity.structure; - -import br.com.gamemods.minecity.MineCity; -import br.com.gamemods.minecity.api.PlayerID; -import br.com.gamemods.minecity.api.command.Message; -import br.com.gamemods.minecity.api.permission.*; -import br.com.gamemods.minecity.api.shape.Shape; -import br.com.gamemods.minecity.api.world.BlockPos; -import br.com.gamemods.minecity.api.world.ChunkPos; -import br.com.gamemods.minecity.api.world.WorldDim; -import br.com.gamemods.minecity.datasource.api.DataSourceException; -import br.com.gamemods.minecity.datasource.api.ICityStorage; -import br.com.gamemods.minecity.datasource.api.IExceptPermissionStorage; -import br.com.gamemods.minecity.datasource.api.INatureStorage; -import br.com.gamemods.minecity.datasource.api.unchecked.UncheckedDataSourceException; -import br.com.gamemods.minecity.economy.Tax; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; - -public class Inconsistency implements ChunkOwner -{ - public static final Message INCONSISTENT_CHUNK_MESSAGE = new Message("inconsistent.chunk", "This chunk is inconsistent."); - public static final Inconsistency INSTANCE = new Inconsistency(); - public static final WorldDim WORLD = new WorldDim(-10000, "inconsistency", "Inconsistent"); - private static MineCity mineCity; - private static Island island; - private static City city; - private Inconsistency(){} - - public static Nature nature(WorldDim world) - { - VoidStorage voidStorage = new VoidStorage(); - try - { - return new Nature(mineCity, world, INCONSISTENT_CHUNK_MESSAGE, voidStorage, voidStorage, true); - } - catch(DataSourceException unexpected) - { - throw new RuntimeException(unexpected); - } - } - - public static void setMineCity(MineCity mineCity) - { - Inconsistency.mineCity = mineCity; - } - - public static City getInconsistentCity(MineCity mineCity) - { - if(city == null) - { - try - { - VoidStorage voidStorage = new VoidStorage(); - synchronized(WORLD) - { - city = new City(mineCity, "#inconsistent", "#Inconsistency", null, new BlockPos(WORLD, 0, 0, 0), - -1000, voidStorage, voidStorage, null, new Tax(0,0), 0, 0 - ); - } - } - catch(DataSourceException unexpected) - { - throw new RuntimeException(unexpected); - } - Arrays.asList(PermissionFlag.values()).forEach(f-> city.deny(f, INCONSISTENT_CHUNK_MESSAGE)); - city.allow(PermissionFlag.LEAVE); - } - return city; - } - - public static ClaimedChunk claim(ChunkPos pos) - { - Nature nature = pos.world.nature; - if(nature != null) - getInconsistentCity(nature.mineCity); - else - getInconsistentCity(); - - ClaimedChunk chunk = new ClaimedChunk(INSTANCE, pos); - chunk.invalidate(); - return chunk; - } - - public static City getInconsistentCity() - { - return getInconsistentCity(mineCity); - } - - public static Island getInconsistentIsland(MineCity mineCity) - { - if(island == null) - getInconsistentCity(mineCity); - return island; - } - - public static Island getInconsistentIsland() - { - return getInconsistentIsland(mineCity); - } - - private static class InconsistentIsland extends Island - { - public InconsistentIsland(VoidStorage storage) - { - super(Inconsistency.city, storage, storage, -1, WORLD, Collections.emptySet()); - } - - @NotNull - @Override - public City getCity() - { - City c = Inconsistency.city; - if(c == null) - synchronized(WORLD) - { - return city; - } - - return c; - } - - @Override - public int getSizeX() - { - return 0; - } - - @Override - public int getSizeZ() - { - return 0; - } - - @Override - public int getChunkCount() - { - return 0; - } - } - - private static class VoidStorage implements ICityStorage, IExceptPermissionStorage, INatureStorage - { - @NotNull - @Override - public Collection loadIslands(City city) throws DataSourceException - { - if(Inconsistency.city == null) - Inconsistency.city = city; - return Collections.singleton(island = new InconsistentIsland(this)); - } - - @Override - public void setOwner(@NotNull City city, @NotNull OptionalPlayer owner) - throws DataSourceException, IllegalStateException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void setSpawn(@NotNull City city, @NotNull BlockPos spawn) - throws DataSourceException, IllegalStateException - { - throw new DataSourceException("Inconsistent city!"); - } - - @NotNull - @Override - public Island createIsland(@NotNull City city, @NotNull ChunkPos chunk) - throws DataSourceException, IllegalStateException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void claim(@NotNull Island island, @NotNull ChunkPos chunk) - throws DataSourceException, IllegalStateException, ClassCastException - { - throw new DataSourceException("Inconsistent city!"); - } - - @NotNull - @Override - public Island claim(@NotNull Set islands, @NotNull ChunkPos chunk) - throws DataSourceException, IllegalStateException, NoSuchElementException, ClassCastException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void deleteIsland(@NotNull Island island) - throws DataSourceException, IllegalArgumentException, ClassCastException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void deleteCity(@NotNull City city) throws DataSourceException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void disclaim(@NotNull ChunkPos chunk, @NotNull Island island) - throws DataSourceException, IllegalArgumentException - { - throw new DataSourceException("Inconsistent city!"); - } - - @NotNull - @Override - public Collection disclaim(@NotNull ChunkPos chunk, @NotNull Island island, - @NotNull Set> groups) - throws DataSourceException, IllegalStateException, NoSuchElementException, ClassCastException, IllegalArgumentException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void setName(@NotNull City city, @NotNull String identity, @NotNull String name) - throws DataSourceException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void setName(@NotNull Group group, @NotNull String identity, @NotNull String name) - throws DataSourceException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void addMember(@NotNull Group group, @NotNull Identity member) - throws DataSourceException, UnsupportedOperationException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void removeMember(@NotNull Group group, @NotNull Identity member) - throws DataSourceException, UnsupportedOperationException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void addManager(@NotNull Group group, @NotNull PlayerID manager) throws DataSourceException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void removeManager(@NotNull Group group, @NotNull PlayerID manager) throws DataSourceException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void deleteGroup(@NotNull Group group) throws DataSourceException - { - throw new DataSourceException("Inconsistent city!"); - } - - @NotNull - @Override - public Collection reserve(@NotNull IslandArea reserve) throws DataSourceException - { - throw new DataSourceException("Inconsistent city!"); - } - - @NotNull - @Override - public Group createGroup(@NotNull City city, @NotNull String id, @NotNull String name) - throws DataSourceException - { - throw new DataSourceException("Inconsistent city!"); - } - - @NotNull - @Override - public Collection loadGroups(@NotNull City city) throws DataSourceException - { - if(city.getId() != -1000) - throw new DataSourceException("Inconsistent city!"); - return Collections.emptyList(); - } - - @Override - public void setDefaultMessage(@NotNull SimpleFlagHolder holder, @Nullable Message message) - throws DataSourceException - { - if(holder != city) - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void deny(@NotNull SimpleFlagHolder holder, @NotNull PermissionFlag flag, @Nullable Message message) - throws DataSourceException - { - if(holder != city) - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void denyAll(SimpleFlagHolder holder, Map flags) - throws DataSourceException - { - if(holder != city) - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void allow(@NotNull SimpleFlagHolder holder, @NotNull PermissionFlag flag) throws DataSourceException - { - if(holder != city) - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void allowAll(@NotNull SimpleFlagHolder holder) throws DataSourceException - { - if(holder != city) - throw new DataSourceException("Inconsistent city!"); - } - - @NotNull - @Override - public EnumMap loadSimplePermissions(@NotNull SimpleFlagHolder holder) - throws DataSourceException - { - EnumMap result = new EnumMap<>(PermissionFlag.class); - Arrays.stream(PermissionFlag.values()).forEach(f-> result.put(f, INCONSISTENT_CHUNK_MESSAGE)); - return result; - } - - @Override - public void set(@NotNull ExceptFlagHolder holder, @NotNull PermissionFlag flag, boolean allow, - @NotNull Identity identity, @Nullable Message message) throws DataSourceException - { - if(holder != city) - throw new UncheckedDataSourceException(new DataSourceException("Inconsistent city!")); - } - - @Override - public void remove(@NotNull ExceptFlagHolder holder, @NotNull PermissionFlag flag, - @NotNull Identity identity) - throws DataSourceException - { - if(holder != city) - throw new DataSourceException("Inconsistent city!"); - } - - @NotNull - @Override - public Map, Optional>> loadExceptPermissions(@NotNull ExceptFlagHolder holder) - throws DataSourceException - { - return Collections.emptyMap(); - } - - @Override - public int createPlot(Plot plot) throws DataSourceException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void setOwner(@NotNull Plot plot, @Nullable PlayerID owner) - throws DataSourceException, IllegalStateException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void setShape(@NotNull Plot plot, @NotNull Shape shape, BlockPos spawn, @NotNull Island newIsland) throws DataSourceException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void setName(@NotNull Plot plot, @NotNull String identity, @NotNull String name) - throws DataSourceException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void setSpawn(@NotNull Plot plot, @NotNull BlockPos spawn) throws DataSourceException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void deletePlot(@NotNull Plot plot) throws DataSourceException - { - throw new DataSourceException("Inconsistent city!"); - } - - @NotNull - @Override - public Set loadPlots(@NotNull Island island) throws DataSourceException - { - throw new DataSourceException("Inconsistent city!"); - } - - @Override - public void setCityCreationDenied(@NotNull Nature nature, boolean denied) throws DataSourceException - { - throw new DataSourceException("Inconsistent nature!"); - } - - @Override - public void setName(@NotNull Nature nature, @NotNull String name) throws DataSourceException - { - throw new DataSourceException("Inconsistent nature!"); - } - - @Override - public double invested(@NotNull City city, double value) throws DataSourceException - { - throw new DataSourceException("Inconsistent nature!"); - } - - @Override - public double invested(@NotNull Plot plot, double value) throws DataSourceException - { - throw new DataSourceException("Inconsistent nature!"); - } - - @Override - public void setInvestment(@NotNull Plot plot, double investment) throws DataSourceException - { - throw new DataSourceException("Inconsistent nature!"); - } - - @Override - public void setPrice(@NotNull City city, double price) throws DataSourceException - { - throw new DataSourceException("Inconsistent nature!"); - } - - @Override - public void setPrice(@NotNull Plot plot, double price) throws DataSourceException - { - throw new DataSourceException("Inconsistent nature!"); - } - } - - @Override - public String toString() - { - return "#Inconsistency!"; - } -} +package br.com.gamemods.minecity.structure; + +import br.com.gamemods.minecity.MineCity; +import br.com.gamemods.minecity.api.PlayerID; +import br.com.gamemods.minecity.api.command.Message; +import br.com.gamemods.minecity.api.permission.*; +import br.com.gamemods.minecity.api.shape.Shape; +import br.com.gamemods.minecity.api.world.BlockPos; +import br.com.gamemods.minecity.api.world.ChunkPos; +import br.com.gamemods.minecity.api.world.WorldDim; +import br.com.gamemods.minecity.datasource.api.DataSourceException; +import br.com.gamemods.minecity.datasource.api.ICityStorage; +import br.com.gamemods.minecity.datasource.api.IExceptPermissionStorage; +import br.com.gamemods.minecity.datasource.api.INatureStorage; +import br.com.gamemods.minecity.datasource.api.unchecked.UncheckedDataSourceException; +import br.com.gamemods.minecity.economy.Tax; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class Inconsistency implements ChunkOwner +{ + public static final Message INCONSISTENT_CHUNK_MESSAGE = new Message("inconsistent.chunk", "This chunk is inconsistent."); + public static final Inconsistency INSTANCE = new Inconsistency(); + public static final WorldDim WORLD = new WorldDim(-10000, "inconsistency", "Inconsistent"); + private static MineCity mineCity; + private static Island island; + private static City city; + private Inconsistency(){} + + public static Nature nature(WorldDim world) + { + VoidStorage voidStorage = new VoidStorage(); + try + { + return new Nature(mineCity, world, INCONSISTENT_CHUNK_MESSAGE, voidStorage, voidStorage, true); + } + catch(DataSourceException unexpected) + { + throw new RuntimeException(unexpected); + } + } + + public static void setMineCity(MineCity mineCity) + { + Inconsistency.mineCity = mineCity; + } + + public static City getInconsistentCity(MineCity mineCity) + { + if(city == null) + { + try + { + VoidStorage voidStorage = new VoidStorage(); + synchronized(WORLD) + { + city = new City(mineCity, "#inconsistent", "#Inconsistency", null, new BlockPos(WORLD, 0, 0, 0), + -1000, voidStorage, voidStorage, null, new Tax(0,0), 0, 0 + ); + } + } + catch(DataSourceException unexpected) + { + throw new RuntimeException(unexpected); + } + Arrays.asList(PermissionFlag.values()).forEach(f-> city.deny(f, INCONSISTENT_CHUNK_MESSAGE)); + city.allow(PermissionFlag.LEAVE); + } + return city; + } + + public static ClaimedChunk claim(ChunkPos pos) + { + Nature nature = pos.world.nature; + if(nature != null) + getInconsistentCity(nature.mineCity); + else + getInconsistentCity(); + + ClaimedChunk chunk = new ClaimedChunk(INSTANCE, pos); + chunk.invalidate(); + return chunk; + } + + public static City getInconsistentCity() + { + return getInconsistentCity(mineCity); + } + + public static Island getInconsistentIsland(MineCity mineCity) + { + if(island == null) + getInconsistentCity(mineCity); + return island; + } + + public static Island getInconsistentIsland() + { + return getInconsistentIsland(mineCity); + } + + private static class InconsistentIsland extends Island + { + public InconsistentIsland(VoidStorage storage) + { + super(Inconsistency.city, storage, storage, -1, WORLD, Collections.emptySet()); + } + + @NotNull + @Override + public City getCity() + { + City c = Inconsistency.city; + if(c == null) + synchronized(WORLD) + { + return city; + } + + return c; + } + + @Override + public int getSizeX() + { + return 0; + } + + @Override + public int getSizeZ() + { + return 0; + } + + @Override + public int getChunkCount() + { + return 0; + } + } + + private static class VoidStorage implements ICityStorage, IExceptPermissionStorage, INatureStorage + { + @NotNull + @Override + public Collection loadIslands(City city) throws DataSourceException + { + if(Inconsistency.city == null) + Inconsistency.city = city; + return Collections.singleton(island = new InconsistentIsland(this)); + } + + @Override + public void setOwner(@NotNull City city, @NotNull OptionalPlayer owner) + throws DataSourceException, IllegalStateException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void setSpawn(@NotNull City city, @NotNull BlockPos spawn) + throws DataSourceException, IllegalStateException + { + throw new DataSourceException("Inconsistent city!"); + } + + @NotNull + @Override + public Island createIsland(@NotNull City city, @NotNull ChunkPos chunk) + throws DataSourceException, IllegalStateException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void claim(@NotNull Island island, @NotNull ChunkPos chunk) + throws DataSourceException, IllegalStateException, ClassCastException + { + throw new DataSourceException("Inconsistent city!"); + } + + @NotNull + @Override + public Island claim(@NotNull Set islands, @NotNull ChunkPos chunk) + throws DataSourceException, IllegalStateException, NoSuchElementException, ClassCastException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void deleteIsland(@NotNull Island island) + throws DataSourceException, IllegalArgumentException, ClassCastException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void deleteCity(@NotNull City city) throws DataSourceException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void disclaim(@NotNull ChunkPos chunk, @NotNull Island island) + throws DataSourceException, IllegalArgumentException + { + throw new DataSourceException("Inconsistent city!"); + } + + @NotNull + @Override + public Collection disclaim(@NotNull ChunkPos chunk, @NotNull Island island, + @NotNull Set> groups) + throws DataSourceException, IllegalStateException, NoSuchElementException, ClassCastException, IllegalArgumentException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void setName(@NotNull City city, @NotNull String identity, @NotNull String name) + throws DataSourceException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void setName(@NotNull Group group, @NotNull String identity, @NotNull String name) + throws DataSourceException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void addMember(@NotNull Group group, @NotNull Identity member) + throws DataSourceException, UnsupportedOperationException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void removeMember(@NotNull Group group, @NotNull Identity member) + throws DataSourceException, UnsupportedOperationException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void addManager(@NotNull Group group, @NotNull PlayerID manager) throws DataSourceException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void removeManager(@NotNull Group group, @NotNull PlayerID manager) throws DataSourceException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void deleteGroup(@NotNull Group group) throws DataSourceException + { + throw new DataSourceException("Inconsistent city!"); + } + + @NotNull + @Override + public Collection reserve(@NotNull IslandArea reserve) throws DataSourceException + { + throw new DataSourceException("Inconsistent city!"); + } + + @NotNull + @Override + public Group createGroup(@NotNull City city, @NotNull String id, @NotNull String name) + throws DataSourceException + { + throw new DataSourceException("Inconsistent city!"); + } + + @NotNull + @Override + public Collection loadGroups(@NotNull City city) throws DataSourceException + { + if(city.getId() != -1000) + throw new DataSourceException("Inconsistent city!"); + return Collections.emptyList(); + } + + @Override + public void setDefaultMessage(@NotNull SimpleFlagHolder holder, @Nullable Message message) + throws DataSourceException + { + if(holder != city) + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void deny(@NotNull SimpleFlagHolder holder, @NotNull PermissionFlag flag, @Nullable Message message) + throws DataSourceException + { + if(holder != city) + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void denyAll(SimpleFlagHolder holder, Map flags) + throws DataSourceException + { + if(holder != city) + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void allow(@NotNull SimpleFlagHolder holder, @NotNull PermissionFlag flag) throws DataSourceException + { + if(holder != city) + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void allowAll(@NotNull SimpleFlagHolder holder) throws DataSourceException + { + if(holder != city) + throw new DataSourceException("Inconsistent city!"); + } + + @NotNull + @Override + public EnumMap loadSimplePermissions(@NotNull SimpleFlagHolder holder) + throws DataSourceException + { + EnumMap result = new EnumMap<>(PermissionFlag.class); + Arrays.stream(PermissionFlag.values()).forEach(f-> result.put(f, INCONSISTENT_CHUNK_MESSAGE)); + return result; + } + + @Override + public void set(@NotNull ExceptFlagHolder holder, @NotNull PermissionFlag flag, boolean allow, + @NotNull Identity identity, @Nullable Message message) throws DataSourceException + { + if(holder != city) + throw new UncheckedDataSourceException(new DataSourceException("Inconsistent city!")); + } + + @Override + public void remove(@NotNull ExceptFlagHolder holder, @NotNull PermissionFlag flag, + @NotNull Identity identity) + throws DataSourceException + { + if(holder != city) + throw new DataSourceException("Inconsistent city!"); + } + + @NotNull + @Override + public Map, Optional>> loadExceptPermissions(@NotNull ExceptFlagHolder holder) + throws DataSourceException + { + return Collections.emptyMap(); + } + + @Override + public int createPlot(Plot plot) throws DataSourceException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void setOwner(@NotNull Plot plot, @Nullable PlayerID owner) + throws DataSourceException, IllegalStateException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void setShape(@NotNull Plot plot, @NotNull Shape shape, BlockPos spawn, @NotNull Island newIsland) throws DataSourceException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void setName(@NotNull Plot plot, @NotNull String identity, @NotNull String name) + throws DataSourceException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void setSpawn(@NotNull Plot plot, @NotNull BlockPos spawn) throws DataSourceException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void deletePlot(@NotNull Plot plot) throws DataSourceException + { + throw new DataSourceException("Inconsistent city!"); + } + + @NotNull + @Override + public Set loadPlots(@NotNull Island island) throws DataSourceException + { + throw new DataSourceException("Inconsistent city!"); + } + + @Override + public void setCityCreationDenied(@NotNull Nature nature, boolean denied) throws DataSourceException + { + throw new DataSourceException("Inconsistent nature!"); + } + + @Override + public void setName(@NotNull Nature nature, @NotNull String name) throws DataSourceException + { + throw new DataSourceException("Inconsistent nature!"); + } + + @Override + public double invested(@NotNull City city, double value) throws DataSourceException + { + throw new DataSourceException("Inconsistent nature!"); + } + + @Override + public double invested(@NotNull Plot plot, double value) throws DataSourceException + { + throw new DataSourceException("Inconsistent nature!"); + } + + @Override + public void setInvestment(@NotNull Plot plot, double investment) throws DataSourceException + { + throw new DataSourceException("Inconsistent nature!"); + } + + @Override + public void setPrice(@NotNull City city, double price) throws DataSourceException + { + throw new DataSourceException("Inconsistent nature!"); + } + + @Override + public void setPrice(@NotNull Plot plot, double price) throws DataSourceException + { + throw new DataSourceException("Inconsistent nature!"); + } + } + + @Override + public String toString() + { + return "#Inconsistency!"; + } +} diff --git a/Core/src/main/java/br/com/gamemods/minecity/structure/Island.java b/Core/src/main/java/br/com/gamemods/minecity/structure/Island.java index 3f2017a6..c9cb16dc 100644 --- a/Core/src/main/java/br/com/gamemods/minecity/structure/Island.java +++ b/Core/src/main/java/br/com/gamemods/minecity/structure/Island.java @@ -1,170 +1,170 @@ -package br.com.gamemods.minecity.structure; - -import br.com.gamemods.minecity.api.PlayerID; -import br.com.gamemods.minecity.api.Slow; -import br.com.gamemods.minecity.api.StringUtil; -import br.com.gamemods.minecity.api.shape.Shape; -import br.com.gamemods.minecity.api.world.BlockPos; -import br.com.gamemods.minecity.api.world.ChunkPos; -import br.com.gamemods.minecity.api.world.WorldDim; -import br.com.gamemods.minecity.datasource.api.DataSourceException; -import br.com.gamemods.minecity.datasource.api.ICityStorage; -import br.com.gamemods.minecity.datasource.api.IExceptPermissionStorage; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; -import java.util.stream.Stream; - -public abstract class Island implements ChunkOwner -{ - public final Reserve reserve = new Reserve(this); - - @NotNull - protected final ICityStorage storage; - - @NotNull - protected final IExceptPermissionStorage permissionStorage; - public final int id; - public final WorldDim world; - protected Map plots; - @NotNull - protected City city; - - public Island(@NotNull City city, @NotNull ICityStorage storage, @NotNull IExceptPermissionStorage permissionStorage, - int id, WorldDim world, Set plots) - { - this.city = city; - this.storage = storage; - this.permissionStorage = permissionStorage; - this.id = id; - this.world = world; - this.plots = new HashMap<>(plots.size()); - plots.forEach(plot -> this.plots.put(plot.getIdentityName(), plot)); - } - - public Island(@NotNull City city, @NotNull ICityStorage storage, @NotNull IExceptPermissionStorage permissionStorage, - int id, WorldDim world) - throws DataSourceException - { - this.city = city; - this.storage = storage; - this.permissionStorage = permissionStorage; - this.id = id; - this.world = world; - - Set plots = storage.loadPlots(this); - this.plots = new HashMap<>(plots.size()); - plots.forEach(plot -> this.plots.put(plot.getIdentityName(), plot)); - } - - @Slow - public Plot createPlot(@NotNull String name, @Nullable PlayerID owner, @NotNull BlockPos spawn, @NotNull Shape shape) - throws DataSourceException - { - String identity = StringUtil.identity(name); - for(Island island : getCity().islands()) - { - Plot conflict = island.plots.get(identity); - if(conflict != null) - throw new IllegalArgumentException("The name "+name+" conflicts with "+conflict.getName()); - } - - if(!spawn.world.equals(world)) - throw new IllegalArgumentException("The spawn is in a different world"); - - Plot plot = new Plot(storage, permissionStorage, this, identity, name, owner, spawn, shape); - plots.put(identity, plot); - return plot; - } - - public Optional getPlot(String name) - { - return Optional.ofNullable(plots.get(StringUtil.identity(name))); - } - - public Collection getPlots() - { - return Collections.unmodifiableCollection(plots.values()); - } - - public Stream getPlotsAt(ChunkPos pos) - { - if(!pos.world.equals(world)) - return Stream.empty(); - - return plots.values().stream().filter(plot -> plot.getShape().affects(pos)); - } - - public Optional getPlotAt(BlockPos pos) - { - if(!pos.world.equals(world)) - return Optional.empty(); - - for(Plot plot : plots.values()) - if(plot.getShape().contains(pos.x, pos.y, pos.z)) - return Optional.of(plot); - - return Optional.empty(); - } - - public Stream getPlotNames() - { - return plots.values().stream().map(Plot::getName); - } - - public Set getPlotIdNames() - { - return Collections.unmodifiableSet(plots.keySet()); - } - - public final int getId() - { - return id; - } - - @NotNull - public final WorldDim getWorld() - { - return world; - } - - @NotNull - public City getCity() - { - return city; - } - - protected void setCity(@NotNull City city) - { - this.city = city; - } - - public abstract int getSizeX(); - public abstract int getSizeZ(); - public abstract int getChunkCount(); - - public IslandArea getArea() throws DataSourceException - { - return getCity().mineCity.dataSource.getArea(this); - } - - @Override - public String toString() - { - return "Island{" + - "id=" + id + - ", world=" + world + - ", city=" + getCity().getName() + - '}'; - } - - public Optional searchPlot(String name) - { - Optional plot = getPlot(name); - if(plot.isPresent()) - return plot; - - return getCity().getPlot(name); - } -} +package br.com.gamemods.minecity.structure; + +import br.com.gamemods.minecity.api.PlayerID; +import br.com.gamemods.minecity.api.Slow; +import br.com.gamemods.minecity.api.StringUtil; +import br.com.gamemods.minecity.api.shape.Shape; +import br.com.gamemods.minecity.api.world.BlockPos; +import br.com.gamemods.minecity.api.world.ChunkPos; +import br.com.gamemods.minecity.api.world.WorldDim; +import br.com.gamemods.minecity.datasource.api.DataSourceException; +import br.com.gamemods.minecity.datasource.api.ICityStorage; +import br.com.gamemods.minecity.datasource.api.IExceptPermissionStorage; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.stream.Stream; + +public abstract class Island implements ChunkOwner +{ + public final Reserve reserve = new Reserve(this); + + @NotNull + protected final ICityStorage storage; + + @NotNull + protected final IExceptPermissionStorage permissionStorage; + public final int id; + public final WorldDim world; + protected Map plots; + @NotNull + protected City city; + + public Island(@NotNull City city, @NotNull ICityStorage storage, @NotNull IExceptPermissionStorage permissionStorage, + int id, WorldDim world, Set plots) + { + this.city = city; + this.storage = storage; + this.permissionStorage = permissionStorage; + this.id = id; + this.world = world; + this.plots = new HashMap<>(plots.size()); + plots.forEach(plot -> this.plots.put(plot.getIdentityName(), plot)); + } + + public Island(@NotNull City city, @NotNull ICityStorage storage, @NotNull IExceptPermissionStorage permissionStorage, + int id, WorldDim world) + throws DataSourceException + { + this.city = city; + this.storage = storage; + this.permissionStorage = permissionStorage; + this.id = id; + this.world = world; + + Set plots = storage.loadPlots(this); + this.plots = new HashMap<>(plots.size()); + plots.forEach(plot -> this.plots.put(plot.getIdentityName(), plot)); + } + + @Slow + public Plot createPlot(@NotNull String name, @Nullable PlayerID owner, @NotNull BlockPos spawn, @NotNull Shape shape) + throws DataSourceException + { + String identity = StringUtil.identity(name); + for(Island island : getCity().islands()) + { + Plot conflict = island.plots.get(identity); + if(conflict != null) + throw new IllegalArgumentException("The name "+name+" conflicts with "+conflict.getName()); + } + + if(!spawn.world.equals(world)) + throw new IllegalArgumentException("The spawn is in a different world"); + + Plot plot = new Plot(storage, permissionStorage, this, identity, name, owner, spawn, shape); + plots.put(identity, plot); + return plot; + } + + public Optional getPlot(String name) + { + return Optional.ofNullable(plots.get(StringUtil.identity(name))); + } + + public Collection getPlots() + { + return Collections.unmodifiableCollection(plots.values()); + } + + public Stream getPlotsAt(ChunkPos pos) + { + if(!pos.world.equals(world)) + return Stream.empty(); + + return plots.values().stream().filter(plot -> plot.getShape().affects(pos)); + } + + public Optional getPlotAt(BlockPos pos) + { + if(!pos.world.equals(world)) + return Optional.empty(); + + for(Plot plot : plots.values()) + if(plot.getShape().contains(pos.x, pos.y, pos.z)) + return Optional.of(plot); + + return Optional.empty(); + } + + public Stream getPlotNames() + { + return plots.values().stream().map(Plot::getName); + } + + public Set getPlotIdNames() + { + return Collections.unmodifiableSet(plots.keySet()); + } + + public final int getId() + { + return id; + } + + @NotNull + public final WorldDim getWorld() + { + return world; + } + + @NotNull + public City getCity() + { + return city; + } + + protected void setCity(@NotNull City city) + { + this.city = city; + } + + public abstract int getSizeX(); + public abstract int getSizeZ(); + public abstract int getChunkCount(); + + public IslandArea getArea() throws DataSourceException + { + return getCity().mineCity.dataSource.getArea(this); + } + + @Override + public String toString() + { + return "Island{" + + "id=" + id + + ", world=" + world + + ", city=" + getCity().getName() + + '}'; + } + + public Optional searchPlot(String name) + { + Optional plot = getPlot(name); + if(plot.isPresent()) + return plot; + + return getCity().getPlot(name); + } +} diff --git a/Core/src/main/java/br/com/gamemods/minecity/structure/Nature.java b/Core/src/main/java/br/com/gamemods/minecity/structure/Nature.java index 91f149da..c74bb8a2 100644 --- a/Core/src/main/java/br/com/gamemods/minecity/structure/Nature.java +++ b/Core/src/main/java/br/com/gamemods/minecity/structure/Nature.java @@ -1,112 +1,112 @@ -package br.com.gamemods.minecity.structure; - -import br.com.gamemods.minecity.MineCity; -import br.com.gamemods.minecity.api.Slow; -import br.com.gamemods.minecity.api.command.Message; -import br.com.gamemods.minecity.api.permission.NatureID; -import br.com.gamemods.minecity.api.world.WorldDim; -import br.com.gamemods.minecity.datasource.api.DataSourceException; -import br.com.gamemods.minecity.datasource.api.INatureStorage; -import br.com.gamemods.minecity.datasource.api.ISimplePermissionStorage; -import br.com.gamemods.minecity.datasource.api.SimpleStorageHolder; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public final class Nature extends SimpleStorageHolder implements ChunkOwner -{ - @NotNull - private final INatureStorage storage; - - @NotNull - public final MineCity mineCity; - - @NotNull - public final WorldDim world; - - @NotNull - private final NatureID id; - - private boolean valid = true; - private boolean denyCityCreation; - - public Nature(@NotNull MineCity mineCity, @NotNull WorldDim world, - @NotNull INatureStorage storage, @NotNull ISimplePermissionStorage permissionStorage) - { - this.storage = storage; - this.permissionStorage = permissionStorage; - this.mineCity = mineCity; - this.world = world; - this.id = new NatureID(world); - - defaultMessages = mineCity.defaultNatureFlags.getDefaultMessages(); - denyAll(mineCity.defaultNatureFlags); - } - - public Nature(@NotNull MineCity mineCity, @NotNull WorldDim world, @Nullable Message defaultDenialMessage, - @NotNull INatureStorage storage, @NotNull ISimplePermissionStorage permissionStorage, - boolean denyCityCreation) - throws DataSourceException - { - super(defaultDenialMessage); - this.mineCity = mineCity; - this.permissionStorage = permissionStorage; - this.storage = storage; - this.world = world; - this.id = new NatureID(world); - this.denyCityCreation = denyCityCreation; - - defaultMessages = mineCity.defaultNatureFlags.getDefaultMessages(); - loadPermissions(); - } - - @Slow - public void setName(String name) throws DataSourceException - { - if(!valid) - throw new IllegalStateException(); - - storage.setName(this, name); - world.name = name; - } - - @Slow - public void setCityCreationDenied(boolean denied) - throws IllegalStateException, DataSourceException - { - if(!valid) - throw new IllegalStateException(); - - storage.setCityCreationDenied(this, denied); - denyCityCreation = denied; - } - - public boolean isCityCreationDenied() - { - return denyCityCreation; - } - - public void invalidate() - { - valid = false; - } - - public boolean isValid() - { - return valid; - } - - @NotNull - @Override - public NatureID owner() - { - return id; - } - - @Override - public String toString() - { - return "Nature{" + - "world=" + world + - '}'; - } -} +package br.com.gamemods.minecity.structure; + +import br.com.gamemods.minecity.MineCity; +import br.com.gamemods.minecity.api.Slow; +import br.com.gamemods.minecity.api.command.Message; +import br.com.gamemods.minecity.api.permission.NatureID; +import br.com.gamemods.minecity.api.world.WorldDim; +import br.com.gamemods.minecity.datasource.api.DataSourceException; +import br.com.gamemods.minecity.datasource.api.INatureStorage; +import br.com.gamemods.minecity.datasource.api.ISimplePermissionStorage; +import br.com.gamemods.minecity.datasource.api.SimpleStorageHolder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class Nature extends SimpleStorageHolder implements ChunkOwner +{ + @NotNull + private final INatureStorage storage; + + @NotNull + public final MineCity mineCity; + + @NotNull + public final WorldDim world; + + @NotNull + private final NatureID id; + + private boolean valid = true; + private boolean denyCityCreation; + + public Nature(@NotNull MineCity mineCity, @NotNull WorldDim world, + @NotNull INatureStorage storage, @NotNull ISimplePermissionStorage permissionStorage) + { + this.storage = storage; + this.permissionStorage = permissionStorage; + this.mineCity = mineCity; + this.world = world; + this.id = new NatureID(world); + + defaultMessages = mineCity.defaultNatureFlags.getDefaultMessages(); + denyAll(mineCity.defaultNatureFlags); + } + + public Nature(@NotNull MineCity mineCity, @NotNull WorldDim world, @Nullable Message defaultDenialMessage, + @NotNull INatureStorage storage, @NotNull ISimplePermissionStorage permissionStorage, + boolean denyCityCreation) + throws DataSourceException + { + super(defaultDenialMessage); + this.mineCity = mineCity; + this.permissionStorage = permissionStorage; + this.storage = storage; + this.world = world; + this.id = new NatureID(world); + this.denyCityCreation = denyCityCreation; + + defaultMessages = mineCity.defaultNatureFlags.getDefaultMessages(); + loadPermissions(); + } + + @Slow + public void setName(String name) throws DataSourceException + { + if(!valid) + throw new IllegalStateException(); + + storage.setName(this, name); + world.name = name; + } + + @Slow + public void setCityCreationDenied(boolean denied) + throws IllegalStateException, DataSourceException + { + if(!valid) + throw new IllegalStateException(); + + storage.setCityCreationDenied(this, denied); + denyCityCreation = denied; + } + + public boolean isCityCreationDenied() + { + return denyCityCreation; + } + + public void invalidate() + { + valid = false; + } + + public boolean isValid() + { + return valid; + } + + @NotNull + @Override + public NatureID owner() + { + return id; + } + + @Override + public String toString() + { + return "Nature{" + + "world=" + world + + '}'; + } +} diff --git a/Core/src/test/java/br/com/gamemods/minecity/MineCityTest.java b/Core/src/test/java/br/com/gamemods/minecity/MineCityTest.java index 4360914e..fd5312fd 100644 --- a/Core/src/test/java/br/com/gamemods/minecity/MineCityTest.java +++ b/Core/src/test/java/br/com/gamemods/minecity/MineCityTest.java @@ -1,70 +1,70 @@ -package br.com.gamemods.minecity; - -import br.com.gamemods.minecity.api.world.BlockPos; -import br.com.gamemods.minecity.api.world.ChunkPos; -import br.com.gamemods.minecity.api.world.WorldDim; -import br.com.gamemods.minecity.datasource.test.TestData; -import br.com.gamemods.minecity.structure.City; -import br.com.gamemods.minecity.structure.ClaimedChunk; -import br.com.gamemods.minecity.structure.Nature; -import org.junit.Before; -import org.junit.Test; - -import java.util.Optional; - -import static org.junit.Assert.*; - -public class MineCityTest -{ - private TestData test; - private City city; - private BlockPos spawn; - - @Before - public void setUp() throws Exception - { - test = new TestData(); - spawn = new BlockPos(test.overWorld, 0,64,0); - city = new City(test.mineCity, "Test City", test.joserobjr, spawn, 0); - test.mineCity.loadChunk(city.getSpawn().getChunk()); - } - - @Test - public void testGetChunk() throws Exception - { - BlockPos off = spawn.subtract(1, 0, 1); - assertEquals(Optional.empty(), test.mineCity.getChunk(off)); - - Optional expected = Optional.of(new ClaimedChunk(test.mineCity.nature(spawn.world), off.getChunk())); - assertEquals(Optional.empty(), test.mineCity.getChunk(off)); - - assertEquals(expected.get(), test.mineCity.loadChunk(off.getChunk())); - assertEquals(expected, test.mineCity.getChunk(off)); - - ChunkPos chunk = spawn.getChunk(); - expected = Optional.of(new ClaimedChunk(city.islands().iterator().next(), chunk)); - assertEquals(expected, test.mineCity.getChunk(spawn)); - assertEquals(expected, test.mineCity.getChunk(chunk)); - - test.mineCity.unloadChunk(off.getChunk()); - assertEquals(Optional.empty(), test.mineCity.getChunk(off)); - } - - @Test - public void testGetNature() throws Exception - { - WorldDim nether = new WorldDim(-1, "nether", "Nether"); - assertNull(test.mineCity.getNature(nether)); - - Nature nature = test.mineCity.nature(nether); - assertNotNull(nature); - assertEquals(test.mineCity.getNature(nether), nature); - ChunkPos pos = new ChunkPos(nether, 0, 0); - ClaimedChunk claim = test.mineCity.loadChunk(pos); - assertNotNull(claim); - assertEquals(nature, claim.owner); - assertEquals(test.mineCity.unloadNature(nether), nature); - assertNull(test.mineCity.getNature(nether)); - assertEquals(Optional.empty(), test.mineCity.getChunk(pos)); - } +package br.com.gamemods.minecity; + +import br.com.gamemods.minecity.api.world.BlockPos; +import br.com.gamemods.minecity.api.world.ChunkPos; +import br.com.gamemods.minecity.api.world.WorldDim; +import br.com.gamemods.minecity.datasource.test.TestData; +import br.com.gamemods.minecity.structure.City; +import br.com.gamemods.minecity.structure.ClaimedChunk; +import br.com.gamemods.minecity.structure.Nature; +import org.junit.Before; +import org.junit.Test; + +import java.util.Optional; + +import static org.junit.Assert.*; + +public class MineCityTest +{ + private TestData test; + private City city; + private BlockPos spawn; + + @Before + public void setUp() throws Exception + { + test = new TestData(); + spawn = new BlockPos(test.overWorld, 0,64,0); + city = new City(test.mineCity, "Test City", test.joserobjr, spawn, 0); + test.mineCity.loadChunk(city.getSpawn().getChunk()); + } + + @Test + public void testGetChunk() throws Exception + { + BlockPos off = spawn.subtract(1, 0, 1); + assertEquals(Optional.empty(), test.mineCity.getChunk(off)); + + Optional expected = Optional.of(new ClaimedChunk(test.mineCity.nature(spawn.world), off.getChunk())); + assertEquals(Optional.empty(), test.mineCity.getChunk(off)); + + assertEquals(expected.get(), test.mineCity.loadChunk(off.getChunk())); + assertEquals(expected, test.mineCity.getChunk(off)); + + ChunkPos chunk = spawn.getChunk(); + expected = Optional.of(new ClaimedChunk(city.islands().iterator().next(), chunk)); + assertEquals(expected, test.mineCity.getChunk(spawn)); + assertEquals(expected, test.mineCity.getChunk(chunk)); + + test.mineCity.unloadChunk(off.getChunk()); + assertEquals(Optional.empty(), test.mineCity.getChunk(off)); + } + + @Test + public void testGetNature() throws Exception + { + WorldDim nether = new WorldDim(-1, "nether", "Nether"); + assertNull(test.mineCity.getNature(nether)); + + Nature nature = test.mineCity.nature(nether); + assertNotNull(nature); + assertEquals(test.mineCity.getNature(nether), nature); + ChunkPos pos = new ChunkPos(nether, 0, 0); + ClaimedChunk claim = test.mineCity.loadChunk(pos); + assertNotNull(claim); + assertEquals(nature, claim.owner); + assertEquals(test.mineCity.unloadNature(nether), nature); + assertNull(test.mineCity.getNature(nether)); + assertEquals(Optional.empty(), test.mineCity.getChunk(pos)); + } } \ No newline at end of file diff --git a/Core/src/test/java/br/com/gamemods/minecity/structure/CityTest.java b/Core/src/test/java/br/com/gamemods/minecity/structure/CityTest.java index aaef7a10..a261be93 100644 --- a/Core/src/test/java/br/com/gamemods/minecity/structure/CityTest.java +++ b/Core/src/test/java/br/com/gamemods/minecity/structure/CityTest.java @@ -1,314 +1,314 @@ -package br.com.gamemods.minecity.structure; - -import br.com.gamemods.minecity.api.PlayerID; -import br.com.gamemods.minecity.api.world.BlockPos; -import br.com.gamemods.minecity.api.world.ChunkPos; -import br.com.gamemods.minecity.api.world.Direction; -import br.com.gamemods.minecity.datasource.api.DataSourceException; -import br.com.gamemods.minecity.datasource.test.TestData; -import org.junit.Before; -import org.junit.Test; - -import java.util.*; - -import static com.github.kolorobot.exceptions.java8.AssertJThrowableAssert.assertThrown; -import static org.junit.Assert.*; - -public class CityTest -{ - private TestData test; - @Before - public void setUp() throws Exception - { - test = new TestData(); - } - - @Test - @SuppressWarnings("SpellCheckingInspection") - public void testDisclaim() throws Exception - { - BlockPos spawn = new BlockPos(test.overWorld, 200,64,100); - ChunkPos spawnChunk = spawn.getChunk(); - City city = new City(test.mineCity, "Disclaim", test.joserobjr, spawn, 0); - Island spawnIsland = city.islands().iterator().next(); - - assertThrown(()-> city.disclaim(spawnChunk, false)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("last"); - - - ChunkPos chunk = spawnChunk.add(Direction.NORTH); - assertEquals(spawnIsland, city.claim(chunk, false)); - - assertThrown(()-> city.disclaim(spawnChunk, false)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("spawn"); - - assertThrown(()-> city.claim(spawnChunk, false)) - .hasMessageContaining("reserved") - .isInstanceOf(IllegalArgumentException.class); - - assertEquals(Collections.singleton(spawnIsland), city.disclaim(chunk, false)); - assertEquals(1, city.getSizeX()); - assertEquals(1, city.getSizeZ()); - assertEquals(1, city.getChunkCount()); - - chunk = spawnChunk.add(Direction.SOUTH, 5); - Island islandA = city.claim(chunk, true); - assertNotEquals(spawnIsland, islandA); - assertEquals(2, city.getSizeX()); - assertEquals(2, city.getSizeZ()); - assertEquals(2, city.getChunkCount()); - assertEquals(Arrays.asList(spawnIsland, islandA), new ArrayList<>(city.islands())); - - assertEquals(Collections.singleton(islandA), city.disclaim(chunk, false)); - assertEquals(1, city.getSizeX()); - assertEquals(1, city.getSizeZ()); - assertEquals(1, city.getChunkCount()); - assertEquals(Collections.singletonList(spawnIsland), new ArrayList<>(city.islands())); - - Island islandB = city.claim(chunk, true); - assertNotEquals(islandA, islandB); - assertEquals(2, city.getSizeX()); - assertEquals(2, city.getSizeZ()); - assertEquals(2, city.getChunkCount()); - assertEquals(Arrays.asList(spawnIsland, islandB), new ArrayList<>(city.islands())); - - assertEquals(islandB, city.claim(chunk.add(Direction.EAST), false)); - assertEquals(3, city.getSizeX()); - assertEquals(2, city.getSizeZ()); - assertEquals(3, city.getChunkCount()); - assertEquals(Collections.singleton(islandB), city.disclaim(chunk.add(Direction.EAST), false)); - assertEquals(2, city.getSizeX()); - assertEquals(2, city.getSizeZ()); - assertEquals(2, city.getChunkCount()); - - /* - * X → - * 123456789 - * Z-1| X | - * ↓ 0| XX | - * 1| XXDXXXX | - * 4| X | - * 5| XX | - * 6| X | - */ - assertEquals(islandB, city.claim(chunk.add(Direction.EAST), false)); - chunk = chunk.add(Direction.EAST, 2); - assertEquals(islandB, city.claim(chunk, false)); - assertEquals(islandB, city.claim(chunk.add(Direction.EAST), false)); - assertEquals(islandB, city.claim(chunk.add(Direction.EAST, 2), false)); - assertEquals(islandB, city.claim(chunk.add(Direction.EAST, 3), false)); - assertEquals(islandB, city.claim(chunk.add(Direction.EAST, 4), false)); - assertEquals(islandB, city.claim(chunk.add(Direction.NORTH), false)); - assertEquals(islandB, city.claim(chunk.add(Direction.NORTH, 2), false)); - assertEquals(islandB, city.claim(chunk.add(Direction.NORTH_EAST), false)); - assertEquals(islandB, city.claim(chunk.add(Direction.SOUTH), false)); - assertEquals(islandB, city.claim(chunk.add(Direction.SOUTH, 2), false)); - assertEquals(islandB, city.claim(chunk.add(Direction.SOUTH, 2).add(Direction.EAST), false)); - assertEquals(islandB, city.claim(chunk.add(Direction.SOUTH, 3), false)); - assertEquals(14, islandB.getChunkCount()); - assertEquals(7, islandB.getSizeX()); - assertEquals(6, islandB.getSizeZ()); - assertEquals(15, city.getChunkCount()); - assertEquals(8, city.getSizeX()); - assertEquals(7, city.getSizeZ()); - assertEquals(2, city.islands().size()); - - /* - * X → - * 123456789 - * Z-1| B | - * ↓ 0| BB | - * 1| XX BBBB | - * 4| Y | - * 5| YY | - * 6| Y | - */ - ChunkPos pos = chunk; - assertThrown(()-> city.disclaim(pos, false)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("required"); - - Collection islands = city.disclaim(pos, true); - assertEquals(2, islands.size()); - Island islandX = islands.stream().min((a,b)-> a.getChunkCount()-b.getChunkCount()).get(); - Island islandY = islands.stream().filter(i-> i != islandX).findAny().get(); - assertEquals(2, islandX.getChunkCount()); - assertEquals(4, islandY.getChunkCount()); - assertEquals(7, islandB.getChunkCount()); - assertEquals(5, islandB.getSizeX()); - assertEquals(3, islandB.getSizeZ()); - assertEquals(14, city.getChunkCount()); - assertEquals(10, city.getSizeX()); - assertEquals(8, city.getSizeZ()); - assertEquals(4, city.islands().size()); - - city.islands().forEach(island -> assertEquals(island, city.getIsland(island.getId()))); - } - - @Test - public void testCreateCity() throws DataSourceException - { - BlockPos spawn = new BlockPos(test.overWorld, 0, 64, 0); - test.mineCity.loadNature(spawn.world); - test.mineCity.loadChunk(spawn.getChunk()); - - City city = new City(test.mineCity, "Test City", test.joserobjr, spawn, 0); - assertTrue(city.getId() > 0); - assertEquals(test.joserobjr, city.owner()); - assertEquals(spawn, city.getSpawn()); - assertEquals(1, city.getSizeX()); - assertEquals(1, city.getSizeZ()); - assertEquals(1, city.getChunkCount()); - assertEquals(1, city.islands().size()); - assertEquals("Test City", city.getName()); - assertEquals(city, test.mineCity.getChunk(spawn.getChunk()).flatMap(ClaimedChunk::getCity).orElse(null)); - - Island island = city.islands().iterator().next(); - assertEquals(city, island.getCity()); - assertEquals(1, island.getSizeX()); - assertEquals(1, island.getSizeZ()); - assertEquals(1, island.getChunkCount()); - assertTrue(island.getId() > 0); - assertEquals(island, city.getIsland(island.getId())); - - assertEquals(test.mineCity.loadChunk(city.getSpawn().getChunk()), new ClaimedChunk( - island, city.getSpawn().getChunk())); - - assertThrown(()-> new City(test.mineCity, "Bad City", test.joserobjr, spawn, 0)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("reserved"); - - } - - @Test - public void testSetId() throws Exception - { - City badCity = new City(test.mineCity, "Bad City", test.joserobjr, new BlockPos(test.overWorld, 400,40, 65), 0); - assertThrown(()-> badCity.setId(-3)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("id = "+-3); - assertThrown(()-> badCity.setId(3)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("change"); - } - - @Test - public void testSetName() throws Exception - { - City first = new City(test.mineCity, "First City", test.joserobjr, new BlockPos(test.overWorld, -598, 44, -998), 0); - //noinspection SpellCheckingInspection - assertEquals("firstcity", first.getIdentityName()); - assertThrown(()-> new City(test.mineCity, "first_ciTy!", test.joserobjr, new BlockPos(test.overWorld, 98988,55,9874), 0)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("taken"); - - City second = new City(test.mineCity, "City 2", test.joserobjr, new BlockPos(test.overWorld, 788,68,9885), 0); - assertEquals("city2", second.getIdentityName()); - first.setName("City1"); - assertEquals("city1", first.getIdentityName()); - assertEquals("City1", first.getName()); - - assertThrown(() -> first.setName("CITY2")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("taken"); - first.setName("CITY 1"); - assertEquals("city1", first.getIdentityName()); - assertEquals("CITY 1", first.getName()); - - assertThrown(()-> first.setName("c1")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Bad"); - - assertThrown(()-> new City(test.mineCity, "c1", test.joserobjr, new BlockPos(test.overWorld, 5846487,4,448), 0)); - } - - @Test - public void testSetSpawn() throws Exception - { - BlockPos spawn = new BlockPos(test.overWorld, 54648, 32, 5855); - City spawnCity = new City(test.mineCity, "SpawnCity", test.joserobjr, spawn, 0); - - assertEquals(spawn, spawnCity.getSpawn()); - assertThrown(()-> spawnCity.setSpawn(spawn.getChunk().add(Direction.EAST).getMaxBlock())) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("not part of the city"); - - BlockPos newSpawn = spawn.getChunk().getMaxBlock(); - spawnCity.setSpawn(newSpawn); - assertEquals(newSpawn, spawnCity.getSpawn()); - } - - @Test - public void testSetOwner() throws Exception - { - City owned = new City(test.mineCity, "Owned", test.joserobjr, new BlockPos(test.overWorld, 456484878, 32, 445454), 0); - - assertEquals(test.joserobjr, owned.owner()); - PlayerID newOwner = new PlayerID(UUID.randomUUID(), "Randy"); - owned.setOwner(newOwner); - assertEquals(newOwner, owned.owner()); - } - - @Test - public void testClaim() throws DataSourceException - { - BlockPos spawn = new BlockPos(test.overWorld, 250, 32, -200); - ChunkPos chunk = spawn.getChunk(); - City city = new City(test.mineCity, "City 2", test.joserobjr, spawn, 0); - - Island islandA = city.islands().iterator().next(); - assertEquals(islandA, city.claim(chunk.add(Direction.NORTH), false)); - assertEquals(2, islandA.getChunkCount()); - assertEquals(2, islandA.getSizeZ()); - assertEquals(1, islandA.getSizeX()); - assertEquals(2, city.getChunkCount()); - assertEquals(2, city.getSizeZ()); - assertEquals(1, city.getSizeX()); - - assertEquals(islandA, city.claim(chunk.add(Direction.WEST), false)); - assertEquals(3, islandA.getChunkCount()); - assertEquals(2, islandA.getSizeZ()); - assertEquals(2, islandA.getSizeX()); - assertEquals(3, city.getChunkCount()); - assertEquals(2, city.getSizeZ()); - assertEquals(2, city.getSizeX()); - - Island islandB = city.claim(chunk.add(Direction.EAST, 2), true); - assertNotEquals(islandA, islandB); - assertEquals(1, islandB.getChunkCount()); - assertEquals(1, islandB.getSizeZ()); - assertEquals(1, islandB.getSizeX()); - assertEquals(4, city.getChunkCount()); - assertEquals(3, city.getSizeZ()); - assertEquals(3, city.getSizeX()); - assertEquals(2, city.islands().size()); - - assertEquals(islandA, city.claim(chunk.add(Direction.EAST), false)); - assertEquals(5, islandA.getChunkCount()); - assertEquals(2, islandA.getSizeZ()); - assertEquals(4, islandA.getSizeX()); - assertEquals(5, city.getChunkCount()); - assertEquals(2, city.getSizeZ()); - assertEquals(4, city.getSizeX()); - assertEquals(1, city.islands().size()); - assertEquals(0, islandB.getChunkCount()); - assertEquals(0, islandB.getSizeX()); - assertEquals(0, islandB.getSizeX()); - } - - @Test - public void testClaimIsland() throws Exception - { - City farCity = new City(test.mineCity, "FarCity", test.joserobjr, new BlockPos(test.overWorld, 655,55,488), 0); - - BlockPos far = new BlockPos(test.overWorld, -4847,44,688); - assertThrown(()-> farCity.claim(far.getChunk(), false)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("not touching"); - - farCity.claim(far.getChunk(), true); - assertEquals(2, farCity.islands().size()); - } +package br.com.gamemods.minecity.structure; + +import br.com.gamemods.minecity.api.PlayerID; +import br.com.gamemods.minecity.api.world.BlockPos; +import br.com.gamemods.minecity.api.world.ChunkPos; +import br.com.gamemods.minecity.api.world.Direction; +import br.com.gamemods.minecity.datasource.api.DataSourceException; +import br.com.gamemods.minecity.datasource.test.TestData; +import org.junit.Before; +import org.junit.Test; + +import java.util.*; + +import static com.github.kolorobot.exceptions.java8.AssertJThrowableAssert.assertThrown; +import static org.junit.Assert.*; + +public class CityTest +{ + private TestData test; + @Before + public void setUp() throws Exception + { + test = new TestData(); + } + + @Test + @SuppressWarnings("SpellCheckingInspection") + public void testDisclaim() throws Exception + { + BlockPos spawn = new BlockPos(test.overWorld, 200,64,100); + ChunkPos spawnChunk = spawn.getChunk(); + City city = new City(test.mineCity, "Disclaim", test.joserobjr, spawn, 0); + Island spawnIsland = city.islands().iterator().next(); + + assertThrown(()-> city.disclaim(spawnChunk, false)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("last"); + + + ChunkPos chunk = spawnChunk.add(Direction.NORTH); + assertEquals(spawnIsland, city.claim(chunk, false)); + + assertThrown(()-> city.disclaim(spawnChunk, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("spawn"); + + assertThrown(()-> city.claim(spawnChunk, false)) + .hasMessageContaining("reserved") + .isInstanceOf(IllegalArgumentException.class); + + assertEquals(Collections.singleton(spawnIsland), city.disclaim(chunk, false)); + assertEquals(1, city.getSizeX()); + assertEquals(1, city.getSizeZ()); + assertEquals(1, city.getChunkCount()); + + chunk = spawnChunk.add(Direction.SOUTH, 5); + Island islandA = city.claim(chunk, true); + assertNotEquals(spawnIsland, islandA); + assertEquals(2, city.getSizeX()); + assertEquals(2, city.getSizeZ()); + assertEquals(2, city.getChunkCount()); + assertEquals(Arrays.asList(spawnIsland, islandA), new ArrayList<>(city.islands())); + + assertEquals(Collections.singleton(islandA), city.disclaim(chunk, false)); + assertEquals(1, city.getSizeX()); + assertEquals(1, city.getSizeZ()); + assertEquals(1, city.getChunkCount()); + assertEquals(Collections.singletonList(spawnIsland), new ArrayList<>(city.islands())); + + Island islandB = city.claim(chunk, true); + assertNotEquals(islandA, islandB); + assertEquals(2, city.getSizeX()); + assertEquals(2, city.getSizeZ()); + assertEquals(2, city.getChunkCount()); + assertEquals(Arrays.asList(spawnIsland, islandB), new ArrayList<>(city.islands())); + + assertEquals(islandB, city.claim(chunk.add(Direction.EAST), false)); + assertEquals(3, city.getSizeX()); + assertEquals(2, city.getSizeZ()); + assertEquals(3, city.getChunkCount()); + assertEquals(Collections.singleton(islandB), city.disclaim(chunk.add(Direction.EAST), false)); + assertEquals(2, city.getSizeX()); + assertEquals(2, city.getSizeZ()); + assertEquals(2, city.getChunkCount()); + + /* + * X → + * 123456789 + * Z-1| X | + * ↓ 0| XX | + * 1| XXDXXXX | + * 4| X | + * 5| XX | + * 6| X | + */ + assertEquals(islandB, city.claim(chunk.add(Direction.EAST), false)); + chunk = chunk.add(Direction.EAST, 2); + assertEquals(islandB, city.claim(chunk, false)); + assertEquals(islandB, city.claim(chunk.add(Direction.EAST), false)); + assertEquals(islandB, city.claim(chunk.add(Direction.EAST, 2), false)); + assertEquals(islandB, city.claim(chunk.add(Direction.EAST, 3), false)); + assertEquals(islandB, city.claim(chunk.add(Direction.EAST, 4), false)); + assertEquals(islandB, city.claim(chunk.add(Direction.NORTH), false)); + assertEquals(islandB, city.claim(chunk.add(Direction.NORTH, 2), false)); + assertEquals(islandB, city.claim(chunk.add(Direction.NORTH_EAST), false)); + assertEquals(islandB, city.claim(chunk.add(Direction.SOUTH), false)); + assertEquals(islandB, city.claim(chunk.add(Direction.SOUTH, 2), false)); + assertEquals(islandB, city.claim(chunk.add(Direction.SOUTH, 2).add(Direction.EAST), false)); + assertEquals(islandB, city.claim(chunk.add(Direction.SOUTH, 3), false)); + assertEquals(14, islandB.getChunkCount()); + assertEquals(7, islandB.getSizeX()); + assertEquals(6, islandB.getSizeZ()); + assertEquals(15, city.getChunkCount()); + assertEquals(8, city.getSizeX()); + assertEquals(7, city.getSizeZ()); + assertEquals(2, city.islands().size()); + + /* + * X → + * 123456789 + * Z-1| B | + * ↓ 0| BB | + * 1| XX BBBB | + * 4| Y | + * 5| YY | + * 6| Y | + */ + ChunkPos pos = chunk; + assertThrown(()-> city.disclaim(pos, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("required"); + + Collection islands = city.disclaim(pos, true); + assertEquals(2, islands.size()); + Island islandX = islands.stream().min((a,b)-> a.getChunkCount()-b.getChunkCount()).get(); + Island islandY = islands.stream().filter(i-> i != islandX).findAny().get(); + assertEquals(2, islandX.getChunkCount()); + assertEquals(4, islandY.getChunkCount()); + assertEquals(7, islandB.getChunkCount()); + assertEquals(5, islandB.getSizeX()); + assertEquals(3, islandB.getSizeZ()); + assertEquals(14, city.getChunkCount()); + assertEquals(10, city.getSizeX()); + assertEquals(8, city.getSizeZ()); + assertEquals(4, city.islands().size()); + + city.islands().forEach(island -> assertEquals(island, city.getIsland(island.getId()))); + } + + @Test + public void testCreateCity() throws DataSourceException + { + BlockPos spawn = new BlockPos(test.overWorld, 0, 64, 0); + test.mineCity.loadNature(spawn.world); + test.mineCity.loadChunk(spawn.getChunk()); + + City city = new City(test.mineCity, "Test City", test.joserobjr, spawn, 0); + assertTrue(city.getId() > 0); + assertEquals(test.joserobjr, city.owner()); + assertEquals(spawn, city.getSpawn()); + assertEquals(1, city.getSizeX()); + assertEquals(1, city.getSizeZ()); + assertEquals(1, city.getChunkCount()); + assertEquals(1, city.islands().size()); + assertEquals("Test City", city.getName()); + assertEquals(city, test.mineCity.getChunk(spawn.getChunk()).flatMap(ClaimedChunk::getCity).orElse(null)); + + Island island = city.islands().iterator().next(); + assertEquals(city, island.getCity()); + assertEquals(1, island.getSizeX()); + assertEquals(1, island.getSizeZ()); + assertEquals(1, island.getChunkCount()); + assertTrue(island.getId() > 0); + assertEquals(island, city.getIsland(island.getId())); + + assertEquals(test.mineCity.loadChunk(city.getSpawn().getChunk()), new ClaimedChunk( + island, city.getSpawn().getChunk())); + + assertThrown(()-> new City(test.mineCity, "Bad City", test.joserobjr, spawn, 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("reserved"); + + } + + @Test + public void testSetId() throws Exception + { + City badCity = new City(test.mineCity, "Bad City", test.joserobjr, new BlockPos(test.overWorld, 400,40, 65), 0); + assertThrown(()-> badCity.setId(-3)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("id = "+-3); + assertThrown(()-> badCity.setId(3)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("change"); + } + + @Test + public void testSetName() throws Exception + { + City first = new City(test.mineCity, "First City", test.joserobjr, new BlockPos(test.overWorld, -598, 44, -998), 0); + //noinspection SpellCheckingInspection + assertEquals("firstcity", first.getIdentityName()); + assertThrown(()-> new City(test.mineCity, "first_ciTy!", test.joserobjr, new BlockPos(test.overWorld, 98988,55,9874), 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("taken"); + + City second = new City(test.mineCity, "City 2", test.joserobjr, new BlockPos(test.overWorld, 788,68,9885), 0); + assertEquals("city2", second.getIdentityName()); + first.setName("City1"); + assertEquals("city1", first.getIdentityName()); + assertEquals("City1", first.getName()); + + assertThrown(() -> first.setName("CITY2")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("taken"); + first.setName("CITY 1"); + assertEquals("city1", first.getIdentityName()); + assertEquals("CITY 1", first.getName()); + + assertThrown(()-> first.setName("c1")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Bad"); + + assertThrown(()-> new City(test.mineCity, "c1", test.joserobjr, new BlockPos(test.overWorld, 5846487,4,448), 0)); + } + + @Test + public void testSetSpawn() throws Exception + { + BlockPos spawn = new BlockPos(test.overWorld, 54648, 32, 5855); + City spawnCity = new City(test.mineCity, "SpawnCity", test.joserobjr, spawn, 0); + + assertEquals(spawn, spawnCity.getSpawn()); + assertThrown(()-> spawnCity.setSpawn(spawn.getChunk().add(Direction.EAST).getMaxBlock())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("not part of the city"); + + BlockPos newSpawn = spawn.getChunk().getMaxBlock(); + spawnCity.setSpawn(newSpawn); + assertEquals(newSpawn, spawnCity.getSpawn()); + } + + @Test + public void testSetOwner() throws Exception + { + City owned = new City(test.mineCity, "Owned", test.joserobjr, new BlockPos(test.overWorld, 456484878, 32, 445454), 0); + + assertEquals(test.joserobjr, owned.owner()); + PlayerID newOwner = new PlayerID(UUID.randomUUID(), "Randy"); + owned.setOwner(newOwner); + assertEquals(newOwner, owned.owner()); + } + + @Test + public void testClaim() throws DataSourceException + { + BlockPos spawn = new BlockPos(test.overWorld, 250, 32, -200); + ChunkPos chunk = spawn.getChunk(); + City city = new City(test.mineCity, "City 2", test.joserobjr, spawn, 0); + + Island islandA = city.islands().iterator().next(); + assertEquals(islandA, city.claim(chunk.add(Direction.NORTH), false)); + assertEquals(2, islandA.getChunkCount()); + assertEquals(2, islandA.getSizeZ()); + assertEquals(1, islandA.getSizeX()); + assertEquals(2, city.getChunkCount()); + assertEquals(2, city.getSizeZ()); + assertEquals(1, city.getSizeX()); + + assertEquals(islandA, city.claim(chunk.add(Direction.WEST), false)); + assertEquals(3, islandA.getChunkCount()); + assertEquals(2, islandA.getSizeZ()); + assertEquals(2, islandA.getSizeX()); + assertEquals(3, city.getChunkCount()); + assertEquals(2, city.getSizeZ()); + assertEquals(2, city.getSizeX()); + + Island islandB = city.claim(chunk.add(Direction.EAST, 2), true); + assertNotEquals(islandA, islandB); + assertEquals(1, islandB.getChunkCount()); + assertEquals(1, islandB.getSizeZ()); + assertEquals(1, islandB.getSizeX()); + assertEquals(4, city.getChunkCount()); + assertEquals(3, city.getSizeZ()); + assertEquals(3, city.getSizeX()); + assertEquals(2, city.islands().size()); + + assertEquals(islandA, city.claim(chunk.add(Direction.EAST), false)); + assertEquals(5, islandA.getChunkCount()); + assertEquals(2, islandA.getSizeZ()); + assertEquals(4, islandA.getSizeX()); + assertEquals(5, city.getChunkCount()); + assertEquals(2, city.getSizeZ()); + assertEquals(4, city.getSizeX()); + assertEquals(1, city.islands().size()); + assertEquals(0, islandB.getChunkCount()); + assertEquals(0, islandB.getSizeX()); + assertEquals(0, islandB.getSizeX()); + } + + @Test + public void testClaimIsland() throws Exception + { + City farCity = new City(test.mineCity, "FarCity", test.joserobjr, new BlockPos(test.overWorld, 655,55,488), 0); + + BlockPos far = new BlockPos(test.overWorld, -4847,44,688); + assertThrown(()-> farCity.claim(far.getChunk(), false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("not touching"); + + farCity.claim(far.getChunk(), true); + assertEquals(2, farCity.islands().size()); + } } \ No newline at end of file diff --git a/gradlew b/gradlew old mode 100755 new mode 100644