diff --git a/src/main/java/com/mythlane/gravityflip/wand/WandSelectionStore.java b/src/main/java/com/mythlane/gravityflip/wand/WandSelectionStore.java new file mode 100644 index 0000000..0b99f17 --- /dev/null +++ b/src/main/java/com/mythlane/gravityflip/wand/WandSelectionStore.java @@ -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}. + * + *

Pure-data : 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. + * + *

Thread-safety : 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). + * + *

Lifecycle : 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 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(); + } +}