From 75a4ddd59054583fb6e2f836d87fd4a91ad68eb6 Mon Sep 17 00:00:00 2001 From: Remy Duijsens Date: Tue, 4 Aug 2026 11:41:16 +0200 Subject: [PATCH 01/35] Make Capacity groups canonical --- .../config/CapacityConfigValidator.java | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfigValidator.java b/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfigValidator.java index a517cad9..b5bb1942 100644 --- a/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfigValidator.java +++ b/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfigValidator.java @@ -1,9 +1,7 @@ package nl.hauntedmc.proxyfeatures.features.capacity.config; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Set; /** Strict structural validation shared by config loading and command mutations. */ public final class CapacityConfigValidator { @@ -20,7 +18,6 @@ public static void validate(CapacityConfig config) { validateLimit(new CapacityConfig.Limit( config.unknownCapacity(), config.unknownReservedSlots()), "unknown servers"); - Set assignedServers = new HashSet<>(); Map membership = new HashMap<>(); for (var entry : config.groups().entrySet()) { String name = requiredName(entry.getKey(), "group"); @@ -31,10 +28,11 @@ public static void validate(CapacityConfig config) { validateLimit(rule.limit(), "group " + name); for (String rawServer : rule.servers()) { String server = requiredName(rawServer, "server"); - if (!assignedServers.add(server)) { - throw new IllegalArgumentException("Server belongs to multiple groups: " + server); + String previous = membership.putIfAbsent(server, name); + if (previous != null) { + throw new IllegalArgumentException("Server " + server + + " belongs to multiple groups: " + previous + " and " + name); } - membership.put(server, name); } } @@ -45,14 +43,10 @@ public static void validate(CapacityConfig config) { throw new IllegalArgumentException("Server key and name differ: " + name); } validateLimit(rule.limit(), "server " + name); - String declaredGroup = rule.group().isBlank() ? "" : requiredName(rule.group(), "group"); - if (!declaredGroup.isBlank() && !config.groups().containsKey(declaredGroup)) { - throw new IllegalArgumentException("Server " + name - + " references unknown group " + declaredGroup); - } - if (!membership.getOrDefault(name, "").equals(declaredGroup)) { - throw new IllegalArgumentException("Server " + name - + " has inconsistent explicit and group-list membership."); + String expectedGroup = membership.getOrDefault(name, ""); + if (!expectedGroup.equals(rule.group())) { + throw new IllegalArgumentException("Resolved group for server " + name + + " does not match groups.*.servers membership."); } } } From 22849ba5f716ae94584d4a6d839758a355bbad55 Mon Sep 17 00:00:00 2001 From: Remy Duijsens Date: Tue, 4 Aug 2026 11:41:42 +0200 Subject: [PATCH 02/35] Reduce Capacity config mutations --- .../capacity/config/CapacityConfigEditor.java | 267 +++--------------- 1 file changed, 42 insertions(+), 225 deletions(-) diff --git a/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfigEditor.java b/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfigEditor.java index 146d193c..125e4175 100644 --- a/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfigEditor.java +++ b/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfigEditor.java @@ -1,192 +1,61 @@ package nl.hauntedmc.proxyfeatures.features.capacity.config; -import nl.hauntedmc.proxyfeatures.api.capacity.CapacityState; - import java.time.Duration; -import java.util.ArrayList; import java.util.LinkedHashMap; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.UnaryOperator; -/** Pure immutable Capacity configuration mutations used by the administration command. */ +/** Pure immutable mutations for the small operational Capacity command surface. */ public final class CapacityConfigEditor { private CapacityConfigEditor() { } public static CapacityConfig proxyLimit(CapacityConfig current, int capacity, int reserved) { return checked(copy(current, strictLimit(capacity, reserved, "proxy"), current.gameplay(), - current.groups(), current.servers(), current.unknownCountsTowardGameplay(), - current.unknownQueueable(), current.unknownCapacity(), current.unknownReservedSlots(), - current.leaseTtl(), current.loginReservationTtl(), current.reconciliationInterval(), - current.maintenanceSyncInterval(), current.queueOnFull(), current.failOpenOnInternalError(), - current.restartReturnUsesReservedSlots())); + current.groups(), current.servers())); } public static CapacityConfig gameplayLimit(CapacityConfig current, int capacity, int reserved) { return checked(copy(current, current.proxy(), strictLimit(capacity, reserved, "gameplay"), - current.groups(), current.servers(), current.unknownCountsTowardGameplay(), - current.unknownQueueable(), current.unknownCapacity(), current.unknownReservedSlots(), - current.leaseTtl(), current.loginReservationTtl(), current.reconciliationInterval(), - current.maintenanceSyncInterval(), current.queueOnFull(), current.failOpenOnInternalError(), - current.restartReturnUsesReservedSlots())); + current.groups(), current.servers())); } public static CapacityConfig groupLimit(CapacityConfig current, String groupName, int capacity, int reserved) { String group = requiredName(groupName, "group"); - Map groups = new LinkedHashMap<>(current.groups()); - CapacityConfig.GroupRule existing = groups.get(group); - groups.put(group, new CapacityConfig.GroupRule(group, - strictLimit(capacity, reserved, "group " + group), - existing == null ? List.of() : existing.servers())); - return checked(withGroupsAndServers(current, groups, current.servers())); - } - - public static CapacityConfig addGroupServer(CapacityConfig current, String groupName, - String serverName) { - String group = requiredName(groupName, "group"); - String server = requiredName(serverName, "server"); - if (!current.groups().containsKey(group)) { - throw new IllegalArgumentException("Unknown group: " + group); - } - Map groups = moveGroupMembership(current, group, server); - Map servers = new LinkedHashMap<>(current.servers()); - CapacityConfig.ServerRule explicit = servers.get(server); - if (explicit != null) { - servers.put(server, new CapacityConfig.ServerRule(server, explicit.limit(), group, - explicit.countsTowardGameplay(), explicit.queueable(), explicit.initialState())); - } - return checked(withGroupsAndServers(current, groups, servers)); - } - - public static CapacityConfig removeGroupServer(CapacityConfig current, String groupName, - String serverName) { - String group = requiredName(groupName, "group"); - String server = requiredName(serverName, "server"); - CapacityConfig.GroupRule existing = current.groups().get(group); - if (existing == null) throw new IllegalArgumentException("Unknown group: " + group); - Map groups = new LinkedHashMap<>(current.groups()); - List members = new ArrayList<>(existing.servers()); - members.remove(server); - groups.put(group, new CapacityConfig.GroupRule(group, existing.limit(), members)); - Map servers = new LinkedHashMap<>(current.servers()); - CapacityConfig.ServerRule explicit = servers.get(server); - if (explicit != null && explicit.group().equals(group)) { - servers.put(server, new CapacityConfig.ServerRule(server, explicit.limit(), "", - explicit.countsTowardGameplay(), explicit.queueable(), explicit.initialState())); - } - return checked(withGroupsAndServers(current, groups, servers)); - } - - public static CapacityConfig deleteGroup(CapacityConfig current, String groupName) { - String group = requiredName(groupName, "group"); CapacityConfig.GroupRule existing = current.groups().get(group); if (existing == null) throw new IllegalArgumentException("Unknown group: " + group); - boolean referenced = !existing.servers().isEmpty() || current.servers().values().stream() - .anyMatch(server -> server.group().equals(group)); - if (referenced) throw new IllegalArgumentException("Group is still used: " + group); Map groups = new LinkedHashMap<>(current.groups()); - groups.remove(group); - return checked(withGroupsAndServers(current, groups, current.servers())); + groups.put(group, new CapacityConfig.GroupRule( + group, + strictLimit(capacity, reserved, "group " + group), + existing.servers() + )); + return checked(copy(current, current.proxy(), current.gameplay(), groups, current.servers())); } public static CapacityConfig serverLimit(CapacityConfig current, String serverName, int capacity, int reserved) { return checked(updateServer(current, serverName, rule -> new CapacityConfig.ServerRule( - rule.name(), strictLimit(capacity, reserved, "server " + rule.name()), rule.group(), - rule.countsTowardGameplay(), rule.queueable(), rule.initialState()))); - } - - public static CapacityConfig serverGroup(CapacityConfig current, String serverName, - String groupName) { - String server = requiredName(serverName, "server"); - String group = normalizeNone(groupName); - if (!group.isBlank() && !current.groups().containsKey(group)) { - throw new IllegalArgumentException("Unknown group: " + group); - } - Map groups = moveGroupMembership(current, group, server); - Map servers = new LinkedHashMap<>(current.servers()); - CapacityConfig.ServerRule existing = current.server(server); - servers.put(server, new CapacityConfig.ServerRule(server, existing.limit(), group, - existing.countsTowardGameplay(), existing.queueable(), existing.initialState())); - return checked(withGroupsAndServers(current, groups, servers)); - } - - public static CapacityConfig serverGameplay(CapacityConfig current, String serverName, boolean value) { - return checked(updateServer(current, serverName, rule -> new CapacityConfig.ServerRule( - rule.name(), rule.limit(), rule.group(), value, rule.queueable(), rule.initialState()))); + rule.name(), + strictLimit(capacity, reserved, "server " + rule.name()), + rule.group(), + rule.countsTowardGameplay(), + rule.queueable(), + rule.initialState() + ))); } public static CapacityConfig serverQueueable(CapacityConfig current, String serverName, boolean value) { return checked(updateServer(current, serverName, rule -> new CapacityConfig.ServerRule( - rule.name(), rule.limit(), rule.group(), rule.countsTowardGameplay(), value, - rule.initialState()))); - } - - public static CapacityConfig serverInitialState(CapacityConfig current, String serverName, - CapacityState state) { - if (state == null) throw new IllegalArgumentException("State is required."); - return checked(updateServer(current, serverName, rule -> new CapacityConfig.ServerRule( - rule.name(), rule.limit(), rule.group(), rule.countsTowardGameplay(), rule.queueable(), state))); - } - - public static CapacityConfig deleteServer(CapacityConfig current, String serverName) { - String server = requiredName(serverName, "server"); - if (!current.servers().containsKey(server)) { - throw new IllegalArgumentException("Unknown explicit server rule: " + server); - } - Map servers = new LinkedHashMap<>(current.servers()); - servers.remove(server); - Map groups = moveGroupMembership(current, "", server); - return checked(withGroupsAndServers(current, groups, servers)); - } - - public static CapacityConfig unknownLimit(CapacityConfig current, int capacity, int reserved) { - CapacityConfig.Limit limit = strictLimit(capacity, reserved, "unknown servers"); - return checked(copy(current, current.proxy(), current.gameplay(), current.groups(), current.servers(), - current.unknownCountsTowardGameplay(), current.unknownQueueable(), limit.capacity(), - limit.reservedSlots(), current.leaseTtl(), current.loginReservationTtl(), - current.reconciliationInterval(), current.maintenanceSyncInterval(), current.queueOnFull(), - current.failOpenOnInternalError(), current.restartReturnUsesReservedSlots())); - } - - public static CapacityConfig unknownGameplay(CapacityConfig current, boolean value) { - return checked(copy(current, current.proxy(), current.gameplay(), current.groups(), current.servers(), - value, current.unknownQueueable(), current.unknownCapacity(), current.unknownReservedSlots(), - current.leaseTtl(), current.loginReservationTtl(), current.reconciliationInterval(), - current.maintenanceSyncInterval(), current.queueOnFull(), current.failOpenOnInternalError(), - current.restartReturnUsesReservedSlots())); - } - - public static CapacityConfig unknownQueueable(CapacityConfig current, boolean value) { - return checked(copy(current, current.proxy(), current.gameplay(), current.groups(), current.servers(), - current.unknownCountsTowardGameplay(), value, current.unknownCapacity(), - current.unknownReservedSlots(), current.leaseTtl(), current.loginReservationTtl(), - current.reconciliationInterval(), current.maintenanceSyncInterval(), current.queueOnFull(), - current.failOpenOnInternalError(), current.restartReturnUsesReservedSlots())); - } - - public static CapacityConfig option(CapacityConfig current, String option, String rawValue) { - String key = requiredName(option, "option").replace('_', '-'); - return switch (key) { - case "lease-ttl" -> withTimings(current, duration(rawValue), current.loginReservationTtl(), - current.reconciliationInterval(), current.maintenanceSyncInterval()); - case "login-ttl" -> withTimings(current, current.leaseTtl(), duration(rawValue), - current.reconciliationInterval(), current.maintenanceSyncInterval()); - case "reconciliation" -> withTimings(current, current.leaseTtl(), - current.loginReservationTtl(), duration(rawValue), current.maintenanceSyncInterval()); - case "maintenance-sync" -> withTimings(current, current.leaseTtl(), - current.loginReservationTtl(), current.reconciliationInterval(), duration(rawValue)); - case "queue-on-full" -> withBehavior(current, bool(rawValue), - current.failOpenOnInternalError(), current.restartReturnUsesReservedSlots()); - case "fail-open" -> withBehavior(current, current.queueOnFull(), bool(rawValue), - current.restartReturnUsesReservedSlots()); - case "restart-reserved" -> withBehavior(current, current.queueOnFull(), - current.failOpenOnInternalError(), bool(rawValue)); - default -> throw new IllegalArgumentException("Unknown option: " + option); - }; + rule.name(), + rule.limit(), + rule.group(), + rule.countsTowardGameplay(), + value, + rule.initialState() + ))); } public static Duration duration(String raw) { @@ -228,70 +97,32 @@ private static CapacityConfig updateServer(CapacityConfig current, String server String server = requiredName(serverName, "server"); Map servers = new LinkedHashMap<>(current.servers()); servers.put(server, edit.apply(current.server(server))); - return withGroupsAndServers(current, current.groups(), servers); - } - - private static Map moveGroupMembership( - CapacityConfig current, String targetGroup, String server) { - Map groups = new LinkedHashMap<>(); - current.groups().forEach((name, rule) -> { - List members = new ArrayList<>(rule.servers()); - members.remove(server); - if (name.equals(targetGroup)) members.add(server); - groups.put(name, new CapacityConfig.GroupRule(name, rule.limit(), members)); - }); - return groups; - } - - private static CapacityConfig withGroupsAndServers(CapacityConfig current, - Map groups, - Map servers) { - return copy(current, current.proxy(), current.gameplay(), groups, servers, - current.unknownCountsTowardGameplay(), current.unknownQueueable(), current.unknownCapacity(), - current.unknownReservedSlots(), current.leaseTtl(), current.loginReservationTtl(), - current.reconciliationInterval(), current.maintenanceSyncInterval(), current.queueOnFull(), - current.failOpenOnInternalError(), current.restartReturnUsesReservedSlots()); - } - - private static CapacityConfig withTimings(CapacityConfig current, Duration leaseTtl, - Duration loginTtl, Duration reconciliation, - Duration maintenanceSync) { - return checked(copy(current, current.proxy(), current.gameplay(), current.groups(), current.servers(), - current.unknownCountsTowardGameplay(), current.unknownQueueable(), current.unknownCapacity(), - current.unknownReservedSlots(), leaseTtl, loginTtl, reconciliation, maintenanceSync, - current.queueOnFull(), current.failOpenOnInternalError(), - current.restartReturnUsesReservedSlots())); - } - - private static CapacityConfig withBehavior(CapacityConfig current, boolean queueOnFull, - boolean failOpen, boolean restartReserved) { - return checked(copy(current, current.proxy(), current.gameplay(), current.groups(), current.servers(), - current.unknownCountsTowardGameplay(), current.unknownQueueable(), current.unknownCapacity(), - current.unknownReservedSlots(), current.leaseTtl(), current.loginReservationTtl(), - current.reconciliationInterval(), current.maintenanceSyncInterval(), queueOnFull, failOpen, - restartReserved)); + return copy(current, current.proxy(), current.gameplay(), current.groups(), servers); } private static CapacityConfig copy(CapacityConfig current, CapacityConfig.Limit proxy, CapacityConfig.Limit gameplay, Map groups, - Map servers, - boolean unknownGameplay, - boolean unknownQueueable, - int unknownCapacity, - int unknownReserved, - Duration leaseTtl, - Duration loginTtl, - Duration reconciliation, - Duration maintenanceSync, - boolean queueOnFull, - boolean failOpen, - boolean restartReserved) { + Map servers) { if (current == null) throw new IllegalArgumentException("Current configuration is required."); - return new CapacityConfig(proxy, gameplay, groups, servers, unknownGameplay, unknownQueueable, - unknownCapacity, unknownReserved, leaseTtl, loginTtl, reconciliation, maintenanceSync, - queueOnFull, failOpen, restartReserved); + return new CapacityConfig( + proxy, + gameplay, + groups, + servers, + current.unknownCountsTowardGameplay(), + current.unknownQueueable(), + current.unknownCapacity(), + current.unknownReservedSlots(), + current.leaseTtl(), + current.loginReservationTtl(), + current.reconciliationInterval(), + current.maintenanceSyncInterval(), + current.queueOnFull(), + current.failOpenOnInternalError(), + current.restartReturnUsesReservedSlots() + ); } private static CapacityConfig checked(CapacityConfig config) { @@ -306,20 +137,6 @@ private static CapacityConfig.Limit strictLimit(int capacity, int reserved, Stri return new CapacityConfig.Limit(capacity, reserved); } - private static boolean bool(String raw) { - if (raw == null) throw new IllegalArgumentException("Boolean is required."); - return switch (raw.trim().toLowerCase(Locale.ROOT)) { - case "true", "yes", "on", "1" -> true; - case "false", "no", "off", "0" -> false; - default -> throw new IllegalArgumentException("Expected true or false, got: " + raw); - }; - } - - private static String normalizeNone(String value) { - String normalized = CapacityConfig.normalize(value); - return normalized.equals("none") ? "" : normalized; - } - private static String requiredName(String value, String type) { String normalized = CapacityConfig.normalize(value); if (normalized.isBlank()) throw new IllegalArgumentException(type + " is required."); From 723f75cf86aa37b0a4c63ad40b0139df389d0294 Mon Sep 17 00:00:00 2001 From: Remy Duijsens Date: Tue, 4 Aug 2026 11:42:20 +0200 Subject: [PATCH 03/35] Derive Capacity groups from group members --- .../capacity/config/CapacityConfig.java | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfig.java b/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfig.java index f09ce0e3..5fc4af5b 100644 --- a/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfig.java +++ b/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfig.java @@ -43,14 +43,12 @@ public record CapacityConfig( "proxyfeatures.feature.capacity.admin.state"; public static final String ADMIN_CONFIG_PERMISSION = "proxyfeatures.feature.capacity.admin.config"; - public static final String ADMIN_DANGEROUS_PERMISSION = - "proxyfeatures.feature.capacity.admin.dangerous"; public CapacityConfig { proxy = proxy == null ? new Limit(0, 0) : proxy; gameplay = gameplay == null ? new Limit(0, 0) : gameplay; groups = immutableGroups(groups); - servers = immutableServers(servers); + servers = immutableServers(servers, groups); unknownCapacity = Math.max(0, unknownCapacity); unknownReservedSlots = Math.max(0, Math.min(unknownCapacity, unknownReservedSlots)); leaseTtl = positive(leaseTtl, Duration.ofSeconds(15)); @@ -83,6 +81,7 @@ public record GroupRule(String name, Limit limit, List servers) { } } + /** Effective server policy. Group membership is derived exclusively from {@link GroupRule#servers()}. */ public record ServerRule( String name, Limit limit, @@ -122,7 +121,7 @@ public static CapacityConfig load(Capacity feature) { servers.put(name, new ServerRule( name, limit(node, 0, 0), - stringValue(node.get("group").as(String.class, ""), ""), + "", booleanValue(node.get("counts_toward_gameplay").as(Boolean.class, true), true), booleanValue(node.get("queueable").as(Boolean.class, true), true), parseState(stringValue(node.get("initial_state").as(String.class, "OPEN"), "OPEN")) @@ -158,15 +157,10 @@ public ServerRule server(String serverName) { } ServerRule explicit = servers.get(normalized); if (explicit != null) return explicit; - String group = groups.values().stream() - .filter(rule -> rule.servers().contains(normalized)) - .map(GroupRule::name) - .findFirst() - .orElse(""); return new ServerRule( normalized, new Limit(unknownCapacity, unknownReservedSlots), - group, + groupFor(groups, normalized), unknownCountsTowardGameplay, unknownQueueable, CapacityState.OPEN @@ -203,7 +197,9 @@ private static Map immutableGroups(Map sou return Collections.unmodifiableMap(result); } - private static Map immutableServers(Map source) { + private static Map immutableServers( + Map source, + Map groups) { Map result = new LinkedHashMap<>(); if (source != null) { source.entrySet().stream() @@ -211,12 +207,28 @@ private static Map immutableServers(Map .forEach(entry -> { ServerRule rule = entry.getValue(); if (rule == null || rule.name().isBlank()) return; - result.put(normalize(entry.getKey()), rule); + String key = normalize(entry.getKey()); + result.put(key, new ServerRule( + rule.name(), + rule.limit(), + groupFor(groups, key), + rule.countsTowardGameplay(), + rule.queueable(), + rule.initialState() + )); }); } return Collections.unmodifiableMap(result); } + private static String groupFor(Map groups, String server) { + return groups.values().stream() + .filter(rule -> rule.servers().contains(server)) + .map(GroupRule::name) + .findFirst() + .orElse(""); + } + private static Limit limit(ConfigNode node, int defaultCapacity, int defaultReserved) { int capacity = nonNegative(node.get("capacity").as(Integer.class, defaultCapacity), defaultCapacity); int reserved = nonNegative(node.get("reserved_slots").as(Integer.class, defaultReserved), defaultReserved); From 3ae8abc6ef35ad67e2899abad60831d9d3c9dac1 Mon Sep 17 00:00:00 2001 From: Remy Duijsens Date: Tue, 4 Aug 2026 11:42:56 +0200 Subject: [PATCH 04/35] Store Capacity group membership once --- .../features/capacity/config/CapacityConfigStore.java | 1 - 1 file changed, 1 deletion(-) diff --git a/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfigStore.java b/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfigStore.java index 5c6aadb3..e02c83ec 100644 --- a/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfigStore.java +++ b/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/config/CapacityConfigStore.java @@ -172,7 +172,6 @@ private static Map servers(CapacityConfig config) { Map servers = new LinkedHashMap<>(); config.servers().forEach((name, rule) -> { Map raw = limit(rule.limit()); - raw.put("group", rule.group()); raw.put("counts_toward_gameplay", rule.countsTowardGameplay()); raw.put("queueable", rule.queueable()); raw.put("initial_state", rule.initialState().name()); From 431a7ebab5b4fedd7ce0c1db5bf2bd84ea3fd9d4 Mon Sep 17 00:00:00 2001 From: Remy Duijsens Date: Tue, 4 Aug 2026 11:44:20 +0200 Subject: [PATCH 05/35] Simplify Capacity command interface --- .../capacity/command/CapacityCommand.java | 410 +++++++----------- 1 file changed, 164 insertions(+), 246 deletions(-) diff --git a/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/command/CapacityCommand.java b/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/command/CapacityCommand.java index 3eef1b9e..c2c657e4 100644 --- a/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/command/CapacityCommand.java +++ b/proxyfeatures-platform-velocity/src/main/java/nl/hauntedmc/proxyfeatures/features/capacity/command/CapacityCommand.java @@ -2,7 +2,6 @@ import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.proxy.Player; -import net.kyori.adventure.text.Component; import nl.hauntedmc.proxyfeatures.api.capacity.CapacityScopeSnapshot; import nl.hauntedmc.proxyfeatures.api.capacity.CapacitySnapshot; import nl.hauntedmc.proxyfeatures.api.capacity.CapacityState; @@ -17,19 +16,21 @@ import java.time.Duration; import java.time.Instant; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.function.Consumer; import java.util.function.UnaryOperator; -/** Small asynchronous administration surface for the current Capacity configuration. */ +/** Small operational administration surface for Capacity. Topology remains config-only. */ public final class CapacityCommand implements FeatureCommand { - private static final List ROOT = List.of("status", "info", "config", "state", "reload"); - private static final List CONFIG_ROOT = List.of( - "show", "proxy", "gameplay", "group", "server", "unknown", "option"); - private static final List STATES = Arrays.stream(CapacityState.values()) + private static final List ROOT = List.of( + "status", "info", "slots", "queueable", "state", "reload"); + private static final List SLOT_SCOPES = List.of("proxy", "gameplay", "group", "server"); + private static final List STATES = java.util.Arrays.stream(CapacityState.values()) .map(state -> state.name().toLowerCase(Locale.ROOT)) .toList(); @@ -71,7 +72,8 @@ public void execute(Invocation invocation) { } switch (args[0].toLowerCase(Locale.ROOT)) { case "info" -> info(source, args); - case "config" -> config(source, args); + case "slots" -> slots(source, args); + case "queueable" -> queueable(source, args); case "state" -> state(source, args); case "reload" -> reload(source); default -> message(source, "capacity.command.usage"); @@ -87,9 +89,10 @@ private void status(CommandSource source, String[] args) { require(source, CapacityConfig.ADMIN_VIEW_PERMISSION); CapacitySnapshot snapshot = service.snapshot(); if (args.length >= 3 && equals(args[1], "group")) { - CapacityScopeSnapshot group = snapshot.groups().get(normalize(args[2])); + String groupName = normalize(args[2]); + CapacityScopeSnapshot group = snapshot.groups().get(groupName); if (group == null) throw new IllegalArgumentException("Onbekende groep: " + args[2]); - sendScope(source, "group:" + normalize(args[2]), group); + sendScope(source, "group:" + groupName, group); return; } message(source, "capacity.command.status.header"); @@ -97,6 +100,8 @@ private void status(CommandSource source, String[] args) { sendScope(source, "gameplay", snapshot.gameplay()); source.sendMessage(feature.getLocalizationHandler() .getMessage("capacity.command.status.footer") + .with("groups", controlPlane.config().groups().size()) + .with("servers", snapshot.servers().size()) .with("leases", snapshot.activeLeases()) .with("manual_states", controlPlane.manualStates().size()) .forAudience(source) @@ -116,7 +121,21 @@ private void info(CommandSource source, String[] args) { .build()); return; } + + CapacityConfig.ServerRule rule = controlPlane.config().server(server); sendScope(source, "server:" + server, snapshot); + if (!rule.group().isBlank()) { + CapacityScopeSnapshot group = service.snapshot().groups().get(rule.group()); + if (group != null) sendScope(source, "group:" + rule.group(), group); + } + source.sendMessage(feature.getLocalizationHandler() + .getMessage("capacity.command.info.policy") + .with("group", rule.group().isBlank() ? "none" : rule.group()) + .with("gameplay", rule.countsTowardGameplay()) + .with("queueable", rule.queueable()) + .with("initial_state", rule.initialState().name()) + .forAudience(source) + .build()); source.sendMessage(feature.getLocalizationHandler() .getMessage("capacity.command.state_behavior") .with("effect", stateEffect(snapshot.state())) @@ -125,206 +144,116 @@ private void info(CommandSource source, String[] args) { controlPlane.manualStates().stream() .filter(state -> state.server().equals(server)) .findFirst() - .ifPresent(state -> source.sendMessage(Component.text( - "Manual: " + state.state() + ", expires=" + format(state.expiresAt()) - + ", reason=" + state.reason() + ", actor=" + state.actorName()))); + .ifPresent(state -> source.sendMessage(feature.getLocalizationHandler() + .getMessage("capacity.command.info.manual") + .with("state", state.state().name()) + .with("expiry", format(state.expiresAt())) + .with("reason", state.reason()) + .with("actor", state.actorName()) + .forAudience(source) + .build())); } - private void config(CommandSource source, String[] args) { - if (args.length < 2 || equals(args[1], "show")) { - showConfig(source); - return; - } + private void slots(CommandSource source, String[] args) { require(source, CapacityConfig.ADMIN_CONFIG_PERMISSION); - switch (args[1].toLowerCase(Locale.ROOT)) { - case "proxy" -> limit(source, args, "proxy"); - case "gameplay" -> limit(source, args, "gameplay"); - case "group" -> group(source, args); - case "server" -> server(source, args); - case "unknown" -> unknown(source, args); - case "option" -> option(source, args); - default -> message(source, "capacity.command.config_usage"); + if (args.length < 3) { + throw new IllegalArgumentException("Gebruik: /capacity slots ..."); } - } - private void showConfig(CommandSource source) { - require(source, CapacityConfig.ADMIN_VIEW_PERMISSION); CapacityConfig config = controlPlane.config(); - message(source, "capacity.command.config.header"); - source.sendMessage(feature.getLocalizationHandler() - .getMessage("capacity.command.config.global") - .with("proxy_capacity", config.proxy().capacity()) - .with("proxy_reserved", config.proxy().reservedSlots()) - .with("gameplay_capacity", config.gameplay().capacity()) - .with("gameplay_reserved", config.gameplay().reservedSlots()) - .with("queue_on_full", config.queueOnFull()) - .with("fail_open", config.failOpenOnInternalError()) - .forAudience(source) - .build()); - source.sendMessage(feature.getLocalizationHandler() - .getMessage("capacity.command.config.counts") - .with("groups", config.groups().size()) - .with("servers", config.servers().size()) - .forAudience(source) - .build()); - } - - private void limit(CommandSource source, String[] args, String scope) { - if (args.length < 4) { - throw new IllegalArgumentException("Gebruik: /capacity config " + scope - + " [--confirm]"); - } - int capacity = number(args[2], "capacity"); - int reserved = number(args[3], "reserved"); - CapacityScopeSnapshot currentScope = scope.equals("proxy") - ? service.snapshot().proxy() : service.snapshot().gameplay(); - boolean dangerous = capacity > 0 && capacity < currentScope.effectiveUsed(); - UnaryOperator mutation = scope.equals("proxy") - ? config -> CapacityConfigEditor.proxyLimit(config, capacity, reserved) - : config -> CapacityConfigEditor.gameplayLimit(config, capacity, reserved); - mutate(source, args, dangerous, mutation); - } - - private void group(CommandSource source, String[] args) { - if (args.length < 4) { - throw new IllegalArgumentException( - "Gebruik: /capacity config group ..."); - } - String group = normalize(args[2]); - String action = args[3].toLowerCase(Locale.ROOT); - switch (action) { - case "limit" -> { - if (args.length < 6) throw new IllegalArgumentException("Capacity en reserved ontbreken."); - int capacity = number(args[4], "capacity"); - int reserved = number(args[5], "reserved"); - CapacityScopeSnapshot scope = service.snapshot().groups().get(group); - boolean dangerous = scope != null && capacity > 0 && capacity < scope.effectiveUsed(); - mutate(source, args, dangerous, - config -> CapacityConfigEditor.groupLimit(config, group, capacity, reserved)); - } - case "add-server" -> { - if (args.length < 5) throw new IllegalArgumentException("Servernaam ontbreekt."); - String server = normalize(args[4]); - mutate(source, args, occupied(server) > 0, - config -> CapacityConfigEditor.addGroupServer(config, group, server)); - } - case "remove-server" -> { - if (args.length < 5) throw new IllegalArgumentException("Servernaam ontbreekt."); - String server = normalize(args[4]); - mutate(source, args, occupied(server) > 0, - config -> CapacityConfigEditor.removeGroupServer(config, group, server)); - } - case "delete" -> mutate(source, args, true, - config -> CapacityConfigEditor.deleteGroup(config, group)); - default -> throw new IllegalArgumentException("Onbekende group-actie: " + action); - } - } - - private void server(CommandSource source, String[] args) { - if (args.length < 4) { - throw new IllegalArgumentException( - "Gebruik: /capacity config server ..."); - } - String server = normalize(args[2]); - String action = args[3].toLowerCase(Locale.ROOT); - switch (action) { - case "limit" -> { - if (args.length < 6) throw new IllegalArgumentException("Capacity en reserved ontbreken."); - int capacity = number(args[4], "capacity"); - int reserved = number(args[5], "reserved"); - mutate(source, args, capacity > 0 && capacity < used(server), - config -> CapacityConfigEditor.serverLimit(config, server, capacity, reserved)); - } - case "group" -> { - if (args.length < 5) throw new IllegalArgumentException("Groep of none ontbreekt."); - mutate(source, args, occupied(server) > 0, - config -> CapacityConfigEditor.serverGroup(config, server, args[4])); + String scopeType = args[1].toLowerCase(Locale.ROOT); + String scopeName; + CapacityConfig.Limit current; + int valueIndex; + UnaryOperator mutation; + + switch (scopeType) { + case "proxy" -> { + scopeName = "proxy"; + current = config.proxy(); + valueIndex = 2; + int capacity = number(args[valueIndex], "capacity"); + int reserved = reserved(args, valueIndex + 1, current, capacity); + mutation = value -> CapacityConfigEditor.proxyLimit(value, capacity, reserved); } case "gameplay" -> { - if (args.length < 5) throw new IllegalArgumentException("Boolean ontbreekt."); - boolean value = bool(args[4]); - mutate(source, args, occupied(server) > 0, - config -> CapacityConfigEditor.serverGameplay(config, server, value)); + scopeName = "gameplay"; + current = config.gameplay(); + valueIndex = 2; + int capacity = number(args[valueIndex], "capacity"); + int reserved = reserved(args, valueIndex + 1, current, capacity); + mutation = value -> CapacityConfigEditor.gameplayLimit(value, capacity, reserved); } - case "queueable" -> { - if (args.length < 5) throw new IllegalArgumentException("Boolean ontbreekt."); - boolean value = bool(args[4]); - mutate(source, args, false, - config -> CapacityConfigEditor.serverQueueable(config, server, value)); + case "group" -> { + if (args.length < 4) { + throw new IllegalArgumentException( + "Gebruik: /capacity slots group [reserved]"); + } + String group = normalize(args[2]); + CapacityConfig.GroupRule rule = config.groups().get(group); + if (rule == null) throw new IllegalArgumentException("Onbekende groep: " + group); + scopeName = "group:" + group; + current = rule.limit(); + valueIndex = 3; + int capacity = number(args[valueIndex], "capacity"); + int reserved = reserved(args, valueIndex + 1, current, capacity); + mutation = value -> CapacityConfigEditor.groupLimit(value, group, capacity, reserved); } - case "initial-state" -> { - if (args.length < 5) throw new IllegalArgumentException("State ontbreekt."); - CapacityState state = capacityState(args[4]); - mutate(source, args, state != CapacityState.OPEN, - config -> CapacityConfigEditor.serverInitialState(config, server, state)); + case "server" -> { + if (args.length < 4) { + throw new IllegalArgumentException( + "Gebruik: /capacity slots server [reserved]"); + } + String server = requireKnownServer(args[2]); + scopeName = "server:" + server; + current = config.server(server).limit(); + valueIndex = 3; + int capacity = number(args[valueIndex], "capacity"); + int reserved = reserved(args, valueIndex + 1, current, capacity); + mutation = value -> CapacityConfigEditor.serverLimit(value, server, capacity, reserved); } - case "delete" -> mutate(source, args, true, - config -> CapacityConfigEditor.deleteServer(config, server)); - default -> throw new IllegalArgumentException("Onbekende server-actie: " + action); + default -> throw new IllegalArgumentException("Onbekend slots-bereik: " + scopeType); } - } - private void unknown(CommandSource source, String[] args) { - if (args.length < 3) { - throw new IllegalArgumentException( - "Gebruik: /capacity config unknown ..."); - } - String action = args[2].toLowerCase(Locale.ROOT); - switch (action) { - case "limit" -> { - if (args.length < 5) throw new IllegalArgumentException("Capacity en reserved ontbreken."); - int capacity = number(args[3], "capacity"); - int reserved = number(args[4], "reserved"); - boolean dangerous = capacity < controlPlane.config().unknownCapacity(); - mutate(source, args, dangerous, - config -> CapacityConfigEditor.unknownLimit(config, capacity, reserved)); - } - case "gameplay" -> { - if (args.length < 4) throw new IllegalArgumentException("Boolean ontbreekt."); - boolean value = bool(args[3]); - mutate(source, args, false, - config -> CapacityConfigEditor.unknownGameplay(config, value)); - } - case "queueable" -> { - if (args.length < 4) throw new IllegalArgumentException("Boolean ontbreekt."); - boolean value = bool(args[3]); - mutate(source, args, false, - config -> CapacityConfigEditor.unknownQueueable(config, value)); - } - default -> throw new IllegalArgumentException("Onbekende unknown-actie: " + action); - } + int capacity = number(args[valueIndex], "capacity"); + int reserved = reserved(args, valueIndex + 1, current, capacity); + mutate(source, mutation, audience -> audience.sendMessage(feature.getLocalizationHandler() + .getMessage("capacity.command.slots_changed") + .with("scope", scopeName) + .with("capacity", capacity) + .with("reserved", reserved) + .forAudience(audience) + .build())); } - private void option(CommandSource source, String[] args) { - if (args.length < 4) { - throw new IllegalArgumentException("Gebruik: /capacity config option