fix various issues

This commit is contained in:
2026-07-20 22:41:41 +02:00
parent 3b150bcf7d
commit afd98209f1
5 changed files with 122 additions and 31 deletions
+2 -1
View File
@@ -95,7 +95,8 @@ minecraft {
// By default, the server will crash when no gametests are provided.
// The gametest system is also enabled by default for other run configs under the /test command.
gameTestServer {
property 'forge.enabledGameTestNamespaces', mod_id
// The tests use a small vanilla structure template, so its namespace must also be enabled.
property 'forge.enabledGameTestNamespaces', "${mod_id},minecraft"
}
data {
@@ -27,6 +27,7 @@ import net.minecraft.world.entity.monster.EnderMan;
import net.minecraft.world.entity.monster.Endermite;
import net.minecraft.world.entity.monster.Evoker;
import net.minecraft.world.entity.monster.Guardian;
import net.minecraft.world.entity.monster.MagmaCube;
import net.minecraft.world.entity.monster.Ghast;
import net.minecraft.world.entity.monster.Phantom;
import net.minecraft.world.entity.monster.Pillager;
@@ -55,6 +56,7 @@ import net.minecraft.world.phys.Vec3;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.EntityJoinLevelEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.event.level.ExplosionEvent;
import net.minecraftforge.common.util.BlockSnapshot;
import net.minecraftforge.event.ForgeEventFactory;
@@ -67,6 +69,7 @@ public final class AdaptiveSpecialAbilities {
private static final String COOLDOWN_PREFIX = "am_special_cd_";
private static final String SPLIT_FIREBALL_KEY = "am_split_fireball";
private static final double SPLIT_FIREBALL_SPACING = 1.35D;
private static final List<DelayedReappear> DELAYED_REAPPEARS = new ArrayList<>();
private AdaptiveSpecialAbilities() {
@@ -241,6 +244,10 @@ public final class AdaptiveSpecialAbilities {
if (tier <= 0 || !AdaptiveAIGoalUtils.isValidAdaptiveTarget(player)) {
return;
}
if (mob instanceof MagmaCube && tier >= 3 && mob.getRandom().nextFloat() < 0.50F) {
player.setSecondsOnFire(4);
AdaptiveAIGoalUtils.debug(mob, () -> tier, "magma cube ignited target");
}
if (tier >= 4 && mob instanceof Drowned && mob.isInWater() && player.isInWater() && ready(mob, "dragging_grip", 160)) {
player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 60, 1));
player.setDeltaMovement(player.getDeltaMovement().add(0.0D, -0.22D, 0.0D));
@@ -363,6 +370,7 @@ public final class AdaptiveSpecialAbilities {
if (original.getPersistentData().getBoolean(SPLIT_FIREBALL_KEY)) {
return;
}
original.getPersistentData().putBoolean(SPLIT_FIREBALL_KEY, true);
Vec3 direction = original.getDeltaMovement();
if (direction.lengthSqr() < 1.0E-4D && AdaptiveAIGoalUtils.isValidAdaptiveTarget(ghast.getTarget())) {
direction = ghast.getTarget().getEyePosition().subtract(original.position());
@@ -377,19 +385,28 @@ public final class AdaptiveSpecialAbilities {
} else {
side = side.normalize();
}
spawnSplitFireball(level, ghast, original, direction.add(side.scale(0.18D)).normalize());
spawnSplitFireball(level, ghast, original, direction.subtract(side.scale(0.18D)).normalize());
spawnSplitFireball(level, ghast, original, direction.add(side.scale(0.24D)).normalize(),
side.scale(SPLIT_FIREBALL_SPACING));
spawnSplitFireball(level, ghast, original, direction.subtract(side.scale(0.24D)).normalize(),
side.scale(-SPLIT_FIREBALL_SPACING));
AdaptiveAIGoalUtils.debug(ghast, () -> 5, "ghast splitting fireball");
}
private static void spawnSplitFireball(ServerLevel level, Ghast ghast, LargeFireball original, Vec3 direction) {
private static void spawnSplitFireball(ServerLevel level, Ghast ghast, LargeFireball original,
Vec3 direction, Vec3 offset) {
LargeFireball split = new LargeFireball(level, ghast, direction.x, direction.y, direction.z,
ghast.getExplosionPower());
split.getPersistentData().putBoolean(SPLIT_FIREBALL_KEY, true);
split.setPos(original.getX(), original.getY(), original.getZ());
Vec3 position = original.position().add(offset).add(direction.scale(0.35D));
split.setPos(position.x, position.y, position.z);
level.addFreshEntity(split);
}
public static void protectSplitFireballVolley(ExplosionEvent.Detonate event) {
event.getAffectedEntities().removeIf(entity -> entity instanceof LargeFireball fireball
&& fireball.getPersistentData().getBoolean(SPLIT_FIREBALL_KEY));
}
public static void invalidateTemporaryBlock(ServerLevel level, BlockPos pos) {
TemporaryBlockSavedData.get(level).remove(level, pos);
}
@@ -35,6 +35,14 @@ import java.util.function.IntSupplier;
public class AdaptiveAntiCheeseGoal extends Goal {
private static final double TRAP_VEHICLE_BREAK_DISTANCE_SQR = 9.0D;
private static final double ZOMBIE_TOWER_COORDINATION_RADIUS = 12.0D;
// A tight radius lets body collision create a permanent crowd around the selected tower.
// Four blocks is still local, but allows the mount handoff before the crowd deadlocks.
private static final double ZOMBIE_MOUNT_DISTANCE_SQR = 16.0D;
private static final double ESTABLISHED_TOWER_MOUNT_DISTANCE_SQR =
ZOMBIE_TOWER_COORDINATION_RADIUS * ZOMBIE_TOWER_COORDINATION_RADIUS;
private static final String ZOMBIE_CLIMBER_UNTIL_KEY = "am_zombie_climber_until";
private static final String LADDER_CLIMBER_UNTIL_KEY = "am_ladder_climber_until";
private final PathfinderMob mob;
private final IntSupplier tierSupplier;
@@ -58,6 +66,13 @@ public class AdaptiveAntiCheeseGoal extends Goal {
if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) {
return false;
}
if (mob instanceof Zombie && mob.isPassenger() && AdaptiveAIGoalUtils.isAdaptiveStackedZombie(mob)) {
return true;
}
if (mob instanceof Zombie
&& mob.getPersistentData().getLong(ZOMBIE_CLIMBER_UNTIL_KEY) > mob.level().getGameTime()) {
return false;
}
if (isInTrapVehicle() || nearestTrapBlock() != null || trappedAllyVehicle() != null) {
return true;
}
@@ -92,18 +107,24 @@ public class AdaptiveAntiCheeseGoal extends Goal {
cooldown = 10 + mob.getRandom().nextInt(20);
return;
}
if (mob instanceof Zombie && mob.isPassenger() && shouldUnstack(target)) {
mob.stopRiding();
AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob);
cooldown = 12 + mob.getRandom().nextInt(18);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "zombie unstacking");
return;
}
if (!AdaptiveAIGoalUtils.isValidAdaptiveTarget(target)) {
return;
}
mob.getLookControl().setLookAt(target, 30.0F, 30.0F);
if (mob instanceof Zombie && mob.isPassenger() && AdaptiveAIGoalUtils.isAdaptiveStackedZombie(mob)) {
activePillarTarget = target;
activePillarTicks = 40;
if (shouldUnstack(target)) {
mob.stopRiding();
AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "zombie ladder released after target fell");
} else {
tryCorpseLadderPressure(target);
}
return;
}
if (mob instanceof Zombie && isVerticalObstacleCheese(target)) {
activePillarTarget = target;
activePillarTicks = 24 + mob.getRandom().nextInt(16);
@@ -181,7 +202,8 @@ public class AdaptiveAntiCheeseGoal extends Goal {
return activePillarTicks > 0
&& mob instanceof Zombie
&& AdaptiveAIGoalUtils.isValidAdaptiveTarget(activePillarTarget)
&& isVerticalObstacleCheese(activePillarTarget);
&& ((mob.isPassenger() && AdaptiveAIGoalUtils.isAdaptiveStackedZombie(mob))
|| isVerticalObstacleCheese(activePillarTarget));
}
@Override
@@ -289,25 +311,42 @@ public class AdaptiveAntiCheeseGoal extends Goal {
if (mob.isPassenger() || mob.isVehicle()) {
return false;
}
List<Zombie> carriers = mob.level().getEntitiesOfClass(Zombie.class, mob.getBoundingBox().inflate(2.0D),
List<Zombie> carriers = mob.level().getEntitiesOfClass(Zombie.class,
mob.getBoundingBox().inflate(ZOMBIE_TOWER_COORDINATION_RADIUS),
zombie -> zombie != mob
&& zombie.isAlive()
&& !zombie.isVehicle()
&& zombie.getTarget() == target
&& zombie.getY() < target.getY()
&& !isInVehicleChain(zombie, mob)
&& Math.abs(zombie.getY() - mob.getY()) < 8.0D);
Zombie bestCarrier = null;
double best = -1.0D;
int bestHeight = -1;
int bestBaseId = Integer.MAX_VALUE;
double bestDistance = Double.MAX_VALUE;
for (Zombie carrier : carriers) {
double score = stackHeight(carrier) * 10.0D - mob.distanceToSqr(carrier);
if (score > best) {
best = score;
int height = stackHeight(carrier);
int baseId = stackBase(carrier).getId();
double distance = mob.distanceToSqr(carrier);
if (height > bestHeight
|| (height == bestHeight && baseId < bestBaseId)
|| (height == bestHeight && baseId == bestBaseId && distance < bestDistance)) {
bestHeight = height;
bestBaseId = baseId;
bestDistance = distance;
bestCarrier = carrier;
}
}
if (bestCarrier == null) {
return false;
}
double mountDistanceSqr = bestHeight > 1
? ESTABLISHED_TOWER_MOUNT_DISTANCE_SQR : ZOMBIE_MOUNT_DISTANCE_SQR;
if (bestDistance > mountDistanceSqr) {
mob.getNavigation().moveTo(bestCarrier, 1.1D);
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "zombie converging on existing tower");
return true;
}
boolean mounted = mob.startRiding(bestCarrier, true);
if (mounted) {
AdaptiveAIGoalUtils.markAdaptiveStackedZombie(mob);
@@ -329,7 +368,28 @@ public class AdaptiveAntiCheeseGoal extends Goal {
double vertical = target.getY() - mob.getY();
double horizontal = Math.sqrt((target.getX() - mob.getX()) * (target.getX() - mob.getX())
+ (target.getZ() - mob.getZ()) * (target.getZ() - mob.getZ()));
if (horizontal > 3.2D || vertical < 0.25D || vertical > 5.5D) {
if (mob.isVehicle()) {
return true;
}
if (horizontal <= 3.2D && vertical <= 1.0D) {
long now = mob.level().getGameTime();
if (base.getPersistentData().getLong(LADDER_CLIMBER_UNTIL_KEY) > now) {
return true;
}
base.getPersistentData().putLong(LADDER_CLIMBER_UNTIL_KEY, now + 60L);
mob.stopRiding();
AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob);
mob.getPersistentData().putLong(ZOMBIE_CLIMBER_UNTIL_KEY, now + 80L);
Vec3 toward = target.position().subtract(mob.position());
double len = Math.max(0.001D, Math.sqrt(toward.x * toward.x + toward.z * toward.z));
mob.setDeltaMovement(toward.x / len * 0.30D, 0.34D, toward.z / len * 0.30D);
mob.hurtMarked = true;
mob.getNavigation().moveTo(target, 1.2D);
activePillarTicks = 0;
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "top zombie launched from ladder");
return true;
}
if (horizontal > 3.2D || vertical > 5.5D) {
return true;
}
@@ -339,10 +399,6 @@ public class AdaptiveAntiCheeseGoal extends Goal {
mob.setDeltaMovement(mob.getDeltaMovement().add(toward.x / len * 0.18D, lift, toward.z / len * 0.18D));
mob.hurtMarked = true;
if (vertical <= 1.8D && horizontal <= 2.4D) {
mob.stopRiding();
AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob);
}
AdaptiveAIGoalUtils.debug(mob, tierSupplier, "zombie corpse ladder pressure");
return true;
}
@@ -368,19 +424,18 @@ public class AdaptiveAntiCheeseGoal extends Goal {
AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob);
return true;
}
if (mob.distanceToSqr(target) > 8.0D * 8.0D) {
AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob);
return true;
}
Entity base = stackBase(mob);
double verticalFromBase = target.getY() - base.getY();
double horizontalFromBase = Math.sqrt((target.getX() - base.getX()) * (target.getX() - base.getX())
+ (target.getZ() - base.getZ()) * (target.getZ() - base.getZ()));
if (verticalFromBase > 2.5D && horizontalFromBase < 5.5D) {
// Preserve the ladder while its base approaches an elevated target. Rider-to-target
// distance includes tower height and caused tall, otherwise valid ladders to oscillate.
if (verticalFromBase > 2.5D
&& horizontalFromBase <= ZOMBIE_TOWER_COORDINATION_RADIUS + 4.0D) {
return false;
}
AdaptiveAIGoalUtils.clearAdaptiveStackedZombie(mob);
return !isVerticalObstacleCheese(target);
return true;
}
private int stackHeight(Entity entity) {
@@ -18,6 +18,7 @@ import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.monster.Enemy;
import net.minecraft.world.entity.projectile.Projectile;
import net.minecraft.tags.DamageTypeTags;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.event.entity.living.LivingEntityUseItemEvent;
import net.minecraftforge.event.entity.EntityJoinLevelEvent;
@@ -57,6 +58,17 @@ public class AdaptiveMobsEvents {
@SubscribeEvent
public void onLivingAttack(LivingAttackEvent event) {
if (event.getEntity() instanceof ServerPlayer player
&& event.getSource().getEntity() instanceof Mob explosiveMob
&& event.getSource().is(DamageTypeTags.IS_EXPLOSION)
&& player.level() instanceof ServerLevel level
&& AMConfig.enabled && AMConfig.AI_ENABLED.get()
&& AdaptiveGoalInjector.isAIEnabledFor(explosiveMob)
&& AdaptiveDifficultyManager.getMobTier(level, explosiveMob.getType()) >= 5
&& AdaptiveAIGoalUtils.isBlockingShield(player)) {
player.disableShield(true);
AdaptiveAIGoalUtils.debug(explosiveMob, () -> 5, "mob explosion disabled shield");
}
if (!AMConfig.enabled || !AMConfig.AI_ENABLED.get()
|| !(event.getEntity() instanceof Mob victim) || !(victim instanceof Enemy)
|| !(event.getSource().getDirectEntity() instanceof Projectile)
@@ -182,6 +194,7 @@ public class AdaptiveMobsEvents {
@SubscribeEvent
public void onExplosion(ExplosionEvent.Detonate event) {
AdaptiveSpecialAbilities.protectSplitFireballVolley(event);
if (AMConfig.enabled && AMConfig.ENABLE_SOUND_INVESTIGATION_AI.get()
&& event.getLevel() instanceof ServerLevel level) {
AdaptiveMemory.rememberSound(level, event.getExplosion().getPosition(), AdaptiveMemory.SoundKind.EXPLOSION, 20 * 10);
@@ -9,15 +9,17 @@ import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.monster.Slime;
import net.minecraftforge.gametest.GameTestHolder;
import net.minecraftforge.gametest.PrefixGameTestTemplate;
import java.util.Iterator;
@GameTestHolder("adaptivemobs")
@PrefixGameTestTemplate(false)
public final class AdaptiveServerBehaviorGameTests {
private AdaptiveServerBehaviorGameTests() {
}
@GameTest(template = "empty")
@GameTest(templateNamespace = "minecraft", template = "igloo/top")
public static void temporaryBlocksSurviveSavedDataReload(GameTestHelper helper) {
TemporaryBlockSavedData original = new TemporaryBlockSavedData();
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
@@ -35,12 +37,15 @@ public final class AdaptiveServerBehaviorGameTests {
helper.succeed();
}
@GameTest(template = "empty")
@GameTest(templateNamespace = "minecraft", template = "igloo/top")
public static void slimeRecombinationCancelsWhenStuck(GameTestHelper helper) {
Slime slime = helper.spawn(EntityType.SLIME, new BlockPos(1, 2, 1));
Slime partner = helper.spawn(EntityType.SLIME, new BlockPos(8, 2, 1));
slime.setSize(1, true);
partner.setSize(1, true);
long now = helper.getLevel().getGameTime();
slime.getPersistentData().putLong("am_recombine_ready_at", now);
partner.getPersistentData().putLong("am_recombine_ready_at", now);
AdaptiveSlimeTacticsGoal goal = new AdaptiveSlimeTacticsGoal(slime, () -> 5);
helper.assertTrue(goal.canUse(), "slime did not select its eligible partner");