feat(03-04): FallDamageSuppressorSystem + wiring + cleanup [DBG throttle] (Task 3)
- FallDamageSuppressorSystem: subclass DamageEventSystem, cancel Damage cause=Fall quand guard.shouldSuppressFallDamage; FALL_INDEX lazy via DamageCause.getAssetMap().getIndex; groupe inspectDamageGroup - GravityApplier: 3-arg constructor avec FallDamageGuard; first-match region, filtres AffectPlayers/Npcs/Items avant wake, seed addForce paramétré par VerticalForce (remplace hardcode 0.1); notifie guard.markInRegion / markExit (via lastKnownRegion map pour Pass 2) - Cleanup: retire les AtomicInteger counters + log [DBG tick=...] throttle 1s; conserve logs one-shot [DBG npc.woken]/[DBG npc.ctrlNull] - GravityFlipPlugin.setup(): instancie FallDamageGuard, registerSystem sur entityStoreRegistry; start() passe guard au GravityApplier - Imports Query + CommandBuffer alignés sur FlockMembershipSystems
This commit is contained in:
@@ -6,6 +6,8 @@ import com.hypixel.hytale.server.core.universe.Universe;
|
|||||||
import com.hypixel.hytale.server.core.universe.world.World;
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
import com.hypixel.hytale.server.core.util.Config;
|
import com.hypixel.hytale.server.core.util.Config;
|
||||||
import com.mythlane.gravityflip.config.GravityFlipConfig;
|
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.physics.GravityApplier;
|
||||||
import com.mythlane.gravityflip.region.RegionRegistry;
|
import com.mythlane.gravityflip.region.RegionRegistry;
|
||||||
import com.mythlane.gravityflip.tick.RegionTickLoop;
|
import com.mythlane.gravityflip.tick.RegionTickLoop;
|
||||||
@@ -38,6 +40,7 @@ public class GravityFlipPlugin extends JavaPlugin {
|
|||||||
private RegionRegistry registry;
|
private RegionRegistry registry;
|
||||||
private RegionTickLoop tickLoop;
|
private RegionTickLoop tickLoop;
|
||||||
private GravityApplier gravityApplier;
|
private GravityApplier gravityApplier;
|
||||||
|
private FallDamageGuard fallDamageGuard;
|
||||||
|
|
||||||
public GravityFlipPlugin(JavaPluginInit init) {
|
public GravityFlipPlugin(JavaPluginInit init) {
|
||||||
super(init);
|
super(init);
|
||||||
@@ -55,6 +58,13 @@ public class GravityFlipPlugin extends JavaPlugin {
|
|||||||
// a Supplier<World> that resolves Universe.get().getDefaultWorld() lazily on each
|
// a Supplier<World> that resolves Universe.get().getDefaultWorld() lazily on each
|
||||||
// tick (matching the MythWorld WorldBorderManager precedent). Until the universe
|
// tick (matching the MythWorld WorldBorderManager precedent). Until the universe
|
||||||
// is ready, the supplier returns null and the tick is a no-op.
|
// 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")));
|
||||||
|
|
||||||
getLogger().at(Level.INFO).log("Gravity Flip enabled");
|
getLogger().at(Level.INFO).log("Gravity Flip enabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +75,8 @@ public class GravityFlipPlugin extends JavaPlugin {
|
|||||||
this.registry = new RegionRegistry(cfg, configHolder);
|
this.registry = new RegionRegistry(cfg, configHolder);
|
||||||
this.gravityApplier = new GravityApplier(
|
this.gravityApplier = new GravityApplier(
|
||||||
th -> getLogger().at(Level.WARNING).withCause(th).log("gravityApply failed"),
|
th -> getLogger().at(Level.WARNING).withCause(th).log("gravityApply failed"),
|
||||||
msg -> getLogger().at(Level.INFO).log("%s", msg));
|
msg -> getLogger().at(Level.INFO).log("%s", msg),
|
||||||
|
fallDamageGuard);
|
||||||
this.tickLoop = new RegionTickLoop(registry, gravityApplier, th ->
|
this.tickLoop = new RegionTickLoop(registry, gravityApplier, th ->
|
||||||
getLogger().at(Level.WARNING).withCause(th).log("detectTick failed"));
|
getLogger().at(Level.WARNING).withCause(th).log("detectTick failed"));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.mythlane.gravityflip.physics;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.component.Archetype;
|
||||||
|
import com.hypixel.hytale.component.ArchetypeChunk;
|
||||||
|
import com.hypixel.hytale.component.CommandBuffer;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.component.SystemGroup;
|
||||||
|
import com.hypixel.hytale.component.query.Query;
|
||||||
|
import com.hypixel.hytale.server.core.entity.UUIDComponent;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entity.damage.Damage;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entity.damage.DamageCause;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entity.damage.DamageEventSystem;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entity.damage.DamageModule;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
|
||||||
|
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}.
|
||||||
|
*
|
||||||
|
* <p>Registered in {@code GravityFlipPlugin.setup()} via
|
||||||
|
* {@code getEntityStoreRegistry().registerSystem(new FallDamageSuppressorSystem(guard, errorHandler))}.
|
||||||
|
*
|
||||||
|
* <p>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}.
|
||||||
|
*
|
||||||
|
* <p>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.
|
||||||
|
*/
|
||||||
|
public final class FallDamageSuppressorSystem extends DamageEventSystem {
|
||||||
|
|
||||||
|
private final FallDamageGuard guard;
|
||||||
|
private final Consumer<Throwable> errorHandler;
|
||||||
|
private volatile int fallIndexCache = -1;
|
||||||
|
|
||||||
|
public FallDamageSuppressorSystem(FallDamageGuard guard, Consumer<Throwable> errorHandler) {
|
||||||
|
this.guard = guard;
|
||||||
|
this.errorHandler = errorHandler == null ? t -> {} : errorHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public SystemGroup<EntityStore> getGroup() {
|
||||||
|
return DamageModule.get().getInspectDamageGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nonnull
|
||||||
|
public Query<EntityStore> getQuery() {
|
||||||
|
return Archetype.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(int index,
|
||||||
|
@Nonnull ArchetypeChunk<EntityStore> chunk,
|
||||||
|
@Nonnull Store<EntityStore> store,
|
||||||
|
@Nonnull CommandBuffer<EntityStore> cmdBuf,
|
||||||
|
@Nonnull Damage damage) {
|
||||||
|
try {
|
||||||
|
int fall = fallIndex();
|
||||||
|
if (fall < 0) return;
|
||||||
|
if (damage.getDamageCauseIndex() != fall) return;
|
||||||
|
|
||||||
|
UUIDComponent uc;
|
||||||
|
try {
|
||||||
|
uc = chunk.getComponent(index, UUIDComponent.getComponentType());
|
||||||
|
} catch (Throwable ignored) { return; }
|
||||||
|
if (uc == null) return;
|
||||||
|
|
||||||
|
if (guard.shouldSuppressFallDamage(uc.getUuid(), System.currentTimeMillis())) {
|
||||||
|
damage.setCancelled(true);
|
||||||
|
}
|
||||||
|
} catch (Throwable th) {
|
||||||
|
errorHandler.accept(th);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int fallIndex() {
|
||||||
|
int i = fallIndexCache;
|
||||||
|
if (i < 0) {
|
||||||
|
try {
|
||||||
|
i = DamageCause.getAssetMap().getIndex("Fall");
|
||||||
|
fallIndexCache = i;
|
||||||
|
} catch (Throwable th) {
|
||||||
|
errorHandler.accept(th);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,8 +25,6 @@ import java.util.HashSet;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,18 +33,31 @@ import java.util.function.Consumer;
|
|||||||
* {@code CommandBuffer.replaceComponent(...)} inside a {@code Store.forEachEntityParallel(...)}
|
* {@code CommandBuffer.replaceComponent(...)} inside a {@code Store.forEachEntityParallel(...)}
|
||||||
* lambda — the ECS engine commits them on the main thread after the parallel pass.
|
* lambda — the ECS engine commits them on the main thread after the parallel pass.
|
||||||
*
|
*
|
||||||
* <p>Phase 03-02: in addition to the ECS toggle (still required for items, consumed by
|
* <p>Phase 03-02: wake-ups per-entity :
|
||||||
* {@code ItemPrePhysicsSystem}), we wake up the per-entity cached movement settings:
|
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Players: {@code MovementManager.setDefaultSettings + applyDefaultSettings + update(packetHandler)}
|
* <li>Players: {@code MovementManager.setDefaultSettings + applyDefaultSettings + update(packetHandler)}</li>
|
||||||
* — sends the {@code UpdateMovementSettings} packet (ID 110) to the client.</li>
|
* <li>NPCs: {@code Role.getActiveMotionController().updatePhysicsValues(PhysicsValues)}</li>
|
||||||
* <li>NPCs: {@code Role.getActiveMotionController().updatePhysicsValues(PhysicsValues)}
|
|
||||||
* — re-applies {@code MovementManager.MASTER_DEFAULT.apply} on the cached settings.</li>
|
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <p>Dedup: a thread-safe {@link Set}{@code <UUID>} tracks entities flipped at the previous tick.
|
* <p>Phase 03-03: seed {@code addForce(0, +0.1, 0)} on NPCs each tick in-region to activate
|
||||||
* Entities that left an active region are restored to {@code invertedGravity=false} in a second
|
* {@code computeNewFallSpeed} path which honours {@code invertedGravity}.
|
||||||
* pass (trade-off documented in the plan — second O(N) scan accepted v1).
|
*
|
||||||
|
* <p>Phase 03-04:
|
||||||
|
* <ul>
|
||||||
|
* <li>Per-region tuning consumed : {@code AffectPlayers} / {@code AffectNpcs} / {@code AffectItems}
|
||||||
|
* filter BEFORE any wake / cmdBuf flip ; {@code VerticalForce} replaces the hardcoded 0.1.</li>
|
||||||
|
* <li>{@link FallDamageGuard} notified on entry (pass 1) and exit (pass 2) with the
|
||||||
|
* first-matched region to drive {@link FallDamageSuppressorSystem}.</li>
|
||||||
|
* <li>Debug throttle logs {@code [DBG tick=...]} removed. One-shot {@code [DBG npc.woken]}
|
||||||
|
* and {@code [DBG npc.ctrlNull]} preserved — useful when diagnosing new NPC role classes.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><b>Multi-region precedence :</b> 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).
|
||||||
*/
|
*/
|
||||||
public final class GravityApplier {
|
public final class GravityApplier {
|
||||||
// Lazy ComponentType holders — pattern identique à RegionRegistry.transform()
|
// Lazy ComponentType holders — pattern identique à RegionRegistry.transform()
|
||||||
@@ -132,38 +143,33 @@ public final class GravityApplier {
|
|||||||
|
|
||||||
// THREADING: écrit/lu depuis les workers ECS via forEachEntityParallel → ConcurrentHashMap.newKeySet obligatoire.
|
// THREADING: écrit/lu depuis les workers ECS via forEachEntityParallel → ConcurrentHashMap.newKeySet obligatoire.
|
||||||
private final Set<UUID> previouslyInverted = ConcurrentHashMap.newKeySet();
|
private final Set<UUID> previouslyInverted = ConcurrentHashMap.newKeySet();
|
||||||
|
/** First-matched region per UUID at the previous tick — consulted in Pass 2 for markExit. */
|
||||||
|
private final ConcurrentHashMap<UUID, GravityFlipRegion> lastKnownRegion = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private final Consumer<Throwable> errorHandler;
|
private final Consumer<Throwable> errorHandler;
|
||||||
private final Consumer<String> infoHandler;
|
private final Consumer<String> infoHandler;
|
||||||
|
private final FallDamageGuard guard;
|
||||||
|
|
||||||
// --- Debug diag (throttled counters + per-UUID one-shot) ---
|
/** One-shot per-UUID log tracking (diagnostic prod) — retained after Plan 03-04 cleanup. */
|
||||||
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();
|
private final Set<UUID> loggedNpcUuids = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
public GravityApplier(Consumer<Throwable> errorHandler) {
|
public GravityApplier(Consumer<Throwable> errorHandler) {
|
||||||
this(errorHandler, null);
|
this(errorHandler, null, new FallDamageGuard());
|
||||||
}
|
}
|
||||||
|
|
||||||
public GravityApplier(Consumer<Throwable> errorHandler, Consumer<String> infoHandler) {
|
public GravityApplier(Consumer<Throwable> errorHandler, Consumer<String> infoHandler) {
|
||||||
|
this(errorHandler, infoHandler, new FallDamageGuard());
|
||||||
|
}
|
||||||
|
|
||||||
|
public GravityApplier(Consumer<Throwable> errorHandler, Consumer<String> infoHandler, FallDamageGuard guard) {
|
||||||
this.errorHandler = errorHandler == null ? t -> {} : errorHandler;
|
this.errorHandler = errorHandler == null ? t -> {} : errorHandler;
|
||||||
this.infoHandler = infoHandler == null ? m -> {} : infoHandler;
|
this.infoHandler = infoHandler == null ? m -> {} : infoHandler;
|
||||||
|
this.guard = guard == null ? new FallDamageGuard() : guard;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construit un nouveau {@link PhysicsValues} en copiant mass/drag de la source et en fixant
|
* 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.
|
* {@code invertedGravity=target}. Pure data — pas d'effet de bord.
|
||||||
*
|
|
||||||
* <p>NOTE test : le constructeur statique de {@link PhysicsValues} dépend du runtime Hytale
|
|
||||||
* (ModuleRegistry / PluginBase) et échoue hors serveur. La décomposition pure est exposée par
|
|
||||||
* {@link #buildFlaggedDecision(double, double, boolean)} pour les tests unitaires — Rule 3
|
|
||||||
* fallback documenté dans le plan 03-02 (section Task 1 action).
|
|
||||||
*/
|
*/
|
||||||
static PhysicsValues buildPhysicsValuesWithFlag(PhysicsValues source, boolean target) {
|
static PhysicsValues buildPhysicsValuesWithFlag(PhysicsValues source, boolean target) {
|
||||||
FlaggedDecision d = buildFlaggedDecision(source.getMass(), source.getDragCoefficient(), target);
|
FlaggedDecision d = buildFlaggedDecision(source.getMass(), source.getDragCoefficient(), target);
|
||||||
@@ -188,11 +194,6 @@ public final class GravityApplier {
|
|||||||
/** Tick entry point. NO-OP si world ou snapshot est null. */
|
/** Tick entry point. NO-OP si world ou snapshot est null. */
|
||||||
public void apply(World world, RegionSnapshot snapshot) {
|
public void apply(World world, RegionSnapshot snapshot) {
|
||||||
if (world == null || snapshot == null) return;
|
if (world == null || snapshot == null) return;
|
||||||
// THREADING (fix WorldThread assert 2026-04-23) : `Store.forEachEntityParallel` exige
|
|
||||||
// d'être initié depuis la WorldThread (assertThread @Store.java:2362). Le tick loop tourne
|
|
||||||
// sur `GravityFlip-Detect` (ScheduledExecutorService) → on dispatch tout le travail ECS
|
|
||||||
// via `world.execute(Runnable)`. Le CommandBuffer reste utilisé côté mutations, mais
|
|
||||||
// ne résout PAS l'assert côté appelant (amendement D-04 du plan 03-01).
|
|
||||||
world.execute(() -> applyOnWorldThread(world, snapshot));
|
world.execute(() -> applyOnWorldThread(world, snapshot));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,34 +227,51 @@ public final class GravityApplier {
|
|||||||
com.hypixel.hytale.math.vector.Vector3d pos = t.getPosition();
|
com.hypixel.hytale.math.vector.Vector3d pos = t.getPosition();
|
||||||
double x = pos.x, y = pos.y, z = pos.z;
|
double x = pos.x, y = pos.y, z = pos.z;
|
||||||
|
|
||||||
boolean inAnyRegion = false;
|
// First-match wins for multi-region precedence (Plan 03-04).
|
||||||
|
GravityFlipRegion matchedRegion = null;
|
||||||
for (GravityFlipRegion r : enabledRegions) {
|
for (GravityFlipRegion r : enabledRegions) {
|
||||||
if (r.asBox().containsPosition(x, y, z)) { inAnyRegion = true; break; }
|
if (r.asBox().containsPosition(x, y, z)) { matchedRegion = r; break; }
|
||||||
}
|
}
|
||||||
if (!inAnyRegion) return;
|
if (matchedRegion == null) return;
|
||||||
|
|
||||||
UUID u = uc.getUuid();
|
UUID u = uc.getUuid();
|
||||||
currentlyInRegion.add(u);
|
|
||||||
entitiesInRegionTick.incrementAndGet();
|
|
||||||
|
|
||||||
// --- existant (plan 03-01) : flip ECS native ---
|
// --- Plan 03-04 : AffectXxx filters applied BEFORE wake ---
|
||||||
|
EntityKind kind = classify(chunk, index, MMT, PRT, PLT, NPCT);
|
||||||
|
boolean allowed;
|
||||||
|
switch (kind) {
|
||||||
|
case PLAYER: allowed = matchedRegion.isAffectPlayers(); break;
|
||||||
|
case NPC: allowed = matchedRegion.isAffectNpcs(); break;
|
||||||
|
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.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentlyInRegion.add(u);
|
||||||
|
lastKnownRegion.put(u, matchedRegion);
|
||||||
|
guard.markInRegion(u, matchedRegion);
|
||||||
|
|
||||||
|
// --- Flip ECS native (plan 03-01) ---
|
||||||
if (!v.isInvertedGravity()) {
|
if (!v.isInvertedGravity()) {
|
||||||
Ref<EntityStore> ref = chunk.getReferenceTo(index);
|
Ref<EntityStore> ref = chunk.getReferenceTo(index);
|
||||||
cmdBuf.replaceComponent(ref, PHYST,
|
cmdBuf.replaceComponent(ref, PHYST,
|
||||||
new PhysicsValues(v.getMass(), v.getDragCoefficient(), true));
|
new PhysicsValues(v.getMass(), v.getDragCoefficient(), true));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NOUVEAU (plan 03-02) : wake-up joueur/NPC dans le même lambda ---
|
// --- Wake-up joueur/NPC (plan 03-02) + seed VerticalForce (plan 03-03 paramétré 03-04) ---
|
||||||
wakePlayerOrNpc(chunk, index, v, true, MMT, PRT, PLT, NPCT);
|
wakePlayerOrNpc(chunk, index, v, true, matchedRegion,
|
||||||
|
MMT, PRT, PLT, NPCT);
|
||||||
});
|
});
|
||||||
|
|
||||||
// PASS 2 — restore : pour chaque UUID dans previouslyInverted \ currentlyInRegion,
|
// PASS 2 — restore : pour chaque UUID dans previouslyInverted \ currentlyInRegion.
|
||||||
// re-localiser l'entité (second scan) et queue le flip OFF via cmdBuf +
|
|
||||||
// wake-up joueur/NPC avec flag=false.
|
|
||||||
Set<UUID> toRestore = ConcurrentHashMap.newKeySet();
|
Set<UUID> toRestore = ConcurrentHashMap.newKeySet();
|
||||||
toRestore.addAll(previouslyInverted);
|
toRestore.addAll(previouslyInverted);
|
||||||
toRestore.removeAll(currentlyInRegion);
|
toRestore.removeAll(currentlyInRegion);
|
||||||
if (!toRestore.isEmpty()) {
|
if (!toRestore.isEmpty()) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
store.forEachEntityParallel(PHYST, (index, chunk, cmdBuf) -> {
|
store.forEachEntityParallel(PHYST, (index, chunk, cmdBuf) -> {
|
||||||
UUIDComponent uc;
|
UUIDComponent uc;
|
||||||
PhysicsValues v;
|
PhysicsValues v;
|
||||||
@@ -262,7 +280,8 @@ public final class GravityApplier {
|
|||||||
v = chunk.getComponent(index, PHYST);
|
v = chunk.getComponent(index, PHYST);
|
||||||
} catch (Throwable ignored) { return; }
|
} catch (Throwable ignored) { return; }
|
||||||
if (uc == null || v == null) return;
|
if (uc == null || v == null) return;
|
||||||
if (!toRestore.contains(uc.getUuid())) return;
|
UUID u = uc.getUuid();
|
||||||
|
if (!toRestore.contains(u)) return;
|
||||||
|
|
||||||
if (v.isInvertedGravity()) {
|
if (v.isInvertedGravity()) {
|
||||||
Ref<EntityStore> ref = chunk.getReferenceTo(index);
|
Ref<EntityStore> ref = chunk.getReferenceTo(index);
|
||||||
@@ -270,49 +289,63 @@ public final class GravityApplier {
|
|||||||
new PhysicsValues(v.getMass(), v.getDragCoefficient(), false));
|
new PhysicsValues(v.getMass(), v.getDragCoefficient(), false));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- NOUVEAU (plan 03-02) : wake-up joueur/NPC avec flag=false ---
|
// Wake-up avec flag=false pour restaurer les settings natifs.
|
||||||
wakePlayerOrNpc(chunk, index, v, false, MMT, PRT, PLT, NPCT);
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update tracker — ces ops sont sur le tick thread après la fin du pass parallel.
|
// Update tracker — ces ops sont sur le tick thread après la fin du pass parallel.
|
||||||
previouslyInverted.clear();
|
previouslyInverted.clear();
|
||||||
previouslyInverted.addAll(currentlyInRegion);
|
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) {
|
} catch (Throwable th) {
|
||||||
errorHandler.accept(th);
|
errorHandler.accept(th);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum EntityKind { PLAYER, NPC, OTHER }
|
||||||
|
|
||||||
|
/** Classify the entity at {@code index} into player / NPC / other (items fall into other). */
|
||||||
|
private EntityKind classify(ArchetypeChunk<EntityStore> chunk, int index,
|
||||||
|
ComponentType<EntityStore, MovementManager> MMT,
|
||||||
|
ComponentType<EntityStore, PlayerRef> PRT,
|
||||||
|
ComponentType<EntityStore, Player> PLT,
|
||||||
|
ComponentType<EntityStore, NPCEntity> NPCT) {
|
||||||
|
try {
|
||||||
|
MovementManager mm = chunk.getComponent(index, MMT);
|
||||||
|
if (mm != null) {
|
||||||
|
PlayerRef pr = null; Player pl = null;
|
||||||
|
try { pr = chunk.getComponent(index, PRT); } catch (Throwable ignored) {}
|
||||||
|
try { pl = chunk.getComponent(index, PLT); } catch (Throwable ignored) {}
|
||||||
|
if (pr != null && pl != null) return EntityKind.PLAYER;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {}
|
||||||
|
try {
|
||||||
|
NPCEntity npc = chunk.getComponent(index, NPCT);
|
||||||
|
if (npc != null) return EntityKind.NPC;
|
||||||
|
} catch (Throwable ignored) {}
|
||||||
|
return EntityKind.OTHER;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wake-up dans le MÊME lambda parallèle :
|
* Wake-up dans le MÊME lambda parallèle :
|
||||||
* - joueur (MovementManager + PlayerRef + Player) → setDefaultSettings + applyDefaultSettings + update(ph)
|
* - joueur (MovementManager + PlayerRef + Player) → setDefaultSettings + applyDefaultSettings + update(ph)
|
||||||
* - NPC (NPCEntity avec role non-null et active controller non-null) → updatePhysicsValues(targetValues)
|
* - 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)
|
* - sinon (items, autres) : no-op (le flip cmdBuf.replaceComponent du pass 1 suffit)
|
||||||
*
|
*
|
||||||
* <p>On NE relit PAS le PhysicsValues via l'ECS — le cmdBuf n'est pas encore commit. On construit
|
* <p>Plan 03-04 : le paramètre {@code matchedRegion} (non-null en entrée, null en sortie)
|
||||||
* localement le PhysicsValues cible via {@link #buildPhysicsValuesWithFlag}.
|
* fournit {@code VerticalForce} — remplace le hardcode 0.1 du plan 03-03.
|
||||||
*/
|
*/
|
||||||
private void wakePlayerOrNpc(
|
private void wakePlayerOrNpc(
|
||||||
ArchetypeChunk<EntityStore> chunk, int index,
|
ArchetypeChunk<EntityStore> chunk, int index,
|
||||||
PhysicsValues sourceValues, boolean targetFlag,
|
PhysicsValues sourceValues, boolean targetFlag,
|
||||||
|
GravityFlipRegion matchedRegion,
|
||||||
ComponentType<EntityStore, MovementManager> MMT,
|
ComponentType<EntityStore, MovementManager> MMT,
|
||||||
ComponentType<EntityStore, PlayerRef> PRT,
|
ComponentType<EntityStore, PlayerRef> PRT,
|
||||||
ComponentType<EntityStore, Player> PLT,
|
ComponentType<EntityStore, Player> PLT,
|
||||||
@@ -332,12 +365,10 @@ public final class GravityApplier {
|
|||||||
mm.applyDefaultSettings();
|
mm.applyDefaultSettings();
|
||||||
PacketHandler ph = pr.getPacketHandler();
|
PacketHandler ph = pr.getPacketHandler();
|
||||||
mm.update(ph);
|
mm.update(ph);
|
||||||
playersWokenTick.incrementAndGet();
|
|
||||||
} catch (Throwable th) {
|
} catch (Throwable th) {
|
||||||
wakeExceptionsTick.incrementAndGet();
|
|
||||||
errorHandler.accept(th);
|
errorHandler.accept(th);
|
||||||
}
|
}
|
||||||
return; // un joueur n'est pas un NPC — court-circuit
|
return; // un joueur n'est pas un NPC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,16 +376,14 @@ public final class GravityApplier {
|
|||||||
NPCEntity npc = null;
|
NPCEntity npc = null;
|
||||||
try { npc = chunk.getComponent(index, NPCT); } catch (Throwable ignored) {}
|
try { npc = chunk.getComponent(index, NPCT); } catch (Throwable ignored) {}
|
||||||
if (npc != null) {
|
if (npc != null) {
|
||||||
npcsSeenTick.incrementAndGet();
|
|
||||||
try {
|
try {
|
||||||
Role role = npc.getRole();
|
Role role = npc.getRole();
|
||||||
if (role == null) {
|
if (role == null) {
|
||||||
npcsRoleNullTick.incrementAndGet();
|
// rôle non résolu — rien à faire, no-op
|
||||||
} else {
|
} else {
|
||||||
MotionController active = role.getActiveMotionController();
|
MotionController active = role.getActiveMotionController();
|
||||||
if (active == null) {
|
if (active == null) {
|
||||||
npcsControllerNullTick.incrementAndGet();
|
// log one-shot : rôle non-null mais controller null (utile diag prod)
|
||||||
// log one-shot : rôle non-null mais controller null
|
|
||||||
UUIDComponent uc = null;
|
UUIDComponent uc = null;
|
||||||
try { uc = chunk.getComponent(index, uuidType()); } catch (Throwable ignored) {}
|
try { uc = chunk.getComponent(index, uuidType()); } catch (Throwable ignored) {}
|
||||||
if (uc != null && loggedNpcUuids.add(uc.getUuid())) {
|
if (uc != null && loggedNpcUuids.add(uc.getUuid())) {
|
||||||
@@ -364,29 +393,22 @@ public final class GravityApplier {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
active.updatePhysicsValues(targetValues);
|
active.updatePhysicsValues(targetValues);
|
||||||
npcsWokenTick.incrementAndGet();
|
|
||||||
|
|
||||||
// --- NOUVEAU (plan 03-03) : seed forceVelocity.y pour activer le path
|
// Plan 03-04 : seed forceVelocity.y paramétré par VerticalForce (remplace
|
||||||
// MotionControllerWalk ligne ~881 qui appelle computeNewFallSpeed —
|
// le hardcode 0.1 de Plan 03-03). Uniquement en entrée (targetFlag=true) —
|
||||||
// seule fonction qui HONORE movementSettings.invertedGravity pour NPCs.
|
// à la sortie le damping natif zéroe forceVelocity.
|
||||||
// Le path normal WALKING/DESCENDING utilise this.gravity (non-inverse,
|
if (targetFlag && matchedRegion != null) {
|
||||||
// clampé ≥0), d'où l'absence d'effet du flip natif sur les moutons.
|
double vf = matchedRegion.getVerticalForce();
|
||||||
// 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 {
|
try {
|
||||||
active.addForce(
|
active.addForce(
|
||||||
new com.hypixel.hytale.math.vector.Vector3d(0, 0.1, 0),
|
new com.hypixel.hytale.math.vector.Vector3d(0, vf, 0),
|
||||||
null);
|
null);
|
||||||
} catch (Throwable th) {
|
} catch (Throwable th) {
|
||||||
wakeExceptionsTick.incrementAndGet();
|
|
||||||
errorHandler.accept(th);
|
errorHandler.accept(th);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// log one-shot : première fois qu'on voit cet UUID NPC — on dump le controller class
|
// log one-shot : première fois qu'on voit cet UUID NPC
|
||||||
UUIDComponent uc = null;
|
UUIDComponent uc = null;
|
||||||
try { uc = chunk.getComponent(index, uuidType()); } catch (Throwable ignored) {}
|
try { uc = chunk.getComponent(index, uuidType()); } catch (Throwable ignored) {}
|
||||||
if (uc != null && loggedNpcUuids.add(uc.getUuid())) {
|
if (uc != null && loggedNpcUuids.add(uc.getUuid())) {
|
||||||
@@ -397,14 +419,12 @@ public final class GravityApplier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Throwable th) {
|
} catch (Throwable th) {
|
||||||
wakeExceptionsTick.incrementAndGet();
|
|
||||||
errorHandler.accept(th);
|
errorHandler.accept(th);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// sinon : item / autre — pas de wake-up, le cmdBuf.replaceComponent du pass 1 suffit
|
// sinon : item / autre — no-op
|
||||||
otherEntitiesTick.incrementAndGet();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pure data-diff utilitaire pour tests unitaires (pas de runtime Hytale). */
|
/** Pure data-diff utilitaire pour tests unitaires (pas de runtime Hytale). */
|
||||||
|
|||||||
Reference in New Issue
Block a user