diff --git a/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java b/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java
index 6c422c3..6f015fd 100644
--- a/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java
+++ b/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java
@@ -23,25 +23,11 @@ import com.hypixel.hytale.server.core.modules.interaction.interaction.config.Int
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.
- */
+/** Entry point for the Gravity Flip plugin (Hytale Server API setup/shutdown lifecycle). */
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}.
- */
+ // Persisted region store at /regions.json. The named withConfig(name, codec)
+ // overload is REQUIRED — the 1-arg overload hardcodes the filename to config.json.
private final Config configHolder =
withConfig("regions", GravityFlipConfig.CODEC);
@@ -58,28 +44,18 @@ public class GravityFlipPlugin extends JavaPlugin {
@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).
+ // Do NOT call configHolder.get() here — it blocks until preLoad() completes.
+ // Safe call sites are start() and any later lifecycle phase.
//
- // 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(...).
+ // World acquisition: PrepareUniverseEvent only exposes a WorldConfigProvider, not a
+ // Universe/World. We use a Supplier that resolves Universe.get().getDefaultWorld()
+ // lazily on each tick; until the universe is ready, the supplier returns null and the
+ // tick is a no-op.
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(
@@ -107,25 +83,20 @@ public class GravityFlipPlugin extends JavaPlugin {
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()).
+ // TaskRegistry.registerTask only accepts ScheduledFuture; the scheduler returns
+ // ScheduledFuture>. Cast via raw types; the fallback keeps teardown deterministic
+ // 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(
@@ -135,28 +106,22 @@ public class GravityFlipPlugin extends JavaPlugin {
@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).
+ // Clear debug shapes on clients BEFORE stopping the tick loop. If the universe is already
+ // torn down, world==null and shapes will expire 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.
+ // Stop the detector BEFORE super.shutdown() so no tick races the 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.
+ // No auto-save contract: mutations made during the session must already have been
+ // persisted via configHolder().save() by the command handler that performed them.
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}.
- */
+ /** Showcase zone seeded on first run (10x20x10 box at (0,100,0)..(10,120,10) with Torch_Fire particles). */
private static GravityFlipRegion buildShowcaseRegion() {
GravityFlipRegion r = new GravityFlipRegion(
"demo-gravity-flip",
@@ -170,25 +135,17 @@ public class GravityFlipPlugin extends JavaPlugin {
return r;
}
- /** Exposed for Phase 3 (gravity physics) and Phase 4 (commands). */
+ /** Exposes the region registry to commands and physics code. */
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.
- */
+ /** Per-player wand selection store; returns 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.
+ * Accessor for the region config holder. SAVE CONTRACT: any caller that mutates
+ * {@code configHolder().get().getRegions()} MUST call {@code configHolder().save()} afterwards.
+ * {@code Config.get()} returns a SHARED MUTABLE reference; concurrent writers corrupt state —
+ * {@code RegionRegistry} snapshots it into an {@code AtomicReference} for tick-loop reads.
*/
public Config configHolder() {
return configHolder;
diff --git a/src/main/java/com/mythlane/gravityflip/config/GravityFlipConfig.java b/src/main/java/com/mythlane/gravityflip/config/GravityFlipConfig.java
index c44d093..0b79f37 100644
--- a/src/main/java/com/mythlane/gravityflip/config/GravityFlipConfig.java
+++ b/src/main/java/com/mythlane/gravityflip/config/GravityFlipConfig.java
@@ -9,20 +9,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-/**
- * Root config wrapping the persisted list of {@link GravityFlipRegion}s.
- *
- *
Persisted as {@code /regions.json} via the named
- * {@code withConfig("regions", GravityFlipConfig.CODEC)} overload on
- * {@code PluginBase}. The 1-arg overload would hardcode {@code config.json} —
- * deliberately avoided.
- *
- *
The decoded list is wrapped in a mutable {@code ArrayList}
- * (NOT {@code List.of(arr)}, which is immutable) so Phase 4 command handlers
- * can {@code add}/{@code remove} regions at runtime. Callers that mutate the
- * list MUST call {@code Config.save()} afterwards — there is no auto-save
- * lifecycle hook.
- */
+/** Root config wrapping the persisted list of {@link GravityFlipRegion}s (persisted as regions.json). */
public final class GravityFlipConfig {
public static final BuilderCodec CODEC =
@@ -30,7 +17,7 @@ public final class GravityFlipConfig {
.append(
new KeyedCodec<>("Regions",
new ArrayCodec<>(GravityFlipRegion.CODEC, GravityFlipRegion[]::new)),
- // Decode setter: wrap in a MUTABLE ArrayList so commands can add/remove.
+ // Decode into a MUTABLE ArrayList so commands can add/remove at runtime.
(c, arr) -> c.regions = new ArrayList<>(Arrays.asList(arr)),
c -> c.regions.toArray(GravityFlipRegion[]::new)
).add()
diff --git a/src/main/java/com/mythlane/gravityflip/physics/FallDamageGuard.java b/src/main/java/com/mythlane/gravityflip/physics/FallDamageGuard.java
index 3b7c890..cb2d60d 100644
--- a/src/main/java/com/mythlane/gravityflip/physics/FallDamageGuard.java
+++ b/src/main/java/com/mythlane/gravityflip/physics/FallDamageGuard.java
@@ -6,56 +6,30 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
- * Thread-safe tracker that decides whether fall damage should be suppressed for a given UUID
- * based on (a) current in-region membership, (b) post-exit grace window.
- *
- *
Pure-data service : no Hytale runtime dependency (no ECS, no PhysicsValues), injected
- * via constructor into {@link GravityApplier} (populator) and
- * {@link FallDamageSuppressorSystem} (consumer) — no static mutable state.
- *
- *
Precedence rule (multi-region) : when {@link #markInRegion} is called, the LAST
- * call for a given UUID within a single tick wins. {@link GravityApplier} takes care of
- * passing the FIRST matched region (iteration order of {@code enabledRegions}), so for an
- * entity simultaneously in N regions the first-match region's {@code FallDamage}
- * and {@code GracePeriodMs} drive suppression.
- *
- *
If current region != null AND current region {@code FallDamage==false} → true.
- *
Else if grace entry present AND grace region {@code FallDamage==false} AND
- * {@code nowMs - exitMs <= gracePeriodMs} → true.
- *
Else → false.
- *
- *
- *
+ * Thread-safe tracker that decides whether fall damage should be suppressed for a given UUID,
+ * based on current in-region membership and a post-exit grace window.
*/
public final class FallDamageGuard {
- /** Entity currently inside a region (cleared on exit). */
private final ConcurrentHashMap currentRegionByUuid =
new ConcurrentHashMap<>();
- /** Timestamp (ms) at which the entity last exited a region. */
private final ConcurrentHashMap exitTimestampByUuid =
new ConcurrentHashMap<>();
- /** Region referenced at the moment of exit (used to read FallDamage + GracePeriodMs). */
private final ConcurrentHashMap regionAtExitByUuid =
new ConcurrentHashMap<>();
+ /** Marks an entity as currently inside the given region (clears any stale grace entry). */
public void markInRegion(UUID uuid, GravityFlipRegion region) {
if (uuid == null || region == null) return;
currentRegionByUuid.put(uuid, region);
- // Re-entry ⇒ discard any stale grace entry (grace is a post-exit concept).
+ // Re-entry discards any stale grace entry — grace is a post-exit concept.
exitTimestampByUuid.remove(uuid);
regionAtExitByUuid.remove(uuid);
}
+ /** Records the exit timestamp and the region the entity just left. */
public void markExit(UUID uuid, GravityFlipRegion region, long nowMs) {
if (uuid == null || region == null) return;
currentRegionByUuid.remove(uuid);
@@ -63,6 +37,7 @@ public final class FallDamageGuard {
regionAtExitByUuid.put(uuid, region);
}
+ /** Returns true iff fall damage must be cancelled for this UUID at the given time. */
public boolean shouldSuppressFallDamage(UUID uuid, long nowMs) {
if (uuid == null) return false;
@@ -74,11 +49,11 @@ public final class FallDamageGuard {
Long exitMs = exitTimestampByUuid.get(uuid);
GravityFlipRegion exitRegion = regionAtExitByUuid.get(uuid);
if (exitMs == null || exitRegion == null) return false;
- if (exitRegion.isFallDamage()) return false; // region allowed fall damage → no grace
+ if (exitRegion.isFallDamage()) return false;
return (nowMs - exitMs) <= exitRegion.getGracePeriodMs();
}
- // ---- Test hooks / diagnostics (package-private) ----
+ // Test hooks (package-private).
int trackedInRegionCount() { return currentRegionByUuid.size(); }
int trackedGraceCount() { return exitTimestampByUuid.size(); }
diff --git a/src/main/java/com/mythlane/gravityflip/physics/FallDamageSuppressorSystem.java b/src/main/java/com/mythlane/gravityflip/physics/FallDamageSuppressorSystem.java
index 1f0d609..8ac9787 100644
--- a/src/main/java/com/mythlane/gravityflip/physics/FallDamageSuppressorSystem.java
+++ b/src/main/java/com/mythlane/gravityflip/physics/FallDamageSuppressorSystem.java
@@ -17,21 +17,7 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.function.Consumer;
-/**
- * ECS {@link DamageEventSystem} that cancels {@link Damage} events of cause {@code Fall}
- * for any entity whose UUID is currently suppressed by {@link FallDamageGuard}.
- *
- *
Registered in {@code GravityFlipPlugin.setup()} via
- * {@code getEntityStoreRegistry().registerSystem(new FallDamageSuppressorSystem(guard, errorHandler))}.
- *
- *
Dispatched in {@link DamageModule#getInspectDamageGroup()} — same group as
- * {@code FlockMembershipSystems.OnDamageReceived} (reference template), fires during the
- * damage inspection pass before {@code DamageSystems.ApplyDamage} consumes {@code Damage.amount}.
- *
- *
FALL_INDEX is resolved lazily via {@code DamageCause.getAssetMap().getIndex("Fall")} and
- * cached for the life of the server — the asset map is built once during {@code EntityModule}
- * setup and stable thereafter.
- */
+/** Cancels {@link Damage} events of cause {@code Fall} for any UUID suppressed by {@link FallDamageGuard}. */
public final class FallDamageSuppressorSystem extends DamageEventSystem {
private final FallDamageGuard guard;
diff --git a/src/main/java/com/mythlane/gravityflip/physics/GravityApplier.java b/src/main/java/com/mythlane/gravityflip/physics/GravityApplier.java
index 99277ee..b4dbe5e 100644
--- a/src/main/java/com/mythlane/gravityflip/physics/GravityApplier.java
+++ b/src/main/java/com/mythlane/gravityflip/physics/GravityApplier.java
@@ -28,44 +28,12 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
/**
- * Tick-driven service that toggles the native {@code PhysicsValues.invertedGravity} flag on every
- * entity present in an enabled {@link GravityFlipRegion}. Mutations are queued via
- * {@code CommandBuffer.replaceComponent(...)} inside a {@code Store.forEachEntityParallel(...)}
- * lambda — the ECS engine commits them on the main thread after the parallel pass.
- *
- *
Phase 03-03: seed {@code addForce(0, +0.1, 0)} on NPCs each tick in-region to activate
- * {@code computeNewFallSpeed} path which honours {@code invertedGravity}.
- *
- *
Phase 03-04:
- *
- *
Per-region tuning consumed : {@code AffectPlayers} / {@code AffectNpcs} / {@code AffectItems}
- * filter BEFORE any wake / cmdBuf flip ; {@code VerticalForce} replaces the hardcoded 0.1.
- *
{@link FallDamageGuard} notified on entry (pass 1) and exit (pass 2) with the
- * first-matched region to drive {@link FallDamageSuppressorSystem}.
- *
- *
- *
Hotpath note: Pass 1 (flip) and Pass 2 (restore) each iterate
- * {@code forEachEntityParallel}. They CANNOT be fused: restore requires the complete
- * {@code currentlyInRegion} set (computed during Pass 1) to diff against
- * {@code previouslyInverted}. Merging would either miss restores or double-visit
- * entities with inconsistent per-chunk state.
- *
- *
Multi-region precedence : for an entity simultaneously inside N regions, the FIRST
- * region encountered in the iteration of {@code snapshot.byRegion().keySet()} (Java insertion
- * order via the underlying {@code LinkedHashMap}) drives the config values read this tick
- * (VerticalForce, AffectXxx, FallDamage, GracePeriodMs). Rule applies consistently to
- * {@link FallDamageGuard#markInRegion} (same first-matched region) and {@link FallDamageGuard#markExit}
- * (last-known first-matched region at the previous in-region tick).
+ * Tick-driven service that toggles {@code PhysicsValues.invertedGravity} on every entity currently
+ * inside an enabled region, wakes up players/NPCs so the new settings take effect, and drives the
+ * fall-damage guard on entry/exit transitions.
*/
public final class GravityApplier {
- // Lazy ComponentType holders — pattern identique à RegionRegistry.transform()
- // (évite static-init Hytale PluginBase pendant les tests).
+ // Lazy ComponentType holders — avoids Hytale PluginBase static init during tests.
private static volatile ComponentType physicsType;
private static volatile ComponentType uuidType;
private static volatile ComponentType transformType;
@@ -145,7 +113,7 @@ public final class GravityApplier {
return t;
}
- // THREADING: écrit/lu depuis les workers ECS via forEachEntityParallel → ConcurrentHashMap.newKeySet obligatoire.
+ // Written/read from ECS worker threads via forEachEntityParallel — concurrent collection required.
private final Set previouslyInverted = ConcurrentHashMap.newKeySet();
/** First-matched region per UUID at the previous tick — consulted in Pass 2 for markExit. */
private final ConcurrentHashMap lastKnownRegion = new ConcurrentHashMap<>();
@@ -162,21 +130,17 @@ public final class GravityApplier {
this.guard = guard == null ? new FallDamageGuard() : guard;
}
- /**
- * Construit un nouveau {@link PhysicsValues} en copiant mass/drag de la source et en fixant
- * {@code invertedGravity=target}. Pure data — pas d'effet de bord.
- */
+ /** Builds a new PhysicsValues copying mass/drag from the source and setting invertedGravity. */
static PhysicsValues buildPhysicsValuesWithFlag(PhysicsValues source, boolean target) {
FlaggedDecision d = buildFlaggedDecision(source.getMass(), source.getDragCoefficient(), target);
return new PhysicsValues(d.mass, d.drag, d.invertedGravity);
}
- /** Pure-data seam pour tests unitaires (aucune dépendance sur PhysicsValues). */
+ /** Pure-data seam for unit tests — PhysicsValues static init is unavailable outside the server runtime. */
static FlaggedDecision buildFlaggedDecision(double mass, double drag, boolean target) {
return new FlaggedDecision(mass, drag, target);
}
- /** Holder pure-data pour la décomposition testable de {@link #buildPhysicsValuesWithFlag}. */
static final class FlaggedDecision {
final double mass;
final double drag;
@@ -186,7 +150,7 @@ public final class GravityApplier {
}
}
- /** Tick entry point. NO-OP si world ou snapshot est null. */
+ /** Tick entry point; no-op when world or snapshot is null. */
public void apply(World world, RegionSnapshot snapshot) {
if (world == null || snapshot == null) return;
world.execute(() -> applyOnWorldThread(world, snapshot));
@@ -205,8 +169,9 @@ public final class GravityApplier {
ComponentType PLT = playerType();
ComponentType NPCT = npcEntityType();
- // PASS 1 — pour chaque entité avec PhysicsValues : si dans une région activée, queue le flip ON
- // via cmdBuf.replaceComponent ET wake-up MovementManager / MotionController.
+ // PASS 1 — flip ON for every entity with PhysicsValues that is inside an enabled region.
+ // Pass 1 and Pass 2 cannot be fused: restore requires the complete currentlyInRegion set
+ // to diff against previouslyInverted.
Set currentlyInRegion = ConcurrentHashMap.newKeySet();
store.forEachEntityParallel(PHYST, (index, chunk, cmdBuf) -> {
TransformComponent t;
@@ -222,7 +187,7 @@ public final class GravityApplier {
com.hypixel.hytale.math.vector.Vector3d pos = t.getPosition();
double x = pos.x, y = pos.y, z = pos.z;
- // First-match wins for multi-region precedence (Plan 03-04).
+ // First-match wins for multi-region precedence.
GravityFlipRegion matchedRegion = null;
for (GravityFlipRegion r : enabledRegions) {
if (r.asBox().containsPosition(x, y, z)) { matchedRegion = r; break; }
@@ -231,7 +196,6 @@ public final class GravityApplier {
UUID u = uc.getUuid();
- // --- Plan 03-04 : AffectXxx filters applied BEFORE wake ---
EntityKind kind = classify(chunk, index, MMT, PRT, PLT, NPCT);
boolean allowed;
switch (kind) {
@@ -240,8 +204,7 @@ public final class GravityApplier {
default: allowed = matchedRegion.isAffectItems(); break;
}
if (!allowed) {
- // Entité filtrée : ne PAS la compter dans currentlyInRegion, et
- // ne PAS notifier le guard — le filtre se comporte comme hors-zone.
+ // Filtered entity behaves as if outside the region.
return;
}
@@ -249,19 +212,17 @@ public final class GravityApplier {
lastKnownRegion.put(u, matchedRegion);
guard.markInRegion(u, matchedRegion);
- // --- Flip ECS native (plan 03-01) ---
if (!v.isInvertedGravity()) {
Ref ref = chunk.getReferenceTo(index);
cmdBuf.replaceComponent(ref, PHYST,
new PhysicsValues(v.getMass(), v.getDragCoefficient(), true));
}
- // --- Wake-up joueur/NPC (plan 03-02) + seed VerticalForce (plan 03-03 paramétré 03-04) ---
wakePlayerOrNpc(chunk, index, v, true, matchedRegion,
MMT, PRT, PLT, NPCT);
});
- // PASS 2 — restore : pour chaque UUID dans previouslyInverted \ currentlyInRegion.
+ // PASS 2 — restore: for every UUID in previouslyInverted \ currentlyInRegion.
Set toRestore = ConcurrentHashMap.newKeySet();
toRestore.addAll(previouslyInverted);
toRestore.removeAll(currentlyInRegion);
@@ -284,11 +245,8 @@ public final class GravityApplier {
new PhysicsValues(v.getMass(), v.getDragCoefficient(), false));
}
- // Wake-up avec flag=false pour restaurer les settings natifs.
wakePlayerOrNpc(chunk, index, v, false, null, MMT, PRT, PLT, NPCT);
- // Plan 03-04 : notifier guard.markExit avec la région last-known
- // (première région matchée au tick précédent).
GravityFlipRegion lastRegion = lastKnownRegion.remove(u);
if (lastRegion != null) {
guard.markExit(u, lastRegion, now);
@@ -296,7 +254,7 @@ public final class GravityApplier {
});
}
- // Update tracker — ces ops sont sur le tick thread après la fin du pass parallel.
+ // Tick-thread update after the parallel pass completes.
previouslyInverted.clear();
previouslyInverted.addAll(currentlyInRegion);
} catch (Throwable th) {
@@ -306,7 +264,7 @@ public final class GravityApplier {
private enum EntityKind { PLAYER, NPC, OTHER }
- /** Classify the entity at {@code index} into player / NPC / other (items fall into other). */
+ /** Classifies the entity at {@code index} into player / NPC / other (items fall into other). */
private EntityKind classify(ArchetypeChunk chunk, int index,
ComponentType MMT,
ComponentType PRT,
@@ -328,15 +286,7 @@ public final class GravityApplier {
return EntityKind.OTHER;
}
- /**
- * Wake-up dans le MÊME lambda parallèle :
- * - joueur (MovementManager + PlayerRef + Player) → setDefaultSettings + applyDefaultSettings + update(ph)
- * - NPC (NPCEntity avec role non-null et active controller non-null) → updatePhysicsValues(targetValues)
- * - sinon (items, autres) : no-op (le flip cmdBuf.replaceComponent du pass 1 suffit)
- *
- *
Plan 03-04 : le paramètre {@code matchedRegion} (non-null en entrée, null en sortie)
- * fournit {@code VerticalForce} — remplace le hardcode 0.1 du plan 03-03.
- */
+ /** Wakes up the entity so the new PhysicsValues take effect (player movement refresh or NPC controller update). */
private void wakePlayerOrNpc(
ArchetypeChunk chunk, int index,
PhysicsValues sourceValues, boolean targetFlag,
@@ -347,7 +297,7 @@ public final class GravityApplier {
ComponentType NPCT) {
PhysicsValues targetValues = buildPhysicsValuesWithFlag(sourceValues, targetFlag);
- // --- Branche joueur ---
+ // Player branch.
MovementManager mm = null;
try { mm = chunk.getComponent(index, MMT); } catch (Throwable ignored) {}
if (mm != null) {
@@ -363,11 +313,11 @@ public final class GravityApplier {
} catch (Throwable th) {
errorHandler.accept(th);
}
- return; // un joueur n'est pas un NPC
+ return;
}
}
- // --- Branche NPC ---
+ // NPC branch.
NPCEntity npc = null;
try { npc = chunk.getComponent(index, NPCT); } catch (Throwable ignored) {}
if (npc != null) {
@@ -378,9 +328,7 @@ public final class GravityApplier {
if (active != null) {
active.updatePhysicsValues(targetValues);
- // Plan 03-04 : seed forceVelocity.y paramétré par VerticalForce (remplace
- // le hardcode 0.1 de Plan 03-03). Uniquement en entrée (targetFlag=true) —
- // à la sortie le damping natif zéroe forceVelocity.
+ // Seed forceVelocity.y only on entry — on exit the native damping zeroes it.
if (targetFlag && matchedRegion != null) {
double vf = matchedRegion.getVerticalForce();
try {
@@ -399,10 +347,10 @@ public final class GravityApplier {
return;
}
- // sinon : item / autre — no-op
+ // Items / other: no-op.
}
- /** Pure data-diff utilitaire pour tests unitaires (pas de runtime Hytale). */
+ /** Pure data-diff helper for unit tests. */
public static DiffResult diff(Set previous, Set current) {
Set toFlip = new HashSet<>(current);
toFlip.removeAll(previous);
@@ -417,17 +365,14 @@ public final class GravityApplier {
DiffResult(Set f, Set r) { this.toFlip = f; this.toRestore = r; }
}
- // ---- Test hooks (package-private) ----
+ // Test hooks (package-private).
- /** Vue immuable du tracker — pour tests unitaires sémantiques (résolution WARNING 2). */
+ /** Immutable view of the internal tracker for unit tests. */
Set previouslyInvertedView() {
return Collections.unmodifiableSet(previouslyInverted);
}
- /**
- * Force une valeur du tracker hors runtime ECS — pour tester la sémantique sans dépendre de World/Store.
- * Package-private : NE PAS appeler depuis le code de production.
- */
+ /** Test-only tracker override; never call from production code. */
void __updateTrackerForTest(Set newState) {
previouslyInverted.clear();
previouslyInverted.addAll(newState);
diff --git a/src/main/java/com/mythlane/gravityflip/region/GravityFlipRegion.java b/src/main/java/com/mythlane/gravityflip/region/GravityFlipRegion.java
index c6be72b..4cdc29c 100644
--- a/src/main/java/com/mythlane/gravityflip/region/GravityFlipRegion.java
+++ b/src/main/java/com/mythlane/gravityflip/region/GravityFlipRegion.java
@@ -7,53 +7,7 @@ import com.hypixel.hytale.codec.validation.Validators;
import com.hypixel.hytale.math.shape.Box;
import com.hypixel.hytale.math.vector.Vector3d;
-/**
- * A named axis-aligned region in which gravity is inverted for any entity inside.
- *
- *
Persisted as part of {@code GravityFlipConfig} via {@link #CODEC}. The region is stored
- * on disk as up to 9 keys :
- *
- *
{@code Name} — non-null string identifier
- *
{@code Box} — non-null AABB (composed of two {@code Vector3d} corners)
{@code FallDamage} — optional (Plan 03-04). Default {@code false}. When
- * {@code false}, entities in-region do not suffer fall damage, and post-exit fall
- * damage is suppressed for {@code GracePeriodMs} ms (see {@code FallDamageSuppressorSystem}).
- *
{@code GracePeriodMs} — optional, default {@code 2500}. Grace window in ms
- * after exit during which fall damage remains suppressed.
- *
{@code VerticalForce} — optional, default {@code 0.1}. Seed added each tick to
- * NPC {@code forceVelocity.y} to activate the inverted gravity path (Plan 03-03).
- *
{@code AffectPlayers} — optional, default {@code true}. If {@code false},
- * players in-region are NOT flipped.
- *
{@code AffectNpcs} — optional, default {@code true}. If {@code false},
- * NPCs in-region are NOT flipped / seeded.
- *
{@code AffectItems} — optional, default {@code true}. If {@code false},
- * item {@code PhysicsValues.invertedGravity} is NOT mutated.
- *
{@code VisualColor} — optional (Plan 03-05). Default {@code "#00FFFF"} (cyan).
- * Hex string {@code #RRGGBB} rendered as debug cube color. Fallback to cyan on invalid hex
- * (parsing + validation lives in {@code RegionVisualizer}, NOT here).
- *
{@code VisualMode} — optional, default {@code "Outline"}. One of
- * {@code "Outline"} / {@code "Faces"} / {@code "Both"} / {@code "None"}. Unknown value
- * falls back to {@code "Outline"}.
- *
{@code VisualRefreshMs} — optional, default {@code 1000}. Emission period in ms
- * for the debug shape (refresh cadence ; TTL = refreshMs * 1.2 to avoid flicker).
- *
{@code VisualOpacity} — optional, default {@code 0.5}. Cube opacity in
- * {@code [0.0, 1.0]} (clamped in {@code RegionVisualizer}).
- *
- *
- *
Back-compat : a legacy {@code regions.json} containing only Name+Box+Enabled
- * continues to load without error — the codec for the 6 new fields does NOT chain
- * {@code addValidator(Validators.nonNull())}, which makes them optional in BuilderField
- * semantics (absence of the BSON key ⇒ setter skipped ⇒ Java default initializer preserved).
- *
- *
Note: this build pins {@code com.hypixel.hytale:Server:2026.03.26-89796e57b}, in which
- * {@code Box.min}/{@code Box.max} are {@code com.hypixel.hytale.math.vector.Vector3d}
- * (Hytale's own type — NOT {@code org.joml.Vector3d}, which only appeared in later builds).
- *
- *
Fields are package-private mutable so the codec can write into them directly,
- * mirroring the canonical {@code MythLoggerConfig} pattern. Public getters/setters
- * are provided for runtime callers (Phase 4 commands).
- */
+/** A named axis-aligned region in which gravity is inverted for any entity inside. */
public final class GravityFlipRegion {
public static final BuilderCodec CODEC =
@@ -66,7 +20,7 @@ public final class GravityFlipRegion {
.addValidator(Validators.nonNull()).add()
.append(new KeyedCodec<>("Enabled", Codec.BOOLEAN),
(r, v) -> r.enabled = v, r -> r.enabled).add()
- // --- Plan 03-04 : 6 optional tuning fields (no nonNull validator ⇒ optional) ---
+ // Optional tuning fields (no nonNull validator => absence preserves Java defaults).
.append(new KeyedCodec<>("FallDamage", Codec.BOOLEAN),
(r, v) -> r.fallDamage = v, r -> r.fallDamage).add()
.append(new KeyedCodec<>("GracePeriodMs", Codec.INTEGER),
@@ -79,7 +33,6 @@ public final class GravityFlipRegion {
(r, v) -> r.affectNpcs = v, r -> r.affectNpcs).add()
.append(new KeyedCodec<>("AffectItems", Codec.BOOLEAN),
(r, v) -> r.affectItems = v, r -> r.affectItems).add()
- // --- Plan 03-05 : 4 optional visualization fields ---
.append(new KeyedCodec<>("VisualColor", Codec.STRING),
(r, v) -> r.visualColor = v, r -> r.visualColor).add()
.append(new KeyedCodec<>("VisualMode", Codec.STRING),
@@ -88,7 +41,6 @@ public final class GravityFlipRegion {
(r, v) -> r.visualRefreshMs = v, r -> r.visualRefreshMs).add()
.append(new KeyedCodec<>("VisualOpacity", Codec.DOUBLE),
(r, v) -> r.visualOpacity = v, r -> r.visualOpacity).add()
- // --- Plan 03-06 : 2 optional particle-mode fields ---
.append(new KeyedCodec<>("VisualParticleId", Codec.STRING),
(r, v) -> r.visualParticleId = v, r -> r.visualParticleId).add()
.append(new KeyedCodec<>("VisualParticleDensity", Codec.DOUBLE),
@@ -100,7 +52,6 @@ public final class GravityFlipRegion {
Box box = new Box(new Vector3d(), new Vector3d());
boolean enabled = true;
- // Plan 03-04 : tuning fields — defaults applied when key absent in BSON.
boolean fallDamage = false;
int gracePeriodMs = 2500;
double verticalForce = 0.1;
@@ -108,15 +59,12 @@ public final class GravityFlipRegion {
boolean affectNpcs = true;
boolean affectItems = true;
- // Plan 03-05 : visualization fields — defaults applied when key absent in BSON.
String visualColor = "#00FFFF";
String visualMode = "Outline";
int visualRefreshMs = 1000;
double visualOpacity = 0.5;
- // Plan 03-06 : particle-mode fields — default switched to Torch_Fire after UAT
- // showed Dust_Sparkles_Fine is invisible in-world. Torch_Fire is a persistent flame
- // that renders reliably at any altitude. Density 0.3 = ~12 points per 40m edge.
+ // Torch_Fire chosen because Dust_Sparkles_Fine is effectively invisible in-world.
String visualParticleId = "Torch_Fire";
double visualParticleDensity = 0.3;
@@ -138,8 +86,6 @@ public final class GravityFlipRegion {
public void setBox(Box b) { this.box = b; }
public void setEnabled(boolean v) { this.enabled = v; }
- // --- Plan 03-04 getters / setters ---
-
public boolean isFallDamage() { return fallDamage; }
public int getGracePeriodMs() { return gracePeriodMs; }
public double getVerticalForce() { return verticalForce; }
@@ -154,8 +100,6 @@ public final class GravityFlipRegion {
public void setAffectNpcs(boolean v) { this.affectNpcs = v; }
public void setAffectItems(boolean v) { this.affectItems = v; }
- // --- Plan 03-05 getters / setters ---
-
public String getVisualColor() { return visualColor; }
public String getVisualMode() { return visualMode; }
public int getVisualRefreshMs() { return visualRefreshMs; }
@@ -166,14 +110,12 @@ public final class GravityFlipRegion {
public void setVisualRefreshMs(int v) { this.visualRefreshMs = v; }
public void setVisualOpacity(double v) { this.visualOpacity = v; }
- // --- Plan 03-06 getters / setters ---
-
public String getVisualParticleId() { return visualParticleId; }
public double getVisualParticleDensity() { return visualParticleDensity; }
public void setVisualParticleId(String v) { this.visualParticleId = v; }
public void setVisualParticleDensity(double v) { this.visualParticleDensity = v; }
- /** Convenience accessor for tick-loop / physics consumers in Phase 02-02. */
+ /** Convenience accessor for tick-loop / physics consumers. */
public Box asBox() { return box; }
}
diff --git a/src/main/java/com/mythlane/gravityflip/region/RegionRegistry.java b/src/main/java/com/mythlane/gravityflip/region/RegionRegistry.java
index 0dce941..dea0dca 100644
--- a/src/main/java/com/mythlane/gravityflip/region/RegionRegistry.java
+++ b/src/main/java/com/mythlane/gravityflip/region/RegionRegistry.java
@@ -22,32 +22,13 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicReference;
/**
- * In-memory index of {@link GravityFlipRegion}s with two layers of atomic publication:
- *
- *
- *
An {@link AtomicReference} holding an immutable snapshot of the current region list,
- * written by CRUD methods and {@link #refreshFromConfig(GravityFlipConfig)} on the command
- * thread, read by the tick loop on the scheduler thread without locking.
- *
A per-{@link World} {@code AtomicReference<RegionSnapshot>} published by
- * {@link #refreshFor(World)} every tick; off-thread consumers (Phase 3 physics) read it via
- * {@link #currentSnapshot(World)}.
- *
- *
- *
Threading contract: the tick loop NEVER calls {@code config.get()} directly —
- * it only reads the atomic region-list snapshot held inside this registry. Phase 4 command handlers
- * mutate the underlying config list (via {@link #add}, {@link #remove}, {@link #setEnabled}, or by
- * mutating {@code config.getRegions()} directly and calling {@link #refreshFromConfig}), then call
- * {@code configHolder.save().join()} to persist.
+ * In-memory index of {@link GravityFlipRegion}s with atomic publication for lock-free reads
+ * from the tick loop. Mutations go through CRUD methods which swap an immutable snapshot.
*/
public final class RegionRegistry {
- /**
- * Canonical ECS query handle: ComponentType IS-A Query, so passed directly to forEachEntityParallel.
- * Lazily initialised because {@code TransformComponent.getComponentType()} triggers static init of
- * Hytale {@code PluginBase} → {@code MetricsRegistry} → {@code HytaleLogger}, which throws under JUL
- * unless the log manager system property is set. Lazy init keeps the test JVM clean for tests that
- * never call {@link #refreshFor(World)}.
- */
+ // Lazy init: TransformComponent.getComponentType() triggers Hytale PluginBase static init,
+ // which fails under JUL unless the log manager system property is set — avoided in tests.
private static volatile ComponentType transformType;
private static ComponentType transform() {
@@ -67,16 +48,10 @@ public final class RegionRegistry {
private final GravityFlipConfig config;
private final Config holder; // nullable in tests
- /** Immutable snapshot of the region list, swapped atomically on every mutation. */
private final AtomicReference> regionsSnapshot;
private final Object mutationLock = new Object();
- /**
- * Snapshot store keyed by {@link World} reference. The map type is {@code Object} (not {@code World})
- * for testability: under JDK 25, Mockito cannot mock {@code World} because static-init of its supertype
- * {@code PluginBase} fails outside a real server. Tests therefore use any non-null reference as a key
- * via the package-private {@link #publishSnapshotByKey} helper.
- */
+ // Keyed by Object (not World) because Mockito cannot mock World under JDK 25 in tests.
private final ConcurrentHashMap