Files
hytale-gravity-flip/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java
T
kayjaydee 3070353579 feat(04-02): register /gravityflip command in start() (CMD-01)
- wire getCommandRegistry().registerCommand(new GravityFlipCommand(this))
- import com.mythlane.gravityflip.command.GravityFlipCommand
- no manual cleanup: CommandRegistry.register auto-adds shutdownTask
2026-04-24 14:07:44 +02:00

197 lines
9.4 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
*
* <p>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 <dataDirectory>/regions.json} (i.e. {@code Plugins/GravityFlip/regions.json}).
*
* <p>The named {@code withConfig(name, codec)} overload is REQUIRED — the
* 1-arg overload hardcodes the filename to {@code config.json}.
*/
private final Config<GravityFlipConfig> 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<World> 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<Void>; 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<Void> 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+).
* <p>Returns {@code null} until {@link #setup()} has run.
*/
public WandSelectionStore wandSelections() { return wandSelectionStore; }
/**
* Accessor for the region config holder. <strong>SAVE CONTRACT:</strong> 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<GravityFlipConfig> configHolder() {
return configHolder;
}
}