feat(commands): manage height bounds with tab completion
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
@@ -16,12 +18,23 @@ public final class SpigotHeightsPlugin extends JavaPlugin {
|
||||
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());
|
||||
PotionRecipes potions = new PotionRecipes(this);
|
||||
potions.register();
|
||||
getServer().getPluginManager().registerEvents(
|
||||
new StatureListener(this, settings, store, potions), this);
|
||||
getServer().getPluginManager().registerEvents(new TinyPlayerLauncher(settings), this);
|
||||
new StatureListener(this, liveSettings, store, potions), this);
|
||||
getServer().getPluginManager().registerEvents(new TinyPlayerLauncher(liveSettings), this);
|
||||
getLogger().info("Spigot Heights enabled.");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package games.dmg.spigotheights;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.function.Supplier;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.attribute.AttributeInstance;
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -19,11 +20,11 @@ import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public final class StatureListener implements Listener {
|
||||
private final JavaPlugin plugin;
|
||||
private final HeightSettings settings;
|
||||
private final Supplier<HeightSettings> settings;
|
||||
private final HeightStore store;
|
||||
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.settings = settings;
|
||||
this.store = store;
|
||||
@@ -50,7 +51,7 @@ public final class StatureListener implements Listener {
|
||||
}
|
||||
Player player = event.getPlayer();
|
||||
double current = currentScale(player);
|
||||
double scale = kind.scaleAfterDrinking(current, settings,
|
||||
double scale = kind.scaleAfterDrinking(current, settings.get(),
|
||||
bound -> ThreadLocalRandom.current().nextInt(bound));
|
||||
applyAndSave(player, scale);
|
||||
player.sendMessage("Your scale is now " + scale + ".");
|
||||
@@ -58,14 +59,14 @@ public final class StatureListener implements Listener {
|
||||
|
||||
@EventHandler
|
||||
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
|
||||
public void onRespawn(PlayerRespawnEvent event) {
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
Player player = event.getPlayer();
|
||||
apply(player, HeightMath.safeStoredScale(store.find(player.getUniqueId()), settings));
|
||||
apply(player, HeightMath.safeStoredScale(store.find(player.getUniqueId()), settings.get()));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package games.dmg.spigotheights;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
@@ -19,11 +20,11 @@ import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
public final class TinyPlayerLauncher implements Listener {
|
||||
private final HeightSettings settings;
|
||||
private final Supplier<HeightSettings> settingsSupplier;
|
||||
private final Map<UUID, Long> lastLaunchTicks = new HashMap<>();
|
||||
|
||||
public TinyPlayerLauncher(HeightSettings settings) {
|
||||
this.settings = settings;
|
||||
public TinyPlayerLauncher(Supplier<HeightSettings> settingsSupplier) {
|
||||
this.settingsSupplier = settingsSupplier;
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
@@ -32,6 +33,7 @@ public final class TinyPlayerLauncher implements Listener {
|
||||
if (destination == null || sameBlock(event.getFrom(), destination)) {
|
||||
return;
|
||||
}
|
||||
HeightSettings settings = settingsSupplier.get();
|
||||
Player player = event.getPlayer();
|
||||
if (!LauncherPolicy.isSmallEnough(scale(player), settings.launcherThreshold())) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user