refactor: simplify ChainResolver and RayCaster interfaces, enhance ChainDamageApplier logic

- Updated ChainResolver to remove unused shooterOrigin and shooterDirection parameters, streamlining the resolve method.
- Modified RayCaster interface to reflect changes in method signature, focusing on maxBlocks only.
- Enhanced ChainDamageApplier to utilize a new CauseIndexHolder for resolving the PHYSICAL damage cause index, improving clarity and performance.
- Refactored ChainDamageApplier.apply method to use HytaleEntityAdapter for target references, ensuring correct damage application.
- Adjusted tests in ChainResolverTest and ChainDamageApplierTest to align with the new method signatures and logic.

Tests: All tests passing. Build: ./gradlew clean build successful.
This commit is contained in:
2026-04-28 08:20:07 +02:00
parent 03754a0646
commit f6ca35bfc4
9 changed files with 101 additions and 143 deletions
@@ -12,15 +12,12 @@ public final class ChainResolver {
private ChainResolver() {}
/** Resolves the full chain; returns the primary plus up to maxTargets-1 nearest unique neighbors. */
public static List<ChainHit> resolve(
Vec3 shooterOrigin,
Vec3 shooterDirection,
double rayMaxBlocks,
RayCaster ray,
EntitySource neighbors,
ChainParameters params) {
public static List<ChainHit> resolve(double rayMaxBlocks,
RayCaster ray,
EntitySource neighbors,
ChainParameters params) {
Optional<ChainEntity> primaryOpt = ray.firstHit(shooterOrigin, shooterDirection, rayMaxBlocks);
Optional<ChainEntity> primaryOpt = ray.firstHit(rayMaxBlocks);
if (primaryOpt.isEmpty()) {
return List.of();
}
@@ -2,8 +2,8 @@ package com.mythlane.chainlightning.chain;
import java.util.Optional;
/** SAM that returns the first entity hit along a ray, or empty. */
/** SAM that returns the first entity along the caster's look ray, or empty. */
@FunctionalInterface
public interface RayCaster {
Optional<ChainEntity> firstHit(Vec3 origin, Vec3 direction, double maxBlocks);
Optional<ChainEntity> firstHit(double maxBlocks);
}
@@ -15,6 +15,7 @@ import java.util.logging.Level;
import java.util.logging.Logger;
/** Applies DamageSystems.executeDamage to each chain hit; injectable executor keeps unit tests off the Hytale runtime. */
@SuppressWarnings("deprecation")
public final class ChainDamageApplier {
private static final Logger LOGGER = Logger.getLogger(ChainDamageApplier.class.getName());
@@ -28,24 +29,28 @@ public final class ChainDamageApplier {
}
/** Lazy holder keeps DamageSystems.&lt;clinit&gt; out of the test classpath. */
@SuppressWarnings("deprecation")
private static final class DefaultHolder {
static final DamageExecutor INSTANCE = DamageSystems::executeDamage;
}
/** Lazy holder caches the asset-map index of PHYSICAL once the runtime is initialized. */
private static final class CauseIndexHolder {
static final int VALUE = computeIndex();
private static int computeIndex() {
DamageCause physical = DamageCause.PHYSICAL;
return physical == null ? 0 : DamageCause.getAssetMap().getIndex(physical.getId());
}
}
/** Production executor that delegates to DamageSystems.executeDamage. */
public static DamageExecutor defaultExecutor() {
return DefaultHolder.INSTANCE;
}
/** Resolves the PHYSICAL cause index at call time so the runtime asset map ordering is honored. */
@SuppressWarnings("deprecation")
/** PHYSICAL cause index resolved from the asset map; falls back to 0 in unit tests where the runtime is not booted. */
static int physicalDamageCauseIndex() {
DamageCause physical = DamageCause.PHYSICAL;
if (physical == null) {
return 0;
}
return DamageCause.getAssetMap().getIndex(physical.getId());
return CauseIndexHolder.VALUE;
}
private ChainDamageApplier() {}
@@ -64,17 +69,13 @@ public final class ChainDamageApplier {
@Nonnull CommandBuffer<EntityStore> commandBuffer,
@Nonnull DamageExecutor executor) {
int causeIndex = physicalDamageCauseIndex();
Damage.EntitySource source = new Damage.EntitySource(attacker);
int succeeded = 0;
for (ChainHit hit : hits) {
HytaleEntityAdapter adapter = (HytaleEntityAdapter) hit.target();
Ref<EntityStore> targetRef = adapter.ref();
Damage damage = new Damage(
new Damage.EntitySource(attacker),
causeIndex,
(float) hit.damageHp()
);
HytaleEntityAdapter adapter = HytaleEntityAdapter.from(hit);
Damage damage = new Damage(source, causeIndex, (float) hit.damageHp());
try {
executor.execute(targetRef, commandBuffer, damage);
executor.execute(adapter.ref(), commandBuffer, damage);
succeeded++;
} catch (Throwable t) {
LOGGER.log(Level.WARNING,
@@ -11,7 +11,6 @@ import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.mythlane.chainlightning.chain.ChainHit;
import com.mythlane.chainlightning.chain.ChainParameters;
import com.mythlane.chainlightning.chain.ChainResolver;
import com.mythlane.chainlightning.chain.Vec3;
import javax.annotation.Nonnull;
import java.util.List;
@@ -81,10 +80,7 @@ public final class ChainLightningSceptreInteraction extends SimpleInstantInterac
@Nonnull CommandBuffer<EntityStore> commandBuffer) {
HytalePlayerRayCaster ray = new HytalePlayerRayCaster(playerRef, commandBuffer);
HytaleEntitySource neighbors = new HytaleEntitySource(commandBuffer);
return ChainResolver.resolve(
Vec3.ZERO, Vec3.ZERO, RAY_MAX_BLOCKS,
ray, neighbors, ChainParameters.DEFAULT
);
return ChainResolver.resolve(RAY_MAX_BLOCKS, ray, neighbors, ChainParameters.DEFAULT);
}
/** VFX emit is best-effort: damage is already applied so a failure must not abort the cooldown step. */
@@ -9,6 +9,7 @@ import com.hypixel.hytale.server.core.modules.entitystats.EntityStatValue;
import com.hypixel.hytale.server.core.modules.entitystats.asset.DefaultEntityStatTypes;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.mythlane.chainlightning.chain.ChainEntity;
import com.mythlane.chainlightning.chain.ChainHit;
import com.mythlane.chainlightning.chain.Vec3;
import javax.annotation.Nonnull;
@@ -54,6 +55,12 @@ public final class HytaleEntityAdapter implements ChainEntity {
return new HytaleEntityAdapter(ref, id, vec, alive);
}
/** Single cast point so callers stop reaching into ChainEntity to recover the runtime adapter. */
@Nonnull
public static HytaleEntityAdapter from(@Nonnull ChainHit hit) {
return (HytaleEntityAdapter) hit.target();
}
/** Underlying Hytale ref, exposed so DamageSystems and EffectControllerComponent can address the entity. */
@Nonnull
public Ref<EntityStore> ref() {
@@ -6,12 +6,11 @@ import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.server.core.util.TargetUtil;
import com.mythlane.chainlightning.chain.ChainEntity;
import com.mythlane.chainlightning.chain.RayCaster;
import com.mythlane.chainlightning.chain.Vec3;
import javax.annotation.Nonnull;
import java.util.Optional;
/** Phase 3 RayCaster delegating to TargetUtil; origin/direction args are ignored since TargetUtil derives them from playerRef. */
/** Phase 3 RayCaster delegating to TargetUtil; eye origin and look direction are derived from the playerRef inside Hytale. */
public final class HytalePlayerRayCaster implements RayCaster {
private final Ref<EntityStore> playerRef;
@@ -24,7 +23,7 @@ public final class HytalePlayerRayCaster implements RayCaster {
}
@Override
public Optional<ChainEntity> firstHit(Vec3 originIgnored, Vec3 directionIgnored, double maxBlocks) {
public Optional<ChainEntity> firstHit(double maxBlocks) {
Ref<EntityStore> target = TargetUtil.getTargetEntity(playerRef, (float) maxBlocks, accessor);
if (target == null) {
return Optional.empty();
@@ -37,28 +37,17 @@ public final class HytaleVfxEmitter {
ComponentAccessor<EntityStore> accessor = commandBuffer;
int applied = 0;
int skipped = 0;
for (int i = 0; i < hits.size(); i++) {
Ref<EntityStore> targetRef = ((HytaleEntityAdapter) hits.get(i).target()).ref();
if (targetRef == null || !targetRef.isValid()) {
skipped++;
continue;
}
for (ChainHit hit : hits) {
Ref<EntityStore> targetRef = HytaleEntityAdapter.from(hit).ref();
if (!targetRef.isValid()) continue;
EffectControllerComponent ecc = accessor.getComponent(targetRef, EffectControllerComponent.getComponentType());
if (ecc == null) {
skipped++;
continue;
}
boolean ok = ecc.addEffect(targetRef, entityEffect, accessor);
if (ok) {
if (ecc != null && ecc.addEffect(targetRef, entityEffect, accessor)) {
applied++;
} else {
skipped++;
}
}
LOGGER.info(String.format(
"[ChainLightning][Vfx] EntityEffect '%s' applied to %d/%d targets (skipped=%d, caster=%s)",
EFFECT_ID, applied, hits.size(), skipped, playerRef));
"[ChainLightning][Vfx] EntityEffect '%s' applied to %d/%d targets (caster=%s)",
EFFECT_ID, applied, hits.size(), playerRef));
}
}