fix more issues
This commit is contained in:
@@ -37,7 +37,7 @@ Other mods can use the same source-safe machinery for their own registered effec
|
||||
```java
|
||||
MobEffect frost = MyEffects.FROST.get();
|
||||
|
||||
// Raw duration, vanilla merging, real Forge source, and persistent latest-source tracking.
|
||||
// Raw duration, authoritative 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.
|
||||
@@ -50,11 +50,16 @@ 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
|
||||
`applyScaledEffect` provides the non-stacking Arcane-scaled variant. Source-aware applications reject infinite or
|
||||
higher-amplifier active effects instead of creating vanilla hidden fallbacks, ensuring that the visible effect and its
|
||||
single tracked owner cannot diverge. 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.
|
||||
|
||||
Ordinary API applications are not rate-limited. Automatic combat integrations that need the configured PvP proc
|
||||
cooldown should use `applyProcEffect` with either an `AilmentApplication` descriptor or explicit type/duration values.
|
||||
|
||||
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.
|
||||
|
||||
@@ -83,13 +88,15 @@ The single `LivingHurtEvent` pipeline applies modifiers in this order:
|
||||
4. Overcharm's pre-hit Charm snapshot and direct-player-melee bonus
|
||||
|
||||
Each applicable multiplier is applied once. The Overcharm snapshot happens before Charm is refreshed, so its first
|
||||
hit has no pre-existing-Charm bonus.
|
||||
hit has no pre-existing-Charm bonus. For simultaneous control effects, active Fear takes precedence over Taunt, and
|
||||
Taunt takes precedence over Charm's prohibition against attacking its owner.
|
||||
|
||||
## Optional Ascend integration
|
||||
|
||||
`compat.AscendCompat` checks for mod ID `ascend` and reflection-isolates its player-stats capability. Missing classes,
|
||||
missing capabilities, non-player sources, and absent/unloaded source data all fall back to 1.0x. DOT potency is
|
||||
snapshotted on application so source unloading does not alter an active effect.
|
||||
missing capabilities, non-player sources, and absent/unloaded source data all fall back to 1.0x. An incompatible API
|
||||
is logged and cached instead of retrying reflection during every application. DOT potency is snapshotted on
|
||||
application so source unloading does not alter an active effect.
|
||||
|
||||
## Datapack extension
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.fretux.ailments.api;
|
||||
|
||||
import net.fretux.ailments.config.AilmentsConfig;
|
||||
import net.fretux.ailments.compat.AscendCompat;
|
||||
import net.fretux.ailments.registry.ModEffects;
|
||||
import net.fretux.ailments.util.EffectSourceUtil;
|
||||
import net.fretux.ailments.util.ControlEffectHelper;
|
||||
import net.fretux.ailments.util.MentalControlUtil;
|
||||
import net.fretux.ailments.util.ModEntityTypeTags;
|
||||
import net.fretux.ailments.util.HemorrhageTracker;
|
||||
@@ -88,9 +88,33 @@ public final class AilmentApi {
|
||||
return applied;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies an ailment as an automatic combat proc. Unlike ordinary API applications, successful player-versus-
|
||||
* player procs observe the configured per-attacker, per-ailment cooldown.
|
||||
*/
|
||||
public static boolean applyProcEffect(LivingEntity target, @Nullable LivingEntity source,
|
||||
AilmentApplication application) {
|
||||
Objects.requireNonNull(application, "application");
|
||||
if (!pvpProcReady(target, source, application.type())) return false;
|
||||
boolean applied = applyEffect(target, source, application);
|
||||
recordPvpProc(applied, target, source, application.type());
|
||||
return applied;
|
||||
}
|
||||
|
||||
/** Applies an explicit automatic combat proc with the configured PvP cooldown. */
|
||||
public static boolean applyProcEffect(LivingEntity target, @Nullable LivingEntity source, AilmentType type,
|
||||
int requestedTicks, int amplifier) {
|
||||
Objects.requireNonNull(type, "type");
|
||||
if (!pvpProcReady(target, source, type)) return false;
|
||||
boolean applied = applyEffect(target, source, type, requestedTicks, amplifier);
|
||||
recordPvpProc(applied, target, source, type);
|
||||
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.
|
||||
* The requested duration is used exactly and is not Arcane-scaled. Merging is authoritative: a stronger or
|
||||
* infinite active effect is retained instead of creating an untrackable vanilla hidden fallback.
|
||||
*/
|
||||
public static boolean applyEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
|
||||
int requestedTicks, int amplifier) {
|
||||
@@ -202,8 +226,7 @@ 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) || !canSoulRot(target)
|
||||
|| !pvpProcReady(target, source, AilmentType.SOUL_ROT)) return false;
|
||||
if (!harmfulApplicationValid(target, source) || !canSoulRot(target)) return false;
|
||||
MobEffect effect = ModEffects.SOUL_ROT.get();
|
||||
MobEffectInstance current = target.getEffect(effect);
|
||||
int currentStacks = current == null ? 0 : current.getAmplifier() + 1;
|
||||
@@ -219,23 +242,23 @@ public final class AilmentApi {
|
||||
EffectSourceUtil.setPotency(target, EffectSourceUtil.SOUL_ROT,
|
||||
AilmentScaling.getPotencyMultiplier(source, AilmentType.SOUL_ROT));
|
||||
}
|
||||
return finishApplication(applied, target, source, AilmentType.SOUL_ROT);
|
||||
return applied;
|
||||
}
|
||||
|
||||
/** 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) || !canSoulRot(target)
|
||||
|| !pvpProcReady(target, source, AilmentType.SOUL_ROT)) return false;
|
||||
if (!harmfulApplicationValid(target, source) || !canSoulRot(target) || requestedTicks < 1 || amplifier < 0)
|
||||
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,
|
||||
requestedTicks, capped, AilmentType.SOUL_ROT), target, source, AilmentType.SOUL_ROT);
|
||||
return applyDot(target, source, ModEffects.SOUL_ROT.get(), EffectSourceUtil.SOUL_ROT,
|
||||
requestedTicks, capped, AilmentType.SOUL_ROT);
|
||||
}
|
||||
|
||||
/** Applies or adds one Bleed stack, returning false for immune targets. */
|
||||
public static boolean applyBleed(LivingEntity target, @Nullable LivingEntity source) {
|
||||
if (!serverValid(target) || !canBleed(target) || !pvpProcReady(target, source, AilmentType.BLEED)) return false;
|
||||
if (!harmfulApplicationValid(target, source) || !canBleed(target)) return false;
|
||||
MobEffect effect = ModEffects.BLEED.get();
|
||||
MobEffectInstance current = target.getEffect(effect);
|
||||
int amplifier = current == null ? 0 : Math.min(current.getAmplifier() + 1,
|
||||
@@ -250,17 +273,18 @@ public final class AilmentApi {
|
||||
EffectSourceUtil.setPotency(target, EffectSourceUtil.BLEED,
|
||||
AilmentScaling.getPotencyMultiplier(source, AilmentType.BLEED));
|
||||
}
|
||||
return finishApplication(applied, target, source, AilmentType.BLEED);
|
||||
return applied;
|
||||
}
|
||||
|
||||
/** Applies an explicit, capped Bleed amplifier and source-scaled requested duration. */
|
||||
public static boolean applyBleed(LivingEntity target, @Nullable LivingEntity source,
|
||||
int requestedTicks, int amplifier) {
|
||||
if (!serverValid(target) || !canBleed(target) || !pvpProcReady(target, source, AilmentType.BLEED)) return false;
|
||||
if (!harmfulApplicationValid(target, source) || !canBleed(target) || requestedTicks < 1 || amplifier < 0)
|
||||
return false;
|
||||
int capped = Math.min(Math.max(0, amplifier),
|
||||
AilmentsConfig.value(AilmentsConfig.BLEED_MAX_AMPLIFIER));
|
||||
return finishApplication(applyDot(target, source, ModEffects.BLEED.get(), EffectSourceUtil.BLEED,
|
||||
requestedTicks, capped, AilmentType.BLEED), target, source, AilmentType.BLEED);
|
||||
return applyDot(target, source, ModEffects.BLEED.get(), EffectSourceUtil.BLEED,
|
||||
requestedTicks, capped, AilmentType.BLEED);
|
||||
}
|
||||
|
||||
/** Applies one Fracture stack and refreshes its configured, source-scaled duration. */
|
||||
@@ -279,8 +303,8 @@ public final class AilmentApi {
|
||||
*/
|
||||
public static boolean applyFracture(LivingEntity target, @Nullable LivingEntity source,
|
||||
int requestedTicks, int amplifier) {
|
||||
if (!serverValid(target) || !canFracture(target) || requestedTicks < 1 || amplifier < 0
|
||||
|| !pvpProcReady(target, source, AilmentType.FRACTURE)) return false;
|
||||
if (!harmfulApplicationValid(target, source) || !canFracture(target) || requestedTicks < 1 || amplifier < 0)
|
||||
return false;
|
||||
MobEffect effect = ModEffects.FRACTURE.get();
|
||||
MobEffectInstance current = target.getEffect(effect);
|
||||
int stackedAmplifier = current == null ? 0 : Math.min(2, current.getAmplifier() + 1);
|
||||
@@ -288,57 +312,55 @@ public final class AilmentApi {
|
||||
int duration = AilmentScaling.scaleDuration(requestedTicks, source);
|
||||
boolean applied = addStackingEffect(target, source, effect, duration, appliedAmplifier, true);
|
||||
if (applied) EffectSourceUtil.setSource(target, EffectSourceUtil.FRACTURE, source);
|
||||
return finishApplication(applied, target, source, AilmentType.FRACTURE);
|
||||
return applied;
|
||||
}
|
||||
|
||||
public static boolean applyFear(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) {
|
||||
if (!hostileControlValid(target, source) || MentalControlUtil.isMentalControlImmune(target)
|
||||
|| target.getMaxHealth() > AilmentsConfig.value(AilmentsConfig.FEAR_IMMUNITY_HEALTH)
|
||||
|| !pvpProcReady(target, source, AilmentType.FEAR)) return false;
|
||||
if (!hostileControlValid(target, source) || requestedTicks < 1
|
||||
|| MentalControlUtil.isMentalControlImmune(target)
|
||||
|| target.getMaxHealth() > AilmentsConfig.value(AilmentsConfig.FEAR_IMMUNITY_HEALTH)) return false;
|
||||
int duration = AilmentScaling.scaleDuration(requestedTicks, source);
|
||||
if (target.getMaxHealth() > AilmentsConfig.value(AilmentsConfig.FEAR_REDUCED_HEALTH)) duration /= 2;
|
||||
if (MentalControlUtil.isMentalControlResistant(target))
|
||||
duration = MentalControlUtil.resistantDuration(duration, 3);
|
||||
if (target instanceof EnderMan) duration = Math.max(1, duration / 2);
|
||||
return finishApplication(applySourced(target, source, ModEffects.FEAR.get(), EffectSourceUtil.FEAR,
|
||||
Math.max(1, duration), 0), target, source, AilmentType.FEAR);
|
||||
return applySourced(target, source, ModEffects.FEAR.get(), EffectSourceUtil.FEAR,
|
||||
Math.max(1, duration), 0);
|
||||
}
|
||||
|
||||
public static boolean applyCharm(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) {
|
||||
if (!hostileControlValid(target, source) || MentalControlUtil.isMentalControlImmune(target)
|
||||
|| !pvpProcReady(target, source, AilmentType.CHARM)) return false;
|
||||
if (!hostileControlValid(target, source) || requestedTicks < 1
|
||||
|| MentalControlUtil.isMentalControlImmune(target)) return false;
|
||||
int duration = AilmentScaling.scaleDuration(requestedTicks, source);
|
||||
if (MentalControlUtil.isMentalControlResistant(target))
|
||||
duration = MentalControlUtil.resistantDuration(duration, 2);
|
||||
return finishApplication(applySourced(target, source, ModEffects.CHARM.get(), EffectSourceUtil.CHARM,
|
||||
duration, 0), target, source, AilmentType.CHARM);
|
||||
return applySourced(target, source, ModEffects.CHARM.get(), EffectSourceUtil.CHARM, duration, 0);
|
||||
}
|
||||
|
||||
public static boolean applyTaunt(LivingEntity target, @Nullable LivingEntity source, int requestedTicks, int amplifier) {
|
||||
if (!hostileControlValid(target, source) || MentalControlUtil.isMentalControlImmune(target)
|
||||
|| !pvpProcReady(target, source, AilmentType.TAUNT)) return false;
|
||||
if (!hostileControlValid(target, source) || requestedTicks < 1 || amplifier < 0
|
||||
|| MentalControlUtil.isMentalControlImmune(target)) return false;
|
||||
int duration = AilmentScaling.scaleDuration(requestedTicks, source);
|
||||
if (MentalControlUtil.isMentalControlResistant(target))
|
||||
duration = MentalControlUtil.resistantDuration(duration, 2);
|
||||
return finishApplication(applySourced(target, source, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT,
|
||||
duration, Math.max(0, amplifier)), target, source, AilmentType.TAUNT);
|
||||
return applySourced(target, source, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT,
|
||||
duration, amplifier);
|
||||
}
|
||||
|
||||
public static boolean applyOvercharm(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) {
|
||||
if (!serverValid(target) || !pvpProcReady(target, source, AilmentType.OVERCHARM)) return false;
|
||||
if (!applicationValid(target, source) || requestedTicks < 1) return false;
|
||||
int duration = AilmentScaling.scaleDuration(requestedTicks, source);
|
||||
boolean result = addSourcedEffect(target, source, ModEffects.OVERCHARM.get(), duration, 0, true);
|
||||
if (result) {
|
||||
EffectSourceUtil.setSource(target, EffectSourceUtil.OVERCHARM, source);
|
||||
}
|
||||
return finishApplication(result, target, source, AilmentType.OVERCHARM);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static boolean applyWinded(LivingEntity target, @Nullable LivingEntity source, int requestedTicks) {
|
||||
if (!serverValid(target) || !pvpProcReady(target, source, AilmentType.WINDED)) return false;
|
||||
if (!harmfulApplicationValid(target, source) || requestedTicks < 1) return false;
|
||||
int duration = AilmentScaling.scaleDuration(requestedTicks, source);
|
||||
return finishApplication(addSourcedEffect(target, source, ModEffects.WINDED.get(), duration, 0, true),
|
||||
target, source, AilmentType.WINDED);
|
||||
return addSourcedEffect(target, source, ModEffects.WINDED.get(), duration, 0, true);
|
||||
}
|
||||
|
||||
public static boolean canBleed(LivingEntity target) {
|
||||
@@ -370,7 +392,9 @@ public final class AilmentApi {
|
||||
public static double getHemorrhageFraction(LivingEntity target) {
|
||||
return HemorrhageTracker.getFraction(target);
|
||||
}
|
||||
public static void clearHemorrhage(LivingEntity target) { HemorrhageTracker.clear(target); }
|
||||
public static void clearHemorrhage(LivingEntity target) {
|
||||
if (target != null && !target.level().isClientSide) HemorrhageTracker.clear(target);
|
||||
}
|
||||
@Nullable public static LivingEntity getCharmSource(LivingEntity target) {
|
||||
return EffectSourceUtil.getSource(target, EffectSourceUtil.CHARM);
|
||||
}
|
||||
@@ -392,6 +416,7 @@ public final class AilmentApi {
|
||||
|
||||
/** Removes all registered ailments and all associated source/potency metadata. */
|
||||
public static void clearAll(LivingEntity target) {
|
||||
if (target == null || target.level().isClientSide) return;
|
||||
target.removeEffect(ModEffects.SOUL_ROT.get()); target.removeEffect(ModEffects.BLEED.get());
|
||||
target.removeEffect(ModEffects.FEAR.get()); target.removeEffect(ModEffects.CHARM.get());
|
||||
target.removeEffect(ModEffects.TAUNT.get()); target.removeEffect(ModEffects.OVERCHARM.get());
|
||||
@@ -404,6 +429,7 @@ public final class AilmentApi {
|
||||
EffectSourceUtil.clear(target, EffectSourceUtil.TAUNT);
|
||||
EffectSourceUtil.clear(target, EffectSourceUtil.OVERCHARM);
|
||||
EffectSourceUtil.clear(target, EffectSourceUtil.FRACTURE);
|
||||
ControlEffectHelper.clearRuntimeState(target);
|
||||
HemorrhageTracker.clear(target);
|
||||
}
|
||||
|
||||
@@ -446,13 +472,17 @@ public final class AilmentApi {
|
||||
private static boolean addSourcedEffect(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.isInfiniteDuration() || previous.getAmplifier() > amplifier)) return false;
|
||||
if (previous == null || previous.getAmplifier() == amplifier)
|
||||
return target.addEffect(instance(effect, duration, amplifier, ambient, visible, icon), source);
|
||||
|
||||
// Source metadata has one authoritative owner, so never allow vanilla to retain the previous source as a
|
||||
// hidden fallback. A snapshot provides a real rollback even after the live duration has been set to zero.
|
||||
MobEffectInstance snapshot = new MobEffectInstance(previous);
|
||||
previous.mapDuration(ignored -> 0);
|
||||
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.
|
||||
MobEffectInstance active = target.getEffect(effect);
|
||||
if (active != null && active.getAmplifier() == amplifier) active.mapDuration(ignored -> duration);
|
||||
}
|
||||
if (!applied && target.getEffect(effect) == previous) previous.update(snapshot);
|
||||
return applied;
|
||||
}
|
||||
private static boolean addStackingEffect(LivingEntity target, @Nullable LivingEntity source, MobEffect effect,
|
||||
@@ -462,18 +492,7 @@ public final class AilmentApi {
|
||||
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, ambient, visible, icon);
|
||||
|
||||
// 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, 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) {
|
||||
@@ -499,9 +518,18 @@ public final class AilmentApi {
|
||||
return target != null && !target.level().isClientSide && target.isAlive()
|
||||
&& (!(target instanceof Player player) || !player.isSpectator());
|
||||
}
|
||||
private static boolean applicationValid(LivingEntity target, @Nullable LivingEntity source) {
|
||||
if (!serverValid(target)) return false;
|
||||
return source == null || source.isAlive() && !source.isSpectator()
|
||||
&& !source.level().isClientSide && source.level() == target.level();
|
||||
}
|
||||
private static boolean harmfulApplicationValid(LivingEntity target, @Nullable LivingEntity source) {
|
||||
if (!applicationValid(target, source)) return false;
|
||||
return !(source instanceof Player sourcePlayer && target instanceof Player targetPlayer)
|
||||
|| source == target || sourcePlayer.canHarmPlayer(targetPlayer);
|
||||
}
|
||||
private static boolean hostileControlValid(LivingEntity target, @Nullable LivingEntity source) {
|
||||
if (!serverValid(target) || source == null || !source.isAlive()
|
||||
|| source.level().isClientSide || source.isSpectator()) return false;
|
||||
if (source == null || !applicationValid(target, source)) return false;
|
||||
return !(source instanceof Player sourcePlayer && target instanceof Player targetPlayer)
|
||||
|| sourcePlayer.canHarmPlayer(targetPlayer);
|
||||
}
|
||||
@@ -513,22 +541,21 @@ public final class AilmentApi {
|
||||
return (int) Math.min(Integer.MAX_VALUE, (long) a + b);
|
||||
}
|
||||
private static boolean pvpProcReady(LivingEntity target, @Nullable LivingEntity source, AilmentType type) {
|
||||
if (source != null && source.level() != target.level()) return false;
|
||||
if (type == AilmentType.OVERCHARM ? !applicationValid(target, source)
|
||||
: !harmfulApplicationValid(target, source)) return false;
|
||||
if (!(source instanceof Player sourcePlayer) || !(target instanceof Player targetPlayer) || source == target)
|
||||
return true;
|
||||
if (!sourcePlayer.canHarmPlayer(targetPlayer)) return false;
|
||||
String key = pvpCooldownKey(type);
|
||||
long now = source.level().getGameTime();
|
||||
return !source.getPersistentData().contains(key)
|
||||
|| now - source.getPersistentData().getLong(key)
|
||||
>= AilmentsConfig.value(AilmentsConfig.PVP_PROC_COOLDOWN_TICKS);
|
||||
}
|
||||
private static boolean finishApplication(boolean applied, LivingEntity target,
|
||||
private static void recordPvpProc(boolean applied, LivingEntity target,
|
||||
@Nullable LivingEntity source, AilmentType type) {
|
||||
if (applied && source instanceof Player && target instanceof Player && source != target) {
|
||||
source.getPersistentData().putLong(pvpCooldownKey(type), source.level().getGameTime());
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
private static String pvpCooldownKey(AilmentType type) {
|
||||
return "ascend_ailments.pvp_proc." + type.name().toLowerCase(java.util.Locale.ROOT);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.fretux.ailments.compat;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraftforge.common.capabilities.Capability;
|
||||
@@ -9,11 +10,15 @@ import net.minecraftforge.fml.ModList;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Optional;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
/** Reflection boundary: no Ascend type appears in a loadable signature or constant-pool class reference. */
|
||||
public final class AscendCompat {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final String PROVIDER = "net.fretux.ascend.player.PlayerStatsProvider";
|
||||
private static boolean resolved;
|
||||
private static volatile boolean resolutionAttempted;
|
||||
private static boolean available;
|
||||
private static boolean invocationFailureLogged;
|
||||
private static Capability<?> statsCapability;
|
||||
private static Method getAttributeLevel;
|
||||
|
||||
@@ -22,30 +27,27 @@ public final class AscendCompat {
|
||||
/** Resolves the optional public API without requiring an entity; useful for lifecycle diagnostics. */
|
||||
public static boolean validateIntegration() {
|
||||
if (!isAscendLoaded()) return false;
|
||||
try {
|
||||
resolve();
|
||||
return statsCapability != null && getAttributeLevel != null;
|
||||
} catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) {
|
||||
return false;
|
||||
}
|
||||
return resolve();
|
||||
}
|
||||
|
||||
public static int getArcane(LivingEntity source) {
|
||||
if (!(source instanceof Player player) || !isAscendLoaded()) return 0;
|
||||
try {
|
||||
resolve();
|
||||
if (statsCapability == null || getAttributeLevel == null) return 0;
|
||||
if (!resolve()) return 0;
|
||||
LazyOptional<?> lazy = player.getCapability(statsCapability);
|
||||
Optional<?> stats = lazy.resolve();
|
||||
if (stats.isEmpty()) return 0;
|
||||
return Math.max(0, ((Number) getAttributeLevel.invoke(stats.get(), "arcane")).intValue());
|
||||
} catch (ReflectiveOperationException | LinkageError | RuntimeException ignored) {
|
||||
} catch (ReflectiveOperationException | LinkageError | RuntimeException error) {
|
||||
disableAfterInvocationFailure(error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized void resolve() throws ReflectiveOperationException {
|
||||
if (resolved) return;
|
||||
private static synchronized boolean resolve() {
|
||||
if (resolutionAttempted) return available;
|
||||
resolutionAttempted = true;
|
||||
try {
|
||||
Class<?> provider = Class.forName(PROVIDER, false, AscendCompat.class.getClassLoader());
|
||||
Field capabilityField = provider.getField("PLAYER_STATS");
|
||||
Capability<?> resolvedCapability = (Capability<?>) capabilityField.get(null);
|
||||
@@ -59,7 +61,18 @@ public final class AscendCompat {
|
||||
throw new NoSuchMethodException("getAttributeLevel(String) does not return a number");
|
||||
statsCapability = resolvedCapability;
|
||||
getAttributeLevel = resolvedGetter;
|
||||
resolved = true;
|
||||
available = true;
|
||||
} catch (ReflectiveOperationException | LinkageError | RuntimeException error) {
|
||||
LOGGER.warn("Ascend is loaded, but its player-stats integration is incompatible; Arcane scaling is disabled", error);
|
||||
}
|
||||
return available;
|
||||
}
|
||||
|
||||
private static synchronized void disableAfterInvocationFailure(Throwable error) {
|
||||
available = false;
|
||||
if (invocationFailureLogged) return;
|
||||
invocationFailureLogged = true;
|
||||
LOGGER.warn("Ascend Arcane lookup failed; affected applications will use neutral scaling", error);
|
||||
}
|
||||
private AscendCompat() {}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package net.fretux.ailments.event;
|
||||
|
||||
import net.fretux.ailments.AscendAilments;
|
||||
import net.fretux.ailments.api.AilmentApi;
|
||||
import net.fretux.ailments.api.AilmentApplication;
|
||||
import net.fretux.ailments.api.AilmentType;
|
||||
import net.fretux.ailments.config.AilmentsConfig;
|
||||
import net.fretux.ailments.damage.ModDamageSources;
|
||||
import net.fretux.ailments.registry.ModEffects;
|
||||
@@ -69,13 +71,24 @@ public final class AilmentEvents {
|
||||
Entity attacker = event.getSource().getEntity();
|
||||
if (attacker instanceof LivingEntity living && !(living instanceof Player)
|
||||
&& living.hasEffect(ModEffects.CHARM.get())
|
||||
&& ControlEffectHelper.isOwnCharmSource(living, event.getEntity())) event.setCanceled(true);
|
||||
&& ControlEffectHelper.isOwnCharmSource(living, event.getEntity())) {
|
||||
LivingEntity taunter = living.hasEffect(ModEffects.TAUNT.get())
|
||||
&& !MentalControlUtil.isMentalControlResistant(living)
|
||||
? ControlEffectHelper.validTauntSource(living) : null;
|
||||
if (taunter == null || !taunter.getUUID().equals(event.getEntity().getUUID())) event.setCanceled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onChangeTarget(LivingChangeTargetEvent event) {
|
||||
if (event.getEntity().level().isClientSide) return;
|
||||
if (!(event.getEntity() instanceof Mob mob)) return;
|
||||
LivingEntity fearSource = EffectSourceUtil.getSourceInLevel(mob, EffectSourceUtil.FEAR);
|
||||
if (mob.hasEffect(ModEffects.FEAR.get()) && !MentalControlUtil.isMentalControlResistant(mob)
|
||||
&& fearSource != null && fearSource.isAlive()) {
|
||||
event.setNewTarget(null);
|
||||
return;
|
||||
}
|
||||
LivingEntity taunter = mob.hasEffect(ModEffects.TAUNT.get())
|
||||
&& !MentalControlUtil.isMentalControlResistant(mob)
|
||||
? ControlEffectHelper.validTauntSource(mob) : null;
|
||||
@@ -106,7 +119,9 @@ public final class AilmentEvents {
|
||||
&& ControlEffectHelper.isOwnCharmSource(victim, attacker))
|
||||
amount *= (float) AilmentsConfig.value(AilmentsConfig.CHARM_VICTIM_BONUS);
|
||||
if (attacker != null && attacker.hasEffect(ModEffects.CHARM.get())) {
|
||||
amount *= (float) (ControlEffectHelper.isOwnCharmSource(attacker, victim)
|
||||
LivingEntity charmSource = AilmentApi.getCharmSource(attacker);
|
||||
if (charmSource != null)
|
||||
amount *= (float) (charmSource.getUUID().equals(victim.getUUID())
|
||||
? AilmentsConfig.value(AilmentsConfig.CHARM_SOURCE_PENALTY)
|
||||
: AilmentsConfig.value(AilmentsConfig.CHARM_OTHER_BONUS));
|
||||
}
|
||||
@@ -142,8 +157,8 @@ public final class AilmentEvents {
|
||||
if (!(causing instanceof LivingEntity attacker) || attacker == victim) return;
|
||||
|
||||
if (attacker.hasEffect(ModEffects.OVERCHARM.get()))
|
||||
AilmentApi.applyCharm(victim, attacker,
|
||||
AilmentsConfig.value(AilmentsConfig.OVERCHARM_CHARM_DURATION));
|
||||
AilmentApi.applyProcEffect(victim, attacker, AilmentType.CHARM,
|
||||
AilmentsConfig.value(AilmentsConfig.OVERCHARM_CHARM_DURATION), 0);
|
||||
if (damageSource.getDirectEntity() == attacker)
|
||||
applyTaggedWeaponAilments(attacker.getMainHandItem(), victim, attacker);
|
||||
}
|
||||
@@ -152,16 +167,23 @@ public final class AilmentEvents {
|
||||
if (stack.isEmpty()) return;
|
||||
if (isAutomaticBleedWeapon(stack) && attacker.getRandom().nextDouble()
|
||||
< AilmentsConfig.value(AilmentsConfig.AUTOMATIC_WEAPON_BLEED_CHANCE))
|
||||
AilmentApi.applyBleed(victim, attacker);
|
||||
if (stack.is(ModItemTags.SOUL_ROT_ON_HIT)) AilmentApi.applySoulRot(victim, attacker);
|
||||
if (stack.is(ModItemTags.FRACTURE_ON_HIT)) AilmentApi.applyFracture(victim, attacker);
|
||||
AilmentApi.applyProcEffect(victim, attacker, AilmentApplication.stack(AilmentType.BLEED));
|
||||
if (stack.is(ModItemTags.SOUL_ROT_ON_HIT))
|
||||
AilmentApi.applyProcEffect(victim, attacker, AilmentApplication.stack(AilmentType.SOUL_ROT));
|
||||
if (stack.is(ModItemTags.FRACTURE_ON_HIT))
|
||||
AilmentApi.applyProcEffect(victim, attacker, AilmentApplication.stack(AilmentType.FRACTURE));
|
||||
|
||||
int duration = AilmentsConfig.value(AilmentsConfig.TAGGED_WEAPON_EFFECT_DURATION);
|
||||
if (stack.is(ModItemTags.FEAR_ON_HIT)) AilmentApi.applyFear(victim, attacker, duration);
|
||||
if (stack.is(ModItemTags.CHARM_ON_HIT)) AilmentApi.applyCharm(victim, attacker, duration);
|
||||
if (stack.is(ModItemTags.TAUNT_ON_HIT)) AilmentApi.applyTaunt(victim, attacker, duration, 0);
|
||||
if (stack.is(ModItemTags.OVERCHARM_ON_HIT)) AilmentApi.applyOvercharm(victim, attacker, duration);
|
||||
if (stack.is(ModItemTags.WINDED_ON_HIT)) AilmentApi.applyWinded(victim, attacker, duration);
|
||||
if (stack.is(ModItemTags.FEAR_ON_HIT))
|
||||
AilmentApi.applyProcEffect(victim, attacker, AilmentType.FEAR, duration, 0);
|
||||
if (stack.is(ModItemTags.CHARM_ON_HIT))
|
||||
AilmentApi.applyProcEffect(victim, attacker, AilmentType.CHARM, duration, 0);
|
||||
if (stack.is(ModItemTags.TAUNT_ON_HIT))
|
||||
AilmentApi.applyProcEffect(victim, attacker, AilmentType.TAUNT, duration, 0);
|
||||
if (stack.is(ModItemTags.OVERCHARM_ON_HIT))
|
||||
AilmentApi.applyProcEffect(victim, attacker, AilmentType.OVERCHARM, duration, 0);
|
||||
if (stack.is(ModItemTags.WINDED_ON_HIT))
|
||||
AilmentApi.applyProcEffect(victim, attacker, AilmentType.WINDED, duration, 0);
|
||||
}
|
||||
|
||||
private static boolean isAutomaticBleedWeapon(ItemStack stack) {
|
||||
|
||||
@@ -26,16 +26,41 @@ public final class ControlEffectHelper {
|
||||
|
||||
public static void handleLivingTick(LivingEntity entity) {
|
||||
if (entity.level().isClientSide) return;
|
||||
cleanDotData(entity);
|
||||
handleSoulRotFood(entity);
|
||||
CompoundTag data = entity.getPersistentData();
|
||||
boolean soulRotRelevant = entity.hasEffect(ModEffects.SOUL_ROT.get())
|
||||
|| EffectSourceUtil.hasData(entity, EffectSourceUtil.SOUL_ROT)
|
||||
|| data.contains(SOUL_FOOD) || data.contains(SOUL_SATURATION);
|
||||
boolean bleedRelevant = entity.hasEffect(ModEffects.BLEED.get())
|
||||
|| EffectSourceUtil.hasData(entity, EffectSourceUtil.BLEED) || HemorrhageTracker.hasProgress(entity);
|
||||
boolean fractureRelevant = entity.hasEffect(ModEffects.FRACTURE.get())
|
||||
|| EffectSourceUtil.hasData(entity, EffectSourceUtil.FRACTURE);
|
||||
boolean controlRelevant = entity.hasEffect(ModEffects.FEAR.get()) || entity.hasEffect(ModEffects.TAUNT.get())
|
||||
|| entity.hasEffect(ModEffects.CHARM.get()) || entity.hasEffect(ModEffects.OVERCHARM.get())
|
||||
|| EffectSourceUtil.hasData(entity, EffectSourceUtil.FEAR)
|
||||
|| EffectSourceUtil.hasData(entity, EffectSourceUtil.TAUNT)
|
||||
|| EffectSourceUtil.hasData(entity, EffectSourceUtil.CHARM)
|
||||
|| EffectSourceUtil.hasData(entity, EffectSourceUtil.OVERCHARM);
|
||||
if (!soulRotRelevant && !bleedRelevant && !fractureRelevant && !controlRelevant) return;
|
||||
|
||||
cleanDotData(entity, soulRotRelevant, bleedRelevant, fractureRelevant);
|
||||
if (soulRotRelevant) handleSoulRotFood(entity);
|
||||
if (MentalControlUtil.isMentalControlImmune(entity)) {
|
||||
removeControlEffect(entity, ModEffects.FEAR.get(), EffectSourceUtil.FEAR);
|
||||
removeControlEffect(entity, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT);
|
||||
removeControlEffect(entity, ModEffects.CHARM.get(), EffectSourceUtil.CHARM);
|
||||
}
|
||||
if (entity.hasEffect(ModEffects.FEAR.get())) tickFear(entity);
|
||||
else {
|
||||
EffectSourceUtil.clear(entity, EffectSourceUtil.FEAR);
|
||||
entity.getPersistentData().remove(FEAR_DISTANCE);
|
||||
clearFearPlayerPenalties(entity);
|
||||
}
|
||||
if (entity.hasEffect(ModEffects.TAUNT.get())) tickTaunt(entity);
|
||||
else EffectSourceUtil.clear(entity, EffectSourceUtil.TAUNT);
|
||||
boolean fearDominant = entity.hasEffect(ModEffects.FEAR.get())
|
||||
&& !MentalControlUtil.isMentalControlResistant(entity)
|
||||
&& validControlSource(entity, EffectSourceUtil.FEAR) != null;
|
||||
if (entity.hasEffect(ModEffects.TAUNT.get())) {
|
||||
if (!fearDominant) tickTaunt(entity);
|
||||
} else EffectSourceUtil.clear(entity, EffectSourceUtil.TAUNT);
|
||||
if (entity.hasEffect(ModEffects.CHARM.get())) validateCharm(entity);
|
||||
else EffectSourceUtil.clear(entity, EffectSourceUtil.CHARM);
|
||||
if (!entity.hasEffect(ModEffects.OVERCHARM.get()))
|
||||
@@ -48,9 +73,14 @@ public final class ControlEffectHelper {
|
||||
entity.getPersistentData().remove(FEAR_DISTANCE);
|
||||
return;
|
||||
}
|
||||
LivingEntity source = validControlSource(entity, EffectSourceUtil.FEAR);
|
||||
LivingEntity loadedSource = EffectSourceUtil.getSourceInLevel(entity, EffectSourceUtil.FEAR);
|
||||
if (loadedSource != null && !loadedSource.isAlive()) {
|
||||
removeControlEffect(entity, ModEffects.FEAR.get(), EffectSourceUtil.FEAR);
|
||||
entity.getPersistentData().remove(FEAR_DISTANCE);
|
||||
return;
|
||||
}
|
||||
LivingEntity source = loadedSource;
|
||||
if (source == null) {
|
||||
if (entity instanceof Mob mob) mob.getNavigation().stop();
|
||||
entity.getPersistentData().remove(FEAR_DISTANCE);
|
||||
clearFearPlayerPenalties(entity);
|
||||
return;
|
||||
@@ -74,6 +104,7 @@ public final class ControlEffectHelper {
|
||||
if (horizontal.lengthSqr() < 1.0E-8) horizontal = new Vec3(1, 0, 0);
|
||||
Vec3 destination = mob.position().add(horizontal.normalize()
|
||||
.scale(AilmentsConfig.value(AilmentsConfig.FEAR_FLEE_DISTANCE)));
|
||||
if (mob.tickCount % 10 == 0)
|
||||
mob.getNavigation().moveTo(destination.x, destination.y, destination.z,
|
||||
AilmentsConfig.value(AilmentsConfig.FEAR_FLEE_SPEED));
|
||||
}
|
||||
@@ -107,11 +138,13 @@ public final class ControlEffectHelper {
|
||||
removeControlEffect(entity, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT);
|
||||
return;
|
||||
}
|
||||
LivingEntity source = validControlSource(entity, EffectSourceUtil.TAUNT);
|
||||
if (source == null) {
|
||||
if (entity instanceof Mob mob && mob.getTarget() != null) mob.setTarget(null);
|
||||
LivingEntity loadedSource = EffectSourceUtil.getSourceInLevel(entity, EffectSourceUtil.TAUNT);
|
||||
if (loadedSource != null && !loadedSource.isAlive()) {
|
||||
removeControlEffect(entity, ModEffects.TAUNT.get(), EffectSourceUtil.TAUNT);
|
||||
return;
|
||||
}
|
||||
LivingEntity source = loadedSource;
|
||||
if (source == null) return;
|
||||
if (MentalControlUtil.isMentalControlResistant(entity)) return;
|
||||
if (entity instanceof Mob mob && mob.getTarget() != source) mob.setTarget(source);
|
||||
else if (entity instanceof Player player)
|
||||
@@ -121,17 +154,31 @@ public final class ControlEffectHelper {
|
||||
private static void validateCharm(LivingEntity entity) {
|
||||
if (EffectSourceUtil.getSourceUuid(entity, EffectSourceUtil.CHARM) == null)
|
||||
removeControlEffect(entity, ModEffects.CHARM.get(), EffectSourceUtil.CHARM);
|
||||
else {
|
||||
LivingEntity source = EffectSourceUtil.getSourceInLevel(entity, EffectSourceUtil.CHARM);
|
||||
if (source != null && !source.isAlive())
|
||||
removeControlEffect(entity, ModEffects.CHARM.get(), EffectSourceUtil.CHARM);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isOwnCharmSource(LivingEntity charmed, LivingEntity possibleSource) {
|
||||
LivingEntity source = EffectSourceUtil.getSource(charmed, EffectSourceUtil.CHARM);
|
||||
return source != null && source.getUUID().equals(possibleSource.getUUID());
|
||||
UUID sourceId = EffectSourceUtil.getSourceUuid(charmed, EffectSourceUtil.CHARM);
|
||||
return sourceId != null && sourceId.equals(possibleSource.getUUID());
|
||||
}
|
||||
|
||||
public static LivingEntity validTauntSource(LivingEntity entity) {
|
||||
return validControlSource(entity, EffectSourceUtil.TAUNT);
|
||||
}
|
||||
|
||||
/** Clears transient state that is not owned by the vanilla MobEffect instance itself. */
|
||||
public static void clearRuntimeState(LivingEntity entity) {
|
||||
CompoundTag data = entity.getPersistentData();
|
||||
data.remove(FEAR_DISTANCE);
|
||||
data.remove(SOUL_FOOD);
|
||||
data.remove(SOUL_SATURATION);
|
||||
clearFearPlayerPenalties(entity);
|
||||
}
|
||||
|
||||
private static LivingEntity validControlSource(LivingEntity target, String key) {
|
||||
LivingEntity source = EffectSourceUtil.getSourceInLevel(target, key);
|
||||
return source != null && source.isAlive() ? source : null;
|
||||
@@ -180,24 +227,25 @@ public final class ControlEffectHelper {
|
||||
data.putFloat(SOUL_SATURATION, food.getSaturationLevel());
|
||||
}
|
||||
|
||||
private static void cleanDotData(LivingEntity entity) {
|
||||
if (!AilmentApi.canSoulRot(entity)) {
|
||||
private static void cleanDotData(LivingEntity entity, boolean soulRotRelevant, boolean bleedRelevant,
|
||||
boolean fractureRelevant) {
|
||||
if (soulRotRelevant && !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())) {
|
||||
} else if (soulRotRelevant && !entity.hasEffect(ModEffects.SOUL_ROT.get())) {
|
||||
EffectSourceUtil.clear(entity, EffectSourceUtil.SOUL_ROT);
|
||||
}
|
||||
if (!AilmentApi.canBleed(entity)) {
|
||||
if (bleedRelevant && !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())) {
|
||||
} else if (bleedRelevant && !entity.hasEffect(ModEffects.BLEED.get())) {
|
||||
EffectSourceUtil.clear(entity, EffectSourceUtil.BLEED);
|
||||
}
|
||||
if (!AilmentApi.canFracture(entity)) {
|
||||
if (fractureRelevant && !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())) {
|
||||
} else if (fractureRelevant && !entity.hasEffect(ModEffects.FRACTURE.get())) {
|
||||
EffectSourceUtil.clear(entity, EffectSourceUtil.FRACTURE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,12 @@ public final class EffectSourceUtil {
|
||||
return data.contains(key) ? Math.max(0, data.getFloat(key)) : 1.0F;
|
||||
}
|
||||
|
||||
/** Returns whether source or potency metadata exists for an ailment without resolving an entity. */
|
||||
public static boolean hasData(LivingEntity target, String ailment) {
|
||||
CompoundTag data = target.getPersistentData();
|
||||
return data.contains(sourceKey(ailment)) || data.contains(potencyKey(ailment));
|
||||
}
|
||||
|
||||
public static void clear(LivingEntity target, String ailment) {
|
||||
target.getPersistentData().remove(sourceKey(ailment));
|
||||
target.getPersistentData().remove(potencyKey(ailment));
|
||||
|
||||
@@ -54,6 +54,10 @@ public final class HemorrhageTracker {
|
||||
return data.contains(PROGRESS) ? Math.max(0.0, data.getDouble(PROGRESS)) : 0.0;
|
||||
}
|
||||
|
||||
public static boolean hasProgress(LivingEntity target) {
|
||||
return target.getPersistentData().contains(PROGRESS);
|
||||
}
|
||||
|
||||
public static double getFraction(LivingEntity target) {
|
||||
double threshold = AilmentsConfig.value(AilmentsConfig.HEMORRHAGE_THRESHOLD);
|
||||
return threshold <= 0 ? 0.0 : Math.min(1.0, getProgress(target) / threshold);
|
||||
|
||||
@@ -75,7 +75,7 @@ description='''${mod_description}'''
|
||||
[[dependencies.${mod_id}]]
|
||||
modId="ascend"
|
||||
mandatory=false
|
||||
versionRange="[0,)"
|
||||
versionRange="[3.0.7,4)"
|
||||
ordering="AFTER"
|
||||
side="BOTH"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user