refactor(command): clean GSD comments and translate user-facing messages to English
This commit is contained in:
@@ -2,38 +2,19 @@ package com.mythlane.gravityflip.command;
|
|||||||
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/** Pure-data validation helpers for the define sub-command (region name regex, componentwise min/max). */
|
||||||
* Pure-data validation helpers for {@code /gravityflip define <name>}.
|
|
||||||
*
|
|
||||||
* <p>No Hytale runtime dependency — same philosophy as {@code FallDamageGuard}
|
|
||||||
* and {@code WandSelectionStore}. Testable with JUnit alone.
|
|
||||||
*
|
|
||||||
* <p>Exposes :
|
|
||||||
* <ul>
|
|
||||||
* <li>{@link #isValidName(String)} — region name regex gate.</li>
|
|
||||||
* <li>{@link #componentwiseMin(int[], int[])} / {@link #componentwiseMax(int[], int[])}
|
|
||||||
* — so the builder can click pos1/pos2 in any order.</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* <p>See {@code DefineValidationTest} for the accepted/rejected-name corpus and
|
|
||||||
* the rationale for the inflate-max-by-1 convention applied in
|
|
||||||
* {@code GravityFlipDefineSubCommand}.
|
|
||||||
*/
|
|
||||||
public final class DefineValidation {
|
public final class DefineValidation {
|
||||||
|
|
||||||
private static final Pattern NAME = Pattern.compile("^[a-zA-Z0-9_-]{1,32}$");
|
private static final Pattern NAME = Pattern.compile("^[a-zA-Z0-9_-]{1,32}$");
|
||||||
|
|
||||||
private DefineValidation() {}
|
private DefineValidation() {}
|
||||||
|
|
||||||
/**
|
/** Returns true iff the name matches {@code ^[a-zA-Z0-9_-]{1,32}$}. */
|
||||||
* Returns {@code true} iff {@code n} matches {@code ^[a-zA-Z0-9_-]{1,32}$}.
|
|
||||||
* Rejects {@code null}, blank, spaces, path separators, non-ASCII.
|
|
||||||
*/
|
|
||||||
public static boolean isValidName(String n) {
|
public static boolean isValidName(String n) {
|
||||||
return n != null && NAME.matcher(n).matches();
|
return n != null && NAME.matcher(n).matches();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns {@code {min(a.x,b.x), min(a.y,b.y), min(a.z,b.z)}}. */
|
/** Returns the per-axis minimum of two 3D integer vectors. */
|
||||||
public static int[] componentwiseMin(int[] a, int[] b) {
|
public static int[] componentwiseMin(int[] a, int[] b) {
|
||||||
return new int[]{
|
return new int[]{
|
||||||
Math.min(a[0], b[0]),
|
Math.min(a[0], b[0]),
|
||||||
@@ -42,7 +23,7 @@ public final class DefineValidation {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns {@code {max(a.x,b.x), max(a.y,b.y), max(a.z,b.z)}}. */
|
/** Returns the per-axis maximum of two 3D integer vectors. */
|
||||||
public static int[] componentwiseMax(int[] a, int[] b) {
|
public static int[] componentwiseMax(int[] a, int[] b) {
|
||||||
return new int[]{
|
return new int[]{
|
||||||
Math.max(a[0], b[0]),
|
Math.max(a[0], b[0]),
|
||||||
|
|||||||
@@ -3,23 +3,11 @@ package com.mythlane.gravityflip.command;
|
|||||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection;
|
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection;
|
||||||
import com.mythlane.gravityflip.GravityFlipPlugin;
|
import com.mythlane.gravityflip.GravityFlipPlugin;
|
||||||
|
|
||||||
/**
|
/** Root command {@code /gravityflip} — aggregates all sub-commands. */
|
||||||
* Root command {@code /gravityflip}. Aggregates all Gravity Flip sub-commands.
|
|
||||||
*
|
|
||||||
* <p>Extends {@link AbstractCommandCollection} — pattern sourced from
|
|
||||||
* {@code builtin/teleport/commands/teleport/TeleportCommand} and
|
|
||||||
* {@code modules/debug/commands/DebugCommand}. When invoked without a
|
|
||||||
* sub-command, the base class emits a usage message listing registered
|
|
||||||
* sub-commands (no extra work required here).
|
|
||||||
*
|
|
||||||
* <p>The plugin reference is stored for future sub-commands (04-03 define,
|
|
||||||
* 04-04 list/delete/toggle/tp) that need {@code plugin.wandSelections()} or
|
|
||||||
* {@code plugin.configHolder()}.
|
|
||||||
*/
|
|
||||||
public final class GravityFlipCommand extends AbstractCommandCollection {
|
public final class GravityFlipCommand extends AbstractCommandCollection {
|
||||||
|
|
||||||
public GravityFlipCommand(GravityFlipPlugin plugin) {
|
public GravityFlipCommand(GravityFlipPlugin plugin) {
|
||||||
super("gravityflip", "Commandes de gestion des zones Gravity Flip");
|
super("gravityflip", "Gravity Flip region management commands");
|
||||||
this.addSubCommand(new GravityFlipWandSubCommand());
|
this.addSubCommand(new GravityFlipWandSubCommand());
|
||||||
this.addSubCommand(new GravityFlipDefineSubCommand(plugin));
|
this.addSubCommand(new GravityFlipDefineSubCommand(plugin));
|
||||||
this.addSubCommand(new GravityFlipListSubCommand(plugin));
|
this.addSubCommand(new GravityFlipListSubCommand(plugin));
|
||||||
|
|||||||
@@ -20,42 +20,16 @@ import javax.annotation.Nonnull;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
/**
|
/** Creates and persists a new region from the caller's wand selection. */
|
||||||
* {@code /gravityflip define <name>} — consume the caller's wand selection
|
|
||||||
* (pos1 + pos2) to create and persist a new {@link GravityFlipRegion}.
|
|
||||||
*
|
|
||||||
* <p>Flow :
|
|
||||||
* <ol>
|
|
||||||
* <li>Validate {@code name} via {@link DefineValidation#isValidName}.</li>
|
|
||||||
* <li>Read the caller's selection from {@link WandSelectionStore}. Bail out
|
|
||||||
* if either pos1 or pos2 is unset (builder must click 2 blocks first).</li>
|
|
||||||
* <li>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}).</li>
|
|
||||||
* <li>{@code registry.add(region)} — throws {@link IllegalArgumentException}
|
|
||||||
* on duplicate name (T-04-03-05 mitigation — synchronized mutationLock).</li>
|
|
||||||
* <li>{@code configHolder.save().join()} forces durability before responding.
|
|
||||||
* Cost : a few ms disk. Acceptable in an interactive command.</li>
|
|
||||||
* <li>Clear the caller's selection — {@code define} consumes it, next define
|
|
||||||
* requires re-clicking 2 blocks (avoid chained accidental defines).</li>
|
|
||||||
* </ol>
|
|
||||||
*
|
|
||||||
* <p>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 {
|
public final class GravityFlipDefineSubCommand extends AbstractPlayerCommand {
|
||||||
|
|
||||||
private final GravityFlipPlugin plugin;
|
private final GravityFlipPlugin plugin;
|
||||||
|
|
||||||
/** Required STRING arg; full validation applied via {@link DefineValidation#isValidName}. */
|
|
||||||
private final RequiredArg<String> nameArg =
|
private final RequiredArg<String> nameArg =
|
||||||
this.withRequiredArg("name", "Nom de la région (a-z0-9_-, 1-32 chars)", ArgTypes.STRING);
|
this.withRequiredArg("name", "Region name (a-z0-9_-, 1-32 chars)", ArgTypes.STRING);
|
||||||
|
|
||||||
public GravityFlipDefineSubCommand(GravityFlipPlugin plugin) {
|
public GravityFlipDefineSubCommand(GravityFlipPlugin plugin) {
|
||||||
super("define", "Créer une région Gravity Flip à partir de la sélection wand");
|
super("define", "Create a Gravity Flip region from the wand selection");
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +42,7 @@ public final class GravityFlipDefineSubCommand extends AbstractPlayerCommand {
|
|||||||
String name = nameArg.get(ctx);
|
String name = nameArg.get(ctx);
|
||||||
if (!DefineValidation.isValidName(name)) {
|
if (!DefineValidation.isValidName(name)) {
|
||||||
ctx.sendMessage(Message.raw(
|
ctx.sendMessage(Message.raw(
|
||||||
"[gravityflip] Nom invalide — attendu [a-zA-Z0-9_-]{1,32}."));
|
"[gravityflip] Invalid name — expected [a-zA-Z0-9_-]{1,32}."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,14 +50,13 @@ public final class GravityFlipDefineSubCommand extends AbstractPlayerCommand {
|
|||||||
WandSelectionStore.Selection sel = plugin.wandSelections().get(uuid);
|
WandSelectionStore.Selection sel = plugin.wandSelections().get(uuid);
|
||||||
if (sel.pos1 == null || sel.pos2 == null) {
|
if (sel.pos1 == null || sel.pos2 == null) {
|
||||||
ctx.sendMessage(Message.raw(
|
ctx.sendMessage(Message.raw(
|
||||||
"[gravityflip] Sélection incomplète — left-click puis right-click un bloc avec le wand avant define."));
|
"[gravityflip] Incomplete selection — left-click then right-click a block with the wand before define."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int[] mn = DefineValidation.componentwiseMin(sel.pos1, sel.pos2);
|
int[] mn = DefineValidation.componentwiseMin(sel.pos1, sel.pos2);
|
||||||
int[] mx = DefineValidation.componentwiseMax(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
|
// Inflate max by +1 per axis so the max block is INSIDE the AABB (blocks occupy the unit cube [x,x+1]).
|
||||||
// (see DefineValidationTest#boxFromCorners_inflateMax_includesMaxBlock).
|
|
||||||
Box box = new Box(
|
Box box = new Box(
|
||||||
new Vector3d(mn[0], mn[1], mn[2]),
|
new Vector3d(mn[0], mn[1], mn[2]),
|
||||||
new Vector3d(mx[0] + 1.0, mx[1] + 1.0, mx[2] + 1.0));
|
new Vector3d(mx[0] + 1.0, mx[1] + 1.0, mx[2] + 1.0));
|
||||||
@@ -93,7 +66,7 @@ public final class GravityFlipDefineSubCommand extends AbstractPlayerCommand {
|
|||||||
plugin.regions().add(region);
|
plugin.regions().add(region);
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
ctx.sendMessage(Message.raw(
|
ctx.sendMessage(Message.raw(
|
||||||
"[gravityflip] Une région nommée '" + name + "' existe déjà."));
|
"[gravityflip] A region named '" + name + "' already exists."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,14 +76,14 @@ public final class GravityFlipDefineSubCommand extends AbstractPlayerCommand {
|
|||||||
plugin.getLogger().at(Level.WARNING).withCause(th)
|
plugin.getLogger().at(Level.WARNING).withCause(th)
|
||||||
.log("[define] save failed for region '" + name + "'");
|
.log("[define] save failed for region '" + name + "'");
|
||||||
ctx.sendMessage(Message.raw(
|
ctx.sendMessage(Message.raw(
|
||||||
"[gravityflip] Région créée (en mémoire) mais persistance échouée — voir logs."));
|
"[gravityflip] Region created (in memory) but persistence failed — see logs."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin.wandSelections().clear(uuid);
|
plugin.wandSelections().clear(uuid);
|
||||||
ctx.sendMessage(Message.raw(
|
ctx.sendMessage(Message.raw(
|
||||||
"[gravityflip] Région '" + name + "' créée : "
|
"[gravityflip] Region '" + name + "' created: "
|
||||||
+ "(" + mn[0] + "," + mn[1] + "," + mn[2] + ") → "
|
+ "(" + mn[0] + "," + mn[1] + "," + mn[2] + ") -> "
|
||||||
+ "(" + (mx[0] + 1) + "," + (mx[1] + 1) + "," + (mx[2] + 1) + ")"));
|
+ "(" + (mx[0] + 1) + "," + (mx[1] + 1) + "," + (mx[2] + 1) + ")"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,30 +10,16 @@ import com.mythlane.gravityflip.GravityFlipPlugin;
|
|||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
/**
|
/** Deletes a Gravity Flip region and persists the change. */
|
||||||
* {@code /gravityflip delete <name>} — supprime la région nommée et persiste.
|
|
||||||
*
|
|
||||||
* <p>Étend {@link CommandBase} pour permettre l'usage depuis la console serveur
|
|
||||||
* (opération admin, pas besoin d'un joueur caller).
|
|
||||||
*
|
|
||||||
* <p>Flow :
|
|
||||||
* <ol>
|
|
||||||
* <li>{@code registry.remove(name)} — renvoie {@code false} si nom inconnu → message
|
|
||||||
* d'erreur clair et retour précoce (pas de save inutile).</li>
|
|
||||||
* <li>{@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.</li>
|
|
||||||
* </ol>
|
|
||||||
*/
|
|
||||||
public final class GravityFlipDeleteSubCommand extends CommandBase {
|
public final class GravityFlipDeleteSubCommand extends CommandBase {
|
||||||
|
|
||||||
private final GravityFlipPlugin plugin;
|
private final GravityFlipPlugin plugin;
|
||||||
|
|
||||||
private final RequiredArg<String> nameArg =
|
private final RequiredArg<String> nameArg =
|
||||||
this.withRequiredArg("name", "Nom de la région à supprimer", ArgTypes.STRING);
|
this.withRequiredArg("name", "Name of the region to delete", ArgTypes.STRING);
|
||||||
|
|
||||||
public GravityFlipDeleteSubCommand(GravityFlipPlugin plugin) {
|
public GravityFlipDeleteSubCommand(GravityFlipPlugin plugin) {
|
||||||
super("delete", "Supprime une région Gravity Flip");
|
super("delete", "Delete a Gravity Flip region");
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +27,7 @@ public final class GravityFlipDeleteSubCommand extends CommandBase {
|
|||||||
protected void executeSync(@Nonnull CommandContext ctx) {
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
String name = nameArg.get(ctx);
|
String name = nameArg.get(ctx);
|
||||||
if (!plugin.regions().remove(name)) {
|
if (!plugin.regions().remove(name)) {
|
||||||
ctx.sendMessage(Message.raw("[gravityflip] Région '" + name + "' introuvable."));
|
ctx.sendMessage(Message.raw("[gravityflip] Region '" + name + "' not found."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -50,9 +36,9 @@ public final class GravityFlipDeleteSubCommand extends CommandBase {
|
|||||||
plugin.getLogger().at(Level.WARNING).withCause(th)
|
plugin.getLogger().at(Level.WARNING).withCause(th)
|
||||||
.log("[delete] save failed for region '" + name + "'");
|
.log("[delete] save failed for region '" + name + "'");
|
||||||
ctx.sendMessage(Message.raw(
|
ctx.sendMessage(Message.raw(
|
||||||
"[gravityflip] Suppression en mémoire OK, persistance échouée — voir logs."));
|
"[gravityflip] Deleted in memory, but persistence failed — see logs."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.sendMessage(Message.raw("[gravityflip] Région '" + name + "' supprimée."));
|
ctx.sendMessage(Message.raw("[gravityflip] Region '" + name + "' deleted."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,29 +10,13 @@ import com.mythlane.gravityflip.region.GravityFlipRegion;
|
|||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
/**
|
/** Lists all persisted Gravity Flip regions. */
|
||||||
* {@code /gravityflip list} — liste toutes les régions Gravity Flip persistées.
|
|
||||||
*
|
|
||||||
* <p>É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(...)}.
|
|
||||||
*
|
|
||||||
* <p>Format de sortie (une ligne par région) :
|
|
||||||
* <pre>
|
|
||||||
* - test-zone-1 : (10,64,10) → (21,71,21) [enabled]
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* <p>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 {
|
public final class GravityFlipListSubCommand extends CommandBase {
|
||||||
|
|
||||||
private final GravityFlipPlugin plugin;
|
private final GravityFlipPlugin plugin;
|
||||||
|
|
||||||
public GravityFlipListSubCommand(GravityFlipPlugin plugin) {
|
public GravityFlipListSubCommand(GravityFlipPlugin plugin) {
|
||||||
super("list", "Liste toutes les régions Gravity Flip");
|
super("list", "List all Gravity Flip regions");
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,15 +24,15 @@ public final class GravityFlipListSubCommand extends CommandBase {
|
|||||||
protected void executeSync(@Nonnull CommandContext ctx) {
|
protected void executeSync(@Nonnull CommandContext ctx) {
|
||||||
Collection<GravityFlipRegion> all = plugin.regions().all();
|
Collection<GravityFlipRegion> all = plugin.regions().all();
|
||||||
if (all.isEmpty()) {
|
if (all.isEmpty()) {
|
||||||
ctx.sendMessage(Message.raw("[gravityflip] Aucune région définie."));
|
ctx.sendMessage(Message.raw("[gravityflip] No regions defined."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.sendMessage(Message.raw("[gravityflip] " + all.size() + " région(s) :"));
|
ctx.sendMessage(Message.raw("[gravityflip] " + all.size() + " region(s):"));
|
||||||
for (GravityFlipRegion r : all) {
|
for (GravityFlipRegion r : all) {
|
||||||
Vector3d mn = r.getMin();
|
Vector3d mn = r.getMin();
|
||||||
Vector3d mx = r.getMax();
|
Vector3d mx = r.getMax();
|
||||||
ctx.sendMessage(Message.raw(String.format(
|
ctx.sendMessage(Message.raw(String.format(
|
||||||
" - %s : (%.0f,%.0f,%.0f) → (%.0f,%.0f,%.0f) [%s]",
|
" - %s : (%.0f,%.0f,%.0f) -> (%.0f,%.0f,%.0f) [%s]",
|
||||||
r.getName(), mn.x, mn.y, mn.z, mx.x, mx.y, mx.z,
|
r.getName(), mn.x, mn.y, mn.z, mx.x, mx.y, mx.z,
|
||||||
r.isEnabled() ? "enabled" : "disabled")));
|
r.isEnabled() ? "enabled" : "disabled")));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,29 +11,16 @@ import com.mythlane.gravityflip.region.GravityFlipRegion;
|
|||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
/**
|
/** Enables/disables a Gravity Flip region and persists the change. */
|
||||||
* {@code /gravityflip toggle <name>} — flippe le flag {@code enabled} d'une région
|
|
||||||
* sans toucher aux corners ni aux autres champs.
|
|
||||||
*
|
|
||||||
* <p>Étend {@link CommandBase} (utilisable console + joueur).
|
|
||||||
*
|
|
||||||
* <p>Flow : lookup région → lecture {@code isEnabled()} → {@code registry.setEnabled(name, !current)}
|
|
||||||
* → {@code configHolder.save().join()} → message avec le nouvel état.
|
|
||||||
*
|
|
||||||
* <p>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 {
|
public final class GravityFlipToggleSubCommand extends CommandBase {
|
||||||
|
|
||||||
private final GravityFlipPlugin plugin;
|
private final GravityFlipPlugin plugin;
|
||||||
|
|
||||||
private final RequiredArg<String> nameArg =
|
private final RequiredArg<String> nameArg =
|
||||||
this.withRequiredArg("name", "Nom de la région à toggler", ArgTypes.STRING);
|
this.withRequiredArg("name", "Name of the region to toggle", ArgTypes.STRING);
|
||||||
|
|
||||||
public GravityFlipToggleSubCommand(GravityFlipPlugin plugin) {
|
public GravityFlipToggleSubCommand(GravityFlipPlugin plugin) {
|
||||||
super("toggle", "Active/désactive une région Gravity Flip");
|
super("toggle", "Enable/disable a Gravity Flip region");
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,13 +35,13 @@ public final class GravityFlipToggleSubCommand extends CommandBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (found == null) {
|
if (found == null) {
|
||||||
ctx.sendMessage(Message.raw("[gravityflip] Région '" + name + "' introuvable."));
|
ctx.sendMessage(Message.raw("[gravityflip] Region '" + name + "' not found."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boolean next = !found.isEnabled();
|
boolean next = !found.isEnabled();
|
||||||
if (!plugin.regions().setEnabled(name, next)) {
|
if (!plugin.regions().setEnabled(name, next)) {
|
||||||
// Course ultra-rare : région supprimée entre all() et setEnabled().
|
// Rare race: region removed between all() and setEnabled().
|
||||||
ctx.sendMessage(Message.raw("[gravityflip] Région '" + name + "' introuvable."));
|
ctx.sendMessage(Message.raw("[gravityflip] Region '" + name + "' not found."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -63,10 +50,10 @@ public final class GravityFlipToggleSubCommand extends CommandBase {
|
|||||||
plugin.getLogger().at(Level.WARNING).withCause(th)
|
plugin.getLogger().at(Level.WARNING).withCause(th)
|
||||||
.log("[toggle] save failed for region '" + name + "'");
|
.log("[toggle] save failed for region '" + name + "'");
|
||||||
ctx.sendMessage(Message.raw(
|
ctx.sendMessage(Message.raw(
|
||||||
"[gravityflip] Toggle en mémoire OK, persistance échouée — voir logs."));
|
"[gravityflip] Toggled in memory, but persistence failed — see logs."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.sendMessage(Message.raw(
|
ctx.sendMessage(Message.raw(
|
||||||
"[gravityflip] Région '" + name + "' " + (next ? "activée" : "désactivée") + "."));
|
"[gravityflip] Region '" + name + "' " + (next ? "enabled" : "disabled") + "."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,37 +20,16 @@ import com.mythlane.gravityflip.region.GravityFlipRegion;
|
|||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
/**
|
/** Teleports the calling player to the center of a Gravity Flip region's AABB. */
|
||||||
* {@code /gravityflip tp <name>} — téléporte le joueur appelant au centre de
|
|
||||||
* l'AABB d'une région Gravity Flip. Outil debug/builder (CMD-06).
|
|
||||||
*
|
|
||||||
* <p>Étend {@link AbstractPlayerCommand} : tp ne fait sens que pour un joueur,
|
|
||||||
* la base class gère automatiquement le message "must be player" pour un appel console.
|
|
||||||
*
|
|
||||||
* <p>Pattern Teleport copié ligne-à-ligne sur
|
|
||||||
* {@code com.hypixel.hytale.builtin.teleport.commands.teleport.variant.TeleportToCoordinatesCommand}
|
|
||||||
* (lignes 48-68) :
|
|
||||||
* <ol>
|
|
||||||
* <li>Lire {@link TransformComponent} (rotation corps) et {@link HeadRotation}
|
|
||||||
* (rotation tête) pour préserver l'orientation du joueur.</li>
|
|
||||||
* <li>Calculer le centre componentwise : {@code c = (min + max) / 2}.</li>
|
|
||||||
* <li>Construire via {@code Teleport.createForPlayer(pos, rotation).setHeadRotation(...)}.</li>
|
|
||||||
* <li>{@code store.addComponent(ref, Teleport.getComponentType(), teleport)} —
|
|
||||||
* le {@code TeleportSystem} du core consomme le composant au prochain tick.</li>
|
|
||||||
* </ol>
|
|
||||||
*
|
|
||||||
* <p>T-04-04-02 : permission auto-générée {@code mythlane.gravityflip.command.gravityflip.tp}
|
|
||||||
* sert de gate. Pas de restriction GameMode v1 (builder tool).
|
|
||||||
*/
|
|
||||||
public final class GravityFlipTpSubCommand extends AbstractPlayerCommand {
|
public final class GravityFlipTpSubCommand extends AbstractPlayerCommand {
|
||||||
|
|
||||||
private final GravityFlipPlugin plugin;
|
private final GravityFlipPlugin plugin;
|
||||||
|
|
||||||
private final RequiredArg<String> nameArg =
|
private final RequiredArg<String> nameArg =
|
||||||
this.withRequiredArg("name", "Nom de la région cible", ArgTypes.STRING);
|
this.withRequiredArg("name", "Name of the target region", ArgTypes.STRING);
|
||||||
|
|
||||||
public GravityFlipTpSubCommand(GravityFlipPlugin plugin) {
|
public GravityFlipTpSubCommand(GravityFlipPlugin plugin) {
|
||||||
super("tp", "Téléporte au centre d'une région Gravity Flip");
|
super("tp", "Teleport to the center of a Gravity Flip region");
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +48,7 @@ public final class GravityFlipTpSubCommand extends AbstractPlayerCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (target == null) {
|
if (target == null) {
|
||||||
ctx.sendMessage(Message.raw("[gravityflip] Région '" + name + "' introuvable."));
|
ctx.sendMessage(Message.raw("[gravityflip] Region '" + name + "' not found."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,13 +61,12 @@ public final class GravityFlipTpSubCommand extends AbstractPlayerCommand {
|
|||||||
TransformComponent tc = store.getComponent(ref, TransformComponent.getComponentType());
|
TransformComponent tc = store.getComponent(ref, TransformComponent.getComponentType());
|
||||||
if (tc == null) {
|
if (tc == null) {
|
||||||
ctx.sendMessage(Message.raw(
|
ctx.sendMessage(Message.raw(
|
||||||
"[gravityflip] TransformComponent manquant — tp impossible."));
|
"[gravityflip] TransformComponent missing — teleport impossible."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
HeadRotation hr = store.getComponent(ref, HeadRotation.getComponentType());
|
HeadRotation hr = store.getComponent(ref, HeadRotation.getComponentType());
|
||||||
|
|
||||||
// Préserve l'orientation courante : body rotation depuis Transform, head rotation
|
// Preserve current orientation: body rotation from Transform, head rotation when available.
|
||||||
// si disponible (sinon on laisse la default côté Teleport.createForPlayer).
|
|
||||||
Vector3f bodyRotation = tc.getRotation().clone();
|
Vector3f bodyRotation = tc.getRotation().clone();
|
||||||
Teleport teleport = Teleport.createForPlayer(
|
Teleport teleport = Teleport.createForPlayer(
|
||||||
new Vector3d(cx, cy, cz), bodyRotation);
|
new Vector3d(cx, cy, cz), bodyRotation);
|
||||||
@@ -98,7 +76,7 @@ public final class GravityFlipTpSubCommand extends AbstractPlayerCommand {
|
|||||||
store.addComponent(ref, Teleport.getComponentType(), teleport);
|
store.addComponent(ref, Teleport.getComponentType(), teleport);
|
||||||
|
|
||||||
ctx.sendMessage(Message.raw(String.format(
|
ctx.sendMessage(Message.raw(String.format(
|
||||||
"[gravityflip] Téléporté au centre de '%s' : (%.0f,%.0f,%.0f)",
|
"[gravityflip] Teleported to the center of '%s': (%.0f,%.0f,%.0f)",
|
||||||
name, cx, cy, cz)));
|
name, cx, cy, cz)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,27 +14,13 @@ import com.hypixel.hytale.server.core.universe.world.World;
|
|||||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
/**
|
/** {@code /gravityflip wand} — gives a Gravity Flip Wand to the calling player. */
|
||||||
* {@code /gravityflip wand} — donne un Gravity Flip Wand au joueur appelant.
|
|
||||||
*
|
|
||||||
* <p>Pattern copié ligne-à-ligne sur
|
|
||||||
* {@code com.hypixel.hytale.server.core.command.commands.player.inventory.GiveCommand}
|
|
||||||
* (cf. GiveCommand.java:47-76). La seule différence : l'item est résolu par
|
|
||||||
* constante ({@link #WAND_ITEM_ID}) au lieu d'un {@code RequiredArg<Item>}.
|
|
||||||
*
|
|
||||||
* <p>L'item {@code gravityflip_wand} est enregistré via le JSON bundle
|
|
||||||
* {@code src/main/resources/Items/gravityflip_wand.json} (Phase 04-01).
|
|
||||||
* Si l'asset n'est pas chargé (JSON absent / pas picked-up par l'AssetStore
|
|
||||||
* core), le lookup renvoie {@code null} et la commande envoie un message
|
|
||||||
* d'erreur clair au lieu de crasher (T-04-02 scope).
|
|
||||||
*/
|
|
||||||
public final class GravityFlipWandSubCommand extends AbstractPlayerCommand {
|
public final class GravityFlipWandSubCommand extends AbstractPlayerCommand {
|
||||||
|
|
||||||
/** ItemID du wand défini dans {@code Items/gravityflip_wand.json} (Phase 04-01). */
|
|
||||||
private static final String WAND_ITEM_ID = "gravityflip_wand";
|
private static final String WAND_ITEM_ID = "gravityflip_wand";
|
||||||
|
|
||||||
public GravityFlipWandSubCommand() {
|
public GravityFlipWandSubCommand() {
|
||||||
super("wand", "Obtenir un Gravity Flip Wand");
|
super("wand", "Get a Gravity Flip Wand");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -47,22 +33,22 @@ public final class GravityFlipWandSubCommand extends AbstractPlayerCommand {
|
|||||||
if (item == null) {
|
if (item == null) {
|
||||||
context.sendMessage(Message.raw(
|
context.sendMessage(Message.raw(
|
||||||
"[gravityflip] Item '" + WAND_ITEM_ID
|
"[gravityflip] Item '" + WAND_ITEM_ID
|
||||||
+ "' introuvable — asset pas chargé ?"));
|
+ "' not found — asset not loaded?"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Player playerComponent = store.getComponent(ref, Player.getComponentType());
|
Player playerComponent = store.getComponent(ref, Player.getComponentType());
|
||||||
if (playerComponent == null) {
|
if (playerComponent == null) {
|
||||||
context.sendMessage(Message.raw("[gravityflip] Player component manquant."));
|
context.sendMessage(Message.raw("[gravityflip] Player component missing."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ItemStack stack = new ItemStack(item.getId(), 1, null);
|
ItemStack stack = new ItemStack(item.getId(), 1, null);
|
||||||
ItemStackTransaction transaction = playerComponent.giveItem(stack, ref, store);
|
ItemStackTransaction transaction = playerComponent.giveItem(stack, ref, store);
|
||||||
ItemStack remainder = transaction.getRemainder();
|
ItemStack remainder = transaction.getRemainder();
|
||||||
if (remainder == null || remainder.isEmpty()) {
|
if (remainder == null || remainder.isEmpty()) {
|
||||||
context.sendMessage(Message.raw("[gravityflip] Wand ajouté à ton inventaire."));
|
context.sendMessage(Message.raw("[gravityflip] Wand added to your inventory."));
|
||||||
} else {
|
} else {
|
||||||
context.sendMessage(Message.raw(
|
context.sendMessage(Message.raw(
|
||||||
"[gravityflip] Inventaire plein — impossible d'ajouter le wand."));
|
"[gravityflip] Inventory full — cannot add the wand."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,21 +6,9 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
/**
|
/** Pure-data tests for {@link DefineValidation} — name regex, componentwise min/max, inflate-max-by-1 convention. */
|
||||||
* Pure-data tests for {@link DefineValidation} — no Hytale runtime dependency.
|
|
||||||
*
|
|
||||||
* <p>Covers :
|
|
||||||
* <ul>
|
|
||||||
* <li>Name regex accepts alnum + underscore + dash, 1..32 chars.</li>
|
|
||||||
* <li>Name regex rejects blank, spaces, special chars, non-ASCII, over-length.</li>
|
|
||||||
* <li>Componentwise min/max return smallest/largest per axis.</li>
|
|
||||||
* <li>Inflate-max-by-1 convention (max block is INSIDE the AABB only if we add 1 per axis).</li>
|
|
||||||
* </ul>
|
|
||||||
*/
|
|
||||||
class DefineValidationTest {
|
class DefineValidationTest {
|
||||||
|
|
||||||
// ---------- name regex ----------
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void validName_acceptsAlnumUnderscoreDash() {
|
void validName_acceptsAlnumUnderscoreDash() {
|
||||||
assertTrue(DefineValidation.isValidName("abc"));
|
assertTrue(DefineValidation.isValidName("abc"));
|
||||||
@@ -36,7 +24,7 @@ class DefineValidationTest {
|
|||||||
assertFalse(DefineValidation.isValidName(""));
|
assertFalse(DefineValidation.isValidName(""));
|
||||||
assertFalse(DefineValidation.isValidName(" "));
|
assertFalse(DefineValidation.isValidName(" "));
|
||||||
assertFalse(DefineValidation.isValidName("my zone"));
|
assertFalse(DefineValidation.isValidName("my zone"));
|
||||||
assertFalse(DefineValidation.isValidName("nom avec espaces"));
|
assertFalse(DefineValidation.isValidName("name with spaces"));
|
||||||
assertFalse(DefineValidation.isValidName("a\tb"));
|
assertFalse(DefineValidation.isValidName("a\tb"));
|
||||||
assertFalse(DefineValidation.isValidName(" leading"));
|
assertFalse(DefineValidation.isValidName(" leading"));
|
||||||
assertFalse(DefineValidation.isValidName("trailing "));
|
assertFalse(DefineValidation.isValidName("trailing "));
|
||||||
@@ -47,8 +35,6 @@ class DefineValidationTest {
|
|||||||
assertFalse(DefineValidation.isValidName("has.dot"));
|
assertFalse(DefineValidation.isValidName("has.dot"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- componentwise min/max ----------
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void componentwiseMin_returnsSmallestPerAxis() {
|
void componentwiseMin_returnsSmallestPerAxis() {
|
||||||
int[] a = {5, 10, -3};
|
int[] a = {5, 10, -3};
|
||||||
@@ -75,22 +61,13 @@ class DefineValidationTest {
|
|||||||
DefineValidation.componentwiseMax(b, a));
|
DefineValidation.componentwiseMax(b, a));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- inflate-max convention (block inclusion) ----------
|
/** Blocks occupy the unit cube [x,x+1], so max must be inflated by +1 per axis to include the max block. */
|
||||||
|
|
||||||
/**
|
|
||||||
* A block occupies the cube between (x,y,z) and (x+1,y+1,z+1). If an AABB's max is the
|
|
||||||
* raw block coord (maxBlockX, maxBlockY, maxBlockZ), a player standing on top of that
|
|
||||||
* block is OUTSIDE the AABB. We therefore inflate max by +1 per axis.
|
|
||||||
*/
|
|
||||||
@Test
|
@Test
|
||||||
void boxFromCorners_inflateMax_includesMaxBlock() {
|
void boxFromCorners_inflateMax_includesMaxBlock() {
|
||||||
int[] mn = {0, 64, 0};
|
int[] mn = {0, 64, 0};
|
||||||
int[] mx = {10, 70, 10};
|
int[] mx = {10, 70, 10};
|
||||||
// Player feet at (10.5, 70.5, 10.5) — standing in the maxBlock cube.
|
|
||||||
double px = 10.5, py = 70.5, pz = 10.5;
|
double px = 10.5, py = 70.5, pz = 10.5;
|
||||||
// Without inflate: max = (10,70,10) — player OUT.
|
|
||||||
assertFalse(px < mx[0] && py < mx[1] && pz < mx[2]);
|
assertFalse(px < mx[0] && py < mx[1] && pz < mx[2]);
|
||||||
// With inflate: max = (11,71,11) — player IN.
|
|
||||||
int[] mxInflated = {mx[0] + 1, mx[1] + 1, mx[2] + 1};
|
int[] mxInflated = {mx[0] + 1, mx[1] + 1, mx[2] + 1};
|
||||||
assertTrue(px < mxInflated[0] && py < mxInflated[1] && pz < mxInflated[2]);
|
assertTrue(px < mxInflated[0] && py < mxInflated[1] && pz < mxInflated[2]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user