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 e1e76e729a4..a2a89829cd2 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 @@ -39,6 +39,7 @@ public class PermHUD { 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_OUTPOST_NAME = "outpostName"; 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"; @@ -65,7 +66,7 @@ public static String permHudTestKey() { public static void updatePerms(Player p, WorldCoord worldCoord) { Translator translator = Translator.locale(p); - String districtName, plotName, build, destroy, switching, item, type, pvp, explosions, firespread, mobspawn, title; + String districtName, outpostName, 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. @@ -94,8 +95,11 @@ public static void updatePerms(Player p, WorldCoord worldCoord) { title = GOLD + owner.getName() + (townBlock.hasResident() ? " (" + townBlock.getTownOrNull().getName() + ")" : ""); // District name - districtName = townBlock.hasDistrict() ? townBlock.getDistrict().getFormattedName() : ""; - + districtName = townBlock.hasDistrict() ? DARK_GREEN + "District: " + townBlock.getDistrict().getFormattedName() : ""; + + // Outpost name + outpostName = townBlock.hasOutpostObject() ? DARK_GREEN + "Outpost: " + townBlock.getOutpost().getFormattedName() : ""; + // Plot Type type = townBlock.getType().equals(TownBlockType.RESIDENTIAL) ? " " : townBlock.getType().getName(); @@ -125,6 +129,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_OUTPOST_NAME).setSuffix(outpostName); board.getTeam(TEAM_PLOT_NAME).setSuffix(plotName); board.getTeam(TEAM_PLOT_TYPE).setSuffix(type); board.getTeam(TEAM_PLOT_COST).setSuffix(forSale); @@ -170,6 +175,7 @@ private static void clearPerms (Player p) { try { board.getObjective(HUD_OBJECTIVE).setDisplayName(HUDManager.check(getFormattedWildernessName(p.getWorld()))); board.getTeam(TEAM_DISTRICT_NAME).setSuffix(" "); + board.getTeam(TEAM_OUTPOST_NAME).setSuffix(" "); board.getTeam(TEAM_PLOT_NAME).setSuffix(" "); board.getTeam(TEAM_PLOT_TYPE).setSuffix(" "); board.getTeam(TEAM_PLOT_COST).setSuffix(" "); @@ -212,7 +218,8 @@ public static void toggleOn (Player p) { private static void initializeScoreboard(Translator translator, Scoreboard board) { String PERM_HUD_TITLE = GOLD + ""; - String districtName_entry = ""; + String districtName_entry = GOLD + ""; + String outpostName_entry = GOLD + ""; 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; @@ -241,6 +248,7 @@ private static void initializeScoreboard(Translator translator, Scoreboard board obj.setDisplayName(PERM_HUD_TITLE); //register teams Team districtName = board.registerNewTeam(TEAM_DISTRICT_NAME); + Team outpostName = board.registerNewTeam(TEAM_OUTPOST_NAME); Team plotName = board.registerNewTeam(TEAM_PLOT_NAME); Team keyPlotType = board.registerNewTeam(TEAM_PLOT_TYPE); Team forSaleTitle = board.registerNewTeam(TEAM_PLOT_COST); @@ -263,6 +271,7 @@ private static void initializeScoreboard(Translator translator, Scoreboard board //add each team as an entry (this sets the prefix to each line of the HUD.) districtName.addEntry(districtName_entry); + outpostName.addEntry(outpostName_entry); plotName.addEntry(plotName_entry); keyPlotType.addEntry(keyPlotType_entry); forSaleTitle.addEntry(forSale_entry); @@ -283,24 +292,26 @@ private static void initializeScoreboard(Translator translator, Scoreboard board keyFriend.addEntry(keyNation_entry); keyAlly.addEntry(keyAlly_entry); + int i = 18; //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); - obj.getScore(permsTitle_entry).setScore(13); - obj.getScore(build_entry).setScore(12); - obj.getScore(destroy_entry).setScore(11); - obj.getScore(switching_entry).setScore(10); - obj.getScore(item_entry).setScore(9); - obj.getScore(pvp_entry).setScore(8); - obj.getScore(explosions_entry).setScore(7); - obj.getScore(firespread_entry).setScore(6); - obj.getScore(mobspawn_entry).setScore(5); - obj.getScore(keyTitle_entry).setScore(4); - obj.getScore(keyResident_entry).setScore(3); - obj.getScore(keyNation_entry).setScore(2); - obj.getScore(keyAlly_entry).setScore(1); + obj.getScore(districtName_entry).setScore(i--); + obj.getScore(outpostName_entry).setScore(i--); + obj.getScore(plotName_entry).setScore(i--); + obj.getScore(keyPlotType_entry).setScore(i--); + obj.getScore(forSale_entry).setScore(i--); + obj.getScore(permsTitle_entry).setScore(i--); + obj.getScore(build_entry).setScore(i--); + obj.getScore(destroy_entry).setScore(i--); + obj.getScore(switching_entry).setScore(i--); + obj.getScore(item_entry).setScore(i--); + obj.getScore(pvp_entry).setScore(i--); + obj.getScore(explosions_entry).setScore(i--); + obj.getScore(firespread_entry).setScore(i--); + obj.getScore(mobspawn_entry).setScore(i--); + obj.getScore(keyTitle_entry).setScore(i--); + obj.getScore(keyResident_entry).setScore(i--); + obj.getScore(keyNation_entry).setScore(i--); + obj.getScore(keyAlly_entry).setScore(i--); } private static String prettyMoney(double price) { 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 9b56fa8cd57..0424dcbd84d 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' @@ -2613,3 +2614,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