From bfc53972b75d5de3f448cf633ccd15822a0175d7 Mon Sep 17 00:00:00 2001 From: kayjaydee Date: Fri, 24 Apr 2026 14:54:34 +0200 Subject: [PATCH] feat(04-04): add list/delete/toggle subcommands (CommandBase, CMD-03/04/05) - GravityFlipListSubCommand: iterate registry.all(), format 'name : (min) -> (max) [state]' - GravityFlipDeleteSubCommand: registry.remove + save, clear error on unknown name - GravityFlipToggleSubCommand: read current enabled, flip via setEnabled, save, report new state - All three extend CommandBase (works from server console, not just player) - Save-failure path: keep in-memory change, truthful message (mirror define pattern) --- .../command/GravityFlipDeleteSubCommand.java | 58 +++++++++++++++ .../command/GravityFlipListSubCommand.java | 56 +++++++++++++++ .../command/GravityFlipToggleSubCommand.java | 72 +++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 src/main/java/com/mythlane/gravityflip/command/GravityFlipDeleteSubCommand.java create mode 100644 src/main/java/com/mythlane/gravityflip/command/GravityFlipListSubCommand.java create mode 100644 src/main/java/com/mythlane/gravityflip/command/GravityFlipToggleSubCommand.java diff --git a/src/main/java/com/mythlane/gravityflip/command/GravityFlipDeleteSubCommand.java b/src/main/java/com/mythlane/gravityflip/command/GravityFlipDeleteSubCommand.java new file mode 100644 index 0000000..b7b9386 --- /dev/null +++ b/src/main/java/com/mythlane/gravityflip/command/GravityFlipDeleteSubCommand.java @@ -0,0 +1,58 @@ +package com.mythlane.gravityflip.command; + +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg; +import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; +import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase; +import com.mythlane.gravityflip.GravityFlipPlugin; + +import javax.annotation.Nonnull; +import java.util.logging.Level; + +/** + * {@code /gravityflip delete } — supprime la région nommée et persiste. + * + *

Étend {@link CommandBase} pour permettre l'usage depuis la console serveur + * (opération admin, pas besoin d'un joueur caller). + * + *

Flow : + *

    + *
  1. {@code registry.remove(name)} — renvoie {@code false} si nom inconnu → message + * d'erreur clair et retour précoce (pas de save inutile).
  2. + *
  3. {@code configHolder.save().join()} force la durabilité. Même pattern que + * {@code GravityFlipDefineSubCommand} : sur échec disque, on reporte truthfully + * ("en mémoire OK, persistance échouée") plutôt que silent rollback.
  4. + *
+ */ +public final class GravityFlipDeleteSubCommand extends CommandBase { + + private final GravityFlipPlugin plugin; + + private final RequiredArg nameArg = + this.withRequiredArg("name", "Nom de la région à supprimer", ArgTypes.STRING); + + public GravityFlipDeleteSubCommand(GravityFlipPlugin plugin) { + super("delete", "Supprime une région Gravity Flip"); + this.plugin = plugin; + } + + @Override + protected void executeSync(@Nonnull CommandContext ctx) { + String name = nameArg.get(ctx); + if (!plugin.regions().remove(name)) { + ctx.sendMessage(Message.raw("[gravityflip] Région '" + name + "' introuvable.")); + return; + } + try { + plugin.configHolder().save().join(); + } catch (Throwable th) { + plugin.getLogger().at(Level.WARNING).withCause(th) + .log("[delete] save failed for region '" + name + "'"); + ctx.sendMessage(Message.raw( + "[gravityflip] Suppression en mémoire OK, persistance échouée — voir logs.")); + return; + } + ctx.sendMessage(Message.raw("[gravityflip] Région '" + name + "' supprimée.")); + } +} diff --git a/src/main/java/com/mythlane/gravityflip/command/GravityFlipListSubCommand.java b/src/main/java/com/mythlane/gravityflip/command/GravityFlipListSubCommand.java new file mode 100644 index 0000000..65344e4 --- /dev/null +++ b/src/main/java/com/mythlane/gravityflip/command/GravityFlipListSubCommand.java @@ -0,0 +1,56 @@ +package com.mythlane.gravityflip.command; + +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase; +import com.mythlane.gravityflip.GravityFlipPlugin; +import com.mythlane.gravityflip.region.GravityFlipRegion; + +import javax.annotation.Nonnull; +import java.util.Collection; + +/** + * {@code /gravityflip list} — liste toutes les régions Gravity Flip persistées. + * + *

Étend {@link CommandBase} (et non {@code AbstractPlayerCommand}) pour + * fonctionner aussi depuis la console serveur : cette commande est admin-only + * de par sa permission auto-générée ({@code mythlane.gravityflip.command.gravityflip.list}), + * mais n'a pas besoin d'un {@code PlayerRef} — seulement de {@code ctx.sendMessage(...)}. + * + *

Format de sortie (une ligne par région) : + *

+ *   - test-zone-1 : (10,64,10) → (21,71,21) [enabled]
+ * 
+ * + *

Threat surface : T-04-04-01 (Information Disclosure) accepté — les coords + * des régions sont visibles pour tout opérateur avec la permission list ; c'est + * la spec attendue pour un outil builder. + */ +public final class GravityFlipListSubCommand extends CommandBase { + + private final GravityFlipPlugin plugin; + + public GravityFlipListSubCommand(GravityFlipPlugin plugin) { + super("list", "Liste toutes les régions Gravity Flip"); + this.plugin = plugin; + } + + @Override + protected void executeSync(@Nonnull CommandContext ctx) { + Collection all = plugin.regions().all(); + if (all.isEmpty()) { + ctx.sendMessage(Message.raw("[gravityflip] Aucune région définie.")); + return; + } + ctx.sendMessage(Message.raw("[gravityflip] " + all.size() + " région(s) :")); + for (GravityFlipRegion r : all) { + Vector3d mn = r.getMin(); + Vector3d mx = r.getMax(); + ctx.sendMessage(Message.raw(String.format( + " - %s : (%.0f,%.0f,%.0f) → (%.0f,%.0f,%.0f) [%s]", + r.getName(), mn.x, mn.y, mn.z, mx.x, mx.y, mx.z, + r.isEnabled() ? "enabled" : "disabled"))); + } + } +} diff --git a/src/main/java/com/mythlane/gravityflip/command/GravityFlipToggleSubCommand.java b/src/main/java/com/mythlane/gravityflip/command/GravityFlipToggleSubCommand.java new file mode 100644 index 0000000..9ca123a --- /dev/null +++ b/src/main/java/com/mythlane/gravityflip/command/GravityFlipToggleSubCommand.java @@ -0,0 +1,72 @@ +package com.mythlane.gravityflip.command; + +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg; +import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; +import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase; +import com.mythlane.gravityflip.GravityFlipPlugin; +import com.mythlane.gravityflip.region.GravityFlipRegion; + +import javax.annotation.Nonnull; +import java.util.logging.Level; + +/** + * {@code /gravityflip toggle } — flippe le flag {@code enabled} d'une région + * sans toucher aux corners ni aux autres champs. + * + *

Étend {@link CommandBase} (utilisable console + joueur). + * + *

Flow : lookup région → lecture {@code isEnabled()} → {@code registry.setEnabled(name, !current)} + * → {@code configHolder.save().join()} → message avec le nouvel état. + * + *

Race (T-04-04-04) : lookup + setEnabled non atomique entre eux. Deux toggles + * simultanés sur le même nom produisent un état final indéterminé mais cohérent + * sur disque ({@code RegionRegistry.setEnabled} est synchronisé sur {@code mutationLock}). + * Acceptable v1 (single-operator builder). + */ +public final class GravityFlipToggleSubCommand extends CommandBase { + + private final GravityFlipPlugin plugin; + + private final RequiredArg nameArg = + this.withRequiredArg("name", "Nom de la région à toggler", ArgTypes.STRING); + + public GravityFlipToggleSubCommand(GravityFlipPlugin plugin) { + super("toggle", "Active/désactive une région Gravity Flip"); + this.plugin = plugin; + } + + @Override + protected void executeSync(@Nonnull CommandContext ctx) { + String name = nameArg.get(ctx); + GravityFlipRegion found = null; + for (GravityFlipRegion r : plugin.regions().all()) { + if (r.getName().equals(name)) { + found = r; + break; + } + } + if (found == null) { + ctx.sendMessage(Message.raw("[gravityflip] Région '" + name + "' introuvable.")); + return; + } + boolean next = !found.isEnabled(); + if (!plugin.regions().setEnabled(name, next)) { + // Course ultra-rare : région supprimée entre all() et setEnabled(). + ctx.sendMessage(Message.raw("[gravityflip] Région '" + name + "' introuvable.")); + return; + } + try { + plugin.configHolder().save().join(); + } catch (Throwable th) { + plugin.getLogger().at(Level.WARNING).withCause(th) + .log("[toggle] save failed for region '" + name + "'"); + ctx.sendMessage(Message.raw( + "[gravityflip] Toggle en mémoire OK, persistance échouée — voir logs.")); + return; + } + ctx.sendMessage(Message.raw( + "[gravityflip] Région '" + name + "' " + (next ? "activée" : "désactivée") + ".")); + } +}