diff --git a/Towny/src/main/java/com/palmergames/bukkit/config/ConfigNodes.java b/Towny/src/main/java/com/palmergames/bukkit/config/ConfigNodes.java index 00b03533d2..1de66bbf07 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/config/ConfigNodes.java +++ b/Towny/src/main/java/com/palmergames/bukkit/config/ConfigNodes.java @@ -2020,6 +2020,7 @@ public enum ConfigNodes { NOTIFICATION_PLOT_NOTFORSALE("notification.plot.notforsale", "&e[Not For Sale]"), NOTIFICATION_PLOT_TYPE("notification.plot.type", "&6[%s]"), NOTIFICATION_GROUP("notification.group", "&f[%s]"), + NOTIFICATION_DISTRICT("notification.district", "&2[%s]"), NOTIFICATION_TOWN_NAMES_ARE_VERBOSE( "notification.town_names_are_verbose", "true", diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/ChunkNotification.java b/Towny/src/main/java/com/palmergames/bukkit/towny/ChunkNotification.java index bdaf4e9c15..a73a2e2feb 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/ChunkNotification.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/ChunkNotification.java @@ -14,6 +14,7 @@ import com.palmergames.bukkit.towny.object.TownBlockType; import com.palmergames.bukkit.towny.object.TownyWorld; import com.palmergames.bukkit.towny.object.WorldCoord; +import com.palmergames.bukkit.towny.object.District; import com.palmergames.bukkit.towny.object.PlayerCache.TownBlockStatus; import com.palmergames.bukkit.towny.utils.CombatUtil; import com.palmergames.bukkit.towny.utils.PlayerCacheUtil; @@ -41,6 +42,7 @@ public class ChunkNotification { public static String notForSaleNotificationFormat = Colors.Yellow + "[Not For Sale]"; public static String plotTypeNotificationFormat = Colors.Gold + "[%s]"; public static String groupNotificationFormat = Colors.White + "[%s]"; + public static String districtNotificationFormat = Colors.DARK_GREEN + "[%s]"; /** * Called on Config load. @@ -64,16 +66,18 @@ public static void loadFormatStrings() { notForSaleNotificationFormat = Colors.translateColorCodes(TownySettings.getString(ConfigNodes.NOTIFICATION_PLOT_NOTFORSALE)); plotTypeNotificationFormat = Colors.translateColorCodes(TownySettings.getString(ConfigNodes.NOTIFICATION_PLOT_TYPE)); groupNotificationFormat = Colors.translateColorCodes(TownySettings.getString(ConfigNodes.NOTIFICATION_GROUP)); + districtNotificationFormat = Colors.translateColorCodes(TownySettings.getString(ConfigNodes.NOTIFICATION_DISTRICT)); } WorldCoord from, to; boolean fromWild = false, toWild = false, toForSale = false, fromForSale = false, - toHomeBlock = false, toOutpostBlock = false, toPlotGroupBlock = false; + toHomeBlock = false, toOutpostBlock = false, toPlotGroupBlock = false, toDistrictBlock = false; TownBlock fromTownBlock, toTownBlock = null; Town fromTown = null, toTown = null; Resident fromResident = null, toResident = null; TownBlockType fromPlotType = null, toPlotType = null; PlotGroup fromPlotGroup = null, toPlotGroup = null; + District fromDistrict = null, toDistrict = null; public ChunkNotification(WorldCoord from, WorldCoord to) { @@ -88,6 +92,9 @@ public ChunkNotification(WorldCoord from, WorldCoord to) { fromPlotGroup = fromTownBlock.getPlotObjectGroup(); fromForSale = fromPlotGroup.getPrice() != -1; } + if (fromTownBlock.hasDistrict()) { + fromDistrict = fromTownBlock.getDistrict(); + } fromTown = fromTownBlock.getTownOrNull(); fromResident = fromTownBlock.getResidentOrNull(); @@ -109,7 +116,10 @@ public ChunkNotification(WorldCoord from, WorldCoord to) { toPlotGroup = toTownBlock.getPlotObjectGroup(); toForSale = toPlotGroup.getPrice() != -1; } - + toDistrictBlock = toTownBlock.hasDistrict(); + if (toDistrictBlock) { + toDistrict = toTownBlock.getDistrict(); + } } else { toWild = true; } @@ -277,6 +287,10 @@ public List getPlotNotificationContent() { if (output != null && output.length() > 0) out.add(output); + output = getDistrictNotification(); + if (output != null && output.length() > 0) + out.add(output); + return out; } @@ -323,6 +337,12 @@ public String getGroupNotification() { return null; } + public String getDistrictNotification() { + if (toDistrictBlock && (fromDistrict != toDistrict)) + return String.format(districtNotificationFormat, StringMgmt.remUnderscore(toDistrict.getName())); + return null; + } + public String getPlotTypeNotification() { if (toPlotType != null && !toPlotType.equals(fromPlotType) && !TownBlockType.RESIDENTIAL.equals(toPlotType)) diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/TownyAsciiMap.java b/Towny/src/main/java/com/palmergames/bukkit/towny/TownyAsciiMap.java index 9dd36fc7f3..72646e86cb 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/TownyAsciiMap.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/TownyAsciiMap.java @@ -170,6 +170,7 @@ else if (townblock.isOutpost()) Component forSaleComponent = Component.empty(); Component claimedAtComponent = Component.empty(); Component groupComponent = Component.empty(); + Component districtComponent = Component.empty(); if (TownyEconomyHandler.isActive()) { double cost = townblock.hasPlotObjectGroup() @@ -192,6 +193,13 @@ else if (townblock.isOutpost()) .append(translator.component("map_hover_plots", townblock.getPlotObjectGroup().getTownBlocks().size()).color(NamedTextColor.GREEN) .append(Component.newline())))); + if (townblock.hasDistrict()) + districtComponent = translator.component("map_hover_district").color(NamedTextColor.DARK_GREEN) + .append(Component.text(townblock.getDistrict().getFormattedName(), NamedTextColor.GREEN) + .append(translator.component("map_hover_plot_group_size").color(NamedTextColor.DARK_GREEN) + .append(translator.component("map_hover_plots", townblock.getDistrict().getTownBlocks().size()).color(NamedTextColor.GREEN) + .append(Component.newline())))); + if (townblock.hasResident()) residentComponent = Component.text(" (" + townblock.getResidentOrNull().getName() + ")", NamedTextColor.GREEN); @@ -209,6 +217,7 @@ else if (townblock.isOutpost()) Component hoverComponent = townComponent .append(plotTypeComponent) + .append(districtComponent) .append(groupComponent) .append(forSaleComponent) .append(claimedAtComponent) diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/TownyFormatter.java b/Towny/src/main/java/com/palmergames/bukkit/towny/TownyFormatter.java index 476cad27b1..68d004cdb9 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/TownyFormatter.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/TownyFormatter.java @@ -110,6 +110,9 @@ public static StatusScreen getStatus(TownBlock townBlock, Player player) { screen.addComponentOf("firespread", colourKeyValue(translator.of("firespread"), ((world.isForceFire() || townBlock.getPermissions().fire) ? translator.of("status_on"):translator.of("status_off")))); screen.addComponentOf("mobspawns", colourKeyValue(translator.of("mobspawns"), ((world.isForceTownMobs() || townBlock.getPermissions().mobs || town.isAdminEnabledMobs()) ? translator.of("status_on"): translator.of("status_off")))); + if (townBlock.hasDistrict()) + screen.addComponentOf("district", colourKey(translator.of("status_district_name_and_size", townBlock.getDistrict().getName(), townBlock.getDistrict().getTownBlocks().size()))); + if (townBlock.hasPlotObjectGroup()) screen.addComponentOf("plotgroup", colourKey(translator.of("status_plot_group_name_and_size", townBlock.getPlotObjectGroup().getName(), townBlock.getPlotObjectGroup().getTownBlocks().size()))); diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/TownyUniverse.java b/Towny/src/main/java/com/palmergames/bukkit/towny/TownyUniverse.java index 333e32bab5..f6139a403e 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/TownyUniverse.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/TownyUniverse.java @@ -10,6 +10,7 @@ import com.palmergames.bukkit.towny.exceptions.KeyAlreadyRegisteredException; import com.palmergames.bukkit.towny.exceptions.NotRegisteredException; import com.palmergames.bukkit.towny.exceptions.initialization.TownyInitException; +import com.palmergames.bukkit.towny.object.District; import com.palmergames.bukkit.towny.object.Nation; import com.palmergames.bukkit.towny.object.PlotGroup; import com.palmergames.bukkit.towny.object.Position; @@ -89,6 +90,7 @@ public class TownyUniverse { private final Map jailUUIDMap = new ConcurrentHashMap<>(); private final Map replacementNamesMap = new ConcurrentHashMap<>(); private final Map plotGroupUUIDMap = new ConcurrentHashMap<>(); + private final Map districtUUIDMap = new ConcurrentHashMap<>(); private final Map wildernessMapDataMap = new ConcurrentHashMap(); private final String rootFolder; @@ -905,6 +907,57 @@ public PlotGroup getGroup(UUID groupID) { return plotGroupUUIDMap.get(groupID); } + /* + * District Stuff. + */ + + /** + * Used in loading only. + * @param uuid UUID to assign to the District. + */ + public void newDistrictInternal(UUID uuid) { + District district = new District(uuid, null, null); + registerDistrict(district); + } + + + public void registerDistrict(District district) { + districtUUIDMap.put(district.getUUID(), district); + } + + public void unregisterDistrict(UUID uuid) { + District district = districtUUIDMap.get(uuid); + if (district == null) + return; + district.getTown().removeDistrict(district); + districtUUIDMap.remove(uuid); + } + + /** + * Get all the districts from all towns + * Returns a collection that does not reflect any district additions/removals + * + * @return collection of District + */ + public Collection getDistricts() { + return new ArrayList<>(districtUUIDMap.values()); + } + + public Set getDistrictUUIDs() { + return districtUUIDMap.keySet(); + } + + /** + * Gets the district from the town name and the district UUID + * + * @param districtID UUID of the district + * @return District if found, null if none found. + */ + @Nullable + public District getDistrict(UUID districtID) { + return districtUUIDMap.get(districtID); + } + /* * Metadata Stuff */ diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/command/HelpMenu.java b/Towny/src/main/java/com/palmergames/bukkit/towny/command/HelpMenu.java index fd4047c150..96c5d16fea 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/command/HelpMenu.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/command/HelpMenu.java @@ -827,6 +827,17 @@ protected MenuBuilder load() { .add(Translatable.of("msg_nfs_abr")); } }, + + PLOT_DISTRICT_HELP { + @Override + protected MenuBuilder load() { + return new MenuBuilder("plot district") + .add("add|new|create [name]", Translatable.of("plot_district_help_0")) + .add("remove", Translatable.of("plot_district_help_1")) + .add("delete", Translatable.of("plot_district_help_2")) + .add("rename [newName]", Translatable.of("plot_district_help_3")); + } + }, PLOT_GROUP_HELP { @Override diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/command/PlotCommand.java b/Towny/src/main/java/com/palmergames/bukkit/towny/command/PlotCommand.java index 2096221ecc..5f2b535d55 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/command/PlotCommand.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/command/PlotCommand.java @@ -21,6 +21,9 @@ import com.palmergames.bukkit.towny.event.plot.PlotSetForSaleEvent; import com.palmergames.bukkit.towny.event.plot.PlotTrustAddEvent; import com.palmergames.bukkit.towny.event.plot.PlotTrustRemoveEvent; +import com.palmergames.bukkit.towny.event.plot.district.DistrictAddEvent; +import com.palmergames.bukkit.towny.event.plot.district.DistrictCreatedEvent; +import com.palmergames.bukkit.towny.event.plot.district.DistrictDeletedEvent; import com.palmergames.bukkit.towny.event.plot.group.PlotGroupAddEvent; import com.palmergames.bukkit.towny.event.plot.group.PlotGroupCreatedEvent; import com.palmergames.bukkit.towny.event.plot.group.PlotGroupDeletedEvent; @@ -35,6 +38,7 @@ import com.palmergames.bukkit.towny.exceptions.TownyException; import com.palmergames.bukkit.towny.huds.HUDManager; import com.palmergames.bukkit.towny.object.Coord; +import com.palmergames.bukkit.towny.object.District; import com.palmergames.bukkit.towny.object.PermissionData; import com.palmergames.bukkit.towny.object.PlotGroup; import com.palmergames.bukkit.towny.object.Resident; @@ -61,6 +65,7 @@ import com.palmergames.bukkit.towny.utils.NameUtil; import com.palmergames.bukkit.towny.utils.OutpostUtil; import com.palmergames.bukkit.towny.utils.PermissionGUIUtil; +import com.palmergames.bukkit.towny.utils.ProximityUtil; import com.palmergames.bukkit.util.BukkitTools; import com.palmergames.bukkit.util.ChatTools; import com.palmergames.bukkit.util.Colors; @@ -105,6 +110,7 @@ public class PlotCommand extends BaseCommand implements CommandExecutor { "toggle", "clear", "group", + "district", "jailcell", "trust" ); @@ -123,6 +129,13 @@ public class PlotCommand extends BaseCommand implements CommandExecutor { "trust" ); + private static final List districtTabCompletes = Arrays.asList( + "add", + "delete", + "remove", + "rename" + ); + private static final List plotSetTabCompletes = Arrays.asList( "reset", "shop", @@ -240,6 +253,11 @@ else if (args.length == 3) default: return Collections.emptyList(); } + case "district": + if (args.length == 2) + return NameUtil.filterByStart(districtTabCompletes, args[1]); + if (args.length < 2) + break; case "group": if (args.length == 2) return NameUtil.filterByStart(plotGroupTabCompletes, args[1]); @@ -312,6 +330,7 @@ public void parsePlotCommand(Player player, String[] split) throws TownyExceptio case "evict" -> parsePlotEvict(resident, townBlock); case "fs", "forsale" -> parsePlotForSale(player, StringMgmt.remFirstArg(split), resident, townBlock); case "group" -> parsePlotGroup(StringMgmt.remFirstArg(split), resident, townBlock, player); + case "district" -> parseDistrict(StringMgmt.remFirstArg(split), resident, townBlock, player); case "info" -> sendPlotInfo(player, StringMgmt.remFirstArg(split)); case "jailcell" -> parsePlotJailCell(player, resident, townBlock, StringMgmt.remFirstArg(split)); case "nfs", "notforsale" -> parsePlotNotForSale(player, StringMgmt.remFirstArg(split), resident, townBlock); @@ -1255,6 +1274,183 @@ private void toggleTest(Player player, TownBlock townBlock, String split) throws } } + private void parseDistrict(String[] split, Resident resident, TownBlock townBlock, Player player) throws TownyException { + + Town town = townBlock.getTownOrNull(); + if (town == null) + throw new TownyException(Translatable.of("msg_not_claimed_1")); + + if (!town.hasResident(player)) + throw new TownyException(Translatable.of("msg_err_not_part_town")); + + try { + checkPermOrThrow(player, PermissionNodes.TOWNY_COMMAND_PLOT_ASMAYOR.getNode()); + } catch (NoPermissionException e) { + throw new TownyException(Translatable.of("msg_not_mayor_ass")); + } + + if (split.length <= 0 || split[0].equalsIgnoreCase("?")) { + HelpMenu.PLOT_DISTRICT_HELP.send(player); + if (townBlock.hasDistrict()) + TownyMessaging.sendMsg(player, Translatable.of("status_district_name_and_size", townBlock.getDistrict().getName(), townBlock.getDistrict().getTownBlocks().size())); + return; + } + + switch (split[0].toLowerCase(Locale.ROOT)) { + case "add", "new", "create" -> parseDistrictAdd(split, townBlock, player, town); + case "delete" -> parseDistrictDelete(townBlock, player, town); + case "remove" -> parseDistrictRemove(townBlock, player, town); + case "rename" -> parseDistrictRename(split, townBlock, player); + default -> { + HelpMenu.PLOT_DISTRICT_HELP.send(player); + if (townBlock.hasDistrict()) + TownyMessaging.sendMsg(player, Translatable.of("status_district_name_and_size", townBlock.getDistrict().getName(), townBlock.getDistrict().getTownBlocks().size())); + } + } + } + + public void parseDistrictAdd(String[] split, TownBlock townBlock, Player player, Town town) throws TownyException { + checkPermOrThrow(player, PermissionNodes.TOWNY_COMMAND_PLOT_DISTRICT_ADD.getNode()); + + Resident resident = getResidentOrThrow(player); + + if (split.length != 2 && !resident.hasDistrictName()) + throw new TownyException(Translatable.of("msg_err_district_name_required")); + + String districtName = split.length == 2 + ? NameValidation.checkAndFilterDistrictNameOrThrow(split[1]) + : resident.hasDistrictName() + ? resident.getDistrictName() + : null; + + if (townBlock.hasDistrict()) { + // Already has a District and it is the same name being used to re-add. + if (townBlock.getDistrict().getName().equalsIgnoreCase(districtName)) + throw new TownyException(Translatable.of("msg_err_this_plot_is_already_part_of_the_district_x", districtName)); + + District oldDistrict = townBlock.getDistrict(); + ProximityUtil.testAdjacentRemoveDistrictRulesOrThrow(townBlock.getWorldCoord(), town, oldDistrict, 1); + + final String name = districtName; + // Already has a District, ask if they want to transfer from one district to another. + Confirmation.runOnAccept( ()-> { + + oldDistrict.removeTownBlock(townBlock); + if (oldDistrict.getTownBlocks().isEmpty() && !BukkitTools.isEventCancelled(new DistrictDeletedEvent(oldDistrict, player, DistrictDeletedEvent.Cause.NO_TOWNBLOCKS))) { + String oldName = oldDistrict.getName(); + town.removeDistrict(oldDistrict); + TownyUniverse.getInstance().getDataSource().removeDistrict(oldDistrict); + TownyMessaging.sendMsg(player, Translatable.of("msg_district_deleted", oldName)); + } else + oldDistrict.save(); + + try { + createOrAddOnToDistrict(townBlock, town, player, name); + resident.setDistrictName(name); + TownyMessaging.sendMsg(player, Translatable.of("msg_townblock_transferred_from_x_to_x_district", oldDistrict.getName(), townBlock.getDistrict().getName())); + } catch (TownyException e) { + TownyMessaging.sendErrorMsg(player, e.getMessage(player)); + } + }) + .setTitle(Translatable.of("msg_district_already_exists_did_you_want_to_transfer", townBlock.getDistrict().getName(), split[1])) + .sendTo(player); + } else { + // Create a brand new district. + createOrAddOnToDistrict(townBlock, town, player, districtName); + resident.setDistrictName(districtName); + TownyMessaging.sendMsg(player, Translatable.of("msg_plot_was_put_into_district_x", townBlock.getX(), townBlock.getZ(), townBlock.getDistrict().getName())); + } + } + + private void createOrAddOnToDistrict(TownBlock townBlock, Town town, Player player, String districtName) throws TownyException { + District newDistrict; + + // Don't add the district to the town data if it's already there. + if (town.hasDistrictName(districtName)) { + newDistrict = town.getDistrictFromName(districtName); + + ProximityUtil.testAdjacentAddDistrictRulesOrThrow(townBlock.getWorldCoord(), town, newDistrict, 1); + + BukkitTools.ifCancelledThenThrow(new DistrictAddEvent(newDistrict, townBlock, player)); + + } else { + // This is a brand new District, register it. + newDistrict = new District(UUID.randomUUID(), districtName, town); + + BukkitTools.ifCancelledThenThrow(new DistrictCreatedEvent(newDistrict, townBlock, player)); + + TownyUniverse.getInstance().registerDistrict(newDistrict); + } + + // Add district to townblock, this also adds the townblock to the district. + townBlock.setDistrict(newDistrict); + + // Add the district to the town set. + town.addDistrict(newDistrict); + + // Save changes. + newDistrict.save(); + townBlock.save(); + } + + public void parseDistrictDelete(TownBlock townBlock, Player player, Town town) throws TownyException { + checkPermOrThrow(player, PermissionNodes.TOWNY_COMMAND_PLOT_DISTRICT_DELETE.getNode()); + + District district = catchMissingDistrict(townBlock); + + Confirmation.runOnAccept(()-> { + String name = district.getName(); + if (!BukkitTools.isEventCancelled(new DistrictDeletedEvent(district, player, DistrictDeletedEvent.Cause.DELETED))) { + town.removeDistrict(district); + TownyUniverse.getInstance().getDataSource().removeDistrict(district); + TownyMessaging.sendMsg(player, Translatable.of("msg_district_deleted", name)); + } + }).sendTo(player); + } + + public void parseDistrictRemove(TownBlock townBlock, Player player, Town town) throws TownyException { + checkPermOrThrow(player, PermissionNodes.TOWNY_COMMAND_PLOT_DISTRICT_REMOVE.getNode()); + + District district = catchMissingDistrict(townBlock); + String name = district.getName(); + + try { + ProximityUtil.testAdjacentRemoveDistrictRulesOrThrow(townBlock.getWorldCoord(), town, district, 1); + } catch (TownyException e) { + throw new TownyException(Translatable.of("msg_err_cannot_remove_from_district_not_enough_adjacent_claims", name)); + } + + // Remove the plot from the district. + district.removeTownBlock(townBlock); + + // Detach district from townblock. + townBlock.removeDistrict(); + + // Save + townBlock.save(); + TownyMessaging.sendMsg(player, Translatable.of("msg_plot_was_removed_from_district_x", townBlock.getX(), townBlock.getZ(), name)); + + if (district.getTownBlocks().isEmpty() && !BukkitTools.isEventCancelled(new DistrictDeletedEvent(district, player, DistrictDeletedEvent.Cause.NO_TOWNBLOCKS))) { + town.removeDistrict(district); + TownyUniverse.getInstance().getDataSource().removeDistrict(district); + TownyMessaging.sendMsg(player, Translatable.of("msg_district_empty_deleted", name)); + } + } + + public void parseDistrictRename(String[] split, TownBlock townBlock, Player player) throws TownyException, AlreadyRegisteredException { + checkPermOrThrow(player, PermissionNodes.TOWNY_COMMAND_PLOT_DISTRICT_RENAME.getNode()); + + if (split.length == 1) + throw new TownyException(Translatable.of("msg_err_rename_district_name_required")); + + District district = catchMissingDistrict(townBlock); + String newName= split[1]; + String oldName = district.getName(); + // Change name; + TownyUniverse.getInstance().getDataSource().renameDistrict(district, newName); + TownyMessaging.sendMsg(player, Translatable.of("msg_district_renamed_from_x_to_y", oldName, newName)); + } + private void parsePlotGroup(String[] split, Resident resident, TownBlock townBlock, Player player) throws TownyException { Town town = townBlock.getTownOrNull(); @@ -2072,4 +2268,12 @@ private PlotGroup catchMissingPlotGroup(TownBlock townBlock) throws TownyExcepti return townBlock.getPlotObjectGroup(); } + + private District catchMissingDistrict(TownBlock townBlock) throws TownyException { + // Make sure that the player is in a plotgroup. + if (!townBlock.hasDistrict()) + throw new TownyException(Translatable.of("msg_err_plot_not_associated_with_a_district")); + + return townBlock.getDistrict(); + } } diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/db/SQLSchema.java b/Towny/src/main/java/com/palmergames/bukkit/towny/db/SQLSchema.java index a625821048..848a8f17d2 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/db/SQLSchema.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/db/SQLSchema.java @@ -48,6 +48,9 @@ public static void initTables(Connection cntx) { initTable(cntx, TownyDBTableType.PLOTGROUP); updateTable(cntx, TownyDBTableType.PLOTGROUP, getPlotGroupColumns()); + initTable(cntx, TownyDBTableType.DISTRICT); + updateTable(cntx, TownyDBTableType.DISTRICT, getDistrictColumns()); + initTable(cntx, TownyDBTableType.JAIL); updateTable(cntx, TownyDBTableType.JAIL, getJailsColumns()); @@ -160,6 +163,14 @@ private static List getPlotGroupColumns() { return columns; } + private static List getDistrictColumns() { + List columns = new ArrayList<>(); + columns.add("`districtName` mediumtext NOT NULL"); + columns.add("`town` VARCHAR(36) NOT NULL"); + columns.add("`metadata` text DEFAULT NULL"); + return columns; + } + private static List getResidentColumns(){ List columns = new ArrayList<>(); columns.add("`town` mediumtext"); diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyDataSource.java b/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyDataSource.java index 3a279f9164..47fb4c61b3 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyDataSource.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyDataSource.java @@ -8,6 +8,7 @@ import com.palmergames.bukkit.towny.event.DeleteNationEvent; import com.palmergames.bukkit.towny.exceptions.AlreadyRegisteredException; import com.palmergames.bukkit.towny.exceptions.NotRegisteredException; +import com.palmergames.bukkit.towny.object.District; import com.palmergames.bukkit.towny.object.Nation; import com.palmergames.bukkit.towny.object.PlotGroup; import com.palmergames.bukkit.towny.object.Resident; @@ -54,12 +55,12 @@ public abstract class TownyDataSource { public boolean loadAll() { - return loadWorldList() && loadNationList() && loadTownList() && loadPlotGroupList() && loadJailList() && loadResidentList() && loadTownBlockList() && loadWorlds() && loadResidents() && loadTowns() && loadNations() && loadTownBlocks() && loadPlotGroups() && loadJails() && loadRegenList() && loadCooldowns(); + return loadWorldList() && loadNationList() && loadTownList() && loadPlotGroupList() && loadDistrictList() && loadJailList() && loadResidentList() && loadTownBlockList() && loadWorlds() && loadResidents() && loadTowns() && loadNations() && loadTownBlocks() && loadPlotGroups() && loadDistricts() && loadJails() && loadRegenList() && loadCooldowns(); } public boolean saveAll() { - return saveWorlds() && saveNations() && saveTowns() && saveResidents() && savePlotGroups() && saveTownBlocks() && saveJails() && saveRegenList() && saveCooldowns(); + return saveWorlds() && saveNations() && saveTowns() && saveResidents() && savePlotGroups() && saveDistricts() && saveTownBlocks() && saveJails() && saveRegenList() && saveCooldowns(); } public boolean saveAllWorlds() { @@ -104,6 +105,10 @@ public boolean saveQueues() { abstract public boolean loadPlotGroup(PlotGroup group); + abstract public boolean loadDistrictList(); + + abstract public boolean loadDistrict(District district); + abstract public boolean saveRegenList(); abstract public boolean saveResident(Resident resident); @@ -114,6 +119,8 @@ public boolean saveQueues() { abstract public boolean savePlotGroup(PlotGroup group); + abstract public boolean saveDistrict(District district); + abstract public boolean saveJail(Jail jail); abstract public boolean saveNation(Nation nation); @@ -148,6 +155,8 @@ public boolean saveQueues() { abstract public void deletePlotGroup(PlotGroup group); + abstract public void deleteDistrict(District district); + abstract public void deleteJail(Jail jail); abstract public CompletableFuture> getHibernatedResidentRegistered(UUID uuid); @@ -226,6 +235,17 @@ public boolean loadPlotGroups() { return true; } + public boolean loadDistricts() { + TownyMessaging.sendDebugMsg("Loading Districts"); + for (District district : universe.getDistricts()) { + if (!loadDistrict(district)) { + plugin.getLogger().severe("Loading Error: Could not read District data: '" + district.getUUID() + "'."); + return false; + } + } + return true; + } + abstract public boolean loadCooldowns(); /* @@ -253,6 +273,19 @@ public boolean savePlotGroups() { return true; } + public boolean saveDistricts() { + TownyMessaging.sendDebugMsg("Saving Districts"); + for (District district : universe.getDistricts()) + /* + * Only save districts which actually have townblocks associated with them. + */ + if (district.hasTownBlocks()) + saveDistrict(district); + else + deleteDistrict(district); + return true; + } + public boolean saveJails() { TownyMessaging.sendDebugMsg("Saving Jails"); for (Jail jail : universe.getJails()) @@ -334,6 +367,8 @@ public boolean removeTown(@NotNull Town town, @NotNull DeleteTownEvent.Cause cau abstract public void removeJail(Jail jail); abstract public void removePlotGroup(PlotGroup group); + + abstract public void removeDistrict(District district); abstract public void renameTown(Town town, String newName) throws AlreadyRegisteredException, NotRegisteredException; @@ -347,6 +382,8 @@ public boolean removeTown(@NotNull Town town, @NotNull DeleteTownEvent.Cause cau abstract public void renameGroup(PlotGroup group, String newName) throws AlreadyRegisteredException; + abstract public void renameDistrict(District district, String newName) throws AlreadyRegisteredException; + /** * @deprecated since 0.100.2.9 use {@link #removeTown(Town, com.palmergames.bukkit.towny.event.DeleteTownEvent.Cause)} instead. * @param town diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyDatabaseHandler.java b/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyDatabaseHandler.java index 5ed7bac07c..8bb21720e5 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyDatabaseHandler.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyDatabaseHandler.java @@ -24,6 +24,7 @@ import com.palmergames.bukkit.towny.exceptions.TownyException; import com.palmergames.bukkit.towny.invites.Invite; import com.palmergames.bukkit.towny.invites.InviteHandler; +import com.palmergames.bukkit.towny.object.District; import com.palmergames.bukkit.towny.object.Nation; import com.palmergames.bukkit.towny.object.PlotGroup; import com.palmergames.bukkit.towny.object.Resident; @@ -585,6 +586,12 @@ public void removePlotGroup(PlotGroup group) { deletePlotGroup(group); } + @Override + public void removeDistrict(District district) { + universe.unregisterDistrict(district.getUUID()); + deleteDistrict(district); + } + /* * Rename Object Methods */ @@ -783,6 +790,15 @@ public void renameGroup(PlotGroup group, String newName) throws AlreadyRegistere savePlotGroup(group); } + @Override + public void renameDistrict(District district, String newName) throws AlreadyRegisteredException { + // Create new one + district.setName(newName); + + // Save + saveDistrict(district); + } + @Override public void renamePlayer(Resident resident, String newName) throws AlreadyRegisteredException, NotRegisteredException { diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyFlatFileSource.java b/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyFlatFileSource.java index 2af736d86f..866460f90a 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyFlatFileSource.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyFlatFileSource.java @@ -18,6 +18,7 @@ import com.palmergames.bukkit.towny.exceptions.InvalidNameException; import com.palmergames.bukkit.towny.exceptions.NotRegisteredException; import com.palmergames.bukkit.towny.exceptions.TownyException; +import com.palmergames.bukkit.towny.object.District; import com.palmergames.bukkit.towny.object.Nation; import com.palmergames.bukkit.towny.object.PermissionData; import com.palmergames.bukkit.towny.object.PlotGroup; @@ -83,6 +84,8 @@ public TownyFlatFileSource(Towny plugin, TownyUniverse universe) { dataFolderPath + File.separator + "townblocks", dataFolderPath + File.separator + "plotgroups", dataFolderPath + File.separator + "plotgroups" + File.separator + "deleted", + dataFolderPath + File.separator + "districts", + dataFolderPath + File.separator + "districts" + File.separator + "deleted", dataFolderPath + File.separator + "jails", dataFolderPath + File.separator + "jails" + File.separator + "deleted" )) { @@ -137,6 +140,10 @@ public String getPlotGroupFilename(PlotGroup group) { return dataFolderPath + File.separator + "plotgroups" + File.separator + group.getUUID() + ".data"; } + public String getDistrictFilename(District district) { + return dataFolderPath + File.separator + "districts" + File.separator + district.getUUID() + ".data"; + } + public String getJailFilename(Jail jail) { return dataFolderPath + File.separator + "jails" + File.separator + jail.getUUID() + ".txt"; } @@ -217,6 +224,20 @@ public boolean loadPlotGroupList() { return true; } + + @Override + public boolean loadDistrictList() { + TownyMessaging.sendDebugMsg(Translation.of("flatfile_dbg_loading_district_list")); + File[] districtFiles = receiveObjectFiles("districts", ".data"); + + if (districtFiles == null) + return true; + + for (File districtFile : districtFiles) + universe.newDistrictInternal(UUID.fromString(districtFile.getName().replace(".data", ""))); + + return true; + } @Override public boolean loadResidentList() { @@ -1647,6 +1668,54 @@ public boolean loadPlotGroup(PlotGroup group) { return true; } + + public boolean loadDistrict(District district) { + String line = ""; + String path = getDistrictFilename(district); + + File districtFile = new File(path); + if (districtFile.exists() && districtFile.isFile()) { + try { + HashMap keys = FileMgmt.loadFileIntoHashMap(districtFile); + + line = keys.get("districtName"); + if (line != null) + district.setName(line.trim()); + + line = keys.get("town"); + if (line != null && !line.isEmpty()) { + UUID uuid = UUID.fromString(line.trim()); + if (uuid == null) { + TownyMessaging.sendDebugMsg(Translation.of("flatfile_dbg_missing_file_delete_district_entry", path)); + deleteDistrict(district); + return true; + } + Town town = universe.getTown(uuid); + if (town != null) { + district.setTown(town); + } else { + TownyMessaging.sendDebugMsg(Translation.of("flatfile_dbg_district_file_missing_town_delete", path)); + deleteDistrict(district); + TownyMessaging.sendDebugMsg(Translation.of("flatfile_dbg_missing_file_delete_district_entry", path)); + return true; + } + } else { + TownyMessaging.sendErrorMsg(Translation.of("flatfile_err_could_not_add_to_town")); + deleteDistrict(district); + } + + line = keys.get("metadata"); + if (line != null) + MetadataLoader.getInstance().deserializeMetadata(district, line.trim()); + + } catch (Exception e) { + TownyMessaging.sendErrorMsg(Translation.of("flatfile_err_exception_reading_district_file_at_line", path, line)); + return false; + } + } + + return true; + } @Override public boolean loadTownBlocks() { @@ -1795,6 +1864,21 @@ else if (universe.getReplacementNameMap().containsKey(line.trim())) } } + line = keys.get("districtID"); + UUID districtID = null; + if (line != null && !line.isEmpty()) { + districtID = UUID.fromString(line.trim()); + } + + if (districtID != null) { + District district = universe.getDistrict(districtID); + if (district != null) { + townBlock.setDistrict(district); + } else { + townBlock.removeDistrict(); + } + } + line = keys.get("trustedResidents"); if (line != null && !line.isEmpty() && townBlock.getTrustedResidents().isEmpty()) { for (Resident resident : TownyAPI.getInstance().getResidents(toUUIDArray(line.split(",")))) @@ -2129,6 +2213,24 @@ public boolean savePlotGroup(PlotGroup group) { return true; } + @Override + public boolean saveDistrict(District district) { + List list = new ArrayList<>(); + + try { + list.add("districtName=" + district.getName()); + list.add("town=" + district.getTown().getUUID().toString()); + list.add("metadata=" + serializeMetadata(district)); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, "An exception occurred while saving district " + Optional.ofNullable(district).map(g -> g.getUUID().toString()).orElse("null") + ": ", e); + } + + // Save file + this.queryQueue.add(new FlatFileSaveTask(list, getDistrictFilename(district))); + + return true; + } + @Override public boolean saveNation(Nation nation) { @@ -2415,7 +2517,15 @@ public boolean saveTownBlock(TownBlock townBlock) { } list.add("groupID=" + groupID); + + // District ID + StringBuilder districtID = new StringBuilder(); + if (townBlock.hasDistrict()) { + districtID.append(townBlock.getDistrict().getUUID()); + } + list.add("districtID=" + districtID); + list.add("trustedResidents=" + StringMgmt.join(toUUIDList(townBlock.getTrustedResidents()), ",")); Map stringMap = new HashMap<>(); @@ -2505,6 +2615,12 @@ public void deletePlotGroup(PlotGroup group) { queryQueue.add(new DeleteFileTask(file, false)); } + @Override + public void deleteDistrict(District district) { + File file = new File(getDistrictFilename(district)); + queryQueue.add(new DeleteFileTask(file, false)); + } + @Override public void deleteJail(Jail jail) { File file = new File(getJailFilename(jail)); diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownySQLSource.java b/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownySQLSource.java index 3d7109dd3d..e13ed55290 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownySQLSource.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownySQLSource.java @@ -18,6 +18,7 @@ import com.palmergames.bukkit.towny.exceptions.NotRegisteredException; import com.palmergames.bukkit.towny.exceptions.TownyException; import com.palmergames.bukkit.towny.exceptions.initialization.TownyInitException; +import com.palmergames.bukkit.towny.object.District; import com.palmergames.bukkit.towny.object.Nation; import com.palmergames.bukkit.towny.object.PermissionData; import com.palmergames.bukkit.towny.object.PlotGroup; @@ -422,6 +423,7 @@ public boolean cleanup() { public enum TownyDBTableType { JAIL("JAILS", "SELECT uuid FROM ", "uuid"), PLOTGROUP("PLOTGROUPS", "SELECT groupID FROM ", "groupID"), + DISTRICT("DISTRICTS", "SELECT districtID FROM ", "districtID"), RESIDENT("RESIDENTS", "SELECT name FROM ", "name"), HIBERNATED_RESIDENT("HIBERNATEDRESIDENTS", "", "uuid"), TOWN("TOWNS", "SELECT name FROM ", "name"), @@ -628,6 +630,31 @@ public boolean loadPlotGroupList() { return false; } + @Override + public boolean loadDistrictList() { + TownyMessaging.sendDebugMsg("Loading District List"); + + try (Connection connection = getConnection(); + Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery("SELECT districtID FROM " + tb_prefix + "DISTRICTS")) { + + while (rs.next()) { + try { + universe.newDistrictInternal(UUID.fromString(rs.getString("districtID"))); + } catch (IllegalArgumentException e) { + plugin.getLogger().log(Level.WARNING, "ID for district is not a valid uuid, skipped loading district {}", rs.getString("districtID")); + } + } + + return true; + + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "An exception occurred while loading district list", e); + } + + return false; + } + public boolean loadJailList() { TownyMessaging.sendDebugMsg("Loading Jail List"); @@ -1896,6 +1923,22 @@ public boolean loadTownBlocks() { } catch (SQLException ignored) { } + try { + line = rs.getString("groupID"); + if (line != null && !line.isEmpty()) { + try { + UUID districtID = UUID.fromString(line.trim()); + District district = universe.getDistrict(districtID); + if (district != null) { + townBlock.setDistrict(district); + } + } catch (Exception ignored) { + } + + } + } catch (SQLException ignored) { + } + line = rs.getString("trustedResidents"); if (line != null && !line.isEmpty() && townBlock.getTrustedResidents().isEmpty()) { String search = (line.contains("#")) ? "#" : ","; @@ -1959,6 +2002,27 @@ public boolean loadPlotGroups() { return true; } + @Override + public boolean loadDistricts() { + TownyMessaging.sendDebugMsg("Loading districts."); + + try (Connection connection = getConnection(); + Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery("SELECT * FROM " + tb_prefix + "DISTRICTS ")) { + while (rs.next()) { + if (!loadDistrict(rs)) { + plugin.getLogger().warning("Loading Error: Could not read district data properly."); + return false; + } + } + } catch (SQLException e) { + TownyMessaging.sendErrorMsg("SQL: Load District sql Error - " + e.getMessage()); + return false; + } + + return true; + } + @Override public boolean loadCooldowns() { try (Connection connection = getConnection(); @@ -2043,6 +2107,59 @@ public boolean loadPlotGroup(PlotGroup group) { return true; } + private boolean loadDistrict(ResultSet rs) { + String line = null; + String uuidString = null; + + try { + PlotGroup district = universe.getGroup(UUID.fromString(rs.getString("districtID"))); + if (district == null) { + TownyMessaging.sendErrorMsg("SQL: A district was not registered properly on load!"); + return true; + } + uuidString = district.getUUID().toString(); + + line = rs.getString("districtName"); + if (line != null) + try { + district.setName(line.trim()); + } catch (Exception ignored) { + } + + line = rs.getString("town"); + if (line != null) { + UUID uuid = UUID.fromString(line.trim()); + if (uuid == null) { + deletePlotGroup(district); + return true; + } + Town town = universe.getTown(uuid); + if (town != null) { + district.setTown(town); + } else { + deletePlotGroup(district); + return true; + } + } + + line = rs.getString("metadata"); + if (line != null) { + MetadataLoader.getInstance().deserializeMetadata(district, line); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "Loading Error: Exception while reading district: " + uuidString + + " at line: " + line + " in the sql database", e); + return false; + } + return true; + } + + @Override + public boolean loadDistrict(District district) { + // Unused in SQL. + return true; + } + @Override public boolean loadJails() { TownyMessaging.sendDebugMsg("Loading Jails"); @@ -2314,6 +2431,24 @@ public synchronized boolean savePlotGroup(PlotGroup group) { return false; } + @Override + public boolean saveDistrict(District district) { + TownyMessaging.sendDebugMsg("Saving district " + district.getName()); + try { + HashMap pltgrp_hm = new HashMap<>(); + pltgrp_hm.put("districtID", district.getUUID().toString()); + pltgrp_hm.put("districtName", district.getName()); + pltgrp_hm.put("town", district.getTown().getUUID().toString()); + pltgrp_hm.put("metadata", serializeMetadata(district)); + + updateDB("DISTRICTS", pltgrp_hm, Collections.singletonList("districtID")); + + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, "SQL: Save Districts unknown error", e); + } + return false; + } + @Override public synchronized boolean saveNation(Nation nation) { @@ -2514,6 +2649,10 @@ public synchronized boolean saveTownBlock(TownBlock townBlock) { tb_hm.put("groupID", townBlock.getPlotObjectGroup().getUUID().toString()); else tb_hm.put("groupID", ""); + if (townBlock.hasDistrict()) + tb_hm.put("districtID", townBlock.getDistrict().getUUID().toString()); + else + tb_hm.put("districtID", ""); if (townBlock.hasMeta()) tb_hm.put("metadata", serializeMetadata(townBlock)); else @@ -2620,6 +2759,13 @@ public void deletePlotGroup(PlotGroup group) { DeleteDB("PLOTGROUPS", pltgrp_hm); } + @Override + public void deleteDistrict(District district) { + HashMap district_hm = new HashMap<>(); + district_hm.put("districtID", district.getUUID()); + DeleteDB("DISTRICTS", district_hm); + } + @Override public void deleteJail(Jail jail) { diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/event/player/PlayerEntersIntoDistrictEvent.java b/Towny/src/main/java/com/palmergames/bukkit/towny/event/player/PlayerEntersIntoDistrictEvent.java new file mode 100644 index 0000000000..201551b062 --- /dev/null +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/event/player/PlayerEntersIntoDistrictEvent.java @@ -0,0 +1,68 @@ +package com.palmergames.bukkit.towny.event.player; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.bukkit.event.player.PlayerMoveEvent; +import org.jetbrains.annotations.Nullable; + +import com.palmergames.bukkit.towny.TownyAPI; +import com.palmergames.bukkit.towny.object.District; +import com.palmergames.bukkit.towny.object.Resident; +import com.palmergames.bukkit.towny.object.WorldCoord; + +/** + * Thrown when a player crosses into Town border. + */ +public class PlayerEntersIntoDistrictEvent extends Event { + private static final HandlerList handlers = new HandlerList(); + private final District enteredDistrict; + private final PlayerMoveEvent pme; + private final WorldCoord from; + private final WorldCoord to; + private final Player player; + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } + + public PlayerEntersIntoDistrictEvent(Player player, WorldCoord to, WorldCoord from, District enteredDistrict, PlayerMoveEvent pme) { + super(!Bukkit.getServer().isPrimaryThread()); + this.enteredDistrict = enteredDistrict; + this.player = player; + this.from = from; + this.pme = pme; + this.to = to; + } + + public Player getPlayer() { + return player; + } + + @Nullable + public Resident getResident() { + return TownyAPI.getInstance().getResident(player); + } + + public PlayerMoveEvent getPlayerMoveEvent() { + return pme; + } + + public District getEnteredDistrict() { + return enteredDistrict; + } + + public WorldCoord getFrom() { + return from; + } + + public WorldCoord getTo() { + return to; + } +} diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/event/player/PlayerExitsFromDistrictEvent.java b/Towny/src/main/java/com/palmergames/bukkit/towny/event/player/PlayerExitsFromDistrictEvent.java new file mode 100644 index 0000000000..0cb4cc3194 --- /dev/null +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/event/player/PlayerExitsFromDistrictEvent.java @@ -0,0 +1,66 @@ +package com.palmergames.bukkit.towny.event.player; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.bukkit.event.player.PlayerMoveEvent; +import org.jetbrains.annotations.Nullable; + +import com.palmergames.bukkit.towny.TownyAPI; +import com.palmergames.bukkit.towny.object.District; +import com.palmergames.bukkit.towny.object.Resident; +import com.palmergames.bukkit.towny.object.WorldCoord; + +public class PlayerExitsFromDistrictEvent extends Event { + private static final HandlerList handlers = new HandlerList(); + + private final District leftDistrict; + private final PlayerMoveEvent pme; + private final WorldCoord from; + private final Player player; + private final WorldCoord to; + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } + + public PlayerExitsFromDistrictEvent(Player player, WorldCoord to, WorldCoord from, District leftDistrict, PlayerMoveEvent pme) { + super(!Bukkit.getServer().isPrimaryThread()); + this.leftDistrict = leftDistrict; + this.player = player; + this.from = from; + this.pme = pme; + this.to = to; + } + + public Player getPlayer() { + return player; + } + + @Nullable + public Resident getResident() { + return TownyAPI.getInstance().getResident(player); + } + + public PlayerMoveEvent getPlayerMoveEvent() { + return pme; + } + + public District getLeftDistrict() { + return leftDistrict; + } + + public WorldCoord getFrom() { + return from; + } + + public WorldCoord getTo() { + return to; + } +} diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/event/plot/district/DistrictAddEvent.java b/Towny/src/main/java/com/palmergames/bukkit/towny/event/plot/district/DistrictAddEvent.java new file mode 100644 index 0000000000..05557c97ff --- /dev/null +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/event/plot/district/DistrictAddEvent.java @@ -0,0 +1,50 @@ +package com.palmergames.bukkit.towny.event.plot.district; + +import com.palmergames.bukkit.towny.event.CancellableTownyEvent; +import com.palmergames.bukkit.towny.object.District; +import com.palmergames.bukkit.towny.object.TownBlock; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Called when a townblock is added into a district + */ +public class DistrictAddEvent extends CancellableTownyEvent { + private static final HandlerList HANDLER_LIST = new HandlerList(); + private final District district; + private final TownBlock townBlock; + private final Player player; + + public DistrictAddEvent(final District district, final TownBlock townBlock, final Player player) { + this.district = district; + this.townBlock = townBlock; + this.player = player; + } + + @NotNull + public District getDistrict() { + return district; + } + + @NotNull + public TownBlock getTownBlock() { + return townBlock; + } + + @NotNull + public Player getPlayer() { + return player; + } + + @NotNull + public static HandlerList getHandlerList() { + return HANDLER_LIST; + } + + @NotNull + @Override + public HandlerList getHandlers() { + return HANDLER_LIST; + } +} diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/event/plot/district/DistrictCreatedEvent.java b/Towny/src/main/java/com/palmergames/bukkit/towny/event/plot/district/DistrictCreatedEvent.java new file mode 100644 index 0000000000..7c1889f34b --- /dev/null +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/event/plot/district/DistrictCreatedEvent.java @@ -0,0 +1,24 @@ +package com.palmergames.bukkit.towny.event.plot.district; + +import com.palmergames.bukkit.towny.object.District; +import com.palmergames.bukkit.towny.object.TownBlock; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +/** + * Called when a district is created. + */ +public class DistrictCreatedEvent extends DistrictAddEvent { + public DistrictCreatedEvent(District district, TownBlock townBlock, Player player) { + super(district, townBlock, player); + } + + /** + * @return The initial townblock that this district is being created with. + */ + @Override + @NotNull + public TownBlock getTownBlock() { + return super.getTownBlock(); + } +} diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/event/plot/district/DistrictDeletedEvent.java b/Towny/src/main/java/com/palmergames/bukkit/towny/event/plot/district/DistrictDeletedEvent.java new file mode 100644 index 0000000000..a7b50a5a8a --- /dev/null +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/event/plot/district/DistrictDeletedEvent.java @@ -0,0 +1,67 @@ +package com.palmergames.bukkit.towny.event.plot.district; + +import com.palmergames.bukkit.towny.event.CancellableTownyEvent; +import com.palmergames.bukkit.towny.object.District; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class DistrictDeletedEvent extends CancellableTownyEvent { + private static final HandlerList HANDLER_LIST = new HandlerList(); + private final District district; + private final Player player; + private final Cause deletionCause; + + public DistrictDeletedEvent(@NotNull District district, @Nullable Player player, @NotNull Cause deletionCause) { + this.district = district; + this.player = player; + this.deletionCause = deletionCause; + } + + /** + * @return The district that is being deleted. + */ + @NotNull + public District getDistrict() { + return district; + } + + /** + * @return The player associated with the deletion, if applicable. + */ + @Nullable + public Player getPlayer() { + return player; + } + + public Cause getDeletionCause() { + return deletionCause; + } + + public static HandlerList getHandlerList() { + return HANDLER_LIST; + } + + @NotNull + @Override + public HandlerList getHandlers() { + return HANDLER_LIST; + } + + public enum Cause { + UNKNOWN, + /** + * The district was deleted by a player via the /plot district delete command. + */ + DELETED, + /** + * The district was deleted because all of its townblocks were removed. + */ + NO_TOWNBLOCKS, + /** + * The district was deleted because the town it was in being deleted/ruined. + */ + TOWN_DELETED, + } +} diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/huds/PermHUD.java b/Towny/src/main/java/com/palmergames/bukkit/towny/huds/PermHUD.java index bebc609724..e1e76e729a 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/huds/PermHUD.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/huds/PermHUD.java @@ -38,6 +38,7 @@ public class PermHUD { /* Scoreboards use Teams here is our team names.*/ private static final String HUD_OBJECTIVE = "PERM_HUD_OBJ"; private static final String TEAM_PERMS_TITLE = "permsTitle"; + private static final String TEAM_DISTRICT_NAME = "districtName"; private static final String TEAM_PLOT_NAME = "plot_name"; private static final String TEAM_PLOT_COST = "plot_cost"; private static final String TEAM_BUILD = "build"; @@ -64,7 +65,7 @@ public static String permHudTestKey() { public static void updatePerms(Player p, WorldCoord worldCoord) { Translator translator = Translator.locale(p); - String plotName, build, destroy, switching, item, type, pvp, explosions, firespread, mobspawn, title; + String districtName, plotName, build, destroy, switching, item, type, pvp, explosions, firespread, mobspawn, title; Scoreboard board = p.getScoreboard(); // Due to tick delay (probably not confirmed), a HUD can actually be removed from the player. // Causing board to return null, and since we don't create a new board, a NullPointerException occurs. @@ -92,6 +93,9 @@ public static void updatePerms(Player p, WorldCoord worldCoord) { // Displays the name of the owner, and if the owner is a resident the town name as well. title = GOLD + owner.getName() + (townBlock.hasResident() ? " (" + townBlock.getTownOrNull().getName() + ")" : ""); + // District name + districtName = townBlock.hasDistrict() ? townBlock.getDistrict().getFormattedName() : ""; + // Plot Type type = townBlock.getType().equals(TownBlockType.RESIDENTIAL) ? " " : townBlock.getType().getName(); @@ -120,6 +124,7 @@ public static void updatePerms(Player p, WorldCoord worldCoord) { // Set the values to our Scoreboard's teams. board.getObjective(HUD_OBJECTIVE).setDisplayName(HUDManager.check(title)); + board.getTeam(TEAM_DISTRICT_NAME).setSuffix(districtName); board.getTeam(TEAM_PLOT_NAME).setSuffix(plotName); board.getTeam(TEAM_PLOT_TYPE).setSuffix(type); board.getTeam(TEAM_PLOT_COST).setSuffix(forSale); @@ -164,6 +169,7 @@ private static void clearPerms (Player p) { Scoreboard board = p.getScoreboard(); try { board.getObjective(HUD_OBJECTIVE).setDisplayName(HUDManager.check(getFormattedWildernessName(p.getWorld()))); + board.getTeam(TEAM_DISTRICT_NAME).setSuffix(" "); board.getTeam(TEAM_PLOT_NAME).setSuffix(" "); board.getTeam(TEAM_PLOT_TYPE).setSuffix(" "); board.getTeam(TEAM_PLOT_COST).setSuffix(" "); @@ -206,6 +212,7 @@ public static void toggleOn (Player p) { private static void initializeScoreboard(Translator translator, Scoreboard board) { String PERM_HUD_TITLE = GOLD + ""; + String districtName_entry = ""; String plotName_entry = ""; String keyPlotType_entry = DARK_GREEN + translator.of("msg_perm_hud_plot_type"); String forSale_entry = DARK_GREEN + translator.of("msg_perm_hud_plot_for_sale") + GRAY; @@ -233,6 +240,7 @@ private static void initializeScoreboard(Translator translator, Scoreboard board obj.setDisplaySlot(DisplaySlot.SIDEBAR); obj.setDisplayName(PERM_HUD_TITLE); //register teams + Team districtName = board.registerNewTeam(TEAM_DISTRICT_NAME); Team plotName = board.registerNewTeam(TEAM_PLOT_NAME); Team keyPlotType = board.registerNewTeam(TEAM_PLOT_TYPE); Team forSaleTitle = board.registerNewTeam(TEAM_PLOT_COST); @@ -254,6 +262,7 @@ private static void initializeScoreboard(Translator translator, Scoreboard board Team keyAlly = board.registerNewTeam(TEAM_ALLY); //add each team as an entry (this sets the prefix to each line of the HUD.) + districtName.addEntry(districtName_entry); plotName.addEntry(plotName_entry); keyPlotType.addEntry(keyPlotType_entry); forSaleTitle.addEntry(forSale_entry); @@ -275,6 +284,7 @@ private static void initializeScoreboard(Translator translator, Scoreboard board keyAlly.addEntry(keyAlly_entry); //set scores for positioning + obj.getScore(districtName_entry).setScore(17); obj.getScore(plotName_entry).setScore(16); obj.getScore(keyPlotType_entry).setScore(15); obj.getScore(forSale_entry).setScore(14); diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/listeners/TownyCustomListener.java b/Towny/src/main/java/com/palmergames/bukkit/towny/listeners/TownyCustomListener.java index efc2494a42..dbe9fded42 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/listeners/TownyCustomListener.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/listeners/TownyCustomListener.java @@ -22,12 +22,14 @@ import com.palmergames.bukkit.towny.event.damage.TownyPlayerDamagePlayerEvent; import com.palmergames.bukkit.towny.event.nation.NationPreTownLeaveEvent; import com.palmergames.bukkit.towny.event.town.TownPreUnclaimCmdEvent; +import com.palmergames.bukkit.towny.event.town.TownPreUnclaimEvent; import com.palmergames.bukkit.towny.exceptions.TownyException; import com.palmergames.bukkit.towny.object.CellSurface; import com.palmergames.bukkit.towny.object.PlayerCache; import com.palmergames.bukkit.towny.object.Resident; import com.palmergames.bukkit.towny.object.SpawnType; import com.palmergames.bukkit.towny.object.Town; +import com.palmergames.bukkit.towny.object.TownBlock; import com.palmergames.bukkit.towny.object.TownyWorld; import com.palmergames.bukkit.towny.object.Translatable; import com.palmergames.bukkit.towny.object.Translation; @@ -35,6 +37,7 @@ import com.palmergames.bukkit.towny.utils.BorderUtil; import com.palmergames.bukkit.towny.utils.ChunkNotificationUtil; import com.palmergames.bukkit.towny.utils.PlayerCacheUtil; +import com.palmergames.bukkit.towny.utils.ProximityUtil; import com.palmergames.bukkit.towny.utils.SpawnUtil; import com.palmergames.bukkit.util.Colors; import com.palmergames.bukkit.util.DrawSmokeTaskFactory; @@ -213,6 +216,25 @@ public void onTownUnclaim(TownPreUnclaimCmdEvent event) { } } + /** + * Used to prevent unclaiming when a District would be cut in two parts. + * + * @param event {@link TownPreUnclaimEvent} thrown when someone runs /t unclaim. + */ + @EventHandler(ignoreCancelled = true) + public void onTownUnclaimDistrict(TownPreUnclaimEvent event) { + TownBlock townBlock = event.getTownBlock(); + if (!townBlock.hasDistrict()) + return; + + try { + ProximityUtil.testAdjacentRemoveDistrictRulesOrThrow(townBlock.getWorldCoord(), event.getTown(), townBlock.getDistrict(), 1); + } catch (TownyException e) { + event.setCancelled(true); + event.setCancelMessage(e.getMessage()); + } + } + /** * Used to warn towns when they're approaching their claim limit, when the * takeoverclaim feature is enabled, as well as claiming particles. diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/listeners/TownyPlayerListener.java b/Towny/src/main/java/com/palmergames/bukkit/towny/listeners/TownyPlayerListener.java index f14b19c1d6..dedbbc6a13 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/listeners/TownyPlayerListener.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/listeners/TownyPlayerListener.java @@ -11,13 +11,16 @@ import com.palmergames.bukkit.towny.event.TitleNotificationEvent; import com.palmergames.bukkit.towny.event.executors.TownyActionEventExecutor; import com.palmergames.bukkit.towny.event.player.PlayerDeniedBedUseEvent; +import com.palmergames.bukkit.towny.event.player.PlayerEntersIntoDistrictEvent; import com.palmergames.bukkit.towny.event.player.PlayerEntersIntoTownBorderEvent; +import com.palmergames.bukkit.towny.event.player.PlayerExitsFromDistrictEvent; import com.palmergames.bukkit.towny.event.player.PlayerExitsFromTownBorderEvent; import com.palmergames.bukkit.towny.event.player.PlayerKeepsExperienceEvent; import com.palmergames.bukkit.towny.event.player.PlayerKeepsInventoryEvent; import com.palmergames.bukkit.towny.event.teleport.CancelledTownyTeleportEvent.CancelledTeleportReason; import com.palmergames.bukkit.towny.hooks.PluginIntegrations; import com.palmergames.bukkit.towny.object.CommandList; +import com.palmergames.bukkit.towny.object.District; import com.palmergames.bukkit.towny.object.PlayerCache; import com.palmergames.bukkit.towny.object.Resident; import com.palmergames.bukkit.towny.object.Town; @@ -938,6 +941,48 @@ public void onPlayerChangePlotEvent(PlayerChangePlotEvent event) { } } + /* + * PlayerChangePlotEvent that can fire the PlayerExitsFromDistrictEvent and PlayerEntersIntoDistrictEvent + */ + @EventHandler(priority = EventPriority.NORMAL) + public void onPlayerChangeDistricts(PlayerChangePlotEvent event) { + if (!TownyUniverse.getInstance().hasResident(event.getPlayer().getUniqueId())) + return; + + WorldCoord from = event.getFrom(); + WorldCoord to = event.getTo(); + boolean fromHasDistrict = !from.isWilderness() && from.getTownBlockOrNull().hasDistrict(); + boolean toHasDistrict = !to.isWilderness() && to.getTownBlockOrNull().hasDistrict(); + if (to.isWilderness() && from.isWilderness() || (!fromHasDistrict && !toHasDistrict)) + // Both are wilderness, or neither plot involves a District. No event will fire. + return; + + District fromDistrict = fromHasDistrict ? from.getTownBlockOrNull().getDistrict() : null; + District toDistrict = toHasDistrict ? to.getTownBlockOrNull().getDistrict() : null; + + if (to.isWilderness() && fromHasDistrict) { + // Gone from a Town into the wilderness. + BukkitTools.fireEvent(new PlayerExitsFromDistrictEvent(event.getPlayer(), to, from, fromDistrict, event.getMoveEvent())); + + } else if (from.isWilderness() && toHasDistrict) { + // Gone from wilderness into Town. + BukkitTools.fireEvent(new PlayerEntersIntoDistrictEvent(event.getPlayer(), to, from, toDistrict, event.getMoveEvent())); + + } else if (!to.isWilderness() && !from.isWilderness() && to.getTownOrNull().equals(from.getTownOrNull()) + && fromHasDistrict && toHasDistrict && !fromDistrict.equals(toDistrict)) { + // Moving in same town, between two different Districts. + BukkitTools.fireEvent(new PlayerExitsFromDistrictEvent(event.getPlayer(), to, from, fromDistrict, event.getMoveEvent())); + BukkitTools.fireEvent(new PlayerEntersIntoDistrictEvent(event.getPlayer(), to, from, toDistrict, event.getMoveEvent())); + + } else { + // Player has left one Town and immediately entered a different one, check if there were districts. + if (fromHasDistrict) + BukkitTools.fireEvent(new PlayerExitsFromDistrictEvent(event.getPlayer(), to, from, fromDistrict, event.getMoveEvent())); + if (toHasDistrict) + BukkitTools.fireEvent(new PlayerEntersIntoDistrictEvent(event.getPlayer(), to, from, toDistrict, event.getMoveEvent())); + } + } + /* * onOutlawEnterTown * - Handles outlaws entering a town they are outlawed in. diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/object/District.java b/Towny/src/main/java/com/palmergames/bukkit/towny/object/District.java new file mode 100644 index 0000000000..c0b4405ad6 --- /dev/null +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/object/District.java @@ -0,0 +1,133 @@ +package com.palmergames.bukkit.towny.object; + +import com.palmergames.bukkit.towny.TownyMessaging; +import com.palmergames.bukkit.towny.TownyUniverse; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * @author LlmDl + */ +public class District extends ObjectGroup implements Nameable, Savable { + private List townBlocks; + private Town town; + + /** + * @param id A unique identifier for the district id. + * @param name An alias for the id used for player in-game interaction via commands. + * @param town The town that this district is owned by. + */ + public District(UUID id, String name, Town town) { + super(id, name); + this.town = town; + } + + /** + * Store district in format "name,id,town,price" + * @return The string in the format described. + */ + @Override + public String toString() { + return super.toString() + "," + getTown().toString(); + } + + @Override + public boolean exists() { + return this.town != null && this.town.exists() && this.town.hasDistrictName(getName()); + } + + /** + * Override the name change method to internally rehash the district map. + * @param name The name of the district. + */ + @Override + public void setName(String name) { + if (getName() == null) { + super.setName(name); + } + else { + String oldName = getName(); + super.setName(name); + town.renameDistrict(oldName, this); + } + } + + public void setTown(Town town) { + this.town = town; + + try { + town.addDistrict(this); + } catch (Exception e) { + TownyMessaging.sendErrorMsg(e.getMessage()); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Objects.hash(town, townBlocks, getName()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + District other = (District) obj; + return Objects.equals(town, other.town) && Objects.equals(getName(), other.getName()); + } + + public Town getTown() { + return town; + } + + /** + * + * @return The qualified resident mode string. + */ + public String toModeString() { + return "District{" + this.toString() + "}"; + } + + public void addTownBlock(TownBlock townBlock) { + if (townBlocks == null) + townBlocks = new ArrayList<>(); + + townBlocks.add(townBlock); + } + + public void removeTownBlock(TownBlock townBlock) { + if (townBlocks != null) + townBlocks.remove(townBlock); + } + + public void setTownblocks(List townBlocks) { + this.townBlocks = townBlocks; + } + + public Collection getTownBlocks() { + return Collections.unmodifiableCollection(townBlocks); + } + + public boolean hasTownBlocks() { + return townBlocks != null && !townBlocks.isEmpty(); + } + + public boolean hasTownBlock(TownBlock townBlock) { + return townBlocks.contains(townBlock); + } + + @Override + public void save() { + TownyUniverse.getInstance().getDataSource().saveDistrict(this); + } +} diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/object/Resident.java b/Towny/src/main/java/com/palmergames/bukkit/towny/object/Resident.java index b16401ede1..32b5ba4c21 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/object/Resident.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/object/Resident.java @@ -90,6 +90,7 @@ public class Resident extends TownyObject implements InviteReceiver, EconomyHand private ScheduledTask respawnProtectionTask = null; private boolean respawnPickupWarningShown = false; // Prevents chat spam when a player attempts to pick up an item while under respawn protection. private String plotGroupName = null; + private String districtName = null; protected CachedTaxOwing cachedTaxOwing = null; public Resident(String name) { @@ -1062,6 +1063,18 @@ public void setPlotGroupName(String plotGroupName) { this.plotGroupName = plotGroupName; } + public boolean hasDistrictName() { + return districtName != null; + } + + public String getDistrictName() { + return districtName; + } + + public void setDistrictName(String districtName) { + this.districtName = districtName; + } + @ApiStatus.Internal @Override public boolean exists() { diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/object/Town.java b/Towny/src/main/java/com/palmergames/bukkit/towny/object/Town.java index 10998c72b5..f7316a46be 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/object/Town.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/object/Town.java @@ -81,6 +81,7 @@ public class Town extends Government implements TownBlockOwner { private List jails = null; private HashMap plotGroups = null; private TownBlockTypeCache plotTypeCache = new TownBlockTypeCache(); + private HashMap districts = null; private Resident mayor; private String founderName; @@ -1444,6 +1445,53 @@ public PlotGroup getPlotObjectGroupFromName(String name) { return null; } + public void renameDistrict(String oldName, District district) { + districts.remove(oldName); + districts.put(district.getName(), district); + } + + public void addDistrict(District district) { + if (!hasDistricts()) + districts = new HashMap<>(); + + districts.put(district.getName(), district); + } + + public void removeDistrict(District district) { + if (hasDistricts() && districts.remove(district.getName()) != null) { + for (TownBlock tb : new ArrayList<>(district.getTownBlocks())) { + if (tb.hasDistrict() && tb.getDistrict().getUUID().equals(district.getUUID())) { + district.removeTownBlock(tb); + tb.removeDistrict(); + tb.save(); + } + } + } + } + + // Abstract to collection in case we want to change structure in the future + public Collection getDistricts() { + if (districts == null || districts.isEmpty()) + return Collections.emptyList(); + + return Collections.unmodifiableCollection(districts.values()); + } + + public boolean hasDistricts() { + return districts != null; + } + + public boolean hasDistrictName(String name) { + return hasDistricts() && districts.containsKey(name); + } + + @Nullable + public District getDistrictFromName(String name) { + if (hasDistricts() && hasDistrictName(name)) + return districts.get(name); + return null; + } + @Override public double getBankCap() { return TownySettings.getTownBankCap(this); diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownBlock.java b/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownBlock.java index d0ed30a35f..f45bf7272d 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownBlock.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/object/TownBlock.java @@ -45,6 +45,7 @@ public class TownBlock extends TownyObject { private boolean taxed = true; private boolean outpost = false; private PlotGroup plotGroup; + private District district; private long claimedAt; private Jail jail; private Map permissionOverrides = new HashMap<>(); @@ -561,6 +562,27 @@ public void setPlotObjectGroup(PlotGroup group) { } } + + public boolean hasDistrict() { return district != null; } + + public District getDistrict() { + return district; + } + + public void removeDistrict() { + this.district = null; + } + + public void setDistrict(District district) { + this.district = district; + + try { + district.addTownBlock(this); + } catch (NullPointerException e) { + TownyMessaging.sendErrorMsg("Townblock failed to setDistrict(district), district is null. "); + } + } + @Override public void save() { TownyUniverse.getInstance().getDataSource().saveTownBlock(this); diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/permissions/PermissionNodes.java b/Towny/src/main/java/com/palmergames/bukkit/towny/permissions/PermissionNodes.java index 56c1e6ff25..95dce01793 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/permissions/PermissionNodes.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/permissions/PermissionNodes.java @@ -231,6 +231,13 @@ public enum PermissionNodes { TOWNY_COMMAND_PLOT_SET_SPLEEF("towny.command.plot.set.spleef"), TOWNY_COMMAND_PLOT_SET_INN("towny.command.plot.set.inn"), TOWNY_COMMAND_PLOT_SET_JAIL("towny.command.plot.set.jail"), + + TOWNY_COMMAND_PLOT_DISTRICT("towny.command.plot.district.*"), + TOWNY_COMMAND_PLOT_DISTRICT_ADD("towny.command.plot.district.add"), + TOWNY_COMMAND_PLOT_DISTRICT_REMOVE("towny.command.plot.district.remove"), + TOWNY_COMMAND_PLOT_DISTRICT_DELETE("towny.command.plot.district.delete"), + TOWNY_COMMAND_PLOT_DISTRICT_RENAME("towny.command.plot.district.rename"), + TOWNY_COMMAND_PLOT_GROUP("towny.command.plot.group.*"), TOWNY_COMMAND_PLOT_GROUP_ADD("towny.command.plot.group.add"), diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/utils/ProximityUtil.java b/Towny/src/main/java/com/palmergames/bukkit/towny/utils/ProximityUtil.java index 8717102f44..b1c8f4d280 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/utils/ProximityUtil.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/utils/ProximityUtil.java @@ -11,6 +11,7 @@ import com.palmergames.bukkit.towny.TownySettings; import com.palmergames.bukkit.towny.exceptions.TownyException; import com.palmergames.bukkit.towny.object.Coord; +import com.palmergames.bukkit.towny.object.District; import com.palmergames.bukkit.towny.object.Nation; import com.palmergames.bukkit.towny.object.Town; import com.palmergames.bukkit.towny.object.TownBlock; @@ -96,6 +97,10 @@ public static void allowTownClaimOrThrow(TownyWorld world, WorldCoord townBlockT public static void testAdjacentClaimsRulesOrThrow(WorldCoord townBlockToClaim, Town town, boolean outpost) throws TownyException { int minAdjacentBlocks = TownySettings.getMinAdjacentBlocks(); + testAdjacentClaimsRulesOrThrow(townBlockToClaim, town, outpost, minAdjacentBlocks); + } + + public static void testAdjacentClaimsRulesOrThrow(WorldCoord townBlockToClaim, Town town, boolean outpost, int minAdjacentBlocks) throws TownyException { if (!outpost && minAdjacentBlocks > 0 && townHasClaimedEnoughLandToBeRestrictedByAdjacentClaims(town, minAdjacentBlocks)) { // Only consider the first worldCoord, larger selection-claims will automatically "bubble" anyways. int numAdjacent = numAdjacentTownOwnedTownBlocks(town, townBlockToClaim); @@ -162,6 +167,11 @@ public static void allowTownUnclaimOrThrow(TownyWorld world, WorldCoord townBloc public static void testAdjacentUnclaimsRulesOrThrow(WorldCoord townBlockToUnclaim, Town town) throws TownyException { // Prevent unclaiming land that would reduce the number of adjacent claims of neighbouring plots below the threshold. int minAdjacentBlocks = TownySettings.getMinAdjacentBlocks(); + testAdjacentUnclaimsRulesOrThrow(townBlockToUnclaim, town, minAdjacentBlocks); + } + + public static void testAdjacentUnclaimsRulesOrThrow(WorldCoord townBlockToUnclaim, Town town, int minAdjacentBlocks) throws TownyException { + // Prevent unclaiming land that would reduce the number of adjacent claims of neighbouring plots below the threshold. if (minAdjacentBlocks > 0 && townHasClaimedEnoughLandToBeRestrictedByAdjacentClaims(town, minAdjacentBlocks)) { WorldCoord firstWorldCoord = townBlockToUnclaim; for (WorldCoord wc : firstWorldCoord.getCardinallyAdjacentWorldCoords(true)) { @@ -175,6 +185,42 @@ public static void testAdjacentUnclaimsRulesOrThrow(WorldCoord townBlockToUnclai } } + /* + * District add/remove methods + */ + + public static void testAdjacentAddDistrictRulesOrThrow(WorldCoord townBlockToClaim, Town town, District district, int minAdjacentBlocks) throws TownyException { + if (minAdjacentBlocks > 0 && townHasClaimedEnoughLandToBeRestrictedByAdjacentClaims(town, minAdjacentBlocks)) { + int numAdjacent = numAdjacentDistrictTownBlocks(town, district, townBlockToClaim); + // The number of adjacement TBs with the same District is not enough. + if (numAdjacent < minAdjacentBlocks) + throw new TownyException(Translatable.of("msg_min_adjacent_blocks", minAdjacentBlocks, numAdjacent)); + } + } + + public static void testAdjacentRemoveDistrictRulesOrThrow(WorldCoord townBlockToUnclaim, Town town, District district, int minAdjacentBlocks) throws TownyException { + // Prevent removing parts of Districts that would cause a district to split into two sections. + if (minAdjacentBlocks > 0 && townHasClaimedEnoughLandToBeRestrictedByAdjacentClaims(town, minAdjacentBlocks)) { + WorldCoord firstWorldCoord = townBlockToUnclaim; + for (WorldCoord wc : firstWorldCoord.getCardinallyAdjacentWorldCoords(true)) { + if (wc.isWilderness() || !wc.hasTown(town) || !wc.getTownBlock().hasDistrict() || !wc.getTownBlock().getDistrict().getName().equals(district.getName())) + continue; + int numAdjacent = numAdjacentDistrictTownBlocks(town, district, wc); + // The number of adjacement TBs with the same District is not enough + if (numAdjacent - 1 < minAdjacentBlocks) + throw new TownyException(Translatable.of("msg_err_cannot_remove_from_district_not_enough_adjacent_claims", wc.getX(), wc.getZ(), numAdjacent)); + } + } + } + + private static int numAdjacentDistrictTownBlocks(Town town, District district, WorldCoord worldCoord) { + return (int) worldCoord.getCardinallyAdjacentWorldCoords(true).stream() + .filter(wc -> wc.hasTown(town) && wc.getTownBlockOrNull() != null) + .map(wc -> wc.getTownBlockOrNull()) + .filter(tb -> tb.hasDistrict() && tb.getDistrict().equals(district)) + .count(); + } + /* * Nation Promixity Methods */ diff --git a/Towny/src/main/java/com/palmergames/bukkit/util/NameValidation.java b/Towny/src/main/java/com/palmergames/bukkit/util/NameValidation.java index b1a117e3f5..c5362e1901 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/util/NameValidation.java +++ b/Towny/src/main/java/com/palmergames/bukkit/util/NameValidation.java @@ -158,6 +158,18 @@ public static String checkAndFilterPlotGroupNameOrThrow(String name) throws Inva return checkAndFilterPlotNameOrThrow(filterCommas(name)); } + + /** + * Check and perform regex on District names + * + * @param name of a District object in {@link String} format. + * @return String of the valid name result. + * @throws InvalidNameException if the District name is invalid. + */ + public static String checkAndFilterDistrictNameOrThrow(String name) throws InvalidNameException { + return checkAndFilterPlotNameOrThrow(filterCommas(name)); + } + /** * Check and perform regex on Titles and Surnames given to residents. * diff --git a/Towny/src/main/resources/lang/en-US.yml b/Towny/src/main/resources/lang/en-US.yml index 7f74bec6fa..b2529e7d49 100644 --- a/Towny/src/main/resources/lang/en-US.yml +++ b/Towny/src/main/resources/lang/en-US.yml @@ -438,6 +438,11 @@ plot_help_8: "Opens the permission editor gui." plot_help_9: "Adds a resident as Trusted in the plot." plot_help_10: "Removes a resident as Trusted in the plot." +plot_district_help_0: "Ex: /plot district new ExpensivePlots" +plot_district_help_1: "Removes a plot from the specified district." +plot_district_help_2: "Deletes a district completely." +plot_district_help_3: "Renames the district you are standing in." + plot_group_help_0: "Ex: /plot group new ExpensivePlots" plot_group_help_1: "Removes a plot from the specified group." plot_group_help_2: "Deletes a plotgroup completely." @@ -1545,6 +1550,10 @@ flatfile_dbg_deleting_duplicate: 'Deleting: %s which is a duplicate of %s' flatfile_dbg_folders_found: 'Folders found %s' flatfile_dbg_group_file_missing_town_delete: 'Group file missing Town, deleting %s' flatfile_dbg_loading_group_list: 'Loading Group list...' +flatfile_dbg_district_file_missing_town_delete: 'District file missing Town, deleting %s' +flatfile_dbg_missing_file_delete_district_entry: 'Missing file: %s deleting entry in [districtuuid].data' +flatfile_err_exception_reading_district_file_at_line: 'Loading Error: Exception while reading District file %s at line: %s' +flatfile_dbg_loading_district_list: 'Loading District list...' flatfile_dbg_loading_nation: 'Loading Nation: %s' flatfile_dbg_loading_nation_list: 'Loading Nation list...' flatfile_dbg_loading_resident: 'Loading Resident: %s' @@ -2523,4 +2532,32 @@ msg_recieved_refund_for_deleted_object: "You have received the bank balance of y msg_err_you_already_own_this_plot: "You already own this plot." -msg_err_you_cannot_delete_this_nation: "You have been prevented from deleting this nation." \ No newline at end of file +msg_err_you_cannot_delete_this_nation: "You have been prevented from deleting this nation." + +status_district_name_and_size: 'Townblock is part of District: %s. District has %s Townblocks total.' + +msg_err_district_name_required: 'You must specify district name ie, /plot district add {name}' + +msg_err_rename_district_name_required: 'You must specify district name ie, /plot district rename {name}' + +msg_district_renamed_from_x_to_y: 'District %s has been renamed to %s.' + +msg_err_plot_not_associated_with_a_district: 'This plot is not associated with a district.' + +msg_plot_was_removed_from_district_x: 'Plot (%s,%s) was removed from district %s.' + +msg_district_empty_deleted: 'The district %s had no townblocks and has been deleted.' + +msg_district_deleted: 'The district %s has been deleted.' + +msg_err_this_plot_is_already_part_of_the_district_x: 'This townblock is already part of the district: %s.' + +msg_townblock_transferred_from_x_to_x_district: 'Townblock transferred from %s to %s district.' + +msg_district_already_exists_did_you_want_to_transfer: 'This townblock is already a part of the %s district. Are you certain you want to transfer it to the %s district.' + +msg_plot_was_put_into_district_x: 'Plot (%s,%s) was put into district %s.' + +msg_err_cannot_remove_from_district_not_enough_adjacent_claims: "You cannot remove this townblock from the district %s, because the a part of the District would be detached from the rest." + +map_hover_district: 'District: ' \ No newline at end of file diff --git a/Towny/src/main/resources/plugin.yml b/Towny/src/main/resources/plugin.yml index f19f1331b4..e4d79bc31e 100644 --- a/Towny/src/main/resources/plugin.yml +++ b/Towny/src/main/resources/plugin.yml @@ -564,6 +564,7 @@ permissions: towny.command.plot.set.*: true towny.command.plot.unclaim: true towny.command.plot.group.*: true + towny.command.plot.district.*: true towny.command.plot.trust: true towny.command.plot.toggle.*: @@ -607,6 +608,15 @@ permissions: towny.command.plot.perm.add: true towny.command.plot.perm.remove: true + towny.command.plot.district.*: + description: User can access the plot district command. + default: false + children: + towny.command.plot.district.add: true + towny.command.plot.district.remove: true + towny.command.plot.district.delete: true + towny.command.plot.district.rename: true + towny.command.plot.group.*: description: User can access the plot group command. default: false