diff --git a/Human-Tests.md b/Human-Tests.md index 39cc0b6..7186232 100644 --- a/Human-Tests.md +++ b/Human-Tests.md @@ -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_` 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. diff --git a/README.md b/README.md index 79bebf0..a0eb7a1 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/TODO.md b/TODO.md index e1c985b..ce78a4c 100644 --- a/TODO.md +++ b/TODO.md @@ -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. diff --git a/src/main/java/com/majinnaibu/bukkitplugins/metropolis/HomeNumberAllocator.java b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/HomeNumberAllocator.java new file mode 100644 index 0000000..f373458 --- /dev/null +++ b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/HomeNumberAllocator.java @@ -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 . +*/ + +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 plots) { + Set 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; + } +} diff --git a/src/main/java/com/majinnaibu/bukkitplugins/metropolis/HomePermissionOverride.java b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/HomePermissionOverride.java new file mode 100644 index 0000000..28bf69e --- /dev/null +++ b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/HomePermissionOverride.java @@ -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 . +*/ + +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 load( + ConfigurationSection section, Consumer warningLogger) { + if (section == null) { + return List.of(); + } + + List overrides = new ArrayList<>(); + collect(section, "", overrides, warningLogger); + return List.copyOf(overrides); + } + + private static void collect( + ConfigurationSection section, + String permissionPrefix, + List overrides, + Consumer warningLogger) { + for (Map.Entry 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 overrides, Predicate 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 overrides, + Predicate 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 overrides, + Predicate 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; + } +} diff --git a/src/main/java/com/majinnaibu/bukkitplugins/metropolis/MetropolisPlugin.java b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/MetropolisPlugin.java index 3358a3d..80fc7a6 100644 --- a/src/main/java/com/majinnaibu/bukkitplugins/metropolis/MetropolisPlugin.java +++ b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/MetropolisPlugin.java @@ -85,6 +85,7 @@ public class MetropolisPlugin extends JavaPlugin { private List _occupiedPlots; private HashMap> _ownedPlots; private HashMap _userOverrides; + private List _permissionOverrides; private HashMap _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 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 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 { diff --git a/src/main/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisHomeAcquire.java b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisHomeAcquire.java index 6f0d08b..ef69c0e 100644 --- a/src/main/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisHomeAcquire.java +++ b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisHomeAcquire.java @@ -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; } } diff --git a/src/main/resources/Examples/Multiple Plot Sizes.txt b/src/main/resources/Examples/Multiple Plot Sizes.txt deleted file mode 100644 index b1093b9..0000000 --- a/src/main/resources/Examples/Multiple Plot Sizes.txt +++ /dev/null @@ -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 for each mod. - 3. If you have not completed a full ring then run /metropolis-home-generate 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_. - /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_ if you want a specific user to have a really big space as well. \ No newline at end of file diff --git a/src/main/resources/Examples/Multiple Plots.txt b/src/main/resources/Examples/Multiple Plots.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/test/java/com/majinnaibu/bukkitplugins/metropolis/HomeNumberAllocatorTest.java b/src/test/java/com/majinnaibu/bukkitplugins/metropolis/HomeNumberAllocatorTest.java new file mode 100644 index 0000000..3fed293 --- /dev/null +++ b/src/test/java/com/majinnaibu/bukkitplugins/metropolis/HomeNumberAllocatorTest.java @@ -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 . +*/ + +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))); + } +} diff --git a/src/test/java/com/majinnaibu/bukkitplugins/metropolis/HomePermissionOverrideTest.java b/src/test/java/com/majinnaibu/bukkitplugins/metropolis/HomePermissionOverrideTest.java new file mode 100644 index 0000000..7b88a52 --- /dev/null +++ b/src/test/java/com/majinnaibu/bukkitplugins/metropolis/HomePermissionOverrideTest.java @@ -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 . +*/ + +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 warnings = new ArrayList<>(); + + List 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 warnings = new ArrayList<>(); + + List 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 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)); + } +} diff --git a/src/test/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisHomeAcquireTest.java b/src/test/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisHomeAcquireTest.java index d953ede..8fea159 100644 --- a/src/test/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisHomeAcquireTest.java +++ b/src/test/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisHomeAcquireTest.java @@ -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); } }