Skip to content

Commit

Permalink
feat: implement liquid motion
Browse files Browse the repository at this point in the history
this commit also has some bug fixes, see changelog for details
  • Loading branch information
smartcmd committed Dec 30, 2024
1 parent 5ddfe1c commit a232734
Show file tree
Hide file tree
Showing 14 changed files with 317 additions and 46 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ Unless otherwise specified, any version comparison below is the comparison of se
- (API) Added an extra argument to `Dimension#breakBlock` method to control if the block breaking particle should be
played.
- (API) Added `LiquidHardenEvent#setHardenedBlockState` method to allow changing the hardened block state.
- (API) Introduced `MathUtils#normalizeIfNotZero` method to normalize a vector only if it is not zero, this method
prevents NaN caused by `Vector3fc#normalize` method.
- (API) Introduced `EntityBaseComponent#computeLiquidMotion` method to control whether an entity has liquid motion.
- (API) Introduced `EntityDamageComponent#hasDrowningDamage` method to control whether an entity has drowning damage.
- Added liquid motion for water and lava. Now entity will be moved by liquid flow if it is in the liquid.

### Changed

Expand All @@ -29,11 +34,18 @@ Unless otherwise specified, any version comparison below is the comparison of se

- (API) Corrected the return type of `Dimension#breakBlock(Vector3ic, ItemStack, Entity)` from `void` to `boolean`. Some
overloads for this method are also added.
- (API) Fixed incorrect bit operations in `BlockLiquidBaseComponent#getLiquidBlockState` and
`BlockLiquidBaseComponent#getLiquidBlockState#getDepth`,
although it seems that they do not cause any issues.
- Block breaking particle won't be sent if block is broken by flowing liquid.
- Water placed in nether dimension will disappear immediately now.
- `Pos`, `Motion` and `Rotation` in entity nbt are now saved as list tag instead of compound tag to match vanilla.
This also fixed the bug that entities being spawned in incorrect position when placing structure using `/structure`
command. Please note that this change is not backward compatible and will break the old world and player data.
- Fixed several NaNs caused by `Vector3fc#normalize` methods in the physics engine, and now setting the motion of an
entity to a vector which contains NaN will result in an error.
- EntityItem now won't have drowning damage when it is in water, this bug causes entity item died after a period of time
in water.

## 0.1.1 (API 0.2.0)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ static int getDepth(BlockState blockState) {
if (isFalling(blockState) || isSource(blockState)) {
return 8;
}
return 8 - blockState.getPropertyValue(BlockPropertyTypes.LIQUID_DEPTH) & 0b0111;
return 8 - (blockState.getPropertyValue(BlockPropertyTypes.LIQUID_DEPTH) & 0b0111);
}

/**
Expand All @@ -61,7 +61,7 @@ static boolean isSource(BlockState blockState) {
* @return the block state of the liquid block with given depth and falling state.
*/
default BlockState getLiquidBlockState(int depth, boolean falling) {
return getBlockType().ofState(BlockPropertyTypes.LIQUID_DEPTH.createValue(falling ? 0b1000 | 8 - depth : 8 - depth));
return getBlockType().ofState(BlockPropertyTypes.LIQUID_DEPTH.createValue(falling ? 0b1000 | (8 - depth) : 8 - depth));
}

/**
Expand Down
11 changes: 5 additions & 6 deletions api/src/main/java/org/allaymc/api/block/data/BlockFace.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,9 @@ public enum BlockFace {
WEST(1, new Vector3i(-1, 0, 0)),
EAST(3, new Vector3i(1, 0, 0));

private static final BlockFace[] STAIR_DIRECTION_VALUE_TO_BLOCK_FACE = new BlockFace[]{
BlockFace.EAST, BlockFace.WEST,
BlockFace.SOUTH, BlockFace.NORTH
};
private static final BlockFace[] HORIZONTAL_BLOCK_FACES = {NORTH, EAST, SOUTH, WEST};
private static final BlockFace[] VERTICAL_BLOCK_FACES = {UP, DOWN};
private static final BlockFace[] STAIR_DIRECTION_VALUE_TO_BLOCK_FACE = {EAST, WEST, SOUTH, NORTH};

private final int horizontalIndex;
private final Vector3ic offset;
Expand Down Expand Up @@ -63,7 +62,7 @@ public static BlockFace fromId(int value) {
* @return the horizontal block faces.
*/
public static BlockFace[] getHorizontalBlockFaces() {
return new BlockFace[]{NORTH, EAST, SOUTH, WEST};
return HORIZONTAL_BLOCK_FACES;
}

/**
Expand All @@ -72,7 +71,7 @@ public static BlockFace[] getHorizontalBlockFaces() {
* @return the vertical block faces.
*/
public static BlockFace[] getVerticalBlockFaces() {
return new BlockFace[]{UP, DOWN};
return VERTICAL_BLOCK_FACES;
}

public static BlockFace getBlockFaceByStairDirectionValue(int value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,15 @@ default boolean computeBlockCollisionMotion() {
return false;
}

/**
* Check if the entity has liquid motion.
*
* @return {@code true} if the entity has liquid motion.
*/
default boolean computeLiquidMotion() {
return true;
}

/**
* Called when the entity collides with another entity.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ default boolean attack(Entity attacker, float damage) {
*/
boolean hasFireDamage();

/**
* Check if the entity has drowning damage.
*
* @return {@code true} if the entity has drowning damage, {@code false} otherwise.
*/
boolean hasDrowningDamage();

/**
* Check if this entity can against fire damage even if
* it does not have fire resistance effect.
Expand Down
79 changes: 71 additions & 8 deletions api/src/main/java/org/allaymc/api/math/MathUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,19 +171,82 @@ public static double getPitchFromVector(Vector3fc vector) {
return StrictMath.abs(pitch) < 1E-10 ? 0 : pitch;
}

public static float fastSin(float p) {
return SIN_LOOK_UP_TABLE[((int) (p * 10430.378F) & 0xFFFF)];
/**
* Calculate sin value quickly by looking up a pre-calculated table.
*
* @param radian the radian.
*
* @return the sin value.
*/
public static float fastSin(float radian) {
return SIN_LOOK_UP_TABLE[((int) (radian * 10430.378F) & 0xFFFF)];
}

/**
* Calculate sin value quickly by looking up a pre-calculated table.
*
* @param radian the radian.
*
* @return the sin value.
*/
public static float fastSin(double radian) {
return SIN_LOOK_UP_TABLE[((int) (radian * 10430.378F) & 0xFFFF)];
}

/**
* Calculate cos value quickly by looking up a pre-calculated table.
*
* @param radian the radian.
*
* @return the cos value.
*/
public static float fastCos(float radian) {
return SIN_LOOK_UP_TABLE[((int) (radian * 10430.378F + 16384.0F) & 0xFFFF)];
}

/**
* Calculate cos value quickly by looking up a pre-calculated table.
*
* @param radian the radian.
*
* @return the cos value.
*/
public static float fastCos(double radian) {
return SIN_LOOK_UP_TABLE[((int) (radian * 10430.378F + 16384.0F) & 0xFFFF)];
}

public static float fastSin(double p) {
return SIN_LOOK_UP_TABLE[((int) (p * 10430.378F) & 0xFFFF)];
/**
* Check if the vector contains NaN.
*
* @param v the vector to check.
*
* @return {@code true} if the vector contains NaN, {@code false} otherwise.
*/
public static boolean hasNaN(Vector3fc v) {
return Float.isNaN(v.x()) || Float.isNaN(v.y()) || Float.isNaN(v.z());
}

public static float fastCos(float p) {
return SIN_LOOK_UP_TABLE[((int) (p * 10430.378F + 16384.0F) & 0xFFFF)];
/**
* Check if the vector contains NaN.
*
* @param v the vector to check.
*
* @return {@code true} if the vector contains NaN, {@code false} otherwise.
*/
public static boolean hasNaN(Vector3dc v) {
return Double.isNaN(v.x()) || Double.isNaN(v.y()) || Double.isNaN(v.z());
}

public static float fastCos(double p) {
return SIN_LOOK_UP_TABLE[((int) (p * 10430.378F + 16384.0F) & 0xFFFF)];
/**
* Normalize the vector if it is not zero.
* <p>
* If the vector is zero, it can't be normalized, otherwise a vector with three NaN values will be produced.
*
* @param v the vector.
*
* @return the normalized vector.
*/
public static Vector3f normalizeIfNotZero(Vector3f v) {
return v.lengthSquared() > 0 ? v.normalize(v) : v;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
import org.allaymc.api.block.type.BlockState;
import org.allaymc.api.block.type.BlockType;
import org.allaymc.api.block.type.BlockTypes;
import org.allaymc.api.entity.Entity;
import org.allaymc.api.eventbus.event.block.LiquidDecayEvent;
import org.allaymc.api.eventbus.event.block.LiquidFlowEvent;
import org.allaymc.api.math.MathUtils;
import org.allaymc.api.math.position.Position3i;
import org.allaymc.api.world.Dimension;
import org.joml.Vector3f;
import org.joml.Vector3i;
import org.joml.Vector3ic;

Expand Down Expand Up @@ -94,9 +95,74 @@ public void onScheduledUpdate(BlockStateWithPos blockStateWithPos) {
updateLiquid(dimension, pos, liquid, blockStateWithPos.layer());
}

@Override
public void onCollideWithEntity(BlockStateWithPos blockStateWithPos, Entity entity) {
// TODO
/**
* This method is used in {@link org.allaymc.server.world.service.AllayEntityPhysicsService}
*/
public Vector3f calculateFlowVector(Dimension dimension, int x, int y, int z, BlockState current) {
// TODO: cache the flow vector for better performance
var vx = 0;
var vy = 0;
var vz = 0;
var decay = getEffectiveFlowDecay(current);

for (var face : BlockFace.getHorizontalBlockFaces()) {
var offset = face.getOffset();

var sideX = x + offset.x();
var sideY = y + offset.y();
var sideZ = z + offset.z();
var sideBlock = dimension.getBlockState(sideX, sideY, sideZ);
var blockDecay = getEffectiveFlowDecay(sideBlock);

if (blockDecay < 0) {
if (!sideBlock.getBlockStateData().liquidReactionOnTouch().canLiquidFlowInto()) {
continue;
}

blockDecay = getEffectiveFlowDecay(dimension.getBlockState(sideX, sideY - 1, sideZ));

if (blockDecay >= 0) {
var realDecay = blockDecay - (decay - 8);
vx += offset.x() * realDecay;
vy += offset.y() * realDecay;
vz += offset.z() * realDecay;
}

continue;
}

var realDecay = blockDecay - decay;
vx += offset.x() * realDecay;
vy += offset.y() * realDecay;
vz += offset.z() * realDecay;
}

var vector = new Vector3f(vx, vy, vz);

if (isFalling(current)) {
for (var face : BlockFace.getHorizontalBlockFaces()) {
var offset = face.getOffset();
if (!canFlowInto(dimension, x + offset.x(), y + offset.y(), z + offset.z(), true) &&
!canFlowInto(dimension, x + offset.x(), y + offset.y() + 1, z + offset.z(), true)) {
// normalize() should only be called when the vector is not zero,
// otherwise it will produce a vector with three NaN values.
MathUtils.normalizeIfNotZero(vector);
vector.add(0, -6, 0);
break;
}
}
}

// Same to above
return MathUtils.normalizeIfNotZero(vector);
}

protected int getEffectiveFlowDecay(BlockState liquid) {
if (!isSameLiquidType(liquid.getBlockType())) {
return -1;
}

return isFalling(liquid) ? 0 : 8 - getDepth(liquid);
}

/**
Expand Down Expand Up @@ -390,17 +456,26 @@ protected boolean spreadNeighbor(Dimension dimension, Vector3ic source, LiquidNo
return false;
}

/**
* @see #canFlowInto(Dimension, int, int, int, boolean)
*/
protected boolean canFlowInto(Dimension dimension, Vector3ic pos, boolean sideways) {
return canFlowInto(dimension, pos.x(), pos.y(), pos.z(), sideways);
}

/**
* Checks if a liquid can flow into the block present in the world at a specific block position.
*
* @param dimension The dimension the block is in.
* @param pos The position of the block to flow into.
* @param x The x coordinate of the block.
* @param y The y coordinate of the block.
* @param z The z coordinate of the block.
* @param sideways Whether the flow is sideways or downwards.
*
* @return Whether the liquid can flow into the block.
*/
protected boolean canFlowInto(Dimension dimension, Vector3ic pos, boolean sideways) {
var existing = dimension.getBlockState(pos);
protected boolean canFlowInto(Dimension dimension, int x, int y, int z, boolean sideways) {
var existing = dimension.getBlockState(x, y, z);
if (existing.getBlockType() == BlockTypes.AIR ||
existing.getBlockStateData().liquidReactionOnTouch().removedOnTouch()) {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public BlockBehaviorImpl(List<ComponentProvider<? extends Component>> componentP
}

@Delegate
protected BlockBaseComponent getBaseComponent() {
public BlockBaseComponent getBaseComponent() {
return baseComponent;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public BlockLiquidBehaviorImpl(

@Delegate
@Override
protected BlockLiquidBaseComponent getBaseComponent() {
public BlockLiquidBaseComponent getBaseComponent() {
return (BlockLiquidBaseComponent) super.getBaseComponent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,13 @@ public Map<Long, EntityPlayer> getViewers() {

@Override
public void setMotion(Vector3fc motion) {
if (MathUtils.hasNaN(motion)) {
// Sometimes there may be bugs in the physics engine, which will cause the motion to be NaN.
// This check help us find the bug quickly as we usually can't realize that a strange bug
// is caused by NaN motion.
log.error("Entity {} is set by a motion which contains NaN: {}", runtimeId, motion);
return;
}
this.lastMotion = this.motion;
this.motion = new Vector3f(motion);
}
Expand Down Expand Up @@ -522,7 +529,7 @@ protected Vector3f calculateKnockbackMotion(Vector3fc source, float kb, boolean
var rand = ThreadLocalRandom.current();
var rx = rand.nextFloat(1) - 0.5f;
var rz = rand.nextFloat(1) - 0.5f;
vec = new Vector3f(rx, 0, rz).normalize().mul(kb);
vec = MathUtils.normalizeIfNotZero(new Vector3f(rx, 0, rz)).mul(kb);
} else {
vec = getLocation().sub(source, new Vector3f()).normalize().mul(kb);
vec.y = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ public boolean hasFireDamage() {
baseComponent.getWorld().getWorldData().<Boolean>getGameRuleValue(GameRule.FIRE_DAMAGE);
}

@Override
public boolean hasDrowningDamage() {
return !thisEntity.hasEffect(EffectTypes.WATER_BREATHING);
}

@Override
public boolean setOnFireTicks(int newOnFireTicks) {
if (!hasFireDamage()) {
Expand Down Expand Up @@ -248,6 +253,10 @@ protected void onFall(CEntityFallEvent event) {

@EventHandler
protected void onDrown(CEntityDrownEvent event) {
if (!hasDrowningDamage()) {
return;
}

attack(DamageContainer.drown(2));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,7 @@ public void initializePlayer() {
baseComponent.setLocationBeforeSpawn(new Location3f(currentPos.x(), currentPos.y(), currentPos.z(), dimension));
dimension.addPlayer(thisPlayer);

var spawnWorld = dimension.getWorld();
var startGamePacket = encodeStartGamePacket(spawnWorld, playerData, dimension);
var startGamePacket = encodeStartGamePacket(dimension.getWorld(), playerData, dimension);
sendPacket(startGamePacket);

clientSession.getPeer().getCodecHelper().setItemDefinitions(
Expand Down
Loading

0 comments on commit a232734

Please sign in to comment.