Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e802319f45 | ||
|
|
fd601961eb |
@@ -33,6 +33,16 @@ Gold Ingot | Rabbit's Foot | Gold Ingot
|
|||||||
|
|
||||||
Use the Growth recipe with a Fermented Spider Eye instead of the Rabbit's Foot. Growth and Diminution adjust scale by one configured step and clamp at the limits.
|
Use the Growth recipe with a Fermented Spider Eye instead of the Rabbit's Foot. Growth and Diminution adjust scale by one configured step and clamp at the limits.
|
||||||
|
|
||||||
|
### Potion of Restoration
|
||||||
|
|
||||||
|
```text
|
||||||
|
Gold Ingot | Amethyst Shard | Gold Ingot
|
||||||
|
Amethyst Shard | Potion of Shifting Stature | Amethyst Shard
|
||||||
|
Gold Ingot | Sugar | Gold Ingot
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces one Potion of Restoration. Drinking it restores scale to exactly `1.0` (normal size), even outside configured limits. This size is saved across reconnects, respawns, and server restarts. Other saved scales are still clamped to the configured range.
|
||||||
|
|
||||||
## Tiny-player launchers
|
## Tiny-player launchers
|
||||||
|
|
||||||
A player below the configured scale threshold can walk onto a hopper whose output points into a dispenser. If the block in front of that dispenser is passable, the player is moved there and launched in the direction the dispenser faces. The launcher does not require redstone.
|
A player below the configured scale threshold can walk onto a hopper whose output points into a dispenser. If the block in front of that dispenser is passable, the player is moved there and launched in the direction the dispenser faces. The launcher does not require redstone.
|
||||||
@@ -54,6 +64,22 @@ launcher:
|
|||||||
|
|
||||||
Player scales are stored by UUID in `plugins/SpigotHeights/state.yml`.
|
Player scales are stored by UUID in `plugins/SpigotHeights/state.yml`.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
Operators (or senders granted `spigotheights.admin`) can use:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/heights settings
|
||||||
|
/heights set min 0.2
|
||||||
|
/heights set max 3.0
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -6,6 +6,30 @@ description: Chronological record of material decisions affecting Spigot Heights
|
|||||||
|
|
||||||
# Spigot Heights Design Log
|
# Spigot Heights Design Log
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
- Validated changes are persisted through temporary-file replacement (atomic where supported) before activating shared live settings. Unrelated YAML values are preserved; malformed files and write failures do not activate new limits.
|
||||||
|
- Updated gameplay listeners to read current settings without immediately resizing players or resetting launcher cooldowns.
|
||||||
|
- Verified `./gradlew clean check jar`: all 26 tests passed. Inspected command registration and event wiring; live-server/client verification remains unperformed. Completed US-007 and the related US-004 change.
|
||||||
|
|
||||||
|
## 2026-09-06T23:45:28Z — Settings commands approved and started
|
||||||
|
|
||||||
|
- Approved [US-007](user-stories/us-007-manage-settings-commands.md): permission-protected min/max commands, tab completion, validation, and persistent live updates.
|
||||||
|
- Existing players are not resized immediately; subsequent gameplay reads current limits. The exact `1.0` restoration exception remains intact.
|
||||||
|
|
||||||
|
## 2026-09-06T23:41:16Z — Restoration completed
|
||||||
|
|
||||||
|
- Added the white Potion of Restoration with persistent identity and the approved Sugar upgrade recipe.
|
||||||
|
- Consumption restores exact scale `1.0` through the existing report/save path; saved `1.0` bypasses configured-range clamping on join and respawn.
|
||||||
|
- Verified `./gradlew clean check jar`: all 17 tests passed, including restoration, persistence reload, range exceptions, recipe shape, identity, and existing potion regression coverage.
|
||||||
|
- Inspected Bukkit recipe/metadata and event wiring; live-server gameplay verification remains unperformed. Updated the README and completed US-006 and the related US-004 change.
|
||||||
|
|
||||||
|
## 2026-09-06T23:37:19Z — Restoration approved and implementation started
|
||||||
|
|
||||||
|
- Approved [US-006](user-stories/us-006-restore-default-stature.md): a Sugar-based upgrade of Shifting Stature restores exact scale `1.0`.
|
||||||
|
- Approved an exception to saved-scale clamping in [US-004](user-stories/us-004-configure-and-persist.md) so restoration survives reconnects and respawns even outside configured limits.
|
||||||
|
|
||||||
## 2026-09-04 — Initial design approved
|
## 2026-09-04 — Initial design approved
|
||||||
|
|
||||||
- Player scale defaults to a configurable range of `0.4` through `2.0`.
|
- Player scale defaults to a configurable range of `0.4` through `2.0`.
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ description: Catalog of user stories for the Spigot Heights plugin.
|
|||||||
3. [US-003: Launch tiny players through dispensers](us-003-launch-tiny-players.md)
|
3. [US-003: Launch tiny players through dispensers](us-003-launch-tiny-players.md)
|
||||||
4. [US-004: Configure and persist stature behavior](us-004-configure-and-persist.md)
|
4. [US-004: Configure and persist stature behavior](us-004-configure-and-persist.md)
|
||||||
5. [US-005: Build and release the plugin](us-005-build-and-release.md)
|
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)
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ As a **server operator**, I want validated stature and launcher settings with du
|
|||||||
- [x] Player scales are stored by UUID using atomic file replacement where supported.
|
- [x] Player scales are stored by UUID using atomic file replacement where supported.
|
||||||
- [x] Updating known state preserves unknown forward-compatible YAML fields.
|
- [x] Updating known state preserves unknown forward-compatible YAML fields.
|
||||||
- [x] Missing state defaults safely to scale `1.0` clamped to the configured range.
|
- [x] Missing state defaults safely to scale `1.0` clamped to the configured range.
|
||||||
- [x] Saved out-of-range state is clamped before it is applied.
|
- [x] Saved out-of-range state is clamped before it is applied, except exact scale `1.0`, which is preserved for restoration.
|
||||||
- [x] Configuration and state behavior have automated tests.
|
- [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.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [User-story catalog](index.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)
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
type: User Story
|
||||||
|
title: "US-006: Restore default stature"
|
||||||
|
description: Let players craft a potion that permanently restores their scale to 1.0.
|
||||||
|
status: done
|
||||||
|
---
|
||||||
|
|
||||||
|
# US-006: Restore default stature
|
||||||
|
|
||||||
|
As a **player**, I want a Potion of Restoration so that I can return to normal size.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] Drinking the potion sets scale to exactly `1.0`, regardless of configured limits.
|
||||||
|
- [x] The potion uses distinct persistent metadata rather than its display name for identity.
|
||||||
|
- [x] The new scale is reported and saved by UUID, and retained across reconnects and respawns.
|
||||||
|
- [x] The recipe is `GAG/ASA/GUG`, where `G` is Gold Ingot, `A` is Amethyst Shard, `S` is an authenticated Potion of Shifting Stature, and `U` is Sugar.
|
||||||
|
- [x] Automated tests cover restoration, saved-scale handling, recipe shape, and distinct identity; the README documents the recipe and behavior.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `./gradlew clean check jar` passed all 17 tests.
|
||||||
|
- Automated coverage checks scale calculation, storage reload, range exceptions, recipe shape, and enum identity. Code inspection confirms metadata authentication, ingredient registration, and the shared consumption/join/respawn adapters; no live-server gameplay test was performed.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [US-002: Make precise stature adjustments](us-002-adjust-stature.md)
|
||||||
|
- [US-004: Configure and persist stature behavior](us-004-configure-and-persist.md)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
type: User Story
|
||||||
|
title: "US-007: Manage stature settings with commands"
|
||||||
|
description: Let administrators view and persistently change stature bounds with tab-completed commands.
|
||||||
|
status: done
|
||||||
|
---
|
||||||
|
|
||||||
|
# US-007: Manage stature settings with commands
|
||||||
|
|
||||||
|
As a **server administrator**, I want commands with tab completion so that I can change stature limits without restarting the server.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] `/heights settings` displays current settings; `/heights set min|max <value>` changes the selected bound.
|
||||||
|
- [x] Operators and senders with `spigotheights.admin` may use the commands, including console; unauthorized senders cannot change settings or receive suggestions.
|
||||||
|
- [x] Tab completion suggests subcommands, `min`/`max`, and valid example values, filtered by the typed prefix.
|
||||||
|
- [x] Successful changes apply immediately to gameplay and persist to `plugins/SpigotHeights/config.yml` without discarding unrelated configuration.
|
||||||
|
- [x] Invalid arguments or settings produce clear explanations and leave active and saved settings unchanged; persistence failure does not activate the new settings.
|
||||||
|
- [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.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `./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-004: Configure and persist stature behavior](us-004-configure-and-persist.md)
|
||||||
|
- [US-006: Restore default stature](us-006-restore-default-stature.md)
|
||||||
@@ -27,7 +27,8 @@ public final class HeightMath {
|
|||||||
if (stored == null || !Double.isFinite(stored)) {
|
if (stored == null || !Double.isFinite(stored)) {
|
||||||
return clamp(1.0, settings);
|
return clamp(1.0, settings);
|
||||||
}
|
}
|
||||||
return clamp(stored, settings);
|
// Restoration is an explicit escape from configured stature limits.
|
||||||
|
return stored == 1.0 ? 1.0 : clamp(stored, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static double clamp(double value, HeightSettings settings) {
|
public static double clamp(double value, HeightSettings settings) {
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package games.dmg.spigotheights;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
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 final LiveHeightSettings settings;
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
|
public HeightsCommand(LiveHeightSettings settings, Logger logger) {
|
||||||
|
this.settings = settings;
|
||||||
|
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.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (args.length == 1 && args[0].equalsIgnoreCase("settings")) {
|
||||||
|
HeightSettings current = settings.get();
|
||||||
|
sender.sendMessage("Spigot Heights: min=" + current.minimum() + ", max=" + current.maximum()
|
||||||
|
+ ", adjustment-step=" + current.adjustmentStep()
|
||||||
|
+ ", launcher-threshold=" + current.launcherThreshold()
|
||||||
|
+ ", launcher-speed=" + current.launcherSpeed()
|
||||||
|
+ ", launcher-cooldown-ticks=" + current.launcherCooldownTicks());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (args.length != 3 || !args[0].equalsIgnoreCase("set") || !isBound(args[1])) {
|
||||||
|
sender.sendMessage(USAGE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String bound = args[1].toLowerCase(Locale.ROOT);
|
||||||
|
double value = Double.parseDouble(args[2]);
|
||||||
|
settings.setBound(bound, value);
|
||||||
|
sender.sendMessage("Set " + bound + " to " + value + ". Saved and active; existing players were not resized.");
|
||||||
|
} catch (NumberFormatException exception) {
|
||||||
|
sender.sendMessage("Invalid value: enter a finite number, for example 0.2 or 3.0.");
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
sender.sendMessage("Invalid settings: " + exception.getMessage());
|
||||||
|
} catch (IOException exception) {
|
||||||
|
logger.warning("Could not save height settings: " + exception.getMessage());
|
||||||
|
sender.sendMessage("Could not save settings. Active settings are 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]);
|
||||||
|
}
|
||||||
|
if (args.length == 2 && args[0].equalsIgnoreCase("set")) {
|
||||||
|
return matching(Stream.of("min", "max"), args[1]);
|
||||||
|
}
|
||||||
|
if (args.length == 3 && args[0].equalsIgnoreCase("set") && isBound(args[1])) {
|
||||||
|
String bound = args[1].toLowerCase(Locale.ROOT);
|
||||||
|
HeightSettings current = settings.get();
|
||||||
|
return matching(Stream.of(current.minimum(), current.maximum(), current.launcherThreshold(),
|
||||||
|
0.0625, 0.2, 0.4, 0.5, 1.0, 2.0, 3.0, 16.0)
|
||||||
|
.distinct().sorted().filter(value -> isValid(bound, value)).map(String::valueOf), args[2]);
|
||||||
|
}
|
||||||
|
// Never fall back to Bukkit's player-name completion.
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValid(String bound, double value) {
|
||||||
|
try {
|
||||||
|
settings.candidate(bound, value);
|
||||||
|
return true;
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBound(String value) {
|
||||||
|
return value.equalsIgnoreCase("min") || value.equalsIgnoreCase("max");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> matching(Stream<String> candidates, String prefix) {
|
||||||
|
String normalized = prefix.toLowerCase(Locale.ROOT);
|
||||||
|
return candidates.filter(value -> value.startsWith(normalized)).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package games.dmg.spigotheights;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/** Server-thread settings shared by commands and gameplay listeners. */
|
||||||
|
public final class LiveHeightSettings implements Supplier<HeightSettings> {
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Saver {
|
||||||
|
void save(HeightSettings settings) throws IOException;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HeightSettings current;
|
||||||
|
private final Saver saver;
|
||||||
|
|
||||||
|
public LiveHeightSettings(HeightSettings initial, Saver saver) {
|
||||||
|
this.current = initial;
|
||||||
|
this.saver = saver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HeightSettings get() {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HeightSettings candidate(String bound, double value) {
|
||||||
|
if (!bound.equals("min") && !bound.equals("max")) {
|
||||||
|
throw new IllegalArgumentException("Choose min or max.");
|
||||||
|
}
|
||||||
|
return new HeightSettings(
|
||||||
|
bound.equals("min") ? value : current.minimum(),
|
||||||
|
bound.equals("max") ? value : current.maximum(),
|
||||||
|
current.adjustmentStep(), current.launcherThreshold(),
|
||||||
|
current.launcherSpeed(), current.launcherCooldownTicks());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBound(String bound, double value) throws IOException {
|
||||||
|
HeightSettings next = candidate(bound, value);
|
||||||
|
saver.save(next);
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ public final class PotionRecipes {
|
|||||||
static final String[] SHIFTING_SHAPE = {"ACA", "AWA", "ACA"};
|
static final String[] SHIFTING_SHAPE = {"ACA", "AWA", "ACA"};
|
||||||
static final String[] GROWTH_SHAPE = {"GAG", "ASA", "GRG"};
|
static final String[] GROWTH_SHAPE = {"GAG", "ASA", "GRG"};
|
||||||
static final String[] DIMINUTION_SHAPE = {"GAG", "ASA", "GFG"};
|
static final String[] DIMINUTION_SHAPE = {"GAG", "ASA", "GFG"};
|
||||||
|
static final String[] RESTORATION_SHAPE = {"GAG", "ASA", "GUG"};
|
||||||
|
|
||||||
private final JavaPlugin plugin;
|
private final JavaPlugin plugin;
|
||||||
private final NamespacedKey potionKindKey;
|
private final NamespacedKey potionKindKey;
|
||||||
@@ -30,6 +31,7 @@ public final class PotionRecipes {
|
|||||||
registerShifting();
|
registerShifting();
|
||||||
registerGrowth();
|
registerGrowth();
|
||||||
registerDiminution();
|
registerDiminution();
|
||||||
|
registerRestoration();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ItemStack create(StaturePotion kind) {
|
public ItemStack create(StaturePotion kind) {
|
||||||
@@ -96,11 +98,23 @@ public final class PotionRecipes {
|
|||||||
plugin.getServer().addRecipe(recipe);
|
plugin.getServer().addRecipe(recipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void registerRestoration() {
|
||||||
|
ShapedRecipe recipe = new ShapedRecipe(new NamespacedKey(plugin, "restoration"),
|
||||||
|
create(StaturePotion.RESTORATION));
|
||||||
|
recipe.shape(RESTORATION_SHAPE);
|
||||||
|
recipe.setIngredient('G', Material.GOLD_INGOT);
|
||||||
|
recipe.setIngredient('A', Material.AMETHYST_SHARD);
|
||||||
|
recipe.setIngredient('S', new RecipeChoice.ExactChoice(create(StaturePotion.SHIFTING)));
|
||||||
|
recipe.setIngredient('U', Material.SUGAR);
|
||||||
|
plugin.getServer().addRecipe(recipe);
|
||||||
|
}
|
||||||
|
|
||||||
private static Color color(StaturePotion kind) {
|
private static Color color(StaturePotion kind) {
|
||||||
return switch (kind) {
|
return switch (kind) {
|
||||||
case SHIFTING -> Color.PURPLE;
|
case SHIFTING -> Color.PURPLE;
|
||||||
case GROWTH -> Color.LIME;
|
case GROWTH -> Color.LIME;
|
||||||
case DIMINUTION -> Color.FUCHSIA;
|
case DIMINUTION -> Color.FUCHSIA;
|
||||||
|
case RESTORATION -> Color.WHITE;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package games.dmg.spigotheights;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.AtomicMoveNotSupportedException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import org.bukkit.configuration.InvalidConfigurationException;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
|
||||||
|
public final class SettingsFileStore {
|
||||||
|
private final Path path;
|
||||||
|
|
||||||
|
public SettingsFileStore(Path path) {
|
||||||
|
this.path = path.toAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(HeightSettings settings) throws IOException {
|
||||||
|
// Read a separate document so failed writes cannot mutate the active configuration.
|
||||||
|
YamlConfiguration config = new YamlConfiguration();
|
||||||
|
try {
|
||||||
|
config.load(path.toFile());
|
||||||
|
} catch (InvalidConfigurationException exception) {
|
||||||
|
throw new IOException("Existing config.yml is invalid; refusing to overwrite it", exception);
|
||||||
|
}
|
||||||
|
config.set("height.minimum", settings.minimum());
|
||||||
|
config.set("height.maximum", settings.maximum());
|
||||||
|
Path temporary = Files.createTempFile(path.getParent(), "config-", ".yml.tmp");
|
||||||
|
try {
|
||||||
|
Files.writeString(temporary, config.saveToString(), StandardCharsets.UTF_8);
|
||||||
|
try {
|
||||||
|
Files.move(temporary, path, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} catch (AtomicMoveNotSupportedException exception) {
|
||||||
|
Files.move(temporary, path, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(temporary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package games.dmg.spigotheights;
|
package games.dmg.spigotheights;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import org.bukkit.command.PluginCommand;
|
||||||
import org.bukkit.configuration.file.FileConfiguration;
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
@@ -16,12 +18,23 @@ public final class SpigotHeightsPlugin extends JavaPlugin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsFileStore configStore = new SettingsFileStore(getDataFolder().toPath().resolve("config.yml"));
|
||||||
|
LiveHeightSettings liveSettings = new LiveHeightSettings(settings, next -> {
|
||||||
|
configStore.save(next);
|
||||||
|
getConfig().set("height.minimum", next.minimum());
|
||||||
|
getConfig().set("height.maximum", next.maximum());
|
||||||
|
});
|
||||||
|
HeightsCommand executor = new HeightsCommand(liveSettings, getLogger());
|
||||||
|
PluginCommand command = Objects.requireNonNull(getCommand("heights"), "Missing heights command declaration");
|
||||||
|
command.setExecutor(executor);
|
||||||
|
command.setTabCompleter(executor);
|
||||||
|
|
||||||
HeightStore store = new HeightStore(getDataFolder());
|
HeightStore store = new HeightStore(getDataFolder());
|
||||||
PotionRecipes potions = new PotionRecipes(this);
|
PotionRecipes potions = new PotionRecipes(this);
|
||||||
potions.register();
|
potions.register();
|
||||||
getServer().getPluginManager().registerEvents(
|
getServer().getPluginManager().registerEvents(
|
||||||
new StatureListener(this, settings, store, potions), this);
|
new StatureListener(this, liveSettings, store, potions), this);
|
||||||
getServer().getPluginManager().registerEvents(new TinyPlayerLauncher(settings), this);
|
getServer().getPluginManager().registerEvents(new TinyPlayerLauncher(liveSettings), this);
|
||||||
getLogger().info("Spigot Heights enabled.");
|
getLogger().info("Spigot Heights enabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package games.dmg.spigotheights;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import org.bukkit.attribute.Attribute;
|
import org.bukkit.attribute.Attribute;
|
||||||
import org.bukkit.attribute.AttributeInstance;
|
import org.bukkit.attribute.AttributeInstance;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
@@ -19,11 +20,11 @@ import org.bukkit.plugin.java.JavaPlugin;
|
|||||||
|
|
||||||
public final class StatureListener implements Listener {
|
public final class StatureListener implements Listener {
|
||||||
private final JavaPlugin plugin;
|
private final JavaPlugin plugin;
|
||||||
private final HeightSettings settings;
|
private final Supplier<HeightSettings> settings;
|
||||||
private final HeightStore store;
|
private final HeightStore store;
|
||||||
private final PotionRecipes potions;
|
private final PotionRecipes potions;
|
||||||
|
|
||||||
public StatureListener(JavaPlugin plugin, HeightSettings settings, HeightStore store, PotionRecipes potions) {
|
public StatureListener(JavaPlugin plugin, Supplier<HeightSettings> settings, HeightStore store, PotionRecipes potions) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.store = store;
|
this.store = store;
|
||||||
@@ -50,26 +51,22 @@ public final class StatureListener implements Listener {
|
|||||||
}
|
}
|
||||||
Player player = event.getPlayer();
|
Player player = event.getPlayer();
|
||||||
double current = currentScale(player);
|
double current = currentScale(player);
|
||||||
double scale = switch (kind) {
|
double scale = kind.scaleAfterDrinking(current, settings.get(),
|
||||||
case SHIFTING -> HeightMath.randomScale(settings,
|
bound -> ThreadLocalRandom.current().nextInt(bound));
|
||||||
bound -> ThreadLocalRandom.current().nextInt(bound));
|
|
||||||
case GROWTH -> HeightMath.grow(current, settings);
|
|
||||||
case DIMINUTION -> HeightMath.shrink(current, settings);
|
|
||||||
};
|
|
||||||
applyAndSave(player, scale);
|
applyAndSave(player, scale);
|
||||||
player.sendMessage("Your scale is now " + scale + ".");
|
player.sendMessage("Your scale is now " + scale + ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onJoin(PlayerJoinEvent event) {
|
public void onJoin(PlayerJoinEvent event) {
|
||||||
apply(event.getPlayer(), HeightMath.safeStoredScale(store.find(event.getPlayer().getUniqueId()), settings));
|
apply(event.getPlayer(), HeightMath.safeStoredScale(store.find(event.getPlayer().getUniqueId()), settings.get()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onRespawn(PlayerRespawnEvent event) {
|
public void onRespawn(PlayerRespawnEvent event) {
|
||||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||||
Player player = event.getPlayer();
|
Player player = event.getPlayer();
|
||||||
apply(player, HeightMath.safeStoredScale(store.find(player.getUniqueId()), settings));
|
apply(player, HeightMath.safeStoredScale(store.find(player.getUniqueId()), settings.get()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package games.dmg.spigotheights;
|
package games.dmg.spigotheights;
|
||||||
|
|
||||||
|
import java.util.function.IntUnaryOperator;
|
||||||
|
|
||||||
public enum StaturePotion {
|
public enum StaturePotion {
|
||||||
SHIFTING("Potion of Shifting Stature"),
|
SHIFTING("Potion of Shifting Stature"),
|
||||||
GROWTH("Potion of Growth"),
|
GROWTH("Potion of Growth"),
|
||||||
DIMINUTION("Potion of Diminution");
|
DIMINUTION("Potion of Diminution"),
|
||||||
|
RESTORATION("Potion of Restoration");
|
||||||
|
|
||||||
private final String displayName;
|
private final String displayName;
|
||||||
|
|
||||||
@@ -11,6 +14,15 @@ public enum StaturePotion {
|
|||||||
this.displayName = displayName;
|
this.displayName = displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double scaleAfterDrinking(double current, HeightSettings settings, IntUnaryOperator randomIndex) {
|
||||||
|
return switch (this) {
|
||||||
|
case SHIFTING -> HeightMath.randomScale(settings, randomIndex);
|
||||||
|
case GROWTH -> HeightMath.grow(current, settings);
|
||||||
|
case DIMINUTION -> HeightMath.shrink(current, settings);
|
||||||
|
case RESTORATION -> 1.0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public String displayName() {
|
public String displayName() {
|
||||||
return displayName;
|
return displayName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package games.dmg.spigotheights;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
@@ -19,11 +20,11 @@ import org.bukkit.event.player.PlayerMoveEvent;
|
|||||||
import org.bukkit.util.Vector;
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
public final class TinyPlayerLauncher implements Listener {
|
public final class TinyPlayerLauncher implements Listener {
|
||||||
private final HeightSettings settings;
|
private final Supplier<HeightSettings> settingsSupplier;
|
||||||
private final Map<UUID, Long> lastLaunchTicks = new HashMap<>();
|
private final Map<UUID, Long> lastLaunchTicks = new HashMap<>();
|
||||||
|
|
||||||
public TinyPlayerLauncher(HeightSettings settings) {
|
public TinyPlayerLauncher(Supplier<HeightSettings> settingsSupplier) {
|
||||||
this.settings = settings;
|
this.settingsSupplier = settingsSupplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler(ignoreCancelled = true)
|
@EventHandler(ignoreCancelled = true)
|
||||||
@@ -32,6 +33,7 @@ public final class TinyPlayerLauncher implements Listener {
|
|||||||
if (destination == null || sameBlock(event.getFrom(), destination)) {
|
if (destination == null || sameBlock(event.getFrom(), destination)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
HeightSettings settings = settingsSupplier.get();
|
||||||
Player player = event.getPlayer();
|
Player player = event.getPlayer();
|
||||||
if (!LauncherPolicy.isSmallEnough(scale(player), settings.launcherThreshold())) {
|
if (!LauncherPolicy.isSmallEnough(scale(player), settings.launcherThreshold())) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4,3 +4,12 @@ main: games.dmg.spigotheights.SpigotHeightsPlugin
|
|||||||
api-version: "1.21"
|
api-version: "1.21"
|
||||||
description: Craftable player stature potions and tiny-player dispenser launchers.
|
description: Craftable player stature potions and tiny-player dispenser launchers.
|
||||||
author: dmg.games
|
author: dmg.games
|
||||||
|
commands:
|
||||||
|
heights:
|
||||||
|
description: View and change Spigot Heights stature limits.
|
||||||
|
usage: /heights settings | /heights set min|max <value>
|
||||||
|
permission: spigotheights.admin
|
||||||
|
permissions:
|
||||||
|
spigotheights.admin:
|
||||||
|
description: View and change Spigot Heights settings.
|
||||||
|
default: op
|
||||||
|
|||||||
@@ -20,6 +20,20 @@ class HeightMathTest {
|
|||||||
assertEquals(1.1, HeightMath.grow(1.0, SETTINGS), 0.000001);
|
assertEquals(1.1, HeightMath.grow(1.0, SETTINGS), 0.000001);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void savedDefaultSizeSurvivesRangesThatExcludeIt() {
|
||||||
|
for (HeightSettings settings : new HeightSettings[] {
|
||||||
|
new HeightSettings(0.2, 0.8, 0.1, 0.5, 1.5, 20),
|
||||||
|
new HeightSettings(1.2, 2.0, 0.1, 1.5, 1.5, 20)}) {
|
||||||
|
assertEquals(1.0, HeightMath.safeStoredScale(1.0, settings));
|
||||||
|
assertEquals(settings.minimum(), HeightMath.safeStoredScale(0.1, settings));
|
||||||
|
assertEquals(settings.maximum(), HeightMath.safeStoredScale(3.0, settings));
|
||||||
|
double fallback = HeightMath.clamp(1.0, settings);
|
||||||
|
assertEquals(fallback, HeightMath.safeStoredScale(null, settings));
|
||||||
|
assertEquals(fallback, HeightMath.safeStoredScale(Double.NaN, settings));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void missingAndStoredValuesAreSafelyClamped() {
|
void missingAndStoredValuesAreSafelyClamped() {
|
||||||
assertEquals(1.0, HeightMath.safeStoredScale(null, SETTINGS), 0.000001);
|
assertEquals(1.0, HeightMath.safeStoredScale(null, SETTINGS), 0.000001);
|
||||||
|
|||||||
@@ -12,6 +12,19 @@ class HeightStoreTest {
|
|||||||
@TempDir
|
@TempDir
|
||||||
Path temporaryDirectory;
|
Path temporaryDirectory;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void restorationReplacesPreviousSizeAndSurvivesReloadOutsideLimits() throws Exception {
|
||||||
|
UUID playerId = UUID.randomUUID();
|
||||||
|
HeightSettings settings = new HeightSettings(0.2, 0.8, 0.1, 0.5, 1.5, 20);
|
||||||
|
HeightStore store = new HeightStore(temporaryDirectory.toFile());
|
||||||
|
store.save(playerId, 0.4);
|
||||||
|
store.save(playerId, StaturePotion.RESTORATION.scaleAfterDrinking(0.4, settings, bound -> 0));
|
||||||
|
|
||||||
|
HeightStore reloaded = new HeightStore(temporaryDirectory.toFile());
|
||||||
|
assertEquals(1.0, reloaded.find(playerId));
|
||||||
|
assertEquals(1.0, HeightMath.safeStoredScale(reloaded.find(playerId), settings));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void storesByUuidAndPreservesUnknownYamlFields() throws Exception {
|
void storesByUuidAndPreservesUnknownYamlFields() throws Exception {
|
||||||
UUID playerId = UUID.randomUUID();
|
UUID playerId = UUID.randomUUID();
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package games.dmg.spigotheights;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void authorizedSenderCanInspectAndChangeBounds() {
|
||||||
|
CommandSender sender = sender(true);
|
||||||
|
command.onCommand(sender, null, "heights", new String[] {"settings"});
|
||||||
|
assertTrue(messages.getLast().contains("min=0.4"));
|
||||||
|
assertTrue(messages.getLast().contains("max=2.0"));
|
||||||
|
command.onCommand(sender, null, "heights", new String[] {"set", "min", "0.2"});
|
||||||
|
command.onCommand(sender, null, "heights", new String[] {"set", "max", "3"});
|
||||||
|
assertEquals(0.2, live.get().minimum());
|
||||||
|
assertEquals(3.0, live.get().maximum());
|
||||||
|
assertEquals(2, saved.size());
|
||||||
|
assertTrue(messages.getLast().contains("3.0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unauthorizedSenderCannotInspectMutateOrComplete() {
|
||||||
|
CommandSender sender = sender(false);
|
||||||
|
command.onCommand(sender, null, "heights", new String[] {"set", "min", "0.2"});
|
||||||
|
command.onCommand(sender, null, "heights", new String[] {"settings"});
|
||||||
|
assertTrue(messages.stream().allMatch(message -> message.contains("permission")));
|
||||||
|
assertEquals(List.of(), saved);
|
||||||
|
assertEquals(List.of(), command.onTabComplete(sender, null, "heights", new String[] {""}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void malformedAndInvalidInputNeverSaves() {
|
||||||
|
for (String[] args : new String[][] {
|
||||||
|
{}, {"set"}, {"set", "min"}, {"set", "min", "0.2", "extra"},
|
||||||
|
{"settings", "extra"}, {"unknown"}, {"set", "speed", "1"},
|
||||||
|
{"set", "min", "nope"}, {"set", "min", "NaN"},
|
||||||
|
{"set", "max", "Infinity"}, {"set", "min", "0.6"}, {"set", "max", "0.3"}}) {
|
||||||
|
int before = messages.size();
|
||||||
|
assertTrue(command.onCommand(sender(true), null, "heights", args));
|
||||||
|
assertEquals(before + 1, messages.size());
|
||||||
|
}
|
||||||
|
assertEquals(List.of(), saved);
|
||||||
|
assertEquals(0.4, live.get().minimum());
|
||||||
|
assertEquals(2.0, live.get().maximum());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void completionsArePrefixFilteredAndValidForCurrentRange() throws Exception {
|
||||||
|
CommandSender sender = sender(true);
|
||||||
|
assertEquals(List.of("settings", "set"), complete(sender, "s"));
|
||||||
|
assertEquals(List.of("settings"), complete(sender, "sett"));
|
||||||
|
assertEquals(List.of("min", "max"), complete(sender, "set", "m"));
|
||||||
|
assertEquals(List.of("max"), complete(sender, "set", "ma"));
|
||||||
|
assertEquals(List.of(), complete(sender, "settings", ""));
|
||||||
|
assertEquals(List.of(), complete(sender, "set", "other", ""));
|
||||||
|
assertEquals(List.of(), complete(sender, "set", "min", "0.2", ""));
|
||||||
|
assertTrue(complete(sender, "set", "min", "0.").contains("0.2"));
|
||||||
|
for (String bound : List.of("min", "max")) {
|
||||||
|
for (String value : complete(sender, "set", bound, "")) {
|
||||||
|
live.candidate(bound, Double.parseDouble(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
live.setBound("max", 0.8);
|
||||||
|
assertTrue(complete(sender, "set", "max", "0.").contains("0.8"));
|
||||||
|
assertEquals(List.of(), complete(sender, "set", "min", "1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveFailureIsReportedWithoutChangingActiveSettings() {
|
||||||
|
LiveHeightSettings failing = new LiveHeightSettings(live.get(), settings -> {
|
||||||
|
throw new IOException("test save failure");
|
||||||
|
});
|
||||||
|
new HeightsCommand(failing, 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> complete(CommandSender sender, String... args) {
|
||||||
|
return command.onTabComplete(sender, null, "heights", args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CommandSender sender(boolean permitted) {
|
||||||
|
return (CommandSender) Proxy.newProxyInstance(CommandSender.class.getClassLoader(),
|
||||||
|
new Class<?>[] {CommandSender.class}, (proxy, method, args) -> {
|
||||||
|
if (method.getName().equals("hasPermission")) {
|
||||||
|
assertEquals("spigotheights.admin", args[0]);
|
||||||
|
return permitted;
|
||||||
|
}
|
||||||
|
if (method.getName().equals("sendMessage") && args[0] instanceof String text) {
|
||||||
|
messages.add(text);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException(method.getName());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package games.dmg.spigotheights;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class LiveHeightSettingsTest {
|
||||||
|
private static final HeightSettings DEFAULTS = new HeightSettings(0.4, 2.0, 0.1, 0.5, 1.5, 20);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void savesBeforePublishingAndGameplayReadsNewBounds() throws Exception {
|
||||||
|
List<HeightSettings> saved = new ArrayList<>();
|
||||||
|
LiveHeightSettings live = new LiveHeightSettings(DEFAULTS, saved::add);
|
||||||
|
live.setBound("min", 0.2);
|
||||||
|
live.setBound("max", 3.0);
|
||||||
|
assertEquals(2, saved.size());
|
||||||
|
assertSame(saved.getLast(), live.get());
|
||||||
|
assertEquals(0.2, HeightMath.randomScale(live.get(), bound -> 0));
|
||||||
|
assertEquals(3.0, HeightMath.grow(3.0, live.get()));
|
||||||
|
assertEquals(0.2, HeightMath.safeStoredScale(0.1, live.get()));
|
||||||
|
assertEquals(1.0, HeightMath.safeStoredScale(1.0, live.get()));
|
||||||
|
assertEquals(0.1, live.get().adjustmentStep());
|
||||||
|
assertEquals(0.5, live.get().launcherThreshold());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidChangesAndSaveFailuresDoNotPublish() {
|
||||||
|
List<HeightSettings> saved = new ArrayList<>();
|
||||||
|
LiveHeightSettings live = new LiveHeightSettings(DEFAULTS, saved::add);
|
||||||
|
for (double value : new double[] {Double.NaN, Double.POSITIVE_INFINITY, -1, 0, 0.01, 0.6, 3}) {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> live.setBound("min", value));
|
||||||
|
}
|
||||||
|
for (double value : new double[] {0.3, 0.45, 17, Double.NEGATIVE_INFINITY}) {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> live.setBound("max", value));
|
||||||
|
}
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> live.setBound("speed", 1));
|
||||||
|
assertEquals(List.of(), saved);
|
||||||
|
assertSame(DEFAULTS, live.get());
|
||||||
|
LiveHeightSettings failing = new LiveHeightSettings(DEFAULTS, settings -> {
|
||||||
|
throw new IOException("disk full");
|
||||||
|
});
|
||||||
|
assertThrows(IOException.class, () -> failing.setBound("min", 0.2));
|
||||||
|
assertSame(DEFAULTS, failing.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package games.dmg.spigotheights;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
@@ -13,6 +14,17 @@ class PotionRecipesTest {
|
|||||||
assertArrayEquals(new String[] {"GAG", "ASA", "GFG"}, PotionRecipes.DIMINUTION_SHAPE);
|
assertArrayEquals(new String[] {"GAG", "ASA", "GFG"}, PotionRecipes.DIMINUTION_SHAPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void restorationUsesApprovedShapeAndDistinctIdentity() {
|
||||||
|
assertArrayEquals(new String[] {"GAG", "ASA", "GUG"}, PotionRecipes.RESTORATION_SHAPE);
|
||||||
|
assertEquals("RESTORATION", StaturePotion.RESTORATION.name());
|
||||||
|
for (StaturePotion kind : StaturePotion.values()) {
|
||||||
|
if (kind != StaturePotion.RESTORATION) {
|
||||||
|
assertNotEquals(kind.name(), StaturePotion.RESTORATION.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void everyPotionHasASeparatePersistentIdentity() {
|
void everyPotionHasASeparatePersistentIdentity() {
|
||||||
assertNotEquals(StaturePotion.SHIFTING.name(), StaturePotion.GROWTH.name());
|
assertNotEquals(StaturePotion.SHIFTING.name(), StaturePotion.GROWTH.name());
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
class SettingsFileStoreTest {
|
||||||
|
@TempDir
|
||||||
|
Path directory;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void savedBoundsSurviveReloadAndPreserveOtherConfiguration() throws Exception {
|
||||||
|
Path config = directory.resolve("config.yml");
|
||||||
|
try (var defaults = getClass().getResourceAsStream("/config.yml")) {
|
||||||
|
Files.copy(defaults, config);
|
||||||
|
}
|
||||||
|
Files.writeString(config, Files.readString(config) + "\nfuture-setting: retained\n");
|
||||||
|
HeightSettings initial = SpigotHeightsPlugin.loadSettings(YamlConfiguration.loadConfiguration(config.toFile()));
|
||||||
|
LiveHeightSettings live = new LiveHeightSettings(initial, new SettingsFileStore(config)::save);
|
||||||
|
live.setBound("min", 0.2);
|
||||||
|
live.setBound("max", 3.0);
|
||||||
|
YamlConfiguration reloaded = YamlConfiguration.loadConfiguration(config.toFile());
|
||||||
|
assertEquals(live.get(), SpigotHeightsPlugin.loadSettings(reloaded));
|
||||||
|
assertEquals("retained", reloaded.getString("future-setting"));
|
||||||
|
try (var files = Files.list(directory)) {
|
||||||
|
assertEquals(1, files.count());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void malformedConfigurationIsNotOverwrittenOrActivated() throws Exception {
|
||||||
|
Path config = directory.resolve("config.yml");
|
||||||
|
String malformed = "height: [unterminated";
|
||||||
|
Files.writeString(config, malformed);
|
||||||
|
HeightSettings initial = new HeightSettings(0.4, 2.0, 0.1, 0.5, 1.5, 20);
|
||||||
|
LiveHeightSettings live = new LiveHeightSettings(initial, new SettingsFileStore(config)::save);
|
||||||
|
assertThrows(IOException.class, () -> live.setBound("min", 0.2));
|
||||||
|
assertEquals(initial, live.get());
|
||||||
|
assertEquals(malformed, Files.readString(config));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package games.dmg.spigotheights;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class StaturePotionTest {
|
||||||
|
@Test
|
||||||
|
void existingPotionsKeepTheirScaleRules() {
|
||||||
|
HeightSettings settings = new HeightSettings(0.4, 2.0, 0.1, 0.5, 1.5, 20);
|
||||||
|
assertEquals(0.4, StaturePotion.SHIFTING.scaleAfterDrinking(1.0, settings, bound -> 0));
|
||||||
|
assertEquals(2.0, StaturePotion.SHIFTING.scaleAfterDrinking(1.0, settings, bound -> bound - 1));
|
||||||
|
assertEquals(1.1, StaturePotion.GROWTH.scaleAfterDrinking(1.0, settings, bound -> 0));
|
||||||
|
assertEquals(0.9, StaturePotion.DIMINUTION.scaleAfterDrinking(1.0, settings, bound -> 0));
|
||||||
|
assertEquals(2.0, StaturePotion.GROWTH.scaleAfterDrinking(2.0, settings, bound -> 0));
|
||||||
|
assertEquals(0.4, StaturePotion.DIMINUTION.scaleAfterDrinking(0.4, settings, bound -> 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void restorationAlwaysReturnsNormalSizeWithoutRandomness() {
|
||||||
|
for (HeightSettings settings : new HeightSettings[] {
|
||||||
|
new HeightSettings(0.4, 2.0, 0.1, 0.5, 1.5, 20),
|
||||||
|
new HeightSettings(0.2, 0.8, 0.1, 0.5, 1.5, 20),
|
||||||
|
new HeightSettings(1.2, 2.0, 0.1, 1.5, 1.5, 20)}) {
|
||||||
|
for (double current : new double[] {0.2, 1.0, 2.0}) {
|
||||||
|
assertEquals(1.0, StaturePotion.RESTORATION.scaleAfterDrinking(current, settings,
|
||||||
|
bound -> { throw new AssertionError("Restoration must not roll a random size"); }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals("Potion of Restoration", StaturePotion.RESTORATION.displayName());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user