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.server.core.util.Config;
import com.mythlane.gravityflip.config.GravityFlipConfig;
import com.mythlane.gravityflip.physics.GravityApplier;
import com.mythlane.gravityflip.region.RegionRegistry;
import com.mythlane.gravityflip.tick.RegionTickLoop;
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;
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.
getLogger().at(Level.INFO).log("Gravity Flip enabled");
}
@Override
protected void start() {
super.start();
GravityFlipConfig cfg = configHolder.get();
this.registry = new RegionRegistry(cfg, configHolder);
this.gravityApplier = new GravityApplier(th ->
getLogger().at(Level.WARNING).withCause(th).log("gravityApply failed"));
this.tickLoop = new RegionTickLoop(registry, gravityApplier, 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 */ }
getLogger().at(Level.INFO).log(
"Gravity Flip enabled — %d region(s) loaded, detector @100ms, gravity inversion active",
cfg.getRegions().size());
}
@Override
protected void shutdown() {
// 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();
}
/** Exposed for Phase 3 (gravity physics) and Phase 4 (commands). */
public RegionRegistry regions() { return registry; }
/**
* 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;
}
}