feat(04-01): implement WandSelectionStore pure-data per-UUID selection store

- ConcurrentHashMap<UUID, Selection>, atomic compute() for set/get
- Selection immutable holder with isComplete()
- No Hytale imports; testable standalone
This commit is contained in:
2026-04-23 18:49:46 +02:00
parent 7afdc5c1a6
commit e0a329480a
@@ -0,0 +1,82 @@
package com.mythlane.gravityflip.wand;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Thread-safe per-player wand selection store. Tracks the two corner positions
* ({@code pos1} = Primary click, {@code pos2} = Secondary click) of each
* builder's current selection, keyed by player {@link UUID}.
*
* <p><b>Pure-data :</b> no Hytale runtime dependency — same philosophy as
* {@code FallDamageGuard}. Testable with JUnit alone, reusable by future
* {@code /gravityflip define} command (Phase 04-02+) without runtime coupling.
*
* <p><b>Thread-safety :</b> backed by a {@link ConcurrentHashMap} whose
* {@code compute(...)} mutations are atomic. Safe for concurrent
* {@link #setPos1}/{@link #setPos2} calls on the same UUID (STRIDE T-04-01-04).
*
* <p><b>Lifecycle :</b> in-memory only — selections are discarded on plugin
* shutdown. Conscious design : a builder will not quit mid-{@code define}.
*/
public final class WandSelectionStore {
/** Immutable holder for a (possibly partial) selection. */
public static final class Selection {
/** {@code {x,y,z}} of the Primary click, or {@code null} if unset. */
public final int[] pos1;
/** {@code {x,y,z}} of the Secondary click, or {@code null} if unset. */
public final int[] pos2;
Selection(int[] pos1, int[] pos2) {
this.pos1 = pos1;
this.pos2 = pos2;
}
/** True iff both corners have been picked. */
public boolean isComplete() {
return pos1 != null && pos2 != null;
}
}
private static final Selection EMPTY = new Selection(null, null);
private final ConcurrentHashMap<UUID, Selection> byUuid = new ConcurrentHashMap<>();
/** Record the Primary-click corner for {@code uuid}. Keeps any existing pos2. */
public void setPos1(UUID uuid, int x, int y, int z) {
if (uuid == null) return;
byUuid.compute(uuid, (k, prev) -> new Selection(
new int[]{x, y, z},
prev != null ? prev.pos2 : null));
}
/** Record the Secondary-click corner for {@code uuid}. Keeps any existing pos1. */
public void setPos2(UUID uuid, int x, int y, int z) {
if (uuid == null) return;
byUuid.compute(uuid, (k, prev) -> new Selection(
prev != null ? prev.pos1 : null,
new int[]{x, y, z}));
}
/**
* Return the current selection for {@code uuid}. Never returns {@code null} :
* an unknown UUID yields a {@link Selection} with both corners {@code null}.
*/
public Selection get(UUID uuid) {
if (uuid == null) return EMPTY;
Selection s = byUuid.get(uuid);
return s != null ? s : EMPTY;
}
/** Forget the selection for {@code uuid} (e.g. after {@code /gravityflip define}). */
public void clear(UUID uuid) {
if (uuid == null) return;
byUuid.remove(uuid);
}
/** Diagnostic : number of players with an in-flight selection. */
public int size() {
return byUuid.size();
}
}