Adds multiple home support.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled

This commit is contained in:
2026-09-27 04:15:26 -07:00
parent 013dc6749c
commit a13f512be6
12 changed files with 488 additions and 45 deletions

View File

@@ -44,7 +44,16 @@ Use this checklist for behavior that depends on a real Paper world, player inter
### Commands, permissions, and player state
- Exercise `/metropolis-home-acquire` below and at the configured home limit. Confirm a successful acquisition creates one home and an over-limit request creates none.
- For a new player with no overrides, join and confirm exactly one `h_1_<UUID>` home is created. Rejoin and confirm no additional home is created.
- Configure two `permissionOverrides` tiers in `plugins/Metropolis/config.yml`, then restart. Grant each permission node to a test user and to a test group through the server's permission manager. Confirm the player's effective `plotMultiplier` and `maxPlots` match the granted rule.
- Give one player multiple configured permission nodes with different priorities. Confirm the highest-priority rule controls both values; then give two matching rules the same priority and confirm the last matching YAML entry wins.
- Give a player both a permission tier and a matching `userOverrides` entry. Confirm the username override wins. Remove both overrides from another player and confirm `plot.multiplier` and `plot.maxPerPlayer` are used as defaults.
- Change or revoke a permission while the player is online, then acquire another home. Confirm the next acquisition uses the currently granted tier without requiring a restart.
- With plot multipliers of 1 and 2, acquire homes and inspect their WorldGuard bounds. Confirm size follows the tier, a larger footprint does not overlap an existing home, and the multiplier is applied to the X and Z footprint only once.
- Exercise `/metropolis-home-acquire` below and at each effective home limit. Confirm every successful request creates a distinct next-numbered home, creates none beyond the limit, and does not change other players' homes.
- After acquiring a second home, verify it becomes active: check the player's selected home in `plugins/Metropolis/currentHomes.yml` and use `/metropolis-home-go` to confirm travel to that home. Switch homes with `/metropolis-home-move` and verify the selection changes as expected.
- Restart after acquiring multiple homes. Confirm all home regions remain owned by the player, the active home selection persists, and joining does not allocate another first home.
- Generate a home for an offline player administratively. Confirm permission tiers are not assumed for offline players; the username override or global settings determine plot size and limit.
- Exercise `/metropolis-home-go` with a safe bed spawn inside the home and with no such bed spawn. Confirm teleportation chooses a valid location inside the home and reports a clear failure if no safe location exists.
- Exercise `/metropolis-home-move` for a player with multiple homes. Confirm the selected home persists across reconnect and restart; reject nonexistent home numbers.
- As an administrator, test `/metropolis-home-generate`, `/metropolis-home-evict`, and `/metropolis-home-list` with online and previously seen offline players. Confirm the intended region ownership changes, persistence, and subsequent allocation behavior.
@@ -53,15 +62,17 @@ Use this checklist for behavior that depends on a real Paper world, player inter
## Existing Automated Coverage
Run `./gradlew test` for the server-free JUnit suite. It has 13 tests covering:
Run `./gradlew test` for the server-free JUnit suite. It has 17 tests covering:
- `CuboidTest`: WorldEdit bound conversion, vector ordering/null handling, touching intersections, and point containment.
- `CurrentHomesStoreTest`: UUID-based persistence and resolving a legacy player-name key.
- `PlayerJoinListenerTest`: the welcome bounds message when a home is supplied by a mock.
- `PlayerLookupTest`: case-insensitive full-name matching and rejection of prefix matching.
- `PlayerHomeTest`: UUID-based region identity and display-name retention.
- `HomePermissionOverrideTest`: dotted permission-node parsing, priority and tie resolution, invalid entries, and username/permission/default precedence.
- `HomeNumberAllocatorTest`: choosing the first unused positive home number.
- `MetropolisPlotReserveCommandTest`: parsing six coordinates and rejecting malformed coordinates.
- `MetropolisHomeAcquireTest`: assignment below the home limit and refusal at the limit.
- `MetropolisHomeAcquireTest`: acquisition below the limit and refusal at the limit.
The two opt-in tests in `src/integrationTest` require a running Paper server and RCON. They check that Metropolis appears in `/plugins` and that a coordinate reservation is persisted by WorldGuard. They do not currently verify restart persistence, join allocation, WorldEdit-selection reservations, permissions, teleports, generated blocks, protection behavior, or the other command workflows above. Run them with the environment variables documented in `README.md`; use a disposable server because the reservation test changes its WorldGuard data.

View File

@@ -55,7 +55,23 @@ Copy `.env.example` to `.env` and configure the Paper version/build and server p
## Configuration
`plugins/Metropolis/config.yml` controls plot dimensions and limits, floor and support generation, signs, road width/level/material/clearance, spawn behavior, wall generation, world name, and per-player overrides. The bundled configuration provides the defaults. Metropolis targets Paper 26.2 and 26.3 only; Bukkit and Spigot servers are not supported.
`plugins/Metropolis/config.yml` controls plot dimensions and limits, floor and support generation, signs, road width/level/material/clearance, spawn behavior, wall generation, world name, and per-player overrides. `plot.multiplier` sets the default plot-size multiplier; `plot.maxPerPlayer` sets the default home limit.
Use `permissionOverrides` to assign plot size and home limits through any permission manager. Each permission node maps to a `priority`, `plotMultiplier`, and `maxPlots`; the matching node with the highest priority wins, and the last matching entry in YAML order wins ties. An explicit username entry in `userOverrides` takes precedence over permission rules. If no override matches, the global `plot.*` defaults apply. For example:
```yaml
permissionOverrides:
metropolis.plots.vip:
priority: 10
plotMultiplier: 2
maxPlots: 2
metropolis.plots.elite:
priority: 20
plotMultiplier: 3
maxPlots: 5
```
Assign these permission nodes to groups or individual users in your permission plugin. Bukkit permissions are boolean, so the configured values determine the numeric limits. The bundled configuration provides the defaults. Metropolis targets Paper 26.2 and 26.3 only; Bukkit and Spigot servers are not supported.
## Commands

View File

@@ -62,12 +62,12 @@ Or better yet just use uhome or create your own, integrated that records the hom
# GIT-003 - Add support for multiple homes
**Status:** Ready
**Status:** Done
**Description:**
Support per-player home limits using numbered permission nodes, for example `metropolis.maxhomes.2`. Permission plugins such as LuckPerms can assign these nodes to groups or individual users; when multiple limit nodes are granted, use the highest limit. Keep the configured home limit as the fallback for players without a limit permission.
Add a top-level `permissionOverrides` map keyed by permission node. Each rule defines `priority`, `plotMultiplier`, and `maxPlots`; any permission manager can grant the boolean nodes to groups or individual players. For online players, the matching rule with the highest priority wins, with the last matching entry in YAML order breaking ties. An explicit `userOverrides` entry takes precedence over permission rules. Without either override, use `plot.multiplier` and `plot.maxPerPlayer`. Offline-player generation uses username overrides or global defaults because permissions are only queried for online players.
On join, automatically allocate one home only when the player does not already have a home. Additional homes must be explicitly requested with `/metropolis-home-acquire`, up to the player's effective limit. When a new home is granted, make it the player's active home.
On join, automatically allocate one home only when the player has no existing home, and restore a valid existing home as active if the saved selection is missing. Additional homes must be explicitly requested with `/metropolis-home-acquire`, up to the player's effective limit. Acquisition allocates the first unused positive home number and makes the new home active.
The alternative login allocation policies are tracked separately in MET-023 and MET-024; they are not part of the default policy in this task.

View File

@@ -0,0 +1,41 @@
/*
This file is part of Metropolis.
Metropolis is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Metropolis is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
*/
package com.majinnaibu.bukkitplugins.metropolis;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class HomeNumberAllocator {
private HomeNumberAllocator() {}
public static int nextHomeNumber(List<Plot> plots) {
Set<Integer> allocatedNumbers = new HashSet<>();
for (Plot plot : plots) {
if (plot instanceof PlayerHome home) {
allocatedNumbers.add(home.getNumber());
}
}
int nextHomeNumber = 1;
while (allocatedNumbers.contains(nextHomeNumber)) {
nextHomeNumber++;
}
return nextHomeNumber;
}
}

View File

@@ -0,0 +1,154 @@
/*
This file is part of Metropolis.
Metropolis is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Metropolis is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
*/
package com.majinnaibu.bukkitplugins.metropolis;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.bukkit.configuration.ConfigurationSection;
public final class HomePermissionOverride {
private final String _permission;
private final int _priority;
private final int _plotMultiplier;
private final int _maxPlots;
private HomePermissionOverride(
String permission, int priority, int plotMultiplier, int maxPlots) {
_permission = permission;
_priority = priority;
_plotMultiplier = plotMultiplier;
_maxPlots = maxPlots;
}
public String getPermission() {
return _permission;
}
public int getPriority() {
return _priority;
}
public int getPlotMultiplier() {
return _plotMultiplier;
}
public int getMaxPlots() {
return _maxPlots;
}
public static List<HomePermissionOverride> load(
ConfigurationSection section, Consumer<String> warningLogger) {
if (section == null) {
return List.of();
}
List<HomePermissionOverride> overrides = new ArrayList<>();
collect(section, "", overrides, warningLogger);
return List.copyOf(overrides);
}
private static void collect(
ConfigurationSection section,
String permissionPrefix,
List<HomePermissionOverride> overrides,
Consumer<String> warningLogger) {
for (Map.Entry<String, Object> entry : section.getValues(false).entrySet()) {
String permission =
permissionPrefix.isEmpty() ? entry.getKey() : permissionPrefix + "." + entry.getKey();
Map<?, ?> values = getValues(entry.getValue());
if (values != null && !hasRuleValues(values)) {
collect((ConfigurationSection) entry.getValue(), permission, overrides, warningLogger);
continue;
}
if (permission.isBlank()
|| values == null
|| !(values.get("priority") instanceof Number priority)
|| !(values.get("plotMultiplier") instanceof Number plotMultiplier)
|| !(values.get("maxPlots") instanceof Number maxPlots)
|| priority.intValue() != priority.doubleValue()
|| plotMultiplier.intValue() != plotMultiplier.doubleValue()
|| maxPlots.intValue() != maxPlots.doubleValue()
|| plotMultiplier.intValue() < 1
|| maxPlots.intValue() < 0) {
warningLogger.accept("Ignoring invalid permissionOverrides entry '" + permission + "'.");
continue;
}
overrides.add(
new HomePermissionOverride(
permission, priority.intValue(), plotMultiplier.intValue(), maxPlots.intValue()));
}
}
private static boolean hasRuleValues(Map<?, ?> values) {
return values.containsKey("priority")
|| values.containsKey("plotMultiplier")
|| values.containsKey("maxPlots");
}
public static HomePermissionOverride findMatch(
List<HomePermissionOverride> overrides, Predicate<String> hasPermission) {
HomePermissionOverride match = null;
for (HomePermissionOverride override : overrides) {
if (hasPermission.test(override.getPermission())
&& (match == null || override.getPriority() >= match.getPriority())) {
match = override;
}
}
return match;
}
public static int resolveMaxPlots(
UserOverride userOverride,
List<HomePermissionOverride> overrides,
Predicate<String> hasPermission,
int fallback) {
if (userOverride != null) {
return userOverride.getMaxPlots();
}
HomePermissionOverride match = findMatch(overrides, hasPermission);
return match == null ? fallback : match.getMaxPlots();
}
public static int resolvePlotMultiplier(
UserOverride userOverride,
List<HomePermissionOverride> overrides,
Predicate<String> hasPermission,
int fallback) {
if (userOverride != null) {
return userOverride.getPlotMultiplier();
}
HomePermissionOverride match = findMatch(overrides, hasPermission);
return match == null ? fallback : match.getPlotMultiplier();
}
private static Map<?, ?> getValues(Object value) {
if (value instanceof ConfigurationSection section) {
return section.getValues(false);
}
if (value instanceof Map<?, ?> map) {
return map;
}
return null;
}
}

View File

@@ -85,6 +85,7 @@ public class MetropolisPlugin extends JavaPlugin {
private List<Plot> _occupiedPlots;
private HashMap<UUID, List<Plot>> _ownedPlots;
private HashMap<UUID, UserOverride> _userOverrides;
private List<HomePermissionOverride> _permissionOverrides;
private HashMap<UUID, Integer> _currentHomes;
private PlayerJoinListener _playerJoinListener = null;
@@ -196,10 +197,14 @@ public class MetropolisPlugin extends JavaPlugin {
wallMaterial = safeGetMaterialFromConfig(config, "wall.material");
wallHeight = safeGetIntFromConfig(config, "wall.height");
worldName = safeGetStringFromConfig(config, "worldname");
_maxPlots = safeGetIntFromConfig(config, "plot.multiplier");
_plotMultiplier = safeGetIntFromConfig(config, "plot.maxPerPlayer");
_plotMultiplier = safeGetIntFromConfig(config, "plot.multiplier");
_maxPlots = safeGetIntFromConfig(config, "plot.maxPerPlayer");
buildUserOverrides();
_permissionOverrides =
HomePermissionOverride.load(
config.getConfigurationSection("permissionOverrides"),
warning -> getLogger().warning(warning));
saveConfig();
if (DEBUG) {
@@ -494,9 +499,6 @@ public class MetropolisPlugin extends JavaPlugin {
ProtectedCuboidRegion cuboidRegion = (ProtectedCuboidRegion) region;
if (cuboidRegion.getId().startsWith("h_")) {
PlayerHome home = PlayerHome.get(region);
if (!_currentHomes.containsKey(home.getPlayerId())) {
_currentHomes.put(home.getPlayerId(), home.getNumber());
}
_occupiedPlots.add(home);
addOwnedPlot(home.getPlayerId(), home);
} else if (cuboidRegion.getId().startsWith("r_")) {
@@ -505,7 +507,17 @@ public class MetropolisPlugin extends JavaPlugin {
}
}
for (UUID playerId : _ownedPlots.keySet()) {
if (!_currentHomes.containsKey(playerId)) {
PlayerHome firstHome = getFirstOwnedHome(playerId);
if (firstHome != null) {
_currentHomes.put(playerId, firstHome.getNumber());
}
}
}
size = calculateCitySize();
saveCurrentHomes();
}
private void addOwnedPlot(UUID playerId, Plot plot) {
@@ -527,20 +539,27 @@ public class MetropolisPlugin extends JavaPlugin {
public PlayerHome getPlayerHome(Player player) {
PlayerHome home = null;
int homeNumber = _currentHomes.getOrDefault(player.getUniqueId(), 1);
UUID playerId = player.getUniqueId();
int homeNumber = _currentHomes.getOrDefault(playerId, 1);
String regionName = String.format("h_%d_%s", homeNumber, player.getUniqueId());
ProtectedRegion homeRegion = regionManager.getRegion(regionName);
if (homeRegion == null) {
PlayerHome existingHome = getOwnedHome(player.getUniqueId(), homeNumber);
PlayerHome existingHome = getOwnedHome(playerId, homeNumber);
if (existingHome != null) {
existingHome.setPlayerName(player.getName());
return existingHome;
}
existingHome = getFirstOwnedHome(playerId);
if (existingHome != null) {
existingHome.setPlayerName(player.getName());
setHome(playerId, existingHome.getNumber());
return existingHome;
}
if (DEBUG) {
getLogger().info(String.format("Creating home for player %s", player.getName()));
}
home = generateHome(player);
home = generateHome(player, 1);
} else {
home = new PlayerHome(homeRegion);
home.setPlayerName(player.getName());
@@ -891,7 +910,7 @@ public class MetropolisPlugin extends JavaPlugin {
BlockVector3 bv =
BlockVector3.at(
(col + plotMultiplier) * gridSizeX * plotMultiplier - 1,
(col + plotMultiplier) * gridSizeX - 1,
(level + 1 /*plotMultiplier*/) * gridSizeY - 1,
(row + plotMultiplier) * gridSizeZ - 1);
getLogger().info(String.format("getGridMax (%d, %d, %d)", bv.x(), bv.y(), bv.z()));
@@ -924,10 +943,14 @@ public class MetropolisPlugin extends JavaPlugin {
}
public PlayerHome generateHome(OfflinePlayer player) {
int homeNumber = _currentHomes.getOrDefault(player.getUniqueId(), 1);
return generateHome(player, homeNumber);
}
public PlayerHome generateHome(OfflinePlayer player, int homeNumber) {
UUID playerId = player.getUniqueId();
String playerName = player.getName() == null ? playerId.toString() : player.getName();
int homeNumber = _currentHomes.getOrDefault(playerId, 1);
int multiplier = getPlotMultiplier(playerId);
int multiplier = getPlotMultiplier(player);
if (DEBUG) {
getLogger().info(String.format("Generating home for %s", playerName));
@@ -993,7 +1016,7 @@ public class MetropolisPlugin extends JavaPlugin {
homeNumber,
newHomeRegion.getMinimumPoint(),
newHomeRegion.getMaximumPoint());
_currentHomes.putIfAbsent(playerId, homeNumber);
_currentHomes.put(playerId, homeNumber);
saveCurrentHomes();
createRoads(homeCuboid);
@@ -1130,10 +1153,28 @@ public class MetropolisPlugin extends JavaPlugin {
}
}
public int getMaxPlots(Player player) {
return HomePermissionOverride.resolveMaxPlots(
_userOverrides.get(player.getUniqueId()),
_permissionOverrides,
player::hasPermission,
_maxPlots);
}
public void assignPlot(OfflinePlayer player) {
generateHome(player);
}
public PlayerHome acquireHome(OfflinePlayer player) {
List<Plot> ownedPlots = _ownedPlots.getOrDefault(player.getUniqueId(), List.of());
int nextHomeNumber = HomeNumberAllocator.nextHomeNumber(ownedPlots);
PlayerHome home = generateHome(player, nextHomeNumber);
if (home != null) {
setHome(player.getUniqueId(), home.getNumber());
}
return home;
}
private int getPlotMultiplier(UUID playerId) {
if (_userOverrides.containsKey(playerId)) {
return _userOverrides.get(playerId).getPlotMultiplier();
@@ -1142,6 +1183,21 @@ public class MetropolisPlugin extends JavaPlugin {
}
}
private int getPlotMultiplier(OfflinePlayer player) {
if (player instanceof Player onlinePlayer) {
return HomePermissionOverride.resolvePlotMultiplier(
_userOverrides.get(player.getUniqueId()),
_permissionOverrides,
onlinePlayer::hasPermission,
_plotMultiplier);
}
return HomePermissionOverride.resolvePlotMultiplier(
_userOverrides.get(player.getUniqueId()),
_permissionOverrides,
ignored -> false,
_plotMultiplier);
}
public Plot getPlot(String string) {
/**
* string is the name of the region to get a plot for
@@ -1229,6 +1285,18 @@ public class MetropolisPlugin extends JavaPlugin {
return null;
}
private PlayerHome getFirstOwnedHome(UUID playerId) {
List<Plot> plots = _ownedPlots.get(playerId);
if (plots == null) {
return null;
}
return plots.stream()
.filter(PlayerHome.class::isInstance)
.map(PlayerHome.class::cast)
.min(java.util.Comparator.comparingInt(PlayerHome::getNumber))
.orElse(null);
}
private void saveCurrentHomes() {
File homesFile = new File(getDataFolder(), "currentHomes.yml");
try {

View File

@@ -40,12 +40,11 @@ public class MetropolisHomeAcquire implements CommandExecutor {
Player player = (Player) sender;
if (_plugin.getNumPlots(player.getUniqueId()) >= _plugin.getMaxPlots(player.getUniqueId())) {
if (_plugin.getNumPlots(player.getUniqueId()) >= _plugin.getMaxPlots(player)) {
sender.sendMessage("You cannot have any more plots");
return false;
}
_plugin.assignPlot(player);
return true;
return _plugin.acquireHome(player) != null;
}
}

View File

@@ -1,19 +0,0 @@
Goal: Have 1 plot size for mods and 1 plot size for normals.
Implementation:
1. Set the plotSize in the config to the larger plot size there are other possible sizes but powers of 2 work really well so lets give mods a 64x64 plot. Roads are recomended but will eat into the plot on all sides so the roadsize can be ignored for this example.
2. Start the server and run /metropolis-home-generate <username> for each mod.
3. If you have not completed a full ring then run /metropolis-home-generate <test n> where n starts at 1 and increases until you have filled a full ring. This will reserve those plots for later or anyone who happens to have that username, but that can be dealth with later.
4. Stop the server and change the plotSize to a smaller number like 32.
5. Restart the server and let your users log in.
Notes:
All users will be assigned a small plot when they first connect. When you make them a mod just delete their current region and change the name of one of the reserved home regions to h_<their username>.
/region remove h_jonnynewadmin
/region select test_1
/region remove test_1
/region define h_johnnynewadmin
JohnnyNewAdmin's old home will become available for resettlement. If you want to copy johnny's old blocks into his new home region I'd suggest WorldEdit.
This process including optionally copying the blocks will be an upcoming feature. Probably in 0.5.x and maybe in 0.6.
If you later run out of larger plots you can just set the size back to the larger number and let more generate. Moving and evictions will be coming soon and make this easier.
You can manually just create a region for a user before they login named like h_<username> if you want a specific user to have a really big space as well.

View File

@@ -0,0 +1,41 @@
/*
This file is part of Metropolis.
Metropolis is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Metropolis is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
*/
package com.majinnaibu.bukkitplugins.metropolis;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import java.util.UUID;
import com.sk89q.worldedit.math.BlockVector3;
import org.junit.jupiter.api.Test;
class HomeNumberAllocatorTest {
@Test
void choosesTheFirstUnusedPositiveHomeNumber() {
UUID playerId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");
PlayerHome first =
new PlayerHome(playerId, "PlayerName", 1, BlockVector3.ZERO, BlockVector3.ZERO);
PlayerHome third =
new PlayerHome(playerId, "PlayerName", 3, BlockVector3.ZERO, BlockVector3.ZERO);
assertEquals(1, HomeNumberAllocator.nextHomeNumber(List.of()));
assertEquals(2, HomeNumberAllocator.nextHomeNumber(List.of(first, third)));
}
}

View File

@@ -0,0 +1,130 @@
/*
This file is part of Metropolis.
Metropolis is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Metropolis is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
*/
package com.majinnaibu.bukkitplugins.metropolis;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import org.junit.jupiter.api.Test;
class HomePermissionOverrideTest {
@Test
void selectsTheHighestPriorityAndUsesLastEntryForTies() throws InvalidConfigurationException {
YamlConfiguration config = new YamlConfiguration();
config.loadFromString(
"""
permissionOverrides:
metropolis.plots.vip:
priority: 10
plotMultiplier: 2
maxPlots: 2
metropolis.plots.elite:
priority: 20
plotMultiplier: 3
maxPlots: 5
metropolis.plots.staff:
priority: 20
plotMultiplier: 4
maxPlots: 8
""");
List<String> warnings = new ArrayList<>();
List<HomePermissionOverride> overrides =
HomePermissionOverride.load(
config.getConfigurationSection("permissionOverrides"), warnings::add);
HomePermissionOverride match =
HomePermissionOverride.findMatch(
overrides,
Set.of("metropolis.plots.vip", "metropolis.plots.elite", "metropolis.plots.staff")
::contains);
assertEquals(3, overrides.size());
assertEquals("metropolis.plots.staff", match.getPermission());
assertEquals(4, match.getPlotMultiplier());
assertEquals(8, match.getMaxPlots());
assertEquals(List.of(), warnings);
}
@Test
void preservesDottedPermissionNamesAndSkipsInvalidEntries() throws InvalidConfigurationException {
YamlConfiguration config = new YamlConfiguration();
config.loadFromString(
"""
permissionOverrides:
metropolis.plots.vip:
priority: 3
plotMultiplier: 2
maxPlots: 4
metropolis.plots.invalid:
plotMultiplier: 2
maxPlots: 4
""");
List<String> warnings = new ArrayList<>();
List<HomePermissionOverride> overrides =
HomePermissionOverride.load(
config.getConfigurationSection("permissionOverrides"), warnings::add);
HomePermissionOverride match =
HomePermissionOverride.findMatch(overrides, "metropolis.plots.vip"::equals);
assertEquals(1, overrides.size());
assertEquals("metropolis.plots.vip", overrides.get(0).getPermission());
assertSame(overrides.get(0), match);
assertNull(HomePermissionOverride.findMatch(overrides, ignored -> false));
assertEquals(1, warnings.size());
}
@Test
void resolvesUserOverrideBeforePermissionAndGlobalFallback()
throws InvalidConfigurationException {
YamlConfiguration config = new YamlConfiguration();
config.set("permissionOverrides.metropolis.plots.vip.priority", 10);
config.set("permissionOverrides.metropolis.plots.vip.plotMultiplier", 2);
config.set("permissionOverrides.metropolis.plots.vip.maxPlots", 4);
List<HomePermissionOverride> overrides =
HomePermissionOverride.load(
config.getConfigurationSection("permissionOverrides"), ignored -> {});
UserOverride userOverride = new UserOverride("PlayerName", 3, 6);
assertEquals(
6,
HomePermissionOverride.resolveMaxPlots(
userOverride, overrides, "metropolis.plots.vip"::equals, 1));
assertEquals(
3,
HomePermissionOverride.resolvePlotMultiplier(
userOverride, overrides, "metropolis.plots.vip"::equals, 1));
assertEquals(
4,
HomePermissionOverride.resolveMaxPlots(null, overrides, "metropolis.plots.vip"::equals, 1));
assertEquals(
2,
HomePermissionOverride.resolvePlotMultiplier(
null, overrides, "metropolis.plots.vip"::equals, 1));
assertEquals(1, HomePermissionOverride.resolveMaxPlots(null, overrides, ignored -> false, 1));
assertEquals(
1, HomePermissionOverride.resolvePlotMultiplier(null, overrides, ignored -> false, 1));
}
}

View File

@@ -27,6 +27,7 @@ import static org.mockito.Mockito.when;
import java.util.UUID;
import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin;
import com.majinnaibu.bukkitplugins.metropolis.PlayerHome;
import org.bukkit.command.Command;
import org.bukkit.entity.Player;
@@ -40,12 +41,13 @@ class MetropolisHomeAcquireTest {
UUID playerId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");
when(player.getUniqueId()).thenReturn(playerId);
when(plugin.getNumPlots(playerId)).thenReturn(0);
when(plugin.getMaxPlots(playerId)).thenReturn(1);
when(plugin.getMaxPlots(player)).thenReturn(1);
when(plugin.acquireHome(player)).thenReturn(mock(PlayerHome.class));
MetropolisHomeAcquire executor = new MetropolisHomeAcquire(plugin);
assertTrue(executor.onCommand(player, mock(Command.class), "acquire", new String[0]));
verify(plugin).assignPlot(player);
verify(plugin).acquireHome(player);
}
@Test
@@ -55,11 +57,11 @@ class MetropolisHomeAcquireTest {
UUID playerId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");
when(player.getUniqueId()).thenReturn(playerId);
when(plugin.getNumPlots(playerId)).thenReturn(1);
when(plugin.getMaxPlots(playerId)).thenReturn(1);
when(plugin.getMaxPlots(player)).thenReturn(1);
MetropolisHomeAcquire executor = new MetropolisHomeAcquire(plugin);
assertFalse(executor.onCommand(player, mock(Command.class), "acquire", new String[0]));
verify(plugin, never()).assignPlot(player);
verify(plugin, never()).acquireHome(player);
}
}