From 3b150bcf7d37f541b28f61ae7a18475a938bc7ac Mon Sep 17 00:00:00 2001 From: fretuks Date: Mon, 20 Jul 2026 21:07:14 +0200 Subject: [PATCH] fix various issues --- .../adaptivemobs/ai/AdaptiveGoalInjector.java | 5 +- .../ai/AdaptivePositioningUtils.java | 46 ++++++++++- .../ai/AdaptiveSpecialAbilities.java | 47 +++-------- .../ai/goals/AdaptiveCreeperFuseGoal.java | 14 +++- .../ai/goals/AdaptiveSlimeTacticsGoal.java | 81 +++++++++++++++++-- .../fretux/adaptivemobs/config/AMConfig.java | 1 + .../data/TemporaryBlockSavedData.java | 75 +++++++++++++++++ .../event/AdaptiveMobsEvents.java | 3 + .../AdaptiveServerBehaviorGameTests.java | 55 +++++++++++++ 9 files changed, 279 insertions(+), 48 deletions(-) create mode 100644 src/main/java/net/fretux/adaptivemobs/data/TemporaryBlockSavedData.java create mode 100644 src/main/java/net/fretux/adaptivemobs/gametest/AdaptiveServerBehaviorGameTests.java diff --git a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveGoalInjector.java b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveGoalInjector.java index 10b94cb..3a281e3 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveGoalInjector.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveGoalInjector.java @@ -297,7 +297,10 @@ public final class AdaptiveGoalInjector { || mob instanceof Hoglin || mob instanceof Zoglin) { return AMConfig.ENABLE_NETHER_MOB_AI.get() && AMConfig.AI_PIGLIN_FAMILY.get(); } - if (mob instanceof Vindicator || mob instanceof Ravager) { + if (mob instanceof Vindicator) { + return AMConfig.ENABLE_RAID_MOB_AI.get() && AMConfig.AI_VINDICATOR.get(); + } + if (mob instanceof Ravager) { return AMConfig.ENABLE_RAID_MOB_AI.get() && AMConfig.AI_RAVAGER.get(); } if (mob instanceof Silverfish) { diff --git a/src/main/java/net/fretux/adaptivemobs/ai/AdaptivePositioningUtils.java b/src/main/java/net/fretux/adaptivemobs/ai/AdaptivePositioningUtils.java index 36bb7d7..233cb8b 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/AdaptivePositioningUtils.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/AdaptivePositioningUtils.java @@ -7,6 +7,9 @@ import net.minecraft.world.entity.Mob; import net.minecraft.world.level.ClipContext; import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.core.Direction; +import net.minecraft.world.phys.AABB; public final class AdaptivePositioningUtils { @@ -64,6 +67,9 @@ public final class AdaptivePositioningUtils { if (target == null) { return false; } + if (!chunksLoaded(mob, mob.getEyePosition(), target.getEyePosition())) { + return false; + } HitResult hit = mob.level().clip(new ClipContext(mob.getEyePosition(), target.getEyePosition(), ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, mob)); return hit.getType() == HitResult.Type.MISS; @@ -71,9 +77,25 @@ public final class AdaptivePositioningUtils { public static boolean isReasonableGround(Mob mob, Vec3 pos) { BlockPos block = BlockPos.containing(pos); - return mob.level().getWorldBorder().isWithinBounds(block) - && !mob.level().getBlockState(block).liquid() - && mob.level().getBlockState(block).getCollisionShape(mob.level(), block).isEmpty(); + BlockPos below = block.below(); + AABB destinationBox = mob.getBoundingBox().move(pos.subtract(mob.position())); + if (!mob.level().getWorldBorder().isWithinBounds(block) + || !chunksLoaded(mob, new Vec3(destinationBox.minX, destinationBox.minY, destinationBox.minZ), + new Vec3(destinationBox.maxX, destinationBox.maxY, destinationBox.maxZ)) + || !mob.level().hasChunkAt(below)) { + return false; + } + var state = mob.level().getBlockState(block); + var support = mob.level().getBlockState(below); + if (state.liquid() || !state.getCollisionShape(mob.level(), block).isEmpty() + || !support.isFaceSturdy(mob.level(), below, Direction.UP) + || support.is(Blocks.CACTUS) || support.is(Blocks.MAGMA_BLOCK) + || support.is(Blocks.CAMPFIRE) || support.is(Blocks.SOUL_CAMPFIRE) + || support.is(Blocks.POWDER_SNOW) || support.is(Blocks.FIRE) || support.is(Blocks.SOUL_FIRE) + || !mob.level().noCollision(mob, destinationBox)) { + return false; + } + return mob.getNavigation().createPath(block, 0) != null; } public static boolean isOpenToSky(Mob mob) { @@ -81,7 +103,8 @@ public final class AdaptivePositioningUtils { } public static int localLight(Mob mob, Vec3 pos) { - return mob.level().getMaxLocalRawBrightness(BlockPos.containing(pos)); + BlockPos block = BlockPos.containing(pos); + return mob.level().hasChunkAt(block) ? mob.level().getMaxLocalRawBrightness(block) : Integer.MAX_VALUE; } public static Vec3 elevatedPosition(Mob mob, LivingEntity target, double radius) { @@ -94,4 +117,19 @@ public final class AdaptivePositioningUtils { } return base; } + + private static boolean chunksLoaded(Mob mob, Vec3 from, Vec3 to) { + int minChunkX = ((int) Math.floor(Math.min(from.x, to.x))) >> 4; + int maxChunkX = ((int) Math.floor(Math.max(from.x, to.x))) >> 4; + int minChunkZ = ((int) Math.floor(Math.min(from.z, to.z))) >> 4; + int maxChunkZ = ((int) Math.floor(Math.max(from.z, to.z))) >> 4; + for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { + if (!mob.level().hasChunkAt(new BlockPos(chunkX << 4, 0, chunkZ << 4))) { + return false; + } + } + } + return true; + } } diff --git a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java index 223c646..5f2fead 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/AdaptiveSpecialAbilities.java @@ -2,6 +2,7 @@ package net.fretux.adaptivemobs.ai; import net.fretux.adaptivemobs.config.AMConfig; import net.fretux.adaptivemobs.difficulty.AdaptiveDifficultyManager; +import net.fretux.adaptivemobs.data.TemporaryBlockSavedData; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.server.level.ServerLevel; @@ -66,7 +67,6 @@ 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 List TEMPORARY_BLOCKS = new ArrayList<>(); private static final List DELAYED_REAPPEARS = new ArrayList<>(); private AdaptiveSpecialAbilities() { @@ -136,17 +136,20 @@ public final class AdaptiveSpecialAbilities { if (event.phase != TickEvent.Phase.END || event.getServer() == null) { return; } - Iterator blockIterator = TEMPORARY_BLOCKS.iterator(); + TemporaryBlockSavedData temporaryBlocks = TemporaryBlockSavedData.get(event.getServer().overworld()); + Iterator blockIterator = temporaryBlocks.entries(); while (blockIterator.hasNext()) { - TemporaryBlock block = blockIterator.next(); - ServerLevel level = block.server == event.getServer() ? event.getServer().getLevel(block.dimension) : null; - if (level == null || level.getGameTime() < block.expireTick) { + TemporaryBlockSavedData.Entry block = blockIterator.next(); + ServerLevel level = event.getServer().getLevel(net.minecraft.resources.ResourceKey.create( + net.minecraft.core.registries.Registries.DIMENSION, block.dimension())); + if (level == null || level.getGameTime() < block.expireTick() || !level.hasChunkAt(block.pos())) { continue; } - if (level.getBlockState(block.pos).equals(block.placedState)) { - block.replaced.restore(true, false); + if (level.getBlockState(block.pos()).is(Blocks.COBWEB)) { + level.setBlock(block.pos(), Blocks.AIR.defaultBlockState(), 3); } blockIterator.remove(); + temporaryBlocks.changed(); } Iterator reappearIterator = DELAYED_REAPPEARS.iterator(); @@ -348,8 +351,7 @@ public final class AdaptiveSpecialAbilities { if (!level.setBlock(pos, placedState, 3)) { return; } - TEMPORARY_BLOCKS.add(new TemporaryBlock(level.getServer(), level.dimension(), pos.immutable(), - expireTick, replaced, placedState)); + TemporaryBlockSavedData.get(level).add(level, pos, expireTick); level.playSound(null, pos, SoundEvents.SPIDER_AMBIENT, SoundSource.HOSTILE, 0.6F, 1.25F); } @@ -389,34 +391,14 @@ public final class AdaptiveSpecialAbilities { } public static void invalidateTemporaryBlock(ServerLevel level, BlockPos pos) { - TEMPORARY_BLOCKS.removeIf(block -> block.server == level.getServer() - && block.dimension == level.dimension() && block.pos.equals(pos)); + TemporaryBlockSavedData.get(level).remove(level, pos); } public static void clear(ServerLevel level) { - TEMPORARY_BLOCKS.removeIf(block -> { - if (block.server != level.getServer() || block.dimension != level.dimension()) { - return false; - } - if (level.getBlockState(block.pos).equals(block.placedState)) { - block.replaced.restore(true, false); - } - return true; - }); DELAYED_REAPPEARS.removeIf(pending -> pending.server == level.getServer() && pending.dimension == level.dimension()); } public static void clear(net.minecraft.server.MinecraftServer server) { - TEMPORARY_BLOCKS.removeIf(block -> { - if (block.server != server) { - return false; - } - ServerLevel level = server.getLevel(block.dimension); - if (level != null && level.getBlockState(block.pos).equals(block.placedState)) { - block.replaced.restore(true, false); - } - return true; - }); DELAYED_REAPPEARS.removeIf(pending -> pending.server == server); } @@ -424,11 +406,6 @@ public final class AdaptiveSpecialAbilities { return AMConfig.enabled && AMConfig.AI_ENABLED.get() && AMConfig.ENABLE_ADVANCED_AI.get(); } - private record TemporaryBlock(net.minecraft.server.MinecraftServer server, - net.minecraft.resources.ResourceKey dimension, - BlockPos pos, long expireTick, BlockSnapshot replaced, BlockState placedState) { - } - private record DelayedReappear(net.minecraft.server.MinecraftServer server, net.minecraft.resources.ResourceKey dimension, long tick, UUID mobId, UUID targetId, int tier) { diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveCreeperFuseGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveCreeperFuseGoal.java index d661493..c67fc55 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveCreeperFuseGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveCreeperFuseGoal.java @@ -7,6 +7,7 @@ import java.util.function.IntSupplier; /** Keeps the creeper fuse synchronized with the current config-gated adaptive tier. */ public final class AdaptiveCreeperFuseGoal extends Goal { + private static final String ORIGINAL_FUSE_KEY = "am_original_fuse"; private final Creeper creeper; private final IntSupplier tierSupplier; private int updateCooldown; @@ -33,6 +34,17 @@ public final class AdaptiveCreeperFuseGoal extends Goal { } updateCooldown = 20; int tier = tierSupplier.getAsInt(); - creeper.maxSwell = tier <= 0 ? 30 : Math.max(18, 30 - Math.max(0, tier - 1) * 3); + if (tier <= 0) { + if (creeper.getPersistentData().contains(ORIGINAL_FUSE_KEY)) { + creeper.maxSwell = creeper.getPersistentData().getInt(ORIGINAL_FUSE_KEY); + creeper.getPersistentData().remove(ORIGINAL_FUSE_KEY); + } + return; + } + if (!creeper.getPersistentData().contains(ORIGINAL_FUSE_KEY)) { + creeper.getPersistentData().putInt(ORIGINAL_FUSE_KEY, creeper.maxSwell); + } + int original = creeper.getPersistentData().getInt(ORIGINAL_FUSE_KEY); + creeper.maxSwell = Math.max(18, original - Math.max(0, tier - 1) * 3); } } diff --git a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSlimeTacticsGoal.java b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSlimeTacticsGoal.java index 1dd8683..43de8aa 100644 --- a/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSlimeTacticsGoal.java +++ b/src/main/java/net/fretux/adaptivemobs/ai/goals/AdaptiveSlimeTacticsGoal.java @@ -17,15 +17,23 @@ import java.util.function.IntSupplier; public class AdaptiveSlimeTacticsGoal extends Goal { private static final String RECOMBINE_READY_AT_KEY = "am_recombine_ready_at"; + private static final String RECOMBINE_CLAIM_KEY = "am_recombine_claim"; + private static final String RECOMBINE_CLAIM_EXPIRES_KEY = "am_recombine_claim_expires"; private static final int MAX_RECOMBINE_SIZE = 4; private static final double RECOMBINE_SEARCH_RADIUS = 12.0D; private static final double RECOMBINE_DISTANCE_SQR = 3.24D; + private static final double MAX_CHASE_DISTANCE_SQR = 256.0D; + private static final int MAX_CHASE_TICKS = 200; + private static final int MAX_STUCK_TICKS = 40; private final Slime slime; private final IntSupplier tierSupplier; private int cooldown; private int recombineCheckCooldown; private Slime recombinePartner; + private int chaseTicks; + private int stuckTicks; + private Vec3 lastChasePosition; public AdaptiveSlimeTacticsGoal(Slime slime, IntSupplier tierSupplier) { this.slime = slime; @@ -48,9 +56,10 @@ public class AdaptiveSlimeTacticsGoal extends Goal { && --recombineCheckCooldown <= 0) { recombineCheckCooldown = 10; recombinePartner = findRecombinePartner(); - if (recombinePartner != null) { + if (recombinePartner != null && claim(recombinePartner)) { return true; } + recombinePartner = null; } return AdaptiveAIGoalUtils.isValidAdaptiveTarget(slime.getTarget()) && --cooldown <= 0; } @@ -59,6 +68,9 @@ public class AdaptiveSlimeTacticsGoal extends Goal { public void start() { int tier = tierSupplier.getAsInt(); if (recombinePartner != null && recombinePartner.isAlive()) { + chaseTicks = 0; + stuckTicks = 0; + lastChasePosition = slime.position(); if (tryRecombine(recombinePartner)) { recombinePartner = null; } @@ -87,13 +99,20 @@ public class AdaptiveSlimeTacticsGoal extends Goal { @Override public boolean canContinueToUse() { - return recombinePartner != null && recombinePartner.isAlive() - && slime.isAlive() && tierSupplier.getAsInt() >= 5 - && slime.getSize() < MAX_RECOMBINE_SIZE; + return validPartner(recombinePartner, true) && chaseTicks < MAX_CHASE_TICKS + && stuckTicks < MAX_STUCK_TICKS + && slime.distanceToSqr(recombinePartner) <= MAX_CHASE_DISTANCE_SQR; } @Override public void tick() { + chaseTicks++; + if (lastChasePosition != null && slime.position().distanceToSqr(lastChasePosition) < 0.0025D) { + stuckTicks++; + } else { + stuckTicks = 0; + lastChasePosition = slime.position(); + } if (recombinePartner != null && tryRecombine(recombinePartner)) { recombinePartner = null; } @@ -101,6 +120,7 @@ public class AdaptiveSlimeTacticsGoal extends Goal { @Override public void stop() { + releaseClaim(recombinePartner); recombinePartner = null; } @@ -108,13 +128,17 @@ public class AdaptiveSlimeTacticsGoal extends Goal { boolean magma = slime instanceof MagmaCube; List candidates = slime.level().getEntitiesOfClass(Slime.class, slime.getBoundingBox().inflate(RECOMBINE_SEARCH_RADIUS), - other -> other != slime && other.isAlive() && other.getSize() == slime.getSize() + other -> validPartner(other, false) && (other instanceof MagmaCube) == magma - && other.level().getGameTime() >= other.getPersistentData().getLong(RECOMBINE_READY_AT_KEY)); + && claimAvailable(other)); return candidates.stream().min(Comparator.comparingDouble(slime::distanceToSqr)).orElse(null); } private boolean tryRecombine(Slime partner) { + if (!validPartner(partner, true) || slime.distanceToSqr(partner) > MAX_CHASE_DISTANCE_SQR) { + releaseClaim(partner); + return false; + } if (slime.distanceToSqr(partner) > RECOMBINE_DISTANCE_SQR) { slime.getLookControl().setLookAt(partner, 30.0F, 30.0F); slime.getMoveControl().setWantedPosition(partner.getX(), partner.getY(), partner.getZ(), 1.2D); @@ -122,8 +146,10 @@ public class AdaptiveSlimeTacticsGoal extends Goal { } LivingEntity inheritedTarget = AdaptiveAIGoalUtils.isValidAdaptiveTarget(slime.getTarget()) ? slime.getTarget() : partner.getTarget(); + float healthFraction = slime.getHealth() / slime.getMaxHealth(); partner.discard(); - slime.setSize(Math.min(MAX_RECOMBINE_SIZE, slime.getSize() * 2), true); + slime.setSize(Math.min(MAX_RECOMBINE_SIZE, slime.getSize() * 2), false); + slime.setHealth(Math.max(1.0F, Math.min(slime.getMaxHealth(), slime.getMaxHealth() * healthFraction))); if (AdaptiveAIGoalUtils.isValidAdaptiveTarget(inheritedTarget)) { slime.setTarget(inheritedTarget); } @@ -133,6 +159,47 @@ public class AdaptiveSlimeTacticsGoal extends Goal { return true; } + private boolean validPartner(Slime partner, boolean requireClaim) { + if (partner == null || partner == slime || !partner.isAlive() || !slime.isAlive() + || tierSupplier.getAsInt() < 5 || slime.getSize() >= MAX_RECOMBINE_SIZE + || partner.getSize() != slime.getSize() + || (partner instanceof MagmaCube) != (slime instanceof MagmaCube)) { + return false; + } + long now = slime.level().getGameTime(); + if (now < slime.getPersistentData().getLong(RECOMBINE_READY_AT_KEY) + || now < partner.getPersistentData().getLong(RECOMBINE_READY_AT_KEY)) { + return false; + } + return !requireClaim || (partner.getPersistentData().hasUUID(RECOMBINE_CLAIM_KEY) + && partner.getPersistentData().getUUID(RECOMBINE_CLAIM_KEY).equals(slime.getUUID()) + && partner.getPersistentData().getLong(RECOMBINE_CLAIM_EXPIRES_KEY) >= now); + } + + private boolean claimAvailable(Slime partner) { + long now = slime.level().getGameTime(); + return !partner.getPersistentData().hasUUID(RECOMBINE_CLAIM_KEY) + || partner.getPersistentData().getLong(RECOMBINE_CLAIM_EXPIRES_KEY) < now; + } + + private boolean claim(Slime partner) { + if (!validPartner(partner, false) || !claimAvailable(partner)) { + return false; + } + partner.getPersistentData().putUUID(RECOMBINE_CLAIM_KEY, slime.getUUID()); + partner.getPersistentData().putLong(RECOMBINE_CLAIM_EXPIRES_KEY, + slime.level().getGameTime() + MAX_CHASE_TICKS + 1L); + return true; + } + + private void releaseClaim(Slime partner) { + if (partner != null && partner.getPersistentData().hasUUID(RECOMBINE_CLAIM_KEY) + && partner.getPersistentData().getUUID(RECOMBINE_CLAIM_KEY).equals(slime.getUUID())) { + partner.getPersistentData().remove(RECOMBINE_CLAIM_KEY); + partner.getPersistentData().remove(RECOMBINE_CLAIM_EXPIRES_KEY); + } + } + private void scheduleRecombine(int delayTicks) { if (slime.getPersistentData().getLong(RECOMBINE_READY_AT_KEY) == 0L) { slime.getPersistentData().putLong(RECOMBINE_READY_AT_KEY, slime.level().getGameTime() + delayTicks); diff --git a/src/main/java/net/fretux/adaptivemobs/config/AMConfig.java b/src/main/java/net/fretux/adaptivemobs/config/AMConfig.java index 5cf0c50..33afae2 100644 --- a/src/main/java/net/fretux/adaptivemobs/config/AMConfig.java +++ b/src/main/java/net/fretux/adaptivemobs/config/AMConfig.java @@ -199,6 +199,7 @@ public final class AMConfig { public static final ForgeConfigSpec.BooleanValue AI_GUARDIAN = BUILDER.define("ai.guardians", true); public static final ForgeConfigSpec.BooleanValue AI_PHANTOM = BUILDER.define("ai.phantoms", true); public static final ForgeConfigSpec.BooleanValue AI_RAVAGER = BUILDER.define("ai.ravagers", true); + public static final ForgeConfigSpec.BooleanValue AI_VINDICATOR = BUILDER.define("ai.vindicators", true); public static final ForgeConfigSpec.BooleanValue AI_EVOKER = BUILDER.define("ai.evokers", true); public static final ForgeConfigSpec.BooleanValue AI_VEX = BUILDER.define("ai.vexes", true); public static final ForgeConfigSpec.BooleanValue AI_PIGLIN_FAMILY = BUILDER.define("ai.piglinFamily", true); diff --git a/src/main/java/net/fretux/adaptivemobs/data/TemporaryBlockSavedData.java b/src/main/java/net/fretux/adaptivemobs/data/TemporaryBlockSavedData.java new file mode 100644 index 0000000..a9fdbf6 --- /dev/null +++ b/src/main/java/net/fretux/adaptivemobs/data/TemporaryBlockSavedData.java @@ -0,0 +1,75 @@ +package net.fretux.adaptivemobs.data; + +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.saveddata.SavedData; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** Crash-safe registry for temporary blocks. Cobwebs only replace air, so restoration is unambiguous. */ +public final class TemporaryBlockSavedData extends SavedData { + private static final String DATA_NAME = "adaptivemobs_temporary_blocks"; + private final List entries = new ArrayList<>(); + + public static TemporaryBlockSavedData get(ServerLevel level) { + ServerLevel overworld = level.getServer().overworld(); + return overworld.getDataStorage().computeIfAbsent(TemporaryBlockSavedData::load, + TemporaryBlockSavedData::new, DATA_NAME); + } + + public void add(ServerLevel level, BlockPos pos, long expireTick) { + entries.add(new Entry(level.dimension().location(), pos.immutable(), expireTick)); + setDirty(); + } + + public void remove(ServerLevel level, BlockPos pos) { + if (entries.removeIf(entry -> entry.dimension.equals(level.dimension().location()) && entry.pos.equals(pos))) { + setDirty(); + } + } + + public Iterator entries() { + return entries.iterator(); + } + + public void changed() { + setDirty(); + } + + public static TemporaryBlockSavedData load(CompoundTag tag) { + TemporaryBlockSavedData data = new TemporaryBlockSavedData(); + ListTag list = tag.getList("Blocks", Tag.TAG_COMPOUND); + for (Tag raw : list) { + CompoundTag entry = (CompoundTag) raw; + ResourceLocation dimension = ResourceLocation.tryParse(entry.getString("Dimension")); + if (dimension != null) { + data.entries.add(new Entry(dimension, BlockPos.of(entry.getLong("Pos")), entry.getLong("Expires"))); + } + } + return data; + } + + @Override + public CompoundTag save(CompoundTag tag) { + ListTag list = new ListTag(); + for (Entry entry : entries) { + CompoundTag encoded = new CompoundTag(); + encoded.putString("Dimension", entry.dimension.toString()); + encoded.putLong("Pos", entry.pos.asLong()); + encoded.putLong("Expires", entry.expireTick); + list.add(encoded); + } + tag.put("Blocks", list); + return tag; + } + + public record Entry(ResourceLocation dimension, BlockPos pos, long expireTick) { + } +} diff --git a/src/main/java/net/fretux/adaptivemobs/event/AdaptiveMobsEvents.java b/src/main/java/net/fretux/adaptivemobs/event/AdaptiveMobsEvents.java index ebb2434..9607e5a 100644 --- a/src/main/java/net/fretux/adaptivemobs/event/AdaptiveMobsEvents.java +++ b/src/main/java/net/fretux/adaptivemobs/event/AdaptiveMobsEvents.java @@ -65,6 +65,9 @@ public class AdaptiveMobsEvents { return; } int tier = AdaptiveDifficultyManager.getMobTier(level, victim.getType()); + if (!attacker.isAlliedTo(victim) && !victim.isAlliedTo(attacker)) { + return; + } if (tier < 2 || !AMConfig.isMobEnabled(victim.getType())) { return; } diff --git a/src/main/java/net/fretux/adaptivemobs/gametest/AdaptiveServerBehaviorGameTests.java b/src/main/java/net/fretux/adaptivemobs/gametest/AdaptiveServerBehaviorGameTests.java new file mode 100644 index 0000000..181c8f4 --- /dev/null +++ b/src/main/java/net/fretux/adaptivemobs/gametest/AdaptiveServerBehaviorGameTests.java @@ -0,0 +1,55 @@ +package net.fretux.adaptivemobs.gametest; + +import net.fretux.adaptivemobs.ai.goals.AdaptiveSlimeTacticsGoal; +import net.fretux.adaptivemobs.data.TemporaryBlockSavedData; +import net.minecraft.core.BlockPos; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.monster.Slime; +import net.minecraftforge.gametest.GameTestHolder; + +import java.util.Iterator; + +@GameTestHolder("adaptivemobs") +public final class AdaptiveServerBehaviorGameTests { + private AdaptiveServerBehaviorGameTests() { + } + + @GameTest(template = "empty") + public static void temporaryBlocksSurviveSavedDataReload(GameTestHelper helper) { + TemporaryBlockSavedData original = new TemporaryBlockSavedData(); + BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1)); + long expires = helper.getLevel().getGameTime() + 100L; + original.add(helper.getLevel(), pos, expires); + + TemporaryBlockSavedData reloaded = TemporaryBlockSavedData.load(original.save(new CompoundTag())); + Iterator entries = reloaded.entries(); + helper.assertTrue(entries.hasNext(), "temporary block record was lost during save/reload"); + TemporaryBlockSavedData.Entry entry = entries.next(); + helper.assertTrue(entry.dimension().equals(helper.getLevel().dimension().location()), "dimension was not preserved"); + helper.assertTrue(entry.pos().equals(pos), "position was not preserved"); + helper.assertTrue(entry.expireTick() == expires, "expiration was not preserved"); + helper.assertTrue(!entries.hasNext(), "unexpected duplicate temporary block record"); + helper.succeed(); + } + + @GameTest(template = "empty") + 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); + AdaptiveSlimeTacticsGoal goal = new AdaptiveSlimeTacticsGoal(slime, () -> 5); + + helper.assertTrue(goal.canUse(), "slime did not select its eligible partner"); + goal.start(); + for (int i = 0; i <= 40; i++) { + goal.tick(); + } + helper.assertTrue(!goal.canContinueToUse(), "stuck recombination chase did not time out"); + goal.stop(); + helper.succeed(); + } +}