package net.minecraft.world.entity.ai.goal; import com.google.common.collect.Lists; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.BooleanSupplier; import net.minecraft.core.BlockPos; import net.minecraft.core.Position; import net.minecraft.core.Vec3i; import net.minecraft.server.level.ServerLevel; import net.minecraft.tags.PoiTypeTags; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.PathfinderMob; import net.minecraft.world.entity.ai.goal.Goal; import net.minecraft.world.entity.ai.navigation.PathNavigation; import net.minecraft.world.entity.ai.util.DefaultRandomPos; import net.minecraft.world.entity.ai.util.GoalUtils; import net.minecraft.world.entity.ai.util.LandRandomPos; import net.minecraft.world.entity.ai.village.poi.PoiManager; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.DoorBlock; import net.minecraft.world.level.pathfinder.Node; import net.minecraft.world.level.pathfinder.Path; import net.minecraft.world.phys.Vec3; import org.jspecify.annotations.Nullable; // FIX minecraft-0004: companion HashSet for O(1) hasNotVisited (was O(N) linear scan) // Bounded at 15 entries — structurally correct, negligible practical speedup at current cap. @SuppressWarnings({"unchecked", "rawtypes"}) public class MoveThroughVillageGoal extends Goal { protected final PathfinderMob mob; private final double speedModifier; private @Nullable Path path; private BlockPos poiPos; private final boolean onlyAtNight; private final List visited = Lists.newArrayList(); private final Set visitedSet = new HashSet<>(); // FIX: parallel set for O(1) lookup private final int distanceToPoi; private final BooleanSupplier canDealWithDoors; public MoveThroughVillageGoal(PathfinderMob mob, double speedModifier, boolean onlyAtNight, int distanceToPoi, BooleanSupplier canDealWithDoors) { this.mob = mob; this.speedModifier = speedModifier; this.onlyAtNight = onlyAtNight; this.distanceToPoi = distanceToPoi; this.canDealWithDoors = canDealWithDoors; this.setFlags(EnumSet.of(Goal.Flag.MOVE)); if (!GoalUtils.hasGroundPathNavigation((Mob)mob)) { throw new IllegalArgumentException("Unsupported mob for MoveThroughVillageGoal"); } } public boolean canUse() { BlockPos pos; if (!GoalUtils.hasGroundPathNavigation((Mob)this.mob)) { return false; } this.updateVisited(); if (this.onlyAtNight && this.mob.level().isBrightOutside()) { return false; } ServerLevel level = (ServerLevel)this.mob.level(); if (!level.isCloseToVillage(pos = this.mob.blockPosition(), 6)) { return false; } final BlockPos finalPos = pos; Vec3 landPos = LandRandomPos.getPos((PathfinderMob)this.mob, (int)15, (int)7, p -> { if (!level.isVillage(p)) { return Double.NEGATIVE_INFINITY; } Optional newPoiPos = level.getPoiManager().find(e -> e.is(PoiTypeTags.VILLAGE), this::hasNotVisited, p, 10, PoiManager.Occupancy.IS_OCCUPIED); if (newPoiPos.isEmpty()) return Double.NEGATIVE_INFINITY; return -((BlockPos)newPoiPos.get()).distSqr((Vec3i)finalPos); }); if (landPos == null) { return false; } Optional target = level.getPoiManager().find(e -> e.is(PoiTypeTags.VILLAGE), this::hasNotVisited, BlockPos.containing((Position)landPos), 10, PoiManager.Occupancy.IS_OCCUPIED); if (target.isEmpty()) { return false; } this.poiPos = ((BlockPos)target.get()).immutable(); PathNavigation navigation = this.mob.getNavigation(); navigation.setCanOpenDoors(this.canDealWithDoors.getAsBoolean()); this.path = navigation.createPath(this.poiPos, 0); navigation.setCanOpenDoors(true); if (this.path == null) { Vec3 partialStep = DefaultRandomPos.getPosTowards((PathfinderMob)this.mob, (int)10, (int)7, (Vec3)Vec3.atBottomCenterOf((Vec3i)this.poiPos), (double)1.5707963705062866); if (partialStep == null) { return false; } navigation.setCanOpenDoors(this.canDealWithDoors.getAsBoolean()); this.path = this.mob.getNavigation().createPath(partialStep.x, partialStep.y, partialStep.z, 0); navigation.setCanOpenDoors(true); if (this.path == null) { return false; } } for (int i = 0; i < this.path.getNodeCount(); ++i) { Node node = this.path.getNode(i); BlockPos doorPos = new BlockPos(node.x, node.y + 1, node.z); if (!DoorBlock.isWoodenDoor((Level)this.mob.level(), (BlockPos)doorPos)) continue; this.path = this.mob.getNavigation().createPath((double)node.x, (double)node.y, (double)node.z, 0); break; } return this.path != null; } public boolean canContinueToUse() { if (this.mob.getNavigation().isDone()) { return false; } return !this.poiPos.closerToCenterThan((Position)this.mob.position(), (double)(this.mob.getBbWidth() + (float)this.distanceToPoi)); } public void start() { this.mob.getNavigation().moveTo(this.path, this.speedModifier); } public void stop() { if (this.mob.getNavigation().isDone() || this.poiPos.closerToCenterThan((Position)this.mob.position(), (double)this.distanceToPoi)) { this.visited.add(this.poiPos); this.visitedSet.add(this.poiPos); // FIX } } private boolean hasNotVisited(BlockPos poi) { return !this.visitedSet.contains(poi); // FIX: O(1) HashSet lookup } private void updateVisited() { if (this.visited.size() > 15) { this.visitedSet.remove(this.visited.remove(0)); // FIX: evict from set on FIFO removal } } }