Skip to content

Commit

Permalink
feat: implement liquid (#533)
Browse files Browse the repository at this point in the history
  • Loading branch information
smartcmd authored Dec 28, 2024
1 parent 38952a7 commit 2ef0d35
Show file tree
Hide file tree
Showing 26 changed files with 1,235 additions and 29 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ Unless otherwise specified, any version comparison below is the comparison of se
- (API) Added `TextFormat#MATERIAL_RESIN`.
- (API) Entity#teleport method now accepts an extra `Reason` argument.
- (API) Added structure API by @harry-xi.
- (API) Added a number of new methods to `BlockLiquidBaseComponent`.
- (API) Added `LiquidFlowEvent`, `LiquidDecayEvent` and `LiquidHardenEvent`.
- (API) Added `BlockBehavior#afterPlaced` and `BlockBehavior#afterReplaced` that are correspond to
`BlockBehavior#onPlace` and `BlockBehavior#onReplace`.
- (API) Added `BlockStateData#liquidReactionOnTouch`, there are also a number of new methods in `BlockStateData`.
- Added `/structure` command to manage structures.
- Implemented liquid features including water, lava and related features.

### Changed

Expand All @@ -28,10 +34,13 @@ Unless otherwise specified, any version comparison below is the comparison of se
- Improved code readability for I18n module.
- NBT library is now updated to 3.0.10.
- `/gametest` command is now only available in dev build.
- Removed the `dimension` field in `BlockBreakEvent`, which is duplicated with `blockStateWithPos#dimension`.

### Fixed

- (API) Fixed exception when setting item count or meta to zero.
- (API) `VoxelShapes#buildLiquidShape` now works correctly.
- (API) Correct `BlockStateData#canContainLiquid` to `BlockStateData#canContainLiquidSource`.
- Passing non-positive amount or negative meta arguments to `/give` command now will result in a syntax error.
- Entity#teleport method now will reset fall distance correctly.
- Fixed visual flashes when eating chorus fruits.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,30 @@ default boolean canRandomUpdate() {
boolean place(Dimension dimension, BlockState blockState, Vector3ic placeBlockPos, PlayerInteractInfo placementInfo);

/**
* Called when a block is placed.
* Called when place a block.
* <p>
* Please note that at this moment the block has not been placed yet, so
* you can't set a new block state using {@link Dimension#setBlockState}
* at this time, as the block state will be placed later and will override
* the block state you set here.
* <p>
* If you want to do the thing said above, consider using {@link BlockBehavior#afterPlaced}.
*
* @param currentBlockState The block that is being replaced.
* @param newBlockState The block that is replacing the current block.
* @param placementInfo The player placement info, can be {@code null}.
*/
void onPlace(BlockStateWithPos currentBlockState, BlockState newBlockState, PlayerInteractInfo placementInfo);

/**
* Called after a block is placed.
*
* @param oldBlockState The block that is being replaced.
* @param newBlockState The block that is replacing the current block.
* @param placementInfo The player placement info, can be {@code null}.
*/
void afterPlaced(BlockStateWithPos oldBlockState, BlockState newBlockState, PlayerInteractInfo placementInfo);

/**
* Check if the block will drop as item when it is broke by the specified entity using the specified item.
*
Expand Down Expand Up @@ -119,6 +135,15 @@ default boolean canRandomUpdate() {
*/
void onReplace(BlockStateWithPos currentBlockState, BlockState newBlockState, PlayerInteractInfo placementInfo);

/**
* Called after a block is replaced.
*
* @param oldBlockState the block that is being replaced.
* @param newBlockState the block that is replacing the current block.
* @param placementInfo the player placement info, can be null.
*/
void afterReplaced(BlockStateWithPos oldBlockState, BlockState newBlockState, PlayerInteractInfo placementInfo);

/**
* Called when a neighbor layer block is replaced.
* <pr>
Expand Down Expand Up @@ -311,6 +336,20 @@ default double calculateBreakTime(BlockState blockState, ItemStack usedItem, Ent
return 1d / speed;
}

/**
* Check if a position on the side of the block placed in the world at a specific position is
* closed. When this returns true (for example, when the side is below the position and the block is a
* slab), liquid inside the block won't flow from pos into side.
*
* @param blockState the block to check.
* @param blockFace the side of the block to check.
*
* @return {@code true} if the side is closed, {@code false} otherwise.
*/
default boolean canLiquidFlowIntoSide(BlockState blockState, BlockFace blockFace) {
return true;
}

private double speedBonusByEfficiency(int efficiencyLevel) {
if (efficiencyLevel == 0) return 0;
return efficiencyLevel * efficiencyLevel + 1;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package org.allaymc.api.block.component;

import org.allaymc.api.block.dto.BlockStateWithPos;
import org.allaymc.api.block.property.type.BlockPropertyTypes;
import org.allaymc.api.block.type.BlockState;
import org.allaymc.api.block.type.BlockType;
import org.allaymc.api.world.DimensionInfo;

/**
* BlockLiquidBaseComponent is the base component for liquid blocks.
*
* @author daoge_cmd
*/
public interface BlockLiquidBaseComponent extends BlockBaseComponent {
/**
* Check if the liquid is falling.
*
* @param blockState the block state to check.
*
* @return {@code true} if the liquid is flowing down, {@code false} otherwise.
*/
static boolean isFalling(BlockState blockState) {
// The first bit of the liquid depth property is set to 1 if the liquid is falling
return (blockState.getPropertyValue(BlockPropertyTypes.LIQUID_DEPTH) & 0b1000) == 0b1000;
}

/**
* Get the liquid depth of the block.
* <p>
* Falling blocks and source blocks have a liquid depth of 8.
* Other blocks have a liquid depth between 1 and 7.
*
* @param blockState the block state to get the liquid depth from.
*
* @return the liquid depth of the block.
*/
static int getDepth(BlockState blockState) {
if (isFalling(blockState) || isSource(blockState)) {
return 8;
}
return 8 - blockState.getPropertyValue(BlockPropertyTypes.LIQUID_DEPTH) & 0b0111;
}

/**
* Check if the block state represents a liquid source.
*
* @param blockState the block state to check.
*
* @return {@code true} if the block state represents a liquid source, {@code false} otherwise.
*/
static boolean isSource(BlockState blockState) {
return blockState.getPropertyValue(BlockPropertyTypes.LIQUID_DEPTH) == 0;
}

/**
* Get the block state of the liquid block with given depth and falling state.
*
* @param depth the depth of the liquid.
* @param falling {@code true} if the liquid is falling, {@code false} otherwise.
*
* @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));
}

/**
* Get the block state of the falling block.
*
* @return the block state of the falling block.
*/
default BlockState getFallingBlockState() {
return getBlockType().ofState(BlockPropertyTypes.LIQUID_DEPTH.createValue(0b1000));
}

/**
* Get the block state of the source block.
*
* @return the block state of the source block.
*/
default BlockState getSourceBlockState() {
return getBlockType().ofState(BlockPropertyTypes.LIQUID_DEPTH.createValue(0));
}

/**
* Check if the liquid can be contained in a block.
*
* @return {@code true} if the liquid can be contained in a block, {@code false} otherwise.
*/
default boolean canBeContained() {
return true;
}

/**
* Check if the block should harden when looking at the surrounding blocks and sets the position
* to the hardened block when adequate. If the block was hardened, the method returns true.
*
* @param current the current block.
* @param flownIntoBy the block flown into by, can be {@code null}.
*
* @return {@code true} if the block was hardened, {@code false} otherwise.
*/
default boolean tryHarden(BlockStateWithPos current, BlockStateWithPos flownIntoBy) {
return false;
}

/**
* Get the flow decay of the liquid.
* <p>
* Flow decay represents how many liquid levels are lost per block flowed horizontally.
* Affects how far the liquid can flow.
*
* @return the flow decay of the liquid.
*/
int getFlowDecay(DimensionInfo dimensionInfo);

/**
* Get the flow speed of the liquid in ticks.
* <p>
* Flow speed represents how fast the liquid spreads.
*
* @return the flow speed of the liquid in ticks.
*/
int getFlowSpeed(DimensionInfo dimensionInfo);

/**
* Check whether the block can become a source block if there are more than
* two source block nearby horizontally.
*
* @return {@code true} if the block can become a source block, {@code false} otherwise.
*/
boolean canFormSource();

/**
* Check if the given block type is the same liquid type as this block type.
*
* @param blockType the block type to check.
*
* @return {@code true} if the given block type is the same liquid type as this block type, {@code false} otherwise.
*/
boolean isSameLiquidType(BlockType<?> blockType);
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ public class BlockStateData {
color.getGreen(),
color.getBlue(),
Integer.parseInt(str.substring(7), 16));
}).create();
})
.registerTypeAdapter(LiquidReactionOnTouch.class, (JsonDeserializer<Object>) (json, typeOfT, context) -> LiquidReactionOnTouch.valueOf(json.getAsString()))
.create();
/**
* The burnOdds of this block state.
* <p>
Expand All @@ -67,10 +69,15 @@ public class BlockStateData {
@Builder.Default
protected int burnOdds = 0;
/**
* Whether the block state can contain liquid.
* Whether the block state can contain liquid source.
*/
@Builder.Default
protected boolean canContainLiquidSource = false;
/**
* The reaction of this block state when liquid flow into.
*/
@Builder.Default
protected boolean canContainLiquid = false;
protected LiquidReactionOnTouch liquidReactionOnTouch = LiquidReactionOnTouch.BLOCKING;
/**
* The collision shape of the block state.
* <p>
Expand Down Expand Up @@ -187,4 +194,13 @@ public VoxelShape computeOffsetShape(Vector3ic vector) {
public boolean isTransparent() {
return translucency() != 1.0f;
}

/**
* Check if the block state can contain liquid, no matter it is liquid source or not.
*
* @return {@code true} if the block state can contain liquid, otherwise {@code false}.
*/
public boolean canContainLiquid() {
return canContainLiquidSource || liquidReactionOnTouch.canLiquidFlowInto();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.allaymc.api.block.component.data;

/**
* @author daoge_cmd
*/
public enum LiquidReactionOnTouch {
/**
* The block will be broken without dropping anything.
*/
BROKEN,
/**
* The block will drop itself.
*/
POPPED,
/**
* The block will prevent the liquid from flowing into.
* Compared to {@link BlockStateData#canContainLiquidSource}, this only
* affects flowing liquid, not source liquid. That's mean the block
* may still can contain source liquid block.
*/
BLOCKING,
/**
* The block won't have any reaction, also won't have any impact on the liquid just like air.
*/
NOREACTION;

/**
* Check if the block should be removed when touched by liquid.
*
* @return {@code true} if the block should be removed when touched by liquid.
*/
public boolean removedOnTouch() {
return this == BROKEN || this == POPPED;
}

/**
* Check if liquid can flow into this block.
* <p>
* Please note that this method only checks if the liquid can flow into this block,
* it doesn't mean the block can contain the liquid source block.
*
* @return {@code true} if liquid can flow into this block.
*/
public boolean canLiquidFlowInto() {
return this == NOREACTION;
}
}
11 changes: 11 additions & 0 deletions api/src/main/java/org/allaymc/api/block/dto/BlockStateWithPos.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ public record BlockStateWithPos(
Position3ic pos,
int layer
) {

/**
* Construct a new block state with the given block state and position. The layer is default to 0.
*
* @param blockState the block state.
* @param pos the position.
*/
public BlockStateWithPos(BlockState blockState, Position3ic pos) {
this(blockState, pos, 0);
}

/**
* Offset the pos with the given {@link BlockFace}, and the other properties remain the same.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.allaymc.api.block.interfaces;

import org.allaymc.api.block.BlockBehavior;
import org.allaymc.api.block.component.BlockLiquidBaseComponent;

public interface BlockLiquidBehavior extends BlockBehavior {
public interface BlockLiquidBehavior extends BlockBehavior, BlockLiquidBaseComponent {
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
import org.allaymc.api.entity.Entity;
import org.allaymc.api.eventbus.event.CancellableEvent;
import org.allaymc.api.item.ItemStack;
import org.allaymc.api.world.Dimension;
import org.joml.Vector3ic;

/**
* @author daoge_cmd
*/
@Getter
public class BlockBreakEvent extends BlockEvent implements CancellableEvent {
protected Dimension dimension;
protected Vector3ic blockPos;
/**
* Can be null
Expand Down
Loading

0 comments on commit 2ef0d35

Please sign in to comment.