diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/ChainDamageApplier.java b/src/main/java/com/mythlane/chainlightning/sceptre/ChainDamageApplier.java index 0188d22..9482dda 100644 --- a/src/main/java/com/mythlane/chainlightning/sceptre/ChainDamageApplier.java +++ b/src/main/java/com/mythlane/chainlightning/sceptre/ChainDamageApplier.java @@ -4,31 +4,38 @@ import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.ComponentAccessor; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.server.core.modules.entity.damage.Damage; -// DamageCause import supprime : on utilise l'index int pour eviter la dependance au runtime Hytale -// import com.hypixel.hytale.server.core.modules.entity.damage.DamageCause; +import com.hypixel.hytale.server.core.modules.entity.damage.DamageCause; import com.hypixel.hytale.server.core.modules.entity.damage.DamageSystems; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.mythlane.chainlightning.chain.ChainHit; import javax.annotation.Nonnull; import java.util.List; +import java.util.logging.Logger; /** * Helper static qui applique {@link DamageSystems#executeDamage} pour chaque hit d'une chaine resolue. * - *

Utilise {@link DamageCause#PHYSICAL} (deprecie mais fonctionnel -- RESEARCH Q5). Une cause - * custom "chain_lightning" via DamageCause asset map est deferee v2. + *

Utilise {@link DamageCause#PHYSICAL} pour resoudre l'index au moment de l'appel (runtime). + * L'index est lu depuis l'asset map via {@code DamageCause.PHYSICAL} -- initialise par EntityModule + * au boot serveur, pattern identique aux builtins Hytale (DeployableTurretConfig, ProjectileComponent). + * + *

Pourquoi ne pas hardcoder index=0 : {@code IndexedLookupTableAssetMap} assigne les index + * dans l'ordre de chargement filesystem des JSON -- non-deterministe. Index 0 != PHYSICAL en runtime + * reel (bug chain-no-damage : damage.getCause() retournait null -> NPE silencieuse dans ArmorDamageReduction). * *

Cast explicite {@code ChainHit.target() -> HytaleEntityAdapter} : Phase 3 garantit que c'est - * la SEULE implementation de ChainEntity produite par les adapters Phase 3 (les snapshots - * proviennent uniquement de HytalePlayerRayCaster.firstHit + HytaleEntitySource.nearby). + * la SEULE implementation de ChainEntity produite par les adapters Phase 3. * *

Testabilite : l'overload a {@link DamageExecutor} permet d'injecter un stub en test * sans avoir a initialiser le runtime Hytale (DamageSystems possede un initialiseur statique - * dependant de PluginBase/HytaleLogger). + * dependant de PluginBase/HytaleLogger). En contexte de test, {@code DamageCause.PHYSICAL} est null ; + * {@link #physicalDamageCauseIndex()} retourne alors 0 comme index neutre (non verifie par les tests). */ public final class ChainDamageApplier { + private static final Logger LOGGER = Logger.getLogger(ChainDamageApplier.class.getName()); + /** * SAM injectable pour l'application des degats -- permet de stubber DamageSystems en test. */ @@ -54,6 +61,23 @@ public final class ChainDamageApplier { return DefaultHolder.INSTANCE; } + /** + * Resout l'index de la cause PHYSICAL au moment de l'appel. + * + *

En runtime Hytale, {@code DamageCause.PHYSICAL} est initialise par EntityModule au boot. + * En contexte de test unitaire (pas de runtime), il est null -- on retourne 0 comme index + * neutre (non interprete par les tests qui mockent le DamageExecutor). + */ + @SuppressWarnings("deprecation") + static int physicalDamageCauseIndex() { + DamageCause physical = DamageCause.PHYSICAL; + if (physical == null) { + // Contexte de test : DamageCause.PHYSICAL non initialise, index neutre. + return 0; + } + return DamageCause.getAssetMap().getIndex(physical.getId()); + } + private ChainDamageApplier() {} /** @@ -80,22 +104,35 @@ public final class ChainDamageApplier { * @param commandBuffer buffer de commandes du tick courant * @param executor executeur de degats (DEFAULT en prod, stub en test) */ - @SuppressWarnings("deprecation") public static void apply(@Nonnull List hits, @Nonnull Ref attacker, @Nonnull CommandBuffer commandBuffer, @Nonnull DamageExecutor executor) { - for (ChainHit hit : hits) { + int causeIndex = physicalDamageCauseIndex(); + LOGGER.info(String.format("[ChainLightning][Damage] apply START hits=%d attacker=ref:%d causeIndex=%d (PHYSICAL)", + hits.size(), attacker.getIndex(), causeIndex)); + for (int i = 0; i < hits.size(); i++) { + ChainHit hit = hits.get(i); HytaleEntityAdapter adapter = (HytaleEntityAdapter) hit.target(); - // Utilise l'overload int damageCauseIndex pour eviter DamageCause.PHYSICAL (null hors runtime). - // Index 0 = PHYSICAL par convention dans l'asset map Hytale (verifie : DamageCause.CODEC - // initialise PHYSICAL en premier lors du boot serveur). + Ref targetRef = adapter.ref(); + // Utilise l'overload int avec l'index resolu depuis DamageCause.PHYSICAL au runtime. + // En runtime : causeIndex = index reel de Physical dans l'asset map (deterministe). + // En test : causeIndex = 0 (neutre, non verifie par les tests unitaires). Damage damage = new Damage( new Damage.EntitySource(attacker), - 0, // damageCauseIndex 0 = PHYSICAL + causeIndex, (float) hit.damageHp() ); - executor.execute(adapter.ref(), commandBuffer, damage); + LOGGER.info(String.format("[ChainLightning][Damage] [%d/%d] target=ref:%d (id=%s) amount=%dHP causeIndex=%d -> calling executeDamage...", + i + 1, hits.size(), targetRef.getIndex(), adapter.id(), hit.damageHp(), causeIndex)); + try { + executor.execute(targetRef, commandBuffer, damage); + LOGGER.info(String.format("[ChainLightning][Damage] [%d/%d] executeDamage OK", i + 1, hits.size())); + } catch (Throwable t) { + LOGGER.log(java.util.logging.Level.WARNING, + String.format("[ChainLightning][Damage] [%d/%d] executeDamage THREW", i + 1, hits.size()), t); + } } + LOGGER.info("[ChainLightning][Damage] apply DONE"); } } diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java b/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java index 677b012..e621c4c 100644 --- a/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java +++ b/src/main/java/com/mythlane/chainlightning/sceptre/ChainLightningSceptreInteraction.java @@ -71,25 +71,34 @@ public final class ChainLightningSceptreInteraction extends SimpleInstantInterac protected void firstRun(@Nonnull InteractionType type, @Nonnull InteractionContext context, @Nonnull CooldownHandler cooldownHandler) { + LOGGER.info(String.format("[ChainLightning][1/9] firstRun ENTRY type=%s", type)); + // --- Étape 1 : récupérer le cooldown --- CooldownHandler.Cooldown cooldown = cooldownHandler.getCooldown( COOLDOWN_ID, COOLDOWN_TIME, CHARGE_TIMES, FORCE_CREATE, INTERRUPT_RECHARGE); if (cooldown == null) { - LOGGER.log(Level.WARNING, "[ChainLightning] cooldown handler returned null — aborting"); + LOGGER.warning("[ChainLightning][1/9] cooldown handler returned null — aborting"); return; } + LOGGER.info(String.format("[ChainLightning][1/9] cooldown obtenu id=%s maxTime=%.1fs", COOLDOWN_ID, COOLDOWN_TIME)); // --- Étape 2 : check cooldown sans décompter --- - if (cooldown.hasCooldown(false)) { - return; // refus silencieux + boolean onCooldown = cooldown.hasCooldown(false); + LOGGER.info(String.format("[ChainLightning][2/9] hasCooldown(false)=%s", onCooldown)); + if (onCooldown) { + LOGGER.info("[ChainLightning][2/9] still on cooldown — silent refuse"); + return; } try { // --- Étape 3 : extraire player + commandBuffer --- Ref playerRef = context.getEntity(); CommandBuffer commandBuffer = context.getCommandBuffer(); + LOGGER.info(String.format("[ChainLightning][3/9] playerRef=%s commandBuffer=%s", + playerRef == null ? "null" : ("ref:" + playerRef.getIndex() + " valid=" + playerRef.isValid()), + commandBuffer == null ? "null" : commandBuffer.getClass().getSimpleName())); if (playerRef == null || commandBuffer == null) { - LOGGER.log(Level.FINE, "[ChainLightning] missing playerRef or commandBuffer — abort"); + LOGGER.warning("[ChainLightning][3/9] missing playerRef or commandBuffer — abort"); return; } @@ -97,36 +106,48 @@ public final class ChainLightningSceptreInteraction extends SimpleInstantInterac // CommandBuffer implémente ComponentAccessor — passé directement HytalePlayerRayCaster ray = new HytalePlayerRayCaster(playerRef, commandBuffer); HytaleEntitySource neighbors = new HytaleEntitySource(commandBuffer); + LOGGER.info("[ChainLightning][4/9] adapters built (RayCaster + EntitySource)"); // --- Étape 5 : résolution BFS (origin/direction = placeholders ignorés par le wrapper) --- + LOGGER.info(String.format("[ChainLightning][5/9] resolving chain rayMax=%.1f maxTargets=%d radius=%.1f", + RAY_MAX_BLOCKS, ChainParameters.DEFAULT.maxTargets(), ChainParameters.DEFAULT.chainRadius())); List hits = ChainResolver.resolve( Vec3.ZERO, Vec3.ZERO, RAY_MAX_BLOCKS, ray, neighbors, ChainParameters.DEFAULT ); + LOGGER.info(String.format("[ChainLightning][5/9] resolution returned %d hits", hits.size())); // --- Étape 6 : pas de cible → return SANS cooldown --- if (hits.isEmpty()) { - LOGGER.log(Level.FINE, "[ChainLightning] no target"); + LOGGER.info("[ChainLightning][6/9] no target — re-click immediately allowed (no cooldown deducted)"); return; } + // Détail des hits avant damage + for (int i = 0; i < hits.size(); i++) { + ChainHit h = hits.get(i); + LOGGER.info(String.format("[ChainLightning][6/9] hit[%d] target=%s damageHp=%d hopIndex=%d", + i, h.target().id(), h.damageHp(), h.hopIndex())); + } + // --- Étape 7 : appliquer les dégâts --- + LOGGER.info(String.format("[ChainLightning][7/9] applying damage to %d targets (attacker=ref:%d)", + hits.size(), playerRef.getIndex())); ChainDamageApplier.apply(hits, playerRef, commandBuffer, commandBuffer); + LOGGER.info("[ChainLightning][7/9] damage application returned"); // --- Étape 8 : démarrer le cooldown APRÈS succès --- cooldown.deductCharge(); + LOGGER.info(String.format("[ChainLightning][8/9] cooldown deducted (next available in %.1fs)", COOLDOWN_TIME)); - // --- Étape 9 : log structuré --- - if (LOGGER.isLoggable(Level.INFO)) { - StringBuilder ids = new StringBuilder(); - for (int i = 0; i < hits.size(); i++) { - if (i > 0) ids.append(','); - ids.append(hits.get(i).target().id()); - } - LOGGER.log(Level.INFO, - "[ChainLightning] ref:{0} chained {1} targets [{2}]", - new Object[]{"ref:" + playerRef.getIndex(), hits.size(), ids.toString()}); + // --- Étape 9 : log structuré final --- + StringBuilder ids = new StringBuilder(); + for (int i = 0; i < hits.size(); i++) { + if (i > 0) ids.append(','); + ids.append(hits.get(i).target().id()); } + LOGGER.info(String.format("[ChainLightning][9/9] DONE ref:%d chained %d targets [%s]", + playerRef.getIndex(), hits.size(), ids.toString())); } catch (Throwable t) { // CONTEXT decision : try/catch global pour éviter crash tick serveur LOGGER.log(Level.WARNING, "[ChainLightning] chain resolution failed", t); diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntitySource.java b/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntitySource.java index a95bcbb..4e0f119 100644 --- a/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntitySource.java +++ b/src/main/java/com/mythlane/chainlightning/sceptre/HytaleEntitySource.java @@ -12,6 +12,7 @@ import com.mythlane.chainlightning.chain.Vec3; import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.List; +import java.util.logging.Logger; /** * Implementation Phase 3 de {@link EntitySource} qui delegue a @@ -24,6 +25,8 @@ import java.util.List; */ public final class HytaleEntitySource implements EntitySource { + private static final Logger LOGGER = Logger.getLogger(HytaleEntitySource.class.getName()); + private final ComponentAccessor accessor; public HytaleEntitySource(@Nonnull ComponentAccessor accessor) { @@ -33,11 +36,21 @@ public final class HytaleEntitySource implements EntitySource { @Override public List nearby(Vec3 origin, double radius) { Vector3d hytaleOrigin = new Vector3d(origin.x(), origin.y(), origin.z()); + LOGGER.info(String.format("[ChainLightning][EntitySource] getAllEntitiesInSphere(origin=%s, radius=%.1f)", + origin, radius)); List> refs = TargetUtil.getAllEntitiesInSphere(hytaleOrigin, radius, accessor); + LOGGER.info(String.format("[ChainLightning][EntitySource] Hytale returned %d refs", refs == null ? -1 : refs.size())); + if (refs == null) { + return new ArrayList<>(); + } List snapshots = new ArrayList<>(refs.size()); for (Ref ref : refs) { - snapshots.add(HytaleEntityAdapter.snapshot(ref, accessor)); + ChainEntity adapter = HytaleEntityAdapter.snapshot(ref, accessor); + LOGGER.info(String.format("[ChainLightning][EntitySource] ref:%d -> id=%s pos=%s alive=%s", + ref.getIndex(), adapter.id(), adapter.position(), adapter.isAlive())); + snapshots.add(adapter); } + LOGGER.info(String.format("[ChainLightning][EntitySource] returning %d snapshots", snapshots.size())); return snapshots; } } diff --git a/src/main/java/com/mythlane/chainlightning/sceptre/HytalePlayerRayCaster.java b/src/main/java/com/mythlane/chainlightning/sceptre/HytalePlayerRayCaster.java index fefce5e..99b7860 100644 --- a/src/main/java/com/mythlane/chainlightning/sceptre/HytalePlayerRayCaster.java +++ b/src/main/java/com/mythlane/chainlightning/sceptre/HytalePlayerRayCaster.java @@ -10,6 +10,7 @@ import com.mythlane.chainlightning.chain.Vec3; import javax.annotation.Nonnull; import java.util.Optional; +import java.util.logging.Logger; /** * Implementation Phase 3 de {@link RayCaster} qui delegue a {@link TargetUtil#getTargetEntity}. @@ -24,6 +25,8 @@ import java.util.Optional; */ public final class HytalePlayerRayCaster implements RayCaster { + private static final Logger LOGGER = Logger.getLogger(HytalePlayerRayCaster.class.getName()); + private final Ref playerRef; private final ComponentAccessor accessor; @@ -35,10 +38,18 @@ public final class HytalePlayerRayCaster implements RayCaster { @Override public Optional firstHit(Vec3 originIgnored, Vec3 directionIgnored, double maxBlocks) { + LOGGER.info(String.format("[ChainLightning][RayCast] TargetUtil.getTargetEntity(playerRef=ref:%d, maxBlocks=%.1f)", + playerRef.getIndex(), maxBlocks)); Ref target = TargetUtil.getTargetEntity(playerRef, (float) maxBlocks, accessor); if (target == null) { + LOGGER.info("[ChainLightning][RayCast] no entity hit — returning Optional.empty()"); return Optional.empty(); } - return Optional.of(HytaleEntityAdapter.snapshot(target, accessor)); + LOGGER.info(String.format("[ChainLightning][RayCast] HIT target=ref:%d valid=%s — snapshotting", + target.getIndex(), target.isValid())); + ChainEntity adapter = HytaleEntityAdapter.snapshot(target, accessor); + LOGGER.info(String.format("[ChainLightning][RayCast] snapshot id=%s pos=%s alive=%s", + adapter.id(), adapter.position(), adapter.isAlive())); + return Optional.of(adapter); } }