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

@@ -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);
}
}