From d501359857a078e2bc08735d1805ea4dcba45cdb Mon Sep 17 00:00:00 2001 From: Leijurv Date: Tue, 18 May 2021 00:16:30 -0700 Subject: [PATCH] progress on properties and entity oriented blocks --- .../builder/BlockStateCachedData.java | 11 +- .../builder/BlockStateCachedDataBuilder.java | 109 ++++++++++--- .../builder/BlockStatePlacementOption.java | 101 ++++++++---- src/main/java/baritone/builder/Face.java | 10 ++ src/main/java/baritone/builder/Main.java | 6 +- .../baritone/builder/PlaceAgainstData.java | 10 +- .../java/baritone/builder/PlayerPhysics.java | 18 +-- src/main/java/baritone/builder/README.md | 11 +- .../java/baritone/builder/Scaffolder.java | 16 +- src/main/java/baritone/builder/Vec3d.java | 8 + .../mc/BlockStatePropertiesExtractor.java | 146 ++++++++++++++---- .../java/baritone/builder/mc/DebugStates.java | 108 +++++++++++++ .../mc/VanillaBlockStateDataProvider.java | 8 +- 13 files changed, 459 insertions(+), 103 deletions(-) create mode 100644 src/main/java/baritone/builder/mc/DebugStates.java diff --git a/src/main/java/baritone/builder/BlockStateCachedData.java b/src/main/java/baritone/builder/BlockStateCachedData.java index c4bf52bfa..f64e30665 100644 --- a/src/main/java/baritone/builder/BlockStateCachedData.java +++ b/src/main/java/baritone/builder/BlockStateCachedData.java @@ -17,6 +17,7 @@ package baritone.builder; +import java.util.Collections; import java.util.List; /** @@ -27,10 +28,10 @@ import java.util.List; public final class BlockStateCachedData { private static final BlockStateCachedData[] PER_STATE = Main.DATA_PROVIDER.allNullable(); - public static final BlockStateCachedData SCAFFOLDING = new BlockStateCachedData(new BlockStateCachedDataBuilder().collidesWithPlayer(true).fullyWalkableTop().height(1).canPlaceAgainstMe()); + public static final BlockStateCachedData SCAFFOLDING = new BlockStateCachedData(new BlockStateCachedDataBuilder().collidesWithPlayer(true).fullyWalkableTop().collisionHeight(1).canPlaceAgainstMe()); public final boolean fullyWalkableTop; - public final Integer supportedPlayerY; + public final Integer collisionHeightBlips; public final boolean isAir; public final boolean collidesWithPlayer; @@ -40,7 +41,7 @@ public final class BlockStateCachedData { public final List options; - private final PlaceAgainstData[] againstMe; + public final PlaceAgainstData[] againstMe; public static BlockStateCachedData get(int state) { return PER_STATE[state]; @@ -51,10 +52,10 @@ public final class BlockStateCachedData { this.isAir = builder.isAir(); this.fullyWalkableTop = builder.isFullyWalkableTop(); this.collidesWithPlayer = builder.isCollidesWithPlayer(); - this.supportedPlayerY = builder.supportedPlayerY(); + this.collisionHeightBlips = builder.collisionHeightBlips(); this.mustSneakWhenPlacingAgainstMe = builder.isMustSneakWhenPlacingAgainstMe(); - this.options = builder.howCanIBePlaced(); + this.options = Collections.unmodifiableList(builder.howCanIBePlaced()); this.againstMe = builder.placeAgainstMe(); } diff --git a/src/main/java/baritone/builder/BlockStateCachedDataBuilder.java b/src/main/java/baritone/builder/BlockStateCachedDataBuilder.java index 13d993b43..ec7588871 100644 --- a/src/main/java/baritone/builder/BlockStateCachedDataBuilder.java +++ b/src/main/java/baritone/builder/BlockStateCachedDataBuilder.java @@ -41,8 +41,12 @@ public class BlockStateCachedDataBuilder { * Normal blocks must be placed against EITHER */ private Half mustBePlacedAgainst = Half.EITHER; - private Face playerMustBeFacingInOrderToPlaceMe; - private Integer height; + private Face playerMustBeHorizontalFacingInOrderToPlaceMe; + private Integer collisionHeightBlips; + private Face canOnlyPlaceAgainst; + private boolean fakeLessThanFullHeight; // snow layers and soul sand + private boolean placementLogicNotImplementedYet; + private Face playerMustBeEntityFacingInOrderToPlaceMe; public BlockStateCachedDataBuilder() { } @@ -72,18 +76,18 @@ public class BlockStateCachedDataBuilder { return fullyWalkableTop; } - public BlockStateCachedDataBuilder height(double y) { + public BlockStateCachedDataBuilder collisionHeight(double y) { for (int h = 1; h <= Blip.PER_BLOCK + Blip.HALF_BLOCK; h++) { if (y == h * Blip.RATIO) { - height = h; + collisionHeightBlips = h; return this; } } throw new IllegalStateException(); } - public Integer supportedPlayerY() { // e.g. slabs are 0.5, soul sand is 0.875, normal blocks are 1, fences are 1.5 - return height; + public Integer collisionHeightBlips() { // e.g. slabs are 0.5, soul sand is 0.875, normal blocks are 1, fences are 1.5 + return collisionHeightBlips; } public BlockStateCachedDataBuilder mustSneakWhenPlacingAgainstMe() { @@ -109,12 +113,20 @@ public class BlockStateCachedDataBuilder { return this; } - public BlockStateCachedDataBuilder playerMustBeFacingInOrderToPlaceMe(Face face) { - playerMustBeFacingInOrderToPlaceMe = face; + public BlockStateCachedDataBuilder playerMustBeHorizontalFacingInOrderToPlaceMe(Face face) { + playerMustBeHorizontalFacingInOrderToPlaceMe = face; + return this; + } + + public BlockStateCachedDataBuilder playerMustBeEntityFacingInOrderToPlaceMe(Face face) { + playerMustBeEntityFacingInOrderToPlaceMe = face; return this; } public BlockStateCachedDataBuilder mustBePlacedAgainst(Half half) { + if (half == null) { + throw new IllegalArgumentException(); + } mustBePlacedAgainst = half; return this; } @@ -124,16 +136,40 @@ public class BlockStateCachedDataBuilder { return this; } + public BlockStateCachedDataBuilder canOnlyPlaceAgainst(Face face) { + canOnlyPlaceAgainst = face; + return this; + } + + public BlockStateCachedDataBuilder placementLogicNotImplementedYet() { + placementLogicNotImplementedYet = true; + return this; + } + + public BlockStateCachedDataBuilder fakeLessThanFullHeight() { + fakeLessThanFullHeight = true; + return this; + } + public List howCanIBePlaced() { - if (mustBePlacedAgainst == null) { + if (mustBePlacedAgainst == null || placementLogicNotImplementedYet) { return Collections.emptyList(); } List ret = new ArrayList<>(); for (Face face : Face.VALUES) { if (Main.STRICT_Y && face == Face.UP) { + continue; // TODO don't do this... + } + if (playerMustBeHorizontalFacingInOrderToPlaceMe == face.opposite()) { // obv, this won't happen if playerMustBeHorizontalFacing is null continue; } - if (playerMustBeFacingInOrderToPlaceMe == face.opposite()) { // obv, this won't happen if playerMustBeFacing is null + if (playerMustBeEntityFacingInOrderToPlaceMe == face) { + continue; + } + if (falling && face != Face.DOWN) { + continue; + } + if (canOnlyPlaceAgainst != null && face != canOnlyPlaceAgainst) { continue; } Half overrideHalf = mustBePlacedAgainst; @@ -151,9 +187,9 @@ public class BlockStateCachedDataBuilder { overrideHalf = Half.EITHER; } } - ret.add(BlockStatePlacementOption.get(face, overrideHalf, Optional.ofNullable(playerMustBeFacingInOrderToPlaceMe))); + ret.add(BlockStatePlacementOption.get(face, overrideHalf, Optional.ofNullable(playerMustBeHorizontalFacingInOrderToPlaceMe), Optional.ofNullable(playerMustBeEntityFacingInOrderToPlaceMe))); } - return Collections.unmodifiableList(ret); + return ret; } public PlaceAgainstData[] placeAgainstMe() { @@ -168,6 +204,7 @@ public class BlockStateCachedDataBuilder { } protected PlaceAgainstData placeAgainstFace(Face face) { + // TODO this makes the stair/slab assumption that the same half is the mustBePlacedAgainst as the faces offered for placement... counterexample is daylight sensor if (mustBePlacedAgainst == Half.TOP && face == Face.DOWN) { return null; } @@ -192,23 +229,31 @@ public class BlockStateCachedDataBuilder { if (mustBePlacedAgainst == null ^ isAir()) { throw new IllegalStateException(); } - if (mustBePlacedAgainst == null ^ howCanIBePlaced().isEmpty()) { - throw new IllegalStateException(); + if (howCanIBePlaced().isEmpty()) { + if (mustBePlacedAgainst != null && !placementLogicNotImplementedYet) { + throw new IllegalStateException(); + } + if (playerMustBeHorizontalFacingInOrderToPlaceMe != null || playerMustBeEntityFacingInOrderToPlaceMe != null) { + throw new IllegalStateException(); + } + if (canOnlyPlaceAgainst != null) { + throw new IllegalStateException(); + } } if (isMustSneakWhenPlacingAgainstMe() && mustBePlacedAgainst != Half.EITHER) { throw new IllegalArgumentException(); } - if (playerMustBeFacingInOrderToPlaceMe != null && mustBePlacedAgainst == null) { + if ((playerMustBeHorizontalFacingInOrderToPlaceMe != null || playerMustBeEntityFacingInOrderToPlaceMe != null) && mustBePlacedAgainst == null) { throw new IllegalStateException(); } - if (isFullyWalkableTop() ^ height != null) { - if (!isFullyWalkableTop() && height > Blip.PER_BLOCK) { + if (isFullyWalkableTop() ^ collisionHeightBlips != null) { + if (!isFullyWalkableTop() && collisionHeightBlips > Blip.PER_BLOCK) { // exception for fences, walls } else { throw new IllegalStateException(); } } - if (height != null && height > Blip.FULL_BLOCK + Blip.HALF_BLOCK) { // playerphysics assumes this is never true + if (collisionHeightBlips != null && (collisionHeightBlips > Blip.FULL_BLOCK + Blip.HALF_BLOCK || collisionHeightBlips <= 0)) { // playerphysics assumes this is never true throw new IllegalStateException(); } if (fullyWalkableTop && !collidesWithPlayer) { @@ -217,7 +262,7 @@ public class BlockStateCachedDataBuilder { if (canPlaceAgainstMe && !collidesWithPlayer) { throw new IllegalStateException(); } - if (playerMustBeFacingInOrderToPlaceMe != null && playerMustBeFacingInOrderToPlaceMe.vertical) { + if (playerMustBeHorizontalFacingInOrderToPlaceMe != null && playerMustBeHorizontalFacingInOrderToPlaceMe.vertical) { throw new IllegalStateException(); } if (Main.STRICT_Y && howCanIBePlaced().stream().anyMatch(opt -> opt.against == Face.UP)) { @@ -227,9 +272,31 @@ public class BlockStateCachedDataBuilder { if (data.length != Face.NUM_FACES) { throw new IllegalStateException(); } + boolean any = false; for (int i = 0; i < Face.NUM_FACES; i++) { - if (data[i] != null && data[i].against != Face.VALUES[i]) { - throw new IllegalStateException(); + if (data[i] != null) { + if (data[i].against != Face.VALUES[i]) { + throw new IllegalStateException(); + } + if (!canPlaceAgainstMe) { + throw new IllegalStateException(); + } + any = true; + } + } + if (canPlaceAgainstMe && !any) { + throw new IllegalStateException(); + } + if (collisionHeightBlips != null && !fakeLessThanFullHeight) { + for (PlaceAgainstData d : data) { + if (d == null) { + continue; + } + d.streamRelativeToMyself().forEach(hit -> { + if (hit.y > collisionHeightBlips * Blip.RATIO) { + throw new IllegalStateException(d.against + " " + hit.y + " " + collisionHeightBlips * Blip.RATIO); + } + }); } } } diff --git a/src/main/java/baritone/builder/BlockStatePlacementOption.java b/src/main/java/baritone/builder/BlockStatePlacementOption.java index b98866ea4..cf6be9fff 100644 --- a/src/main/java/baritone/builder/BlockStatePlacementOption.java +++ b/src/main/java/baritone/builder/BlockStatePlacementOption.java @@ -32,6 +32,8 @@ import java.util.stream.Collectors; * 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 *

* 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 + *

+ * 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 { @@ -40,15 +42,20 @@ public class BlockStatePlacementOption { */ public final Face against; public final Half half; - public final Optional playerMustBeFacing; + public final Optional playerMustBeHorizontalFacing; // getHorizontalFacing + /** + * IMPORTANT this is the RAW getDirectionFromEntityLiving meaning that it is the OPPOSITE of getHorizontalFacing (when in the horizontal plane) + */ + public final Optional playerMustBeEntityFacing; // EnumFacing.getDirectionFromEntityLiving, used by piston, dispenser, observer - private BlockStatePlacementOption(Face against, Half half, Optional playerMustBeFacing) { + private BlockStatePlacementOption(Face against, Half half, Optional playerMustBeHorizontalFacing, Optional playerMustBeEntityFacing) { Objects.requireNonNull(against); Objects.requireNonNull(half); this.against = against; this.half = half; - this.playerMustBeFacing = playerMustBeFacing; - validate(against, half, playerMustBeFacing); + this.playerMustBeHorizontalFacing = playerMustBeHorizontalFacing; + this.playerMustBeEntityFacing = playerMustBeEntityFacing; + validate(against, half, playerMustBeHorizontalFacing, playerMustBeEntityFacing); } /** @@ -62,7 +69,7 @@ public class BlockStatePlacementOption { if (!BlockStateCachedData.possible(this, placingAgainst)) { throw new IllegalStateException(); } - if (Main.DEBUG && placingAgainst.stream().noneMatch(hit -> hitOk(half, hit))) { + if (Main.DEBUG && placingAgainst.streamRelativeToPlace().noneMatch(hit -> hitOk(half, hit))) { throw new IllegalStateException(); } List acceptableVantages = new ArrayList<>(); @@ -98,7 +105,7 @@ public class BlockStatePlacementOption { .stream() .map(playerEyeXZ -> new Vec3d(playerEyeXZ.x, Blip.playerEyeFromFeetBlips(playerFeetBlips, placingAgainst.mustSneak), playerEyeXZ.z)) .flatMap(eye -> - placingAgainst.stream() + placingAgainst.streamRelativeToPlace() .filter(hit -> hitOk(half, hit)) .filter(hit -> eye.distSq(hit) < blockReachDistance * blockReachDistance) .filter(hit -> directionOk(eye, hit)) @@ -128,51 +135,85 @@ public class BlockStatePlacementOption { } private boolean directionOk(Vec3d eye, Vec3d hit) { - if (playerMustBeFacing.isPresent()) { - return eye.flatDirectionTo(hit) == playerMustBeFacing.get(); + if (playerMustBeHorizontalFacing.isPresent()) { + return eye.flatDirectionTo(hit) == playerMustBeHorizontalFacing.get(); + } + if (playerMustBeEntityFacing.isPresent()) { // handle piston, dispenser, observer + if (!hit.inOriginUnitVoxel()) { + throw new IllegalStateException(); + } + Face entFace = playerMustBeEntityFacing.get(); + // see EnumFacing.getDirectionFromEntityLiving + double dx = Math.abs(eye.x - 0.5); + double dz = Math.abs(eye.z - 0.5); + if (dx < 1.99 && dz < 1.99) { + // both within 2 = normal + if (eye.y < 0) { + return entFace == Face.DOWN; + } + if (eye.y > 2) { + return entFace == Face.UP; + } + } else if (!(dx > 2.01 || dz > 2.01)) { + // ambiguous case + // UP/DOWN are impossible (caught by flat check), and anything that could cause up/down instead of horizontal is also not allowed sadly + if (eye.y < 0 || eye.y > 2) { + return false; + } + } // else either is above 2.01, putting us in simple horizontal mode, so fallthrough to flat condition is correct, yay + return eye.flatDirectionTo(hit) == entFace.opposite(); } - // TODO include the other conditions for stupid blocks like pistons return true; } - public static BlockStatePlacementOption get(Face against, Half half, Optional playerMustBeFacing) { - BlockStatePlacementOption ret = PLACEMENT_OPTION_SINGLETON_CACHE[against.index][half.ordinal()][playerMustBeFacing.map(face -> face.index).orElse(Face.NUM_FACES)]; + public static BlockStatePlacementOption get(Face against, Half half, Optional playerMustBeHorizontalFacing, Optional 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 + " " + playerMustBeFacing); + throw new IllegalStateException(against + " " + half + " " + playerMustBeHorizontalFacing + " " + playerMustBeEntityFacing); } return ret; } - private static final BlockStatePlacementOption[][][] PLACEMENT_OPTION_SINGLETON_CACHE; + private static final BlockStatePlacementOption[][][][] PLACEMENT_OPTION_SINGLETON_CACHE; static { - PLACEMENT_OPTION_SINGLETON_CACHE = new BlockStatePlacementOption[Face.NUM_FACES][Half.values().length][Face.NUM_FACES + 1]; + 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()) { - BlockStatePlacementOption[] saveInto = PLACEMENT_OPTION_SINGLETON_CACHE[against.index][half.ordinal()]; - for (Face player : Face.VALUES) { - try { - saveInto[player.index] = new BlockStatePlacementOption(against, half, Optional.of(player)); - } catch (RuntimeException ex) { + for (Optional horizontalFacing : Face.OPTS) { + for (Optional 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) {} } } - try { - saveInto[Face.NUM_FACES] = new BlockStatePlacementOption(against, half, Optional.empty()); - } catch (RuntimeException ex) { - } } } } - private void validate(Face against, Half half, Optional playerMustBeFacing) { + static { + for (int i = 0; i < Face.OPTS.size(); i++) { + if (Face.OPTS.indexOf(Face.OPTS.get(i)) != i) { + throw new IllegalStateException(); + } + if (Face.OPTS.get(i).map(face -> face.index).orElse(Face.NUM_FACES) != i) { + throw new IllegalStateException(); + } + } + } + + private void validate(Face against, Half half, Optional playerMustBeHorizontalFacing, Optional 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(); } - playerMustBeFacing.ifPresent(face -> { + playerMustBeHorizontalFacing.ifPresent(face -> { if (face.vertical) { throw new IllegalArgumentException(); } @@ -180,6 +221,14 @@ public class BlockStatePlacementOption { 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(); + } + }); } static { @@ -200,7 +249,7 @@ public class BlockStatePlacementOption { for (PlayerVantage vantage : new PlayerVantage[]{PlayerVantage.STRICT_CENTER, PlayerVantage.LOOSE_CENTER}) { for (Face playerFacing : new Face[]{Face.NORTH, Face.EAST, Face.WEST}) { sanity.append(vantage).append(playerFacing); - List traces = BlockStatePlacementOption.get(Face.NORTH, Half.BOTTOM, Optional.of(playerFacing)).computeTraceOptions(new PlaceAgainstData(Face.SOUTH, Half.EITHER, false), 1, 0, 0, vantage, 4); + List traces = BlockStatePlacementOption.get(Face.NORTH, Half.BOTTOM, Optional.of(playerFacing), Optional.empty()).computeTraceOptions(new PlaceAgainstData(Face.SOUTH, Half.EITHER, false), 1, 0, 0, vantage, 4); sanity.append(traces.size()); sanity.append(" "); if (!traces.isEmpty()) { diff --git a/src/main/java/baritone/builder/Face.java b/src/main/java/baritone/builder/Face.java index 4dd53d63c..b1f136c8d 100644 --- a/src/main/java/baritone/builder/Face.java +++ b/src/main/java/baritone/builder/Face.java @@ -20,6 +20,11 @@ package baritone.builder; import baritone.api.utils.BetterBlockPos; import net.minecraft.util.EnumFacing; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + /** * I hate porting things to new versions of Minecraft *

@@ -38,11 +43,16 @@ public enum Face { public static final int NUM_FACES = 6; public static final Face[] VALUES = new Face[NUM_FACES]; public static final Face[] HORIZONTALS; + public static final List> OPTS; static { + List> lst = new ArrayList<>(); for (Face face : values()) { VALUES[face.index] = face; + lst.add(Optional.of(face)); } + lst.add(Optional.empty()); + OPTS = Collections.unmodifiableList(lst); HORIZONTALS = new Face[]{Face.SOUTH, Face.WEST, Face.NORTH, Face.EAST}; } diff --git a/src/main/java/baritone/builder/Main.java b/src/main/java/baritone/builder/Main.java index b1e2efba3..dd3580c73 100644 --- a/src/main/java/baritone/builder/Main.java +++ b/src/main/java/baritone/builder/Main.java @@ -18,6 +18,7 @@ package baritone.builder; import baritone.api.utils.BetterBlockPos; +import baritone.builder.mc.DebugStates; import baritone.builder.mc.VanillaBlockStateDataProvider; import it.unimi.dsi.fastutil.doubles.DoubleArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList; @@ -163,7 +164,7 @@ public class Main { { BlockStatePlacementOption.sanityCheck(); } - { + for (int aaaa = 0; aaaa < 0; aaaa++) { /*Raytracer.raytraceMode++; Raytracer.raytraceMode %= 3;*/ Random rand = new Random(5021); @@ -262,6 +263,9 @@ public class Main { } } } + { + DebugStates.debug(); + } System.exit(0); } } diff --git a/src/main/java/baritone/builder/PlaceAgainstData.java b/src/main/java/baritone/builder/PlaceAgainstData.java index 07b0c447f..9baf77c93 100644 --- a/src/main/java/baritone/builder/PlaceAgainstData.java +++ b/src/main/java/baritone/builder/PlaceAgainstData.java @@ -27,7 +27,7 @@ public class PlaceAgainstData { public final Face against; public final boolean mustSneak; // like if its a crafting table - public final Vec3d[] hits; + private final Vec3d[] hits; private final boolean top; private final boolean bottom; @@ -61,10 +61,14 @@ public class PlaceAgainstData { } } - public Stream stream() { + public Stream streamRelativeToPlace() { return Stream.of(hits); } + public Stream streamRelativeToMyself() { + return streamRelativeToPlace().map(v -> v.plus(against.x, against.y, against.z)); + } + private boolean validatePossibleHit(Vec3d hit) { double[] h = {hit.x, hit.y, hit.z}; for (int i = 0; i < 3; i++) { @@ -92,7 +96,7 @@ public class PlaceAgainstData { return true; } - // TODO for playerMustBeFacing, do i need something like andThatOptionExtendsTheFullHorizontalSpaceOfTheVoxel()? + // TODO for playerMustBeHorizontalFacing, do i need something like andThatOptionExtendsTheFullHorizontalSpaceOfTheVoxel()? public boolean presentsAnOptionStrictlyInTheTopHalfOfTheStandardVoxelPlane() { if (Main.DEBUG && against.vertical) { diff --git a/src/main/java/baritone/builder/PlayerPhysics.java b/src/main/java/baritone/builder/PlayerPhysics.java index 4a0c81974..145f129e8 100644 --- a/src/main/java/baritone/builder/PlayerPhysics.java +++ b/src/main/java/baritone/builder/PlayerPhysics.java @@ -24,21 +24,21 @@ public class PlayerPhysics { */ public static int determinePlayerRealSupport(BlockStateCachedData underneath, BlockStateCachedData within) { if (within.collidesWithPlayer) { - if (underneath.supportedPlayerY != null && underneath.supportedPlayerY - Blip.FULL_BLOCK > within.supportedPlayerY) { // TODO > or >= + if (underneath.collisionHeightBlips != null && underneath.collisionHeightBlips - Blip.FULL_BLOCK > within.collisionHeightBlips) { // TODO > or >= if (!underneath.fullyWalkableTop) { return -1; } - return underneath.supportedPlayerY - Blip.FULL_BLOCK; // this could happen if "underneath" is a fence and "within" is a carpet + return underneath.collisionHeightBlips - Blip.FULL_BLOCK; // this could happen if "underneath" is a fence and "within" is a carpet } - if (!within.fullyWalkableTop || within.supportedPlayerY >= Blip.FULL_BLOCK) { + if (!within.fullyWalkableTop || within.collisionHeightBlips >= Blip.FULL_BLOCK) { return -1; } - return within.supportedPlayerY; + return within.collisionHeightBlips; } else { - if (!underneath.fullyWalkableTop || underneath.supportedPlayerY < Blip.FULL_BLOCK) { + if (!underneath.fullyWalkableTop || underneath.collisionHeightBlips < Blip.FULL_BLOCK) { return -1; } - return underneath.supportedPlayerY - Blip.FULL_BLOCK; + return underneath.collisionHeightBlips - Blip.FULL_BLOCK; } } @@ -118,13 +118,13 @@ public class PlayerPhysics { if (!D.collidesWithPlayer) { return Collision.FALL; } - if (Main.DEBUG && D.supportedPlayerY == null) { + if (Main.DEBUG && D.collisionHeightBlips == null) { throw new IllegalStateException(); } - if (Main.DEBUG && D.supportedPlayerY >= Blip.FULL_BLOCK && D.fullyWalkableTop) { + if (Main.DEBUG && D.collisionHeightBlips >= Blip.FULL_BLOCK && D.fullyWalkableTop) { throw new IllegalStateException(); } - if (D.supportedPlayerY < Blip.FULL_BLOCK + feet) { + if (D.collisionHeightBlips < Blip.FULL_BLOCK + feet) { return Collision.FALL; } else { return Collision.BLOCKED; diff --git a/src/main/java/baritone/builder/README.md b/src/main/java/baritone/builder/README.md index 1283b9f04..c5d7162ab 100644 --- a/src/main/java/baritone/builder/README.md +++ b/src/main/java/baritone/builder/README.md @@ -25,4 +25,13 @@ I'm not sure how to think about performance for this. Obviously there are copes One particularly annoying thing is needing to reimplement portions of Minecraft. For example, there is a bit of code that I can easily call that says "If I looked in this direction right now, and right clicked, what block precisely would it place". Because, well, Minecraft needs to decide what block appears if and when I right click. This works great for current Baritone and I can just call it without needing to worry. For this builder though, I need to do hypotheticals. "If I were standing here on top of this block (that doesn't exist yet), looking at this face of this block (which doesn't exist yet), and right clicked while holding this item (which I don't have), what block would appear?". Minecraft code isn't built like that. There are so many conditions based on Y hit, face hit, player orientation, etc. Just look at the trapdoor place logic to instantly die. So I need to reimplement all the code for "I desire this block state (northward facing upside down acacia stairs). How can I make this coordinate contain that block? (well, you need to be facing north and right click the top face of an adjacent block with such a stair)". That code needs to be written for every block that has custom placement like that. The question of "If block X is at coordinate Y, could I right click against that to place block Z on face D" is incredibly frustratingly not easily answerable. It all depends on external variables like the horizontal orientation of the player, and even the Y coordinate of the player (for some blocks like pistons and dispensers). -I'm trying my best to minimize floating point calculations as much as possible. The fast case for the graph search should never use floating point. I'm too annoyed by even simple things like `> 1` being randomly wrong (with `0.99999` or `1.000001`). Also, no need to have path costs be floating point. Both path costs and player Y will be fixed point integers. Costs have a to-be-determined denominator, for player Y it's sixteen. \ No newline at end of file +I'm trying my best to minimize floating point calculations as much as possible. The fast case for the graph search should never use floating point. I'm too annoyed by even simple things like `> 1` being randomly wrong (with `0.99999` or `1.000001`). Also, no need to have path costs be floating point. Both path costs and player Y will be fixed point integers. Costs have a to-be-determined denominator, for player Y it's sixteen. + +I'm going to list stupid things about Minecraft that have pissed me off during this: +* Every `IBlockState` in the game has an entry in `Block.BLOCK_STATE_IDS`. Except for some variants of tripwire. Nothing else. Just tripwire is the exception. And it isn't even for a good or elegant reason. It's just hardcoded in the thing that constructs the block state registry to skip some of the tripwire states. No idea why. +* `Block.BLOCK_STATE_IDS` has double entries. There are many states that map to the same integer ID, when you go back to states it sets certain properties to default. Such as `BlockStairs.SHAPE`. + +Things about old Baritone builder that are stupid and that I will do better: +* The thing I mentioned above about left and right clicking +* Complete nonunderstanding of orientable blocks. It has no idea that in order to place a stair / a torch it has to go walk around and look at the block from the other side. +* Instead of just placing blocks normally, it sneaks before EVERY right click. This makes it look unnatural, stupid, and jittery, for no reason. diff --git a/src/main/java/baritone/builder/Scaffolder.java b/src/main/java/baritone/builder/Scaffolder.java index ed6a7c4f9..17535dbc1 100644 --- a/src/main/java/baritone/builder/Scaffolder.java +++ b/src/main/java/baritone/builder/Scaffolder.java @@ -94,8 +94,9 @@ public class Scaffolder { } private ScaffoldingSearchNode dijkstra(CollapsedDependencyGraphComponent root) { - Set descendents = new ObjectOpenHashSet<>(); - walkAllDescendents(root, descendents); + Set exclusiveDescendents = new ObjectOpenHashSet<>(); + walkAllDescendents(root, exclusiveDescendents); + exclusiveDescendents.remove(root); PriorityQueue openSet = new PriorityQueue<>(Comparator.comparingInt(node -> node.costSoFar)); Long2ObjectOpenHashMap nodeMap = new Long2ObjectOpenHashMap<>(); LongIterator it = root.getPositions().iterator(); @@ -108,12 +109,15 @@ public class Scaffolder { ScaffoldingSearchNode node = openSet.poll(); CollapsedDependencyGraphComponent tentativeComponent = componentLocations.get(node.pos); if (tentativeComponent != null) { - if (descendents.contains(tentativeComponent)) { + if (exclusiveDescendents.contains(tentativeComponent)) { // have gone back onto a descendent of this node // sadly this can happen even at the same Y level even in Y_STRICT mode due to orientable blocks forming a loop continue; // TODO does this need to be here? can I expand THROUGH an unrelated component? probably requires testing, this is quite a mind bending possibility } else { - return node; // all done! found a path to a component unrelated to this one, meaning we have successfully connected this part of the build with scaffolding back to the rest of it + // found a path to a component that isn't a descendent of the root + if (tentativeComponent != root) { // but if it IS the root, then we're just on our first loop iteration, we are far from done + return node; // all done! found a path to a component unrelated to this one, meaning we have successfully connected this part of the build with scaffolding back to the rest of it + } } } for (Face face : Face.VALUES) { @@ -127,7 +131,7 @@ public class Scaffolder { // we can accomplish this and kill two birds with one stone by skipping all nodes already in the node map // any position in the initial frontier is clearly in the node map, but also any node that has already been considered // this prevents useless cycling of equivalent paths - // this is okay because our search has no heuristic so this is a uniform cost search + // this is okay because all paths are equivalent, so there is no possible way to find a better path (because currently it's a fixed value for horizontal / vertical movements) if (existingNode.costSoFar < newCost) { throw new IllegalStateException(); } @@ -157,7 +161,7 @@ public class Scaffolder { return 2; } - private class ScaffoldingSearchNode { + private static class ScaffoldingSearchNode { private final long pos; private int costSoFar; diff --git a/src/main/java/baritone/builder/Vec3d.java b/src/main/java/baritone/builder/Vec3d.java index 5c6f513c6..d30472343 100644 --- a/src/main/java/baritone/builder/Vec3d.java +++ b/src/main/java/baritone/builder/Vec3d.java @@ -38,6 +38,14 @@ public class Vec3d { } } + public boolean inOriginUnitVoxel() { + return x >= 0 && x <= 1 && y >= 0 && y <= 1 && z >= 0 && z <= 1; + } + + public Vec3d plus(double x, double y, double z) { + return new Vec3d(this.x + x, this.y + y, this.z + z); + } + public long getRoundedToZeroPositionUnsafeDontUse() { return BetterBlockPos.toLong((int) x, (int) y, (int) z); } diff --git a/src/main/java/baritone/builder/mc/BlockStatePropertiesExtractor.java b/src/main/java/baritone/builder/mc/BlockStatePropertiesExtractor.java index 6e8006ee8..607079786 100644 --- a/src/main/java/baritone/builder/mc/BlockStatePropertiesExtractor.java +++ b/src/main/java/baritone/builder/mc/BlockStatePropertiesExtractor.java @@ -20,10 +20,9 @@ package baritone.builder.mc; import baritone.builder.*; import net.minecraft.block.*; import net.minecraft.block.state.IBlockState; -import net.minecraft.init.Blocks; +import net.minecraft.util.EnumFacing; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; @@ -38,9 +37,12 @@ public class BlockStatePropertiesExtractor { Block block = state.getBlock(); BlockStateCachedDataBuilder builder = new BlockStateCachedDataBuilder(); + // returns null only if we don't know how to walk on / walk around / place against this block + // if we don't know how to place the block, get everything else but add .placementLogicNotImplementedYe + // special cases { - if (block instanceof BlockAir) { + if (block instanceof BlockAir || block instanceof BlockStructureVoid) { return builder.setAir(); } if (block instanceof BlockStairs) { @@ -58,30 +60,23 @@ public class BlockStatePropertiesExtractor { }; if (!rightsideUp) { stairBuilder.fullyWalkableTop(); - stairBuilder.height(1); + stairBuilder.collisionHeight(1); } return stairBuilder.mustBePlacedAgainst(rightsideUp ? Half.BOTTOM : Half.TOP) .collidesWithPlayer(true) .canPlaceAgainstMe() - .playerMustBeFacingInOrderToPlaceMe(facing); + .playerMustBeHorizontalFacingInOrderToPlaceMe(facing); } if (block instanceof BlockSlab) { - double height; - Half mustBePlacedAgainst; if (((BlockSlab) block).isDouble()) { - height = 1; - mustBePlacedAgainst = Half.EITHER; + builder.placementLogicNotImplementedYet().collisionHeight(1); } else if (state.getValue(BlockSlab.HALF) == BlockSlab.EnumBlockHalf.BOTTOM) { - height = 0.5; - mustBePlacedAgainst = Half.BOTTOM; + builder.mustBePlacedAgainst(Half.BOTTOM).collisionHeight(0.5); } else { - height = 1; - mustBePlacedAgainst = Half.TOP; + builder.mustBePlacedAgainst(Half.TOP).collisionHeight(1); } return builder - .mustBePlacedAgainst(mustBePlacedAgainst) .fullyWalkableTop() - .height(height) .canPlaceAgainstMe() .collidesWithPlayer(true); } @@ -93,12 +88,52 @@ public class BlockStatePropertiesExtractor { public List howCanIBePlaced() { List ret = new ArrayList<>(); if (!(Main.STRICT_Y && !bottom)) { - ret.add(BlockStatePlacementOption.get(bottom ? Face.DOWN : Face.UP, Half.EITHER, Optional.ofNullable(facing.opposite()))); + ret.add(BlockStatePlacementOption.get(bottom ? Face.DOWN : Face.UP, Half.EITHER, Optional.ofNullable(facing.opposite()), Optional.empty())); } - ret.add(BlockStatePlacementOption.get(facing.opposite(), bottom ? Half.BOTTOM : Half.TOP, Optional.empty())); - return Collections.unmodifiableList(ret); + ret.add(BlockStatePlacementOption.get(facing.opposite(), bottom ? Half.BOTTOM : Half.TOP, Optional.empty(), Optional.empty())); + return ret; } - }.collidesWithPlayer(true); // TODO allow walking on top of closed top-half trapdoor? lol + }.collidesWithPlayer(true); // dont allow walking on top of closed top-half trapdoor because redstone activation is scary and im not gonna predict it + } + if (block instanceof BlockLog) { + BlockLog.EnumAxis axis = state.getValue(BlockLog.LOG_AXIS); + BlockStateCachedDataBuilder logBuilder = new BlockStateCachedDataBuilder() { + @Override + public List howCanIBePlaced() { + List ret = super.howCanIBePlaced(); + ret.removeIf(place -> BlockLog.EnumAxis.fromFacingAxis(place.against.toMC().getAxis()) != axis); + return ret; + } + }; + if (axis == BlockLog.EnumAxis.NONE) { + logBuilder.placementLogicNotImplementedYet(); // ugh + } + return logBuilder + .fullyWalkableTop() + .collisionHeight(1) + .canPlaceAgainstMe() + .collidesWithPlayer(true); + } + if (block instanceof BlockRotatedPillar) { // hay block, bone block + // even though blocklog inherits from blockrotatedpillar it uses its own stupid enum, ugh + // this is annoying because this is pretty much identical + EnumFacing.Axis axis = state.getValue(BlockRotatedPillar.AXIS); + BlockStateCachedDataBuilder rotatedPillarBuilder = new BlockStateCachedDataBuilder() { + @Override + public List howCanIBePlaced() { + List ret = super.howCanIBePlaced(); + ret.removeIf(place -> place.against.toMC().getAxis() != axis); + return ret; + } + }; + return rotatedPillarBuilder + .fullyWalkableTop() + .collisionHeight(1) + .canPlaceAgainstMe() + .collidesWithPlayer(true); + } + if (block instanceof BlockStructure) { + return null; // truly an error to encounter this } } @@ -107,14 +142,14 @@ public class BlockStatePropertiesExtractor { // rotated clockwise about the Y axis if (block instanceof BlockAnvil) { - builder.playerMustBeFacingInOrderToPlaceMe(Face.fromMC(state.getValue(BlockAnvil.FACING).rotateYCCW())); + builder.playerMustBeHorizontalFacingInOrderToPlaceMe(Face.fromMC(state.getValue(BlockAnvil.FACING).rotateYCCW())); } // unchanged if (block instanceof BlockChest // it is not right || block instanceof BlockSkull // TODO is this right?? skull can be any facing? ) { // TODO fence gate and lever - builder.playerMustBeFacingInOrderToPlaceMe(Face.fromMC(state.getValue(BlockHorizontal.FACING))); + builder.playerMustBeHorizontalFacingInOrderToPlaceMe(Face.fromMC(state.getValue(BlockHorizontal.FACING))); } // opposite @@ -126,12 +161,27 @@ public class BlockStatePropertiesExtractor { || block instanceof BlockPumpkin || block instanceof BlockRedstoneDiode // both repeater and comparator ) { - builder.playerMustBeFacingInOrderToPlaceMe(Face.fromMC(state.getValue(BlockHorizontal.FACING)).opposite()); + builder.playerMustBeHorizontalFacingInOrderToPlaceMe(Face.fromMC(state.getValue(BlockHorizontal.FACING)).opposite()); } } - // getStateForPlacement.against is the against face. placing a torch will have it as UP. placing a bottom slab will have it as UP. placing a top slab will have it as DOWN. // ladder + { + if (block instanceof BlockContainer || block instanceof BlockWorkbench) { + builder.mustSneakWhenPlacingAgainstMe(); + } + } + + if (block instanceof BlockCommandBlock + || block instanceof BlockDispenser // dropper extends from dispenser + || block instanceof BlockPistonBase) { + builder.playerMustBeEntityFacingInOrderToPlaceMe(Face.fromMC(state.getValue(BlockDirectional.FACING))); + } + + if (block instanceof BlockObserver) { + builder.playerMustBeEntityFacingInOrderToPlaceMe(Face.fromMC(state.getValue(BlockDirectional.FACING)).opposite()); + } + // fully passable blocks { // some ways of determining this list that don't work: @@ -159,39 +209,75 @@ public class BlockStatePropertiesExtractor { } } - // TODO getDirectionFromEntityLiving + if (block instanceof BlockFalling) { + builder.falling(); + } + // TODO multiblocks like door and bed and double plant boolean fullyUnderstood = false; // set this flag to true for any state for which we have fully and completely described it + // getStateForPlacement.against is the against face. placing a torch will have it as UP. placing a bottom slab will have it as UP. placing a top slab will have it as DOWN. + if (block instanceof BlockTorch) { // includes redstone torch + builder.canOnlyPlaceAgainst(Face.fromMC(state.getValue(BlockTorch.FACING)).opposite()); + fullyUnderstood = true; + } + + if (block instanceof BlockShulkerBox) { + builder.canOnlyPlaceAgainst(Face.fromMC(state.getValue(BlockShulkerBox.FACING)).opposite()); + fullyUnderstood = true; + } + + if (block instanceof BlockFenceGate // because if we place it we need to think about hypothetically walking through it + || block instanceof BlockLever + || block instanceof BlockButton + || block instanceof BlockBanner) { + builder.placementLogicNotImplementedYet(); + fullyUnderstood = true; + } + + if (block instanceof BlockSapling + || block instanceof BlockRedstoneWire + || block instanceof BlockRailBase + || block instanceof BlockFlower + || block instanceof BlockDeadBush + ) { + fullyUnderstood = true; + } + + //isBlockNormalCube=true implies isFullCube=true if (state.isBlockNormalCube() || state.isFullBlock() || block instanceof BlockGlass || block instanceof BlockStainedGlass) { builder.canPlaceAgainstMe(); fullyUnderstood = true; } - if (state.isBlockNormalCube()) { - builder.fullyWalkableTop().height(1); + if ((state.isBlockNormalCube() || block instanceof BlockGlass || block instanceof BlockStainedGlass) && !(block instanceof BlockMagma || block instanceof BlockSlime)) { + builder.fullyWalkableTop().collisionHeight(1); fullyUnderstood = true; } if (block instanceof BlockSnow) { fullyUnderstood = true; if (state.getValue(BlockSnow.LAYERS) > 1) { // collidesWithPlayer false from earlier - builder.fullyWalkableTop().height(0.125 * (state.getValue(BlockSnow.LAYERS) - 1)); + builder.fullyWalkableTop().collisionHeight(0.125 * (state.getValue(BlockSnow.LAYERS) - 1)); // funny - if you have snow layers packed 8 high, it only supports the player to a height of 0.875, but it still counts as "isTopSolid" for placing stuff like torches on it } } if (block instanceof BlockSoulSand) { - builder.height(0.875); + builder.collisionHeight(0.875).fakeLessThanFullHeight(); + fullyUnderstood = true; } // TODO fully walkable top and height - - return builder; + if (fullyUnderstood) { + return builder; + } else { + return null; + } } } diff --git a/src/main/java/baritone/builder/mc/DebugStates.java b/src/main/java/baritone/builder/mc/DebugStates.java new file mode 100644 index 000000000..2bf3fdf83 --- /dev/null +++ b/src/main/java/baritone/builder/mc/DebugStates.java @@ -0,0 +1,108 @@ +/* + * 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 . + */ + +package baritone.builder.mc; + +import baritone.builder.BlockStateCachedData; +import net.minecraft.block.Block; +import net.minecraft.block.state.IBlockState; +import net.minecraft.init.Blocks; + +import java.util.*; +import java.util.stream.Stream; + +public class DebugStates { + + public static void debug() { + Map> bts = new HashMap<>(); + Map> byString = new HashMap<>(); + for (IBlockState state : Block.BLOCK_STATE_IDS) { + //System.out.println(state); + bts.computeIfAbsent(state.getBlock(), $ -> new ArrayList<>()).add(state); + String str = toString(BlockStateCachedData.get(Block.BLOCK_STATE_IDS.get(state))); + byString.computeIfAbsent(str, $ -> new ArrayList<>()).add(state); + } + for (String key : (Iterable) byString.keySet().stream().sorted()::iterator) { + System.out.println("\n"); + System.out.println(key); + Set skip = new HashSet<>(); + List matches = byString.get(key); + for (IBlockState state : matches) { + if (skip.contains(state.getBlock())) { + continue; + } + if (matches.containsAll(bts.get(state.getBlock()))) { + System.out.println("All " + bts.get(state.getBlock()).size() + " variants of " + state.getBlock()); + skip.add(state.getBlock()); + } else { + System.out.println(state); + } + } + + } + /*System.out.println(Blocks.OAK_STAIRS.getDefaultState()); + System.out.println(Block.BLOCK_STATE_IDS.get(Blocks.OAK_STAIRS.getDefaultState())); + System.out.println(Block.BLOCK_STATE_IDS.getByValue(Block.BLOCK_STATE_IDS.get(Blocks.OAK_STAIRS.getDefaultState())));*/ + Set normal = new HashSet<>(); + Block.REGISTRY.iterator().forEachRemaining(normal::add); + Set alternate = new HashSet<>(); + for (IBlockState state : Block.BLOCK_STATE_IDS) { + alternate.add(state.getBlock()); + } + if (!alternate.equals(normal)) { + throw new IllegalStateException(); + } + outer: + for (Block block : normal) { + for (IBlockState state : block.getBlockState().getValidStates()) { + if (Block.BLOCK_STATE_IDS.get(state) == -1) { + System.out.println(state + " doesn't exist?!"); + continue; + } + if (block == Blocks.TRIPWIRE) { + System.out.println(state + " does exist"); + } + if (!Block.BLOCK_STATE_IDS.getByValue(Block.BLOCK_STATE_IDS.get(state)).equals(state)) { + System.out.println(block + " is weird"); + continue outer; + } + } + } + } + + private static String toString(BlockStateCachedData data) { + if (data == null) { + return "UNKNOWN"; + } + Map props = new LinkedHashMap<>(); + props(data, props); + return props.toString(); + } + + private static void props(BlockStateCachedData data, Map props) { + if (data.isAir) { + props.put("air", "true"); + return; + } + props.put("collides", "" + data.collidesWithPlayer); + props.put("walkabletop", "" + data.fullyWalkableTop); + props.put("placeme", "" + data.options.size()); + props.put("sneak", "" + data.mustSneakWhenPlacingAgainstMe); + props.put("againstme", "" + Stream.of(data.againstMe).filter(Objects::nonNull).count()); + props.put("y", "" + data.collisionHeightBlips); + } +} diff --git a/src/main/java/baritone/builder/mc/VanillaBlockStateDataProvider.java b/src/main/java/baritone/builder/mc/VanillaBlockStateDataProvider.java index 4ac13604a..099adc43b 100644 --- a/src/main/java/baritone/builder/mc/VanillaBlockStateDataProvider.java +++ b/src/main/java/baritone/builder/mc/VanillaBlockStateDataProvider.java @@ -18,6 +18,7 @@ package baritone.builder.mc; import baritone.builder.BlockStateCachedData; +import baritone.builder.BlockStateCachedDataBuilder; import baritone.builder.IBlockStateDataProvider; import net.minecraft.block.Block; import net.minecraft.block.state.IBlockState; @@ -36,7 +37,12 @@ public class VanillaBlockStateDataProvider implements IBlockStateDataProvider { return null; } try { - return new BlockStateCachedData(BlockStatePropertiesExtractor.getData(state)); + BlockStateCachedDataBuilder builder = BlockStatePropertiesExtractor.getData(state); + if (builder == null) { + return null; + } else { + return new BlockStateCachedData(builder); + } } catch (Throwable th) { throw new RuntimeException("Exception while extracting " + state + " ID " + i, th); }