feat(commands): view and set online player heights
This commit is contained in:
@@ -72,13 +72,23 @@ Operators (or senders granted `spigotheights.admin`) can use:
|
||||
/heights settings
|
||||
/heights set min 0.2
|
||||
/heights set max 3.0
|
||||
/heights player Steve
|
||||
/heights player Steve 0.5
|
||||
```
|
||||
|
||||
Console is supported. Tab completion suggests subcommands, bound names, and valid example values; other valid numbers may also be entered.
|
||||
|
||||
Changes are saved to `plugins/SpigotHeights/config.yml` and take effect without a restart. Minimum must be at least `0.0625`, maximum at most `16.0`, minimum cannot exceed maximum, and the launcher threshold must remain within the range. Invalid changes or save failures leave active settings unchanged.
|
||||
|
||||
Players are not immediately resized. Subsequent potion use, joins, and respawns use the new limits; Restoration still returns and preserves exact scale `1.0`. These commands only change min/max; edit other settings in the configuration file and restart the server.
|
||||
Players are not immediately resized. Subsequent potion use, joins, and respawns use the new limits; Restoration still returns and preserves exact scale `1.0`. The settings commands only change min/max; edit other settings in the configuration file and restart the server.
|
||||
|
||||
### Player height
|
||||
|
||||
`/heights player <name>` reports an online player's scale (`1.0` = normal size). If another plugin adds scale modifiers, both the base scale and effective scale are shown.
|
||||
|
||||
`/heights player <name> <scale>` immediately sets and saves that player's base scale by UUID. Values must be finite and within current min/max limits, except `1.0` is always allowed. Saved scales follow the usual limits on reconnect and respawn if configuration changes later. A failed save leaves the player's height unchanged.
|
||||
|
||||
Player names must match an online player exactly (case-insensitive); partial names and offline players are rejected. Tab completion suggests online names and valid example sizes. Operators, console, and senders with `spigotheights.admin` may view or set heights, including their own.
|
||||
|
||||
## Build
|
||||
|
||||
|
||||
@@ -6,6 +6,18 @@ description: Chronological record of material decisions affecting Spigot Heights
|
||||
|
||||
# Spigot Heights Design Log
|
||||
|
||||
## 2026-09-07T00:00:54Z — Player-height commands completed
|
||||
|
||||
- Added `/heights player <name> [scale]` for exact online-player lookup, base/effective scale reporting, immediate saved changes, and permission-protected name/size completion.
|
||||
- Command-set scales honor live limits and the exact `1.0` restoration exception. The player-state store now publishes cached changes only after successful persistence, and commands save before applying scale.
|
||||
- Verified `./gradlew clean check jar`: all 33 tests passed, including permissions, input validation, completions, scale reporting, reload, and failed-save protection. Inspected Bukkit lookup/attribute and join/respawn wiring; live-server/client verification remains unperformed.
|
||||
- Updated README and completed US-008 and related US-004/US-007 criteria.
|
||||
|
||||
## 2026-09-06T23:54:36Z — Player-height commands approved and started
|
||||
|
||||
- Approved [US-008](user-stories/us-008-manage-player-height.md): `/heights player <name> [scale]` for online players, admin permissions, tab completion, and UUID persistence.
|
||||
- Values follow current min/max bounds with the exact `1.0` exception; failed saves must not change cached or live player height.
|
||||
|
||||
## 2026-09-06T23:51:22Z — Settings commands completed
|
||||
|
||||
- Added `/heights settings` and `/heights set min|max <value>` with `spigotheights.admin` (default op), generic sender support, and prefix-filtered tab completion of valid example values.
|
||||
|
||||
@@ -13,3 +13,4 @@ description: Catalog of user stories for the Spigot Heights plugin.
|
||||
5. [US-005: Build and release the plugin](us-005-build-and-release.md)
|
||||
6. [US-006: Restore default stature](us-006-restore-default-stature.md)
|
||||
7. [US-007: Manage stature settings with commands](us-007-manage-settings-commands.md)
|
||||
8. [US-008: View and set player height](us-008-manage-player-height.md)
|
||||
|
||||
@@ -21,8 +21,12 @@ As a **server operator**, I want validated stature and launcher settings with du
|
||||
- [x] Configuration and state behavior have automated tests.
|
||||
- [x] Approved min/max commands validate and persist changes before publishing live settings to gameplay listeners, without immediately resizing players.
|
||||
|
||||
- [x] Command-set player heights persist by UUID under existing reload rules; failed saves do not publish a new cached or live height.
|
||||
|
||||
## Related
|
||||
|
||||
- [US-008: View and set player height](us-008-manage-player-height.md)
|
||||
|
||||
- [User-story catalog](index.md)
|
||||
- [US-007: Manage stature settings with commands](us-007-manage-settings-commands.md)
|
||||
- [US-006: Restore default stature](us-006-restore-default-stature.md)
|
||||
|
||||
@@ -19,12 +19,18 @@ As a **server administrator**, I want commands with tab completion so that I can
|
||||
- [x] Existing players are not immediately resized; subsequent potion use, joins, and respawns use current limits, preserving the restoration exception for `1.0`.
|
||||
- [x] Automated tests and README documentation cover commands, permissions, completion, validation, persistence, and live settings.
|
||||
|
||||
- [x] Command help and completion include the player-height subcommand without changing existing min/max command behavior.
|
||||
|
||||
## Verification
|
||||
|
||||
- Player-command extension verified with `./gradlew clean check jar` (33 passing tests), including existing settings-command regressions and new player-name/size completions.
|
||||
|
||||
- `./gradlew clean check jar` passed all 26 tests, including command execution with authorized/unauthorized generic senders, prefix-filtered valid completions, argument validation, failed saves, YAML reload/preservation, and live domain behavior.
|
||||
- Code inspection confirms command/permission registration and shared settings suppliers in consumption, join, respawn, and launcher adapters. No live-server command or client tab-completion test was performed.
|
||||
|
||||
## Related
|
||||
|
||||
- [US-008: View and set player height](us-008-manage-player-height.md)
|
||||
|
||||
- [US-004: Configure and persist stature behavior](us-004-configure-and-persist.md)
|
||||
- [US-006: Restore default stature](us-006-restore-default-stature.md)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: User Story
|
||||
title: "US-008: View and set player height"
|
||||
description: Let administrators inspect and persistently set online player scales through tab-completed commands.
|
||||
status: done
|
||||
---
|
||||
|
||||
# US-008: View and set player height
|
||||
|
||||
As a **server administrator**, I want to view and set a player's height so that I can manage individual stature without potions.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] `/heights player <name>` reports the online player's scale, where `1.0` is normal size.
|
||||
- [x] `/heights player <name> <scale>` immediately applies and saves the scale by UUID for reconnects and respawns.
|
||||
- [x] Operators and senders with `spigotheights.admin` can use the commands, including console and targeting themselves.
|
||||
- [x] Tab completion suggests online player names and valid example sizes, filtered by prefix; unauthorized senders receive no suggestions.
|
||||
- [x] Values must be finite and within current configured limits, except exact `1.0` is always allowed as the restoration exception.
|
||||
- [x] Invalid input, unknown/offline players, and persistence failures produce clear messages; failed saves do not change live or cached player height.
|
||||
- [x] Automated tests and README documentation cover viewing, setting, permissions, completion, validation, and persistence.
|
||||
|
||||
## Verification
|
||||
|
||||
- `./gradlew clean check jar` passed all 33 tests. Coverage includes command routing and permissions, completion, base/effective scale reporting, live limits, the restoration exception, UUID reload, and failed-save protection for live/cached/disk state.
|
||||
- Code inspection confirms exact online lookup, scale-attribute access, shared storage with join/respawn handlers, and command registration. No live-server/client test was performed.
|
||||
|
||||
## Related
|
||||
|
||||
- [US-004: Configure and persist stature behavior](us-004-configure-and-persist.md)
|
||||
- [US-007: Manage stature settings with commands](us-007-manage-settings-commands.md)
|
||||
@@ -0,0 +1,34 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import java.util.List;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.attribute.AttributeInstance;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/** Bukkit boundary for exact online-player lookup; used only on the server thread. */
|
||||
public final class BukkitPlayerHeights {
|
||||
private final Server server;
|
||||
|
||||
public BukkitPlayerHeights(Server server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
public PlayerHeights.Target find(String name) {
|
||||
Player player = server.getPlayerExact(name);
|
||||
if (player == null) {
|
||||
return null;
|
||||
}
|
||||
AttributeInstance scale = player.getAttribute(Attribute.SCALE);
|
||||
if (scale == null) {
|
||||
throw new IllegalArgumentException("Player " + player.getName() + " has no scale attribute.");
|
||||
}
|
||||
return new PlayerHeights.Target(player.getUniqueId(), player.getName(), scale.getBaseValue(),
|
||||
scale.getValue(), scale::setBaseValue);
|
||||
}
|
||||
|
||||
public List<String> names() {
|
||||
return server.getOnlinePlayers().stream().map(Player::getName)
|
||||
.sorted(String.CASE_INSENSITIVE_ORDER).toList();
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,12 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
public final class HeightStore {
|
||||
private final Path statePath;
|
||||
private final YamlConfiguration state;
|
||||
private YamlConfiguration state;
|
||||
|
||||
public HeightStore(File dataFolder) {
|
||||
statePath = dataFolder.toPath().resolve("state.yml");
|
||||
@@ -25,16 +26,23 @@ public final class HeightStore {
|
||||
}
|
||||
|
||||
public synchronized void save(UUID playerId, double scale) throws IOException {
|
||||
state.set(path(playerId), scale);
|
||||
YamlConfiguration next = new YamlConfiguration();
|
||||
try {
|
||||
next.loadFromString(state.saveToString());
|
||||
} catch (InvalidConfigurationException exception) {
|
||||
throw new IOException("Could not copy player state", exception);
|
||||
}
|
||||
next.set(path(playerId), scale);
|
||||
Files.createDirectories(statePath.getParent());
|
||||
Path temporary = statePath.resolveSibling("state.yml.tmp");
|
||||
Files.writeString(temporary, state.saveToString(), StandardCharsets.UTF_8);
|
||||
Files.writeString(temporary, next.saveToString(), StandardCharsets.UTF_8);
|
||||
try {
|
||||
Files.move(temporary, statePath, StandardCopyOption.ATOMIC_MOVE,
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (AtomicMoveNotSupportedException exception) {
|
||||
Files.move(temporary, statePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
state = next;
|
||||
}
|
||||
|
||||
private static String path(UUID playerId) {
|
||||
|
||||
@@ -11,21 +11,27 @@ import org.bukkit.command.TabExecutor;
|
||||
|
||||
public final class HeightsCommand implements TabExecutor {
|
||||
private static final String PERMISSION = "spigotheights.admin";
|
||||
private static final String USAGE = "Usage: /heights settings | /heights set min|max <value>";
|
||||
private static final String USAGE = "Usage: /heights settings | /heights set min|max <value>"
|
||||
+ " | /heights player <name> [scale]";
|
||||
private final LiveHeightSettings settings;
|
||||
private final Logger logger;
|
||||
private final PlayerHeights players;
|
||||
|
||||
public HeightsCommand(LiveHeightSettings settings, Logger logger) {
|
||||
public HeightsCommand(LiveHeightSettings settings, PlayerHeights players, Logger logger) {
|
||||
this.settings = settings;
|
||||
this.players = players;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (!sender.hasPermission(PERMISSION)) {
|
||||
sender.sendMessage("You do not have permission to manage Spigot Heights settings.");
|
||||
sender.sendMessage("You do not have permission to manage Spigot Heights.");
|
||||
return true;
|
||||
}
|
||||
if (args.length > 0 && args[0].equalsIgnoreCase("player")) {
|
||||
return playerCommand(sender, args);
|
||||
}
|
||||
if (args.length == 1 && args[0].equalsIgnoreCase("settings")) {
|
||||
HeightSettings current = settings.get();
|
||||
sender.sendMessage("Spigot Heights: min=" + current.minimum() + ", max=" + current.maximum()
|
||||
@@ -55,13 +61,41 @@ public final class HeightsCommand implements TabExecutor {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean playerCommand(CommandSender sender, String[] args) {
|
||||
if (args.length != 2 && args.length != 3) {
|
||||
sender.sendMessage(USAGE);
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
sender.sendMessage(args.length == 2 ? players.describe(args[1])
|
||||
: players.set(args[1], Double.parseDouble(args[2])));
|
||||
} catch (NumberFormatException exception) {
|
||||
sender.sendMessage("Invalid scale: enter a finite number, for example 0.5 or 1.0.");
|
||||
} catch (IllegalArgumentException exception) {
|
||||
sender.sendMessage(exception.getMessage());
|
||||
} catch (IOException exception) {
|
||||
logger.warning("Could not save player height: " + exception.getMessage());
|
||||
sender.sendMessage("Could not save player height. Their height is unchanged; check the server log.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
if (!sender.hasPermission(PERMISSION)) {
|
||||
return List.of();
|
||||
}
|
||||
if (args.length == 1) {
|
||||
return matching(Stream.of("settings", "set"), args[0]);
|
||||
return matching(Stream.of("settings", "set", "player"), args[0]);
|
||||
}
|
||||
if (args.length == 2 && args[0].equalsIgnoreCase("player")) {
|
||||
return matching(players.names().stream(), args[1]);
|
||||
}
|
||||
if (args.length == 3 && args[0].equalsIgnoreCase("player")
|
||||
&& players.names().stream().anyMatch(name -> name.equalsIgnoreCase(args[1]))) {
|
||||
HeightSettings current = settings.get();
|
||||
return matching(Stream.of(current.minimum(), current.maximum(), 0.0625, 0.2, 0.4, 0.5, 1.0, 2.0, 3.0, 16.0)
|
||||
.distinct().sorted().filter(players::isValid).map(String::valueOf), args[2]);
|
||||
}
|
||||
if (args.length == 2 && args[0].equalsIgnoreCase("set")) {
|
||||
return matching(Stream.of("min", "max"), args[1]);
|
||||
@@ -92,6 +126,6 @@ public final class HeightsCommand implements TabExecutor {
|
||||
|
||||
private static List<String> matching(Stream<String> candidates, String prefix) {
|
||||
String normalized = prefix.toLowerCase(Locale.ROOT);
|
||||
return candidates.filter(value -> value.startsWith(normalized)).toList();
|
||||
return candidates.filter(value -> value.toLowerCase(Locale.ROOT).startsWith(normalized)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.DoubleConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** Online-player operations, called on the server thread. */
|
||||
public final class PlayerHeights {
|
||||
public record Target(UUID id, String name, double baseScale, double effectiveScale, DoubleConsumer applyScale) {}
|
||||
|
||||
private final Supplier<HeightSettings> settings;
|
||||
private final HeightStore store;
|
||||
private final Function<String, Target> lookup;
|
||||
private final Supplier<List<String>> onlineNames;
|
||||
|
||||
public PlayerHeights(Supplier<HeightSettings> settings, HeightStore store,
|
||||
Function<String, Target> lookup, Supplier<List<String>> onlineNames) {
|
||||
this.settings = settings;
|
||||
this.store = store;
|
||||
this.lookup = lookup;
|
||||
this.onlineNames = onlineNames;
|
||||
}
|
||||
|
||||
public String describe(String name) {
|
||||
Target target = requireTarget(name);
|
||||
String message = target.name() + " has scale " + target.baseScale() + " (1.0 = normal)";
|
||||
if (target.effectiveScale() != target.baseScale()) {
|
||||
message += "; effective scale with modifiers: " + target.effectiveScale();
|
||||
}
|
||||
return message + ".";
|
||||
}
|
||||
|
||||
public String set(String name, double value) throws IOException {
|
||||
Target target = requireTarget(name);
|
||||
if (!isValid(value)) {
|
||||
HeightSettings current = settings.get();
|
||||
throw new IllegalArgumentException("Scale must be a finite number between " + current.minimum()
|
||||
+ " and " + current.maximum() + ", or exactly 1.0 for normal size.");
|
||||
}
|
||||
store.save(target.id(), value);
|
||||
target.applyScale().accept(value);
|
||||
return "Set " + target.name() + "'s base scale to " + value + ". Saved and active.";
|
||||
}
|
||||
|
||||
public List<String> names() {
|
||||
return onlineNames.get();
|
||||
}
|
||||
|
||||
public boolean isValid(double value) {
|
||||
HeightSettings current = settings.get();
|
||||
return Double.isFinite(value) && (value == 1.0
|
||||
|| value >= current.minimum() && value <= current.maximum());
|
||||
}
|
||||
|
||||
private Target requireTarget(String name) {
|
||||
Target target = lookup.apply(name);
|
||||
if (target == null) {
|
||||
throw new IllegalArgumentException("No online player named '" + name + "'. Use their full name.");
|
||||
}
|
||||
return target;
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,14 @@ public final class SpigotHeightsPlugin extends JavaPlugin {
|
||||
getConfig().set("height.minimum", next.minimum());
|
||||
getConfig().set("height.maximum", next.maximum());
|
||||
});
|
||||
HeightsCommand executor = new HeightsCommand(liveSettings, getLogger());
|
||||
HeightStore store = new HeightStore(getDataFolder());
|
||||
BukkitPlayerHeights onlinePlayers = new BukkitPlayerHeights(getServer());
|
||||
PlayerHeights playerHeights = new PlayerHeights(liveSettings, store, onlinePlayers::find, onlinePlayers::names);
|
||||
HeightsCommand executor = new HeightsCommand(liveSettings, playerHeights, getLogger());
|
||||
PluginCommand command = Objects.requireNonNull(getCommand("heights"), "Missing heights command declaration");
|
||||
command.setExecutor(executor);
|
||||
command.setTabCompleter(executor);
|
||||
|
||||
HeightStore store = new HeightStore(getDataFolder());
|
||||
PotionRecipes potions = new PotionRecipes(this);
|
||||
potions.register();
|
||||
getServer().getPluginManager().registerEvents(
|
||||
|
||||
@@ -6,10 +6,10 @@ description: Craftable player stature potions and tiny-player dispenser launcher
|
||||
author: dmg.games
|
||||
commands:
|
||||
heights:
|
||||
description: View and change Spigot Heights stature limits.
|
||||
usage: /heights settings | /heights set min|max <value>
|
||||
description: View and change stature limits and online player heights.
|
||||
usage: /heights settings | /heights set min|max <value> | /heights player <name> [scale]
|
||||
permission: spigotheights.admin
|
||||
permissions:
|
||||
spigotheights.admin:
|
||||
description: View and change Spigot Heights settings.
|
||||
description: View and change Spigot Heights settings and player heights.
|
||||
default: op
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -12,6 +15,18 @@ class HeightStoreTest {
|
||||
@TempDir
|
||||
Path temporaryDirectory;
|
||||
|
||||
@Test
|
||||
void failedSaveDoesNotChangeCachedOrSavedScale() throws Exception {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
HeightStore store = new HeightStore(temporaryDirectory.toFile());
|
||||
store.save(playerId, 0.7);
|
||||
String original = Files.readString(temporaryDirectory.resolve("state.yml"));
|
||||
Files.createDirectory(temporaryDirectory.resolve("state.yml.tmp"));
|
||||
assertThrows(IOException.class, () -> store.save(playerId, 0.2));
|
||||
assertEquals(0.7, store.find(playerId));
|
||||
assertEquals(original, Files.readString(temporaryDirectory.resolve("state.yml")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void restorationReplacesPreviousSizeAndSurvivesReloadOutsideLimits() throws Exception {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
|
||||
@@ -5,18 +5,77 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class HeightsCommandTest {
|
||||
private final List<HeightSettings> saved = new ArrayList<>();
|
||||
private final List<String> messages = new ArrayList<>();
|
||||
private final LiveHeightSettings live = new LiveHeightSettings(
|
||||
new HeightSettings(0.4, 2.0, 0.1, 0.5, 1.5, 20), saved::add);
|
||||
private final HeightsCommand command = new HeightsCommand(live, Logger.getAnonymousLogger());
|
||||
@TempDir
|
||||
Path directory;
|
||||
private final UUID playerId = UUID.randomUUID();
|
||||
private double playerScale = 0.7;
|
||||
private PlayerHeights players;
|
||||
private HeightStore playerStore;
|
||||
private HeightsCommand command;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
playerStore = new HeightStore(directory.toFile());
|
||||
players = new PlayerHeights(live, playerStore, name -> name.equalsIgnoreCase("Steve")
|
||||
? new PlayerHeights.Target(playerId, "Steve", playerScale, playerScale, value -> playerScale = value)
|
||||
: null, () -> List.of("Steve"));
|
||||
command = new HeightsCommand(live, players, Logger.getAnonymousLogger());
|
||||
}
|
||||
|
||||
@Test
|
||||
void viewsAndSetsOnlinePlayerWithNameAndSizeCompletion() {
|
||||
CommandSender sender = sender(true);
|
||||
command.onCommand(sender, null, "heights", new String[] {"player", "Steve"});
|
||||
assertTrue(messages.getLast().contains("0.7"));
|
||||
command.onCommand(sender, null, "heights", new String[] {"player", "steve", "0.5"});
|
||||
assertEquals(0.5, playerScale);
|
||||
assertEquals(0.5, playerStore.find(playerId));
|
||||
assertTrue(messages.getLast().contains("Steve"));
|
||||
assertEquals(List.of("player"), complete(sender, "pl"));
|
||||
assertEquals(List.of("Steve"), complete(sender, "player", "st"));
|
||||
assertTrue(complete(sender, "player", "Steve", "0.").contains("0.5"));
|
||||
assertTrue(complete(sender, "player", "Steve", "1").contains("1.0"));
|
||||
assertEquals(List.of(), complete(sender, "player", "Offline", ""));
|
||||
assertEquals(List.of(), complete(sender, "player", "Steve", "0.5", ""));
|
||||
for (String value : complete(sender, "player", "Steve", "")) {
|
||||
assertTrue(players.isValid(Double.parseDouble(value)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidPlayerCommandsAndFailedSavesAreReportedWithoutResizing() throws Exception {
|
||||
for (String[] args : new String[][] {
|
||||
{"player"}, {"player", "Steve", "0.5", "extra"},
|
||||
{"player", "Offline"}, {"player", "Ste", "0.5"},
|
||||
{"player", "Steve", "nope"}, {"player", "Steve", "NaN"},
|
||||
{"player", "Steve", "0.2"}, {"player", "Steve", "3.0"}}) {
|
||||
int before = messages.size();
|
||||
command.onCommand(sender(true), null, "heights", args);
|
||||
assertEquals(before + 1, messages.size());
|
||||
assertEquals(0.7, playerScale);
|
||||
assertEquals(null, playerStore.find(playerId));
|
||||
}
|
||||
java.nio.file.Files.createDirectory(directory.resolve("state.yml.tmp"));
|
||||
command.onCommand(sender(true), null, "heights", new String[] {"player", "Steve", "0.5"});
|
||||
assertTrue(messages.getLast().contains("Could not save"));
|
||||
assertEquals(0.7, playerScale);
|
||||
assertEquals(null, playerStore.find(playerId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void authorizedSenderCanInspectAndChangeBounds() {
|
||||
@@ -37,6 +96,12 @@ class HeightsCommandTest {
|
||||
CommandSender sender = sender(false);
|
||||
command.onCommand(sender, null, "heights", new String[] {"set", "min", "0.2"});
|
||||
command.onCommand(sender, null, "heights", new String[] {"settings"});
|
||||
command.onCommand(sender, null, "heights", new String[] {"player", "Steve"});
|
||||
command.onCommand(sender, null, "heights", new String[] {"player", "Steve", "0.5"});
|
||||
assertEquals(0.7, playerScale);
|
||||
assertEquals(null, playerStore.find(playerId));
|
||||
assertEquals(List.of(), complete(sender, "player", ""));
|
||||
assertEquals(List.of(), complete(sender, "player", "Steve", ""));
|
||||
assertTrue(messages.stream().allMatch(message -> message.contains("permission")));
|
||||
assertEquals(List.of(), saved);
|
||||
assertEquals(List.of(), command.onTabComplete(sender, null, "heights", new String[] {""}));
|
||||
@@ -84,7 +149,7 @@ class HeightsCommandTest {
|
||||
LiveHeightSettings failing = new LiveHeightSettings(live.get(), settings -> {
|
||||
throw new IOException("test save failure");
|
||||
});
|
||||
new HeightsCommand(failing, Logger.getAnonymousLogger()).onCommand(
|
||||
new HeightsCommand(failing, players, Logger.getAnonymousLogger()).onCommand(
|
||||
sender(true), null, "heights", new String[] {"set", "min", "0.2"});
|
||||
assertTrue(messages.getLast().contains("Could not save"));
|
||||
assertEquals(0.4, failing.get().minimum());
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class PlayerHeightsTest {
|
||||
@TempDir
|
||||
Path directory;
|
||||
private final UUID id = UUID.randomUUID();
|
||||
private double scale = 0.7;
|
||||
private LiveHeightSettings settings;
|
||||
private HeightStore store;
|
||||
private PlayerHeights heights;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
settings = new LiveHeightSettings(new HeightSettings(0.4, 2.0, 0.1, 0.5, 1.5, 20), value -> {});
|
||||
store = new HeightStore(directory.toFile());
|
||||
heights = new PlayerHeights(settings, store, name -> name.equalsIgnoreCase("Steve")
|
||||
? new PlayerHeights.Target(id, "Steve", scale, scale * 2, value -> scale = value) : null,
|
||||
() -> List.of("Steve"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void viewsActualBaseAndEffectiveScaleAndPersistsChangesByUuid() throws Exception {
|
||||
assertTrue(heights.describe("steve").contains("0.7"));
|
||||
assertTrue(heights.describe("Steve").contains("1.4"));
|
||||
heights.set("STEVE", 0.5);
|
||||
assertEquals(0.5, scale);
|
||||
HeightStore reloaded = new HeightStore(directory.toFile());
|
||||
assertEquals(0.5, reloaded.find(id));
|
||||
assertEquals(0.5, HeightMath.safeStoredScale(reloaded.find(id), settings.get()));
|
||||
assertEquals(List.of("Steve"), heights.names());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownPlayersAndInvalidScalesWithoutChangingState() {
|
||||
assertThrows(IllegalArgumentException.class, () -> heights.describe("Ste"));
|
||||
assertThrows(IllegalArgumentException.class, () -> heights.set("Offline", 0.5));
|
||||
for (double value : new double[] {Double.NaN, Double.POSITIVE_INFINITY, -1, 0.2, 3.0}) {
|
||||
assertThrows(IllegalArgumentException.class, () -> heights.set("Steve", value));
|
||||
}
|
||||
assertEquals(0.7, scale);
|
||||
assertEquals(null, store.find(id));
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesLiveLimitsAndAllowsRestorationOutsideThem() throws Exception {
|
||||
settings.setBound("max", 0.8);
|
||||
heights.set("Steve", 1.0);
|
||||
assertEquals(1.0, scale);
|
||||
assertEquals(1.0, HeightMath.safeStoredScale(new HeightStore(directory.toFile()).find(id), settings.get()));
|
||||
assertThrows(IllegalArgumentException.class, () -> heights.set("Steve", 0.9));
|
||||
settings.setBound("min", 0.2);
|
||||
heights.set("Steve", 0.2);
|
||||
assertEquals(0.2, scale);
|
||||
}
|
||||
|
||||
@Test
|
||||
void failedSaveLeavesLiveAndCachedHeightUnchanged() throws Exception {
|
||||
store.save(id, scale);
|
||||
Files.createDirectory(directory.resolve("state.yml.tmp"));
|
||||
assertThrows(IOException.class, () -> heights.set("Steve", 0.5));
|
||||
assertEquals(0.7, scale);
|
||||
assertEquals(0.7, store.find(id));
|
||||
assertEquals(0.7, new HeightStore(directory.toFile()).find(id));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user