feat(03-04): ajoute FallDamageGuard tracker thread-safe (Task 2)
- ConcurrentHashMap<UUID, region> current + exit + regionAtExit - shouldSuppressFallDamage: in-region FallDamage=false OU grace window - Pas de static mutable, injecté via constructeur - 7 tests pure-data couvrant entry/in-region/exit/grace/re-entry/override
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
package com.mythlane.gravityflip.physics;
|
||||
|
||||
import com.mythlane.gravityflip.region.GravityFlipRegion;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Thread-safe tracker that decides whether fall damage should be suppressed for a given UUID
|
||||
* based on (a) current in-region membership, (b) post-exit grace window.
|
||||
*
|
||||
* <p>Pure-data service : no Hytale runtime dependency (no ECS, no PhysicsValues), injected
|
||||
* via constructor into {@link GravityApplier} (populator) and
|
||||
* {@link FallDamageSuppressorSystem} (consumer) — no static mutable state.
|
||||
*
|
||||
* <p><b>Precedence rule (multi-region) :</b> when {@link #markInRegion} is called, the LAST
|
||||
* call for a given UUID within a single tick wins. {@link GravityApplier} takes care of
|
||||
* passing the FIRST matched region (iteration order of {@code enabledRegions}), so for an
|
||||
* entity simultaneously in N regions the first-match region's {@code FallDamage}
|
||||
* and {@code GracePeriodMs} drive suppression.
|
||||
*
|
||||
* <p><b>State machine :</b>
|
||||
* <ul>
|
||||
* <li>{@code markInRegion(uuid, region)} → currentRegion[uuid]=region ; grace entries cleared.</li>
|
||||
* <li>{@code markExit(uuid, region, nowMs)} → currentRegion[uuid] removed ; grace entry stored
|
||||
* (region + timestamp).</li>
|
||||
* <li>{@code shouldSuppressFallDamage(uuid, nowMs)} :
|
||||
* <ol>
|
||||
* <li>If current region != null AND current region {@code FallDamage==false} → true.</li>
|
||||
* <li>Else if grace entry present AND grace region {@code FallDamage==false} AND
|
||||
* {@code nowMs - exitMs <= gracePeriodMs} → true.</li>
|
||||
* <li>Else → false.</li>
|
||||
* </ol>
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class FallDamageGuard {
|
||||
|
||||
/** Entity currently inside a region (cleared on exit). */
|
||||
private final ConcurrentHashMap<UUID, GravityFlipRegion> currentRegionByUuid =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
/** Timestamp (ms) at which the entity last exited a region. */
|
||||
private final ConcurrentHashMap<UUID, Long> exitTimestampByUuid =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
/** Region referenced at the moment of exit (used to read FallDamage + GracePeriodMs). */
|
||||
private final ConcurrentHashMap<UUID, GravityFlipRegion> regionAtExitByUuid =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
public void markInRegion(UUID uuid, GravityFlipRegion region) {
|
||||
if (uuid == null || region == null) return;
|
||||
currentRegionByUuid.put(uuid, region);
|
||||
// Re-entry ⇒ discard any stale grace entry (grace is a post-exit concept).
|
||||
exitTimestampByUuid.remove(uuid);
|
||||
regionAtExitByUuid.remove(uuid);
|
||||
}
|
||||
|
||||
public void markExit(UUID uuid, GravityFlipRegion region, long nowMs) {
|
||||
if (uuid == null || region == null) return;
|
||||
currentRegionByUuid.remove(uuid);
|
||||
exitTimestampByUuid.put(uuid, nowMs);
|
||||
regionAtExitByUuid.put(uuid, region);
|
||||
}
|
||||
|
||||
public boolean shouldSuppressFallDamage(UUID uuid, long nowMs) {
|
||||
if (uuid == null) return false;
|
||||
|
||||
GravityFlipRegion current = currentRegionByUuid.get(uuid);
|
||||
if (current != null) {
|
||||
return !current.isFallDamage();
|
||||
}
|
||||
|
||||
Long exitMs = exitTimestampByUuid.get(uuid);
|
||||
GravityFlipRegion exitRegion = regionAtExitByUuid.get(uuid);
|
||||
if (exitMs == null || exitRegion == null) return false;
|
||||
if (exitRegion.isFallDamage()) return false; // region allowed fall damage → no grace
|
||||
return (nowMs - exitMs) <= exitRegion.getGracePeriodMs();
|
||||
}
|
||||
|
||||
// ---- Test hooks / diagnostics (package-private) ----
|
||||
|
||||
int trackedInRegionCount() { return currentRegionByUuid.size(); }
|
||||
int trackedGraceCount() { return exitTimestampByUuid.size(); }
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.mythlane.gravityflip.physics;
|
||||
|
||||
import com.hypixel.hytale.math.shape.Box;
|
||||
import com.hypixel.hytale.math.vector.Vector3d;
|
||||
import com.mythlane.gravityflip.region.GravityFlipRegion;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Pure-data tests for {@link FallDamageGuard} — no Hytale runtime dependency.
|
||||
* Covers entry / in-region / exit / grace-window / re-entry / FallDamage=true override.
|
||||
*/
|
||||
class FallDamageGuardTest {
|
||||
|
||||
@Test
|
||||
void notInRegionReturnsFalse() {
|
||||
FallDamageGuard guard = new FallDamageGuard();
|
||||
assertFalse(guard.shouldSuppressFallDamage(UUID.randomUUID(), 1000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void inRegionFallDamageTrueReturnsFalse() {
|
||||
FallDamageGuard guard = new FallDamageGuard();
|
||||
UUID uuid = UUID.randomUUID();
|
||||
GravityFlipRegion region = region(true, 2500);
|
||||
guard.markInRegion(uuid, region);
|
||||
assertFalse(guard.shouldSuppressFallDamage(uuid, 1000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void inRegionFallDamageFalseReturnsTrue() {
|
||||
FallDamageGuard guard = new FallDamageGuard();
|
||||
UUID uuid = UUID.randomUUID();
|
||||
GravityFlipRegion region = region(false, 2500);
|
||||
guard.markInRegion(uuid, region);
|
||||
assertTrue(guard.shouldSuppressFallDamage(uuid, 1000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void afterExitWithinGraceReturnsTrue() {
|
||||
FallDamageGuard guard = new FallDamageGuard();
|
||||
UUID uuid = UUID.randomUUID();
|
||||
GravityFlipRegion region = region(false, 2500);
|
||||
guard.markInRegion(uuid, region);
|
||||
long t0 = 1000L;
|
||||
guard.markExit(uuid, region, t0);
|
||||
assertTrue(guard.shouldSuppressFallDamage(uuid, t0 + 2499));
|
||||
}
|
||||
|
||||
@Test
|
||||
void afterExitBeyondGraceReturnsFalse() {
|
||||
FallDamageGuard guard = new FallDamageGuard();
|
||||
UUID uuid = UUID.randomUUID();
|
||||
GravityFlipRegion region = region(false, 2500);
|
||||
guard.markInRegion(uuid, region);
|
||||
long t0 = 1000L;
|
||||
guard.markExit(uuid, region, t0);
|
||||
assertFalse(guard.shouldSuppressFallDamage(uuid, t0 + 2501));
|
||||
}
|
||||
|
||||
@Test
|
||||
void reEntryAfterExitResetsTracking() {
|
||||
FallDamageGuard guard = new FallDamageGuard();
|
||||
UUID uuid = UUID.randomUUID();
|
||||
GravityFlipRegion region = region(false, 2500);
|
||||
guard.markInRegion(uuid, region);
|
||||
guard.markExit(uuid, region, 1000L);
|
||||
guard.markInRegion(uuid, region); // re-enter
|
||||
// In-region again with FallDamage=false → immediate suppression, grace reset.
|
||||
assertTrue(guard.shouldSuppressFallDamage(uuid, 1500L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallDamageTrueRegionNeverSuppressesEvenDuringGrace() {
|
||||
FallDamageGuard guard = new FallDamageGuard();
|
||||
UUID uuid = UUID.randomUUID();
|
||||
GravityFlipRegion suppressed = region(false, 2500);
|
||||
GravityFlipRegion allowed = region(true, 2500);
|
||||
guard.markInRegion(uuid, suppressed);
|
||||
guard.markExit(uuid, suppressed, 1000L);
|
||||
// New region has FallDamage=true → override immediately.
|
||||
guard.markInRegion(uuid, allowed);
|
||||
assertFalse(guard.shouldSuppressFallDamage(uuid, 1500L));
|
||||
}
|
||||
|
||||
private static GravityFlipRegion region(boolean fallDamage, int graceMs) {
|
||||
GravityFlipRegion r = new GravityFlipRegion(
|
||||
"t", new Box(new Vector3d(0, 0, 0), new Vector3d(1, 1, 1)), true);
|
||||
r.setFallDamage(fallDamage);
|
||||
r.setGracePeriodMs(graceMs);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user