refactor(03-06): remove debug logs + efficiency pass
Log cleanup: - Drop redundant INFO "Gravity Flip enabled" in setup() (substantive version at start() remains as the single startup line). - Drop INFO "debug enter/exit notifier ENABLED" (the notifier itself emits per region transition; extra startup noise not needed). - Remove dead back-compat GravityApplier ctors that accepted an infoHandler Consumer (no callers post-03-06; debug logs already stripped). - Clean stale javadoc referencing removed [DBG npc.woken]/[DBG npc.ctrlNull] one-shot logs. - Also ship the pre-staged cleanup work: RegionEnterNotifier gated behind GRAVITYFLIP_DEBUG_NOTIFY env var, GravityApplier infoHandler field removed, tick logs stripped. Efficiency: - RegionVisualizer.resolveParticleId now memoises the requested->resolved mapping in a ConcurrentHashMap, eliminating the ParticleSystem.getAssetMap() lookup on every tick emission per region. Warn-once semantics preserved via warnedInvalidIds. Fail-open path (AssetMap unavailable in tests) intentionally does not populate the cache. - Document in GravityApplier javadoc why Pass 1 + Pass 2 cannot be fused: restore requires the complete currentlyInRegion set before diffing against previouslyInverted. Considered but not applied: - ParticleEdgeEmitter.edgePoints caching per (box, density): throttled to >=100ms refresh and typical <20 regions => alloc pressure negligible; premature without measurement. - Reusing RegionRegistry snapshot in RegionEnterNotifier: notifier is opt-in/off-by-default, so its independent ECS scan has zero prod cost. Tests: ./gradlew clean build green, no test changes required.
This commit is contained in:
@@ -7,6 +7,7 @@ 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.command.DumpParticlesCommand;
|
import com.mythlane.gravityflip.command.DumpParticlesCommand;
|
||||||
import com.mythlane.gravityflip.config.GravityFlipConfig;
|
import com.mythlane.gravityflip.config.GravityFlipConfig;
|
||||||
|
import com.mythlane.gravityflip.debug.RegionEnterNotifier;
|
||||||
import com.mythlane.gravityflip.physics.FallDamageGuard;
|
import com.mythlane.gravityflip.physics.FallDamageGuard;
|
||||||
import com.mythlane.gravityflip.physics.FallDamageSuppressorSystem;
|
import com.mythlane.gravityflip.physics.FallDamageSuppressorSystem;
|
||||||
import com.mythlane.gravityflip.physics.GravityApplier;
|
import com.mythlane.gravityflip.physics.GravityApplier;
|
||||||
@@ -67,8 +68,6 @@ public class GravityFlipPlugin extends JavaPlugin {
|
|||||||
getEntityStoreRegistry().registerSystem(new FallDamageSuppressorSystem(
|
getEntityStoreRegistry().registerSystem(new FallDamageSuppressorSystem(
|
||||||
fallDamageGuard,
|
fallDamageGuard,
|
||||||
th -> getLogger().at(Level.WARNING).withCause(th).log("fallDamageSuppressor handle failed")));
|
th -> getLogger().at(Level.WARNING).withCause(th).log("fallDamageSuppressor handle failed")));
|
||||||
|
|
||||||
getLogger().at(Level.INFO).log("Gravity Flip enabled");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -78,13 +77,21 @@ 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),
|
|
||||||
fallDamageGuard);
|
fallDamageGuard);
|
||||||
this.regionVisualizer = new RegionVisualizer(
|
this.regionVisualizer = new RegionVisualizer(
|
||||||
th -> getLogger().at(Level.WARNING).withCause(th).log("regionVisualize failed"));
|
th -> getLogger().at(Level.WARNING).withCause(th).log("regionVisualize failed"));
|
||||||
this.tickLoop = new RegionTickLoop(registry, gravityApplier, regionVisualizer, th ->
|
this.tickLoop = new RegionTickLoop(registry, gravityApplier, regionVisualizer, th ->
|
||||||
getLogger().at(Level.WARNING).withCause(th).log("detectTick failed"));
|
getLogger().at(Level.WARNING).withCause(th).log("detectTick failed"));
|
||||||
|
|
||||||
|
// Debug enter/exit notifier — opt-in via env var GRAVITYFLIP_DEBUG_NOTIFY (or sysprop
|
||||||
|
// gravityflip.debugNotify). Emits chat + INFO log on every region transition. Off by
|
||||||
|
// default to keep prod chat/logs clean.
|
||||||
|
if (isDebugNotifyEnabled()) {
|
||||||
|
this.tickLoop.setEnterNotifier(new RegionEnterNotifier(registry,
|
||||||
|
th -> getLogger().at(Level.WARNING).withCause(th).log("regionEnterNotify failed"),
|
||||||
|
msg -> getLogger().at(Level.INFO).log("%s", msg)));
|
||||||
|
}
|
||||||
|
|
||||||
// Lazy world resolution — see setup() comment.
|
// Lazy world resolution — see setup() comment.
|
||||||
this.tickLoop.startWithDelay(2_000L, () -> {
|
this.tickLoop.startWithDelay(2_000L, () -> {
|
||||||
Universe u = Universe.get();
|
Universe u = Universe.get();
|
||||||
@@ -122,6 +129,21 @@ public class GravityFlipPlugin extends JavaPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Env var {@code GRAVITYFLIP_DEBUG_NOTIFY=1} or sysprop {@code -Dgravityflip.debugNotify=true}
|
||||||
|
* enables the chat + log enter/exit notifier. Off by default — debug tool only.
|
||||||
|
*/
|
||||||
|
private static boolean isDebugNotifyEnabled() {
|
||||||
|
return truthy(System.getenv("GRAVITYFLIP_DEBUG_NOTIFY"))
|
||||||
|
|| truthy(System.getProperty("gravityflip.debugNotify"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean truthy(String v) {
|
||||||
|
if (v == null) return false;
|
||||||
|
String s = v.trim().toLowerCase();
|
||||||
|
return s.equals("1") || s.equals("true") || s.equals("yes") || s.equals("on");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void shutdown() {
|
protected void shutdown() {
|
||||||
// Plan 03-05 : clear des debug shapes cote clients AVANT tickLoop.stop().
|
// Plan 03-05 : clear des debug shapes cote clients AVANT tickLoop.stop().
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package com.mythlane.gravityflip.debug;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.component.ComponentType;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.math.shape.Box;
|
||||||
|
import com.hypixel.hytale.protocol.FormattedMessage;
|
||||||
|
import com.hypixel.hytale.protocol.packets.interface_.ChatType;
|
||||||
|
import com.hypixel.hytale.protocol.packets.interface_.ServerMessage;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import com.mythlane.gravityflip.region.GravityFlipRegion;
|
||||||
|
import com.mythlane.gravityflip.region.RegionRegistry;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug: envoie un message chat au joueur à l'entrée/sortie d'une région gravity-flip.
|
||||||
|
* Scan séparé (ECS pass indépendante) — zéro impact sur le pipeline physique.
|
||||||
|
*/
|
||||||
|
public final class RegionEnterNotifier {
|
||||||
|
|
||||||
|
private final RegionRegistry registry;
|
||||||
|
private final Consumer<Throwable> errorHandler;
|
||||||
|
private final Consumer<String> logHandler;
|
||||||
|
private final Map<UUID, Set<String>> lastRegions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public RegionEnterNotifier(RegionRegistry registry, Consumer<Throwable> errorHandler) {
|
||||||
|
this(registry, errorHandler, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RegionEnterNotifier(RegionRegistry registry, Consumer<Throwable> errorHandler,
|
||||||
|
Consumer<String> logHandler) {
|
||||||
|
this.registry = registry;
|
||||||
|
this.errorHandler = errorHandler == null ? t -> {} : errorHandler;
|
||||||
|
this.logHandler = logHandler == null ? s -> {} : logHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick(World world) {
|
||||||
|
if (world == null) return;
|
||||||
|
try {
|
||||||
|
world.execute(() -> {
|
||||||
|
try { doTick(world); } catch (Throwable th) { errorHandler.accept(th); }
|
||||||
|
});
|
||||||
|
} catch (Throwable th) {
|
||||||
|
errorHandler.accept(th);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doTick(World world) {
|
||||||
|
List<GravityFlipRegion> enabled = new ArrayList<>();
|
||||||
|
for (GravityFlipRegion r : registry.all()) if (r.isEnabled()) enabled.add(r);
|
||||||
|
if (enabled.isEmpty()) { lastRegions.clear(); return; }
|
||||||
|
|
||||||
|
Store<EntityStore> store = world.getEntityStore().getStore();
|
||||||
|
ComponentType<EntityStore, TransformComponent> TRANSFORM = TransformComponent.getComponentType();
|
||||||
|
ComponentType<EntityStore, PlayerRef> PLAYER = PlayerRef.getComponentType();
|
||||||
|
|
||||||
|
Map<UUID, Set<String>> current = new ConcurrentHashMap<>();
|
||||||
|
Map<UUID, PlayerRef> refs = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
store.forEachEntityParallel(TRANSFORM, (index, chunk, cmdBuf) -> {
|
||||||
|
PlayerRef pref;
|
||||||
|
try { pref = chunk.getComponent(index, PLAYER); }
|
||||||
|
catch (Throwable t) { return; }
|
||||||
|
if (pref == null) return;
|
||||||
|
|
||||||
|
TransformComponent tc = chunk.getComponent(index, TRANSFORM);
|
||||||
|
com.hypixel.hytale.math.vector.Vector3d pos = tc.getPosition();
|
||||||
|
double x = pos.x, y = pos.y, z = pos.z;
|
||||||
|
|
||||||
|
UUID uuid = pref.getUuid();
|
||||||
|
refs.putIfAbsent(uuid, pref);
|
||||||
|
Set<String> hit = current.computeIfAbsent(uuid, k -> ConcurrentHashMap.newKeySet());
|
||||||
|
for (GravityFlipRegion r : enabled) {
|
||||||
|
Box box = r.asBox();
|
||||||
|
if (box.containsPosition(x, y, z)) hit.add(r.getName());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Diff & chat
|
||||||
|
for (Map.Entry<UUID, Set<String>> e : current.entrySet()) {
|
||||||
|
UUID uuid = e.getKey();
|
||||||
|
Set<String> now = e.getValue();
|
||||||
|
Set<String> prev = lastRegions.getOrDefault(uuid, Set.of());
|
||||||
|
PlayerRef pref = refs.get(uuid);
|
||||||
|
if (pref == null) continue;
|
||||||
|
|
||||||
|
Set<String> entered = new HashSet<>(now);
|
||||||
|
entered.removeAll(prev);
|
||||||
|
for (String rn : entered) {
|
||||||
|
GravityFlipRegion region = findByName(enabled, rn);
|
||||||
|
if (region != null) sendEnter(pref, region);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> exited = new HashSet<>(prev);
|
||||||
|
exited.removeAll(now);
|
||||||
|
for (String rn : exited) sendExit(pref, rn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last state: replace with current, drop entries for players now in no region
|
||||||
|
lastRegions.clear();
|
||||||
|
for (Map.Entry<UUID, Set<String>> e : current.entrySet()) {
|
||||||
|
if (!e.getValue().isEmpty()) lastRegions.put(e.getKey(), new HashSet<>(e.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GravityFlipRegion findByName(List<GravityFlipRegion> list, String name) {
|
||||||
|
for (GravityFlipRegion r : list) if (r.getName().equals(name)) return r;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendEnter(PlayerRef pref, GravityFlipRegion r) {
|
||||||
|
String msg = String.format(
|
||||||
|
"[GF] ENTER %s | Mode=%s | Particle=%s | Density=%.2f | Force=%.2f | FallDmg=%s | Grace=%dms",
|
||||||
|
r.getName(),
|
||||||
|
r.getVisualMode(),
|
||||||
|
r.getVisualParticleId(),
|
||||||
|
r.getVisualParticleDensity(),
|
||||||
|
r.getVerticalForce(),
|
||||||
|
r.isFallDamage(),
|
||||||
|
r.getGracePeriodMs());
|
||||||
|
sendChat(pref, msg);
|
||||||
|
try { logHandler.accept(pref.getUsername() + " " + msg); } catch (Throwable t) { errorHandler.accept(t); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendExit(PlayerRef pref, String regionName) {
|
||||||
|
String msg = "[GF] EXIT " + regionName;
|
||||||
|
sendChat(pref, msg);
|
||||||
|
try { logHandler.accept(pref.getUsername() + " " + msg); } catch (Throwable t) { errorHandler.accept(t); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendChat(PlayerRef pref, String text) {
|
||||||
|
try {
|
||||||
|
FormattedMessage fm = new FormattedMessage();
|
||||||
|
fm.rawText = text;
|
||||||
|
pref.getPacketHandler().writeNoCache(new ServerMessage(ChatType.Chat, fm));
|
||||||
|
} catch (Throwable th) {
|
||||||
|
errorHandler.accept(th);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,10 +48,14 @@ import java.util.function.Consumer;
|
|||||||
* filter BEFORE any wake / cmdBuf flip ; {@code VerticalForce} replaces the hardcoded 0.1.</li>
|
* 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
|
* <li>{@link FallDamageGuard} notified on entry (pass 1) and exit (pass 2) with the
|
||||||
* first-matched region to drive {@link FallDamageSuppressorSystem}.</li>
|
* 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>
|
* </ul>
|
||||||
*
|
*
|
||||||
|
* <p><b>Hotpath note:</b> Pass 1 (flip) and Pass 2 (restore) each iterate
|
||||||
|
* {@code forEachEntityParallel}. They CANNOT be fused: restore requires the complete
|
||||||
|
* {@code currentlyInRegion} set (computed during Pass 1) to diff against
|
||||||
|
* {@code previouslyInverted}. Merging would either miss restores or double-visit
|
||||||
|
* entities with inconsistent per-chunk state.
|
||||||
|
*
|
||||||
* <p><b>Multi-region precedence :</b> for an entity simultaneously inside N regions, the FIRST
|
* <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
|
* 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
|
* order via the underlying {@code LinkedHashMap}) drives the config values read this tick
|
||||||
@@ -147,23 +151,14 @@ public final class GravityApplier {
|
|||||||
private final ConcurrentHashMap<UUID, GravityFlipRegion> lastKnownRegion = new ConcurrentHashMap<>();
|
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 FallDamageGuard guard;
|
private final FallDamageGuard guard;
|
||||||
|
|
||||||
/** One-shot per-UUID log tracking (diagnostic prod) — retained after Plan 03-04 cleanup. */
|
|
||||||
private final Set<UUID> loggedNpcUuids = ConcurrentHashMap.newKeySet();
|
|
||||||
|
|
||||||
public GravityApplier(Consumer<Throwable> errorHandler) {
|
public GravityApplier(Consumer<Throwable> errorHandler) {
|
||||||
this(errorHandler, null, new FallDamageGuard());
|
this(errorHandler, new FallDamageGuard());
|
||||||
}
|
}
|
||||||
|
|
||||||
public GravityApplier(Consumer<Throwable> errorHandler, Consumer<String> infoHandler) {
|
public GravityApplier(Consumer<Throwable> errorHandler, FallDamageGuard guard) {
|
||||||
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.guard = guard == null ? new FallDamageGuard() : guard;
|
this.guard = guard == null ? new FallDamageGuard() : guard;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,20 +373,9 @@ public final class GravityApplier {
|
|||||||
if (npc != null) {
|
if (npc != null) {
|
||||||
try {
|
try {
|
||||||
Role role = npc.getRole();
|
Role role = npc.getRole();
|
||||||
if (role == null) {
|
if (role != null) {
|
||||||
// rôle non résolu — rien à faire, no-op
|
|
||||||
} else {
|
|
||||||
MotionController active = role.getActiveMotionController();
|
MotionController active = role.getActiveMotionController();
|
||||||
if (active == null) {
|
if (active != null) {
|
||||||
// log one-shot : rôle non-null mais controller null (utile diag prod)
|
|
||||||
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);
|
||||||
|
|
||||||
// Plan 03-04 : seed forceVelocity.y paramétré par VerticalForce (remplace
|
// Plan 03-04 : seed forceVelocity.y paramétré par VerticalForce (remplace
|
||||||
@@ -407,15 +391,6 @@ public final class GravityApplier {
|
|||||||
errorHandler.accept(th);
|
errorHandler.accept(th);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// log one-shot : première fois qu'on voit cet UUID NPC
|
|
||||||
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) {
|
} catch (Throwable th) {
|
||||||
|
|||||||
@@ -114,10 +114,11 @@ public final class GravityFlipRegion {
|
|||||||
int visualRefreshMs = 1000;
|
int visualRefreshMs = 1000;
|
||||||
double visualOpacity = 0.5;
|
double visualOpacity = 0.5;
|
||||||
|
|
||||||
// Plan 03-06 : particle-mode fields — defaults chosen after Task 1 dump
|
// Plan 03-06 : particle-mode fields — default switched to Torch_Fire after UAT
|
||||||
// (Dust_Sparkles_Fine, density 1.0 particle/m on each of the 12 AABB edges).
|
// showed Dust_Sparkles_Fine is invisible in-world. Torch_Fire is a persistent flame
|
||||||
String visualParticleId = "Dust_Sparkles_Fine";
|
// that renders reliably at any altitude. Density 0.3 = ~12 points per 40m edge.
|
||||||
double visualParticleDensity = 1.0;
|
String visualParticleId = "Torch_Fire";
|
||||||
|
double visualParticleDensity = 0.3;
|
||||||
|
|
||||||
public GravityFlipRegion() {}
|
public GravityFlipRegion() {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.mythlane.gravityflip.tick;
|
package com.mythlane.gravityflip.tick;
|
||||||
|
|
||||||
import com.hypixel.hytale.server.core.universe.world.World;
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
|
import com.mythlane.gravityflip.debug.RegionEnterNotifier;
|
||||||
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.viz.RegionVisualizer;
|
import com.mythlane.gravityflip.viz.RegionVisualizer;
|
||||||
@@ -36,6 +37,7 @@ public final class RegionTickLoop {
|
|||||||
private final RegionRegistry registry;
|
private final RegionRegistry registry;
|
||||||
private final GravityApplier gravityApplier;
|
private final GravityApplier gravityApplier;
|
||||||
private final RegionVisualizer regionVisualizer;
|
private final RegionVisualizer regionVisualizer;
|
||||||
|
private volatile RegionEnterNotifier enterNotifier;
|
||||||
|
|
||||||
public RegionTickLoop(RegionRegistry registry, Consumer<Throwable> errorHandler) {
|
public RegionTickLoop(RegionRegistry registry, Consumer<Throwable> errorHandler) {
|
||||||
this(registry, null, null, errorHandler);
|
this(registry, null, null, errorHandler);
|
||||||
@@ -86,6 +88,9 @@ public final class RegionTickLoop {
|
|||||||
if (regionVisualizer != null) {
|
if (regionVisualizer != null) {
|
||||||
regionVisualizer.visualize(w, registry.currentSnapshot(w));
|
regionVisualizer.visualize(w, registry.currentSnapshot(w));
|
||||||
}
|
}
|
||||||
|
if (enterNotifier != null) {
|
||||||
|
enterNotifier.tick(w);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
scheduleGuarded(initialDelayMs, tick);
|
scheduleGuarded(initialDelayMs, tick);
|
||||||
}
|
}
|
||||||
@@ -115,4 +120,7 @@ public final class RegionTickLoop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ScheduledFuture<?> future() { return future; }
|
public ScheduledFuture<?> future() { return future; }
|
||||||
|
|
||||||
|
/** Debug-only: branche un {@link RegionEnterNotifier} appelé après visualize() chaque tick. */
|
||||||
|
public void setEnterNotifier(RegionEnterNotifier notifier) { this.enterNotifier = notifier; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ public final class RegionVisualizer {
|
|||||||
static final int MIN_REFRESH_MS = 100;
|
static final int MIN_REFRESH_MS = 100;
|
||||||
|
|
||||||
/** Fallback asset-id lorsqu'une VisualParticleId invalide est détectée. */
|
/** Fallback asset-id lorsqu'une VisualParticleId invalide est détectée. */
|
||||||
public static final String DEFAULT_PARTICLE_ID = "Dust_Sparkles_Fine";
|
public static final String DEFAULT_PARTICLE_ID = "Torch_Fire";
|
||||||
|
|
||||||
/** Testability: DebugUtils emitter injectable (prod = {@link DebugUtils#add} wrapper). */
|
/** Testability: DebugUtils emitter injectable (prod = {@link DebugUtils#add} wrapper). */
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
@@ -111,7 +111,7 @@ public final class RegionVisualizer {
|
|||||||
1.0f, // default scale
|
1.0f, // default scale
|
||||||
null); // no color override
|
null); // no color override
|
||||||
for (PlayerRef playerRef : world.getPlayerRefs()) {
|
for (PlayerRef playerRef : world.getPlayerRefs()) {
|
||||||
playerRef.getPacketHandler().write((ToClientPacket) packet);
|
playerRef.getPacketHandler().writeNoCache(packet);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -126,6 +126,8 @@ public final class RegionVisualizer {
|
|||||||
private final Map<String, Long> lastEmitMs = new ConcurrentHashMap<>();
|
private final Map<String, Long> lastEmitMs = new ConcurrentHashMap<>();
|
||||||
/** Ids déjà warned comme invalides — évite le log-spam par tick. */
|
/** Ids déjà warned comme invalides — évite le log-spam par tick. */
|
||||||
private final KeySetView<String, Boolean> warnedInvalidIds = ConcurrentHashMap.newKeySet();
|
private final KeySetView<String, Boolean> warnedInvalidIds = ConcurrentHashMap.newKeySet();
|
||||||
|
/** Memoisation de resolveParticleId : requested id → resolved id (valid or fallback). */
|
||||||
|
private final Map<String, String> resolvedIdCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public RegionVisualizer(Consumer<Throwable> errorHandler) {
|
public RegionVisualizer(Consumer<Throwable> errorHandler) {
|
||||||
this(errorHandler, DEFAULT_DEBUG_EMITTER, DEFAULT_PARTICLE_EMITTER,
|
this(errorHandler, DEFAULT_DEBUG_EMITTER, DEFAULT_PARTICLE_EMITTER,
|
||||||
@@ -240,20 +242,27 @@ public final class RegionVisualizer {
|
|||||||
if (requested == null || requested.isEmpty()) {
|
if (requested == null || requested.isEmpty()) {
|
||||||
return DEFAULT_PARTICLE_ID;
|
return DEFAULT_PARTICLE_ID;
|
||||||
}
|
}
|
||||||
|
String cached = resolvedIdCache.get(requested);
|
||||||
|
if (cached != null) return cached;
|
||||||
|
String resolved;
|
||||||
try {
|
try {
|
||||||
if (ParticleSystem.getAssetMap().getAsset(requested) != null) {
|
if (ParticleSystem.getAssetMap().getAsset(requested) != null) {
|
||||||
return requested;
|
resolved = requested;
|
||||||
|
} else {
|
||||||
|
if (warnedInvalidIds.add(requested)) {
|
||||||
|
java.util.logging.Logger.getLogger(RegionVisualizer.class.getName())
|
||||||
|
.warning("[RegionVisualizer] Unknown VisualParticleId '" + requested
|
||||||
|
+ "' — falling back to '" + DEFAULT_PARTICLE_ID + "'");
|
||||||
|
}
|
||||||
|
resolved = DEFAULT_PARTICLE_ID;
|
||||||
}
|
}
|
||||||
} catch (Throwable th) {
|
} catch (Throwable th) {
|
||||||
// AssetMap indisponible (tests hors serveur) — fail-open.
|
// AssetMap indisponible (tests hors serveur) — fail-open sans caching
|
||||||
|
// (le premier appel prod bootera le cache avec la valeur définitive).
|
||||||
return requested;
|
return requested;
|
||||||
}
|
}
|
||||||
if (warnedInvalidIds.add(requested)) {
|
resolvedIdCache.put(requested, resolved);
|
||||||
errorHandler.accept(new IllegalArgumentException(
|
return resolved;
|
||||||
"[RegionVisualizer] Unknown VisualParticleId '" + requested
|
|
||||||
+ "' — falling back to '" + DEFAULT_PARTICLE_ID + "'"));
|
|
||||||
}
|
|
||||||
return DEFAULT_PARTICLE_ID;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clear immédiat de toutes les shapes debug côté clients (appelé au shutdown). */
|
/** Clear immédiat de toutes les shapes debug côté clients (appelé au shutdown). */
|
||||||
|
|||||||
@@ -116,8 +116,8 @@ class RegionVisualizerTest {
|
|||||||
// Defaults must match the codec's documented defaults (03-06).
|
// Defaults must match the codec's documented defaults (03-06).
|
||||||
GravityFlipRegion r = new GravityFlipRegion("x",
|
GravityFlipRegion r = new GravityFlipRegion("x",
|
||||||
new Box(new Vector3d(0, 0, 0), new Vector3d(1, 1, 1)), true);
|
new Box(new Vector3d(0, 0, 0), new Vector3d(1, 1, 1)), true);
|
||||||
assertEquals("Dust_Sparkles_Fine", r.getVisualParticleId());
|
assertEquals("Torch_Fire", r.getVisualParticleId());
|
||||||
assertEquals(1.0, r.getVisualParticleDensity(), 1e-9);
|
assertEquals(0.3, r.getVisualParticleDensity(), 1e-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- matrixFromBox ----------
|
// ---------- matrixFromBox ----------
|
||||||
|
|||||||
Reference in New Issue
Block a user