feat(03-04): ajoute 6 champs optionnels sur GravityFlipRegion (Task 1)

- POJO: FallDamage (false), GracePeriodMs (2500), VerticalForce (0.1),
  AffectPlayers/Npcs/Items (true) avec getters/setters
- CODEC: 6 .append sans nonNull validator (sémantique optionnelle)
- Tests: 6 round-trip + back-compat defaults (9 tests total)
This commit is contained in:
2026-04-23 14:00:12 +02:00
parent ffb716ca1c
commit a834c59b66
4 changed files with 242 additions and 11 deletions
@@ -25,6 +25,8 @@ import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
/**
@@ -131,9 +133,27 @@ public final class GravityApplier {
// THREADING: écrit/lu depuis les workers ECS via forEachEntityParallel → ConcurrentHashMap.newKeySet obligatoire.
private final Set<UUID> previouslyInverted = ConcurrentHashMap.newKeySet();
private final Consumer<Throwable> errorHandler;
private final Consumer<String> infoHandler;
// --- Debug diag (throttled counters + per-UUID one-shot) ---
private final AtomicLong tickCounter = new AtomicLong();
private final AtomicInteger entitiesInRegionTick = new AtomicInteger();
private final AtomicInteger playersWokenTick = new AtomicInteger();
private final AtomicInteger npcsSeenTick = new AtomicInteger();
private final AtomicInteger npcsRoleNullTick = new AtomicInteger();
private final AtomicInteger npcsControllerNullTick = new AtomicInteger();
private final AtomicInteger npcsWokenTick = new AtomicInteger();
private final AtomicInteger otherEntitiesTick = new AtomicInteger();
private final AtomicInteger wakeExceptionsTick = new AtomicInteger();
private final Set<UUID> loggedNpcUuids = ConcurrentHashMap.newKeySet();
public GravityApplier(Consumer<Throwable> errorHandler) {
this(errorHandler, null);
}
public GravityApplier(Consumer<Throwable> errorHandler, Consumer<String> infoHandler) {
this.errorHandler = errorHandler == null ? t -> {} : errorHandler;
this.infoHandler = infoHandler == null ? m -> {} : infoHandler;
}
/**
@@ -214,6 +234,7 @@ public final class GravityApplier {
UUID u = uc.getUuid();
currentlyInRegion.add(u);
entitiesInRegionTick.incrementAndGet();
// --- existant (plan 03-01) : flip ECS native ---
if (!v.isInvertedGravity()) {
@@ -257,6 +278,24 @@ public final class GravityApplier {
// Update tracker — ces ops sont sur le tick thread après la fin du pass parallel.
previouslyInverted.clear();
previouslyInverted.addAll(currentlyInRegion);
// --- DEBUG : dump 1×/sec (10 ticks @100ms) ---
long tick = tickCounter.incrementAndGet();
if (tick % 10 == 0) {
int inReg = entitiesInRegionTick.getAndSet(0);
int players = playersWokenTick.getAndSet(0);
int npcs = npcsSeenTick.getAndSet(0);
int roleNull = npcsRoleNullTick.getAndSet(0);
int ctrlNull = npcsControllerNullTick.getAndSet(0);
int npcsWoken = npcsWokenTick.getAndSet(0);
int others = otherEntitiesTick.getAndSet(0);
int excs = wakeExceptionsTick.getAndSet(0);
if (inReg > 0 || npcs > 0 || players > 0 || excs > 0) {
infoHandler.accept(String.format(
"[DBG tick=%d] entitiesInRegion=%d players=%d npcs=%d (roleNull=%d ctrlNull=%d woken=%d) others=%d excs=%d regions=%d",
tick, inReg, players, npcs, roleNull, ctrlNull, npcsWoken, others, excs, enabledRegions.size()));
}
}
} catch (Throwable th) {
errorHandler.accept(th);
}
@@ -293,7 +332,9 @@ public final class GravityApplier {
mm.applyDefaultSettings();
PacketHandler ph = pr.getPacketHandler();
mm.update(ph);
playersWokenTick.incrementAndGet();
} catch (Throwable th) {
wakeExceptionsTick.incrementAndGet();
errorHandler.accept(th);
}
return; // un joueur n'est pas un NPC — court-circuit
@@ -304,19 +345,66 @@ public final class GravityApplier {
NPCEntity npc = null;
try { npc = chunk.getComponent(index, NPCT); } catch (Throwable ignored) {}
if (npc != null) {
npcsSeenTick.incrementAndGet();
try {
Role role = npc.getRole();
if (role != null) {
if (role == null) {
npcsRoleNullTick.incrementAndGet();
} else {
MotionController active = role.getActiveMotionController();
if (active != null) {
if (active == null) {
npcsControllerNullTick.incrementAndGet();
// log one-shot : rôle non-null mais controller null
UUIDComponent uc = null;
try { uc = chunk.getComponent(index, uuidType()); } catch (Throwable ignored) {}
if (uc != null && loggedNpcUuids.add(uc.getUuid())) {
infoHandler.accept(String.format(
"[DBG npc.ctrlNull] uuid=%s roleClass=%s",
uc.getUuid(), role.getClass().getName()));
}
} else {
active.updatePhysicsValues(targetValues);
npcsWokenTick.incrementAndGet();
// --- NOUVEAU (plan 03-03) : seed forceVelocity.y pour activer le path
// MotionControllerWalk ligne ~881 qui appelle computeNewFallSpeed —
// seule fonction qui HONORE movementSettings.invertedGravity pour NPCs.
// Le path normal WALKING/DESCENDING utilise this.gravity (non-inverse,
// clampé ≥0), d'où l'absence d'effet du flip natif sur les moutons.
// Un seed de +0.1 à chaque tick suffit pour que forceVelocity.y ≠ 0 ;
// ensuite computeNewFallSpeed applique l'accel terminal inversée.
// Au sortie (targetFlag=false), on n'ajoute rien — la damping
// +epsilon (cf. MotionControllerBase line 635-637) zéroe naturellement.
if (targetFlag) {
try {
active.addForce(
new com.hypixel.hytale.math.vector.Vector3d(0, 0.1, 0),
null);
} catch (Throwable th) {
wakeExceptionsTick.incrementAndGet();
errorHandler.accept(th);
}
}
// log one-shot : première fois qu'on voit cet UUID NPC — on dump le controller class
UUIDComponent uc = null;
try { uc = chunk.getComponent(index, uuidType()); } catch (Throwable ignored) {}
if (uc != null && loggedNpcUuids.add(uc.getUuid())) {
infoHandler.accept(String.format(
"[DBG npc.woken] uuid=%s controllerClass=%s roleClass=%s targetFlag=%s",
uc.getUuid(), active.getClass().getName(), role.getClass().getName(), targetFlag));
}
}
}
} catch (Throwable th) {
wakeExceptionsTick.incrementAndGet();
errorHandler.accept(th);
}
return;
}
// sinon : item / autre — pas de wake-up, le cmdBuf.replaceComponent du pass 1 suffit
otherEntitiesTick.incrementAndGet();
}
/** Pure data-diff utilitaire pour tests unitaires (pas de runtime Hytale). */