Skip to content
Merged
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ commits to the fight, and defeats every monster the lure draws in.
- **Lure raids** — an InteractibleFurniture lure opens a join window, then
releases a paced wave; defeating the full wave clears the infestation.
- **Committed parties** — a committed player who leaves the province takes
damage until they opt out of the fight. Losing the whole party
damage until they opt out of the fight. Rivers and lakes next to the
province's land still count as inside. Losing the whole party
fails the lure, bystanders who stay are warned and damaged, and logged-out
players get a grace period. A player still away when it ends no longer holds
the lure, and that character dies when it next logs in.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public final class Cache {
public static int minPlayerDistance = 16;
public static int logoutGraceSeconds = 300;
public static double deserterDamage = 2.0;
public static int riverReach = 16;
public static double hologramViewRange = 96;
public static Set<String> skipTerrains = new HashSet<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@ private void tickOutsideCommitted(Infestation infestation) {
if (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR) {
continue;
}
if (Provinces.at(player) == infestation.getProvinceId() || notAnswerable(infestation, player)) {
if (Provinces.within(player, infestation.getProvinceId()) || notAnswerable(infestation, player)) {
continue;
}
actionBar(player, bar);
Expand All @@ -674,7 +674,7 @@ public boolean leaveLure(Player player) {
player.sendMessage(Messages.get("lure-left"));
}
found = true;
boolean inProvince = Provinces.at(player) == infestation.getProvinceId();
boolean inProvince = Provinces.within(player, infestation.getProvinceId());
// Leaving cannot forgive a death owed for abandoning the fight before.
keepOwedDeath(infestation, id);
infestation.getCommitted().remove(id);
Expand Down Expand Up @@ -804,7 +804,7 @@ private void failLure(Infestation infestation) {
loc.getWorld().playSound(loc, Sound.ENTITY_WITHER_SPAWN, 0.4f, 1.4f);
}
for (Player player : Bukkit.getOnlinePlayers()) {
if (Provinces.at(player) == provinceId || committed.contains(player.getUniqueId())) {
if (Provinces.within(player, provinceId) || committed.contains(player.getUniqueId())) {
player.sendMessage(Messages.get("lure-fail"));
}
}
Expand Down Expand Up @@ -839,7 +839,7 @@ private void victory(Infestation infestation) {
loc.getWorld().playSound(loc, Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, 1.1f);
}
for (Player player : Bukkit.getOnlinePlayers()) {
if (Provinces.at(player) == provinceId) {
if (Provinces.within(player, provinceId)) {
player.sendMessage(Messages.get("lure-victory"));
}
}
Expand Down Expand Up @@ -1412,6 +1412,9 @@ public void onLeaveProvince(PlayerProvinceLeaveEvent event) {
if (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR) {
return;
}
if (Provinces.within(player, infestation.getProvinceId())) {
return;
}
player.sendMessage(Messages.get("lure-outside"));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public boolean loadSafe(File configFile) {
Cache.minPlayerDistance = Math.max(0, config.getInt("min-player-distance", 16));
Cache.logoutGraceSeconds = Math.max(1, config.getInt("logout-grace-seconds", 300));
Cache.deserterDamage = Math.max(0.5, config.getDouble("deserter-damage", 2.0));
Cache.riverReach = Math.clamp(config.getInt("river-reach", 16), 0, 64);
Cache.hologramViewRange = Math.max(16, config.getDouble("hologram-view-range", 96));

Set<String> skip = new HashSet<>();
Expand Down
43 changes: 38 additions & 5 deletions src/main/java/net/tfminecraft/infestations/utils/Provinces.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,50 @@ public static int at(Location location) {
if (location == null) {
return UNKNOWN;
}
SimpleFactions sf = SimpleFactions.getInstance();
if (sf == null) {
return UNKNOWN;
}
ProvinceGrid grid = sf.getProvinceGrid();
ProvinceGrid grid = grid();
if (grid == null) {
return UNKNOWN;
}
return grid.getAt(location.getBlockX(), location.getBlockZ());
}

/**
* Whether a player is inside a province for a lure: on its land, or in a river or lake
* ({@code WATER} province) with its land within {@link Cache#riverReach} blocks. Rivers are
* provinces of their own, so a river cutting through a province would otherwise count as
* leaving it.
*/
public static boolean within(Player player, int provinceId) {
int at = at(player);
if (at == provinceId) {
return true;
}
return isWater(at) && near(player.getLocation(), provinceId, Cache.riverReach);
}

private static boolean near(Location location, int provinceId, int reach) {
ProvinceGrid grid = grid();
if (grid == null) {
return false;
}
int x = location.getBlockX();
int z = location.getBlockZ();
int reachSquared = reach * reach;
for (int dx = -reach; dx <= reach; dx++) {
for (int dz = -reach; dz <= reach; dz++) {
if (dx * dx + dz * dz <= reachSquared && grid.getAt(x + dx, z + dz) == provinceId) {
return true;
}
}
}
return false;
}

private static ProvinceGrid grid() {
SimpleFactions sf = SimpleFactions.getInstance();
return sf == null ? null : sf.getProvinceGrid();
}

public static Terrain terrain(int provinceId) {
if (provinceId <= 0) {
return null;
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ logout-grace-seconds: 300
# Hearts are 2 health. Applied each second to uncommitted players who stay
# after a lure starts, and to committed players who leave the province.
deserter-damage: 2
# Rivers and lakes are provinces of their own. A player in one still counts
# as inside a lure's province while that province's land is within this many
# blocks (0 to 64). 0 counts every river as outside.
river-reach: 16
hologram-view-range: 96

skip-terrains:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@
assertTrue(deadline >= before + Cache.logoutGraceSeconds * 1000L);
assertEquals(Map.of(owner, bob.toString()), previous.getLogoutCharacter());
assertEquals(Map.of(owner, 1234L), previous.getLogoutGraceUntil());
Bukkit.getPluginManager().callEvent(new PlayerQuitEvent(player, net.kyori.adventure.text.Component.empty()));

Check warning on line 142 in src/test/java/net/tfminecraft/infestations/InfestationsServicesTest.java

View workflow job for this annotation

GitHub Actions / build

PlayerQuitEvent(org.bukkit.entity.Player,net.kyori.adventure.text.Component) in org.bukkit.event.player.PlayerQuitEvent has been deprecated and marked for removal
assertEquals(deadline, current.getLogoutGraceUntil().get(owner));
}
}
Expand Down Expand Up @@ -257,7 +257,7 @@
manager.characterReturned(player);
Bukkit.getPluginManager().callEvent(new CharacterSessionStartEvent(player, session));
Bukkit.getPluginManager().callEvent(new PlayerJoinEvent(player, net.kyori.adventure.text.Component.empty()));
Bukkit.getPluginManager().callEvent(new PlayerQuitEvent(player, net.kyori.adventure.text.Component.empty()));

Check warning on line 260 in src/test/java/net/tfminecraft/infestations/InfestationsServicesTest.java

View workflow job for this annotation

GitHub Actions / build

PlayerQuitEvent(org.bukkit.entity.Player,net.kyori.adventure.text.Component) in org.bukkit.event.player.PlayerQuitEvent has been deprecated and marked for removal
assertFalse(manager.leaveLure(player));
verify(player, never()).damage(anyDouble());
assertEquals(Map.of(owner, Long.MAX_VALUE), grace.getLogoutGraceUntil());
Expand Down Expand Up @@ -365,6 +365,34 @@
Cache.skipTerrains = Set.of(); assertTrue(Provinces.validLand(7));
}
}
@Test void riversWithinReachOfTheProvinceLandCountAsInside() {
int previousReach = Cache.riverReach;
try (var sf = mockStatic(SimpleFactions.class); var rest = mockStatic(RestServer.class)) {
Player player = mock(Player.class); when(player.getLocation()).thenReturn(new Location(null, 100, 64, 200));
rest.when(() -> RestServer.getProvince(player)).thenReturn(7);
assertTrue(Provinces.within(player, 7)); assertFalse(Provinces.within(null, 7));
rest.when(() -> RestServer.getProvince(player)).thenReturn(9);
assertFalse(Provinces.within(player, 7));
SimpleFactions factions = mock(SimpleFactions.class); sf.when(SimpleFactions::getInstance).thenReturn(factions);
ProvinceManager manager = mock(ProvinceManager.class); when(factions.getProvinceManager()).thenReturn(manager);
Province river = mock(Province.class); when(manager.get(9)).thenReturn(river);
when(river.getTerrain()).thenReturn(Terrain.WATER);
assertFalse(Provinces.within(player, 7));
ProvinceGrid grid = mock(ProvinceGrid.class); when(factions.getProvinceGrid()).thenReturn(grid);
when(grid.getAt(anyInt(), anyInt())).thenReturn(9);
Cache.riverReach = 16;
assertFalse(Provinces.within(player, 7));
when(grid.getAt(100, 216)).thenReturn(7);
assertTrue(Provinces.within(player, 7));
when(grid.getAt(100, 216)).thenReturn(9); when(grid.getAt(116, 216)).thenReturn(7);
assertFalse(Provinces.within(player, 7));
when(grid.getAt(100, 201)).thenReturn(7);
Cache.riverReach = 0; assertFalse(Provinces.within(player, 7));
Cache.riverReach = 1; assertTrue(Provinces.within(player, 7));
when(river.getTerrain()).thenReturn(Terrain.SEA);
assertFalse(Provinces.within(player, 7));
} finally { Cache.riverReach = previousReach; }
}
@Test void terrainNamesUseStableLocale() {
Locale previous = Locale.getDefault();
try (var sf = mockStatic(SimpleFactions.class)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class InfestationManagerTest {
private final Map<PersistentDataContainer, Integer> tags = new IdentityHashMap<>();
private final Map<PersistentDataContainer, String> kinds = new IdentityHashMap<>();
private final Map<Object, Integer> provinces = new IdentityHashMap<>();
private final Map<Player, Integer> riverBanks = new IdentityHashMap<>();
private final Map<UUID, Player> online = new HashMap<>();
private final Map<String, Object> savedCache = new HashMap<>();
private Infestations previousPlugin;
Expand Down Expand Up @@ -181,6 +182,11 @@ class InfestationManagerTest {
groups.when(() -> GroupLoader.allowsTerrain(anyString(), anyInt())).thenReturn(true);
provinceApi.when(() -> Provinces.at(any(Player.class))).thenAnswer(i -> provinces.getOrDefault(i.getArgument(0), -2));
provinceApi.when(() -> Provinces.at(any(Location.class))).thenReturn(1);
provinceApi.when(() -> Provinces.within(any(Player.class), anyInt())).thenAnswer(i -> {
int province = i.getArgument(1);
return provinces.getOrDefault(i.getArgument(0), -2) == province
|| Objects.equals(riverBanks.get(i.<Player>getArgument(0)), province);
});
provinceApi.when(() -> Provinces.validLand(anyInt())).thenAnswer(i -> (int) i.getArgument(0) > 0);
planner.when(() -> SpawnPlanner.clearOfPlayers(any(), anyDouble())).thenReturn(true);
planner.when(() -> SpawnPlanner.anchor(any())).thenReturn(new Location(world, 5.5, 65, 5.5));
Expand Down Expand Up @@ -1183,8 +1189,23 @@ private static CharacterDeathEvent died(UUID owner, String character) {
verify(p, never()).damage(anyDouble()); assertTrue(lure.getDeathOnLogin().contains(id));
}

@Test void riversWithinReachKeepOnlyCommittedPlayersInside() throws Exception {
Infestation active = lure(1, LurePhase.ACTIVE);
Player wading = player(10), bystander = player(10), farRiver = player(10);
riverBanks.put(wading, 1); riverBanks.put(bystander, 1); riverBanks.put(farRiver, 2);
active.getCommitted().add(wading.getUniqueId()); active.getCommitted().add(farRiver.getUniqueId());
atTick(100); call("tickOutsideCommitted", active); call("warnNonJoiners", active);
verify(wading, never()).damage(anyDouble()); verify(wading, never()).sendMessage("lure-outside");
verify(farRiver).damage(2.0); verify(farRiver).sendMessage("lure-outside");
verify(bystander, never()).damage(anyDouble()); verify(bystander, never()).sendMessage("lure-nonjoiner");
PlayerProvinceLeaveEvent leave = mock(PlayerProvinceLeaveEvent.class);
when(leave.getPlayer()).thenReturn(wading); when(leave.getProvinceId()).thenReturn(1);
manager.onLeaveProvince(leave);
verify(wading, never()).sendMessage("lure-outside");
}

@Test void provinceEventsWarnOnlyRelevantParticipants() {
Player p = player(1); PlayerProvinceLeaveEvent leave = mock(PlayerProvinceLeaveEvent.class);
Player p = player(2); PlayerProvinceLeaveEvent leave = mock(PlayerProvinceLeaveEvent.class);
when(leave.getPlayer()).thenReturn(p); when(leave.getProvinceId()).thenReturn(1);
manager.onLeaveProvince(leave); Infestation state = infestation(1); manager.onLeaveProvince(leave);
state.setPhase(LurePhase.JOINING); manager.onLeaveProvince(leave);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,17 @@ void emptyConfigurationRestoresDocumentedDefaultsWithoutAliasingConstantTables()
assertEquals(16, Cache.minPlayerDistance);
assertEquals(300, Cache.logoutGraceSeconds);
assertEquals(2.0, Cache.deserterDamage);
assertEquals(16, Cache.riverReach);
assertEquals(96.0, Cache.hologramViewRange);
assertEquals(Set.of("water", "sea"), Cache.skipTerrains);
}

@Test
void riverReachIsCappedSoTheGridScanStaysSmall() throws Exception {
new ConfigLoader().load(file("river-reach: 10000"));
assertEquals(64, Cache.riverReach);
}

@Test
void completeConfigClampsUnsafeRangesAndNormalizesTerrainNames() throws Exception {
assertTrue(new ConfigLoader().loadSafe(file("""
Expand All @@ -125,6 +132,7 @@ void completeConfigClampsUnsafeRangesAndNormalizesTerrainNames() throws Exceptio
min-player-distance: -1
logout-grace-seconds: 0
deserter-damage: 0
river-reach: -1
hologram-view-range: 0
skip-terrains: ['', 'FOREST', null]
""")));
Expand All @@ -144,6 +152,7 @@ void completeConfigClampsUnsafeRangesAndNormalizesTerrainNames() throws Exceptio
assertEquals(0, Cache.minPlayerDistance);
assertEquals(1, Cache.logoutGraceSeconds);
assertEquals(0.5, Cache.deserterDamage);
assertEquals(0, Cache.riverReach);
assertEquals(16.0, Cache.hologramViewRange);
assertEquals(Set.of("forest"), Cache.skipTerrains);
}
Expand Down