fabric icon indicating copy to clipboard operation
fabric copied to clipboard

Add LandPathNodeTypesRegistry

Open devpelux opened this issue 2 years ago • 1 comments

Content

A registry for associations between blocks and path node types, for land entities.

This PR adds a new registry to the content registries module, this registry is useful when some mod adds a new block that cause damage and want to flag this block to be avoided by entities.

In vanilla this is done by getting the node type for the block while calculating the path for the entity (every block is a node).

To establish which node is a specific block is done in LandPathNodeMaker.getCommonNodeType(BlockView world, BlockPos pos).
This is basically a series of ifs, so normally you should add a mixin to this method with an if to give a PathNodeType for your block.

This PR adds a way to do this common thing inside Fabric API.
I use this way in a mod that i'm developing.

public class LandPathNodeMaker extends PathNodeMaker {
[...]
    protected static PathNodeType getCommonNodeType(BlockView world, BlockPos pos) {
        BlockState blockState = world.getBlockState(pos);

// INJECTED HERE ANOTHER IF AND RETURN

        Block block = blockState.getBlock();
        Material material = blockState.getMaterial();
        if (blockState.isAir()) {
            return PathNodeType.OPEN;
        } else if (!blockState.isIn(BlockTags.TRAPDOORS) && !blockState.isOf(Blocks.LILY_PAD) && !blockState.isOf(Blocks.BIG_DRIPLEAF)) {
            if (blockState.isOf(Blocks.POWDER_SNOW)) {
                return PathNodeType.POWDER_SNOW;
            } else if (blockState.isOf(Blocks.CACTUS)) {
                return PathNodeType.DAMAGE_CACTUS;
            } else if (blockState.isOf(Blocks.SWEET_BERRY_BUSH)) {
                return PathNodeType.DAMAGE_OTHER;
            } else if (blockState.isOf(Blocks.HONEY_BLOCK)) {
                return PathNodeType.STICKY_HONEY;
            } else if (blockState.isOf(Blocks.COCOA)) {
                return PathNodeType.COCOA;
            } else {
                FluidState fluidState = world.getFluidState(pos);
                if (fluidState.isIn(FluidTags.LAVA)) {
                    return PathNodeType.LAVA;
                } else if (inflictsFireDamage(blockState)) {
                    return PathNodeType.DAMAGE_FIRE;
                } else if (DoorBlock.isWoodenDoor(blockState) && !(Boolean)blockState.get(DoorBlock.OPEN)) {
                    return PathNodeType.DOOR_WOOD_CLOSED;
                } else if (block instanceof DoorBlock && material == Material.METAL && !(Boolean)blockState.get(DoorBlock.OPEN)) {
                    return PathNodeType.DOOR_IRON_CLOSED;
                } else if (block instanceof DoorBlock && (Boolean)blockState.get(DoorBlock.OPEN)) {
                    return PathNodeType.DOOR_OPEN;
                } else if (block instanceof AbstractRailBlock) {
                    return PathNodeType.RAIL;
                } else if (block instanceof LeavesBlock) {
                    return PathNodeType.LEAVES;
                } else if (blockState.isIn(BlockTags.FENCES) || blockState.isIn(BlockTags.WALLS) || block instanceof FenceGateBlock && !(Boolean)blockState.get(FenceGateBlock.OPEN)) {
                    return PathNodeType.FENCE;
                } else if (!blockState.canPathfindThrough(world, pos, NavigationType.LAND)) {
                    return PathNodeType.BLOCKED;
                } else {
                    return fluidState.isIn(FluidTags.WATER) ? PathNodeType.WATER : PathNodeType.OPEN;
                }
            }
        } else {
            return PathNodeType.TRAPDOOR;
        }
    }
[...]
}

Getting the node type

@Mixin(LandPathNodeMaker.class)
public class LandPathNodeMakerMixin {
    /**
     * Gets the node type for the specified position.
     */
    @Inject(method = "getCommonNodeType", at = @At("HEAD"), cancellable = true)
    private static void getCommonNodeType(@NotNull BlockView world, BlockPos pos, @NotNull CallbackInfoReturnable<PathNodeType> cir, BlockState state) {
        PathNodeType nodeType = LandPathNodeTypesRegistry.getPathNodeType(state, world, pos);

        if (nodeType != null) {
            cir.setReturnValue(nodeType);
        }
    }
}

Why a provider?

I've added the registry as:

private static final HashMap<Block, PathNodeTypeProvider> NODE_TYPES = new HashMap<>();

PathNodeTypeProvider is an interface that has one method:

PathNodeType getPathNodeType(BlockState state, BlockView world, BlockPos pos);

I've chose this way instead to add directly PathNodeType because in a position there is always a block, but in case this block is "fluid-logged", in the same position there is the block and a fluid. With the provider, is possible to specify what to do if there is a "fluid-logged" block, so you can prioritize the block or the fluid.

Testing

For testing Dead Bush is added to the registry with PathNodeType.DAMAGE_OTHER, so it is avoided by entities except foxes.
This is the same as sweet berry bushes.
This is implemented in the testmod.

https://user-images.githubusercontent.com/32025549/182221967-e24de458-94d0-47d3-8ca9-6e513223cdfb.mp4

devpelux avatar Aug 01 '22 19:08 devpelux

Update

I've added the possibility to specify the node type of block if the block is found in a neighbor position in the path and improved docs.

Basically is a mixin to the method above: LandPathNodeMaker.getNodeTypeFromNeighbors(BlockView world, BlockPos.Mutable pos, PathNodeType nodeType)

This is completed now.

Now the provider is changed as this:

PathNodeType getPathNodeType(BlockState state, BlockView world, BlockPos pos, boolean isNeighbor);

Please check if the documentation is written correctly in english or there are grammar errors.

Tecnical

public static PathNodeType getNodeTypeFromNeighbors(BlockView world, BlockPos.Mutable pos, PathNodeType nodeType) {
    int i = pos.getX();
    int j = pos.getY();
    int k = pos.getZ();

    for(int l = -1; l <= 1; ++l) {
        for(int m = -1; m <= 1; ++m) {
            for(int n = -1; n <= 1; ++n) {
                if (l != 0 || n != 0) {
                    pos.set(i + l, j + m, k + n);
                    BlockState blockState = world.getBlockState(pos);

// INJECTED HERE ANOTHER IF AND RETURN

                    if (blockState.isOf(Blocks.CACTUS)) {
                        return PathNodeType.DANGER_CACTUS;
                    }

                    if (blockState.isOf(Blocks.SWEET_BERRY_BUSH)) {
                        return PathNodeType.DANGER_OTHER;
                    }

                    if (inflictsFireDamage(blockState)) {
                        return PathNodeType.DANGER_FIRE;
                    }

                    if (world.getFluidState(pos).isIn(FluidTags.WATER)) {
                        return PathNodeType.WATER_BORDER;
                    }
                }
            }
        }
    }

    return nodeType;
}

devpelux avatar Aug 11 '22 10:08 devpelux

I've added missing CompostingChanceRegistry and FuelRegistry to ContentRegistryTest in the testmod.

devpelux avatar Sep 01 '22 18:09 devpelux

Merged some minor edits into one single commit, then I've reverted to Objects.requireNotNull, and used IdentityHashMap, that is also used for the main tables in the internal vanilla main registries.

devpelux avatar Sep 01 '22 22:09 devpelux

Postmortem: apparently Lithium @Overwrites the very method we mixin into. Whoops.

apple502j avatar Sep 11 '22 13:09 apple502j

September Update (For new readers)

This is reworked in https://github.com/FabricMC/fabric/pull/2519 because of potential performance issues with the hashmap, some useless things, and problems with lithium.

devpelux avatar Sep 11 '22 16:09 devpelux