From 86123a9a0bddfc04c77d3d052cc5ce8ba657922f Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Thu, 30 Apr 2026 14:35:08 +0200 Subject: [PATCH 01/12] fluid registration --- docs/blocks/fluids.md | 151 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 docs/blocks/fluids.md diff --git a/docs/blocks/fluids.md b/docs/blocks/fluids.md new file mode 100644 index 000000000..d29fa632f --- /dev/null +++ b/docs/blocks/fluids.md @@ -0,0 +1,151 @@ +--- +description: How to work with fluids, fluid states and fluid stacks, and how to add your own. +sidebar_position: 3 +--- +# Fluids + +In vanilla Minecraft, the two fluids - water and lava - are special types of [blocks][block] that can spread to neighboring blocks over a certain distance. They are generally not solid, and [entities][entity] can enter and "swim" in them. + +In modded Minecraft, especially in many tech mods, fluids also take on the role of recipe ingredients. This is possible because fluids exist in a separate registry and are only added to the world using fluid blocks, essentially meaning that fluids can be seen in complete independence from blocks. + +This article aims to showcase both the in-world and the recipe aspects of fluids. + +:::warning +Due to vanilla only having two fluids, and those fluids having a lot of special-casing, some of these systems are very hacky and - due to a lot of edge cases that cannot be reasonably caught in testing - may not always work correctly. If you find a bug with fluids, please reach out to us on Discord. +::: + +## `Fluid` and `FluidType` + +Before we can register a fluid, we must first understand a few design decisions made by Minecraft and NeoForge. + +In Minecraft, water and lava each have two variants: a flowing fluid and a source fluid. The way this works is mostly due to hardcoding, in some association with `FluidState`s (see below). Since this hardcoding is inconvenient at best and practically impossible to use at worst, NeoForge introduces the `FluidType` class and patches a ton of places to use it. The main purpose of the `FluidType` is to contain the common logic of the fluid - e.g. the sounds it makes, whether boats can be used in it, etc. - and only leave the actual flowing logic in the fluid itself. `FluidType`s live in a separate registry added by NeoForge, and thus must be registered in addition to `Fluid`s. + +With that in mind, let's start creating our fluid! For the sake of example, we're going to create a molten iron fluid. To get started, we need two [registries][registries]: + +```java +public static final DeferredRegister FLUIDS = + DeferredRegister.create(Registries.FLUID, ExampleMod.MOD_ID); +public static final DeferredRegister FLUID_TYPES = + DeferredRegister.create(NeoForgeRegistries.FLUID_TYPES, ExampleMod.MOD_ID); +``` + +Since `Fluid`s require a `FluidType` to be created, we create the `FluidType` first. A `FluidType`'s options are defined in a `Properties` object, similar to block properties. + +```java +public static final DeferredHolder MOLTEN_IRON_TYPE = FLUID_TYPES.register( + // The registry name of the fluid type. Usually it makes sense to name it the same as the `Fluid`. + "molten_iron", + // The supplier for the fluid type, accepting a `FluidType.Properties` object. + () -> new FluidType(FluidType.Properties.create() + // The translation key of the fluid. While this will not be visible in vanilla Minecraft, + // it will be visible if the fluid is stored in e.g. a modded tank, or when looked at in-world + // with WAILA (What Am I Looking At?) or similar mods installed. + // In order to later make datagen easier, we use a block translation key here. + // If you do not plan on adding a block, you can replace "block." with "fluid." + .descriptionId("block." + ExampleMod.MOD_ID + ".molten_iron") + // Set lava-like sounds for our fluid. This is only relevant if you have a bucket item, + // which we will look at later. + .sound(SoundActions.BUCKET_FILL, SoundEvents.BUCKET_FILL_LAVA) + .sound(SoundActions.BUCKET_EMPTY, SoundEvents.BUCKET_EMPTY_LAVA) + // We cannot swim or drown in molten iron. + .canDrown(false) + .canSwim(false) + // We want molten iron to slightly glow. + .lightLevel(5) + )); +``` + +:::tip +There are a bunch of other methods in `FluidType`. For example, if you were to make a more water-like fluid, the `supportsBoating()` and `isWaterLike()` methods could be interesting to you. For a full list of available methods, please see the source of `FluidType.Properties`. + +Not all of these methods are used by vanilla systems. Some of them, such as `temperature()` or `density()`, were requested in the original design phase of the `FluidType` system for mod compatibility, and may or may not be used by modded systems. +::: + +With our `FluidType` created, we can move to the `Fluid` itself. NeoForge provides the `BaseFlowingFluid` class as a base for us to use, which has three inner classes: `Source`, `Flowing` and `Properties`. `Source` and `Flowing` are subclasses of `BaseFlowingFluid`, following the layout of vanilla's `WaterFluid` and `LavaFluid`, while `Properties` is once again a block properties-like object, this time responsible for tying the fluid type, source fluid, flowing fluid and later also stuff like the bucket or the fluid block together. + +Since the source and flowing fluids depend on the fluid properties but the fluid properties also depends on the two fluids, we need to be a little careful with static initialization order and qualify with the class name in some places. Assuming you are keeping your fluids in a class named `ModFluids`, the code looks as follows: + +```java +// The source fluid. This is usually named without specifying "source" in the name. +public static final DeferredHolder MOLTEN_IRON = FLUIDS.register( + // The registry name. + "molten_iron", + // The source fluid supplier. Qualify the properties with the class name here. + () -> new BaseFlowingFluid.Source(ModFluids.MOLTEN_IRON_PROPERTIES)); + +// The flowing fluid. The name is commonly prefixed with "flowing_". +public static final DeferredHolder FLOWING_MOLTEN_IRON = FLUIDS.register( + // The registry name. + "flowing_molten_iron", + // The flowing fluid supplier. Again, qualify the properties with the class name. + () -> new BaseFlowingFluid.Flowing(ModFluids.MOLTEN_IRON_PROPERTIES)); + +// The fluid properties. We will use this later to connect additional stuff to the fluid, for example the bucket. +public static final BaseFlowingFluid.Properties MOLTEN_IRON_PROPERTIES = + // Parameters are the fluid type, the source fluid and the flowing fluid. + new BaseFlowingFluid.Properties(MOLTEN_IRON_TYPE, MOLTEN_IRON, FLOWING_MOLTEN_IRON); +``` + +With this done, your fluid should now be loaded into the game, and recipes will be able to make use of it. + +## Resources + +While our fluid now exists, we aren't done yet: we still need to add the resource files for the fluid. For a fluid without a block, this is limited to textures and a translation. Blocks later also require a model and a renderer to be set up. + +Let's start by adding the texture files. When creating your assets, it is recommended to use the vanilla water or lava texture as a basis; this is especially important with flowing fluids as they use what is effectively a 2x2 texture that is sampled by the flowing fluid renderer. The texture files must be named and placed as follows (where `examplemod` is your mod id): + +- `assets/examplemod/textures/block/molten_iron_still.png` for the still texture, and +- `assets/examplemod/textures/block/molten_iron_flowing.png` for the flowing texture. + +Most fluids are animated, so they will also need accompanying `.png.mcmeta` files. Again, you can base these off the vanilla files. For more information, see the article on [textures]. + +Now for the translations. The translation key used by fluids is defined by `FluidType#descriptionId()`. In our example, we used `block.examplemod.molten_iron`, so we would add a translation like so: + +```java + @Override + protected void addTranslations() { + // other translations here + + add("block.examplemod.molten_iron", "Molten Iron"); + + // Alternatively, once you have created a fluid block later: + addBlock(ModBlocks.MOLTEN_IRON.get(), "Molten Iron"); + } +``` + +For more information, see [I18n and L10n/Datagen][i18n]. + +## In-World Fluids + +TODO + +### `FluidState` and Waterlogging + +TODO + +### Fluid Blocks + +TODO + +### Cauldrons + +TODO + +## Fluids in Recipes + +TODO + +### `FluidStack` + +TODO + +### `FluidIngredient` + +TODO + +[block]: index.md +[entity]: ../entities/index.md +[i18n]: ../resources/client/i18n.md#datagen +[registries]: ../concepts/registries.md +[tags]: ../resources/server/tags.md#datagen +[textures]: ../resources/client/textures.md From 60923ccf5042e319d190b6dbc31ef4902a7b0d06 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Thu, 30 Apr 2026 15:16:01 +0200 Subject: [PATCH 02/12] fluid states and waterlogging --- docs/blocks/fluids.md | 45 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/docs/blocks/fluids.md b/docs/blocks/fluids.md index d29fa632f..9a2d0e975 100644 --- a/docs/blocks/fluids.md +++ b/docs/blocks/fluids.md @@ -117,11 +117,49 @@ For more information, see [I18n and L10n/Datagen][i18n]. ## In-World Fluids -TODO +When placing fluids in world, `FluidState`s are used instead of `Fluid`s, closely mirroring the use of [`BlockState`s][blockstate] versus `Block`s. Similar to `BlockState`s, `FluidState`s can be set into a level using `Level#setFluidState()`, a `FluidState` at a position can be queried using `Level#getFluidState()`, and the default state can be obtained using `Fluid#defaultFluidState()`. -### `FluidState` and Waterlogging +However, `FluidState`s also exhibit a few differences to `BlockState`s. Most notably, their different states do not operate using properties, at least not properties defined in the same way as block state properties, instead the exact `FluidState` is computed by the level from fluid spreading mechanics. For most use cases the exact `FluidState` is irrelevant, save for some properties such as `isSource()` which can be queried from the `FluidState` if needed. -TODO +Unfortunately, the current implementation of `FluidState`s in levels is very much half-baked. Even more unfortunately, it is impossible for NeoForge to fix this without breaking compatibility with vanilla worlds. Basically all `FluidState` logic is tied to `BlockState` in some way, despite there not really being a need to. In the current implementation, `Level#getFluidState()` essentially boils down to `BlockState#getFluidState()`, happening very deep in chunk storage. It is expected that Mojang will eventually rework this, however for now we have to make do with what we have. + +### Waterlogging + +_See also [Blocks][block] and [Block States][blockstate]._ + +The epitome of this half-baked `FluidState` system is waterlogging. Waterlogging is the ability of certain non-full blocks, e.g. slabs, to also contain a water source at the same time. This is currently implemented via the `WATERLOGGED` block state property: + +```java +// Implementing SimpleWaterloggedBlock automatically enables bucket pickup +// and makes some helper methods available. +public class MyBlock extends Block implements SimpleWaterloggedBlock { + // Add the WATERLOGGED property to our class for easy access. + public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED; + + // Set WATERLOGGED to false by default. + public MyBlock(Properties properties) { + super(properties); + registerDefaultState(getStateDefinition().any().setValue(WATERLOGGED, false)); + } + + // Add WATERLOGGED to the block state definition. + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + super.createBlockStateDefinition(builder); + builder.add(WATERLOGGED); + } + + // The important part: Query the WATERLOGGED property when asked for the fluid state. + // The `false` parameter in Fluids.WATER.getSource(false) means "falling" and is set to false + // for all vanilla waterlogging implementations. + @Override + public FluidState getFluidState(BlockState state) { + return state.getValue(WATERLOGGED) ? Fluids.WATER.getSource(false) : super.getFluidState(state); + } +} +``` + +The `WATERLOGGED` is also the #1 reason NeoForge cannot easily fix this, because removing the `WATERLOGGED` property would break compatibility with vanilla servers due to a different set of block states. ### Fluid Blocks @@ -144,6 +182,7 @@ TODO TODO [block]: index.md +[blockstate]: states.md [entity]: ../entities/index.md [i18n]: ../resources/client/i18n.md#datagen [registries]: ../concepts/registries.md From ec95c48d99d2668814f195842d75e93ee5d28929 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Mon, 4 May 2026 17:40:39 +0200 Subject: [PATCH 03/12] fluid blocks, buckets and cauldrons --- docs/blocks/fluids.md | 416 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 400 insertions(+), 16 deletions(-) diff --git a/docs/blocks/fluids.md b/docs/blocks/fluids.md index 9a2d0e975..54274d284 100644 --- a/docs/blocks/fluids.md +++ b/docs/blocks/fluids.md @@ -94,23 +94,19 @@ While our fluid now exists, we aren't done yet: we still need to add the resourc Let's start by adding the texture files. When creating your assets, it is recommended to use the vanilla water or lava texture as a basis; this is especially important with flowing fluids as they use what is effectively a 2x2 texture that is sampled by the flowing fluid renderer. The texture files must be named and placed as follows (where `examplemod` is your mod id): -- `assets/examplemod/textures/block/molten_iron_still.png` for the still texture, and -- `assets/examplemod/textures/block/molten_iron_flowing.png` for the flowing texture. +- `assets/examplemod/textures/block/molten_iron_still.png` for the still texture, +- `assets/examplemod/textures/block/molten_iron_flowing.png` for the flowing texture, and +- `assets/examplemod/textures/block/molten_iron_overlay.png` for the overlay texture (the overlay texture is optional and only used if the fluid has an associated block; it is displayed transparently when the player is inside the fluid's block). Most fluids are animated, so they will also need accompanying `.png.mcmeta` files. Again, you can base these off the vanilla files. For more information, see the article on [textures]. -Now for the translations. The translation key used by fluids is defined by `FluidType#descriptionId()`. In our example, we used `block.examplemod.molten_iron`, so we would add a translation like so: +Now for the translations. The translation key used by fluids is defined by `FluidType#descriptionId()`, and we can get it from a `FluidType` using `#getDescriptionId()`: ```java - @Override - protected void addTranslations() { - // other translations here - - add("block.examplemod.molten_iron", "Molten Iron"); - - // Alternatively, once you have created a fluid block later: - addBlock(ModBlocks.MOLTEN_IRON.get(), "Molten Iron"); - } +@Override +protected void addTranslations() { + add(AMFluids.MOLTEN_IRON_TYPE.getDescriptionId(), "Molten Iron"); +} ``` For more information, see [I18n and L10n/Datagen][i18n]. @@ -159,15 +155,396 @@ public class MyBlock extends Block implements SimpleWaterloggedBlock { } ``` -The `WATERLOGGED` is also the #1 reason NeoForge cannot easily fix this, because removing the `WATERLOGGED` property would break compatibility with vanilla servers due to a different set of block states. - ### Fluid Blocks -TODO +In order to be able to place our fluid in the world, we need to create a `LiquidBlock` for it: + +```java +// Assuming a DeferredRegister.Blocks named BLOCKS, and assuming the fluid stuff +// is in another class named ModFluids. +public static final DeferredBlock MOLTEN_IRON = BLOCKS.registerBlock( + // The block registry name. + "molten_iron", + // The liquid block factory. + properties -> new LiquidBlock(ModFluids.MOLTEN_IRON.get(), properties), + // The block properties. + () -> BlockBehaviour.Properties.of() + // Standard properties for both vanilla fluids. Strength 100 disables vanilla TNT + // from having effects while allowing modded explosives to still work. + .liquid() + .noLootTable() + .noCollision() + .replaceable() + .pushReaction(PushReaction.DESTROY) + .sound(SoundType.EMPTY) + .strength(100) + // You may define additional properties depending on what your fluid does. + // For example, we could make our molten iron fluid glow slightly: + .lightLevel(_ -> 5) +); +``` + +The block should then be added to the fluid properties like so: + +```java +public static final BaseFlowingFluid.Properties MOLTEN_IRON_PROPERTIES = + new BaseFlowingFluid.Properties(MOLTEN_IRON_TYPE, MOLTEN_IRON, FLOWING_MOLTEN_IRON) + // Set the block, assuming it is located in the `ModBlocks` class. + // Make sure that `ModBlocks` is classloaded before `ModFluids`! + .block(ModBlocks.MOLTEN_IRON); +``` + +Finally, the block needs a model and a renderer. Let's start with the model, which is fairly simple to [generate][modeldatagen]: + +```java +@Override +protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) { + blockModels.createNonTemplateModelBlock(ModBlocks.MOLTEN_IRON.get()); +} +``` + +The renderer, on the other hand, is registered in a [client-only][sides] [mod bus][modbus] [event handler][events]: + +```java +@SubscribeEvent // on the mod event bus only on the physical client +private static void registerFluidModels(RegisterFluidModelsEvent event) { + event.register(new FluidModel.Unbaked( + // The still, flowing and overlay texture materials. + // The overlay material is nullable; if null, no overlay will be displayed. + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_still")), + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_flowing")), + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_overlay")), + // The fluid tint source. We leave it at null, which means no tint. See below for more info. + null), + // Suppliers for the still and flowing fluids. + ModFluids.MOLTEN_IRON::value, + ModFluids.FLOWING_MOLTEN_IRON::value + ); +} +``` + +### Fluid Tint Sources + +_See also: [Tinting][tinting]_ + +Like blocks, fluids can be tinted. In vanilla, water does this, while lava does not. NeoForge patches this system to enable mod support. All related logic goes through the `FluidTintSource` interface. In a simple implementation, it only overrides `#color()`: + +```java +// If possible, we want to use a singleton. +public final class MoltenIronTintSource implements FluidTintSource { + public static final MoltenIronTintSource INSTANCE = new MoltenIronTintSource(); + + private MoltenIronTintSource() {} + + @Override + public int color(FluidState state) { + // Return whatever color you want here. + return 0xff000000; + } +} +``` + +Once we have our tint source, we use it in the `RegisterFluidModelsEvent` like so: + +```java +@SubscribeEvent // on the mod event bus only on the physical client +private static void registerFluidModels(RegisterFluidModelsEvent event) { + event.register(new FluidModel.Unbaked( + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_still")), + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_flowing")), + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_overlay")), + // Use our tint source instance here. + MoltenIronTintSource.INSTANCE), + ModFluids.MOLTEN_IRON::value, + ModFluids.FLOWING_MOLTEN_IRON::value + ); +} +``` + +:::tip +If the implementation only overrides `#color(FluidState)`, you can also use a functional interface lambda instead of a singleton class. +::: + +For more complex behavior, additional methods are available, both of which defer to `#color(FluidState)` by default: + +- `colorInWorld(FluidState fluidState, BlockState blockState, BlockAndTintGetter level, BlockPos pos)` - A position-sensitive method used when displaying the fluid in world. Water uses this for biome-dependent colors. +- `colorAsStack(FluidStack stack)` - A `FluidStack`-sensitive method, which can be used for e.g. [data component][datacomponent]-sensitive tinting. Unused in vanilla, as `FluidStack` is a NeoForge system. + +In addition, `FluidTintSource` extends `BlockTintSource`, which means that all the `BlockState`-sensitive methods are available as well. + +### Buckets + +Fluids can usually be picked up in a bucket. A custom bucket for our fluid can be added like so: + +```java +// Assuming a DeferredRegister.Items named ITEMS, and assuming the fluid stuff +// is in another class named ModFluids. +public static final DeferredItem MOLTEN_IRON_BUCKET = ITEMS.registerItem( + // The registry name. + "molten_iron_bucket", + // The bucket item factory. + properties -> new BucketItem(AMFluids.LIQUID_ETHERIUM.get(), properties), + // The properties supplier. Buckets stack to 1 and return a bucket when used in crafting. + () -> new Item.Properties().stacksTo(1).craftRemainder(Items.BUCKET) +); +``` + +We then add it to our fluid properties like so: + +```java +public static final BaseFlowingFluid.Properties MOLTEN_IRON_PROPERTIES = + new BaseFlowingFluid.Properties(MOLTEN_IRON_TYPE, MOLTEN_IRON, FLOWING_MOLTEN_IRON) + .block(ModBlocks.MOLTEN_IRON) + // Set the bucket, assuming it is located in the `ModItems` class. + // Make sure that `ModItems` is classloaded before `ModFluids`! + .bucket(ModItems.MOLTEN_IRON_BUCKET); +``` + +Next, it is recommended (but not required) to add a dispenser behavior for the bucket: + +```java +@SubscribeEvent // on the mod event bus +private static void commonSetup(FMLCommonSetupEvent event) { + // `DispenserBlock#registerBehavior` is not thread-safe so we wrap it in a lambda. + // The anonymous class seen here is copied from `DispenseItemBehavior#bootStrap()`. + event.enqueueWork(() -> DispenserBlock.registerBehavior(ModItems.MOLTEN_IRON_BUCKET, new DefaultDispenseItemBehavior() { + private final DefaultDispenseItemBehavior defaultDispenseItemBehavior = new DefaultDispenseItemBehavior(); + + @Override + public ItemStack execute(BlockSource source, ItemStack dispensed) { + DispensibleContainerItem bucket = (DispensibleContainerItem) dispensed.getItem(); + BlockPos target = source.pos().relative(source.state().getValue(DispenserBlock.FACING)); + Level level = source.level(); + if (bucket.emptyContents(null, level, target, null, dispensed)) { + bucket.checkExtraContent(null, level, dispensed, target); + return this.consumeWithRemainder(source, dispensed, new ItemStack(Items.BUCKET)); + } else { + return this.defaultDispenseItemBehavior.dispense(source, dispensed); + } + } + })); +} +``` + +:::tip +If you have multiple buckets, you can reuse the same `DispenseItemBehavior` instance for all buckets. +::: + +Finally, all that's left is a translation and a model: + +```java +// In the language provider +@Override +protected void addTranslations() { + add(AMFluids.MOLTEN_IRON_TYPE.get().getDescriptionId(), "Molten Iron"); + addItem(AMItems.MOLTEN_IRON_BUCKET, "Molten Iron Bucket"); +} + +// In the model provider +@Override +protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) { + blockModels.createNonTemplateModelBlock(ModBlocks.MOLTEN_IRON.get()); + // We use NeoForge's `DynamicFluidContainerModel`. + itemModels.itemModelOutput.accept(AMItems.LIQUID_ETHERIUM_BUCKET.get(), new DynamicFluidContainerModel.Unbaked( + // The model's textures. + new DynamicFluidContainerModel.Textures( + Optional.of(new Material(Identifier.withDefaultNamespace("item/bucket"))), + Optional.of(new Material(Identifier.withDefaultNamespace("item/bucket"))), + Optional.of(new Material(Identifier.fromNamespaceAndPath("neoforge", "item/mask/bucket_fluid"))), + Optional.empty() + ), + // The fluid to use. + AMFluids.LIQUID_ETHERIUM.get(), + // Whether the bucket model should be flipped, commonly used for "gaseous" fluids. + false, + // If true, the "cover" texture is a mask. We generally want this for buckets. + true, + // If this is true, if the fluid emits light, the fluid element of the model becomes emissive. + true)); +} +``` ### Cauldrons -TODO +In addition to buckets, it is common for fluids to go in a cauldron. For this, a separate cauldron block is necessary: + +```java +public class MoltenIronCauldronBlock extends AbstractCauldronBlock { + // Block codec boilerplate. + private static final MapCodec CODEC = simpleCodec(MoltenIronCauldronBlock::new); + + @Override + protected MapCodec codec() { + return CODEC; + } + + // The cauldron interaction dispatcher. See below for more info. + public static final CauldronInteraction.Dispatcher CAULDRON_INTERACTIONS = + new CauldronInteraction.Dispatcher(); + + // Pass our `CauldronInteraction.Dispatcher` to super. + public MoltenIronCauldronBlock(Properties properties) { + super(properties, CAULDRON_INTERACTIONS); + } + + // We assume that our cauldron can only ever be completely full, i.e. that we don't have "bottles" + // or a similar intermediary unit present. + @Override + public boolean isFull(BlockState state) { + return true; + } + + // Vanilla water cauldrons output 1-3 based on the fill level, we are always full and therefore output 3. + @Override + protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { + return 3; + } + + // A full cauldron has its visual height at 0.9375 (= 15/16). + @Override + protected double getContentHeight(BlockState state) { + return 0.9375; + } +} +``` + +We then use this cauldron in registration: + +```java +// Assuming a DeferredRegister.Blocks named BLOCKS. +public static final DeferredBlock MOLTEN_IRON_CAULDRON = BLOCKS.registerBlock( + // The registry name. + "molten_iron_cauldron", + // The cauldron constructor reference. + MoltenIronCauldronBlock::new, + // The properties to use. We generally copy the vanilla cauldron. + // Since we gave molten iron a glow, we also apply that to the cauldron. + () -> BlockBehaviour.Properties.ofFullCopy(Blocks.CAULDRON).lightLevel(_ -> 5) +); +``` + +Next, we need to associate a fluid with the cauldron. We do this in `RegisterCauldronFluidContentEvent` like so: + +```java +@SubscribeEvent // on the mod event bus +private static void registerCauldronFluidContent(RegisterCauldronFluidContentEvent event) { + event.register( + // The cauldron block. + ModBlocks.MOLTEN_IRON_CAULDRON.get(), + // The fluid. + ModFluids.MOLTEN_IRON.get(), + // The amount. 1000 is one bucket. + 1000, + // The "level" block state property. Since we don't have one, we pass null. + null); +} +``` + +Finally, since a fluid cauldron is a block like any other, we need some datagen setup. This includes a translation, a block model, a [loot table][loottable] and some [tags]: + +```java +// In the language provider +@Override +protected void addTranslations() { + add(AMFluids.MOLTEN_IRON_TYPE.get().getDescriptionId(), "Molten Iron"); + addItem(ModItems.MOLTEN_IRON_BUCKET, "Molten Iron Bucket"); + addBlock(ModBlocks.MOLTEN_IRON_CAULDRON, "Molten Iron Cauldron"); +} + +// In the model provider +@Override +protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) { + blockModels.createNonTemplateModelBlock(ModBlocks.MOLTEN_IRON.get()); + itemModels.itemModelOutput.accept(...); + blockModels.blockStateOutput.accept(BlockModelGenerators.createSimpleBlock( + // Our cauldron block. + ModBlocks.MOLTEN_IRON_CAULDRON.get(), + // We use the `CAULDRON_FULL` model template. + BlockModelGenerators.plainVariant(ModelTemplates.CAULDRON_FULL.create( + // Our cauldron block. + ModBlocks.MOLTEN_IRON_CAULDRON.get(), + // The cauldron fluid texture mapping. + TextureMapping.cauldron(TextureMapping.getBlockTexture(ModBlocks.MOLTEN_IRON.get(), "_still")), + blockModels.modelOutput)))); +} + +// In the block loot sub provider +@Override +protected void generate() { + // Drop an empty cauldron when mined. + dropOther(ModBlocks.MOLTEN_IRON_CAULDRON.get(), Items.CAULDRON); +} + +// In the block tags provider +@Override +protected void addTags(HolderLookup.Provider provider) { + tag(BlockTags.CAULDRONS).add(ModBlocks.MOLTEN_IRON_CAULDRON.get()); +} +``` + +### Cauldron Interactions + +We now have our cauldron, however we can't yet interact with it, or even obtain it in survival. For that to work, we need to register cauldron interactions. If you recall back to the cauldron class, we had a `CauldronInteraction.Dispatcher`, which we are going to use now. + +Cauldron interactions happen in two events. First, we need to register the `CauldronInteraction.Dispatcher` like so: + +```java +@SubscribeEvent // on the mod event bus +private static void registerCauldronInteractionDispatchers(RegisterCauldronInteractionEvent.Dispatcher event) { + event.register( + // A unique identifier. + Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "molten_iron_cauldron"), + // Our `CauldronInteraction.Dispatcher`. + MoltenIronCauldronBlock.CAULDRON_INTERACTIONS); +} +``` + +Secondly, we need to register the actual interactions. That works like so: + +```java +@SubscribeEvent +private static void registerCauldronInteractions(RegisterCauldronInteractionEvent.Interaction event) { + // Empty our cauldron when it is right-clicked with an empty bucket. + MoltenIronCauldronBlock.CAULDRON_INTERACTIONS.put(Items.BUCKET, + // Input parameters are the cauldron blockstate, the level, the position, + // the player, the used hand, and the used item stack + (state, level, pos, player, hand, stack) -> CauldronInteractions.fillBucket( + // Pass along the input parameters. + state, level, pos, player, hand, stack, + // The resulting item stack. + ModItems.MOLTEN_IRON_BUCKET.toStack(), + // A predicate for additional checks if the bucket can be filled. + // We have no additional checks, so we just always return true. + _ -> true, + // The sound event to play when emptying the cauldron. + SoundEvents.BUCKET_FILL_LAVA)); + + // For compat with vanilla, we need to add handling for when our cauldron is right-clicked + // with water, lava and powder snow buckets. Compat with other mods is handled + // by the bucket fill handler method, see below. + LiquidEtheriumCauldronBlock.CAULDRON_INTERACTIONS + .put(Items.LAVA_BUCKET, CauldronInteractions::fillLavaInteraction); + LiquidEtheriumCauldronBlock.CAULDRON_INTERACTIONS + .put(Items.WATER_BUCKET, CauldronInteractions::fillWaterInteraction); + LiquidEtheriumCauldronBlock.CAULDRON_INTERACTIONS + .put(Items.POWDER_SNOW_BUCKET, CauldronInteractions::fillPowderSnowInteraction); + + // When **any** cauldron is right-clicked with our bucket, replace with our cauldron. + // To do so, we use `event#registerToAll()` instead of `CauldronInteraction.Dispatcher#put()`. + event.registerToAll(ModItems.MOLTEN_IRON_BUCKET.get(), + // Input parameters are the cauldron blockstate, the level, the position, + // the player, the used hand, and the used item stack + (state, level, pos, player, hand, stack) -> CauldronInteractions.fillBucket( + // Pass along the input parameters, except the state. + level, pos, player, hand, stack, + // The resulting block state. + ModBlocks.MOLTEN_IRON_CAULDRON.get().defaultBlockState(), + // The sound event to play when filling the cauldron. + SoundEvents.BUCKET_EMPTY_LAVA)); +} +``` + +Cauldron interactions are not limited to buckets. Vanilla adds a couple of other cauldron recipes, mostly for "cleaning" colored items. These work through generally the same mechanism. For more information, see the `CauldronInteractions` class. This is also where you can find the vanilla cauldron interaction dispatchers. ## Fluids in Recipes @@ -183,8 +560,15 @@ TODO [block]: index.md [blockstate]: states.md +[datacomponent]: ../items/datacomponents.md [entity]: ../entities/index.md +[events]: ../concepts/events.md [i18n]: ../resources/client/i18n.md#datagen +[loottable]: ../resources/server/loottables/index.md#datagen +[modbus]: ../concepts/events.md#event-buses +[modeldatagen]: ../resources/client/models/datagen.md [registries]: ../concepts/registries.md +[sides]: ../concepts/sides.md [tags]: ../resources/server/tags.md#datagen [textures]: ../resources/client/textures.md +[tinting]: ../resources/client/models/index.md#tinting From 3150b5710dd749063944060b58a12490265fe53f Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Mon, 4 May 2026 18:04:07 +0200 Subject: [PATCH 04/12] address Champ's comments and do some rearranging --- docs/blocks/fluids.md | 192 ++++++++++++++++++++++-------------------- 1 file changed, 102 insertions(+), 90 deletions(-) diff --git a/docs/blocks/fluids.md b/docs/blocks/fluids.md index 54274d284..933b4799c 100644 --- a/docs/blocks/fluids.md +++ b/docs/blocks/fluids.md @@ -11,7 +11,7 @@ In modded Minecraft, especially in many tech mods, fluids also take on the role This article aims to showcase both the in-world and the recipe aspects of fluids. :::warning -Due to vanilla only having two fluids, and those fluids having a lot of special-casing, some of these systems are very hacky and - due to a lot of edge cases that cannot be reasonably caught in testing - may not always work correctly. If you find a bug with fluids, please reach out to us on Discord. +Due to vanilla only having two fluids, and those fluids having a lot of special-casing, some of these systems are very hacky and - due to a lot of edge cases that cannot be reasonably caught in testing - may not always work correctly. If you find a bug with fluids, please reach out to us on [Discord][discord], or open an issue on [GitHub][github]. ::: ## `Fluid` and `FluidType` @@ -35,14 +35,13 @@ Since `Fluid`s require a `FluidType` to be created, we create the `FluidType` fi public static final DeferredHolder MOLTEN_IRON_TYPE = FLUID_TYPES.register( // The registry name of the fluid type. Usually it makes sense to name it the same as the `Fluid`. "molten_iron", - // The supplier for the fluid type, accepting a `FluidType.Properties` object. - () -> new FluidType(FluidType.Properties.create() + // The factory for the fluid type, accepting a `FluidType.Properties` object. + id -> new FluidType(FluidType.Properties.create() // The translation key of the fluid. While this will not be visible in vanilla Minecraft, - // it will be visible if the fluid is stored in e.g. a modded tank, or when looked at in-world - // with WAILA (What Am I Looking At?) or similar mods installed. - // In order to later make datagen easier, we use a block translation key here. - // If you do not plan on adding a block, you can replace "block." with "fluid." - .descriptionId("block." + ExampleMod.MOD_ID + ".molten_iron") + // it will be visible if the fluid is stored in e.g. a modded tank, or when looked at + // in-world with WAILA (What Am I Looking At?) or similar mods installed. + // `id` is the lambda parameter we got passed in. + .descriptionId(Util.makeDescriptionId("fluid", id)) // Set lava-like sounds for our fluid. This is only relevant if you have a bucket item, // which we will look at later. .sound(SoundActions.BUCKET_FILL, SoundEvents.BUCKET_FILL_LAVA) @@ -80,27 +79,52 @@ public static final DeferredHolder FLOWING_MOLT // The flowing fluid supplier. Again, qualify the properties with the class name. () -> new BaseFlowingFluid.Flowing(ModFluids.MOLTEN_IRON_PROPERTIES)); -// The fluid properties. We will use this later to connect additional stuff to the fluid, for example the bucket. +// The fluid properties. We will use this later to connect additional stuff +// to the fluid, for example the bucket. public static final BaseFlowingFluid.Properties MOLTEN_IRON_PROPERTIES = // Parameters are the fluid type, the source fluid and the flowing fluid. new BaseFlowingFluid.Properties(MOLTEN_IRON_TYPE, MOLTEN_IRON, FLOWING_MOLTEN_IRON); ``` -With this done, your fluid should now be loaded into the game, and recipes will be able to make use of it. +With this done, your fluid should now be loaded into the game, and recipes will be able to make use of it. However, rendering will be broken. To fix that, we need to register a renderer in a [client-only][sides] [mod bus][modbus] [event handler][events]: + +```java +@SubscribeEvent // on the mod event bus only on the physical client +private static void registerFluidModels(RegisterFluidModelsEvent event) { + event.register(new FluidModel.Unbaked( + // The still, flowing and overlay texture materials. The overlay material is nullable; + // if it is null, no overlay will be displayed. Overlays are only used for in-world fluids, + // so if you don't have an in-world fluid, it should always be null. + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_still")), + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_flowing")), + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_overlay")), + // The fluid tint source. We leave it at null, which means no tint. See below for more info. + null), + // Suppliers for the still and flowing fluids. + ModFluids.MOLTEN_IRON::value, + ModFluids.FLOWING_MOLTEN_IRON::value + ); +} +``` ## Resources -While our fluid now exists, we aren't done yet: we still need to add the resource files for the fluid. For a fluid without a block, this is limited to textures and a translation. Blocks later also require a model and a renderer to be set up. +While our fluid now exists, we aren't done yet: we still need to add the resource files for the fluid. This consists of textures, a model and a translation. Let's start by adding the texture files. When creating your assets, it is recommended to use the vanilla water or lava texture as a basis; this is especially important with flowing fluids as they use what is effectively a 2x2 texture that is sampled by the flowing fluid renderer. The texture files must be named and placed as follows (where `examplemod` is your mod id): - `assets/examplemod/textures/block/molten_iron_still.png` for the still texture, - `assets/examplemod/textures/block/molten_iron_flowing.png` for the flowing texture, and -- `assets/examplemod/textures/block/molten_iron_overlay.png` for the overlay texture (the overlay texture is optional and only used if the fluid has an associated block; it is displayed transparently when the player is inside the fluid's block). +- `assets/examplemod/textures/block/molten_iron_overlay.png` for the overlay texture (if applicable). + +:::warning +These paths match the paths we passed into `RegisterFluidModelsEvent#register()` before. You can place the files elsewhere, but you will need to adjust the paths in the renderer as well. +::: Most fluids are animated, so they will also need accompanying `.png.mcmeta` files. Again, you can base these off the vanilla files. For more information, see the article on [textures]. -Now for the translations. The translation key used by fluids is defined by `FluidType#descriptionId()`, and we can get it from a `FluidType` using `#getDescriptionId()`: + +Finally, the translations. The translation key used by fluids is defined by `FluidType#descriptionId()`, and we can get it from a `FluidType` using `#getDescriptionId()`: ```java @Override @@ -111,6 +135,55 @@ protected void addTranslations() { For more information, see [I18n and L10n/Datagen][i18n]. +## Fluid Tint Sources + +_See also: [Tinting][tinting]_ + +Like blocks, fluids can be tinted. In vanilla, water does this, while lava does not. NeoForge patches this system to enable mod support. All related logic goes through the `FluidTintSource` interface. In a simple implementation, it only overrides `#color()`: + +```java +// If possible, we want to use a singleton. +public final class MoltenIronTintSource implements FluidTintSource { + public static final MoltenIronTintSource INSTANCE = new MoltenIronTintSource(); + + private MoltenIronTintSource() {} + + @Override + public int color(FluidState state) { + // Return whatever color you want here. + return 0xff000000; + } +} +``` + +Once we have our tint source, we use it in the `RegisterFluidModelsEvent` like so: + +```java +@SubscribeEvent // on the mod event bus only on the physical client +private static void registerFluidModels(RegisterFluidModelsEvent event) { + event.register(new FluidModel.Unbaked( + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_still")), + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_flowing")), + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_overlay")), + // Use our tint source instance here. + MoltenIronTintSource.INSTANCE), + ModFluids.MOLTEN_IRON::value, + ModFluids.FLOWING_MOLTEN_IRON::value + ); +} +``` + +:::tip +If the implementation only overrides `#color(FluidState)`, you can also use a functional interface lambda instead of a singleton class. +::: + +For more complex behavior, additional methods are available, both of which defer to `#color(FluidState)` by default: + +- `colorInWorld(FluidState fluidState, BlockState blockState, BlockAndTintGetter level, BlockPos pos)` - A position-sensitive method used when displaying the fluid in world. Water uses this for biome-dependent colors. +- `colorAsStack(FluidStack stack)` - A `FluidStack`-sensitive method, which can be used for e.g. [data component][datacomponent]-sensitive tinting. Unused in vanilla, as `FluidStack` is a NeoForge system. + +In addition, `FluidTintSource` extends `BlockTintSource`, which means that all the `BlockState`-sensitive methods are available as well. + ## In-World Fluids When placing fluids in world, `FluidState`s are used instead of `Fluid`s, closely mirroring the use of [`BlockState`s][blockstate] versus `Block`s. Similar to `BlockState`s, `FluidState`s can be set into a level using `Level#setFluidState()`, a `FluidState` at a position can be queried using `Level#getFluidState()`, and the default state can be obtained using `Fluid#defaultFluidState()`. @@ -155,6 +228,10 @@ public class MyBlock extends Block implements SimpleWaterloggedBlock { } ``` +:::info +"Lavalogging" or similar fluid-logging with other fluids is easily possible. To do so, simply create a new `BooleanProperty`, add it to the block as usual, and have `Block#getFluidState()` return the desired fluid if the property is true. +::: + ### Fluid Blocks In order to be able to place our fluid in the world, we need to create a `LiquidBlock` for it: @@ -179,7 +256,7 @@ public static final DeferredBlock MOLTEN_IRON = BLOCKS.registerBloc .sound(SoundType.EMPTY) .strength(100) // You may define additional properties depending on what your fluid does. - // For example, we could make our molten iron fluid glow slightly: + // For example, like before, we make our molten iron fluid glow slightly: .lightLevel(_ -> 5) ); ``` @@ -203,75 +280,6 @@ protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerat } ``` -The renderer, on the other hand, is registered in a [client-only][sides] [mod bus][modbus] [event handler][events]: - -```java -@SubscribeEvent // on the mod event bus only on the physical client -private static void registerFluidModels(RegisterFluidModelsEvent event) { - event.register(new FluidModel.Unbaked( - // The still, flowing and overlay texture materials. - // The overlay material is nullable; if null, no overlay will be displayed. - new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_still")), - new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_flowing")), - new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_overlay")), - // The fluid tint source. We leave it at null, which means no tint. See below for more info. - null), - // Suppliers for the still and flowing fluids. - ModFluids.MOLTEN_IRON::value, - ModFluids.FLOWING_MOLTEN_IRON::value - ); -} -``` - -### Fluid Tint Sources - -_See also: [Tinting][tinting]_ - -Like blocks, fluids can be tinted. In vanilla, water does this, while lava does not. NeoForge patches this system to enable mod support. All related logic goes through the `FluidTintSource` interface. In a simple implementation, it only overrides `#color()`: - -```java -// If possible, we want to use a singleton. -public final class MoltenIronTintSource implements FluidTintSource { - public static final MoltenIronTintSource INSTANCE = new MoltenIronTintSource(); - - private MoltenIronTintSource() {} - - @Override - public int color(FluidState state) { - // Return whatever color you want here. - return 0xff000000; - } -} -``` - -Once we have our tint source, we use it in the `RegisterFluidModelsEvent` like so: - -```java -@SubscribeEvent // on the mod event bus only on the physical client -private static void registerFluidModels(RegisterFluidModelsEvent event) { - event.register(new FluidModel.Unbaked( - new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_still")), - new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_flowing")), - new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_overlay")), - // Use our tint source instance here. - MoltenIronTintSource.INSTANCE), - ModFluids.MOLTEN_IRON::value, - ModFluids.FLOWING_MOLTEN_IRON::value - ); -} -``` - -:::tip -If the implementation only overrides `#color(FluidState)`, you can also use a functional interface lambda instead of a singleton class. -::: - -For more complex behavior, additional methods are available, both of which defer to `#color(FluidState)` by default: - -- `colorInWorld(FluidState fluidState, BlockState blockState, BlockAndTintGetter level, BlockPos pos)` - A position-sensitive method used when displaying the fluid in world. Water uses this for biome-dependent colors. -- `colorAsStack(FluidStack stack)` - A `FluidStack`-sensitive method, which can be used for e.g. [data component][datacomponent]-sensitive tinting. Unused in vanilla, as `FluidStack` is a NeoForge system. - -In addition, `FluidTintSource` extends `BlockTintSource`, which means that all the `BlockState`-sensitive methods are available as well. - ### Buckets Fluids can usually be picked up in a bucket. A custom bucket for our fluid can be added like so: @@ -371,7 +379,8 @@ In addition to buckets, it is common for fluids to go in a cauldron. For this, a ```java public class MoltenIronCauldronBlock extends AbstractCauldronBlock { // Block codec boilerplate. - private static final MapCodec CODEC = simpleCodec(MoltenIronCauldronBlock::new); + private static final MapCodec CODEC = + simpleCodec(MoltenIronCauldronBlock::new); @Override protected MapCodec codec() { @@ -394,7 +403,8 @@ public class MoltenIronCauldronBlock extends AbstractCauldronBlock { return true; } - // Vanilla water cauldrons output 1-3 based on the fill level, we are always full and therefore output 3. + // Vanilla water cauldrons output 1-3 based on the fill level, + // we are always full and therefore output 3. @Override protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { return 3; @@ -461,11 +471,11 @@ protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerat ModBlocks.MOLTEN_IRON_CAULDRON.get(), // We use the `CAULDRON_FULL` model template. BlockModelGenerators.plainVariant(ModelTemplates.CAULDRON_FULL.create( - // Our cauldron block. - ModBlocks.MOLTEN_IRON_CAULDRON.get(), - // The cauldron fluid texture mapping. - TextureMapping.cauldron(TextureMapping.getBlockTexture(ModBlocks.MOLTEN_IRON.get(), "_still")), - blockModels.modelOutput)))); + // Our cauldron block. + ModBlocks.MOLTEN_IRON_CAULDRON.get(), + // The cauldron fluid texture mapping. + TextureMapping.cauldron(TextureMapping.getBlockTexture(ModBlocks.MOLTEN_IRON.get(), "_still")), + blockModels.modelOutput)))); } // In the block loot sub provider @@ -561,8 +571,10 @@ TODO [block]: index.md [blockstate]: states.md [datacomponent]: ../items/datacomponents.md +[discord]: https://discord.neoforged.net/ [entity]: ../entities/index.md [events]: ../concepts/events.md +[github]: https://github.com/neoforged/NeoForge/issues [i18n]: ../resources/client/i18n.md#datagen [loottable]: ../resources/server/loottables/index.md#datagen [modbus]: ../concepts/events.md#event-buses From 5331307326ff94617e2ebed0671cee257aed3c3e Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Mon, 4 May 2026 18:25:57 +0200 Subject: [PATCH 05/12] create a separate fluids category --- docs/advanced/_category_.json | 2 +- docs/datastorage/_category_.json | 2 +- docs/fluids/_category_.json | 4 + docs/fluids/index.md | 200 ++++++++++++++++ docs/{blocks/fluids.md => fluids/inworld.md} | 237 ++----------------- docs/fluids/recipes.md | 15 ++ docs/inventories/_category_.json | 2 +- docs/misc/_category_.json | 2 +- docs/networking/_category_.json | 2 +- docs/rendering/_category_.json | 2 +- docs/resources/_category_.json | 2 +- docs/worldgen/_category_.json | 2 +- 12 files changed, 244 insertions(+), 228 deletions(-) create mode 100644 docs/fluids/_category_.json create mode 100644 docs/fluids/index.md rename docs/{blocks/fluids.md => fluids/inworld.md} (54%) create mode 100644 docs/fluids/recipes.md diff --git a/docs/advanced/_category_.json b/docs/advanced/_category_.json index 4a462c5f9..0e7b30f5b 100644 --- a/docs/advanced/_category_.json +++ b/docs/advanced/_category_.json @@ -1,4 +1,4 @@ { "label": "Advanced Topics", - "position": 13 + "position": 14 } \ No newline at end of file diff --git a/docs/datastorage/_category_.json b/docs/datastorage/_category_.json index c26607fec..6ead69849 100644 --- a/docs/datastorage/_category_.json +++ b/docs/datastorage/_category_.json @@ -1,4 +1,4 @@ { "label": "Data Storage", - "position": 9 + "position": 10 } \ No newline at end of file diff --git a/docs/fluids/_category_.json b/docs/fluids/_category_.json new file mode 100644 index 000000000..ebe759951 --- /dev/null +++ b/docs/fluids/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Fluids", + "position": 7 +} \ No newline at end of file diff --git a/docs/fluids/index.md b/docs/fluids/index.md new file mode 100644 index 000000000..30597ce3a --- /dev/null +++ b/docs/fluids/index.md @@ -0,0 +1,200 @@ +--- +description: How to add your own fluids. +sidebar_position: 1 +--- +# Fluids + +In vanilla Minecraft, the two fluids - water and lava - are special types of [blocks][block] that can spread to neighboring blocks over a certain distance. They are generally not solid, and [entities][entity] can enter and "swim" in them. + +In modded Minecraft, especially in many tech mods, fluids also take on the role of recipe ingredients. This is possible because fluids exist in a separate registry and are only added to the world using fluid blocks, essentially meaning that fluids can be seen in complete independence from blocks. + +This article covers how to add your own fluids. For the in-world component of the fluid system, see [In-World Fluids][inworld]. For using fluids in a recipe context, see [Fluids in Recipes][recipes]. + +:::warning +Due to vanilla only having two fluids, and those fluids having a lot of special-casing, some of the systems in this category are very hacky and - due to a lot of edge cases that cannot be reasonably caught in testing - may not always work correctly. If you find a bug with fluids, please reach out to us on [Discord][discord], or open an issue on [GitHub][github]. +::: + +## `Fluid` and `FluidType` + +Before we can register a fluid, we must first understand a few design decisions made by Minecraft and NeoForge. + +In Minecraft, water and lava each have two variants: a flowing fluid and a source fluid. The way this works is mostly due to hardcoding, in some association with `FluidState`s (see below). Since this hardcoding is inconvenient at best and practically impossible to use at worst, NeoForge introduces the `FluidType` class and patches a ton of places to use it. The main purpose of the `FluidType` is to contain the common logic of the fluid - e.g. the sounds it makes, whether boats can be used in it, etc. - and only leave the actual flowing logic in the fluid itself. `FluidType`s live in a separate registry added by NeoForge, and thus must be registered in addition to `Fluid`s. + +With that in mind, let's start creating our fluid! For the sake of example, we're going to create a molten iron fluid. To get started, we need two [registries][registries]: + +```java +public static final DeferredRegister FLUIDS = + DeferredRegister.create(Registries.FLUID, ExampleMod.MOD_ID); +public static final DeferredRegister FLUID_TYPES = + DeferredRegister.create(NeoForgeRegistries.FLUID_TYPES, ExampleMod.MOD_ID); +``` + +Since `Fluid`s require a `FluidType` to be created, we create the `FluidType` first. A `FluidType`'s options are defined in a `Properties` object, similar to block properties. + +```java +public static final DeferredHolder MOLTEN_IRON_TYPE = FLUID_TYPES.register( + // The registry name of the fluid type. Usually it makes sense to name it the same as the `Fluid`. + "molten_iron", + // The factory for the fluid type, accepting a `FluidType.Properties` object. + id -> new FluidType(FluidType.Properties.create() + // The translation key of the fluid. While this will not be visible in vanilla Minecraft, + // it will be visible if the fluid is stored in e.g. a modded tank, or when looked at + // in-world with WAILA (What Am I Looking At?) or similar mods installed. + // `id` is the lambda parameter we got passed in. + .descriptionId(Util.makeDescriptionId("fluid", id)) + // Set lava-like sounds for our fluid. This is only relevant if you have a bucket item, + // which we will look at later. + .sound(SoundActions.BUCKET_FILL, SoundEvents.BUCKET_FILL_LAVA) + .sound(SoundActions.BUCKET_EMPTY, SoundEvents.BUCKET_EMPTY_LAVA) + // We cannot swim or drown in molten iron. + .canDrown(false) + .canSwim(false) + // We want molten iron to slightly glow. + .lightLevel(5) + )); +``` + +:::tip +There are a bunch of other methods in `FluidType`. For example, if you were to make a more water-like fluid, the `supportsBoating()` and `isWaterLike()` methods could be interesting to you. For a full list of available methods, please see the source of `FluidType.Properties`. + +Not all of these methods are used by vanilla systems. Some of them, such as `temperature()` or `density()`, were requested in the original design phase of the `FluidType` system for mod compatibility, and may or may not be used by modded systems. +::: + +With our `FluidType` created, we can move to the `Fluid` itself. NeoForge provides the `BaseFlowingFluid` class as a base for us to use, which has three inner classes: `Source`, `Flowing` and `Properties`. `Source` and `Flowing` are subclasses of `BaseFlowingFluid`, following the layout of vanilla's `WaterFluid` and `LavaFluid`, while `Properties` is once again a block properties-like object, this time responsible for tying the fluid type, source fluid, flowing fluid and later also stuff like the bucket or the fluid block together. + +Since the source and flowing fluids depend on the fluid properties but the fluid properties also depends on the two fluids, we need to be a little careful with static initialization order and qualify with the class name in some places. Assuming you are keeping your fluids in a class named `ModFluids`, the code looks as follows: + +```java +// The source fluid. This is usually named without specifying "source" in the name. +public static final DeferredHolder MOLTEN_IRON = FLUIDS.register( + // The registry name. + "molten_iron", + // The source fluid supplier. Qualify the properties with the class name here. + () -> new BaseFlowingFluid.Source(ModFluids.MOLTEN_IRON_PROPERTIES)); + +// The flowing fluid. The name is commonly prefixed with "flowing_". +public static final DeferredHolder FLOWING_MOLTEN_IRON = FLUIDS.register( + // The registry name. + "flowing_molten_iron", + // The flowing fluid supplier. Again, qualify the properties with the class name. + () -> new BaseFlowingFluid.Flowing(ModFluids.MOLTEN_IRON_PROPERTIES)); + +// The fluid properties. We will use this later to connect additional stuff +// to the fluid, for example the bucket. +public static final BaseFlowingFluid.Properties MOLTEN_IRON_PROPERTIES = + // Parameters are the fluid type, the source fluid and the flowing fluid. + new BaseFlowingFluid.Properties(MOLTEN_IRON_TYPE, MOLTEN_IRON, FLOWING_MOLTEN_IRON); +``` + +With this done, your fluid should now be loaded into the game, and recipes will be able to make use of it. However, rendering will be broken. To fix that, we need to register a renderer in a [client-only][sides] [mod bus][modbus] [event handler][events]: + +```java +@SubscribeEvent // on the mod event bus only on the physical client +private static void registerFluidModels(RegisterFluidModelsEvent event) { + event.register(new FluidModel.Unbaked( + // The still, flowing and overlay texture materials. The overlay material is nullable; + // if it is null, no overlay will be displayed. Overlays are only used for in-world fluids, + // so if you don't have an in-world fluid, it should always be null. + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_still")), + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_flowing")), + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_overlay")), + // The fluid tint source. We leave it at null, which means no tint. See below for more info. + null), + // Suppliers for the still and flowing fluids. + ModFluids.MOLTEN_IRON::value, + ModFluids.FLOWING_MOLTEN_IRON::value + ); +} +``` + +## Resources + +While our fluid now exists, we aren't done yet: we still need to add the resource files for the fluid. This consists of textures, a model and a translation. + +Let's start by adding the texture files. When creating your assets, it is recommended to use the vanilla water or lava texture as a basis; this is especially important with flowing fluids as they use what is effectively a 2x2 texture that is sampled by the flowing fluid renderer. The texture files must be named and placed as follows (where `examplemod` is your mod id): + +- `assets/examplemod/textures/block/molten_iron_still.png` for the still texture, +- `assets/examplemod/textures/block/molten_iron_flowing.png` for the flowing texture, and +- `assets/examplemod/textures/block/molten_iron_overlay.png` for the overlay texture (if applicable). + +:::warning +These paths match the paths we passed into `RegisterFluidModelsEvent#register()` before. You can place the files elsewhere, but you will need to adjust the paths in the renderer as well. +::: + +Most fluids are animated, so they will also need accompanying `.png.mcmeta` files. Again, you can base these off the vanilla files. For more information, see the article on [textures]. + + +Finally, the translations. The translation key used by fluids is defined by `FluidType#descriptionId()`, and we can get it from a `FluidType` using `#getDescriptionId()`: + +```java +@Override +protected void addTranslations() { + add(AMFluids.MOLTEN_IRON_TYPE.getDescriptionId(), "Molten Iron"); +} +``` + +For more information, see [I18n and L10n/Datagen][i18n]. + +## Fluid Tint Sources + +_See also: [Tinting][tinting]_ + +Like blocks, fluids can be tinted. In vanilla, water does this, while lava does not. NeoForge patches this system to enable mod support. All related logic goes through the `FluidTintSource` interface. In a simple implementation, it only overrides `#color()`: + +```java +// If possible, we want to use a singleton. +public final class MoltenIronTintSource implements FluidTintSource { + public static final MoltenIronTintSource INSTANCE = new MoltenIronTintSource(); + + private MoltenIronTintSource() {} + + @Override + public int color(FluidState state) { + // Return whatever color you want here. + return 0xff000000; + } +} +``` + +Once we have our tint source, we use it in the `RegisterFluidModelsEvent` like so: + +```java +@SubscribeEvent // on the mod event bus only on the physical client +private static void registerFluidModels(RegisterFluidModelsEvent event) { + event.register(new FluidModel.Unbaked( + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_still")), + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_flowing")), + new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_overlay")), + // Use our tint source instance here. + MoltenIronTintSource.INSTANCE), + ModFluids.MOLTEN_IRON::value, + ModFluids.FLOWING_MOLTEN_IRON::value + ); +} +``` + +:::tip +If the implementation only overrides `#color(FluidState)`, you can also use a functional interface lambda instead of a singleton class. +::: + +For more complex behavior, additional methods are available, both of which defer to `#color(FluidState)` by default: + +- `colorInWorld(FluidState fluidState, BlockState blockState, BlockAndTintGetter level, BlockPos pos)` - A position-sensitive method used when displaying the fluid in world. Water uses this for biome-dependent colors. +- `colorAsStack(FluidStack stack)` - A `FluidStack`-sensitive method, which can be used for e.g. [data component][datacomponent]-sensitive tinting. Unused in vanilla, as `FluidStack` is a NeoForge system. + +In addition, `FluidTintSource` extends `BlockTintSource`, which means that all the `BlockState`-sensitive methods are available as well. + +[block]: ../blocks/index.md +[datacomponent]: ../items/datacomponents.md +[discord]: https://discord.neoforged.net/ +[entity]: ../entities/index.md +[events]: ../concepts/events.md +[github]: https://github.com/neoforged/NeoForge/issues +[i18n]: ../resources/client/i18n.md#datagen +[inworld]: inworld.md +[modbus]: ../concepts/events.md#event-buses +[recipes]: recipes.md +[registries]: ../concepts/registries.md +[sides]: ../concepts/sides.md +[textures]: ../resources/client/textures.md +[tinting]: ../resources/client/models/index.md#tinting diff --git a/docs/blocks/fluids.md b/docs/fluids/inworld.md similarity index 54% rename from docs/blocks/fluids.md rename to docs/fluids/inworld.md index 933b4799c..53983b895 100644 --- a/docs/blocks/fluids.md +++ b/docs/fluids/inworld.md @@ -1,202 +1,20 @@ --- -description: How to work with fluids, fluid states and fluid stacks, and how to add your own. -sidebar_position: 3 +description: How to add and work with fluids in-world. +sidebar_position: 2 --- -# Fluids +# In-World Fluids -In vanilla Minecraft, the two fluids - water and lava - are special types of [blocks][block] that can spread to neighboring blocks over a certain distance. They are generally not solid, and [entities][entity] can enter and "swim" in them. - -In modded Minecraft, especially in many tech mods, fluids also take on the role of recipe ingredients. This is possible because fluids exist in a separate registry and are only added to the world using fluid blocks, essentially meaning that fluids can be seen in complete independence from blocks. - -This article aims to showcase both the in-world and the recipe aspects of fluids. - -:::warning -Due to vanilla only having two fluids, and those fluids having a lot of special-casing, some of these systems are very hacky and - due to a lot of edge cases that cannot be reasonably caught in testing - may not always work correctly. If you find a bug with fluids, please reach out to us on [Discord][discord], or open an issue on [GitHub][github]. -::: - -## `Fluid` and `FluidType` - -Before we can register a fluid, we must first understand a few design decisions made by Minecraft and NeoForge. - -In Minecraft, water and lava each have two variants: a flowing fluid and a source fluid. The way this works is mostly due to hardcoding, in some association with `FluidState`s (see below). Since this hardcoding is inconvenient at best and practically impossible to use at worst, NeoForge introduces the `FluidType` class and patches a ton of places to use it. The main purpose of the `FluidType` is to contain the common logic of the fluid - e.g. the sounds it makes, whether boats can be used in it, etc. - and only leave the actual flowing logic in the fluid itself. `FluidType`s live in a separate registry added by NeoForge, and thus must be registered in addition to `Fluid`s. - -With that in mind, let's start creating our fluid! For the sake of example, we're going to create a molten iron fluid. To get started, we need two [registries][registries]: - -```java -public static final DeferredRegister FLUIDS = - DeferredRegister.create(Registries.FLUID, ExampleMod.MOD_ID); -public static final DeferredRegister FLUID_TYPES = - DeferredRegister.create(NeoForgeRegistries.FLUID_TYPES, ExampleMod.MOD_ID); -``` - -Since `Fluid`s require a `FluidType` to be created, we create the `FluidType` first. A `FluidType`'s options are defined in a `Properties` object, similar to block properties. - -```java -public static final DeferredHolder MOLTEN_IRON_TYPE = FLUID_TYPES.register( - // The registry name of the fluid type. Usually it makes sense to name it the same as the `Fluid`. - "molten_iron", - // The factory for the fluid type, accepting a `FluidType.Properties` object. - id -> new FluidType(FluidType.Properties.create() - // The translation key of the fluid. While this will not be visible in vanilla Minecraft, - // it will be visible if the fluid is stored in e.g. a modded tank, or when looked at - // in-world with WAILA (What Am I Looking At?) or similar mods installed. - // `id` is the lambda parameter we got passed in. - .descriptionId(Util.makeDescriptionId("fluid", id)) - // Set lava-like sounds for our fluid. This is only relevant if you have a bucket item, - // which we will look at later. - .sound(SoundActions.BUCKET_FILL, SoundEvents.BUCKET_FILL_LAVA) - .sound(SoundActions.BUCKET_EMPTY, SoundEvents.BUCKET_EMPTY_LAVA) - // We cannot swim or drown in molten iron. - .canDrown(false) - .canSwim(false) - // We want molten iron to slightly glow. - .lightLevel(5) - )); -``` - -:::tip -There are a bunch of other methods in `FluidType`. For example, if you were to make a more water-like fluid, the `supportsBoating()` and `isWaterLike()` methods could be interesting to you. For a full list of available methods, please see the source of `FluidType.Properties`. - -Not all of these methods are used by vanilla systems. Some of them, such as `temperature()` or `density()`, were requested in the original design phase of the `FluidType` system for mod compatibility, and may or may not be used by modded systems. -::: - -With our `FluidType` created, we can move to the `Fluid` itself. NeoForge provides the `BaseFlowingFluid` class as a base for us to use, which has three inner classes: `Source`, `Flowing` and `Properties`. `Source` and `Flowing` are subclasses of `BaseFlowingFluid`, following the layout of vanilla's `WaterFluid` and `LavaFluid`, while `Properties` is once again a block properties-like object, this time responsible for tying the fluid type, source fluid, flowing fluid and later also stuff like the bucket or the fluid block together. - -Since the source and flowing fluids depend on the fluid properties but the fluid properties also depends on the two fluids, we need to be a little careful with static initialization order and qualify with the class name in some places. Assuming you are keeping your fluids in a class named `ModFluids`, the code looks as follows: - -```java -// The source fluid. This is usually named without specifying "source" in the name. -public static final DeferredHolder MOLTEN_IRON = FLUIDS.register( - // The registry name. - "molten_iron", - // The source fluid supplier. Qualify the properties with the class name here. - () -> new BaseFlowingFluid.Source(ModFluids.MOLTEN_IRON_PROPERTIES)); - -// The flowing fluid. The name is commonly prefixed with "flowing_". -public static final DeferredHolder FLOWING_MOLTEN_IRON = FLUIDS.register( - // The registry name. - "flowing_molten_iron", - // The flowing fluid supplier. Again, qualify the properties with the class name. - () -> new BaseFlowingFluid.Flowing(ModFluids.MOLTEN_IRON_PROPERTIES)); - -// The fluid properties. We will use this later to connect additional stuff -// to the fluid, for example the bucket. -public static final BaseFlowingFluid.Properties MOLTEN_IRON_PROPERTIES = - // Parameters are the fluid type, the source fluid and the flowing fluid. - new BaseFlowingFluid.Properties(MOLTEN_IRON_TYPE, MOLTEN_IRON, FLOWING_MOLTEN_IRON); -``` - -With this done, your fluid should now be loaded into the game, and recipes will be able to make use of it. However, rendering will be broken. To fix that, we need to register a renderer in a [client-only][sides] [mod bus][modbus] [event handler][events]: - -```java -@SubscribeEvent // on the mod event bus only on the physical client -private static void registerFluidModels(RegisterFluidModelsEvent event) { - event.register(new FluidModel.Unbaked( - // The still, flowing and overlay texture materials. The overlay material is nullable; - // if it is null, no overlay will be displayed. Overlays are only used for in-world fluids, - // so if you don't have an in-world fluid, it should always be null. - new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_still")), - new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_flowing")), - new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_overlay")), - // The fluid tint source. We leave it at null, which means no tint. See below for more info. - null), - // Suppliers for the still and flowing fluids. - ModFluids.MOLTEN_IRON::value, - ModFluids.FLOWING_MOLTEN_IRON::value - ); -} -``` - -## Resources - -While our fluid now exists, we aren't done yet: we still need to add the resource files for the fluid. This consists of textures, a model and a translation. - -Let's start by adding the texture files. When creating your assets, it is recommended to use the vanilla water or lava texture as a basis; this is especially important with flowing fluids as they use what is effectively a 2x2 texture that is sampled by the flowing fluid renderer. The texture files must be named and placed as follows (where `examplemod` is your mod id): - -- `assets/examplemod/textures/block/molten_iron_still.png` for the still texture, -- `assets/examplemod/textures/block/molten_iron_flowing.png` for the flowing texture, and -- `assets/examplemod/textures/block/molten_iron_overlay.png` for the overlay texture (if applicable). - -:::warning -These paths match the paths we passed into `RegisterFluidModelsEvent#register()` before. You can place the files elsewhere, but you will need to adjust the paths in the renderer as well. -::: - -Most fluids are animated, so they will also need accompanying `.png.mcmeta` files. Again, you can base these off the vanilla files. For more information, see the article on [textures]. - - -Finally, the translations. The translation key used by fluids is defined by `FluidType#descriptionId()`, and we can get it from a `FluidType` using `#getDescriptionId()`: - -```java -@Override -protected void addTranslations() { - add(AMFluids.MOLTEN_IRON_TYPE.getDescriptionId(), "Molten Iron"); -} -``` - -For more information, see [I18n and L10n/Datagen][i18n]. - -## Fluid Tint Sources - -_See also: [Tinting][tinting]_ - -Like blocks, fluids can be tinted. In vanilla, water does this, while lava does not. NeoForge patches this system to enable mod support. All related logic goes through the `FluidTintSource` interface. In a simple implementation, it only overrides `#color()`: - -```java -// If possible, we want to use a singleton. -public final class MoltenIronTintSource implements FluidTintSource { - public static final MoltenIronTintSource INSTANCE = new MoltenIronTintSource(); - - private MoltenIronTintSource() {} - - @Override - public int color(FluidState state) { - // Return whatever color you want here. - return 0xff000000; - } -} -``` - -Once we have our tint source, we use it in the `RegisterFluidModelsEvent` like so: - -```java -@SubscribeEvent // on the mod event bus only on the physical client -private static void registerFluidModels(RegisterFluidModelsEvent event) { - event.register(new FluidModel.Unbaked( - new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_still")), - new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_flowing")), - new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_overlay")), - // Use our tint source instance here. - MoltenIronTintSource.INSTANCE), - ModFluids.MOLTEN_IRON::value, - ModFluids.FLOWING_MOLTEN_IRON::value - ); -} -``` - -:::tip -If the implementation only overrides `#color(FluidState)`, you can also use a functional interface lambda instead of a singleton class. -::: - -For more complex behavior, additional methods are available, both of which defer to `#color(FluidState)` by default: - -- `colorInWorld(FluidState fluidState, BlockState blockState, BlockAndTintGetter level, BlockPos pos)` - A position-sensitive method used when displaying the fluid in world. Water uses this for biome-dependent colors. -- `colorAsStack(FluidStack stack)` - A `FluidStack`-sensitive method, which can be used for e.g. [data component][datacomponent]-sensitive tinting. Unused in vanilla, as `FluidStack` is a NeoForge system. - -In addition, `FluidTintSource` extends `BlockTintSource`, which means that all the `BlockState`-sensitive methods are available as well. - -## In-World Fluids - -When placing fluids in world, `FluidState`s are used instead of `Fluid`s, closely mirroring the use of [`BlockState`s][blockstate] versus `Block`s. Similar to `BlockState`s, `FluidState`s can be set into a level using `Level#setFluidState()`, a `FluidState` at a position can be queried using `Level#getFluidState()`, and the default state can be obtained using `Fluid#defaultFluidState()`. +When placing [fluids][fluid] in world, `FluidState`s are used instead of `Fluid`s, closely mirroring the use of [`BlockState`s][blockstate] versus [`Block`s][block]. Similar to `BlockState`s, a `FluidState` at a position can be queried using `Level#getFluidState()`, and the default state can be obtained using `Fluid#defaultFluidState()`. However, `FluidState`s also exhibit a few differences to `BlockState`s. Most notably, their different states do not operate using properties, at least not properties defined in the same way as block state properties, instead the exact `FluidState` is computed by the level from fluid spreading mechanics. For most use cases the exact `FluidState` is irrelevant, save for some properties such as `isSource()` which can be queried from the `FluidState` if needed. -Unfortunately, the current implementation of `FluidState`s in levels is very much half-baked. Even more unfortunately, it is impossible for NeoForge to fix this without breaking compatibility with vanilla worlds. Basically all `FluidState` logic is tied to `BlockState` in some way, despite there not really being a need to. In the current implementation, `Level#getFluidState()` essentially boils down to `BlockState#getFluidState()`, happening very deep in chunk storage. It is expected that Mojang will eventually rework this, however for now we have to make do with what we have. +Unfortunately, the current implementation of `FluidState`s in levels is very much half-baked. Even more unfortunately, it is impossible for NeoForge to fix this without breaking compatibility with vanilla worlds. Basically all `FluidState` logic is tied to `BlockState` in some way, despite there not really being a need to. This is why, for example, there is no `Level#setFluidState()` method. In the current implementation, `Level#getFluidState()` essentially boils down to `BlockState#getFluidState()`, happening very deep in chunk storage. It is expected that Mojang will eventually rework this, however for now we have to make do with what we have. -### Waterlogging +## Waterlogging _See also [Blocks][block] and [Block States][blockstate]._ -The epitome of this half-baked `FluidState` system is waterlogging. Waterlogging is the ability of certain non-full blocks, e.g. slabs, to also contain a water source at the same time. This is currently implemented via the `WATERLOGGED` block state property: +The epitome of the half-baked `FluidState` system is waterlogging. Waterlogging is the ability of certain non-full blocks, e.g. slabs, to also contain a water source at the same time. This is currently implemented via the `WATERLOGGED` block state property: ```java // Implementing SimpleWaterloggedBlock automatically enables bucket pickup @@ -229,10 +47,10 @@ public class MyBlock extends Block implements SimpleWaterloggedBlock { ``` :::info -"Lavalogging" or similar fluid-logging with other fluids is easily possible. To do so, simply create a new `BooleanProperty`, add it to the block as usual, and have `Block#getFluidState()` return the desired fluid if the property is true. +"Lavalogging" or similar fluid-logging with other fluids is easily possible. To do so, simply create a new `BooleanProperty`, add it to the block as usual, and have `Block#getFluidState()` return the desired fluid if the property is true. ::: -### Fluid Blocks +## Fluid Blocks In order to be able to place our fluid in the world, we need to create a `LiquidBlock` for it: @@ -271,7 +89,7 @@ public static final BaseFlowingFluid.Properties MOLTEN_IRON_PROPERTIES = .block(ModBlocks.MOLTEN_IRON); ``` -Finally, the block needs a model and a renderer. Let's start with the model, which is fairly simple to [generate][modeldatagen]: +Finally, the block needs a model, which is fairly simple to [generate][models]: ```java @Override @@ -280,7 +98,7 @@ protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerat } ``` -### Buckets +## Buckets Fluids can usually be picked up in a bucket. A custom bucket for our fluid can be added like so: @@ -372,7 +190,7 @@ protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerat } ``` -### Cauldrons +## Cauldrons In addition to buckets, it is common for fluids to go in a cauldron. For this, a separate cauldron block is necessary: @@ -450,7 +268,7 @@ private static void registerCauldronFluidContent(RegisterCauldronFluidContentEve } ``` -Finally, since a fluid cauldron is a block like any other, we need some datagen setup. This includes a translation, a block model, a [loot table][loottable] and some [tags]: +Finally, since a fluid cauldron is a block like any other, we need some datagen setup. This includes a [translation][i18n], a [block model][models], a [loot table][loottable] and some [tags]: ```java // In the language provider @@ -556,31 +374,10 @@ private static void registerCauldronInteractions(RegisterCauldronInteractionEven Cauldron interactions are not limited to buckets. Vanilla adds a couple of other cauldron recipes, mostly for "cleaning" colored items. These work through generally the same mechanism. For more information, see the `CauldronInteractions` class. This is also where you can find the vanilla cauldron interaction dispatchers. -## Fluids in Recipes - -TODO - -### `FluidStack` - -TODO - -### `FluidIngredient` - -TODO - -[block]: index.md -[blockstate]: states.md -[datacomponent]: ../items/datacomponents.md -[discord]: https://discord.neoforged.net/ -[entity]: ../entities/index.md -[events]: ../concepts/events.md -[github]: https://github.com/neoforged/NeoForge/issues +[block]: ../blocks/index.md +[blockstate]: ../blocks/states.md +[fluid]: index.md [i18n]: ../resources/client/i18n.md#datagen [loottable]: ../resources/server/loottables/index.md#datagen -[modbus]: ../concepts/events.md#event-buses -[modeldatagen]: ../resources/client/models/datagen.md -[registries]: ../concepts/registries.md -[sides]: ../concepts/sides.md +[models]: ../resources/client/models/datagen.md [tags]: ../resources/server/tags.md#datagen -[textures]: ../resources/client/textures.md -[tinting]: ../resources/client/models/index.md#tinting diff --git a/docs/fluids/recipes.md b/docs/fluids/recipes.md new file mode 100644 index 000000000..0799123f7 --- /dev/null +++ b/docs/fluids/recipes.md @@ -0,0 +1,15 @@ +--- +description: How to work with fluids in recipe contexts. +sidebar_position: 3 +--- +# Fluids in Recipes + +TODO + +### `FluidStack` + +TODO + +### `FluidIngredient` + +TODO diff --git a/docs/inventories/_category_.json b/docs/inventories/_category_.json index f01580186..86b6bd7c6 100644 --- a/docs/inventories/_category_.json +++ b/docs/inventories/_category_.json @@ -1,4 +1,4 @@ { "label": "Inventories & Transfers", - "position": 8 + "position": 9 } \ No newline at end of file diff --git a/docs/misc/_category_.json b/docs/misc/_category_.json index 78da091bd..f3b9c1df4 100644 --- a/docs/misc/_category_.json +++ b/docs/misc/_category_.json @@ -1,4 +1,4 @@ { "label": "Miscellaneous", - "position": 14 + "position": 15 } \ No newline at end of file diff --git a/docs/networking/_category_.json b/docs/networking/_category_.json index 31c81bdb0..af33ba975 100644 --- a/docs/networking/_category_.json +++ b/docs/networking/_category_.json @@ -1,4 +1,4 @@ { "label": "Networking", - "position": 11 + "position": 12 } \ No newline at end of file diff --git a/docs/rendering/_category_.json b/docs/rendering/_category_.json index 6656a69f0..7c36b279a 100644 --- a/docs/rendering/_category_.json +++ b/docs/rendering/_category_.json @@ -1,4 +1,4 @@ { "label": "Rendering", - "position": 12 + "position": 13 } \ No newline at end of file diff --git a/docs/resources/_category_.json b/docs/resources/_category_.json index 5f05cd315..2470a8dd9 100644 --- a/docs/resources/_category_.json +++ b/docs/resources/_category_.json @@ -1,4 +1,4 @@ { "label": "Resources", - "position": 7 + "position": 8 } \ No newline at end of file diff --git a/docs/worldgen/_category_.json b/docs/worldgen/_category_.json index 019b3d56f..63daf1f34 100644 --- a/docs/worldgen/_category_.json +++ b/docs/worldgen/_category_.json @@ -1,4 +1,4 @@ { "label": "Worldgen", - "position": 10 + "position": 11 } \ No newline at end of file From 8bd5fe482a7b8c1cf675f007e200fc457fc0000e Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Mon, 4 May 2026 21:27:14 +0200 Subject: [PATCH 06/12] replace copypastas --- docs/fluids/index.md | 2 +- docs/fluids/inworld.md | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/fluids/index.md b/docs/fluids/index.md index 30597ce3a..03000378d 100644 --- a/docs/fluids/index.md +++ b/docs/fluids/index.md @@ -129,7 +129,7 @@ Finally, the translations. The translation key used by fluids is defined by `Flu ```java @Override protected void addTranslations() { - add(AMFluids.MOLTEN_IRON_TYPE.getDescriptionId(), "Molten Iron"); + add(ModFluids.MOLTEN_IRON_TYPE.getDescriptionId(), "Molten Iron"); } ``` diff --git a/docs/fluids/inworld.md b/docs/fluids/inworld.md index 53983b895..22a0c2f9a 100644 --- a/docs/fluids/inworld.md +++ b/docs/fluids/inworld.md @@ -109,7 +109,7 @@ public static final DeferredItem MOLTEN_IRON_BUCKET = ITEMS.register // The registry name. "molten_iron_bucket", // The bucket item factory. - properties -> new BucketItem(AMFluids.LIQUID_ETHERIUM.get(), properties), + properties -> new BucketItem(ModFluids.MOLTEN_IRON.get(), properties), // The properties supplier. Buckets stack to 1 and return a bucket when used in crafting. () -> new Item.Properties().stacksTo(1).craftRemainder(Items.BUCKET) ); @@ -162,8 +162,8 @@ Finally, all that's left is a translation and a model: // In the language provider @Override protected void addTranslations() { - add(AMFluids.MOLTEN_IRON_TYPE.get().getDescriptionId(), "Molten Iron"); - addItem(AMItems.MOLTEN_IRON_BUCKET, "Molten Iron Bucket"); + add(ModFluids.MOLTEN_IRON_TYPE.get().getDescriptionId(), "Molten Iron"); + addItem(ModItems.MOLTEN_IRON_BUCKET, "Molten Iron Bucket"); } // In the model provider @@ -171,7 +171,7 @@ protected void addTranslations() { protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) { blockModels.createNonTemplateModelBlock(ModBlocks.MOLTEN_IRON.get()); // We use NeoForge's `DynamicFluidContainerModel`. - itemModels.itemModelOutput.accept(AMItems.LIQUID_ETHERIUM_BUCKET.get(), new DynamicFluidContainerModel.Unbaked( + itemModels.itemModelOutput.accept(ModItems.MOLTEN_IRON_BUCKET.get(), new DynamicFluidContainerModel.Unbaked( // The model's textures. new DynamicFluidContainerModel.Textures( Optional.of(new Material(Identifier.withDefaultNamespace("item/bucket"))), @@ -180,7 +180,7 @@ protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerat Optional.empty() ), // The fluid to use. - AMFluids.LIQUID_ETHERIUM.get(), + ModFluids.MOLTEN_IRON.get(), // Whether the bucket model should be flipped, commonly used for "gaseous" fluids. false, // If true, the "cover" texture is a mask. We generally want this for buckets. @@ -274,7 +274,7 @@ Finally, since a fluid cauldron is a block like any other, we need some datagen // In the language provider @Override protected void addTranslations() { - add(AMFluids.MOLTEN_IRON_TYPE.get().getDescriptionId(), "Molten Iron"); + add(ModFluids.MOLTEN_IRON_TYPE.get().getDescriptionId(), "Molten Iron"); addItem(ModItems.MOLTEN_IRON_BUCKET, "Molten Iron Bucket"); addBlock(ModBlocks.MOLTEN_IRON_CAULDRON, "Molten Iron Cauldron"); } @@ -350,11 +350,11 @@ private static void registerCauldronInteractions(RegisterCauldronInteractionEven // For compat with vanilla, we need to add handling for when our cauldron is right-clicked // with water, lava and powder snow buckets. Compat with other mods is handled // by the bucket fill handler method, see below. - LiquidEtheriumCauldronBlock.CAULDRON_INTERACTIONS + MoltenIronCauldronBlock.CAULDRON_INTERACTIONS .put(Items.LAVA_BUCKET, CauldronInteractions::fillLavaInteraction); - LiquidEtheriumCauldronBlock.CAULDRON_INTERACTIONS + MoltenIronCauldronBlock.CAULDRON_INTERACTIONS .put(Items.WATER_BUCKET, CauldronInteractions::fillWaterInteraction); - LiquidEtheriumCauldronBlock.CAULDRON_INTERACTIONS + MoltenIronCauldronBlock.CAULDRON_INTERACTIONS .put(Items.POWDER_SNOW_BUCKET, CauldronInteractions::fillPowderSnowInteraction); // When **any** cauldron is right-clicked with our bucket, replace with our cauldron. From c35f0325c987fca594179d0cc2e8dcf0e7994c59 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Mon, 4 May 2026 21:39:31 +0200 Subject: [PATCH 07/12] address some comments --- docs/fluids/index.md | 11 +++++++---- docs/fluids/inworld.md | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/fluids/index.md b/docs/fluids/index.md index 03000378d..238cae3d3 100644 --- a/docs/fluids/index.md +++ b/docs/fluids/index.md @@ -101,8 +101,8 @@ private static void registerFluidModels(RegisterFluidModelsEvent event) { // The fluid tint source. We leave it at null, which means no tint. See below for more info. null), // Suppliers for the still and flowing fluids. - ModFluids.MOLTEN_IRON::value, - ModFluids.FLOWING_MOLTEN_IRON::value + ModFluids.MOLTEN_IRON, + ModFluids.FLOWING_MOLTEN_IRON ); } ``` @@ -119,14 +119,16 @@ Let's start by adding the texture files. When creating your assets, it is recomm :::warning These paths match the paths we passed into `RegisterFluidModelsEvent#register()` before. You can place the files elsewhere, but you will need to adjust the paths in the renderer as well. + +Be aware that fluid textures generally live in the block atlas, so they should be located in a `textures/block` folder. ::: Most fluids are animated, so they will also need accompanying `.png.mcmeta` files. Again, you can base these off the vanilla files. For more information, see the article on [textures]. - Finally, the translations. The translation key used by fluids is defined by `FluidType#descriptionId()`, and we can get it from a `FluidType` using `#getDescriptionId()`: ```java +// In your LanguageProvider @Override protected void addTranslations() { add(ModFluids.MOLTEN_IRON_TYPE.getDescriptionId(), "Molten Iron"); @@ -150,7 +152,8 @@ public final class MoltenIronTintSource implements FluidTintSource { @Override public int color(FluidState state) { - // Return whatever color you want here. + // Return whatever color you want here. The value is in ARGB; make sure that you include + // a proper alpha value, otherwise the rendering will be invisible. return 0xff000000; } } diff --git a/docs/fluids/inworld.md b/docs/fluids/inworld.md index 22a0c2f9a..549b85011 100644 --- a/docs/fluids/inworld.md +++ b/docs/fluids/inworld.md @@ -261,8 +261,8 @@ private static void registerCauldronFluidContent(RegisterCauldronFluidContentEve ModBlocks.MOLTEN_IRON_CAULDRON.get(), // The fluid. ModFluids.MOLTEN_IRON.get(), - // The amount. 1000 is one bucket. - 1000, + // The amount. + FluidType.BUCKET_VOLUME, // The "level" block state property. Since we don't have one, we pass null. null); } From daf7675e6873aae3442675f04086f11c089a7597 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Wed, 10 Jun 2026 22:00:12 +0200 Subject: [PATCH 08/12] address most of Champ's remaining comments --- docs/fluids/index.md | 9 ++-- docs/fluids/inworld.md | 105 ++++++++++++++++++++++++++++------------- 2 files changed, 77 insertions(+), 37 deletions(-) diff --git a/docs/fluids/index.md b/docs/fluids/index.md index 238cae3d3..d34911385 100644 --- a/docs/fluids/index.md +++ b/docs/fluids/index.md @@ -86,7 +86,7 @@ public static final BaseFlowingFluid.Properties MOLTEN_IRON_PROPERTIES = new BaseFlowingFluid.Properties(MOLTEN_IRON_TYPE, MOLTEN_IRON, FLOWING_MOLTEN_IRON); ``` -With this done, your fluid should now be loaded into the game, and recipes will be able to make use of it. However, rendering will be broken. To fix that, we need to register a renderer in a [client-only][sides] [mod bus][modbus] [event handler][events]: +With this done, your fluid should now be loaded into the game, and recipes will be able to make use of it. However, rendering will be broken. To fix that, we need to register a fluid model in a [client-only][sides] [mod bus][modbus] [event handler][events]: ```java @SubscribeEvent // on the mod event bus only on the physical client @@ -99,6 +99,9 @@ private static void registerFluidModels(RegisterFluidModelsEvent event) { new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_flowing")), new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_overlay")), // The fluid tint source. We leave it at null, which means no tint. See below for more info. + null, + // The fluid renderer. Can be supplied for entirely custom fluid rendering. + // This parameter is optional and will default to null if omitted. null), // Suppliers for the still and flowing fluids. ModFluids.MOLTEN_IRON, @@ -170,8 +173,8 @@ private static void registerFluidModels(RegisterFluidModelsEvent event) { new Material(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "block/molten_iron_overlay")), // Use our tint source instance here. MoltenIronTintSource.INSTANCE), - ModFluids.MOLTEN_IRON::value, - ModFluids.FLOWING_MOLTEN_IRON::value + ModFluids.MOLTEN_IRON, + ModFluids.FLOWING_MOLTEN_IRON ); } ``` diff --git a/docs/fluids/inworld.md b/docs/fluids/inworld.md index 549b85011..748c8f066 100644 --- a/docs/fluids/inworld.md +++ b/docs/fluids/inworld.md @@ -43,11 +43,48 @@ public class MyBlock extends Block implements SimpleWaterloggedBlock { public FluidState getFluidState(BlockState state) { return state.getValue(WATERLOGGED) ? Fluids.WATER.getSource(false) : super.getFluidState(state); } + + // When placing this block in water, place the waterlogged state. + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + return defaultBlockState().setValue( + WATERLOGGED, + context.getLevel().getFluidState(context.getClickedPos()).is(Fluids.WATER) + ); + } + + // When this block receives a block update (because e.g. a neighbor changed), + // a tick should be scheduled. + @Override + protected BlockState updateShape( + BlockState state, + LevelReader level, + ScheduledTickAccess ticks, + BlockPos pos, + Direction directionToNeighbour, + BlockPos neighbourPos, + BlockState neighbourState, + RandomSource random + ) { + if (state.getValue(WATERLOGGED)) { + ticks.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level)); + } + return super.updateShape(state, + level, + ticks, + pos, + directionToNeighbour, + neighbourPos, + neighbourState, + random); + } } ``` :::info "Lavalogging" or similar fluid-logging with other fluids is easily possible. To do so, simply create a new `BooleanProperty`, add it to the block as usual, and have `Block#getFluidState()` return the desired fluid if the property is true. + +However, be aware that waterlogging is hardcoded in some instances, such as world generation or piston moving logic. ::: ## Fluid Blocks @@ -132,28 +169,12 @@ Next, it is recommended (but not required) to add a dispenser behavior for the b @SubscribeEvent // on the mod event bus private static void commonSetup(FMLCommonSetupEvent event) { // `DispenserBlock#registerBehavior` is not thread-safe so we wrap it in a lambda. - // The anonymous class seen here is copied from `DispenseItemBehavior#bootStrap()`. - event.enqueueWork(() -> DispenserBlock.registerBehavior(ModItems.MOLTEN_IRON_BUCKET, new DefaultDispenseItemBehavior() { - private final DefaultDispenseItemBehavior defaultDispenseItemBehavior = new DefaultDispenseItemBehavior(); - - @Override - public ItemStack execute(BlockSource source, ItemStack dispensed) { - DispensibleContainerItem bucket = (DispensibleContainerItem) dispensed.getItem(); - BlockPos target = source.pos().relative(source.state().getValue(DispenserBlock.FACING)); - Level level = source.level(); - if (bucket.emptyContents(null, level, target, null, dispensed)) { - bucket.checkExtraContent(null, level, dispensed, target); - return this.consumeWithRemainder(source, dispensed, new ItemStack(Items.BUCKET)); - } else { - return this.defaultDispenseItemBehavior.dispense(source, dispensed); - } - } - })); + event.enqueueWork(() -> DispenserBlock.registerBehavior(ModItems.MOLTEN_IRON_BUCKET, DispenseFluidContainer.getInstance())); } ``` :::tip -If you have multiple buckets, you can reuse the same `DispenseItemBehavior` instance for all buckets. +If you want custom dispenser behavior for your bucket, you can also create a custom `DispenseItemBehavior`. See the source of `DispenseFluidContainer` for what to implement. ::: Finally, all that's left is a translation and a model: @@ -172,18 +193,22 @@ protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerat blockModels.createNonTemplateModelBlock(ModBlocks.MOLTEN_IRON.get()); // We use NeoForge's `DynamicFluidContainerModel`. itemModels.itemModelOutput.accept(ModItems.MOLTEN_IRON_BUCKET.get(), new DynamicFluidContainerModel.Unbaked( - // The model's textures. + // The model's textures. The model is rendered in the order of base, fluid, cover (lowest to highest). new DynamicFluidContainerModel.Textures( + // The particle texture. Optional.of(new Material(Identifier.withDefaultNamespace("item/bucket"))), + // The base texture. Optional.of(new Material(Identifier.withDefaultNamespace("item/bucket"))), + // The fluid texture, i.e. the part that actually contains the fluid. Optional.of(new Material(Identifier.fromNamespaceAndPath("neoforge", "item/mask/bucket_fluid"))), + // The cover texture. This is rendered last and can be a mask (see booleans below). Optional.empty() ), // The fluid to use. ModFluids.MOLTEN_IRON.get(), // Whether the bucket model should be flipped, commonly used for "gaseous" fluids. false, - // If true, the "cover" texture is a mask. We generally want this for buckets. + // If true, the cover texture is a mask, that is, it "cuts off" all pixels it doesn't cover. true, // If this is true, if the fluid emits light, the fluid element of the model becomes emissive. true)); @@ -205,9 +230,11 @@ public class MoltenIronCauldronBlock extends AbstractCauldronBlock { return CODEC; } - // The cauldron interaction dispatcher. See below for more info. + // The cauldron interaction dispatcher and its id. See below for more info. public static final CauldronInteraction.Dispatcher CAULDRON_INTERACTIONS = new CauldronInteraction.Dispatcher(); + public static final Identifier CAULDRON_INTERACTIONS = + Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "molten_iron_cauldron"); // Pass our `CauldronInteraction.Dispatcher` to super. public MoltenIronCauldronBlock(Properties properties) { @@ -321,7 +348,7 @@ Cauldron interactions happen in two events. First, we need to register the `Caul private static void registerCauldronInteractionDispatchers(RegisterCauldronInteractionEvent.Dispatcher event) { event.register( // A unique identifier. - Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "molten_iron_cauldron"), + MoltenIronCauldronBlock.CAULDRON_INTERACTIONS_ID, // Our `CauldronInteraction.Dispatcher`. MoltenIronCauldronBlock.CAULDRON_INTERACTIONS); } @@ -333,9 +360,13 @@ Secondly, we need to register the actual interactions. That works like so: @SubscribeEvent private static void registerCauldronInteractions(RegisterCauldronInteractionEvent.Interaction event) { // Empty our cauldron when it is right-clicked with an empty bucket. - MoltenIronCauldronBlock.CAULDRON_INTERACTIONS.put(Items.BUCKET, - // Input parameters are the cauldron blockstate, the level, the position, - // the player, the used hand, and the used item stack + event.register( + // The id of our cauldron interactions. + MoltenIronCauldronBlock.CAULDRON_INTERACTIONS_ID, + // The item we're right-clicking with. + Items.BUCKET, + // A callback called when right-clicking. Input parameters are the cauldron blockstate, + // the level, the position, the player, the used hand, and the used item stack. (state, level, pos, player, hand, stack) -> CauldronInteractions.fillBucket( // Pass along the input parameters. state, level, pos, player, hand, stack, @@ -350,18 +381,24 @@ private static void registerCauldronInteractions(RegisterCauldronInteractionEven // For compat with vanilla, we need to add handling for when our cauldron is right-clicked // with water, lava and powder snow buckets. Compat with other mods is handled // by the bucket fill handler method, see below. - MoltenIronCauldronBlock.CAULDRON_INTERACTIONS - .put(Items.LAVA_BUCKET, CauldronInteractions::fillLavaInteraction); - MoltenIronCauldronBlock.CAULDRON_INTERACTIONS - .put(Items.WATER_BUCKET, CauldronInteractions::fillWaterInteraction); - MoltenIronCauldronBlock.CAULDRON_INTERACTIONS - .put(Items.POWDER_SNOW_BUCKET, CauldronInteractions::fillPowderSnowInteraction); + event.register( + MoltenIronCauldronBlock.CAULDRON_INTERACTIONS_ID, + Items.LAVA_BUCKET, + CauldronInteractions::fillLavaInteraction); + event.register( + MoltenIronCauldronBlock.CAULDRON_INTERACTIONS_ID, + Items.WATER_BUCKET, + CauldronInteractions::fillWaterInteraction); + event.register( + MoltenIronCauldronBlock.CAULDRON_INTERACTIONS_ID, + Items.POWDER_SNOW_BUCKET, + CauldronInteractions::fillPowderSnowInteraction); // When **any** cauldron is right-clicked with our bucket, replace with our cauldron. - // To do so, we use `event#registerToAll()` instead of `CauldronInteraction.Dispatcher#put()`. + // To do so, we use `event#registerToAll()` instead of `event#register()`. event.registerToAll(ModItems.MOLTEN_IRON_BUCKET.get(), - // Input parameters are the cauldron blockstate, the level, the position, - // the player, the used hand, and the used item stack + // A callback called when right-clicking. Input parameters are the cauldron blockstate, + // the level, the position, the player, the used hand, and the used item stack. (state, level, pos, player, hand, stack) -> CauldronInteractions.fillBucket( // Pass along the input parameters, except the state. level, pos, player, hand, stack, From 2a89e7a6788872e158ff22ff353ed42b9a098c0d Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Thu, 11 Jun 2026 15:06:57 +0200 Subject: [PATCH 09/12] add fluid stack docs --- docs/fluids/recipes.md | 48 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/docs/fluids/recipes.md b/docs/fluids/recipes.md index 0799123f7..a501b4d37 100644 --- a/docs/fluids/recipes.md +++ b/docs/fluids/recipes.md @@ -4,12 +4,52 @@ sidebar_position: 3 --- # Fluids in Recipes -TODO +In many situations, it is desirable for mods to use [fluids][fluid] in recipes. For this use case, NeoForge provides the `FluidStack` and `FluidIngredient` systems. These systems were designed to closely mirror [`ItemStack`s][itemstack] and [`Ingredient`s][ingredient], respectively, so that if you have worked with them before, most concepts shown on this page should be familiar. -### `FluidStack` +## `FluidStack` -TODO +Like an `ItemStack`, a `FluidStack` consists of three major components: + +- The `Fluid` it represents. + - While both source and flowing fluids can be used, you should generally only use source (non-flowing) fluids, in order to avoid confusing players. +- The amount. +- The [data components][datacomponents] map. + +The way all of them work is generally equivalent to `ItemStack`s. The `Fluid` is the equivalent of what would be the `Item` in the `ItemStack`, and the amount is the equivalent of the count in an `ItemStack`. + +:::warning +Unlike with `ItemStack`s, the amount is **required** to be set in `FluidStack`s. The unit of fluids is millibuckets (mB), one bucket (B) consists of 1000 mB; this value is available as a constant at `FluidType.BUCKET_VOLUME`. +::: -### `FluidIngredient` +Furthermore, similar to `ItemStack`s: + +- `FluidStack`s are created by calling `new FluidStack(fluid, amount)` or `new FluidStack(fluid, amount, dataComponents)`. +- `FluidStack`s are mutable. +- `FluidStack#copy()` and `#copyWithAmount()` are available. +- `FluidStack.EMPTY` should be used where an empty or null value is needed. +- `FluidStackTemplate`s are available and used analogously to [`ItemStackTemplate`s][itemstacktemplate] during [datagen][datagen]. +- `FluidStackTemplate`s have a JSON representation: + +```json5 +{ + // The fluid ID. Required. + "id": "minecraft:water", + // The fluid stack amount. 1000 is one bucket. + "amount": 1000, + // A map of data components. Optional, defaults to an empty map. + "components": { + "minecraft:enchantment_glint_override": true + } +} +``` + +## `FluidIngredient` TODO + +[datacomponents]: ../items/datacomponents.md +[datagen]: ../resources/index.md#data-generation +[fluid]: index.md +[ingredient]: ../resources/server/recipes/ingredients.md +[itemstack]: ../items/index.md#itemstacks +[itemstacktemplate]: ../items/index.md#itemstacktemplates From eb3f92d71b7c8b1c87646bcc37271fdc07dc40f9 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Thu, 18 Jun 2026 00:03:59 +0200 Subject: [PATCH 10/12] fluid ingredients --- docs/fluids/recipes.md | 137 ++++++++++++++++++- docs/resources/server/recipes/ingredients.md | 2 +- 2 files changed, 137 insertions(+), 2 deletions(-) diff --git a/docs/fluids/recipes.md b/docs/fluids/recipes.md index a501b4d37..1ab935680 100644 --- a/docs/fluids/recipes.md +++ b/docs/fluids/recipes.md @@ -45,11 +45,146 @@ Furthermore, similar to `ItemStack`s: ## `FluidIngredient` -TODO +`FluidIngredient`s are to `Fluid`s and `FluidStack`s what `Ingredient`s are to `Item`s and `ItemStack`s. Analogously, they implement `Predicate`, and `#test(FluidStack)` can be called to check whether a particular `FluidStack` matches the `FluidIngredient`. +Like with item `Ingredient`s, to create a simple `FluidIngredient`, call an overload of `FluidIngredient#of()`: + +- `FluidIngredient.of()` returns an empty fluid ingredient. +- `FluidIngredient.of(Fluids.WATER, Fluids.LAVA)` returns a fluid ingredient that accepts water or lava. The parameter is a vararg (`Fluid...`), meaning any amount of `Fluid`s may be supplied. +- `FluidIngredient.of(Stream.of(Fluids.WATER))` works the same as the previous method, except that it accepts a `Stream` instead of a `Fluid...`. +- `FluidIngredient.of(new FluidStack(Fluids.WATER), new FluidStack(Fluids.LAVA))` is the same as the `Fluid...` variant, but with a `FluidStack...` instead. This is provided as a convenience method that extracts the `Fluid` from the `FluidStack`. If you also want to match data components, use `DataComponentFluidIngredient` instead (see below). +- `FluidIngredient.of(BuiltInRegistries.FLUID.getOrThrow(Tags.Fluids.WATER))` returns an ingredient that accepts any fluid from the specified [tag]. + +And again like with item `Ingredient`s, there's a few specialized implementations: + +- `CustomDisplayFluidIngredient.of(FluidIngredient.of(Fluids.WATER), SlotDisplay.Empty.INSTANCE)` returns an ingredient with a custom [`SlotDisplay`][slotdisplay] you provide to determine how the slot gets consumed for rendering on the client. +- `CompoundFluidIngredient.of(FluidIngredient.of(Fluids.WATER))` returns an ingredient with child ingredients, passed in the constructor (vararg parameter). The ingredient matches if any of its children matches. +- `DataComponentFluidIngredient.of(true, new FluidStack(Fluids.WATER))` returns an ingredient that, in addition to the fluid, also matches the data component. The boolean parameter denotes strict matching (true) or partial matching (false). Strict matching means the data components must match exactly, while partial matching means the data components must match, but other data components may also be present. Additional overloads of `#of` exist that allow specifying multiple `Fluid`s, or provide other options. +- `DifferenceFluidIngredient.of(FluidIngredient.of(BuiltInRegistries.FLUID.getOrThrow(Tags.Fluids.WATER)), FluidIngredient.of(BuiltInRegistries.FLUID.getOrThrow(Tags.Fluids.LAVA)))` returns an ingredient that matches everything in the first ingredient that doesn't also match the second ingredient. +- `IntersectionFluidIngredient.of(FluidIngredient.of(BuiltInRegistries.FLUID.getOrThrow(Tags.Fluids.WATER)), FluidIngredient.of(BuiltInRegistries.FLUID.getOrThrow(Tags.Fluids.LAVA)))` returns an ingredient that matches everything that matches both sub-ingredients. + +### Custom Fluid Ingredients + +Modders can add their own fluid ingredient types by subclassing `FluidIngredient`. To mirror [the example on the Ingredients page][customingredients], let's add a fluid ingredient type that only passes if the `FluidStack` is in the given tag and has the provided enchantments: + +```java +public class MinEnchantedFluidIngredient extends FluidIngredient { + private final TagKey tag; + private final Map, Integer> enchantments; + // The codec for serializing the ingredient. + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(inst -> inst.group( + TagKey.codec(Registries.FLUID).fieldOf("tag").forGetter(e -> e.tag), + Codec.unboundedMap(Enchantment.CODEC, Codec.INT) + .optionalFieldOf("enchantments", Map.of()) + .forGetter(e -> e.enchantments) + ).apply(inst, MinEnchantedFluidIngredient::new)); + // Create a stream codec for the ingredient. For our use case, creating one from the regular codec will suffice. + public static final StreamCodec STREAM_CODEC = + ByteBufCodecs.fromCodecWithRegistries(CODEC.codec()); + + // Constructor that initializes the fields. You may also use a #of() pattern or similar instead. + public MinEnchantedFluidIngredient(TagKey tag, Map, Integer> enchantments) { + this.tag = tag; + this.enchantments = enchantments; + } + + // Check if the passed FluidStack matches our requirements. + @Override + public boolean test(FluidStack stack) { + return stack.is(tag) && enchantments.keySet() + .stream() + .allMatch(ench -> stack.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY) + .getLevel(ench) >= enchantments.get(ench)); + } + + // Determines whether this fluid ingredient performs data component matching (false) or not (true). + // Also determines whether a stream codec is used for syncing, more on this later. + // We query enchantments on the stack, therefore our ingredient is not simple. + @Override + public boolean isSimple() { + return false; + } + + // Returns a stream of fluids that match this ingredient. Mostly for display purposes. + // See the Ingredient docs for things to consider here, they apply 1:1. + @Override + protected Stream> generateFluids() { + return BuiltInRegistries.FLUID.getOrThrow(tag).stream(); + } + + // FluidIngredient requires implementations of hashCode() and equals(). + @Override + public int hashCode() { + return Objects.hash(tag, enchantments); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof MinEnchantedFluidIngredient that)) return false; + return Objects.equals(this.tag, that.tag) + && Objects.equals(this.enchantments, that.enchantments); + } +} +``` + +We then register a `FluidIngredientType` like so: + +```java +public static final DeferredRegister> FLUID_INGREDIENT_TYPES = + DeferredRegister.create(NeoForgeRegistries.Keys.FLUID_INGREDIENT_TYPE, ExampleMod.MOD_ID); + +public static final Supplier> MIN_ENCHANTED = + FLUID_INGREDIENT_TYPES.register("min_enchanted", + // The stream codec parameter is optional, a stream codec will be created from the codec + // using ByteBufCodecs#fromCodec or #fromCodecWithRegistries if the stream codec isn't specified. + () -> new FluidIngredientType<>( + MinEnchantedFluidIngredient.CODEC, + MinEnchantedFluidIngredient.STREAM_CODEC)); +``` + +Finally, we also need to override `#getType` in our ingredient class: + +```java +public class MinEnchantedFluidIngredient extends FluidIngredient { + // other stuff here + + @Override + public FluidIngredientType getType() { + return MIN_ENCHANTED.get(); + } +} +``` + +### JSON Representation + +Like everything else about `FluidIngredient`s, their representation in JSON also mirrors item `Ingredient`s. To use our own ingredient as an example: + +```json5 +{ + "neoforge:fluid_ingredient_type": "examplemod:min_enchanted", + "tag": "c:water", + "enchantments": { + "minecraft:sharpness": 4 + } +} +``` + +The "regular" fluid ingredients mirror vanilla as well by not specifying a `type` at all and instead being serialized as a string: + +``` +"minecraft:water" // fluid +"#c:water" // fluid tag +``` + +These "regular" fluid ingredients are represented in code as `SimpleFluidIngredient`s. + +[customingredients]: ../resources/server/recipes/ingredients.md#custom-ingredient-types [datacomponents]: ../items/datacomponents.md [datagen]: ../resources/index.md#data-generation [fluid]: index.md [ingredient]: ../resources/server/recipes/ingredients.md [itemstack]: ../items/index.md#itemstacks [itemstacktemplate]: ../items/index.md#itemstacktemplates +[tag]: ../resources/server/tags.md +[slotdisplay]: ../resources/server/recipes/custom.md#slot-displays diff --git a/docs/resources/server/recipes/ingredients.md b/docs/resources/server/recipes/ingredients.md index e3f4dc802..360345c01 100644 --- a/docs/resources/server/recipes/ingredients.md +++ b/docs/resources/server/recipes/ingredients.md @@ -157,6 +157,6 @@ An example for a vanilla tag ingredient: [itemstack]: ../../../items/index.md#itemstacks [recipes]: index.md [registry]: ../../../concepts/registries.md -[slotdisplay]: index.md#slot-displays +[slotdisplay]: custom.md#slot-displays [streamcodec]: ../../../networking/streamcodecs.md [tag]: ../tags.md From 9d7f04291204e8eb2d19c9f215be5229960649f4 Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Thu, 18 Jun 2026 00:56:35 +0200 Subject: [PATCH 11/12] address ChampionAsh's comments --- docs/fluids/index.md | 6 ++++++ docs/fluids/inworld.md | 22 ++++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/fluids/index.md b/docs/fluids/index.md index d34911385..fd761ca68 100644 --- a/docs/fluids/index.md +++ b/docs/fluids/index.md @@ -20,6 +20,12 @@ Before we can register a fluid, we must first understand a few design decisions In Minecraft, water and lava each have two variants: a flowing fluid and a source fluid. The way this works is mostly due to hardcoding, in some association with `FluidState`s (see below). Since this hardcoding is inconvenient at best and practically impossible to use at worst, NeoForge introduces the `FluidType` class and patches a ton of places to use it. The main purpose of the `FluidType` is to contain the common logic of the fluid - e.g. the sounds it makes, whether boats can be used in it, etc. - and only leave the actual flowing logic in the fluid itself. `FluidType`s live in a separate registry added by NeoForge, and thus must be registered in addition to `Fluid`s. +:::info +`FluidType` is merely a utility system. While strongly recommended as it makes things a lot easier, it is not strictly necessary to use `FluidType` for creating fluids. For vanilla reference, see `WaterFluid` and `LavaFluid`. + +The rest of this documentation will focus on `FluidType`-backed fluids only. +::: + With that in mind, let's start creating our fluid! For the sake of example, we're going to create a molten iron fluid. To get started, we need two [registries][registries]: ```java diff --git a/docs/fluids/inworld.md b/docs/fluids/inworld.md index 748c8f066..dc44c0922 100644 --- a/docs/fluids/inworld.md +++ b/docs/fluids/inworld.md @@ -201,20 +201,38 @@ protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerat Optional.of(new Material(Identifier.withDefaultNamespace("item/bucket"))), // The fluid texture, i.e. the part that actually contains the fluid. Optional.of(new Material(Identifier.fromNamespaceAndPath("neoforge", "item/mask/bucket_fluid"))), - // The cover texture. This is rendered last and can be a mask (see booleans below). + // The cover texture. This is rendered last and can be a mask (see below). Optional.empty() ), // The fluid to use. ModFluids.MOLTEN_IRON.get(), // Whether the bucket model should be flipped, commonly used for "gaseous" fluids. false, - // If true, the cover texture is a mask, that is, it "cuts off" all pixels it doesn't cover. + // If true, the cover texture is a mask. If false, the cover texture is rendered normally. + // See below for more info. true, // If this is true, if the fluid emits light, the fluid element of the model becomes emissive. true)); } ``` +### Bucket Mask Textures + +If the `coverIsMask` boolean is true, the cover texture is instead treated as a mask texture. Mask textures are textures containing either a full white (`0xfffffff`) or transparent black (`0x00000000` or just `0`) pixels, acting as a stencil of sorts. Their function is best exemplified by having a look at them: + +TODO + +Only the white pixels in the mask will be included in rendering, and pixels overlapping with the transparent part of the mask will be discarded. + +:::tip +The mask textures seen above are shipped by Neo, at the following respective locations: + +- `assets/neoforge/textures/item/mask/bucket_fluid.png` +- `assets/neoforge/textures/item/mask/bucket_fluid_drip.png` +- `assets/neoforge/textures/item/mask/bucket_fluid_cover.png` +- `assets/neoforge/textures/item/mask/bucket_fluid_cover_drip.png` +::: + ## Cauldrons In addition to buckets, it is common for fluids to go in a cauldron. For this, a separate cauldron block is necessary: From faccfb03ea71153f8bae93413f7bec7273066c4c Mon Sep 17 00:00:00 2001 From: IchHabeHunger54 Date: Sun, 28 Jun 2026 22:38:53 +0200 Subject: [PATCH 12/12] fix two copypastas --- docs/fluids/inworld.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/fluids/inworld.md b/docs/fluids/inworld.md index dc44c0922..501eb9620 100644 --- a/docs/fluids/inworld.md +++ b/docs/fluids/inworld.md @@ -251,7 +251,7 @@ public class MoltenIronCauldronBlock extends AbstractCauldronBlock { // The cauldron interaction dispatcher and its id. See below for more info. public static final CauldronInteraction.Dispatcher CAULDRON_INTERACTIONS = new CauldronInteraction.Dispatcher(); - public static final Identifier CAULDRON_INTERACTIONS = + public static final Identifier CAULDRON_INTERACTIONS_ID = Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "molten_iron_cauldron"); // Pass our `CauldronInteraction.Dispatcher` to super. @@ -417,7 +417,7 @@ private static void registerCauldronInteractions(RegisterCauldronInteractionEven event.registerToAll(ModItems.MOLTEN_IRON_BUCKET.get(), // A callback called when right-clicking. Input parameters are the cauldron blockstate, // the level, the position, the player, the used hand, and the used item stack. - (state, level, pos, player, hand, stack) -> CauldronInteractions.fillBucket( + (state, level, pos, player, hand, stack) -> CauldronInteractions.emptyBucket( // Pass along the input parameters, except the state. level, pos, player, hand, stack, // The resulting block state.