diff --git a/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java b/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java index e639638..b96cb0e 100644 --- a/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java +++ b/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java @@ -15,6 +15,9 @@ import com.mythlane.gravityflip.region.GravityFlipRegion; import com.mythlane.gravityflip.region.RegionRegistry; import com.mythlane.gravityflip.tick.RegionTickLoop; import com.mythlane.gravityflip.viz.RegionVisualizer; +import com.mythlane.gravityflip.wand.GravityFlipWandInteraction; +import com.mythlane.gravityflip.wand.WandSelectionStore; +import com.hypixel.hytale.server.core.modules.interaction.interaction.config.Interaction; import java.util.concurrent.ScheduledFuture; import java.util.logging.Level; @@ -46,6 +49,7 @@ public class GravityFlipPlugin extends JavaPlugin { private GravityApplier gravityApplier; private FallDamageGuard fallDamageGuard; private RegionVisualizer regionVisualizer; + private WandSelectionStore wandSelectionStore; public GravityFlipPlugin(JavaPluginInit init) { super(init); @@ -69,6 +73,18 @@ public class GravityFlipPlugin extends JavaPlugin { getEntityStoreRegistry().registerSystem(new FallDamageSuppressorSystem( fallDamageGuard, th -> getLogger().at(Level.WARNING).withCause(th).log("fallDamageSuppressor handle failed"))); + + // Plan 04-01 : Gravity Flip wand. + // Interaction binding pattern per 04-00 SPIKE-RESULT (Finding 3) — same shape as + // InstancesPlugin.java:158 / ExitInstanceInteraction. The JSON Item at + // src/main/resources/Items/gravityflip_wand.json references this Type in + // Interactions.Primary / Interactions.Secondary. + this.wandSelectionStore = new WandSelectionStore(); + GravityFlipWandInteraction.bindStore(this.wandSelectionStore); + getCodecRegistry(Interaction.CODEC).register( + "GravityFlipWand", + GravityFlipWandInteraction.class, + GravityFlipWandInteraction.CODEC); } @Override @@ -151,6 +167,14 @@ public class GravityFlipPlugin extends JavaPlugin { /** Exposed for Phase 3 (gravity physics) and Phase 4 (commands). */ public RegionRegistry regions() { return registry; } + /** + * Per-player wand selection store. Populated by + * {@link GravityFlipWandInteraction}; consumed by + * {@code /gravityflip define} (Phase 04-02+). + *

Returns {@code null} until {@link #setup()} has run. + */ + public WandSelectionStore wandSelections() { return wandSelectionStore; } + /** * Accessor for the region config holder. SAVE CONTRACT: any * caller that mutates {@code configHolder().get().getRegions()} MUST call diff --git a/src/main/java/com/mythlane/gravityflip/wand/GravityFlipWandInteraction.java b/src/main/java/com/mythlane/gravityflip/wand/GravityFlipWandInteraction.java new file mode 100644 index 0000000..2fe4f7b --- /dev/null +++ b/src/main/java/com/mythlane/gravityflip/wand/GravityFlipWandInteraction.java @@ -0,0 +1,119 @@ +package com.mythlane.gravityflip.wand; + +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.protocol.BlockPosition; +import com.hypixel.hytale.protocol.InteractionType; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.entity.InteractionContext; +import com.hypixel.hytale.server.core.modules.interaction.interaction.CooldownHandler; +import com.hypixel.hytale.server.core.modules.interaction.interaction.config.SimpleInstantInteraction; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; + +import javax.annotation.Nonnull; +import java.util.UUID; + +/** + * Gravity Flip wand interaction — reads Primary / Secondary clicks and pushes + * the targeted block position into a {@link WandSelectionStore} keyed by the + * clicker's player UUID. + * + *

Binding : registered at {@code setup()} time via + * {@code getCodecRegistry(Interaction.CODEC).register("GravityFlipWand", …)} — + * same pattern as {@code ExitInstanceInteraction} (cf. 04-00 SPIKE-RESULT, + * Finding 3). The matching {@code "Type": "GravityFlipWand"} reference in + * {@code Items/gravityflip_wand.json} wires the click packet to this class. + * + *

One class for both click types : {@link #firstRun} receives the + * {@link InteractionType}. Primary → {@code setPos1}, Secondary → {@code setPos2}. + * Centralising both in one class keeps a single registration entry and a + * single CODEC — any other split would duplicate wiring for no gain. + * + *

Store injection : the Hytale CODEC instantiates interactions via a + * no-arg constructor, so we cannot inject the store through the constructor. + * Instead, {@link #bindStore(WandSelectionStore)} installs a {@code volatile} + * reference at plugin start. If the store is not bound (mis-wired plugin), + * {@link #firstRun} is a safe no-op — fail-silent. + * + *

Threat surface : + *

+ */ +public final class GravityFlipWandInteraction extends SimpleInstantInteraction { + + @Nonnull + public static final BuilderCodec CODEC = + ((BuilderCodec.Builder) BuilderCodec + .builder(GravityFlipWandInteraction.class, + GravityFlipWandInteraction::new, + SimpleInstantInteraction.CODEC) + .documentation( + "Gravity Flip wand: Primary click sets pos1, " + + "Secondary click sets pos2, for the clicker's selection.")) + .build(); + + /** + * Store shared by every instance of this interaction — injected at plugin + * start via {@link #bindStore}. Volatile so the writer thread ({@code setup()}) + * publishes a safe reference to the reader threads (interaction dispatch). + */ + private static volatile WandSelectionStore STORE; + + /** Wire the selection store before any click can be processed. Call once at {@code setup()}. */ + public static void bindStore(WandSelectionStore store) { + STORE = store; + } + + /** Required no-arg constructor used by the CODEC factory. */ + public GravityFlipWandInteraction() { + } + + @Override + protected void firstRun(@Nonnull InteractionType type, + @Nonnull InteractionContext context, + @Nonnull CooldownHandler cooldownHandler) { + WandSelectionStore store = STORE; + if (store == null) { + // Plugin mis-wired (bindStore never called). Silent no-op — don't crash the click. + return; + } + + Ref entityRef = context.getEntity(); + CommandBuffer commandBuffer = context.getCommandBuffer(); + if (entityRef == null || commandBuffer == null) { + return; + } + + PlayerRef playerRef = commandBuffer.getComponent(entityRef, PlayerRef.getComponentType()); + if (playerRef == null) { + // Clicker is not a player (mob, arrow, …) — ignore. + return; + } + + BlockPosition bp = context.getTargetBlock(); + if (bp == null) { + playerRef.sendMessage(Message.raw("[gravityflip] No block targeted.")); + return; + } + + UUID uuid = playerRef.getUuid(); + if (type == InteractionType.Primary) { + store.setPos1(uuid, bp.x, bp.y, bp.z); + playerRef.sendMessage(Message.raw( + "[gravityflip] pos1 set: (%d, %d, %d)".formatted(bp.x, bp.y, bp.z))); + } else if (type == InteractionType.Secondary) { + store.setPos2(uuid, bp.x, bp.y, bp.z); + playerRef.sendMessage(Message.raw( + "[gravityflip] pos2 set: (%d, %d, %d)".formatted(bp.x, bp.y, bp.z))); + } + // Any other InteractionType (Ability1, Pick, Equipped, …) is ignored. + } +} diff --git a/src/main/resources/Items/gravityflip_wand.json b/src/main/resources/Items/gravityflip_wand.json new file mode 100644 index 0000000..8e00e47 --- /dev/null +++ b/src/main/resources/Items/gravityflip_wand.json @@ -0,0 +1,16 @@ +{ + "Icon": "Torch_Fire", + "TranslationProperties": { + "Translation": "Gravity Flip Wand" + }, + "ResourceType": "Utility", + "MaxStackSize": 1, + "Interactions": { + "Primary": { + "Type": "GravityFlipWand" + }, + "Secondary": { + "Type": "GravityFlipWand" + } + } +}