From a23273422c13af7431a181fcb338803e9d723183 Mon Sep 17 00:00:00 2001 From: daoge_cmd <3523206925@qq.com> Date: Mon, 30 Dec 2024 17:45:40 +0800 Subject: [PATCH] feat: implement liquid motion this commit also has some bug fixes, see changelog for details --- CHANGELOG.md | 12 ++ .../component/BlockLiquidBaseComponent.java | 4 +- .../org/allaymc/api/block/data/BlockFace.java | 11 +- .../entity/component/EntityBaseComponent.java | 9 ++ .../component/EntityDamageComponent.java | 7 + .../java/org/allaymc/api/math/MathUtils.java | 79 ++++++++++-- .../BlockLiquidBaseComponentImpl.java | 89 ++++++++++++- .../server/block/impl/BlockBehaviorImpl.java | 2 +- .../block/impl/BlockLiquidBehaviorImpl.java | 2 +- .../component/EntityBaseComponentImpl.java | 9 +- .../component/EntityDamageComponentImpl.java | 9 ++ .../EntityPlayerNetworkComponentImpl.java | 3 +- .../entity/type/EntityTypeInitializer.java | 5 + .../service/AllayEntityPhysicsService.java | 122 +++++++++++++++--- 14 files changed, 317 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 436904a41..55e828c9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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) diff --git a/api/src/main/java/org/allaymc/api/block/component/BlockLiquidBaseComponent.java b/api/src/main/java/org/allaymc/api/block/component/BlockLiquidBaseComponent.java index e9079e4a2..27a61a487 100644 --- a/api/src/main/java/org/allaymc/api/block/component/BlockLiquidBaseComponent.java +++ b/api/src/main/java/org/allaymc/api/block/component/BlockLiquidBaseComponent.java @@ -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); } /** @@ -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)); } /** diff --git a/api/src/main/java/org/allaymc/api/block/data/BlockFace.java b/api/src/main/java/org/allaymc/api/block/data/BlockFace.java index b46aada9f..9daab02e7 100644 --- a/api/src/main/java/org/allaymc/api/block/data/BlockFace.java +++ b/api/src/main/java/org/allaymc/api/block/data/BlockFace.java @@ -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; @@ -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; } /** @@ -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) { diff --git a/api/src/main/java/org/allaymc/api/entity/component/EntityBaseComponent.java b/api/src/main/java/org/allaymc/api/entity/component/EntityBaseComponent.java index d81729382..d1f4deb14 100644 --- a/api/src/main/java/org/allaymc/api/entity/component/EntityBaseComponent.java +++ b/api/src/main/java/org/allaymc/api/entity/component/EntityBaseComponent.java @@ -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. * diff --git a/api/src/main/java/org/allaymc/api/entity/component/EntityDamageComponent.java b/api/src/main/java/org/allaymc/api/entity/component/EntityDamageComponent.java index 1d0de32be..22dcad836 100644 --- a/api/src/main/java/org/allaymc/api/entity/component/EntityDamageComponent.java +++ b/api/src/main/java/org/allaymc/api/entity/component/EntityDamageComponent.java @@ -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. diff --git a/api/src/main/java/org/allaymc/api/math/MathUtils.java b/api/src/main/java/org/allaymc/api/math/MathUtils.java index 041b006b3..54624522b 100644 --- a/api/src/main/java/org/allaymc/api/math/MathUtils.java +++ b/api/src/main/java/org/allaymc/api/math/MathUtils.java @@ -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. + *

+ * 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; } } diff --git a/server/src/main/java/org/allaymc/server/block/component/BlockLiquidBaseComponentImpl.java b/server/src/main/java/org/allaymc/server/block/component/BlockLiquidBaseComponentImpl.java index 366cc7ea7..9fa2c7e91 100644 --- a/server/src/main/java/org/allaymc/server/block/component/BlockLiquidBaseComponentImpl.java +++ b/server/src/main/java/org/allaymc/server/block/component/BlockLiquidBaseComponentImpl.java @@ -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; @@ -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); } /** @@ -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; diff --git a/server/src/main/java/org/allaymc/server/block/impl/BlockBehaviorImpl.java b/server/src/main/java/org/allaymc/server/block/impl/BlockBehaviorImpl.java index d3014af27..211bd150b 100644 --- a/server/src/main/java/org/allaymc/server/block/impl/BlockBehaviorImpl.java +++ b/server/src/main/java/org/allaymc/server/block/impl/BlockBehaviorImpl.java @@ -26,7 +26,7 @@ public BlockBehaviorImpl(List> componentP } @Delegate - protected BlockBaseComponent getBaseComponent() { + public BlockBaseComponent getBaseComponent() { return baseComponent; } diff --git a/server/src/main/java/org/allaymc/server/block/impl/BlockLiquidBehaviorImpl.java b/server/src/main/java/org/allaymc/server/block/impl/BlockLiquidBehaviorImpl.java index 8a221cca6..f7083c4c0 100644 --- a/server/src/main/java/org/allaymc/server/block/impl/BlockLiquidBehaviorImpl.java +++ b/server/src/main/java/org/allaymc/server/block/impl/BlockLiquidBehaviorImpl.java @@ -17,7 +17,7 @@ public BlockLiquidBehaviorImpl( @Delegate @Override - protected BlockLiquidBaseComponent getBaseComponent() { + public BlockLiquidBaseComponent getBaseComponent() { return (BlockLiquidBaseComponent) super.getBaseComponent(); } } diff --git a/server/src/main/java/org/allaymc/server/entity/component/EntityBaseComponentImpl.java b/server/src/main/java/org/allaymc/server/entity/component/EntityBaseComponentImpl.java index db2ab904f..235edfa45 100644 --- a/server/src/main/java/org/allaymc/server/entity/component/EntityBaseComponentImpl.java +++ b/server/src/main/java/org/allaymc/server/entity/component/EntityBaseComponentImpl.java @@ -493,6 +493,13 @@ public Map 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); } @@ -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; diff --git a/server/src/main/java/org/allaymc/server/entity/component/EntityDamageComponentImpl.java b/server/src/main/java/org/allaymc/server/entity/component/EntityDamageComponentImpl.java index 15965756f..ebd2d7e81 100644 --- a/server/src/main/java/org/allaymc/server/entity/component/EntityDamageComponentImpl.java +++ b/server/src/main/java/org/allaymc/server/entity/component/EntityDamageComponentImpl.java @@ -185,6 +185,11 @@ public boolean hasFireDamage() { baseComponent.getWorld().getWorldData().getGameRuleValue(GameRule.FIRE_DAMAGE); } + @Override + public boolean hasDrowningDamage() { + return !thisEntity.hasEffect(EffectTypes.WATER_BREATHING); + } + @Override public boolean setOnFireTicks(int newOnFireTicks) { if (!hasFireDamage()) { @@ -248,6 +253,10 @@ protected void onFall(CEntityFallEvent event) { @EventHandler protected void onDrown(CEntityDrownEvent event) { + if (!hasDrowningDamage()) { + return; + } + attack(DamageContainer.drown(2)); } diff --git a/server/src/main/java/org/allaymc/server/entity/component/player/EntityPlayerNetworkComponentImpl.java b/server/src/main/java/org/allaymc/server/entity/component/player/EntityPlayerNetworkComponentImpl.java index be360b40b..37450dab0 100644 --- a/server/src/main/java/org/allaymc/server/entity/component/player/EntityPlayerNetworkComponentImpl.java +++ b/server/src/main/java/org/allaymc/server/entity/component/player/EntityPlayerNetworkComponentImpl.java @@ -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( diff --git a/server/src/main/java/org/allaymc/server/entity/type/EntityTypeInitializer.java b/server/src/main/java/org/allaymc/server/entity/type/EntityTypeInitializer.java index 74b6c3a7e..3fdcab0d5 100644 --- a/server/src/main/java/org/allaymc/server/entity/type/EntityTypeInitializer.java +++ b/server/src/main/java/org/allaymc/server/entity/type/EntityTypeInitializer.java @@ -35,6 +35,11 @@ public static void initItem() { public boolean hasFallDamage() { return false; } + + @Override + public boolean hasDrowningDamage() { + return false; + } }, EntityDamageComponentImpl.class) .addComponent( () -> new EntityAttributeComponentImpl( diff --git a/server/src/main/java/org/allaymc/server/world/service/AllayEntityPhysicsService.java b/server/src/main/java/org/allaymc/server/world/service/AllayEntityPhysicsService.java index 5d96f447a..3edc46d66 100644 --- a/server/src/main/java/org/allaymc/server/world/service/AllayEntityPhysicsService.java +++ b/server/src/main/java/org/allaymc/server/world/service/AllayEntityPhysicsService.java @@ -4,8 +4,10 @@ import it.unimi.dsi.fastutil.floats.FloatBooleanImmutablePair; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import lombok.extern.slf4j.Slf4j; +import org.allaymc.api.block.component.BlockLiquidBaseComponent; import org.allaymc.api.block.data.BlockFace; import org.allaymc.api.block.type.BlockState; +import org.allaymc.api.block.type.BlockTypes; import org.allaymc.api.entity.Entity; import org.allaymc.api.entity.effect.type.EffectTypes; import org.allaymc.api.entity.interfaces.EntityPlayer; @@ -17,8 +19,11 @@ import org.allaymc.api.math.voxelshape.VoxelShape; import org.allaymc.api.server.Server; import org.allaymc.api.world.Dimension; +import org.allaymc.api.world.DimensionInfo; import org.allaymc.api.world.service.AABBOverlapFilter; import org.allaymc.api.world.service.EntityPhysicsService; +import org.allaymc.server.block.component.BlockLiquidBaseComponentImpl; +import org.allaymc.server.block.impl.BlockLiquidBehaviorImpl; import org.allaymc.server.datastruct.aabb.AABBTree; import org.allaymc.server.datastruct.collections.nb.Long2ObjectNonBlockingMap; import org.allaymc.server.entity.component.EntityBaseComponentImpl; @@ -59,6 +64,10 @@ public class AllayEntityPhysicsService implements EntityPhysicsService { private static final float AIR_VELOCITY_FACTOR = 0.02f; private static final float DRAG_FACTOR = 0.98f; + private static final float WATER_FLOW_MOTION = 0.014f; + private static final float LAVA_FLOW_MOTION = 0.002333333f; + private static final float LAVA_FLOW_MOTION_IN_NETHER = 0.007f; + private static final int X = 0; private static final int Y = 1; private static final int Z = 2; @@ -89,18 +98,29 @@ public void tick() { cacheEntityCollisionResult(); var updatedEntities = new Long2ObjectNonBlockingMap(); entities.values().parallelStream().forEach(entity -> { - if (!entity.computeMovementServerSide()) return; - if (!entity.isCurrentChunkLoaded()) return; - if (entity.getLocation().y() < dimension.getDimensionInfo().minHeight()) return; - // TODO: liquid motion etc... + if (!entity.computeMovementServerSide() || + !entity.isCurrentChunkLoaded() || + entity.getLocation().y() < dimension.getDimensionInfo().minHeight()) { + return; + } + var collidedBlocks = dimension.getCollidingBlockStates(entity.getOffsetAABB()); if (collidedBlocks == null) { // 1. The entity is not stuck in the block - if (entity.computeEntityCollisionMotion()) computeEntityCollisionMotion(entity); + if (entity.computeEntityCollisionMotion()) { + computeEntityCollisionMotion(entity); + } + var hasLiquidMotion = false; + if (entity.computeLiquidMotion()) { + hasLiquidMotion = computeLiquidMotion(entity); + } entity.setMotion(checkMotionThreshold(new Vector3f(entity.getMotion()))); - if (applyMotion(entity)) updatedEntities.put(entity.getRuntimeId(), entity); + if (applyMotion(entity)) { + updatedEntities.put(entity.getRuntimeId(), entity); + } + // Apply friction, gravity etc... - updateMotion(entity); + updateMotion(entity, hasLiquidMotion); } else if (entity.computeBlockCollisionMotion()) { // 2. The entity is stuck in the block // Do not calculate other motion exclude block collision motion @@ -198,7 +218,7 @@ protected void computeEntityCollisionMotion(Entity entity) { for (var other : collidedEntities) { // https://github.com/lovexyn0827/Discovering-Minecraft/blob/master/Minecraft%E5%AE%9E%E4%BD%93%E8%BF%90%E5%8A%A8%E7%A0%94%E7%A9%B6%E4%B8%8E%E5%BA%94%E7%94%A8/5-Chapter-5.md var ol = other.getLocation(); - var direction = new Vector3f(entity.getLocation()).sub(other.getLocation(), new Vector3f()).normalize(); + var direction = MathUtils.normalizeIfNotZero(new Vector3f(entity.getLocation()).sub(other.getLocation(), new Vector3f())); var distance = max(abs(ol.x() - location.x()), abs(ol.z() - location.z())); if (distance <= 0.01) continue; @@ -216,25 +236,90 @@ protected void computeEntityCollisionMotion(Entity entity) { entity.addMotion(collisionMotion); } + /** + * Compute the liquid motion for the entity. + * + * @param entity the entity to compute liquid motion. + * + * @return {@code true} if the entity has liquid motion, otherwise {@code false}. + */ + protected boolean computeLiquidMotion(Entity entity) { + // In calculateFlowVector() method, Dimension#getBlockState() will also being called, + // and because the lambda is running in Chunk#batchProcess, calling such method + // inside the lambda will cause a deadlock (the lock is not reentrant), so we + // need to get the block states first and store them for further use. + var liquids = new ArrayList(); + dimension.forEachBlockStates(entity.getOffsetAABB(), 0, (x, y, z, block) -> { + if (block.getBehavior() instanceof BlockLiquidBehaviorImpl) { + liquids.add(new LiquidWithPos(x, y, z, block)); + } + }); + if (liquids.isEmpty()) { + return false; + } + + var hasWaterMotion = false; + var hasLavaMotion = false; + var waterMotion = new Vector3f(); + var lavaMotion = new Vector3f(); + + var entityY = entity.getLocation().y(); + for (var liquid : liquids) { + var liquidBehavior = (BlockLiquidBehaviorImpl) liquid.blockState.getBehavior(); + var flowVector = ((BlockLiquidBaseComponentImpl) liquidBehavior.getBaseComponent()).calculateFlowVector(dimension, liquid.x, liquid.y, liquid.z, liquid.blockState); + if (flowVector.lengthSquared() <= 0) { + continue; + } + + var d = BlockLiquidBaseComponent.getDepth(liquid.blockState) * 0.125f + liquid.y - entityY; + if (d <= 0) { + continue; + } + if (d < 0.4) { + flowVector.mul(d); + } + + if (liquidBehavior.isSameLiquidType(BlockTypes.WATER)) { + hasWaterMotion = true; + waterMotion.add(flowVector); + } else if (liquidBehavior.isSameLiquidType(BlockTypes.LAVA)) { + hasLavaMotion = true; + lavaMotion.add(flowVector); + } + } + if (!hasWaterMotion && !hasLavaMotion) { + return false; + } + + var finalMotion = new Vector3f(); + if (hasWaterMotion) { + finalMotion.add(waterMotion.normalize().mul(WATER_FLOW_MOTION)); + } + if (hasLavaMotion) { + finalMotion.add(lavaMotion.normalize().mul(dimension.getDimensionInfo() == DimensionInfo.NETHER ? LAVA_FLOW_MOTION_IN_NETHER : LAVA_FLOW_MOTION)); + } + + entity.addMotion(finalMotion); + return true; + } + /** * @see Horizontal Movement Formulas */ - protected void updateMotion(Entity entity) { + protected void updateMotion(Entity entity, boolean hasLiquidMotion) { var motion = entity.getMotion(); var blockStateStandingOn = entity.getBlockStateStandingOn(); // 1. Multiplier factors var movementFactor = entity.getMovementFactor(); - var speedLevel = entity.getEffectLevel(EffectTypes.SPEED); var slownessLevel = entity.getEffectLevel(EffectTypes.SLOWNESS); - var effectFactor = (1f + 0.2f * speedLevel) * (1f - 0.15f * slownessLevel); - - var slipperinessMultiplier = blockStateStandingOn != null ? - blockStateStandingOn.getBlockStateData().friction() : - DEFAULT_FRICTION; - + float slipperinessMultiplier = 1; + if (!hasLiquidMotion) { + // Entity that has liquid motion won't be affected by the friction of the block it stands on + slipperinessMultiplier = blockStateStandingOn != null ? blockStateStandingOn.getBlockStateData().friction() : DEFAULT_FRICTION; + } var momentumMx = motion.x() * slipperinessMultiplier * MOMENTUM_FACTOR; var momentumMz = motion.z() * slipperinessMultiplier * MOMENTUM_FACTOR; @@ -246,11 +331,10 @@ protected void updateMotion(Entity entity) { } var yaw = entity.getLocation().yaw(); - var newMx = (float) (momentumMx + acceleration * sin(yaw)); var newMz = (float) (momentumMz + acceleration * cos(yaw)); - // Skip sprint jump boost because this service does not handle player + // Skip sprint jump boost because this service does not handle player's movement var newMy = (motion.y() - (entity.hasGravity() ? entity.getGravity() : 0f)) * DRAG_FACTOR; entity.setMotion(checkMotionThreshold(new Vector3f(newMx, newMy, newMz))); @@ -582,4 +666,6 @@ public List getCachedEntityCollidingResult(Entity entity, boolean ignore } protected record ClientMove(EntityPlayer player, Location3fc newLoc) {} + + protected record LiquidWithPos(int x, int y, int z, BlockState blockState) {} }