3070353579
- wire getCommandRegistry().registerCommand(new GravityFlipCommand(this)) - import com.mythlane.gravityflip.command.GravityFlipCommand - no manual cleanup: CommandRegistry.register auto-adds shutdownTask
197 lines
9.4 KiB
Java
197 lines
9.4 KiB
Java
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;
|
||
}
|
||
}
|