Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
141 lines
6 KiB
Java
141 lines
6 KiB
Java
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<BlockPos> 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<BlockPos> visited = Lists.newArrayList();
|
|
private final Set<BlockPos> 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
|
|
}
|
|
}
|
|
}
|