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:
@@ -63,8 +63,9 @@ public class GravityFlipPlugin extends JavaPlugin {
|
|||||||
super.start();
|
super.start();
|
||||||
GravityFlipConfig cfg = configHolder.get();
|
GravityFlipConfig cfg = configHolder.get();
|
||||||
this.registry = new RegionRegistry(cfg, configHolder);
|
this.registry = new RegionRegistry(cfg, configHolder);
|
||||||
this.gravityApplier = new GravityApplier(th ->
|
this.gravityApplier = new GravityApplier(
|
||||||
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));
|
||||||
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"));
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ 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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,9 +133,27 @@ 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();
|
||||||
private final Consumer<Throwable> errorHandler;
|
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) {
|
public GravityApplier(Consumer<Throwable> errorHandler) {
|
||||||
|
this(errorHandler, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GravityApplier(Consumer<Throwable> errorHandler, Consumer<String> infoHandler) {
|
||||||
this.errorHandler = errorHandler == null ? t -> {} : errorHandler;
|
this.errorHandler = errorHandler == null ? t -> {} : errorHandler;
|
||||||
|
this.infoHandler = infoHandler == null ? m -> {} : infoHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -214,6 +234,7 @@ public final class GravityApplier {
|
|||||||
|
|
||||||
UUID u = uc.getUuid();
|
UUID u = uc.getUuid();
|
||||||
currentlyInRegion.add(u);
|
currentlyInRegion.add(u);
|
||||||
|
entitiesInRegionTick.incrementAndGet();
|
||||||
|
|
||||||
// --- existant (plan 03-01) : flip ECS native ---
|
// --- existant (plan 03-01) : flip ECS native ---
|
||||||
if (!v.isInvertedGravity()) {
|
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.
|
// 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);
|
||||||
}
|
}
|
||||||
@@ -293,7 +332,9 @@ 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 — court-circuit
|
||||||
@@ -304,19 +345,66 @@ 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();
|
||||||
|
} 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
|
||||||
|
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);
|
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) {
|
} 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
|
||||||
|
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
|
// 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). */
|
/** Pure data-diff utilitaire pour tests unitaires (pas de runtime Hytale). */
|
||||||
|
|||||||
@@ -10,14 +10,32 @@ import com.hypixel.hytale.math.vector.Vector3d;
|
|||||||
/**
|
/**
|
||||||
* A named axis-aligned region in which gravity is inverted for any entity inside.
|
* A named axis-aligned region in which gravity is inverted for any entity inside.
|
||||||
*
|
*
|
||||||
* <p>Persisted as part of {@code GravityFlipConfig} via {@link #CODEC}. The region
|
* <p>Persisted as part of {@code GravityFlipConfig} via {@link #CODEC}. The region is stored
|
||||||
* is stored on disk as three keys:
|
* on disk as up to 9 keys :
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>{@code Name} — non-null string identifier</li>
|
* <li>{@code Name} — non-null string identifier</li>
|
||||||
* <li>{@code Box} — non-null AABB (composed of two {@code Vector3d} corners)</li>
|
* <li>{@code Box} — non-null AABB (composed of two {@code Vector3d} corners)</li>
|
||||||
* <li>{@code Enabled} — boolean toggle, default {@code true}</li>
|
* <li>{@code Enabled} — boolean toggle, default {@code true}</li>
|
||||||
|
* <li>{@code FallDamage} — <b>optional</b> (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}).</li>
|
||||||
|
* <li>{@code GracePeriodMs} — <b>optional</b>, default {@code 2500}. Grace window in ms
|
||||||
|
* after exit during which fall damage remains suppressed.</li>
|
||||||
|
* <li>{@code VerticalForce} — <b>optional</b>, default {@code 0.1}. Seed added each tick to
|
||||||
|
* NPC {@code forceVelocity.y} to activate the inverted gravity path (Plan 03-03).</li>
|
||||||
|
* <li>{@code AffectPlayers} — <b>optional</b>, default {@code true}. If {@code false},
|
||||||
|
* players in-region are NOT flipped.</li>
|
||||||
|
* <li>{@code AffectNpcs} — <b>optional</b>, default {@code true}. If {@code false},
|
||||||
|
* NPCs in-region are NOT flipped / seeded.</li>
|
||||||
|
* <li>{@code AffectItems} — <b>optional</b>, default {@code true}. If {@code false},
|
||||||
|
* item {@code PhysicsValues.invertedGravity} is NOT mutated.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
|
* <p><b>Back-compat :</b> 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).
|
||||||
|
*
|
||||||
* <p>Note: this build pins {@code com.hypixel.hytale:Server:2026.03.26-89796e57b}, in which
|
* <p>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}
|
* {@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).
|
* (Hytale's own type — NOT {@code org.joml.Vector3d}, which only appeared in later builds).
|
||||||
@@ -38,6 +56,19 @@ public final class GravityFlipRegion {
|
|||||||
.addValidator(Validators.nonNull()).add()
|
.addValidator(Validators.nonNull()).add()
|
||||||
.append(new KeyedCodec<>("Enabled", Codec.BOOLEAN),
|
.append(new KeyedCodec<>("Enabled", Codec.BOOLEAN),
|
||||||
(r, v) -> r.enabled = v, r -> r.enabled).add()
|
(r, v) -> r.enabled = v, r -> r.enabled).add()
|
||||||
|
// --- Plan 03-04 : 6 optional tuning fields (no nonNull validator ⇒ optional) ---
|
||||||
|
.append(new KeyedCodec<>("FallDamage", Codec.BOOLEAN),
|
||||||
|
(r, v) -> r.fallDamage = v, r -> r.fallDamage).add()
|
||||||
|
.append(new KeyedCodec<>("GracePeriodMs", Codec.INTEGER),
|
||||||
|
(r, v) -> r.gracePeriodMs = v, r -> r.gracePeriodMs).add()
|
||||||
|
.append(new KeyedCodec<>("VerticalForce", Codec.DOUBLE),
|
||||||
|
(r, v) -> r.verticalForce = v, r -> r.verticalForce).add()
|
||||||
|
.append(new KeyedCodec<>("AffectPlayers", Codec.BOOLEAN),
|
||||||
|
(r, v) -> r.affectPlayers = v, r -> r.affectPlayers).add()
|
||||||
|
.append(new KeyedCodec<>("AffectNpcs", Codec.BOOLEAN),
|
||||||
|
(r, v) -> r.affectNpcs = v, r -> r.affectNpcs).add()
|
||||||
|
.append(new KeyedCodec<>("AffectItems", Codec.BOOLEAN),
|
||||||
|
(r, v) -> r.affectItems = v, r -> r.affectItems).add()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Package-private mutable fields written directly by the codec setters.
|
// Package-private mutable fields written directly by the codec setters.
|
||||||
@@ -45,6 +76,14 @@ public final class GravityFlipRegion {
|
|||||||
Box box = new Box(new Vector3d(), new Vector3d());
|
Box box = new Box(new Vector3d(), new Vector3d());
|
||||||
boolean enabled = true;
|
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;
|
||||||
|
boolean affectPlayers = true;
|
||||||
|
boolean affectNpcs = true;
|
||||||
|
boolean affectItems = true;
|
||||||
|
|
||||||
public GravityFlipRegion() {}
|
public GravityFlipRegion() {}
|
||||||
|
|
||||||
public GravityFlipRegion(String name, Box box, boolean enabled) {
|
public GravityFlipRegion(String name, Box box, boolean enabled) {
|
||||||
@@ -63,6 +102,22 @@ public final class GravityFlipRegion {
|
|||||||
public void setBox(Box b) { this.box = b; }
|
public void setBox(Box b) { this.box = b; }
|
||||||
public void setEnabled(boolean v) { this.enabled = v; }
|
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; }
|
||||||
|
public boolean isAffectPlayers() { return affectPlayers; }
|
||||||
|
public boolean isAffectNpcs() { return affectNpcs; }
|
||||||
|
public boolean isAffectItems() { return affectItems; }
|
||||||
|
|
||||||
|
public void setFallDamage(boolean v) { this.fallDamage = v; }
|
||||||
|
public void setGracePeriodMs(int v) { this.gracePeriodMs = v; }
|
||||||
|
public void setVerticalForce(double v) { this.verticalForce = v; }
|
||||||
|
public void setAffectPlayers(boolean v) { this.affectPlayers = v; }
|
||||||
|
public void setAffectNpcs(boolean v) { this.affectNpcs = v; }
|
||||||
|
public void setAffectItems(boolean v) { this.affectItems = v; }
|
||||||
|
|
||||||
/** Convenience accessor for tick-loop / physics consumers in Phase 02-02. */
|
/** Convenience accessor for tick-loop / physics consumers in Phase 02-02. */
|
||||||
public Box asBox() { return box; }
|
public Box asBox() { return box; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Round-trip tests for {@link GravityFlipRegion#CODEC}. Verifies the codec preserves
|
* Round-trip tests for {@link GravityFlipRegion#CODEC}. Verifies the codec preserves
|
||||||
* Name + Box + Enabled fields across encode -> decode cycles via the BSON intermediate
|
* the legacy Name + Box + Enabled fields across encode -> decode cycles via the BSON
|
||||||
* representation that all Hytale codecs share.
|
* intermediate representation, and (Plan 03-04) the 6 optional tuning fields :
|
||||||
|
* FallDamage, GracePeriodMs, VerticalForce, AffectPlayers, AffectNpcs, AffectItems.
|
||||||
|
*
|
||||||
|
* <p>Back-compat invariant (test {@link #roundTripPreservesDefaultsWhenNewFieldsAbsent}) :
|
||||||
|
* a BSON encoded without the 6 new keys must decode with all Java defaults preserved.
|
||||||
*/
|
*/
|
||||||
class GravityFlipRegionCodecTest {
|
class GravityFlipRegionCodecTest {
|
||||||
|
|
||||||
@@ -64,6 +68,89 @@ class GravityFlipRegionCodecTest {
|
|||||||
assertEquals("", decoded.getName(), "empty name must survive round-trip without substitution");
|
assertEquals("", decoded.getName(), "empty name must survive round-trip without substitution");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Plan 03-04 : 6 nouveaux champs optionnels ----------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void roundTripPreservesDefaultsWhenNewFieldsAbsent() {
|
||||||
|
// Region construite via constructeur legacy 3-arg (comme un regions.json legacy).
|
||||||
|
GravityFlipRegion src = new GravityFlipRegion(
|
||||||
|
"legacy",
|
||||||
|
new Box(new Vector3d(0, 0, 0), new Vector3d(1, 1, 1)),
|
||||||
|
true);
|
||||||
|
|
||||||
|
GravityFlipRegion decoded = roundTrip(src);
|
||||||
|
|
||||||
|
// Tous les 6 nouveaux champs doivent exposer leurs defaults Java.
|
||||||
|
assertFalse(decoded.isFallDamage(), "default FallDamage=false");
|
||||||
|
assertEquals(2500, decoded.getGracePeriodMs(), "default GracePeriodMs=2500");
|
||||||
|
assertEquals(0.1, decoded.getVerticalForce(), 1e-9, "default VerticalForce=0.1");
|
||||||
|
assertTrue(decoded.isAffectPlayers(), "default AffectPlayers=true");
|
||||||
|
assertTrue(decoded.isAffectNpcs(), "default AffectNpcs=true");
|
||||||
|
assertTrue(decoded.isAffectItems(), "default AffectItems=true");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void roundTripFallDamageTrue() {
|
||||||
|
GravityFlipRegion src = baseRegion();
|
||||||
|
src.setFallDamage(true);
|
||||||
|
GravityFlipRegion decoded = roundTrip(src);
|
||||||
|
assertTrue(decoded.isFallDamage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void roundTripGracePeriodCustom() {
|
||||||
|
GravityFlipRegion src = baseRegion();
|
||||||
|
src.setGracePeriodMs(5000);
|
||||||
|
GravityFlipRegion decoded = roundTrip(src);
|
||||||
|
assertEquals(5000, decoded.getGracePeriodMs());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void roundTripVerticalForceCustom() {
|
||||||
|
GravityFlipRegion src = baseRegion();
|
||||||
|
src.setVerticalForce(0.5);
|
||||||
|
GravityFlipRegion decoded = roundTrip(src);
|
||||||
|
assertEquals(0.5, decoded.getVerticalForce(), 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void roundTripAffectPlayersFalse() {
|
||||||
|
GravityFlipRegion src = baseRegion();
|
||||||
|
src.setAffectPlayers(false);
|
||||||
|
GravityFlipRegion decoded = roundTrip(src);
|
||||||
|
assertFalse(decoded.isAffectPlayers());
|
||||||
|
// Les autres filtres restent à true (non-clobber).
|
||||||
|
assertTrue(decoded.isAffectNpcs());
|
||||||
|
assertTrue(decoded.isAffectItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void roundTripAllNewFieldsCustom() {
|
||||||
|
GravityFlipRegion src = baseRegion();
|
||||||
|
src.setFallDamage(true);
|
||||||
|
src.setGracePeriodMs(1234);
|
||||||
|
src.setVerticalForce(0.75);
|
||||||
|
src.setAffectPlayers(false);
|
||||||
|
src.setAffectNpcs(false);
|
||||||
|
src.setAffectItems(false);
|
||||||
|
|
||||||
|
GravityFlipRegion decoded = roundTrip(src);
|
||||||
|
|
||||||
|
assertTrue(decoded.isFallDamage());
|
||||||
|
assertEquals(1234, decoded.getGracePeriodMs());
|
||||||
|
assertEquals(0.75, decoded.getVerticalForce(), 1e-9);
|
||||||
|
assertFalse(decoded.isAffectPlayers());
|
||||||
|
assertFalse(decoded.isAffectNpcs());
|
||||||
|
assertFalse(decoded.isAffectItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GravityFlipRegion baseRegion() {
|
||||||
|
return new GravityFlipRegion(
|
||||||
|
"r",
|
||||||
|
new Box(new Vector3d(0, 0, 0), new Vector3d(1, 1, 1)),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
private static GravityFlipRegion roundTrip(GravityFlipRegion src) {
|
private static GravityFlipRegion roundTrip(GravityFlipRegion src) {
|
||||||
ExtraInfo info = new ExtraInfo();
|
ExtraInfo info = new ExtraInfo();
|
||||||
BsonValue encoded = GravityFlipRegion.CODEC.encode(src, info);
|
BsonValue encoded = GravityFlipRegion.CODEC.encode(src, info);
|
||||||
|
|||||||
Reference in New Issue
Block a user