diff --git a/src/main/java/com/mythlane/gravityflip/command/GravityFlipCommand.java b/src/main/java/com/mythlane/gravityflip/command/GravityFlipCommand.java index e9f432e..5fe3729 100644 --- a/src/main/java/com/mythlane/gravityflip/command/GravityFlipCommand.java +++ b/src/main/java/com/mythlane/gravityflip/command/GravityFlipCommand.java @@ -21,7 +21,7 @@ public final class GravityFlipCommand extends AbstractCommandCollection { public GravityFlipCommand(GravityFlipPlugin plugin) { super("gravityflip", "Commandes de gestion des zones Gravity Flip"); this.addSubCommand(new GravityFlipWandSubCommand()); - // 04-03 will addSubCommand(new GravityFlipDefineSubCommand(plugin)); + this.addSubCommand(new GravityFlipDefineSubCommand(plugin)); // 04-04 will addSubCommand(new GravityFlipListSubCommand(plugin)); // addSubCommand(new GravityFlipDeleteSubCommand(plugin)); // addSubCommand(new GravityFlipToggleSubCommand(plugin)); diff --git a/src/main/java/com/mythlane/gravityflip/command/GravityFlipDefineSubCommand.java b/src/main/java/com/mythlane/gravityflip/command/GravityFlipDefineSubCommand.java new file mode 100644 index 0000000..ab60040 --- /dev/null +++ b/src/main/java/com/mythlane/gravityflip/command/GravityFlipDefineSubCommand.java @@ -0,0 +1,116 @@ +package com.mythlane.gravityflip.command; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.shape.Box; +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.arguments.system.RequiredArg; +import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.mythlane.gravityflip.GravityFlipPlugin; +import com.mythlane.gravityflip.region.GravityFlipRegion; +import com.mythlane.gravityflip.wand.WandSelectionStore; + +import javax.annotation.Nonnull; +import java.util.UUID; +import java.util.logging.Level; + +/** + * {@code /gravityflip define } — consume the caller's wand selection + * (pos1 + pos2) to create and persist a new {@link GravityFlipRegion}. + * + *

Flow : + *

    + *
  1. Validate {@code name} via {@link DefineValidation#isValidName}.
  2. + *
  3. Read the caller's selection from {@link WandSelectionStore}. Bail out + * if either pos1 or pos2 is unset (builder must click 2 blocks first).
  4. + *
  5. Compute componentwise min/max so the builder can click in any order. + * Inflate max by +1 per axis so the max block is INSIDE the AABB (a block + * occupies the unit cube between {@code (x,y,z)} and {@code (x+1,y+1,z+1)}; + * without inflation, a player standing on the max block is OUT of the + * region — see {@code DefineValidationTest#boxFromCorners_inflateMax_includesMaxBlock}).
  6. + *
  7. {@code registry.add(region)} — throws {@link IllegalArgumentException} + * on duplicate name (T-04-03-05 mitigation — synchronized mutationLock).
  8. + *
  9. {@code configHolder.save().join()} forces durability before responding. + * Cost : a few ms disk. Acceptable in an interactive command.
  10. + *
  11. Clear the caller's selection — {@code define} consumes it, next define + * requires re-clicking 2 blocks (avoid chained accidental defines).
  12. + *
+ * + *

On save failure the region remains in-memory (registry is already updated). + * We report this truthfully rather than silently rolling back — operators can + * inspect logs and the region is still active until restart. + */ +public final class GravityFlipDefineSubCommand extends AbstractPlayerCommand { + + private final GravityFlipPlugin plugin; + + /** Required STRING arg; full validation applied via {@link DefineValidation#isValidName}. */ + private final RequiredArg nameArg = + this.withRequiredArg("name", "Nom de la région (a-z0-9_-, 1-32 chars)", ArgTypes.STRING); + + public GravityFlipDefineSubCommand(GravityFlipPlugin plugin) { + super("define", "Créer une région Gravity Flip à partir de la sélection wand"); + this.plugin = plugin; + } + + @Override + protected void execute(@Nonnull CommandContext ctx, + @Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PlayerRef playerRef, + @Nonnull World world) { + String name = nameArg.get(ctx); + if (!DefineValidation.isValidName(name)) { + ctx.sendMessage(Message.raw( + "[gravityflip] Nom invalide — attendu [a-zA-Z0-9_-]{1,32}.")); + return; + } + + UUID uuid = playerRef.getUuid(); + WandSelectionStore.Selection sel = plugin.wandSelections().get(uuid); + if (sel.pos1 == null || sel.pos2 == null) { + ctx.sendMessage(Message.raw( + "[gravityflip] Sélection incomplète — left-click puis right-click un bloc avec le wand avant define.")); + return; + } + + int[] mn = DefineValidation.componentwiseMin(sel.pos1, sel.pos2); + int[] mx = DefineValidation.componentwiseMax(sel.pos1, sel.pos2); + // Inflate max by +1 per axis so the block at maxBlock is inside the AABB + // (see DefineValidationTest#boxFromCorners_inflateMax_includesMaxBlock). + Box box = new Box( + new Vector3d(mn[0], mn[1], mn[2]), + new Vector3d(mx[0] + 1.0, mx[1] + 1.0, mx[2] + 1.0)); + GravityFlipRegion region = new GravityFlipRegion(name, box, true); + + try { + plugin.regions().add(region); + } catch (IllegalArgumentException ex) { + ctx.sendMessage(Message.raw( + "[gravityflip] Une région nommée '" + name + "' existe déjà.")); + return; + } + + try { + plugin.configHolder().save().join(); + } catch (Throwable th) { + plugin.getLogger().at(Level.WARNING).withCause(th) + .log("[define] save failed for region '" + name + "'"); + ctx.sendMessage(Message.raw( + "[gravityflip] Région créée (en mémoire) mais persistance échouée — voir logs.")); + return; + } + + plugin.wandSelections().clear(uuid); + ctx.sendMessage(Message.raw( + "[gravityflip] Région '" + name + "' créée : " + + "(" + mn[0] + "," + mn[1] + "," + mn[2] + ") → " + + "(" + (mx[0] + 1) + "," + (mx[1] + 1) + "," + (mx[2] + 1) + ")")); + } +}