package com.mythlane.gravityflip; import com.hypixel.hytale.server.core.plugin.JavaPlugin; import com.hypixel.hytale.server.core.plugin.JavaPluginInit; import com.hypixel.hytale.server.core.universe.Universe; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.math.shape.Box; import com.hypixel.hytale.math.vector.Vector3d; import com.hypixel.hytale.server.core.util.Config; import com.mythlane.gravityflip.config.GravityFlipConfig; import com.mythlane.gravityflip.physics.FallDamageGuard; import com.mythlane.gravityflip.physics.FallDamageSuppressorSystem; import com.mythlane.gravityflip.physics.GravityApplier; import com.mythlane.gravityflip.region.GravityFlipRegion; import com.mythlane.gravityflip.region.RegionRegistry; import com.mythlane.gravityflip.tick.RegionTickLoop; import com.mythlane.gravityflip.command.GravityFlipCommand; 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; /** * Entry point for the Gravity Flip plugin. * *

Extends {@link JavaPlugin} from the resolved Hytale Server API * ({@code com.hypixel.hytale:Server:2026.03.26-89796e57b}). Lifecycle hooks * for this version are {@code setup()} / {@code shutdown()} (not the legacy * {@code onEnable()} / {@code onDisable()}). See * {@code .planning/phases/01-scaffold-load/JAVAPLUGIN_RESOLUTION.md} for the * empirical resolution. */ public class GravityFlipPlugin extends JavaPlugin { /** * Persisted region store, materialised on disk as * {@code /regions.json} (i.e. {@code Plugins/GravityFlip/regions.json}). * *

The named {@code withConfig(name, codec)} overload is REQUIRED — the * 1-arg overload hardcodes the filename to {@code config.json}. */ private final Config configHolder = withConfig("regions", GravityFlipConfig.CODEC); private RegionRegistry registry; private RegionTickLoop tickLoop; private GravityApplier gravityApplier; private FallDamageGuard fallDamageGuard; private RegionVisualizer regionVisualizer; private WandSelectionStore wandSelectionStore; public GravityFlipPlugin(JavaPluginInit init) { super(init); } @Override protected void setup() { // NOTE: do NOT call configHolder.get() here — it blocks until preLoad() completes. // Safe call sites are start() and any later lifecycle phase (incl. tick loop). // // World acquisition note (Phase 02-02): the plan called for a PrepareUniverseEvent // listener that stashes a World reference. Empirically, PrepareUniverseEvent // (com.hypixel.hytale.server.core.event.events.PrepareUniverseEvent) only carries // a WorldConfigProvider — it does NOT expose a Universe or World. We therefore use // a Supplier that resolves Universe.get().getDefaultWorld() lazily on each // tick (matching the MythWorld WorldBorderManager precedent). Until the universe // is ready, the supplier returns null and the tick is a no-op. // Plan 03-04 : enregistrer le FallDamageSuppressorSystem DANS setup() (fenêtre ECS // de registration). Pattern identique à FlockPlugin.java → entityStoreRegistry.registerSystem(...). this.fallDamageGuard = new FallDamageGuard(); 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 protected void start() { super.start(); GravityFlipConfig cfg = configHolder.get(); if (cfg.getRegions().isEmpty()) { cfg.getRegions().add(buildShowcaseRegion()); configHolder.save(); getLogger().at(Level.INFO).log( "First-run: bootstrapped 1 showcase region in regions.json (edit or delete to customise)"); } this.registry = new RegionRegistry(cfg, configHolder); this.gravityApplier = new GravityApplier( th -> getLogger().at(Level.WARNING).withCause(th).log("gravityApply failed"), fallDamageGuard); this.regionVisualizer = new RegionVisualizer( th -> getLogger().at(Level.WARNING).withCause(th).log("regionVisualize failed")); this.tickLoop = new RegionTickLoop(registry, gravityApplier, regionVisualizer, th -> getLogger().at(Level.WARNING).withCause(th).log("detectTick failed")); // Lazy world resolution — see setup() comment. this.tickLoop.startWithDelay(2_000L, () -> { Universe u = Universe.get(); return u == null ? null : u.getDefaultWorld(); }); // TaskRegistry registration: registerTask only accepts ScheduledFuture; the // scheduler returns ScheduledFuture. Cast via raw types per Mythlane idiom; the // try/catch falls back to manual shutdown() if registration fails (deterministic // either way because shutdown() always invokes tickLoop.stop()). try { @SuppressWarnings({"unchecked", "rawtypes"}) ScheduledFuture vf = (ScheduledFuture) tickLoop.future(); getTaskRegistry().registerTask(vf); } catch (Throwable ignored) { /* manual shutdown() fallback */ } // Plan 04-02 : enregistrer la commande racine /gravityflip + sous-commande `wand`. // Pattern : CommandRegistry.register(...) ajoute automatiquement un shutdownTask qui // unregister au teardown (cf. CommandRegistry base class). Pas de cleanup manuel. getCommandRegistry().registerCommand(new GravityFlipCommand(this)); getLogger().at(Level.INFO).log( "Gravity Flip enabled — %d region(s) loaded, detector @100ms, gravity inversion active", cfg.getRegions().size()); } @Override protected void shutdown() { // Plan 03-05 : clear des debug shapes cote clients AVANT tickLoop.stop(). // Si l'Universe est deja fermee, world==null => shapes expireront via TTL (acceptable). if (regionVisualizer != null) { Universe u = Universe.get(); World w = (u == null) ? null : u.getDefaultWorld(); if (w != null) regionVisualizer.clearAll(w); } // Stop the detector BEFORE super.shutdown() so no tick races plugin teardown. if (tickLoop != null) tickLoop.stop(); // No auto-save contract: any mutation made during the session must already // have been persisted via configHolder().save() by the command handler that // performed it. See configHolder() javadoc. getLogger().at(Level.INFO).log("Gravity Flip disabled"); super.shutdown(); } /** * Showcase zone seeded on first run (when {@code regions.json} is absent or empty). * 10×20×10 box at {@code (0,100,0)..(10,120,10)} with Torch_Fire particles and a * gentle upward force — lets a fresh install demonstrate the feature immediately. * Users are free to edit or delete this zone via {@code regions.json}. */ private static GravityFlipRegion buildShowcaseRegion() { GravityFlipRegion r = new GravityFlipRegion( "demo-gravity-flip", new Box(new Vector3d(0, 100, 0), new Vector3d(10, 120, 10)), true); r.setVerticalForce(0.3); r.setVisualMode("Particles"); r.setVisualParticleId("Torch_Fire"); r.setVisualParticleDensity(0.3); r.setVisualRefreshMs(300); return r; } /** 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 * {@code configHolder().save()} afterwards. There is no lifecycle hook that * auto-saves on shutdown. {@code Config.get()} returns a SHARED MUTABLE * reference; concurrent writers corrupt state — Phase 02-02's * {@code RegionRegistry} snapshots it into an {@code AtomicReference} for * tick-loop reads. */ public Config configHolder() { return configHolder; } }