Last week I fixed a bug related to butterflies landing by rendering debug information in the world. This week I use that same concept to fix another bug. This time, it’s the caterpillars that are behaving badly and living on the edge.
A user that calls themselves D3VILKITTEN has helpfully reported a couple of bugs recently. The one I looked at this week is related to caterpillars and the way they move around the block they are attached to:
Caterpillars will find their way to the edge of leaf blocks and just stop moving, won’t cocoon, and simply stay still forever. It’s all caterpillars, not a specific one. Like their trying to path to a spot they can’t get to and just never stop.
CurseForge
Well, the first thing to do was to try and reproduce the bug. From the description I believed this would be a weird corner case, and hard to reproduce. As it turns out, after loading into a test world and placing around 12 caterpillars, 2 of them would get trapped on the edge of their block within 5 minutes.
This is both good and bad. Bad because it happens a lot more frequently than I thought. But good, since being able to reproduce the problem so easily, would allow me to debug and fix the bug quickly. But first I needed a plan.
MOAR Debug Info
Last week I added a way of rendering debug information over each butterfly. I wanted to use the same method for the caterpillars, so I created a generic helper class that would hold the renderDebugInfo()
method. This way I can use the same code for both caterpillar and butterfly entities.
/** * Helper class that provides debug rendering to entities. */ @OnlyIn(Dist.CLIENT) public class EntityDebugInfoRenderer { /** * Renders debug information for the butterfly. * @param entity The butterfly entity. * @param poseStack The current pose stack. * @param multiBufferSource The render buffer. * @param cameraOrientation The current camera orientation. * @param font The font to use for rendering. * @param packedLightCoordinates The light coordinates. */ public static <T extends Entity & DebugInfoSupplier> void renderDebugInfo(T entity, PoseStack poseStack, MultiBufferSource multiBufferSource, Quaternionf cameraOrientation, Font font, int packedLightCoordinates) { if (ButterfliesConfig.debugInformation.get()) { String debugInfo = entity.getDebugInfo(); if (!debugInfo.isBlank()) { MutableComponent component = Component.literal(debugInfo); float nameTagOffsetY = entity.getNameTagOffsetY(); poseStack.pushPose(); poseStack.translate(0.0F, nameTagOffsetY, 0.0F); poseStack.mulPose(cameraOrientation); poseStack.scale(-0.025F, -0.025F, 0.025F); Matrix4f pose = poseStack.last().pose(); float backgroundOpacity = Minecraft.getInstance().options.getBackgroundOpacity(0.25F); int alpha = (int) (backgroundOpacity * 255.0F) << 24; float fontWidth = (float) (-font.width(component) / 2); font.drawInBatch(component, fontWidth, 0, 553648127, false, pose, multiBufferSource, Font.DisplayMode.SEE_THROUGH, alpha, packedLightCoordinates); font.drawInBatch(component, fontWidth, 0, -1, false, pose, multiBufferSource, Font.DisplayMode.NORMAL, 0, packedLightCoordinates); poseStack.popPose(); } } } }
The only changes to the code are the cameraOrientation
and font
parameters. Originally these were just pulled from the renderer’s base class, but since they are protected they can’t be accessed here. So I pass them in as parameters instead.
The other change is the template parameter for the entity. It’s required to be an Entity
so that we have access to the class’s methods, but it also needs to implement the DebugInfoSupplier
interface. This is just a simple interface that requires the subclass implements the getDebugInfo()
method.
/** * Interface for providing debug information. */ public interface DebugInfoSupplier { String getDebugInfo(); }
I rename the getGoalState()
method from last week to getDebugInfo()
in order to support this in the Butterfly
class. Now I can also add it to the Caterpillar
class as well. Caterpillars don’t use goals like butterflies do, so I use this method to output the position and certain other states. I intentionally output the block position, since I suspect this to be the root cause of the bug in the first place.
/** * Get the current state, used for debugging. * @return The current goal state. */ @Override public String getDebugInfo() { return "Position = [" + String.format("%.3f", this.getX() % 1.f) + ", " + String.format("%.3f", this.getY() % 1.f) + ", " + String.format("%.3f", this.getZ() % 1.f) + "] / BlockPos = [" + this.blockPosition() + "] / SurfaceBlock = [" + this.getSurfaceBlockPos() + "] / SurfaceDirection = [" + this.getSurfaceDirection().getName() + "] / IsNoGravity = [" + this.isNoGravity + "]"; }
For the position I strip away all but the position on the block itself. I also only display it to 3 decimal places. This makes it much easier to read in-game, and strips away irrelevant information that would only be distracting.
Detective Work
Now I can load into the game and see the current state of each caterpillar, including their block position, position within the block, and the direction they are facing. Now all I needed to do was wait.
Eventually one of the caterpillars would stop moving, and it was obvious what the problem was. By moving to the edge of the block, the block position changes. This means that the block the caterpillar checks to see if it can do anything is no longer the block it was crawling on. And if that block is invalid, the caterpillar just… stops…
It was obvious that the solution was to prevent this from happening. The problem was, how?
Solutions
I tried a few things before I settled on a solution that worked. The bug occurs when a caterpillar moves to the edge of a block, and it thinks the next block along is the block that it is standing on. So to fix the bug, I had to prevent the caterpillar from moving to the edge of the block.
I tried three different solutions. Each time I would load into the game and do a soak test. This means I would just leave the game running and check it periodically to see if the bug occurs again. If it does, then it proves the fix doesn’t work.
Clamp Target Position
If the caterpillar doesn’t try to move to the edge, then it won’t ever move to the edge, right? This was the first theory I would work with. I started by implementing some helper methods, believing I’d be using these more often.
/** * Helper method to return a clamped double. * @return A double value between 0.05 and 0.95. */ private double clampedDouble(double x) { return Math.max(0.05, Math.min(0.95, x)); } /** * Helper method to generate a clamped random double. * @return A random clamped double. */ private double clampedRandomDouble() { return clampedDouble(this.random.nextDouble() % 1.0); }
Now I just updated the method where it determines the target position to use these methods instead.
if (axis == Direction.Axis.X) { this.targetPosition = new Vec3( this.targetPosition.x(), Math.floor(this.targetPosition.y()) + clampedRandomDouble(), Math.floor(this.targetPosition.z()) + clampedRandomDouble()); } else if (axis == Direction.Axis.Y) { this.targetPosition = new Vec3( Math.floor(this.targetPosition.x()) + clampedRandomDouble(), this.targetPosition.y(), Math.floor(this.targetPosition.z()) + clampedRandomDouble()); } else { this.targetPosition = new Vec3( Math.floor(this.targetPosition.x()) + clampedRandomDouble(), Math.floor(this.targetPosition.y()) + clampedRandomDouble(), this.targetPosition.z()); }
After a short soak test it was obvious this didn’t work. It took a little longer, but caterpillars could and would still reach the edge and get stuck.
I realised that the reason for this is that the velocity and target position are entirely separate. If the velocity of the caterpillar is high enough, it could overshoot the target position and still reach the edge. This led me to a second idea for a solution.
Stop If Out of Bounds
If the velocity is the problem, then just removing the velocity should work. To test this theory, I added a short piece of code at the end of the updatedDeltaMovement()
method. This method determines the velocity of the caterpillars, so we should be able to stop if they get too close to the edge. The code I added simply sets the speed in a certain direction to zero if a caterpillar is about to move out of bounds.
// Clamp the speed if the caterpillar is too close to the edge. if (this.getX() % 1.0 > 0.95 || this.getX() < 0.05) { updatedDeltaMovement.multiply(0.0, 1.0, 1.0); } if (this.getY() % 1.0 > 0.95 || this.getY() < 0.05) { updatedDeltaMovement.multiply(1.0, 0.0, 1.0); } if (this.getZ() % 1.0 > 0.95 || this.getZ() < 0.05) { updatedDeltaMovement.multiply(1.0, 1.0, 0.0); }
Unfortunately, a short test with this code also proved that it didn’t work. I’m not sure why exactly this one failed, but my guess would be that the caterpillars are still moving fast enough that these bounds are too low. I didn’t want to change these too much, however, as I didn’t want the caterpillars to congregate on the center of a block.
Then it hit me. The actual solution to this problem was obvious.
Just Don’t Set The Position
The root of the problem is the caterpillar’s Block Position changes when it’s position is updated. So the real solution is actually very simple: just don’t set the position if it would change the block position. I looked at the SetPos()
method in the Entity
class to see how it calculated the block position. I then wrote an override that uses the exact same calculation to block the update if the block position would change.
/** * Prevent a caterpillar's block position from changing. * @param x The x-position. * @param y The y-position. * @param z The z-position. */ @Override public void setPos(double x, double y, double z) { // Set the initial position. if (this.getX() == 0 && this.getY() == 0 && this.getZ() == 0) { super.setPos(x, y, z); } // Only set the position if the block position doesn't change. if ( Mth.floor(x) == this.blockPosition().getX() && Mth.floor(y) == this.blockPosition().getY() && Mth.floor(z) == this.blockPosition().getZ()) { super.setPos(x, y, z); } }
With this method, the super is only called if the block position won’t change. It will also allow it to be called if the current position of the entity is (0, 0, 0)
. This is so that the starting position can be set when the entity spawns, otherwise all the entities will congregate in the default position and never move.
I ran a longer soak test with this one, and after around 30 minutes I didn’t see the bug appear again. While this doesn’t 100% prove that the bug is fixed, it’s long enough that I’m confident the fix works.
The End?
That’s one more bug fixed, so I’m happy with the progress. However, D3VILKITTEN has reported another bug involving the Butterfly Book. So next week, I’ll be working on that.
I’m always grateful for bug reports: they help me improve the mod, and make it more stable for everyone who is playing it. So gratitude goes out to D3VILKITTEN for this one, and also to anyone else who has reported a bug this far.