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) + ")")); } }