From 1e47f4e846903b73c8efc6a44ffbdd3fbd9c4eb9 Mon Sep 17 00:00:00 2001 From: kayjaydee Date: Thu, 23 Apr 2026 16:43:07 +0200 Subject: [PATCH] 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. --- .../gravityflip/GravityFlipPlugin.java | 28 +++- .../debug/RegionEnterNotifier.java | 150 ++++++++++++++++++ .../gravityflip/physics/GravityApplier.java | 45 ++---- .../gravityflip/region/GravityFlipRegion.java | 9 +- .../gravityflip/tick/RegionTickLoop.java | 8 + .../gravityflip/viz/RegionVisualizer.java | 29 ++-- .../gravityflip/viz/RegionVisualizerTest.java | 4 +- 7 files changed, 219 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/mythlane/gravityflip/debug/RegionEnterNotifier.java diff --git a/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java b/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java index ceb4b82..7dbebee 100644 --- a/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java +++ b/src/main/java/com/mythlane/gravityflip/GravityFlipPlugin.java @@ -7,6 +7,7 @@ import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.util.Config; import com.mythlane.gravityflip.command.DumpParticlesCommand; import com.mythlane.gravityflip.config.GravityFlipConfig; +import com.mythlane.gravityflip.debug.RegionEnterNotifier; import com.mythlane.gravityflip.physics.FallDamageGuard; import com.mythlane.gravityflip.physics.FallDamageSuppressorSystem; import com.mythlane.gravityflip.physics.GravityApplier; @@ -67,8 +68,6 @@ public class GravityFlipPlugin extends JavaPlugin { getEntityStoreRegistry().registerSystem(new FallDamageSuppressorSystem( fallDamageGuard, th -> getLogger().at(Level.WARNING).withCause(th).log("fallDamageSuppressor handle failed"))); - - getLogger().at(Level.INFO).log("Gravity Flip enabled"); } @Override @@ -78,13 +77,21 @@ public class GravityFlipPlugin extends JavaPlugin { this.registry = new RegionRegistry(cfg, configHolder); this.gravityApplier = new GravityApplier( th -> getLogger().at(Level.WARNING).withCause(th).log("gravityApply failed"), - msg -> getLogger().at(Level.INFO).log("%s", msg), fallDamageGuard); this.regionVisualizer = new RegionVisualizer( th -> getLogger().at(Level.WARNING).withCause(th).log("regionVisualize failed")); this.tickLoop = new RegionTickLoop(registry, gravityApplier, regionVisualizer, th -> 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. this.tickLoop.startWithDelay(2_000L, () -> { 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 protected void shutdown() { // Plan 03-05 : clear des debug shapes cote clients AVANT tickLoop.stop(). diff --git a/src/main/java/com/mythlane/gravityflip/debug/RegionEnterNotifier.java b/src/main/java/com/mythlane/gravityflip/debug/RegionEnterNotifier.java new file mode 100644 index 0000000..92c788a --- /dev/null +++ b/src/main/java/com/mythlane/gravityflip/debug/RegionEnterNotifier.java @@ -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 errorHandler; + private final Consumer logHandler; + private final Map> lastRegions = new ConcurrentHashMap<>(); + + public RegionEnterNotifier(RegionRegistry registry, Consumer errorHandler) { + this(registry, errorHandler, null); + } + + public RegionEnterNotifier(RegionRegistry registry, Consumer errorHandler, + Consumer 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 enabled = new ArrayList<>(); + for (GravityFlipRegion r : registry.all()) if (r.isEnabled()) enabled.add(r); + if (enabled.isEmpty()) { lastRegions.clear(); return; } + + Store store = world.getEntityStore().getStore(); + ComponentType TRANSFORM = TransformComponent.getComponentType(); + ComponentType PLAYER = PlayerRef.getComponentType(); + + Map> current = new ConcurrentHashMap<>(); + Map 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 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> e : current.entrySet()) { + UUID uuid = e.getKey(); + Set now = e.getValue(); + Set prev = lastRegions.getOrDefault(uuid, Set.of()); + PlayerRef pref = refs.get(uuid); + if (pref == null) continue; + + Set entered = new HashSet<>(now); + entered.removeAll(prev); + for (String rn : entered) { + GravityFlipRegion region = findByName(enabled, rn); + if (region != null) sendEnter(pref, region); + } + + Set 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> e : current.entrySet()) { + if (!e.getValue().isEmpty()) lastRegions.put(e.getKey(), new HashSet<>(e.getValue())); + } + } + + private static GravityFlipRegion findByName(List 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); + } + } +} diff --git a/src/main/java/com/mythlane/gravityflip/physics/GravityApplier.java b/src/main/java/com/mythlane/gravityflip/physics/GravityApplier.java index fb72556..99277ee 100644 --- a/src/main/java/com/mythlane/gravityflip/physics/GravityApplier.java +++ b/src/main/java/com/mythlane/gravityflip/physics/GravityApplier.java @@ -48,10 +48,14 @@ import java.util.function.Consumer; * filter BEFORE any wake / cmdBuf flip ; {@code VerticalForce} replaces the hardcoded 0.1. *
  • {@link FallDamageGuard} notified on entry (pass 1) and exit (pass 2) with the * first-matched region to drive {@link FallDamageSuppressorSystem}.
  • - *
  • 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.
  • * * + *

    Hotpath note: 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. + * *

    Multi-region precedence : 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 @@ -147,23 +151,14 @@ public final class GravityApplier { private final ConcurrentHashMap lastKnownRegion = new ConcurrentHashMap<>(); private final Consumer errorHandler; - private final Consumer infoHandler; private final FallDamageGuard guard; - /** One-shot per-UUID log tracking (diagnostic prod) — retained after Plan 03-04 cleanup. */ - private final Set loggedNpcUuids = ConcurrentHashMap.newKeySet(); - public GravityApplier(Consumer errorHandler) { - this(errorHandler, null, new FallDamageGuard()); + this(errorHandler, new FallDamageGuard()); } - public GravityApplier(Consumer errorHandler, Consumer infoHandler) { - this(errorHandler, infoHandler, new FallDamageGuard()); - } - - public GravityApplier(Consumer errorHandler, Consumer infoHandler, FallDamageGuard guard) { + public GravityApplier(Consumer errorHandler, FallDamageGuard guard) { this.errorHandler = errorHandler == null ? t -> {} : errorHandler; - this.infoHandler = infoHandler == null ? m -> {} : infoHandler; this.guard = guard == null ? new FallDamageGuard() : guard; } @@ -378,20 +373,9 @@ public final class GravityApplier { if (npc != null) { try { Role role = npc.getRole(); - if (role == null) { - // rôle non résolu — rien à faire, no-op - } else { + if (role != null) { MotionController active = role.getActiveMotionController(); - 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 { + if (active != null) { active.updatePhysicsValues(targetValues); // Plan 03-04 : seed forceVelocity.y paramétré par VerticalForce (remplace @@ -407,15 +391,6 @@ public final class GravityApplier { 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) { diff --git a/src/main/java/com/mythlane/gravityflip/region/GravityFlipRegion.java b/src/main/java/com/mythlane/gravityflip/region/GravityFlipRegion.java index 7e259a7..c6be72b 100644 --- a/src/main/java/com/mythlane/gravityflip/region/GravityFlipRegion.java +++ b/src/main/java/com/mythlane/gravityflip/region/GravityFlipRegion.java @@ -114,10 +114,11 @@ public final class GravityFlipRegion { int visualRefreshMs = 1000; double visualOpacity = 0.5; - // Plan 03-06 : particle-mode fields — defaults chosen after Task 1 dump - // (Dust_Sparkles_Fine, density 1.0 particle/m on each of the 12 AABB edges). - String visualParticleId = "Dust_Sparkles_Fine"; - double visualParticleDensity = 1.0; + // Plan 03-06 : particle-mode fields — default switched to Torch_Fire after UAT + // showed Dust_Sparkles_Fine is invisible in-world. Torch_Fire is a persistent flame + // that renders reliably at any altitude. Density 0.3 = ~12 points per 40m edge. + String visualParticleId = "Torch_Fire"; + double visualParticleDensity = 0.3; public GravityFlipRegion() {} diff --git a/src/main/java/com/mythlane/gravityflip/tick/RegionTickLoop.java b/src/main/java/com/mythlane/gravityflip/tick/RegionTickLoop.java index a584607..e53e6f3 100644 --- a/src/main/java/com/mythlane/gravityflip/tick/RegionTickLoop.java +++ b/src/main/java/com/mythlane/gravityflip/tick/RegionTickLoop.java @@ -1,6 +1,7 @@ package com.mythlane.gravityflip.tick; 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.region.RegionRegistry; import com.mythlane.gravityflip.viz.RegionVisualizer; @@ -36,6 +37,7 @@ public final class RegionTickLoop { private final RegionRegistry registry; private final GravityApplier gravityApplier; private final RegionVisualizer regionVisualizer; + private volatile RegionEnterNotifier enterNotifier; public RegionTickLoop(RegionRegistry registry, Consumer errorHandler) { this(registry, null, null, errorHandler); @@ -86,6 +88,9 @@ public final class RegionTickLoop { if (regionVisualizer != null) { regionVisualizer.visualize(w, registry.currentSnapshot(w)); } + if (enterNotifier != null) { + enterNotifier.tick(w); + } }; scheduleGuarded(initialDelayMs, tick); } @@ -115,4 +120,7 @@ public final class RegionTickLoop { } 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; } } diff --git a/src/main/java/com/mythlane/gravityflip/viz/RegionVisualizer.java b/src/main/java/com/mythlane/gravityflip/viz/RegionVisualizer.java index 37f281a..11cb1c3 100644 --- a/src/main/java/com/mythlane/gravityflip/viz/RegionVisualizer.java +++ b/src/main/java/com/mythlane/gravityflip/viz/RegionVisualizer.java @@ -73,7 +73,7 @@ public final class RegionVisualizer { static final int MIN_REFRESH_MS = 100; /** 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). */ @FunctionalInterface @@ -111,7 +111,7 @@ public final class RegionVisualizer { 1.0f, // default scale null); // no color override 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 lastEmitMs = new ConcurrentHashMap<>(); /** Ids déjà warned comme invalides — évite le log-spam par tick. */ private final KeySetView warnedInvalidIds = ConcurrentHashMap.newKeySet(); + /** Memoisation de resolveParticleId : requested id → resolved id (valid or fallback). */ + private final Map resolvedIdCache = new ConcurrentHashMap<>(); public RegionVisualizer(Consumer errorHandler) { this(errorHandler, DEFAULT_DEBUG_EMITTER, DEFAULT_PARTICLE_EMITTER, @@ -240,20 +242,27 @@ public final class RegionVisualizer { if (requested == null || requested.isEmpty()) { return DEFAULT_PARTICLE_ID; } + String cached = resolvedIdCache.get(requested); + if (cached != null) return cached; + String resolved; try { 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) { - // 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; } - if (warnedInvalidIds.add(requested)) { - errorHandler.accept(new IllegalArgumentException( - "[RegionVisualizer] Unknown VisualParticleId '" + requested - + "' — falling back to '" + DEFAULT_PARTICLE_ID + "'")); - } - return DEFAULT_PARTICLE_ID; + resolvedIdCache.put(requested, resolved); + return resolved; } /** Clear immédiat de toutes les shapes debug côté clients (appelé au shutdown). */ diff --git a/src/test/java/com/mythlane/gravityflip/viz/RegionVisualizerTest.java b/src/test/java/com/mythlane/gravityflip/viz/RegionVisualizerTest.java index dcde42b..94d2465 100644 --- a/src/test/java/com/mythlane/gravityflip/viz/RegionVisualizerTest.java +++ b/src/test/java/com/mythlane/gravityflip/viz/RegionVisualizerTest.java @@ -116,8 +116,8 @@ class RegionVisualizerTest { // Defaults must match the codec's documented defaults (03-06). GravityFlipRegion r = new GravityFlipRegion("x", new Box(new Vector3d(0, 0, 0), new Vector3d(1, 1, 1)), true); - assertEquals("Dust_Sparkles_Fine", r.getVisualParticleId()); - assertEquals(1.0, r.getVisualParticleDensity(), 1e-9); + assertEquals("Torch_Fire", r.getVisualParticleId()); + assertEquals(0.3, r.getVisualParticleDensity(), 1e-9); } // ---------- matrixFromBox ----------