Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Distance option to prevent new towns to block old towns expansion. #7364

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,15 @@ public enum ConfigNodes {
"# Put in other words: the buffer area around every claim that no other town can claim into.",
"# Does not affect towns which are in the same nation.",
"# This will prevent town encasement to a certain degree."),
CLAIMING_MIN_PLOT_DISTANCE_FROM_OLDER_TOWN_PLOT(
"claiming.distance_rules.min_plot_distance_from_older_town_plot",
"5",
"",
"# The minimum number of plots required between a town and older towns. Must be higher than min_plot_distance_from_town_plot.",
"# Does not affect towns which are in the same nation when min_distances_ignored_for_towns_in_same_nation is set to true.",
"# Does not affect towns which are in the allied when min_distances_ignored_for_towns_in_allied_nation is set to true.",
"# This works exactly as min_plot_distance_from_town_plot except that it only affects towns created newer than already-existing towns.",
"# This will prevent old towns from being unable to expand outwards towards newer towns, which might have tried to claim-block them."),
CLAIMING_MIN_DISTANCE_FROM_TOWN_HOMEBLOCK(
"claiming.distance_rules.min_distance_from_town_homeblock",
"5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2579,6 +2579,10 @@ public static int getMinDistanceFromTownPlotblocks() {
return getInt(ConfigNodes.CLAIMING_MIN_PLOT_DISTANCE_FROM_TOWN_PLOT);
}

public static int getMinDistanceFromOlderTownPlotblocks() {
return getInt(ConfigNodes.CLAIMING_MIN_PLOT_DISTANCE_FROM_OLDER_TOWN_PLOT);
}

public static int getMaxDistanceForTownMerge() {
return getInt(ConfigNodes.GTOWN_SETTINGS_MAX_DISTANCE_FOR_MERGE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.palmergames.bukkit.towny.exceptions.TownyException;
import com.palmergames.bukkit.towny.object.TownyPermission.ActionType;
import com.palmergames.bukkit.towny.object.metadata.CustomDataField;
import com.palmergames.bukkit.towny.utils.CombatUtil;
import com.palmergames.util.MathUtil;

import com.palmergames.util.StringMgmt;
Expand All @@ -29,6 +30,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.function.Predicate;

public class TownyWorld extends TownyObject {
private UUID uuid;
Expand Down Expand Up @@ -878,18 +880,39 @@ public int getMinDistanceFromOtherTownsPlots(Coord key) {
* @return the closest distance to another towns nearest plot.
*/
public int getMinDistanceFromOtherTownsPlots(Coord key, Town homeTown) {
return getMinDistanceFromOtherTownsPlots(key, homeTown, t -> false);
}

/**
* Checks the distance from a another older town's plots.
* It have the same behavior than {@link #getMinDistanceFromOtherTownsPlots(Coord, Town)}
* but it will only consider towns that are older than the homeTown.
*
* @param key - Coord to check from.
* @param homeTown Players town
* @return the closest distance to another older towns nearest plot.
*/
public int getMinDistanceFromOtherOlderTownsPlots(Coord key, Town homeTown) {
return getMinDistanceFromOtherTownsPlots(key, homeTown, t -> t.getRegistered() > homeTown.getRegistered());
}

/**
* Checks the distance from a another town's plots.
*
* @param key - Coord to check from.
* @param homeTown Players town
* @param isIgnorableTown Predicate to filter out towns that should not be considered.
* @return the closest distance to another towns nearest plot.
*/
private int getMinDistanceFromOtherTownsPlots(Coord key, Town homeTown, @NotNull Predicate<Town> isIgnorableTown) {
final int keyX = key.getX();
final int keyZ = key.getZ();

double minSqr = -1;
for (Town town : getTowns().values()) {
if (homeTown != null)
// If the townblock either: the town is the same as homeTown OR
// both towns are in the same nation (and this is set to ignore distance in the config,) skip over the proximity filter.
if (homeTown.getUUID().equals(town.getUUID())
|| (TownySettings.isMinDistanceIgnoringTownsInSameNation() && homeTown.hasNation() && town.hasNation() && town.getNationOrNull().equals(homeTown.getNationOrNull()))
|| (TownySettings.isMinDistanceIgnoringTownsInAlliedNation() && homeTown.isAlliedWith(town)))
continue;
if (homeTown != null && townSkippedByProximityFilter(town, homeTown, isIgnorableTown))
continue;

for (TownBlock b : town.getTownBlocks()) {
if (!b.getWorld().equals(this)) continue;

Expand All @@ -906,8 +929,58 @@ public int getMinDistanceFromOtherTownsPlots(Coord key, Town homeTown) {
}
return minSqr == -1 ? Integer.MAX_VALUE : (int) Math.ceil(Math.sqrt(minSqr));
}



public boolean worldCoordNotTooCloseToOtherTowns(Coord key, Town homeTown) {
final Map<Integer, Predicate<Town>> minDistances = new HashMap<>();
if(TownySettings.getMinDistanceFromTownPlotblocks() > 0){
minDistances.put(TownySettings.getMinDistanceFromTownPlotblocks(), t -> false);
}
if (TownySettings.getMinDistanceFromOlderTownPlotblocks() > TownySettings.getMinDistanceFromTownPlotblocks()){
minDistances.put(TownySettings.getMinDistanceFromOlderTownPlotblocks(), t -> t.getRegistered() > homeTown.getRegistered());
}
final int keyX = key.getX();
final int keyZ = key.getZ();

for(Town town : getTowns().values()){
if (homeTown != null && townSkippedByProximityFilter(town, homeTown, t -> false)) // No skip with the predicate here because it will be done later by the map.
continue;

for (TownBlock b : town.getTownBlocks()) {
if (!b.getWorld().equals(this)) continue;

final int tbX = b.getX();
final int tbZ = b.getZ();

if (keyX == tbX && keyZ == tbZ)
continue;

final double distSqr = MathUtil.distanceSquared((double) tbX - keyX, (double) tbZ - keyZ);
// If there is a town that is too close, return false.
if (minDistances.entrySet().stream().anyMatch(entry -> distSqr >= entry.getKey() && !entry.getValue().test(town)))
return false;
}
}
return true;
}

/**
* Skip over the proximity filter if the town is any one of the following:
* the same as homeTown OR is ignorable for a reason set in the Predicate<Town>
* OR both towns are in the same nation OR both towns are in allied nations
* (and nation/alliednations are set to ignore distance in the config.)
*
* @param town Town we are testing against.
* @param homeTown the Town which is attempting to claim land.
* @param isIgnorableTown Predicate<Town> which tests the town variable to see if it can be ignored by the proximity test.
* @return true if town can claim near to homeTown.
*/
private boolean townSkippedByProximityFilter(Town town, Town homeTown, @NotNull Predicate<Town> isIgnorableTown) {
return homeTown.equals(town)
|| isIgnorableTown.test(town)
|| (TownySettings.isMinDistanceIgnoringTownsInSameNation() && CombatUtil.isSameNation(town, homeTown))
|| (TownySettings.isMinDistanceIgnoringTownsInAlliedNation() && homeTown.isAlliedWith(town));
}

/**
* Returns the distance to the closest townblock
* from the given coord, for the give town.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.palmergames.bukkit.towny.object.TownBlock;
import com.palmergames.bukkit.towny.object.TownBlockOwner;
import com.palmergames.bukkit.towny.object.TownBlockType;
import com.palmergames.bukkit.towny.object.TownyWorld;
import com.palmergames.bukkit.towny.object.Translatable;
import com.palmergames.bukkit.towny.object.WorldCoord;
import com.palmergames.bukkit.util.BiomeUtil;
Expand Down Expand Up @@ -334,13 +335,19 @@ public static List<WorldCoord> filterInvalidProximityTownBlocks(List<WorldCoord>

List<WorldCoord> out = new ArrayList<>();
for (WorldCoord worldCoord : selection)
if (worldCoord.getTownyWorld().getMinDistanceFromOtherTownsPlots(worldCoord, town) >= TownySettings.getMinDistanceFromTownPlotblocks()) {
if (worldCoordNotTooCloseToOtherTowns(town, worldCoord)) {
out.add(worldCoord);
} else {
TownyMessaging.sendDebugMsg("AreaSelectionUtil:filterInvalidProximity - Coord: " + worldCoord + " too close to another town." );
}
return out;
}

private static boolean worldCoordNotTooCloseToOtherTowns(Town town, WorldCoord worldCoord) {
TownyWorld townyWorld = worldCoord.getTownyWorld();
return townyWorld != null
&& townyWorld.worldCoordNotTooCloseToOtherTowns(worldCoord, town);
}

/**
* Returns a list containing only townblocks that can be claimed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public static boolean OutpostTests(Town town, Resident resident, TownyWorld worl
minDistance < TownySettings.getMinDistanceForOutpostsFromPlot())
throw new TownyException(Translatable.of("msg_too_close2", Translatable.of("townblock")));

// Newer towns can be prevented from claiming near to older towns.
if (!isPlotSetOutpost &&
world.getMinDistanceFromOtherOlderTownsPlots(key, town) < TownySettings.getMinDistanceFromOlderTownPlotblocks())
throw new TownyException(Translatable.of("msg_too_close3", Translatable.of("townblock")));

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ public static void allowTownHomeBlockOrThrow(TownyWorld world, Coord key, @Nulla
return;

if (newTown) { // Tests run only when there is a new town involved.
if (TownySettings.getMinDistanceFromTownPlotblocks() > 0 || TownySettings.getNewTownMinDistanceFromTownPlots() > 0) {
if (TownySettings.getMinDistanceFromTownPlotblocks() > 0 ||
TownySettings.getMinDistanceFromOlderTownPlotblocks() > 0 ||
TownySettings.getNewTownMinDistanceFromTownPlots() > 0) {
// Sometimes new towns have special min. distances from other towns.
int minDistance = TownySettings.getNewTownMinDistanceFromTownPlots();
if (minDistance <= 0)
minDistance = TownySettings.getMinDistanceFromTownPlotblocks();
minDistance = Math.max(TownySettings.getMinDistanceFromTownPlotblocks(), TownySettings.getMinDistanceFromOlderTownPlotblocks());

// throws when a new town is being made to close to another town's land.
if (world.getMinDistanceFromOtherTownsPlots(key) < minDistance)
Expand Down Expand Up @@ -86,6 +88,10 @@ public static void allowTownClaimOrThrow(TownyWorld world, WorldCoord townBlockT
if (world.getMinDistanceFromOtherTownsPlots(townBlockToClaim, town) < TownySettings.getMinDistanceFromTownPlotblocks())
throw new TownyException(Translatable.of("msg_too_close2", Translatable.of("townblock")));

// Check distance to other older townblocks.
if (world.getMinDistanceFromOtherOlderTownsPlots(townBlockToClaim, town) < TownySettings.getMinDistanceFromOlderTownPlotblocks())
throw new TownyException(Translatable.of("msg_too_close3", Translatable.of("townblock")));

// Check adjacent claims rules.
testAdjacentClaimsRulesOrThrow(townBlockToClaim, town, outpost);

Expand Down
1 change: 1 addition & 0 deletions Towny/src/main/resources/lang/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,7 @@ msg_save_success: 'Database saved.'
msg_load_success: 'Database loaded.'
msg_err_cannot_afford_to_set_outpost: 'Your town does not have enough funds to set this townblock to an outpost.'
msg_too_close2: 'This area is too close to another town''s %s.'
msg_too_close3: 'This area is too close to %s claimed by a town older than yours.'
homeblock: 'homeblock'
townblock: 'townblock'
outpost: 'outpost'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.palmergames.bukkit.towny.object;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.UUID;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import com.palmergames.bukkit.config.ConfigNodes;
import com.palmergames.bukkit.towny.TownySettings;
import be.seeseemelk.mockbukkit.MockBukkit;

class TownyWorldTest {

@BeforeAll
static void init() {
MockBukkit.getOrCreateMock();
TownySettings.loadDefaultConfig();
// new Towny();
}

@Test
void testTownCreationWithNoOtherTown() {
TownyWorld world = new TownyWorld("world");
Town town = new Town("testTown");
town.setWorld(world);

assertTrue(world.hasTowns());
assertTrue(world.worldCoordNotTooCloseToOtherTowns(new Coord(0, 0), town));
}

@ParameterizedTest
@CsvSource({"0,0, 0,0,0,false", "0,1, 0,0,0,true", "0,1, 1,0,0,false", "0,1, 0,1,0,false", "0,1, 0,0,1,false", "0,2, 1,0,0,true",
"0,2, 0,1,0,true", "0,2, 0,0,1,true"})
void testTownCreationWithOneOtherTown(int x, int z, int minDistanceBetweenHomeblocks, int minPlotDistanceFromTownPlot,
int minPlotDistanceFromOlderTownPlot, boolean expected) {
TownySettings.getConfig().set(ConfigNodes.CLAIMING_MIN_DISTANCE_BETWEEN_HOMEBLOCKS.getRoot(), minDistanceBetweenHomeblocks);
TownySettings.getConfig().set(ConfigNodes.CLAIMING_MIN_PLOT_DISTANCE_FROM_TOWN_PLOT.getRoot(), minPlotDistanceFromTownPlot);
TownySettings.getConfig().set(ConfigNodes.CLAIMING_MIN_PLOT_DISTANCE_FROM_OLDER_TOWN_PLOT.getRoot(),
minPlotDistanceFromOlderTownPlot);
TownyWorld world = new TownyWorld("world");
Town oldTown = new Town("oldTown");
oldTown.setUUID(UUID.randomUUID());
oldTown.setWorld(world);
TownBlock townBlock = new TownBlock(0, 0, world);
// townBlock.setTown(oldTown);
// Upper line throws:
// java.lang.IllegalStateException: Attempted to use getPlugin() while the plugin is null, are you shading Towny? If you do not
// understand this message, join the Towny discord using https://discord.com/invite/gnpVs5m and ask for support.
// at com.palmergames.bukkit.towny.Towny.getPlugin(Towny.java:764)
// at com.palmergames.bukkit.towny.TownyUniverse.(TownyUniverse.java:100)
// at com.palmergames.bukkit.towny.TownyUniverse.getInstance(TownyUniverse.java:106)
// at com.palmergames.bukkit.towny.object.TownBlock.setTown(TownBlock.java:79)
// at com.palmergames.bukkit.towny.object.TownBlock.setTown(TownBlock.java:68)

Town newTown = new Town("newTown");
newTown.setUUID(UUID.randomUUID());
newTown.setWorld(world);


assertEquals(expected, world.worldCoordNotTooCloseToOtherTowns(new Coord(x, z), newTown));
}

}
Loading