From 91c31a81546ec51cd9c6231b7d0de84f4d23b88c Mon Sep 17 00:00:00 2001 From: LlmDl Date: Wed, 6 Nov 2024 06:56:52 -0600 Subject: [PATCH] Beginning steps to making proper Outpost objects. Remove old outpost db stuff from sql source. Start on replacing old town methods using outpostSpawns with new stuff. Add support for moving outpost spawns, setting outposts with /plot set outpost. Add a maybe-working legacy outpost converter. Make the legacy converter twice as likely to succeed. Lots of testing completed, legacy conversion working well. /plot set outpost spawn & /plot set outpost taken care of. TODO: Change Plot Perm Hud to reduce lines to 15. Better logic and message on claim denial. --- .../bukkit/towny/TownyFormatter.java | 3 + .../bukkit/towny/TownyMessaging.java | 16 +- .../bukkit/towny/TownyUniverse.java | 56 ++++++ .../bukkit/towny/command/HelpMenu.java | 2 +- .../bukkit/towny/command/PlotCommand.java | 9 +- .../bukkit/towny/command/TownCommand.java | 19 +- .../towny/command/TownyAdminCommand.java | 6 +- .../bukkit/towny/db/SQLSchema.java | 18 +- .../bukkit/towny/db/TownyDataSource.java | 41 ++++- .../bukkit/towny/db/TownyDatabaseHandler.java | 14 +- .../bukkit/towny/db/TownyFlatFileSource.java | 133 +++++++++++--- .../bukkit/towny/db/TownySQLSource.java | 166 ++++++++++++++++-- .../bukkit/towny/huds/PermHUD.java | 12 +- .../towny/listeners/TownyCustomListener.java | 14 +- .../bukkit/towny/object/Outpost.java | 87 +++++++++ .../towny/object/OutpostWorldCoord.java | 15 ++ .../bukkit/towny/object/Position.java | 5 + .../palmergames/bukkit/towny/object/Town.java | 165 ++++++++--------- .../bukkit/towny/object/TownBlock.java | 28 ++- .../tasks/LegacyOutpostConversionTask.java | 75 ++++++++ .../bukkit/towny/tasks/TownClaim.java | 12 +- .../bukkit/towny/utils/BorderUtil.java | 52 ++++++ Towny/src/main/resources/lang/en-US.yml | 5 + 23 files changed, 791 insertions(+), 162 deletions(-) create mode 100644 Towny/src/main/java/com/palmergames/bukkit/towny/object/Outpost.java create mode 100644 Towny/src/main/java/com/palmergames/bukkit/towny/object/OutpostWorldCoord.java create mode 100644 Towny/src/main/java/com/palmergames/bukkit/towny/tasks/LegacyOutpostConversionTask.java 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 68d004cdb9a..2b3a2dfa005 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/TownyFormatter.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/TownyFormatter.java @@ -113,6 +113,9 @@ public static StatusScreen getStatus(TownBlock townBlock, Player player) { if (townBlock.hasDistrict()) screen.addComponentOf("district", colourKey(translator.of("status_district_name_and_size", townBlock.getDistrict().getName(), townBlock.getDistrict().getTownBlocks().size()))); + if (townBlock.hasOutpostObject()) + screen.addComponentOf("outpost", colourKey(translator.of("status_outpost_name_and_size", townBlock.getOutpost().getName(), townBlock.getOutpost().getNumTownBlocks()))); + 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/TownyMessaging.java b/Towny/src/main/java/com/palmergames/bukkit/towny/TownyMessaging.java index 043c1188cbb..aa4cdf7b2d6 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/TownyMessaging.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/TownyMessaging.java @@ -3,6 +3,7 @@ import com.palmergames.bukkit.towny.confirmations.Confirmation; import com.palmergames.bukkit.towny.invites.Invite; import com.palmergames.bukkit.towny.object.Nation; +import com.palmergames.bukkit.towny.object.Outpost; import com.palmergames.bukkit.towny.object.PlotGroup; import com.palmergames.bukkit.towny.object.Resident; import com.palmergames.bukkit.towny.object.Town; @@ -26,7 +27,6 @@ import org.apache.logging.log4j.Logger; import org.bukkit.Bukkit; import org.bukkit.ChatColor; -import org.bukkit.Location; import org.bukkit.command.CommandSender; import org.bukkit.command.ConsoleCommandSender; import org.bukkit.entity.Player; @@ -588,7 +588,7 @@ public static void sendOutpostList(Player player, Town town, int page, int total Translator translator = Translator.locale(player); int outpostsCount = town.getAllOutpostSpawns().size(); int iMax = Math.min(page * 10, outpostsCount); - List outposts = town.getAllOutpostSpawns(); + List outpostObjects = town.getOutposts(); TextComponent[] outpostsFormatted; @@ -599,19 +599,19 @@ public static void sendOutpostList(Player player, Town town, int page, int total } for (int i = (page - 1) * 10; i < iMax; i++) { - Location outpost = outposts.get(i); - TownBlock tb = TownyAPI.getInstance().getTownBlock(outpost); - if (tb == null) + Outpost outpost = outpostObjects.get(i); + TownBlock tb = outpost.getSpawnTownBlock(); + if (outpost.getSpawn() == null || tb == null) continue; - String name = !tb.hasPlotObjectGroup() ? tb.getName() : tb.getPlotObjectGroup().getName(); + String name = outpost.getName(); TextComponent dash = Component.text(" - ", NamedTextColor.DARK_GRAY); TextComponent line = Component.text(Integer.toString(i + 1), NamedTextColor.GOLD) .clickEvent(ClickEvent.runCommand("/towny:town outpost " + (i + 1))) .append(dash); TextComponent outpostName = Component.text(name, NamedTextColor.GREEN); - TextComponent worldName = Component.text(Optional.ofNullable(outpost.getWorld()).map(w -> w.getName()).orElse("null"), NamedTextColor.BLUE); - TextComponent coords = Component.text("(" + outpost.getBlockX() + "," + outpost.getBlockZ()+ ")", NamedTextColor.BLUE); + TextComponent worldName = Component.text(Optional.ofNullable(outpost.getSpawn().world()).map(w -> w.getName()).orElse("null"), NamedTextColor.BLUE); + TextComponent coords = Component.text("(" + outpost.getSpawn().blockX() + "," + outpost.getSpawn().blockZ()+ ")", NamedTextColor.BLUE); if (!name.equalsIgnoreCase("")) { line = line.append(outpostName).append(dash); 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 f6139a403e2..533e0d74636 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/TownyUniverse.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/TownyUniverse.java @@ -12,6 +12,7 @@ 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.Outpost; import com.palmergames.bukkit.towny.object.PlotGroup; import com.palmergames.bukkit.towny.object.Position; import com.palmergames.bukkit.towny.object.Resident; @@ -91,6 +92,7 @@ public class TownyUniverse { private final Map replacementNamesMap = new ConcurrentHashMap<>(); private final Map plotGroupUUIDMap = new ConcurrentHashMap<>(); private final Map districtUUIDMap = new ConcurrentHashMap<>(); + private final Map outpostUUIDMap = new ConcurrentHashMap<>(); private final Map wildernessMapDataMap = new ConcurrentHashMap(); private final String rootFolder; @@ -134,6 +136,8 @@ public void clearAllObjects() { spawnPoints.clear(); jailUUIDMap.clear(); plotGroupUUIDMap.clear(); + districtUUIDMap.clear(); + outpostUUIDMap.clear(); wildernessMapDataMap.clear(); replacementNamesMap.clear(); } @@ -958,6 +962,58 @@ public District getDistrict(UUID districtID) { return districtUUIDMap.get(districtID); } + + /* + * Outpost Stuff. + */ + + /** + * Used in loading only. + * + * @param uuid UUID to assign to the Outpost. + */ + public void newOutpostInternal(UUID uuid) { + Outpost outpost = new Outpost(uuid, null); + registerOutpost(outpost); + } + + public void registerOutpost(Outpost outpost) { + outpostUUIDMap.put(outpost.getUUID(), outpost); + } + + public void unregisterOutpost(UUID uuid) { + Outpost outpost = outpostUUIDMap.get(uuid); + if (outpost == null) + return; + outpost.getTown().removeOutpost(outpost); + outpostUUIDMap.remove(uuid); + } + + /** + * Get all the outposts from all towns + * Returns a collection that does not reflect any outpost additions/removals + * + * @return collection of Outpost + */ + public Collection getOutposts() { + return new ArrayList<>(outpostUUIDMap.values()); + } + + public Set getOutpostUUIDs() { + return outpostUUIDMap.keySet(); + } + + /** + * Gets the outpost from the town name and the outpost UUID + * + * @param outpostID UUID of the outpost + * @return Outpost if found, null if none found. + */ + @Nullable + public Outpost getOutpost(UUID outpostID) { + return outpostUUIDMap.get(outpostID); + } + /* * 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 c3895c8b9fb..b07cd97b942 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 @@ -644,7 +644,7 @@ protected MenuBuilder load() { protected MenuBuilder load() { return new MenuBuilder("town claim") .add("", Translatable.of("msg_block_claim")) - .add("outpost", Translatable.of("mayor_help_3")) + .add("outpost [name]", Translatable.of("mayor_help_3")) .add("[auto]", Translatable.of("mayor_help_5")) .add("[circle/rect] [radius]", Translatable.of("mayor_help_4")) .add("[circle/rect] auto", Translatable.of("mayor_help_5")); 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 1bca505abfd..833b14beed3 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 @@ -40,8 +40,10 @@ 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.Outpost; import com.palmergames.bukkit.towny.object.PermissionData; import com.palmergames.bukkit.towny.object.PlotGroup; +import com.palmergames.bukkit.towny.object.Position; import com.palmergames.bukkit.towny.object.Resident; import com.palmergames.bukkit.towny.object.SpawnPointLocation; import com.palmergames.bukkit.towny.object.Town; @@ -735,7 +737,9 @@ public void parsePlotSetOutpost(Player player, Resident resident, TownBlock town if (!townBlock.isOutpost()) throw new TownyException(Translatable.of("msg_err_location_is_not_within_an_outpost_plot")); - town.addOutpostSpawn(player.getLocation()); + Outpost outpost = townBlock.getOutpost(); + outpost.setSpawn(Position.ofLocation(player.getLocation())); + outpost.save(); TownyMessaging.sendMsg(player, Translatable.of("msg_set_outpost_spawn")); return; } @@ -743,6 +747,9 @@ public void parsePlotSetOutpost(Player player, Resident resident, TownBlock town TownyWorld townyWorld = townBlock.getWorld(); Coord key = Coord.parseCoord(player.getLocation()); + if (townBlock.hasOutpostObject()) + throw new TownyException("msg_err_plot_already_part_of_outpost_group"); + // Throws a TownyException with message if outpost should not be set. OutpostUtil.OutpostTests(town, resident, townyWorld, key, resident.isAdmin(), true); diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/command/TownCommand.java b/Towny/src/main/java/com/palmergames/bukkit/towny/command/TownCommand.java index 3c362b25366..c825ea16dd0 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/command/TownCommand.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/command/TownCommand.java @@ -66,6 +66,8 @@ import com.palmergames.bukkit.towny.object.comparators.ComparatorType; import com.palmergames.bukkit.towny.object.economy.Account; import com.palmergames.bukkit.towny.object.Nation; +import com.palmergames.bukkit.towny.object.Outpost; +import com.palmergames.bukkit.towny.object.OutpostWorldCoord; import com.palmergames.bukkit.towny.object.Resident; import com.palmergames.bukkit.towny.object.SpawnType; import com.palmergames.bukkit.towny.object.Town; @@ -3384,6 +3386,10 @@ private static List getTownClaimSelectionOrThrow(Player player, Stri if (split.length == 1 && split[0].equalsIgnoreCase("outpost")) { if (!TownySettings.isAllowingOutposts()) throw new TownyException(Translatable.of("msg_outpost_disable")); + + String name = StringMgmt.join(StringMgmt.remFirstArg(split), " "); + if (name.isEmpty()) + throw new TownyException("You must specify a name for this outpost."); checkPermOrThrow(player, PermissionNodes.TOWNY_COMMAND_TOWN_CLAIM_OUTPOST.getNode()); @@ -3396,7 +3402,7 @@ private static List getTownClaimSelectionOrThrow(Player player, Stri // Select a single WorldCoord using the AreaSelectionUtil. selection = new ArrayList<>(); - selection.add(playerWorldCoord); + selection.add(new OutpostWorldCoord(name, playerWorldCoord)); } else if (split.length == 1 && "fill".equalsIgnoreCase(split[0])) { checkPermOrThrow(player, PermissionNodes.TOWNY_COMMAND_TOWN_CLAIM_FILL.getNode()); @@ -3476,6 +3482,17 @@ private static void vetTownAllowedTheseClaims(Town town, boolean outpost, List outpostOptional = selection.get(0).getCardinallyAdjacentWorldCoords(false).stream() + .filter(wc -> wc.hasTownBlock() && wc.getTownBlockOrNull().hasOutpostObject()) + .map(wc -> wc.getTownBlockOrNull().getOutpost()) + .findFirst(); + if (outpostOptional.isPresent()) { + Outpost outpostObject = outpostOptional.get(); + if (outpostObject.getNumTownBlocks() >= town.getMaxAllowedOutpostLandmass()) + throw new TownyException(String.format("Your town is unable to make an outpost larger than %s.", town.getMaxAllowedOutpostLandmass())); + } } private static void fireTownPreClaimEventOrThrow(Player player, Town town, boolean outpost, List selection) throws TownyException { diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/command/TownyAdminCommand.java b/Towny/src/main/java/com/palmergames/bukkit/towny/command/TownyAdminCommand.java index 12e7a8a5cf0..fef75181322 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/command/TownyAdminCommand.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/command/TownyAdminCommand.java @@ -1055,14 +1055,10 @@ private void parseAdminCheckOutpostsCommand(CommandSender sender, @Nullable Town int removed = 0; for (Town town : towns) { for (Location loc : town.getAllOutpostSpawns()) { - boolean save = false; if (TownyAPI.getInstance().isWilderness(loc) || !TownyAPI.getInstance().getTown(loc).getUUID().equals(town.getUUID())) { - town.removeOutpostSpawn(loc); - save = true; + town.removeOutpost(loc); removed++; } - if (save) - town.save(); } } TownyMessaging.sendMsg(sender, Translatable.of("msg_removed_x_invalid_outpost_spawns", removed)); 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 cc7d352db24..1737ce04574 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 @@ -51,6 +51,9 @@ public static void initTables(Connection cntx) { initTable(cntx, TownyDBTableType.DISTRICT); updateTable(cntx, TownyDBTableType.DISTRICT, getDistrictColumns()); + initTable(cntx, TownyDBTableType.OUTPOST); + updateTable(cntx, TownyDBTableType.OUTPOST, getOutpostColumns()); + initTable(cntx, TownyDBTableType.JAIL); updateTable(cntx, TownyDBTableType.JAIL, getJailsColumns()); @@ -81,7 +84,7 @@ private static String fetchTableSchema(TownyDBTableType tableType) { case TOWNBLOCK -> fetchCreateTownBlocksStatement(); case JAIL -> fetchCreateUUIDStatement(tableType); case PLOTGROUP -> fetchCreatePlotGroupStatement(tableType); - case DISTRICT -> fetchCreateUUIDStatement(tableType); + case DISTRICT, OUTPOST -> fetchCreateUUIDStatement(tableType); case COOLDOWN -> fetchCreateCooldownsStatement(tableType); case WORLD -> fetchCreateWorldStatemnt(tableType); case HIBERNATED_RESIDENT -> fetchCreateUUIDStatement(tableType); @@ -172,6 +175,14 @@ private static List getDistrictColumns() { return columns; } + private static List getOutpostColumns() { + List columns = new ArrayList<>(); + columns.add("`outpostName` mediumtext NOT NULL"); + columns.add("`spawn` mediumtext NOT NULL"); + columns.add("`metadata` text DEFAULT NULL"); + return columns; + } + private static List getResidentColumns(){ List columns = new ArrayList<>(); columns.add("`town` mediumtext"); @@ -229,7 +240,6 @@ private static List getTownColumns() { columns.add("`allowedToWar` bool NOT NULL DEFAULT '1'"); columns.add("`homeblock` mediumtext NOT NULL"); columns.add("`spawn` mediumtext NOT NULL"); - columns.add("`outpostSpawns` mediumtext DEFAULT NULL"); columns.add("`jailSpawns` mediumtext DEFAULT NULL"); columns.add("`outlaws` mediumtext DEFAULT NULL"); columns.add("`uuid` VARCHAR(36) DEFAULT NULL"); @@ -338,13 +348,13 @@ private static List getTownBlockColumns() { columns.add("`resident` mediumtext"); columns.add("`type` TINYINT NOT NULL DEFAULT '0'"); columns.add("`typeName` mediumtext"); - columns.add("`outpost` bool NOT NULL DEFAULT '0'"); columns.add("`permissions` mediumtext NOT NULL"); columns.add("`locked` bool NOT NULL DEFAULT '0'"); columns.add("`changed` bool NOT NULL DEFAULT '0'"); columns.add("`metadata` text DEFAULT NULL"); columns.add("`groupID` VARCHAR(36) DEFAULT NULL"); columns.add("`districtID` VARCHAR(36) DEFAULT NULL"); + columns.add("`outpostID` VARCHAR(36) DEFAULT NULL"); columns.add("`claimedAt` BIGINT NOT NULL"); columns.add("`trustedResidents` mediumtext DEFAULT NULL"); columns.add("`customPermissionData` mediumtext DEFAULT NULL"); @@ -381,6 +391,8 @@ public static void cleanup(Connection connection) { cleanups.add(ColumnUpdate.update("TOWNS", "jailSpawns")); cleanups.add(ColumnUpdate.update("WORLDS", "disableplayertrample")); cleanups.add(ColumnUpdate.update("TOWNS", "assistants")); + cleanups.add(ColumnUpdate.update("TOWNBLOCKS", "outpost")); + cleanups.add(ColumnUpdate.update("TOWNS", "outpostSpawns")); for (ColumnUpdate update : cleanups) dropColumn(connection, update.table(), update.column()); 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 47fb4c61b34..9f550451ca9 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 @@ -10,6 +10,7 @@ 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.Outpost; import com.palmergames.bukkit.towny.object.PlotGroup; import com.palmergames.bukkit.towny.object.Resident; import com.palmergames.bukkit.towny.object.Town; @@ -55,12 +56,12 @@ public abstract class TownyDataSource { public boolean loadAll() { - return loadWorldList() && loadNationList() && loadTownList() && loadPlotGroupList() && loadDistrictList() && loadJailList() && loadResidentList() && loadTownBlockList() && loadWorlds() && loadResidents() && loadTowns() && loadNations() && loadTownBlocks() && loadPlotGroups() && loadDistricts() && loadJails() && loadRegenList() && loadCooldowns(); + return loadWorldList() && loadNationList() && loadTownList() && loadPlotGroupList() && loadDistrictList() && loadOutpostList() && loadJailList() && loadResidentList() && loadTownBlockList() && loadWorlds() && loadResidents() && loadTowns() && loadNations() && loadOutposts() && loadTownBlocks() && loadPlotGroups() && loadDistricts() && loadJails() && loadRegenList() && loadCooldowns(); } public boolean saveAll() { - return saveWorlds() && saveNations() && saveTowns() && saveResidents() && savePlotGroups() && saveDistricts() && saveTownBlocks() && saveJails() && saveRegenList() && saveCooldowns(); + return saveWorlds() && saveNations() && saveTowns() && saveResidents() && savePlotGroups() && saveDistricts() && saveOutposts() && saveTownBlocks() && saveJails() && saveRegenList() && saveCooldowns(); } public boolean saveAllWorlds() { @@ -109,6 +110,10 @@ public boolean saveQueues() { abstract public boolean loadDistrict(District district); + abstract public boolean loadOutpostList(); + + abstract public boolean loadOutpost(Outpost outpost); + abstract public boolean saveRegenList(); abstract public boolean saveResident(Resident resident); @@ -120,7 +125,9 @@ public boolean saveQueues() { abstract public boolean savePlotGroup(PlotGroup group); abstract public boolean saveDistrict(District district); - + + abstract public boolean saveOutpost(Outpost outpost); + abstract public boolean saveJail(Jail jail); abstract public boolean saveNation(Nation nation); @@ -157,6 +164,8 @@ public boolean saveQueues() { abstract public void deleteDistrict(District district); + abstract public void deleteOutpost(Outpost outpost); + abstract public void deleteJail(Jail jail); abstract public CompletableFuture> getHibernatedResidentRegistered(UUID uuid); @@ -246,6 +255,17 @@ public boolean loadDistricts() { return true; } + public boolean loadOutposts() { + TownyMessaging.sendDebugMsg("Loading Outposts"); + for (Outpost outpost : universe.getOutposts()) { + if (!loadOutpost(outpost)) { + plugin.getLogger().severe("Loading Error: Could not read Outpost data: '" + outpost.getUUID() + "'."); + return false; + } + } + return true; + } + abstract public boolean loadCooldowns(); /* @@ -286,6 +306,19 @@ public boolean saveDistricts() { return true; } + public boolean saveOutposts() { + TownyMessaging.sendDebugMsg("Saving Outposts"); + for (Outpost outpost : universe.getOutposts()) + /* + * Only save outposts which actually have townblocks associated with them. + */ + if (outpost.hasTownBlocks()) + saveOutpost(outpost); + else + deleteOutpost(outpost); + return true; + } + public boolean saveJails() { TownyMessaging.sendDebugMsg("Saving Jails"); for (Jail jail : universe.getJails()) @@ -370,6 +403,8 @@ public boolean removeTown(@NotNull Town town, @NotNull DeleteTownEvent.Cause cau abstract public void removeDistrict(District district); + abstract public void removeOutpost(Outpost outpost); + abstract public void renameTown(Town town, String newName) throws AlreadyRegisteredException, NotRegisteredException; abstract public void renameNation(Nation nation, String newName) throws AlreadyRegisteredException, NotRegisteredException; 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 8f2252c9d1f..c69f6486f60 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 @@ -26,6 +26,7 @@ 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.Outpost; import com.palmergames.bukkit.towny.object.PlotGroup; import com.palmergames.bukkit.towny.object.Resident; import com.palmergames.bukkit.towny.object.Town; @@ -54,7 +55,6 @@ import com.palmergames.bukkit.util.NameValidation; import com.palmergames.util.FileMgmt; -import org.bukkit.Location; import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -595,6 +595,12 @@ public void removeDistrict(District district) { deleteDistrict(district); } + @Override + public void removeOutpost(Outpost outpost) { + universe.unregisterOutpost(outpost.getUUID()); + deleteOutpost(outpost); + } + /* * Rename Object Methods */ @@ -1177,7 +1183,7 @@ public void mergeTown(Town mergeInto, Town mergeFrom) { List jails = universe.getJailUUIDMap().values().stream() .filter(jail -> jail.getTown().equals(mergeFrom)) .collect(Collectors.toList()); - List outposts = new ArrayList(mergeFrom.getAllOutpostSpawns()); + List outposts = new ArrayList(mergeFrom.getOutposts()); mergeInto.addPurchasedBlocks(mergeFrom.getPurchasedBlocks()); @@ -1229,8 +1235,8 @@ public void mergeTown(Town mergeInto, Town mergeFrom) { jail.setTown(mergeInto); } - for (Location outpost : outposts) - mergeInto.addOutpostSpawn(outpost); + for (Outpost outpost : outposts) + mergeInto.addOutpost(outpost); lock.unlock(); removeTown(mergeFrom, DeleteTownEvent.Cause.MERGED, null, false); 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 4518e7b6108..57772edd9bc 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 @@ -20,6 +20,7 @@ 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.Outpost; import com.palmergames.bukkit.towny.object.PermissionData; import com.palmergames.bukkit.towny.object.PlotGroup; import com.palmergames.bukkit.towny.object.Position; @@ -34,6 +35,7 @@ import com.palmergames.bukkit.towny.object.jail.Jail; import com.palmergames.bukkit.towny.tasks.CooldownTimerTask; import com.palmergames.bukkit.towny.tasks.DeleteFileTask; +import com.palmergames.bukkit.towny.tasks.LegacyOutpostConversionTask; import com.palmergames.bukkit.towny.utils.MapUtil; import com.palmergames.bukkit.util.BukkitTools; import com.palmergames.util.FileMgmt; @@ -86,6 +88,8 @@ public TownyFlatFileSource(Towny plugin, TownyUniverse universe) { dataFolderPath + File.separator + "plotgroups" + File.separator + "deleted", dataFolderPath + File.separator + "districts", dataFolderPath + File.separator + "districts" + File.separator + "deleted", + dataFolderPath + File.separator + "outposts", + dataFolderPath + File.separator + "outposts" + File.separator + "deleted", dataFolderPath + File.separator + "jails", dataFolderPath + File.separator + "jails" + File.separator + "deleted" )) { @@ -144,6 +148,10 @@ public String getDistrictFilename(District district) { return dataFolderPath + File.separator + "districts" + File.separator + district.getUUID() + ".data"; } + public String getOutpostFilename(Outpost outpost) { + return dataFolderPath + File.separator + "outposts" + File.separator + outpost.getUUID() + ".data"; + } + public String getJailFilename(Jail jail) { return dataFolderPath + File.separator + "jails" + File.separator + jail.getUUID() + ".txt"; } @@ -238,7 +246,21 @@ public boolean loadDistrictList() { return true; } - + + @Override + public boolean loadOutpostList() { + TownyMessaging.sendDebugMsg(Translation.of("flatfile_dbg_loading_outpost_list")); + File[] outpostFiles = receiveObjectFiles("outposts", ".data"); + + if (outpostFiles == null) + return true; + + for (File outpostFile : outpostFiles) + universe.newOutpostInternal(UUID.fromString(outpostFile.getName().replace(".data", ""))); + + return true; + } + @Override public boolean loadResidentList() { @@ -907,11 +929,14 @@ public boolean loadTown(Town town) { line = keys.get("outpostspawns"); if (line != null) { String[] outposts = line.split(";"); + int i = 0; for (String spawn : outposts) { + i++; tokens = spawn.split(","); if (tokens.length >= 4) try { - town.forceAddOutpostSpawn(Position.deserialize(tokens)); + Position pos = Position.deserialize(tokens); + plugin.getScheduler().runLater(new LegacyOutpostConversionTask(plugin, pos, town), i * 100L); } catch (IllegalArgumentException e) { plugin.getLogger().warning("Failed to load an outpost spawn location for town " + town.getName() + ": " + e.getMessage()); } @@ -1716,7 +1741,44 @@ public boolean loadDistrict(District district) { return true; } - + + public boolean loadOutpost(Outpost outpost) { + String line = ""; + String path = getOutpostFilename(outpost); + + File districtFile = new File(path); + if (districtFile.exists() && districtFile.isFile()) { + try { + HashMap keys = FileMgmt.loadFileIntoHashMap(districtFile); + + line = keys.get("outpostName"); + if (line != null) + outpost.setName(line.trim()); + + line = keys.get("spawn"); + if (line != null) { + String[] tokens = line.split("#"); + if (tokens.length >= 4) + try { + outpost.setSpawn(Position.deserialize(tokens)); + } catch (IllegalArgumentException e) { + plugin.getLogger().warning("Failed to load outpost spawn location for outpost " + outpost.getName() + ": " + e.getMessage()); + } + } + + line = keys.get("metadata"); + if (line != null) + MetadataLoader.getInstance().deserializeMetadata(outpost, line.trim()); + + } catch (Exception e) { + TownyMessaging.sendErrorMsg(Translation.of("flatfile_err_exception_reading_outpost_file_at_line", path, line)); + return false; + } + } + + return true; + } + @Override public boolean loadTownBlocks() { @@ -1808,13 +1870,6 @@ else if (universe.getReplacementNameMap().containsKey(line.trim())) { } catch (Exception ignored) { } - line = keys.get("outpost"); - if (line != null) - try { - townBlock.setOutpost(Boolean.parseBoolean(line)); - } catch (Exception ignored) { - } - line = keys.get("permissions"); if ((line != null) && !line.isEmpty()) try { @@ -1881,6 +1936,24 @@ else if (universe.getReplacementNameMap().containsKey(line.trim())) { } } + line = keys.get("outpostID"); + UUID outpostID = null; + if (line != null && !line.isEmpty()) { + outpostID = UUID.fromString(line.trim()); + } + + if (outpostID != null) { + Outpost outpost = universe.getOutpost(outpostID); + if (outpost != null) { + outpost.addTownblock(townBlock); + townBlock.setOutpostObject(outpost); + if (outpost.getNumTownBlocks() <= 1) + townBlock.getTownOrNull().addOutpost(outpost); + } else { + townBlock.removeOutpost(); + } + } + line = keys.get("trustedResidents"); if (line != null && !line.isEmpty() && townBlock.getTrustedResidents().isEmpty()) { for (Resident resident : TownyAPI.getInstance().getResidents(toUUIDArray(line.split(",")))) @@ -2152,14 +2225,6 @@ public boolean saveTown(Town town) { if (spawnPos != null) list.add("spawn=" + String.join(",", spawnPos.serialize())); - // Outpost Spawns - StringBuilder outpostArray = new StringBuilder("outpostspawns="); - if (town.hasOutpostSpawn()) - for (Position spawn : town.getOutpostSpawns()) { - outpostArray.append(String.join(",", spawn.serialize())).append(";"); - } - list.add(outpostArray.toString()); - // Outlaws list.add("outlaws=" + StringMgmt.join(town.getOutlaws(), ",")); @@ -2237,6 +2302,25 @@ public boolean saveDistrict(District district) { return true; } + @Override + public boolean saveOutpost(Outpost outpost) { + List list = new ArrayList<>(); + + try { + list.add("outpostName=" + outpost.getName()); + if (outpost.getSpawn() != null) + list.add("spawn=" + String.join("#", outpost.getSpawn().serialize())); + list.add("metadata=" + serializeMetadata(outpost)); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, "An exception occurred while saving outpost " + Optional.ofNullable(outpost).map(g -> g.getUUID().toString()).orElse("null") + ": ", e); + } + + // Save file + this.queryQueue.add(new FlatFileSaveTask(list, getOutpostFilename(outpost))); + + return true; + } + @Override public boolean saveNation(Nation nation) { @@ -2491,9 +2575,6 @@ public boolean saveTownBlock(TownBlock townBlock) { // type list.add("type=" + townBlock.getTypeName()); - // outpost - list.add("outpost=" + townBlock.isOutpost()); - /* * Only include a permissions line IF the plot perms are custom. */ @@ -2532,6 +2613,10 @@ public boolean saveTownBlock(TownBlock townBlock) { list.add("districtID=" + districtID); + // Outpost ID + if (townBlock.hasOutpostObject()) + list.add("outpostID=" + townBlock.getOutpostUUID()); + list.add("trustedResidents=" + StringMgmt.join(toUUIDList(townBlock.getTrustedResidents()), ",")); Map stringMap = new HashMap<>(); @@ -2627,6 +2712,12 @@ public void deleteDistrict(District district) { queryQueue.add(new DeleteFileTask(file, false)); } + @Override + public void deleteOutpost(Outpost outpost) { + File file = new File(getOutpostFilename(outpost)); + 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 b4e57f7aea8..7f61b57142f 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 @@ -20,6 +20,7 @@ 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.Outpost; import com.palmergames.bukkit.towny.object.PermissionData; import com.palmergames.bukkit.towny.object.PlotGroup; import com.palmergames.bukkit.towny.object.Position; @@ -32,6 +33,7 @@ import com.palmergames.bukkit.towny.object.metadata.MetadataLoader; import com.palmergames.bukkit.towny.object.jail.Jail; import com.palmergames.bukkit.towny.tasks.CooldownTimerTask; +import com.palmergames.bukkit.towny.tasks.LegacyOutpostConversionTask; import com.palmergames.bukkit.towny.utils.MapUtil; import com.palmergames.bukkit.util.BukkitTools; import com.palmergames.util.FileMgmt; @@ -426,6 +428,7 @@ public enum TownyDBTableType { JAIL("JAILS", "SELECT uuid FROM ", "uuid"), PLOTGROUP("PLOTGROUPS", "SELECT groupID FROM ", "groupID"), DISTRICT("DISTRICTS", "SELECT uuid FROM ", "uuid"), + OUTPOST("OUTPOSTS", "SELECT uuid FROM ", "uuid"), RESIDENT("RESIDENTS", "SELECT name FROM ", "name"), HIBERNATED_RESIDENT("HIBERNATEDRESIDENTS", "", "uuid"), TOWN("TOWNS", "SELECT name FROM ", "name"), @@ -661,6 +664,31 @@ public boolean loadDistrictList() { return false; } + @Override + public boolean loadOutpostList() { + TownyMessaging.sendDebugMsg("Loading Outpost List"); + + try (Connection connection = getConnection(); + Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery("SELECT uuid FROM " + tb_prefix + "OUTPOSTS")) { + + while (rs.next()) { + try { + universe.newOutpostInternal(UUID.fromString(rs.getString("uuid"))); + } catch (IllegalArgumentException e) { + plugin.getLogger().log(Level.WARNING, "ID for outpost is not a valid uuid, skipped loading outpost {}", rs.getString("uuid")); + } + } + + return true; + + } catch (SQLException e) { + plugin.getLogger().log(Level.SEVERE, "An exception occurred while loading outpost list", e); + } + + return false; + } + public boolean loadJailList() { TownyMessaging.sendDebugMsg("Loading Jail List"); @@ -1078,12 +1106,15 @@ private boolean loadTown(ResultSet rs) { line = rs.getString("outpostSpawns"); if (line != null) { String[] outposts = line.split(";"); + int i = 0; for (String spawn : outposts) { + i++; search = (line.contains("#")) ? "#" : ","; tokens = spawn.split(search); if (tokens.length >= 4) try { - town.forceAddOutpostSpawn(Position.deserialize(tokens)); + Position pos = Position.deserialize(tokens); + plugin.getScheduler().runLater(new LegacyOutpostConversionTask(plugin, pos, town), i * 100L); } catch (IllegalArgumentException e) { plugin.getLogger().warning("Failed to load an outpost spawn location for town " + town.getName() + ": " + e.getMessage()); } @@ -1908,12 +1939,6 @@ public boolean loadTownBlocks() { if (line != null) townBlock.setType(TownBlockTypeHandler.getTypeInternal(line)); - boolean outpost = rs.getBoolean("outpost"); - try { - townBlock.setOutpost(outpost); - } catch (Exception ignored) { - } - line = rs.getString("permissions"); if ((line != null) && !line.isEmpty()) try { @@ -1983,6 +2008,25 @@ public boolean loadTownBlocks() { } catch (SQLException ignored) { } + try { + line = rs.getString("outpostID"); + if (line != null && !line.isEmpty()) { + try { + UUID outpostID = UUID.fromString(line.trim()); + Outpost outpost = universe.getOutpost(outpostID); + if (outpost != null) { + outpost.addTownblock(townBlock); + townBlock.setOutpostObject(outpost); + if (outpost.getNumTownBlocks() <= 1) + townBlock.getTownOrNull().addOutpost(outpost); + } + } catch (Exception ignored) { + } + + } + } catch (SQLException ignored) { + } + line = rs.getString("trustedResidents"); if (line != null && !line.isEmpty() && townBlock.getTrustedResidents().isEmpty()) { String search = (line.contains("#")) ? "#" : ","; @@ -2067,6 +2111,27 @@ public boolean loadDistricts() { return true; } + @Override + public boolean loadOutposts() { + TownyMessaging.sendDebugMsg("Loading outposts."); + + try (Connection connection = getConnection(); + Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery("SELECT * FROM " + tb_prefix + "OUTPOSTS ")) { + while (rs.next()) { + if (!loadOutpost(rs)) { + plugin.getLogger().warning("Loading Error: Could not read outpost data properly."); + return false; + } + } + } catch (SQLException e) { + TownyMessaging.sendErrorMsg("SQL: Load Outpost sql Error - " + e.getMessage()); + return false; + } + + return true; + } + @Override public boolean loadCooldowns() { try (Connection connection = getConnection(); @@ -2204,6 +2269,54 @@ public boolean loadDistrict(District district) { return true; } + private boolean loadOutpost(ResultSet rs) { + String line = null; + String uuidString = null; + + try { + Outpost outpost = universe.getOutpost(UUID.fromString(rs.getString("uuid"))); + if (outpost == null) { + TownyMessaging.sendErrorMsg("SQL: A outpost was not registered properly on load!"); + return true; + } + uuidString = outpost.getUUID().toString(); + + line = rs.getString("outpostName"); + if (line != null) + try { + outpost.setName(line.trim()); + } catch (Exception ignored) { + } + + line = rs.getString("spawn"); + if (line != null) { + String[] tokens = line.split("#"); + if (tokens.length >= 4) + try { + outpost.setSpawn(Position.deserialize(tokens)); + } catch (IllegalArgumentException e) { + plugin.getLogger().warning("Failed to load spawn location for town " + outpost.getName() + ": " + e.getMessage()); + } + } + + line = rs.getString("metadata"); + if (line != null) { + MetadataLoader.getInstance().deserializeMetadata(outpost, line); + } + } catch (SQLException e) { + plugin.getLogger().log(Level.WARNING, "Loading Error: Exception while reading outpost: " + uuidString + + " at line: " + line + " in the sql database", e); + return false; + } + return true; + } + + @Override + public boolean loadOutpost(Outpost outpost) { + // Unused in SQL. + return true; + } + @Override public boolean loadJails() { TownyMessaging.sendDebugMsg("Loading Jails"); @@ -2417,13 +2530,7 @@ public synchronized boolean saveTown(Town town) { final Position spawnPos = town.spawnPosition(); twn_hm.put("spawn", spawnPos != null ? String.join("#", spawnPos.serialize()) : ""); - // Outpost Spawns - StringBuilder outpostArray = new StringBuilder(); - if (town.hasOutpostSpawn()) - for (Position spawn : town.getOutpostSpawns()) { - outpostArray.append(String.join("#", spawn.serialize())).append(";"); - } - twn_hm.put("outpostSpawns", outpostArray.toString()); + if (town.hasValidUUID()) { twn_hm.put("uuid", town.getUUID()); } else { @@ -2493,6 +2600,25 @@ public boolean saveDistrict(District district) { return false; } + @Override + public boolean saveOutpost(Outpost outpost) { + TownyMessaging.sendDebugMsg("Saving outpost " + outpost.getName()); + try { + HashMap outpost_hm = new HashMap<>(); + outpost_hm.put("uuid", outpost.getUUID().toString()); + outpost_hm.put("outpostName", outpost.getName()); + final Position spawnPos = outpost.getSpawn(); + outpost_hm.put("spawn", spawnPos != null ? String.join("#", spawnPos.serialize()) : ""); + outpost_hm.put("metadata", serializeMetadata(outpost)); + + updateDB("OUTPOSTS", outpost_hm, Collections.singletonList("uuid")); + + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, "SQL: Save Outpost unknown error", e); + } + return false; + } + @Override public synchronized boolean saveNation(Nation nation) { @@ -2682,7 +2808,6 @@ public synchronized boolean saveTownBlock(TownBlock townBlock) { tb_hm.put("town", townBlock.getTown().getName()); tb_hm.put("resident", (townBlock.hasResident()) ? townBlock.getResidentOrNull().getName() : ""); tb_hm.put("typeName", townBlock.getTypeName()); - tb_hm.put("outpost", townBlock.isOutpost()); tb_hm.put("permissions", (townBlock.isChanged()) ? townBlock.getPermissions().toString().replaceAll(",", "#") : ""); tb_hm.put("changed", townBlock.isChanged()); @@ -2697,6 +2822,10 @@ public synchronized boolean saveTownBlock(TownBlock townBlock) { tb_hm.put("districtID", townBlock.getDistrict().getUUID().toString()); else tb_hm.put("districtID", ""); + if (townBlock.hasOutpostObject()) + tb_hm.put("outpostID", townBlock.getOutpostUUID().toString()); + else + tb_hm.put("outpostID", ""); if (townBlock.hasMeta()) tb_hm.put("metadata", serializeMetadata(townBlock)); else @@ -2818,6 +2947,13 @@ public void deleteJail(Jail jail) { DeleteDB("JAILS", jail_hm); } + @Override + public void deleteOutpost(Outpost outpost) { + HashMap outpost_hm = new HashMap<>(); + outpost_hm.put("uuid", outpost.getUUID()); + DeleteDB("OUTPOSTS", outpost_hm); + } + @Override public CompletableFuture> getHibernatedResidentRegistered(UUID uuid) { return CompletableFuture.supplyAsync(() -> { 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 78c55d52934..00bc64203e4 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_OUTPOST_NAME = "outpostName"; private static final String TEAM_PLOT_COST = "plot_cost"; private static final String TEAM_BUILD = "build"; private static final String TEAM_DESTROY = "destroy"; @@ -63,7 +64,7 @@ public static String permHudTestKey() { public static void updatePerms(Player p, WorldCoord worldCoord) { Translator translator = Translator.locale(p); - String build, destroy, switching, item, type, pvp, explosions, firespread, mobspawn, title; + String outpostName, 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. @@ -91,6 +92,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() + ")" : ""); + // Outpost name + outpostName = townBlock.hasOutpostObject() ? DARK_GREEN + "Outpost: " + townBlock.getOutpost().getFormattedName() : ""; + // Plot Type type = townBlock.getType().equals(TownBlockType.RESIDENTIAL) ? " " : townBlock.getType().getName(); @@ -113,6 +117,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_OUTPOST_NAME).setSuffix(outpostName); board.getTeam(TEAM_PLOT_TYPE).setSuffix(type); board.getTeam(TEAM_PLOT_COST).setSuffix(forSale); @@ -156,6 +161,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_OUTPOST_NAME).setSuffix(" "); board.getTeam(TEAM_PLOT_TYPE).setSuffix(" "); board.getTeam(TEAM_PLOT_COST).setSuffix(" "); @@ -197,6 +203,7 @@ public static void toggleOn (Player p) { private static void initializeScoreboard(Translator translator, Scoreboard board) { String PERM_HUD_TITLE = GOLD + ""; + String outpostName_entry = GOLD + ""; 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; @@ -223,6 +230,7 @@ private static void initializeScoreboard(Translator translator, Scoreboard board obj.setDisplaySlot(DisplaySlot.SIDEBAR); obj.setDisplayName(PERM_HUD_TITLE); //register teams + Team outpostName = board.registerNewTeam(TEAM_OUTPOST_NAME); Team keyPlotType = board.registerNewTeam(TEAM_PLOT_TYPE); Team forSaleTitle = board.registerNewTeam(TEAM_PLOT_COST); @@ -243,6 +251,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.) + outpostName.addEntry(outpostName_entry); keyPlotType.addEntry(keyPlotType_entry); forSaleTitle.addEntry(forSale_entry); @@ -264,6 +273,7 @@ private static void initializeScoreboard(Translator translator, Scoreboard board int score = HUDManager.MAX_SCOREBOARD_HEIGHT; //set scores for positioning + obj.getScore(outpostName_entry).setScore(score--); obj.getScore(keyPlotType_entry).setScore(score--); obj.getScore(forSale_entry).setScore(score--); obj.getScore(permsTitle_entry).setScore(score--); 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 5241314bda6..57551e9d3e8 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 @@ -26,6 +26,7 @@ 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.Outpost; import com.palmergames.bukkit.towny.object.PlayerCache; import com.palmergames.bukkit.towny.object.Resident; import com.palmergames.bukkit.towny.object.SpawnType; @@ -238,7 +239,8 @@ public void onTownUnclaimDistrict(TownPreUnclaimEvent event) { /** * Used to warn towns when they're approaching their claim limit, when the - * takeoverclaim feature is enabled, as well as claiming particles. + * takeoverclaim feature is enabled, as well as claiming particles, and to + * add an outpost object to townblocks. * * @param event TownClaimEvent. */ @@ -248,6 +250,16 @@ public void onTownClaim(TownClaimEvent event) { Towny.getPlugin().getScheduler().runAsync(() -> CellSurface.getCellSurface(event.getTownBlock().getWorldCoord()).runClaimingParticleOverSurfaceAtPlayer(event.getResident().getPlayer())); + // Add the outpost object to the newly claimed townblock, if it is part of an outpost's landmass. + if (!event.getTownBlock().hasOutpostObject()) { + Optional outpost = event.getTownBlock().getWorldCoord().getCardinallyAdjacentWorldCoords(false).stream() + .filter(wc -> wc.hasTownBlock() && wc.getTownBlockOrNull().hasOutpostObject()) + .map(wc -> wc.getTownBlockOrNull().getOutpost()) + .findFirst(); + if (outpost.isPresent()) + event.getTownBlock().setOutpostObject(outpost.get()); + } + if (!TownySettings.isOverClaimingAllowingStolenLand()) return; if (event.getTown().availableTownBlocks() <= TownySettings.getTownBlockRatio()) diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/object/Outpost.java b/Towny/src/main/java/com/palmergames/bukkit/towny/object/Outpost.java new file mode 100644 index 00000000000..3c09be21d23 --- /dev/null +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/object/Outpost.java @@ -0,0 +1,87 @@ +package com.palmergames.bukkit.towny.object; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import org.jetbrains.annotations.Nullable; + +import com.palmergames.bukkit.towny.TownyUniverse; +import com.palmergames.bukkit.towny.object.SpawnPoint.SpawnPointType; + +public class Outpost extends ObjectGroup { + private Town town; + private Position spawn; + private Set townblocks = new HashSet<>(); + + public Outpost(UUID id, String name) { + super(id, name); + } + + @Override + public void save() { + TownyUniverse.getInstance().getDataSource().saveOutpost(this); + } + + @Override + public boolean exists() { + return this.town != null && this.town.exists() && this.town.getOutposts().contains(this); + } + + public Town getTown() { + return town; + } + + public void setTown(Town town) { + this.town = town; + } + + public Position getSpawn() { + return spawn; + } + + public void setSpawn(Position spawn) { + // Remove any previously set spawn's particles. + if (this.spawn != null) + TownyUniverse.getInstance().removeSpawnPoint(this.spawn.asLocation()); + + this.spawn = spawn; + TownyUniverse.getInstance().addSpawnPoint(new SpawnPoint(spawn, SpawnPointType.OUTPOST_SPAWN)); + } + + public boolean isOutpostHomeBlock(TownBlock tb) { + return tb.getWorldCoord().equals(WorldCoord.parseWorldCoord(spawn.asLocation())); + } + + public int getNumTownBlocks() { + return townblocks.size(); + } + + public Set getTownblocks() { + return townblocks; + } + + public void addTownblock(TownBlock townblock) { + if (this.town == null) + this.town = townblock.getTownOrNull(); + this.townblocks.add(townblock); + } + + public void removeTownblock(TownBlock townblock) { + if (isOutpostHomeBlock(townblock)) + TownyUniverse.getInstance().removeSpawnPoint(spawn.asLocation()); + + this.townblocks.remove(townblock); + } + + public boolean hasTownBlocks() { + return townblocks.size() > 0; + } + + @Nullable + public TownBlock getSpawnTownBlock() { + if (spawn == null) + return null; + return WorldCoord.parseWorldCoord(spawn.asLocation()).getTownBlockOrNull(); + } +} diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/object/OutpostWorldCoord.java b/Towny/src/main/java/com/palmergames/bukkit/towny/object/OutpostWorldCoord.java new file mode 100644 index 00000000000..ae96f14e9ff --- /dev/null +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/object/OutpostWorldCoord.java @@ -0,0 +1,15 @@ +package com.palmergames.bukkit.towny.object; + +public class OutpostWorldCoord extends WorldCoord { + + final String name; + + public OutpostWorldCoord(String name, WorldCoord coord) { + super(coord); + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/object/Position.java b/Towny/src/main/java/com/palmergames/bukkit/towny/object/Position.java index 0a00b520cb1..6535a2d4c88 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/object/Position.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/object/Position.java @@ -163,4 +163,9 @@ public static Position deserialize(@NotNull final String[] data) throws IllegalA return new Position(world, x, y, z, pitch, yaw); } + + @Override + public String toString() { + return String.format("%s - %s, %s, %s", this.world.getName(), x, y, z); + } } 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 6a197484128..3e8a7dc5fec 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 @@ -1,6 +1,5 @@ package com.palmergames.bukkit.towny.object; -import com.google.common.collect.Lists; import com.palmergames.bukkit.towny.Towny; import com.palmergames.bukkit.towny.TownyAPI; import com.palmergames.bukkit.towny.TownyEconomyHandler; @@ -48,6 +47,7 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; import java.util.ArrayList; import java.util.Arrays; @@ -61,6 +61,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -77,7 +78,7 @@ public class Town extends Government implements TownBlockOwner { private Map enemies = new LinkedHashMap<>(); private final Set trustedResidents = new HashSet<>(); private final Map trustedTowns = new LinkedHashMap<>(); - private final List outpostSpawns = new ArrayList<>(); + private List outposts = new ArrayList<>(); private List jails = null; private HashMap plotGroups = null; private TownBlockTypeCache plotTypeCache = new TownBlockTypeCache(); @@ -893,11 +894,9 @@ public void removeTownBlock(TownBlock townBlock) { if (hasTownBlock(townBlock)) { // Remove the spawn point for this outpost. - if (townBlock.isOutpost() || isAnOutpost(townBlock.getCoord())) { - removeOutpostSpawn(townBlock.getCoord()); - townBlock.setOutpost(false); - townBlock.save(); - } + if (townBlock.hasOutpostObject()) + townBlock.getOutpost().removeTownblock(townBlock); + if (townBlock.isJail()) { removeJail(townBlock.getJail()); } @@ -939,51 +938,25 @@ public void setPermissions(String line) { public TownyPermission getPermissions() { return permissions; } - - public void addOutpostSpawn(Location location) { - addOutpostSpawn(Position.ofLocation(location)); - } /** - * Add or update an outpost spawn for a town. - * Saves the TownBlock if it is not already an Outpost. - * Saves the Town when finished. - * - * @param position Position to set an outpost's spawn point + * @deprecated since 0.100.4.10 with no replacement. + * @param outpostSpawns List of Locations that made up the town's outpostspawns. */ - public void addOutpostSpawn(Position position) { - TownBlock townBlock = position.worldCoord().getTownBlockOrNull(); - if (townBlock == null || !this.equals(townBlock.getTownOrNull())) - return; - - // Remove any potential previous outpost spawn at this location (when run via /t set outpost.) - removeOutpostSpawn(position.worldCoord()); + @Deprecated + public void setOutpostSpawns(List outpostSpawns) { + } - // Set the TownBlock to be an outpost. - if (!townBlock.isOutpost()) { - townBlock.setOutpost(true); - townBlock.save(); - } + public void addOutpostSpawn(Location location) { + TownBlock townBlock = TownyAPI.getInstance().getTownBlock(location); + Outpost outpost = townBlock.hasOutpostObject() + ? townBlock.getOutpost() + : new Outpost(UUID.randomUUID(), !townBlock.getName().isEmpty() ? townBlock.getName() : "UnnamedOutpost" + String.valueOf(outposts.size() + 1)); - // Add to the towns' outpost list. - outpostSpawns.add(position); - - // Add a SpawnPoint so a particle effect is displayed. - TownyUniverse.getInstance().addSpawnPoint(new SpawnPoint(spawn, SpawnPointType.OUTPOST_SPAWN)); - - // Save the town. - this.save(); - } - - /** - * Only to be called from the Loading methods. - * - * @param position Location to set Outpost's spawn point - */ - @ApiStatus.Internal - public void forceAddOutpostSpawn(Position position) { - outpostSpawns.add(position); - TownyUniverse.getInstance().addSpawnPoint(new SpawnPoint(position, SpawnPointType.OUTPOST_SPAWN)); + outpost.setSpawn(Position.ofLocation(location)); + if (outpost.getNumTownBlocks() == 0) + outpost.addTownblock(townBlock); + outpost.save(); } /** @@ -998,20 +971,15 @@ public Location getOutpostSpawn(Integer index) throws TownyException { if (getMaxOutpostSpawn() == 0 && TownySettings.isOutpostsLimitedByLevels()) throw new TownyException(Translation.of("msg_err_town_has_no_outpost_spawns_set")); - return outpostSpawns.get(Math.min(getMaxOutpostSpawn() - 1, Math.max(0, index - 1))).asLocation(); + return outposts.get(Math.min(getMaxOutpostSpawn() - 1, Math.max(0, index - 1))).getSpawn().asLocation(); } public int getMaxOutpostSpawn() { - return outpostSpawns.size(); + return outposts.size(); } public boolean hasOutpostSpawn() { - return !outpostSpawns.isEmpty(); - } - - // Used because (perhaps) some mysql databases do not properly save a townblock's outpost flag. - private boolean isAnOutpost(Coord coord) { - return new ArrayList<>(outpostSpawns).stream().anyMatch(spawn -> spawn.worldCoord().equals(coord)); + return getMaxOutpostSpawn() > 0; } /** @@ -1020,49 +988,71 @@ private boolean isAnOutpost(Coord coord) { * @return List of outpostSpawns */ public List getAllOutpostSpawns() { - return Collections.unmodifiableList(Lists.transform(this.outpostSpawns, Position::asLocation)); + return outposts.stream().filter(o -> o.getSpawn() != null).map(Outpost::getSpawn).map(Position::asLocation).collect(Collectors.toUnmodifiableList()); } /** * @return Similar to {@link #getAllOutpostSpawns()}, but with positions. */ public Collection getOutpostSpawns() { - return Collections.unmodifiableList(this.outpostSpawns); + return outposts.stream().filter(o -> o.getSpawn() != null).map(Outpost::getSpawn).collect(Collectors.toUnmodifiableList()); } - public void removeOutpostSpawn(Coord coord) { - new ArrayList<>(getAllOutpostSpawns()).stream() - .filter(spawn -> Coord.parseCoord(spawn).equals(coord)) - .forEach(spawn -> { - removeOutpostSpawn(spawn); - TownyUniverse.getInstance().removeSpawnPoint(spawn); - }); + public void removeOutpost(Location loc) { + Optional optOutpost = outposts.stream().filter(o -> o.getTownblocks().contains(TownyAPI.getInstance().getTownBlock(loc))).findFirst(); + if (optOutpost.isPresent()) + removeOutpost(optOutpost.get()); } - public void removeOutpostSpawn(Location loc) { - outpostSpawns.remove(Position.ofLocation(loc)); + public List getOutpostNames() { + return getOutposts().stream().map(Outpost::getName).collect(Collectors.toList()); +// List outpostNames = new ArrayList<>(); +// int i = 0; +// for (Location loc : getAllOutpostSpawns()) { +// i++; +// TownBlock tboutpost = TownyAPI.getInstance().getTownBlock(loc); +// +// if (tboutpost == null) { +// removeOutpostSpawn(loc); +// save(); +// continue; +// } +// +// String name = !tboutpost.hasPlotObjectGroup() ? tboutpost.getName() : tboutpost.getPlotObjectGroup().getName(); +// if (!name.isEmpty()) +// outpostNames.add(name); +// else +// outpostNames.add(String.valueOf(i)); +// } +// return outpostNames; } - public List getOutpostNames() { - List outpostNames = new ArrayList<>(); - int i = 0; - for (Location loc : getAllOutpostSpawns()) { - i++; - TownBlock tboutpost = TownyAPI.getInstance().getTownBlock(loc); + @Unmodifiable + public List getOutposts() { + return Collections.unmodifiableList(outposts); + } - if (tboutpost == null) { - removeOutpostSpawn(loc); - save(); - continue; - } + public boolean addOutpost(Outpost outpost) { + return outposts.add(outpost); + } - String name = !tboutpost.hasPlotObjectGroup() ? tboutpost.getName() : tboutpost.getPlotObjectGroup().getName(); - if (!name.isEmpty()) - outpostNames.add(name); - else - outpostNames.add(String.valueOf(i)); - } - return outpostNames; + public boolean removeOutpost(Outpost outpost) { + return outposts.remove(outpost); + } + + public boolean hasOutpost(UUID uuid) { + return !getOutposts().stream().map(Outpost::getUUID).filter(oUUID -> oUUID.equals(uuid)).collect(Collectors.toList()).isEmpty(); + } + + @Nullable + public Outpost getOutpost(UUID uuid) { + Optional outpost = getOutposts().stream().filter(o -> o.getUUID().equals(uuid)).findFirst(); + return outpost.isPresent() ? outpost.get() : null; + } + + public int getMaxAllowedOutpostLandmass() { + // TODO: Real logic here + return 10; } /** @@ -1264,13 +1254,6 @@ public boolean hasValidUUID() { return uuid != null; } - public void setOutpostSpawns(List outpostSpawns) { - this.outpostSpawns.clear(); - - for (Location location : outpostSpawns) - addOutpostSpawn(location); - } - public boolean isAlliedWith(Town othertown) { return CombatUtil.isAlly(this, othertown); } 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 f45bf7272d0..6484aad380f 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 @@ -32,6 +32,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.UUID; public class TownBlock extends TownyObject { @@ -43,7 +44,7 @@ public class TownBlock extends TownyObject { private final WorldCoord worldCoord; private double plotPrice = -1; private boolean taxed = true; - private boolean outpost = false; + private Outpost outpostObject = null; private PlotGroup plotGroup; private District district; private long claimedAt; @@ -296,19 +297,38 @@ public void setChanged(boolean isChanged) { } /** - * @return the outpost + * @return True if this townblock is home to the Outpost's spawn point. */ public boolean isOutpost() { - return outpost; + return hasOutpostObject() && getOutpost().isOutpostHomeBlock(this); } /** * @param outpost the outpost to set */ + @Deprecated public void setOutpost(boolean outpost) { + } + + public Outpost getOutpost() { + return outpostObject; + } + + public UUID getOutpostUUID() { + return outpostObject.getUUID(); + } + + public void setOutpostObject(Outpost outpost) { + this.outpostObject = outpost; + } + + public boolean hasOutpostObject() { + return outpostObject != null; + } - this.outpost = outpost; + public void removeOutpost() { + this.outpostObject = null; } public TownBlockType getType() { diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/tasks/LegacyOutpostConversionTask.java b/Towny/src/main/java/com/palmergames/bukkit/towny/tasks/LegacyOutpostConversionTask.java new file mode 100644 index 00000000000..a6ef3e8c7bb --- /dev/null +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/tasks/LegacyOutpostConversionTask.java @@ -0,0 +1,75 @@ +package com.palmergames.bukkit.towny.tasks; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import com.palmergames.bukkit.towny.Towny; +import com.palmergames.bukkit.towny.TownyAPI; +import com.palmergames.bukkit.towny.TownyMessaging; +import com.palmergames.bukkit.towny.exceptions.TownyException; +import com.palmergames.bukkit.towny.object.Outpost; +import com.palmergames.bukkit.towny.object.Position; +import com.palmergames.bukkit.towny.object.Town; +import com.palmergames.bukkit.towny.object.TownBlock; +import com.palmergames.bukkit.towny.object.WorldCoord; +import com.palmergames.bukkit.towny.utils.BorderUtil; +import com.palmergames.bukkit.towny.utils.BorderUtil.FloodfillResult; + +public class LegacyOutpostConversionTask extends TownyTimerTask { + + final Position pos; + final Town town; + + public LegacyOutpostConversionTask(Towny plugin, Position pos, Town town) { + super(plugin); + this.pos = pos; + this.town = town; + } + + @Override + public void run() { + if (plugin.isError()) + return; + + TownBlock townBlock = TownyAPI.getInstance().getTownBlock(pos.asLocation()); + if (!town.hasTownBlock(townBlock)) { + TownyMessaging.sendErrorMsg(String.format("%s tried to load an outpost located at %s, which is not a TownBlock owned by %s.", town.getName(), pos.toString(), town.getName())); + return; + } + + String outpostName = !townBlock.getName().isEmpty() ? townBlock.getName() : "UnnamedOutpost" + String.valueOf(town.getMaxOutpostSpawn()); + Outpost outpost = new Outpost(UUID.randomUUID(), outpostName); + outpost.setSpawn(pos); + outpost.addTownblock(townBlock); + outpost.save(); + townBlock.setOutpostObject(outpost); + townBlock.save(); + town.addOutpost(outpost); + + WorldCoord coord = townBlock.getWorldCoord(); + FloodfillResult result = null; + try { + result = BorderUtil.getFloodFillableCoordsForOutpostConversion(town, coord); + if (result.type() != BorderUtil.FloodfillResult.Type.SUCCESS) + throw result.feedback() != null ? new TownyException(result.feedback()) : new TownyException(); + else if (result.feedback() != null) + TownyMessaging.sendMsg(result.feedback()); + } catch (TownyException e) { + TownyMessaging.sendMsg(e.getMessage()); + return; + } + + List selection = new ArrayList<>(result.coords()); + for (WorldCoord wc : selection) { + TownBlock tb = wc.getTownBlockOrNull(); + if (tb != null) { + outpost.addTownblock(tb); + tb.setOutpostObject(outpost); + tb.save(); + } + } + + TownyMessaging.sendMsg(String.format("%s imported a legacy outpost located at %s, total size: %s.", town.getName(), pos.toString(), selection.size())); + } +} \ No newline at end of file diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/tasks/TownClaim.java b/Towny/src/main/java/com/palmergames/bukkit/towny/tasks/TownClaim.java index 62c59e2e15f..5278805709a 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/tasks/TownClaim.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/tasks/TownClaim.java @@ -9,6 +9,9 @@ import com.palmergames.bukkit.towny.event.TownClaimEvent; import com.palmergames.bukkit.towny.event.town.TownUnclaimEvent; import com.palmergames.bukkit.towny.exceptions.TownyException; +import com.palmergames.bukkit.towny.object.Outpost; +import com.palmergames.bukkit.towny.object.OutpostWorldCoord; +import com.palmergames.bukkit.towny.object.Position; import com.palmergames.bukkit.towny.object.Town; import com.palmergames.bukkit.towny.object.TownBlock; import com.palmergames.bukkit.towny.object.TownBlockType; @@ -25,6 +28,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.UUID; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -228,9 +232,11 @@ private void townClaim(WorldCoord worldCoord) throws TownyException { townBlock.setTown(town); townBlock.setType(!alreadyClaimed ? townBlock.getType() : TownBlockType.RESIDENTIAL); // Sets the plot permissions to mirror the towns. if (outpost) { - townBlock.setOutpost(true); - town.addOutpostSpawn(outpostLocation); - outpost = false; // Reset so we only flag the first plot as an outpost. + Outpost outpostObject = new Outpost(UUID.randomUUID(), ((OutpostWorldCoord) worldCoord).getName()); + outpostObject.setSpawn(Position.ofLocation(outpostLocation)); + outpostObject.addTownblock(townBlock); + town.addOutpost(outpostObject); + outpostObject.save(); } if (!alreadyClaimed) diff --git a/Towny/src/main/java/com/palmergames/bukkit/towny/utils/BorderUtil.java b/Towny/src/main/java/com/palmergames/bukkit/towny/utils/BorderUtil.java index 67572be011f..bafb88dab1b 100644 --- a/Towny/src/main/java/com/palmergames/bukkit/towny/utils/BorderUtil.java +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/utils/BorderUtil.java @@ -256,6 +256,58 @@ public static boolean allowedMove(Block block, Block blockTo, @Nullable Player p return FloodfillResult.success(valid); } + @ApiStatus.Internal + public static @NotNull FloodfillResult getFloodFillableCoordsForOutpostConversion(final @NotNull Town town, final @NotNull WorldCoord origin) { + final TownyWorld originWorld = origin.getTownyWorld(); + if (originWorld == null) + return FloodfillResult.fail(null); + + if (!origin.hasTownBlock()) + return FloodfillResult.fail(Translatable.of("msg_err_floodfill_not_in_wild")); + + // Filter out any coords not in the same world + final Set coords = new HashSet<>(town.getTownBlockMap().keySet()); + coords.removeIf(coord -> !originWorld.equals(coord.getTownyWorld())); + if (coords.isEmpty()) + return FloodfillResult.fail(null); + + final Set valid = new HashSet<>(); + final Set visited = new HashSet<>(); + + final Queue queue = new LinkedList<>(); + queue.offer(origin); + visited.add(origin); + + while (!queue.isEmpty()) { + if (valid.size() >= town.getMaxAllowedOutpostLandmass()) + return FloodfillResult.success(valid); + + final WorldCoord current = queue.poll(); + valid.add(current); + + for (final int[] direction : DIRECTIONS) { + final int xOffset = direction[0]; + final int zOffset = direction[1]; + + final WorldCoord candidate = current.add(xOffset, zOffset); + + if (visited.contains(candidate)) + continue; + visited.add(candidate); + + final TownBlock townBlock = candidate.getTownBlockOrNull(); + // Fail if we're touching another town or the wilderness. + if (townBlock == null || !town.hasTownBlock(townBlock)) { + continue; + } + + queue.offer(candidate); + } + } + + return FloodfillResult.success(valid); + } + public record FloodfillResult(@NotNull Type type, @Nullable Translatable feedback, @NotNull Collection coords) { public enum Type { SUCCESS, diff --git a/Towny/src/main/resources/lang/en-US.yml b/Towny/src/main/resources/lang/en-US.yml index f7cfab9b13d..01db69a75df 100644 --- a/Towny/src/main/resources/lang/en-US.yml +++ b/Towny/src/main/resources/lang/en-US.yml @@ -1579,6 +1579,7 @@ flatfile_dbg_district_file_missing_town_delete: 'District file missing Town, del 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_outpost_list: 'Loading Outpost list...' flatfile_dbg_loading_nation: 'Loading Nation: %s' flatfile_dbg_loading_nation_list: 'Loading Nation list...' flatfile_dbg_loading_resident: 'Loading Resident: %s' @@ -2615,3 +2616,7 @@ msg_admin_eco_convert_success: "Economy conversion successful" msg_err_mode_does_not_exist: "The mode %s is not recognized." msg_err_not_allowed_to_switch: "You aren't allowed to Switch here." + +status_outpost_name_and_size: 'Townblock is part of Outpost: %s. Outpost has %s Townblocks total.' + +msg_err_plot_already_part_of_outpost_group: "You cannot create a new Outpost within an already existing Outpost." \ No newline at end of file