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..fd761ca68 --- /dev/null +++ b/docs/fluids/index.md @@ -0,0 +1,212 @@ +--- +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. + +:::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 +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 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 +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, + // 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, + ModFluids.FLOWING_MOLTEN_IRON + ); +} +``` + +## 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. + +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"); +} +``` + +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. The value is in ARGB; make sure that you include + // a proper alpha value, otherwise the rendering will be invisible. + 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, + ModFluids.FLOWING_MOLTEN_IRON + ); +} +``` + +:::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/fluids/inworld.md b/docs/fluids/inworld.md new file mode 100644 index 000000000..501eb9620 --- /dev/null +++ b/docs/fluids/inworld.md @@ -0,0 +1,438 @@ +--- +description: How to add and work with fluids in-world. +sidebar_position: 2 +--- +# In-World Fluids + +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. 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 + +_See also [Blocks][block] and [Block States][blockstate]._ + +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 +// 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); + } + + // 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 + +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, like before, we 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, which is fairly simple to [generate][models]: + +```java +@Override +protected void registerModels(BlockModelGenerators blockModels, ItemModelGenerators itemModels) { + blockModels.createNonTemplateModelBlock(ModBlocks.MOLTEN_IRON.get()); +} +``` + +## 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(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) +); +``` + +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. + event.enqueueWork(() -> DispenserBlock.registerBehavior(ModItems.MOLTEN_IRON_BUCKET, DispenseFluidContainer.getInstance())); +} +``` + +:::tip +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: + +```java +// In the language provider +@Override +protected void addTranslations() { + add(ModFluids.MOLTEN_IRON_TYPE.get().getDescriptionId(), "Molten Iron"); + addItem(ModItems.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(ModItems.MOLTEN_IRON_BUCKET.get(), new DynamicFluidContainerModel.Unbaked( + // 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 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. 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: + +```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 and its id. See below for more info. + public static final CauldronInteraction.Dispatcher CAULDRON_INTERACTIONS = + new CauldronInteraction.Dispatcher(); + public static final Identifier CAULDRON_INTERACTIONS_ID = + Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "molten_iron_cauldron"); + + // 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. + FluidType.BUCKET_VOLUME, + // 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][i18n], a [block model][models], a [loot table][loottable] and some [tags]: + +```java +// In the language provider +@Override +protected void addTranslations() { + 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"); +} + +// 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. + MoltenIronCauldronBlock.CAULDRON_INTERACTIONS_ID, + // 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. + 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, + // 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. + 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 `event#register()`. + 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.emptyBucket( + // 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. + +[block]: ../blocks/index.md +[blockstate]: ../blocks/states.md +[fluid]: index.md +[i18n]: ../resources/client/i18n.md#datagen +[loottable]: ../resources/server/loottables/index.md#datagen +[models]: ../resources/client/models/datagen.md +[tags]: ../resources/server/tags.md#datagen diff --git a/docs/fluids/recipes.md b/docs/fluids/recipes.md new file mode 100644 index 000000000..1ab935680 --- /dev/null +++ b/docs/fluids/recipes.md @@ -0,0 +1,190 @@ +--- +description: How to work with fluids in recipe contexts. +sidebar_position: 3 +--- +# Fluids in Recipes + +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` + +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`. +::: + +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` + +`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/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/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 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