feat(02-02): add RegionRegistry + RegionSnapshot with ECS refreshFor (Tasks 1+2)

- RegionSnapshot: immutable per-world occupancy view (byRegion/tickId/world)
- RegionRegistry:
  * AtomicReference<List<GravityFlipRegion>> for tick-loop-safe region reads
  * CRUD (add/remove/setEnabled) under mutationLock + atomic snapshot republish
  * refreshFromConfig(cfg) hook for Phase 4 command handlers
  * refreshFor(World) iterates ECS via forEachEntityParallel +
    TransformComponent.getComponentType() (ComponentType IS-A Query, no builder) +
    Box.containsPosition(x,y,z); publishes per-world snapshot via AtomicReference.
  * Lazy ComponentType init (avoids Hytale PluginBase static init in tests)
  * Snapshots map keyed by Object (not World) so JDK 25 tests don't need Mockito
- 7/7 RegionRegistryTest pass: CRUD, snapshot read/publish, cross-thread visibility,
  refreshFromConfig atomic swap.

Probe results (recorded for SUMMARY):
- ArchetypeChunk.getRef(int) NOT present; actual method is getReferenceTo(int)
- TransformComponent.getPosition() returns com.hypixel.hytale.math.vector.Vector3d
  in pinned 2026.03.26 (Phase 02-01 deviation pattern recurs)
This commit is contained in:
2026-04-23 00:54:32 +02:00
parent 216f544d9b
commit 6574c05128
3 changed files with 412 additions and 0 deletions
@@ -0,0 +1,142 @@
package com.mythlane.gravityflip.region;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.math.shape.Box;
import com.hypixel.hytale.math.vector.Vector3d;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.mythlane.gravityflip.config.GravityFlipConfig;
import org.junit.jupiter.api.Test;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
/**
* Pure-math + concurrency tests for {@link RegionRegistry}.
*
* <p>JDK 25 + Mockito + Hytale's {@code World} class is a bad combination — Mockito's inline
* MockMaker (the only one that can mock final classes) triggers static init of the supertype
* {@code PluginBase}, which fails outside a real server because {@code HytaleLogger} requires
* the JUL log manager to be set first. Therefore all snapshot tests use the package-private
* {@code publishSnapshotByKey} / {@code currentSnapshotByKey} hooks with {@code Object}
* sentinels, never a real or mocked {@code World}.
*/
class RegionRegistryTest {
private Box box() {
return new Box(new Vector3d(0, 0, 0), new Vector3d(10, 10, 10));
}
@Test
void newRegistryIsEmpty() {
RegionRegistry reg = new RegionRegistry(new GravityFlipConfig());
assertTrue(reg.all().isEmpty());
assertNull(reg.currentSnapshot(null));
assertNull(reg.currentSnapshotByKey(new Object()));
}
@Test
void addThenRejectDuplicateName() {
RegionRegistry reg = new RegionRegistry(new GravityFlipConfig());
reg.add(new GravityFlipRegion("a", box(), true));
assertEquals(1, reg.all().size());
assertThrows(IllegalArgumentException.class,
() -> reg.add(new GravityFlipRegion("a", box(), true)));
}
@Test
void setEnabledFlipsFlag_unknownReturnsFalse() {
RegionRegistry reg = new RegionRegistry(new GravityFlipConfig());
reg.add(new GravityFlipRegion("a", box(), true));
assertTrue(reg.setEnabled("a", false));
assertFalse(reg.all().iterator().next().isEnabled());
assertFalse(reg.setEnabled("ghost", true));
}
@Test
void removeReturnsTrueOnceThenFalse() {
RegionRegistry reg = new RegionRegistry(new GravityFlipConfig());
reg.add(new GravityFlipRegion("a", box(), true));
assertTrue(reg.remove("a"));
assertFalse(reg.remove("a"));
assertTrue(reg.all().isEmpty());
}
@Test
void publishSnapshotIsReadable_withMatchingKeys_andPositiveTickId() {
RegionRegistry reg = new RegionRegistry(new GravityFlipConfig());
Object key = new Object();
GravityFlipRegion r = new GravityFlipRegion("a", box(), true);
Map<GravityFlipRegion, Collection<Ref<EntityStore>>> byRegion = new HashMap<>();
byRegion.put(r, new java.util.ArrayList<>());
RegionSnapshot snap = new StubSnapshot(byRegion, 42L);
reg.publishSnapshotByKey(key, snap);
RegionSnapshot got = reg.currentSnapshotByKey(key);
assertNotNull(got);
assertTrue(got.byRegion().containsKey(r));
assertEquals(42L, got.tickId());
}
@Test
void publishOnThreadAIsVisibleOnThreadB() throws Exception {
RegionRegistry reg = new RegionRegistry(new GravityFlipConfig());
Object key = new Object();
CountDownLatch published = new CountDownLatch(1);
AtomicReference<RegionSnapshot> readBack = new AtomicReference<>();
Thread writer = new Thread(() -> {
reg.publishSnapshotByKey(key, new StubSnapshot(new HashMap<>(), 7L));
published.countDown();
});
Thread reader = new Thread(() -> {
try {
published.await(2, TimeUnit.SECONDS);
readBack.set(reg.currentSnapshotByKey(key));
} catch (InterruptedException ignored) {}
});
writer.start(); reader.start();
writer.join(2000); reader.join(2000);
assertNotNull(readBack.get());
assertEquals(7L, readBack.get().tickId());
}
@Test
void refreshFromConfigAtomicallySwapsRegionList() {
GravityFlipConfig cfg = new GravityFlipConfig();
cfg.getRegions().add(new GravityFlipRegion("a", box(), true));
RegionRegistry reg = new RegionRegistry(cfg);
// Reader captures the immutable list before the swap.
var before = reg.enabled();
assertEquals(1, before.size());
// Mutator swaps via refreshFromConfig.
cfg.getRegions().add(new GravityFlipRegion("b", box(), true));
reg.refreshFromConfig(cfg);
var after = reg.enabled();
assertEquals(2, after.size());
// The previously-captured list must NOT have been mutated under the reader.
assertEquals(1, before.size());
}
/** Minimal RegionSnapshot for the publish/read tests; world() is unused (returns null). */
private static final class StubSnapshot implements RegionSnapshot {
private final Map<GravityFlipRegion, Collection<Ref<EntityStore>>> by;
private final long tick;
StubSnapshot(Map<GravityFlipRegion, Collection<Ref<EntityStore>>> by, long tick) {
this.by = by; this.tick = tick;
}
@Override public Map<GravityFlipRegion, Collection<Ref<EntityStore>>> byRegion() { return by; }
@Override public long tickId() { return tick; }
@Override public World world() { return null; }
}
}