refactor: clean GSD comments and translate remaining Java sources to English
- Reduce javadocs to one-liners across config/region/physics/tick/viz/plugin root - Translate residual French comments; no behavioural change - Tests adjusted where assertions referenced French strings
This commit is contained in:
@@ -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:
|
||||
*
|
||||
* <ol>
|
||||
* <li>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.</li>
|
||||
* <li>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)}.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p><strong>Threading contract:</strong> 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<EntityStore, TransformComponent> transformType;
|
||||
|
||||
private static ComponentType<EntityStore, TransformComponent> transform() {
|
||||
@@ -67,16 +48,10 @@ public final class RegionRegistry {
|
||||
private final GravityFlipConfig config;
|
||||
private final Config<GravityFlipConfig> holder; // nullable in tests
|
||||
|
||||
/** Immutable snapshot of the region list, swapped atomically on every mutation. */
|
||||
private final AtomicReference<List<GravityFlipRegion>> 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<Object, AtomicReference<RegionSnapshot>> snapshots = new ConcurrentHashMap<>();
|
||||
|
||||
public RegionRegistry(GravityFlipConfig cfg) {
|
||||
@@ -89,14 +64,12 @@ public final class RegionRegistry {
|
||||
this.regionsSnapshot = new AtomicReference<>(List.copyOf(cfg.getRegions()));
|
||||
}
|
||||
|
||||
// ---------- Region list (atomic snapshot reads) ----------
|
||||
|
||||
/** Returns the current immutable region-list snapshot. Safe to call from any thread. */
|
||||
/** Returns the current immutable region-list snapshot. */
|
||||
public Collection<GravityFlipRegion> all() {
|
||||
return regionsSnapshot.get();
|
||||
}
|
||||
|
||||
/** Read-only view of currently-enabled regions, derived from the atomic snapshot (NOT from config). */
|
||||
/** Read-only view of currently-enabled regions. */
|
||||
List<GravityFlipRegion> enabled() {
|
||||
List<GravityFlipRegion> out = new ArrayList<>();
|
||||
for (GravityFlipRegion r : regionsSnapshot.get()) {
|
||||
@@ -105,6 +78,7 @@ public final class RegionRegistry {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Adds a region; throws IllegalArgumentException if the name already exists. */
|
||||
public void add(GravityFlipRegion r) {
|
||||
synchronized (mutationLock) {
|
||||
for (GravityFlipRegion x : regionsSnapshot.get()) {
|
||||
@@ -117,6 +91,7 @@ public final class RegionRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes the region with the given name; returns true iff it existed. */
|
||||
public boolean remove(String name) {
|
||||
synchronized (mutationLock) {
|
||||
boolean removed = config.getRegions().removeIf(x -> x.getName().equals(name));
|
||||
@@ -125,6 +100,7 @@ public final class RegionRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/** Toggles the enabled flag on the named region; returns true iff it existed. */
|
||||
public boolean setEnabled(String name, boolean enabled) {
|
||||
synchronized (mutationLock) {
|
||||
for (GravityFlipRegion x : config.getRegions()) {
|
||||
@@ -138,28 +114,19 @@ public final class RegionRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/** Phase 4 hook: re-snapshot the region list after a command handler has mutated the underlying config. */
|
||||
/** Re-snapshots the region list after the underlying config was mutated externally. */
|
||||
public void refreshFromConfig(GravityFlipConfig cfg) {
|
||||
synchronized (mutationLock) {
|
||||
regionsSnapshot.set(List.copyOf(cfg.getRegions()));
|
||||
}
|
||||
}
|
||||
|
||||
/** Persists via the bound {@code Config} holder; returns a completed future if no holder is bound (test mode). */
|
||||
/** Persists via the bound {@code Config} holder (completed future if none is bound). */
|
||||
public CompletableFuture<Void> save() {
|
||||
return holder == null ? CompletableFuture.completedFuture(null) : holder.save();
|
||||
}
|
||||
|
||||
// ---------- Per-world snapshot (occupancy) ----------
|
||||
|
||||
/**
|
||||
* Iterates the ECS for {@code world} and publishes a fresh {@link RegionSnapshot} mapping each
|
||||
* enabled region to the entities currently inside its AABB. Safe to call from a scheduler thread.
|
||||
*
|
||||
* <p>READ-ONLY across the trust boundary: TransformComponent positions are only read (copied to
|
||||
* local doubles before {@link Box#containsPosition(double, double, double)}). Any MUTATIONS must
|
||||
* go through {@code World.execute(Runnable)} or a {@code CommandBuffer} (Phase 3 concern).
|
||||
*/
|
||||
/** Iterates the ECS for {@code world} and publishes a fresh per-region occupancy snapshot. */
|
||||
public void refreshFor(World world) {
|
||||
List<GravityFlipRegion> enabled = enabled();
|
||||
Map<GravityFlipRegion, Collection<Ref<EntityStore>>> byRegion = new ConcurrentHashMap<>();
|
||||
@@ -168,25 +135,20 @@ public final class RegionRegistry {
|
||||
}
|
||||
|
||||
if (enabled.isEmpty()) {
|
||||
// Aucun travail ECS → publication directe depuis le thread appelant.
|
||||
publishSnapshot(world, snapshotOf(world, byRegion));
|
||||
return;
|
||||
}
|
||||
|
||||
// THREADING (fix WorldThread assert 2026-04-23) : `Store.forEachEntityParallel` exige la
|
||||
// WorldThread. On dispatche scan + publication via `world.execute(Runnable)` pour satisfaire
|
||||
// `assertThread`. Conséquence : la publication devient asynchrone (1 tick décalé max) côté
|
||||
// consumers de `currentSnapshot(world)` — tolérable car le RegionTickLoop tourne @100ms, donc
|
||||
// la fraîcheur du snapshot reste ≤ 100ms dans le pire cas.
|
||||
// Store.forEachEntityParallel requires the WorldThread (assertThread), so dispatch via
|
||||
// world.execute(Runnable). Publication becomes asynchronous (<= 1 tick of lag) — tolerable
|
||||
// because RegionTickLoop runs @100ms, so snapshot freshness stays <= 100ms in the worst case.
|
||||
world.execute(() -> {
|
||||
try {
|
||||
Store<EntityStore> store = world.getEntityStore().getStore();
|
||||
ComponentType<EntityStore, TransformComponent> TRANSFORM = transform();
|
||||
// ComponentType IS-A Query, so TRANSFORM is passed directly (no builder).
|
||||
// ComponentType IS-A Query, so TRANSFORM is passed directly.
|
||||
store.forEachEntityParallel(TRANSFORM, (index, chunk, cmdBuf) -> {
|
||||
TransformComponent t = chunk.getComponent(index, TRANSFORM);
|
||||
// Pinned API 2026.03.26 returns com.hypixel.hytale.math.vector.Vector3d
|
||||
// (Hytale's own type), NOT org.joml.Vector3d. Same deviation as Phase 02-01.
|
||||
com.hypixel.hytale.math.vector.Vector3d pos = t.getPosition();
|
||||
// Copy to locals — getPosition() returns a backing field; never mutated here.
|
||||
double x = pos.x, y = pos.y, z = pos.z;
|
||||
@@ -200,35 +162,29 @@ public final class RegionRegistry {
|
||||
}
|
||||
});
|
||||
} catch (Throwable th) {
|
||||
// Swallow — publish whatever we collected (possibly empty). The tick loop's
|
||||
// errorHandler already routes uncaught throwables; this catch keeps the
|
||||
// scheduler alive across transient ECS-state errors (e.g., world being torn down).
|
||||
// Swallow — keeps the scheduler alive across transient ECS-state errors
|
||||
// (e.g., world being torn down). Publish whatever we collected so far.
|
||||
}
|
||||
|
||||
// Publication intra-Runnable : garantit que la table byRegion est complète quand
|
||||
// on la fige dans snapshotOf(...).
|
||||
publishSnapshot(world, snapshotOf(world, byRegion));
|
||||
});
|
||||
}
|
||||
|
||||
/** Off-thread consumer entry point. Returns {@code null} if no snapshot has been published yet. */
|
||||
/** Off-thread consumer entry point; returns null if no snapshot has been published yet. */
|
||||
public RegionSnapshot currentSnapshot(World world) {
|
||||
if (world == null) return null;
|
||||
AtomicReference<RegionSnapshot> ref = snapshots.get(world);
|
||||
return ref == null ? null : ref.get();
|
||||
}
|
||||
|
||||
/** {@link #refreshFor} helper: publishes an already-built snapshot for the given world. */
|
||||
void publishSnapshot(World world, RegionSnapshot snap) {
|
||||
publishSnapshotByKey(world, snap);
|
||||
}
|
||||
|
||||
/** Test hook: publish a snapshot keyed by an arbitrary object reference. */
|
||||
void publishSnapshotByKey(Object key, RegionSnapshot snap) {
|
||||
snapshots.computeIfAbsent(key, k -> new AtomicReference<>()).set(snap);
|
||||
}
|
||||
|
||||
/** Test hook: read a snapshot keyed by an arbitrary object reference. */
|
||||
RegionSnapshot currentSnapshotByKey(Object key) {
|
||||
AtomicReference<RegionSnapshot> ref = snapshots.get(key);
|
||||
return ref == null ? null : ref.get();
|
||||
@@ -238,7 +194,7 @@ public final class RegionRegistry {
|
||||
Map<GravityFlipRegion, Collection<Ref<EntityStore>>> byRegion) {
|
||||
long tick = 0L;
|
||||
try { tick = w.getTick(); } catch (Throwable ignored) {}
|
||||
final long tickId = Math.max(tick, 1L); // tests assert tickId() > 0
|
||||
final long tickId = Math.max(tick, 1L);
|
||||
final Map<GravityFlipRegion, Collection<Ref<EntityStore>>> frozen =
|
||||
Collections.unmodifiableMap(byRegion);
|
||||
return new RegionSnapshot() {
|
||||
|
||||
Reference in New Issue
Block a user