adjust API

This commit is contained in:
2026-08-27 09:35:42 +02:00
parent 5650e9979f
commit f01e78c1f1
8 changed files with 236 additions and 20 deletions
+29 -1
View File
@@ -32,6 +32,29 @@ int appliedCount = AilmentApi.applyEffects(target, source,
Bleed/Soul Rot stacking and `timed` for an explicit duration and amplifier. `AilmentApi.getEffect(type)` exposes the
registered `MobEffect` when an integration needs to inspect an active effect without depending on registry internals.
Other mods can use the same source-safe machinery for their own registered effects:
```java
MobEffect frost = MyEffects.FROST.get();
// Raw duration, vanilla merging, real Forge source, and persistent latest-source tracking.
AilmentApi.applyEffect(target, source, frost, 100, 0);
// Add one stack per call, cap at 4 stacks, and refresh to 100 ticks.
AilmentApi.applyStackingEffect(target, source, frost, 100, 4);
// Add one stack, extend remaining duration, and Arcane-scale each added duration chunk once.
AilmentApi.applyStackingEffect(target, source, frost, 100, 4, true, true);
LivingEntity loadedSource = AilmentApi.getEffectSource(target, frost);
UUID sourceId = AilmentApi.getEffectSourceUuid(target, frost);
```
`applyScaledEffect` provides the non-stacking Arcane-scaled variant. Generic applications are server-only, reject
invalid or cross-dimension sources, and honor team/friendly-fire rules for harmful player-on-player effects. Source
metadata is keyed by the external effect's registry ID and is cleared lazily when queried after the effect expires;
`clearEffectSource` can clear it explicitly without removing the effect.
Fracture stacks up to three times. Each stack removes 2 armor and 1 armor toughness; reapplication adds a stack and
refreshes its duration. Arcane scales that refreshed duration but never changes the fixed attribute penalties.
@@ -73,10 +96,15 @@ snapshotted on application so source unloading does not alter an active effect.
Entity type tags:
- `ascend_ailments:bleed_immune`
- `ascend_ailments:soulless` (immune to Soul Rot)
- `ascend_ailments:sturdy` (immune to Fracture)
- `ascend_ailments:mental_control_resistant`
- `ascend_ailments:mental_control_immune`
Players remain Bleed-compatible and non-player undead are automatically rejected independently of tags.
Vanilla undead are included in `soulless`, and any vanilla or modded entity classified with `MobType.UNDEAD` is
also rejected independently of the tag. `sturdy` includes iron golems by default. Non-player undead remain naturally
Bleed-immune; `bleed_immune` takes precedence even for players and also prevents and clears Hemorrhage buildup.
Integrations can query `AilmentApi.canSoulRot`, `canFracture`, and `canBleed` before presenting or applying abilities.
Item tags:
@@ -8,14 +8,17 @@ import net.fretux.ailments.util.MentalControlUtil;
import net.fretux.ailments.util.ModEntityTypeTags;
import net.fretux.ailments.util.HemorrhageTracker;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectCategory;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.MobType;
import net.minecraft.world.entity.monster.EnderMan;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.registries.ForgeRegistries;
import javax.annotation.Nullable;
import java.util.Objects;
import java.util.UUID;
/** Stable, source-aware entry point for applying and querying Ascend: Ailments effects. */
public final class AilmentApi {
@@ -85,6 +88,97 @@ public final class AilmentApi {
return applied;
}
/**
* Applies an effect owned by another mod with standard Forge source propagation and persistent source tracking.
* The requested duration is used exactly and is not Arcane-scaled. Vanilla effect-merging rules apply.
*/
public static boolean applyEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int requestedTicks, int amplifier) {
return applyExternalEffect(target, source, effect, requestedTicks, amplifier, false);
}
/** Applies an amplifier-zero external effect with source propagation and persistent source tracking. */
public static boolean applyEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int requestedTicks) {
return applyEffect(target, source, effect, requestedTicks, 0);
}
/**
* Applies an external effect and scales its requested duration once using the optional Ascend Arcane curve.
* Use this overload only when the owning mod wants Ailments to be the duration-scaling authority.
*/
public static boolean applyScaledEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int requestedTicks, int amplifier) {
return applyExternalEffect(target, source, effect, requestedTicks, amplifier, true);
}
/** Applies an amplifier-zero external effect with one-time Arcane duration scaling. */
public static boolean applyScaledEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int requestedTicks) {
return applyScaledEffect(target, source, effect, requestedTicks, 0);
}
/** Adds one amplifier stack to an external effect, refreshes its duration, and tracks the latest source. */
public static boolean applyStackingEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int requestedTicks, int maximumStacks) {
return applyStackingEffect(target, source, effect, requestedTicks, maximumStacks, false, false);
}
/**
* Adds one stack to an external effect with configurable duration behavior and optional Arcane scaling.
* {@code maximumStacks} uses a human-facing one-based count; the applied amplifier is capped at one less.
* When {@code extendDuration} is true, the scaled requested duration is added to the remaining duration.
*/
public static boolean applyStackingEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int requestedTicks, int maximumStacks, boolean extendDuration,
boolean scaleDurationWithArcane) {
Objects.requireNonNull(effect, "effect");
if (!externalApplicationValid(target, source, effect, requestedTicks, 0)
|| maximumStacks < 1 || maximumStacks > 256) return false;
MobEffectInstance current = target.getEffect(effect);
if (current != null && current.isInfiniteDuration()) return false;
int amplifier = current == null ? 0 : current.getAmplifier() >= maximumStacks - 1
? maximumStacks - 1 : current.getAmplifier() + 1;
int duration = scaleDurationWithArcane ? AilmentScaling.scaleDuration(requestedTicks, source) : requestedTicks;
if (extendDuration && current != null) duration = safeAdd(current.getDuration(), duration);
boolean applied = addStackingEffect(target, source, effect, duration, amplifier, false, true, true);
if (applied) trackExternalSource(target, source, effect);
return applied;
}
/** Returns the UUID of the latest successful source tracked for an active external effect. */
@Nullable public static UUID getEffectSourceUuid(LivingEntity target, MobEffect effect) {
Objects.requireNonNull(target, "target");
Objects.requireNonNull(effect, "effect");
String key = externalSourceKey(effect);
if (key == null) return null;
if (!target.hasEffect(effect)) {
EffectSourceUtil.clear(target, key);
return null;
}
return EffectSourceUtil.getSourceUuid(target, key);
}
/** Returns the latest tracked source when it is currently loaded, or {@code null}. */
@Nullable public static LivingEntity getEffectSource(LivingEntity target, MobEffect effect) {
if (getEffectSourceUuid(target, effect) == null) return null;
return EffectSourceUtil.getSource(target, externalSourceKey(effect));
}
/** Resolves the latest tracked source only when it is loaded in the target's current level. */
@Nullable public static LivingEntity getEffectSourceInLevel(LivingEntity target, MobEffect effect) {
if (getEffectSourceUuid(target, effect) == null) return null;
return EffectSourceUtil.getSourceInLevel(target, externalSourceKey(effect));
}
/** Clears source metadata created by the generic external-effect API without removing the effect. */
public static void clearEffectSource(LivingEntity target, MobEffect effect) {
Objects.requireNonNull(target, "target");
Objects.requireNonNull(effect, "effect");
String key = externalSourceKey(effect);
if (key != null) EffectSourceUtil.clear(target, key);
}
/** Returns the registered MobEffect represented by a stable public ailment identifier. */
public static MobEffect getEffect(AilmentType type) {
Objects.requireNonNull(type, "type");
@@ -108,7 +202,8 @@ public final class AilmentApi {
/** Applies one Soul Rot stack; disabling extension always refreshes the configured base duration. */
public static boolean applySoulRot(LivingEntity target, @Nullable LivingEntity source,
boolean extendWithStacks) {
if (!serverValid(target) || !pvpProcReady(target, source, AilmentType.SOUL_ROT)) return false;
if (!serverValid(target) || !canSoulRot(target)
|| !pvpProcReady(target, source, AilmentType.SOUL_ROT)) return false;
MobEffect effect = ModEffects.SOUL_ROT.get();
MobEffectInstance current = target.getEffect(effect);
int currentStacks = current == null ? 0 : current.getAmplifier() + 1;
@@ -130,7 +225,8 @@ public final class AilmentApi {
/** Applies an explicit, capped Soul Rot amplifier and source-scaled requested duration. */
public static boolean applySoulRot(LivingEntity target, @Nullable LivingEntity source,
int requestedTicks, int amplifier) {
if (!serverValid(target) || !pvpProcReady(target, source, AilmentType.SOUL_ROT)) return false;
if (!serverValid(target) || !canSoulRot(target)
|| !pvpProcReady(target, source, AilmentType.SOUL_ROT)) return false;
int capped = Math.min(Math.max(0, amplifier),
AilmentsConfig.value(AilmentsConfig.SOUL_ROT_MAX_STACKS) - 1);
return finishApplication(applyDot(target, source, ModEffects.SOUL_ROT.get(), EffectSourceUtil.SOUL_ROT,
@@ -183,7 +279,7 @@ public final class AilmentApi {
*/
public static boolean applyFracture(LivingEntity target, @Nullable LivingEntity source,
int requestedTicks, int amplifier) {
if (!serverValid(target) || requestedTicks < 1 || amplifier < 0
if (!serverValid(target) || !canFracture(target) || requestedTicks < 1 || amplifier < 0
|| !pvpProcReady(target, source, AilmentType.FRACTURE)) return false;
MobEffect effect = ModEffects.FRACTURE.get();
MobEffectInstance current = target.getEffect(effect);
@@ -246,8 +342,16 @@ public final class AilmentApi {
}
public static boolean canBleed(LivingEntity target) {
if (target instanceof Player) return true;
return target.getMobType() != MobType.UNDEAD && !target.getType().is(ModEntityTypeTags.BLEED_IMMUNE);
if (target.getType().is(ModEntityTypeTags.BLEED_IMMUNE)) return false;
return target instanceof Player || target.getMobType() != MobType.UNDEAD;
}
/** Returns false for the Soulless tag and for any entity classified as undead by Minecraft or another mod. */
public static boolean canSoulRot(LivingEntity target) {
return target.getMobType() != MobType.UNDEAD && !target.getType().is(ModEntityTypeTags.SOULLESS);
}
/** Returns false for entity types in the Sturdy tag. */
public static boolean canFracture(LivingEntity target) {
return !target.getType().is(ModEntityTypeTags.STURDY);
}
public static boolean isMentalControlResistant(LivingEntity target) {
return MentalControlUtil.isMentalControlResistant(target);
@@ -319,12 +423,30 @@ public final class AilmentApi {
}
return applied;
}
private static boolean applyExternalEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int requestedTicks, int amplifier, boolean scaleDurationWithArcane) {
Objects.requireNonNull(effect, "effect");
if (!externalApplicationValid(target, source, effect, requestedTicks, amplifier)) return false;
int duration = scaleDurationWithArcane ? AilmentScaling.scaleDuration(requestedTicks, source) : requestedTicks;
boolean applied = addSourcedEffect(target, source, effect, duration, amplifier, false, true, true);
if (applied) trackExternalSource(target, source, effect);
return applied;
}
private static MobEffectInstance instance(MobEffect effect, int duration, int amplifier, boolean icon) {
return new MobEffectInstance(effect, duration, amplifier, false, false, icon);
}
private static MobEffectInstance instance(MobEffect effect, int duration, int amplifier, boolean ambient,
boolean visible, boolean icon) {
return new MobEffectInstance(effect, duration, amplifier, ambient, visible, icon);
}
private static boolean addSourcedEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int duration, int amplifier, boolean icon) {
boolean applied = target.addEffect(instance(effect, duration, amplifier, icon), source);
return addSourcedEffect(target, source, effect, duration, amplifier, false, false, icon);
}
private static boolean addSourcedEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int duration, int amplifier, boolean ambient, boolean visible,
boolean icon) {
boolean applied = target.addEffect(instance(effect, duration, amplifier, ambient, visible, icon), source);
if (applied && AscendCompat.isAscendLoaded()) {
// Ascend observes the standard Forge source and scales duration during addEffect. Ailments already
// applied its source curve, so restore the requested duration to prevent double scaling.
@@ -335,19 +457,44 @@ public final class AilmentApi {
}
private static boolean addStackingEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int duration, int amplifier, boolean icon) {
return addStackingEffect(target, source, effect, duration, amplifier, false, false, icon);
}
private static boolean addStackingEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
int duration, int amplifier, boolean ambient, boolean visible,
boolean icon) {
MobEffectInstance previous = target.getEffect(effect);
if (previous == null || previous.getAmplifier() >= amplifier)
return addSourcedEffect(target, source, effect, duration, amplifier, icon);
return addSourcedEffect(target, source, effect, duration, amplifier, ambient, visible, icon);
// Vanilla preserves a lower-amplifier effect as a hidden fallback when upgrading. These ailments refresh
// to one authoritative stack state, so suppress that fallback without emitting a remove/add lifecycle.
// Vanilla preserves a lower-amplifier effect as a hidden fallback when upgrading. Stacking applications use
// one authoritative stack state, so suppress that fallback without emitting a remove/add lifecycle.
int previousDuration = previous.getDuration();
previous.mapDuration(ignored -> 0);
boolean applied = addSourcedEffect(target, source, effect, duration, amplifier, icon);
boolean applied = addSourcedEffect(target, source, effect, duration, amplifier, ambient, visible, icon);
if (!applied && target.getEffect(effect) == previous)
previous.mapDuration(ignored -> previousDuration);
return applied;
}
private static boolean externalApplicationValid(LivingEntity target, @Nullable LivingEntity source,
MobEffect effect, int requestedTicks, int amplifier) {
if (!serverValid(target) || requestedTicks < 1 || amplifier < 0) return false;
if ((effect == ModEffects.BLEED.get() && !canBleed(target))
|| (effect == ModEffects.SOUL_ROT.get() && !canSoulRot(target))
|| (effect == ModEffects.FRACTURE.get() && !canFracture(target))) return false;
if (source != null && (!source.isAlive() || source.isSpectator()
|| source.level().isClientSide || source.level() != target.level())) return false;
return effect.getCategory() != MobEffectCategory.HARMFUL
|| !(source instanceof Player sourcePlayer && target instanceof Player targetPlayer)
|| source == target || sourcePlayer.canHarmPlayer(targetPlayer);
}
private static void trackExternalSource(LivingEntity target, @Nullable LivingEntity source, MobEffect effect) {
String key = externalSourceKey(effect);
if (key != null) EffectSourceUtil.setSource(target, key, source);
}
@Nullable private static String externalSourceKey(MobEffect effect) {
var id = ForgeRegistries.MOB_EFFECTS.getKey(effect);
return id == null ? null : "ascend_ailments.external." + id;
}
private static boolean serverValid(LivingEntity target) {
return target != null && !target.level().isClientSide && target.isAlive()
&& (!(target instanceof Player player) || !player.isSpectator());
@@ -1,5 +1,6 @@
package net.fretux.ailments.effect;
import net.fretux.ailments.api.AilmentApi;
import net.fretux.ailments.config.AilmentsConfig;
import net.fretux.ailments.damage.ModDamageSources;
import net.fretux.ailments.util.EffectSourceUtil;
@@ -20,6 +21,10 @@ public final class BleedEffect extends MobEffect {
@Override public void applyEffectTick(LivingEntity entity, int amplifier) {
if (entity.level().isClientSide) return;
if (!AilmentApi.canBleed(entity)) {
HemorrhageTracker.clearIfPresent(entity);
return;
}
double base = amplifier <= 0 ? AilmentsConfig.value(AilmentsConfig.BLEED_DAMAGE_0)
: amplifier == 1 ? AilmentsConfig.value(AilmentsConfig.BLEED_DAMAGE_1)
: AilmentsConfig.value(AilmentsConfig.BLEED_DAMAGE_2);
@@ -1,5 +1,6 @@
package net.fretux.ailments.effect;
import net.fretux.ailments.api.AilmentApi;
import net.fretux.ailments.config.AilmentsConfig;
import net.fretux.ailments.util.EffectSourceUtil;
import net.minecraft.world.effect.MobEffect;
@@ -13,7 +14,8 @@ public final class SoulRotEffect extends MobEffect {
@Override public boolean isDurationEffectTick(int duration, int amplifier) { return duration % 20 == 0; }
@Override public void applyEffectTick(LivingEntity entity, int amplifier) {
if (entity.level().isClientSide || !(entity instanceof Player player)) return;
if (entity.level().isClientSide || !AilmentApi.canSoulRot(entity)
|| !(entity instanceof Player player)) return;
float base = (float) (AilmentsConfig.value(AilmentsConfig.SOUL_ROT_EXHAUSTION_BASE)
+ AilmentsConfig.value(AilmentsConfig.SOUL_ROT_EXHAUSTION_PER_AMP) * amplifier);
player.causeFoodExhaustion(base * EffectSourceUtil.getPotency(entity, EffectSourceUtil.SOUL_ROT));
@@ -58,7 +58,8 @@ public final class AilmentEvents {
@SubscribeEvent
public static void onHeal(LivingHealEvent event) {
if (!event.getEntity().level().isClientSide && !event.isCanceled()
&& event.getEntity().hasEffect(ModEffects.SOUL_ROT.get()))
&& event.getEntity().hasEffect(ModEffects.SOUL_ROT.get())
&& AilmentApi.canSoulRot(event.getEntity()))
event.setAmount((float) (event.getAmount() * AilmentsConfig.value(AilmentsConfig.SOUL_ROT_HEALING)));
}
@@ -97,7 +98,7 @@ public final class AilmentEvents {
float amount = event.getAmount();
// 1. Victim vulnerability.
if (victim.hasEffect(ModEffects.SOUL_ROT.get()))
if (victim.hasEffect(ModEffects.SOUL_ROT.get()) && AilmentApi.canSoulRot(victim))
amount *= (float) AilmentsConfig.value(AilmentsConfig.SOUL_ROT_DAMAGE_TAKEN);
// 2. Charm victim and attacker rules.
@@ -26,8 +26,8 @@ public final class ControlEffectHelper {
public static void handleLivingTick(LivingEntity entity) {
if (entity.level().isClientSide) return;
handleSoulRotFood(entity);
cleanDotData(entity);
handleSoulRotFood(entity);
if (entity.hasEffect(ModEffects.FEAR.get())) tickFear(entity);
else {
EffectSourceUtil.clear(entity, EffectSourceUtil.FEAR);
@@ -181,9 +181,25 @@ public final class ControlEffectHelper {
}
private static void cleanDotData(LivingEntity entity) {
if (!entity.hasEffect(ModEffects.SOUL_ROT.get())) EffectSourceUtil.clear(entity, EffectSourceUtil.SOUL_ROT);
if (!entity.hasEffect(ModEffects.BLEED.get())) EffectSourceUtil.clear(entity, EffectSourceUtil.BLEED);
if (!entity.hasEffect(ModEffects.FRACTURE.get())) EffectSourceUtil.clear(entity, EffectSourceUtil.FRACTURE);
if (!AilmentApi.canSoulRot(entity)) {
if (entity.hasEffect(ModEffects.SOUL_ROT.get())) entity.removeEffect(ModEffects.SOUL_ROT.get());
EffectSourceUtil.clear(entity, EffectSourceUtil.SOUL_ROT);
} else if (!entity.hasEffect(ModEffects.SOUL_ROT.get())) {
EffectSourceUtil.clear(entity, EffectSourceUtil.SOUL_ROT);
}
if (!AilmentApi.canBleed(entity)) {
if (entity.hasEffect(ModEffects.BLEED.get())) entity.removeEffect(ModEffects.BLEED.get());
EffectSourceUtil.clear(entity, EffectSourceUtil.BLEED);
HemorrhageTracker.clearIfPresent(entity);
} else if (!entity.hasEffect(ModEffects.BLEED.get())) {
EffectSourceUtil.clear(entity, EffectSourceUtil.BLEED);
}
if (!AilmentApi.canFracture(entity)) {
if (entity.hasEffect(ModEffects.FRACTURE.get())) entity.removeEffect(ModEffects.FRACTURE.get());
EffectSourceUtil.clear(entity, EffectSourceUtil.FRACTURE);
} else if (!entity.hasEffect(ModEffects.FRACTURE.get())) {
EffectSourceUtil.clear(entity, EffectSourceUtil.FRACTURE);
}
}
private ControlEffectHelper() {}
}
@@ -1,6 +1,7 @@
package net.fretux.ailments.util;
import net.fretux.ailments.config.AilmentsConfig;
import net.fretux.ailments.api.AilmentApi;
import net.fretux.ailments.damage.ModDamageSources;
import net.fretux.ailments.network.AilmentNetwork;
import net.minecraft.nbt.CompoundTag;
@@ -16,8 +17,12 @@ public final class HemorrhageTracker {
* Arcane potency is snapshotted when Bleed is applied, so an unloaded source cannot change an active meter.
*/
public static boolean recordBleedDamage(LivingEntity target, int amplifier, double arcanePotency) {
if (target.level().isClientSide
|| amplifier + 1 < AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_MINIMUM_STACKS)) return false;
if (target.level().isClientSide) return false;
if (!AilmentApi.canBleed(target)) {
clear(target);
return false;
}
if (amplifier + 1 < AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_MINIMUM_STACKS)) return false;
double threshold = AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_THRESHOLD);
double added = AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_FILL_PER_TICK)
* Math.max(0.0, arcanePotency);
@@ -41,6 +46,10 @@ public final class HemorrhageTracker {
}
public static double getProgress(LivingEntity target) {
if (!AilmentApi.canBleed(target)) {
clearIfPresent(target);
return 0.0;
}
CompoundTag data = target.getPersistentData();
return data.contains(PROGRESS) ? Math.max(0.0, data.getDouble(PROGRESS)) : 0.0;
}
@@ -55,6 +64,12 @@ public final class HemorrhageTracker {
hideDisplay(target);
}
/** Clears stored buildup and its HUD only when buildup actually exists. */
public static void clearIfPresent(LivingEntity target) {
if (!target.getPersistentData().contains(PROGRESS)) return;
clear(target);
}
/** Synchronizes the compact client HUD after login or a meter update. */
public static void syncDisplay(LivingEntity target) {
if (!(target instanceof ServerPlayer player)) return;
@@ -8,6 +8,8 @@ import net.minecraft.world.entity.EntityType;
public final class ModEntityTypeTags {
public static final TagKey<EntityType<?>> BLEED_IMMUNE = tag("bleed_immune");
public static final TagKey<EntityType<?>> SOULLESS = tag("soulless");
public static final TagKey<EntityType<?>> STURDY = tag("sturdy");
public static final TagKey<EntityType<?>> MENTAL_CONTROL_RESISTANT = tag("mental_control_resistant");
public static final TagKey<EntityType<?>> MENTAL_CONTROL_IMMUNE = tag("mental_control_immune");
private static TagKey<EntityType<?>> tag(String path) {