Files
baritone/src/main/java/baritone/builder/BlockStatePlacementOption.java
2023-04-05 00:22:15 -07:00

237 lines
12 KiB
Java

/*
* This file is part of Baritone.
*
* Baritone is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Baritone is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Baritone. If not, see <https://www.gnu.org/licenses/>.
*/
package baritone.builder;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* A plane against which this block state can be placed
* <p>
* For a normal block, this will be a full face of a block. In that case, this class is no more than an EnumFacing
* <p>
* For a block like a slab or a stair, this will contain the information that the placement must be against the top or bottom half of the face
* <p>
* For a block like a furnace, this will contain the information that the player must be facing a specific horizontal direction in order to get the desired orientation
* <p>
* For a block like a piston, dispenser, or observer, this will contain the information that be player must pass a combination of: specific relative eye coordinate, specific relative X Z, and specific horizontal facing
*/
public class BlockStatePlacementOption {
/**
* e.g. a torch placed down on the ground is placed against the bottom of "the torch bounding box", so this would be DOWN for the torch
*/
public final Face against;
public final Half half;
public final Optional<Face> playerMustBeHorizontalFacing; // getHorizontalFacing
/**
* IMPORTANT this is the RAW getDirectionFromEntityLiving meaning that it is the OPPOSITE of getHorizontalFacing (when in the horizontal plane)
*/
public final Optional<Face> playerMustBeEntityFacing; // EnumFacing.getDirectionFromEntityLiving, used by piston, dispenser, observer
private BlockStatePlacementOption(Face against, Half half, Optional<Face> playerMustBeHorizontalFacing, Optional<Face> playerMustBeEntityFacing) {
Objects.requireNonNull(against);
Objects.requireNonNull(half);
this.against = against;
this.half = half;
this.playerMustBeHorizontalFacing = playerMustBeHorizontalFacing;
this.playerMustBeEntityFacing = playerMustBeEntityFacing;
validate(against, half, playerMustBeHorizontalFacing, playerMustBeEntityFacing);
}
/**
* This value must be greater than the face projections.
* <p>
* Otherwise certain stair placements would not work. This is verified in the test
*/
public static final double LOOSE_CENTER_DISTANCE = 0.15;
public List<Raytracer.Raytrace> computeTraceOptions(PlaceAgainstData placingAgainst, int playerSupportingX, int playerFeetBlips, int playerSupportingZ, PlayerVantage vantage, double blockReachDistance) {
if (!BlockStateCachedData.possible(this, placingAgainst)) {
throw new IllegalStateException();
}
if (Main.DEBUG && placingAgainst.streamRelativeToPlace().noneMatch(hit -> hitOk(half, hit))) {
throw new IllegalStateException();
}
List<Vec2d> acceptableVantages = new ArrayList<>();
Vec2d center = Vec2d.HALVED_CENTER.plus(playerSupportingX, playerSupportingZ);
switch (vantage) {
case LOOSE_CENTER: {
acceptableVantages.add(center.plus(LOOSE_CENTER_DISTANCE, 0));
acceptableVantages.add(center.plus(-LOOSE_CENTER_DISTANCE, 0));
acceptableVantages.add(center.plus(0, LOOSE_CENTER_DISTANCE));
acceptableVantages.add(center.plus(0, -LOOSE_CENTER_DISTANCE));
// no break!
} // FALLTHROUGH!
case STRICT_CENTER: {
acceptableVantages.add(center);
break;
}
case SNEAK_BACKPLACE: {
if (playerSupportingX != against.x || playerSupportingZ != against.z) {
throw new IllegalStateException();
}
// in a sneak backplace, there is exactly one location where the player will be
acceptableVantages.add(Vec2d.HALVED_CENTER.plus(0.25 * against.x, 0.25 * against.z));
break;
}
default:
throw new IllegalStateException();
}
// direction from placed block to place-against block = this.against
long blockPlacedAt = 0;
long placeAgainstPos = against.offset(blockPlacedAt);
return sanityCheckTraces(acceptableVantages
.stream()
.map(playerEyeXZ -> new Vec3d(playerEyeXZ.x, Blip.playerEyeFromFeetBlips(playerFeetBlips, placingAgainst.mustSneak), playerEyeXZ.z))
.flatMap(eye ->
placingAgainst.streamRelativeToPlace()
.filter(hit -> hitOk(half, hit))
.filter(hit -> eye.distSq(hit) < blockReachDistance * blockReachDistance)
.filter(hit -> directionOk(eye, hit))
.<Supplier<Optional<Raytracer.Raytrace>>>map(hit -> () -> Raytracer.runTrace(eye, placeAgainstPos, against.opposite(), hit))
)
.collect(Collectors.toList())
.parallelStream() // wrap it like this because flatMap forces .sequential() on the interior child stream, defeating the point
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.sorted()
.collect(Collectors.toList()));
}
public static boolean hitOk(Half half, Vec3d hit) {
if (half == Half.EITHER) {
return true;
} else if (hit.y == 0.1) {
return half == Half.BOTTOM;
} else if (hit.y == 0.5) {
return false; // ambiguous, so force it to pick either down or up
} else if (hit.y == 0.9) {
return half == Half.TOP;
} else {
throw new IllegalStateException();
}
}
/**
* In EnumFacing.getDirectionFromEntityLiving, it checks if the player feet is within 2 blocks of the center of the block to be placed.
* Normally, this is a nonissue, but a problem arises because we are considering hypothetical placements where the player stands at the exact +0.5,+0.5 center of a block.
* In that case, it's possible for our hypothetical to have the player at precisely 2 blocks away, i.e. precisely on the edge of this condition being true or false.
* For that reason, we treat those exact cases as "ambiguous". So, if the distance is within this tolerance of 2 (so, 1.99 to 2.01), we treat it as a "could go either way",
* because when we really get there in-game, floating point inaccuracy could indeed actually make it go either way.
*/
private static final double ENTITY_FACING_TOLERANCE = 0.01;
private boolean directionOk(Vec3d eye, Vec3d hit) {
if (playerMustBeHorizontalFacing.isPresent()) {
return eye.flatDirectionTo(hit) == playerMustBeHorizontalFacing.get();
}
if (playerMustBeEntityFacing.isPresent()) { // handle piston, dispenser, dropper, observer
if (!hit.inOriginUnitVoxel()) {
throw new IllegalStateException();
}
Face entFace = playerMustBeEntityFacing.get();
// see EnumFacing.getDirectionFromEntityLiving
double dx = Math.abs(eye.x - 0.5); // TODO this is changed between 1.12 and 1.19, in 1.19 this should be eye.x-hit.x
double dz = Math.abs(eye.z - 0.5);
if (dx < 2 - ENTITY_FACING_TOLERANCE && dz < 2 - ENTITY_FACING_TOLERANCE) { // < 1.99
if (eye.y < 0) { // eye below placement level = it will be facing down, so this is only okay if we want that
return entFace == Face.DOWN;
}
if (eye.y > 2) { // same for up, if y>2 then it will be facing up
return entFace == Face.UP;
}
} else if (!(dx > 2 + ENTITY_FACING_TOLERANCE || dz > 2 + ENTITY_FACING_TOLERANCE)) { // > 2.01
// this is the ambiguous case, because we are neither unambiguously both-within-2 (previous case), nor unambiguously either-above-two (this elseif condition).
// UP/DOWN are impossible, but that's caught by flat check
if (eye.y < 0 || eye.y > 2) { // this check is okay because player eye height is not an even multiple of blips, therefore there's no way for it to == 0 or == 2, so using > and < is safe
return false; // anything that could cause up/down instead of horizontal is also not allowed sadly
}
} // else we are in unambiguous either-above-two, putting us in simple horizontal mode, so fallthrough to flat condition is correct, yay
return eye.flatDirectionTo(hit) == entFace.opposite();
}
return true;
}
public static BlockStatePlacementOption get(Face against, Half half, Optional<Face> playerMustBeHorizontalFacing, Optional<Face> playerMustBeEntityFacing) {
BlockStatePlacementOption ret = PLACEMENT_OPTION_SINGLETON_CACHE[against.index][half.ordinal()][Face.OPTS.indexOf(playerMustBeHorizontalFacing)][Face.OPTS.indexOf(playerMustBeEntityFacing)];
if (ret == null) {
throw new IllegalStateException(against + " " + half + " " + playerMustBeHorizontalFacing + " " + playerMustBeEntityFacing);
}
return ret;
}
private static final BlockStatePlacementOption[][][][] PLACEMENT_OPTION_SINGLETON_CACHE;
static {
PLACEMENT_OPTION_SINGLETON_CACHE = new BlockStatePlacementOption[Face.NUM_FACES][Half.values().length][Face.OPTS.size()][Face.OPTS.size()];
for (Face against : Face.VALUES) {
for (Half half : Half.values()) {
for (Optional<Face> horizontalFacing : Face.OPTS) {
for (Optional<Face> entityFacing : Face.OPTS) {
try {
PLACEMENT_OPTION_SINGLETON_CACHE[against.index][half.ordinal()][Face.OPTS.indexOf(horizontalFacing)][Face.OPTS.indexOf(entityFacing)] = new BlockStatePlacementOption(against, half, horizontalFacing, entityFacing);
} catch (RuntimeException ex) {}
}
}
}
}
}
private void validate(Face against, Half half, Optional<Face> playerMustBeHorizontalFacing, Optional<Face> playerMustBeEntityFacing) {
if (playerMustBeEntityFacing.isPresent() && playerMustBeHorizontalFacing.isPresent()) {
throw new IllegalStateException();
}
if (against.vertical && half != Half.EITHER) {
throw new IllegalArgumentException();
}
if (Main.STRICT_Y && against == Face.UP) {
throw new IllegalStateException();
}
playerMustBeHorizontalFacing.ifPresent(face -> {
if (face.vertical) {
throw new IllegalArgumentException();
}
if (face == against.opposite()) {
throw new IllegalStateException();
}
});
playerMustBeEntityFacing.ifPresent(face -> {
if (half != Half.EITHER) {
throw new IllegalStateException();
}
if (against == face) { // impossible because EnumFacing inverts the horizontal facing AND because the down and up require the eye to be <0 and >2 respectively
throw new IllegalStateException();
}
});
}
private static List<Raytracer.Raytrace> sanityCheckTraces(List<Raytracer.Raytrace> traces) {
if (Main.DEBUG && traces.stream().mapToDouble(Raytracer.Raytrace::centerDistApprox).distinct().count() > 2) {
throw new IllegalStateException();
}
return traces;
}
}