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 c3895c8b9f..b07cd97b94 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/db/TownyFlatFileSource.java b/Towny/src/main/java/com/palmergames/bukkit/towny/db/TownyFlatFileSource.java index 5ecde06b0b..bbbc0b09dd 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 @@ -35,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; @@ -926,11 +927,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 { - // TODO: Load legacy Outposts + 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()); } 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 64acb4929a..7d5b13038a 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 @@ -33,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; @@ -1105,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 { - // TODO: handle loading of legacy outposts + 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()); } 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 0000000000..2888d1ea02 --- /dev/null +++ b/Towny/src/main/java/com/palmergames/bukkit/towny/tasks/LegacyOutpostConversionTask.java @@ -0,0 +1,61 @@ +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)) + return; + + 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.getLocalizedMessage()); + } + + List selection = new ArrayList<>(result.coords()); + Outpost outpost = new Outpost(UUID.randomUUID(), townBlock.getName() != "" ? townBlock.getName() : String.valueOf(town.getMaxOutpostSpawn() + 1)); + outpost.addTownblock(townBlock); + for (WorldCoord wc : selection) { + TownBlock tb = wc.getTownBlockOrNull(); + if (tb != null) + outpost.addTownblock(tb); + } + outpost.save(); + } +} \ No newline at end of file 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 67572be011..cb703fc6e9 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,76 @@ 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); + + int minX = origin.getX(); + int maxX = origin.getX(); + int minZ = origin.getZ(); + int maxZ = origin.getZ(); + + // Establish a min and max X & Z to avoid possibly looking very far + for (final WorldCoord coord : coords) { + minX = Math.min(minX, coord.getX()); + maxX = Math.max(maxX, coord.getX()); + minZ = Math.min(minZ, coord.getZ()); + maxZ = Math.max(maxZ, coord.getZ()); + } + + 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 (!coords.contains(candidate) && (candidate.getX() >= maxX || candidate.getX() <= minX || candidate.getZ() >= maxZ || candidate.getZ() <= minZ)) { + return FloodfillResult.oob(); + } + + final TownBlock townBlock = candidate.getTownBlockOrNull(); + + // Fail if we're touching another town + if (townBlock == null || townBlock.hasTown() && !town.equals(townBlock.getTownOrNull())) { + continue; + } + + if (!visited.contains(candidate) && !coords.contains(candidate)) { + queue.offer(candidate); + visited.add(candidate); + } + } + } + + return FloodfillResult.success(valid); + } + public record FloodfillResult(@NotNull Type type, @Nullable Translatable feedback, @NotNull Collection coords) { public enum Type { SUCCESS,