Powder Snow Axis-Order Bug (Paper 1.21.5)
Description
When tnt travels and collides with a block there is a chance it gets effected by powdered snow it doesnt travel through.
The reason for that is that the block effect is done after the calculation and uses the traveled distance to calculate the path it will use for that.
Reason
The bug is caused by an axis-order mismatch between:
- collision resolution path, and
- inside-block effect traversal path (used for powder snow checks).
When a collision clips one horizontal component (for example x) enough to flip |x| vs |z|, the inside-block traversal path is rebuilt with a different axis order than the one used to resolve movement. This can make entities process powder snow blocks they did not actually traverse on their resolved movement path.
Important code path and snippets
1) TNT tick order uses move(...) first, then applyEffectsFromBlocks()
File: net\minecraft\world\entity\item\PrimedTnt.java (lines 96-101)
this.applyGravity();
this.move(MoverType.SELF, this.getDeltaMovement());
this.applyEffectsFromBlocks();
this.setDeltaMovement(this.getDeltaMovement().scale(0.98));
So inside-block effects (including powder snow) are evaluated after movement using recorded movement segments from Entity.move.
The same post-move call pattern exists for non-TNT entities too (example):
File: net\minecraft\world\entity\LivingEntity.java (lines 2780-2784)
this.travel(vec3);
...
this.applyEffectsFromBlocks();
2) Collision stepping order is chosen from the input movement vector
File: net\minecraft\world\entity\Entity.java (lines 1053-1067)
private static final ImmutableList<Direction.Axis> YXZ_AXIS_ORDER = ImmutableList.of(Direction.Axis.Y, Direction.Axis.X, Direction.Axis.Z);
private static final ImmutableList<Direction.Axis> YZX_AXIS_ORDER = ImmutableList.of(Direction.Axis.Y, Direction.Axis.Z, Direction.Axis.X);
for (Direction.Axis axis : axisStepOrder(deltaMovement)) {
double d = deltaMovement.get(axis);
if (d != 0.0) {
double d1 = Shapes.collide(axis, entityBB.move(vec3), shapes, d);
vec3 = vec3.with(axis, d1);
}
}
private static Iterable<Direction.Axis> axisStepOrder(Vec3 deltaMovement) {
return Math.abs(deltaMovement.x) < Math.abs(deltaMovement.z) ? YZX_AXIS_ORDER : YXZ_AXIS_ORDER;
}
With (x=10, y=5, z=9), this chooses YXZ.
3) Movement recording for block-effects is rebuilt using the post-collision vector
File: net\minecraft\world\entity\Entity.java (lines 658, 671-680)
Vec3 vec3 = this.collide(movement); // resolved movement after collisions
Vec3 vec31 = this.position();
List<Entity.Movement> list = new ObjectArrayList<>();
for (Direction.Axis axis : axisStepOrder(vec3)) {
double d1 = vec3.get(axis);
if (d1 != 0.0) {
Vec3 vec32 = vec31.relative(axis.getPositive(), d1);
list.add(new Entity.Movement(vec31, vec32));
vec31 = vec32;
}
}
This is the key bug source: collision path order is derived from movement, but effect path order is derived from vec3.
4) Those recorded segments are the ones used for inside-block checks
File: net\minecraft\world\entity\Entity.java (lines 772-783, 795-806)
this.finalMovementsThisTick.clear();
this.movementThisTick.forEach(this.finalMovementsThisTick::addAll);
...
this.applyEffectsFromBlocks(this.finalMovementsThisTick);
...
this.checkInsideBlocks(movements, this.insideEffectCollector);
this.insideEffectCollector.applyAndClear(this);
So any ordering error in movementThisTick directly affects which blocks run entityInside(...).
5) Block traversal is done along each segment path (step-based)
File: net\minecraft\world\entity\Entity.java (lines 1089-1124)
for (Entity.Movement movement : movements) {
Vec3 vec3 = movement.from();
Vec3 vec31 = movement.to();
AABB aabb = this.makeBoundingBox(vec31).deflate(1.0E-5F);
BlockGetter.forEachBlockIntersectedBetween(vec3, vec31, aabb, (pos, step) -> {
...
stepBasedCollector.advanceStep(step);
blockState.entityInside(this.level(), pos, this, stepBasedCollector);
...
});
}
File: net\minecraft\world\level\BlockGetter.java (lines 177-189)
static void forEachBlockIntersectedBetween(Vec3 from, Vec3 to, AABB boundingBox, BlockGetter.BlockStepVisitor stepVisitor) {
Vec3 vec3 = to.subtract(from);
...
int i = addCollisionsAlongTravel(set, vec31, minPosition, boundingBox, stepVisitor);
...
}
This traversal is path-sensitive; if segment order changes, visited blocks can change.
6) Powder snow applies movement-stuck + freeze effects when visited
File: net\minecraft\world\level\block\PowderSnowBlock.java (lines 62-65, 94-95)
if (!(entity instanceof LivingEntity) || entity.getInBlockState().is(this)) {
entity.makeStuckInBlock(state, new Vec3(0.9F, 1.5, 0.9F));
}
...
effectApplier.apply(InsideBlockEffectType.FREEZE);
effectApplier.apply(InsideBlockEffectType.EXTINGUISH);
7) Why this looks like “velocity reset”
File: net\minecraft\world\entity\Entity.java (lines 651-654)
if (this.stuckSpeedMultiplier.lengthSqr() > 1.0E-7) {
movement = movement.multiply(this.stuckSpeedMultiplier);
this.stuckSpeedMultiplier = Vec3.ZERO;
this.setDeltaMovement(Vec3.ZERO);
}
makeStuckInBlock(...) sets stuckSpeedMultiplier; then on next move call, the engine zeroes deltaMovement after applying multiplier to this tick's movement input. For entities like TNT, this manifests as abrupt speed loss / reset behavior.
8) makeStuckInBlock(...) is the write point used by powder snow
File: net\minecraft\world\entity\Entity.java (lines 2591-2593)
public void makeStuckInBlock(BlockState state, Vec3 motionMultiplier) {
this.resetFallDistance();
this.stuckSpeedMultiplier = motionMultiplier;
}
Walkthrough of a concrete case (y=5, x=10, z=9)
- Input movement magnitude picks
YXZ(|x|=10 > |z|=9) for collision stepping. - X collision clips horizontal result to
x=5, final resolved vector becomes roughly(x=5, y=5, z=9). - Movement recording then recomputes order from resolved vector and picks
YZX(|x|=5 < |z|=9). - So:
- collision-resolved path:
Y -> X -> Z - inside-effect traversal path:
Y -> Z -> X
- collision-resolved path:
checkInsideBlocks(...)traverses the second path, not the first, so powder snow detection can happen on a path segment the collision solver did not use.- Once powder snow is incorrectly visited,
makeStuckInBlock(...)+ subsequentsetDeltaMovement(Vec3.ZERO)creates the observed “velocity reset”.
Root cause statement
The core bug is that Entity.move(...) records inside-block traversal segments using axisStepOrder(vec3) (post-collision vector), while movement collision itself is resolved using axisStepOrder(movement) (pre-collision vector). When collisions change horizontal component magnitudes enough to flip x/z priority, inside-block checks follow a different axis path than actual movement resolution.
Additional Paper-specific note
Paper patches in Entity.java.patch and InsideBlockEffectApplier.java.patch add block-position tracking (advanceStep(step, pos)) for effect callbacks, but the axis-order mismatch above comes from the vanilla 1.21 movement/effect path split described in the snippets above.