# Setting up Balm ## What is Balm? Balm allows you to write code once and build your mod for [NeoForge](https://neoforged.net/){rel=""nofollow""}, [Fabric](https://fabricmc.net/){rel=""nofollow""} and [Forge](https://files.minecraftforge.net/net/minecraftforge/forge/){rel=""nofollow""} at the same time. For this, Balm provides abstractions to all common functionality such as registries and events. Additionally, Balm comes with utilities to integrate popular mods like [JEI](https://www.curseforge.com/minecraft/mc-mods/jei){rel=""nofollow""}, [Jade](https://www.curseforge.com/minecraft/mc-mods/jade){rel=""nofollow""} or [Curios](https://www.curseforge.com/minecraft/mc-mods/curios){rel=""nofollow""} all on a single common codebase. ## Prerequisites In order to use Balm, you should have - an understanding of Java and Minecraft Modding - an IDE like [IntelliJ IDEA](https://www.jetbrains.com/idea/){rel=""nofollow""} - the ability to clone a [Git](https://www.git-scm.com/){rel=""nofollow""} repository ## Creating a Project Use this generator to download a blank mod with everything already set up. :balm-project-generator You can extract the generated zip file, open the project in your IDE, and jump straight to the [next page](https://balm.twelveiterations.com/getting-started/mod). ::div{.text-sm} Alternatively, you can use the [template repository](https://github.com/TwelveIterations/balm-mod){rel=""nofollow""} on GitHub. However, you will have to follow the steps in the README and manually update the template with your own names and properties. :: --- ## Adding Balm to an Existing Project If you already have a project you're trying to port to Balm, the steps depend on whether you are currently on a multi-loader Gradle setup or not. The following guides give a rough overview on how to migrate your setup to one that supports Balm ### a) Using the mod loader's official MDK / Project Template The templates offered by mod loaders are designed for single-loader projects. To support multiple loaders at once, you will have to upgrade your Gradle setup to one that supports multi-loader projects. You can reference the existing template in the [balm-mod](https://github.com/TwelveIterations/balm-mod){rel=""nofollow""} repository. It essentially comes down to replacing most of your `build.gradle` files, adding the `build-logic` folder from the template, and moving your source code from `src/main/java` to `common/src/main/java`. You will still have a folder for each loader you target, but they will generally only contain your mod's entrypoint (e.g. the Fabric `ModInitializer` or NeoForge `@Mod` class). Once you've got a working Gradle setup, you have to add Balm as a library (see below), and finally you have to initialize your mod with Balm in each loader-specific entrypoint. Learn how your loader-specific entrypoints should look like in the [entrypoint](https://balm.twelveiterations.com/advanced/entrypoints) section. ### b) Using another multi-loader template Add Balm as a library like you would any other dependency: ```groovy [common/build.gradle] repositories { maven { url = 'https://maven.twelveiterations.com/repository/maven-public/' content { includeGroup 'net.blay09.mods' } } } dependencies { implementation("net.blay09.mods:balm-common:${balm_version}") { changing = balm_version.endsWith('SNAPSHOT') } } ``` ```groovy [fabric/build.gradle] repositories { maven { url = 'https://maven.twelveiterations.com/repository/maven-public/' content { includeGroup 'net.blay09.mods' } } } dependencies { modImplementation("net.blay09.mods:balm-fabric:${balm_version}") { changing = balm_version.contains('SNAPSHOT') } } ``` ```groovy [neoforge/build.gradle] repositories { maven { url = 'https://maven.twelveiterations.com/repository/maven-public/' content { includeGroup 'net.blay09.mods' } } } dependencies { implementation("net.blay09.mods:balm-neoforge:${balm_version}") { changing = balm_version.contains('SNAPSHOT') } } ``` ```groovy [forge/build.gradle] repositories { maven { url = 'https://maven.twelveiterations.com/repository/maven-public/' content { includeGroup 'net.blay09.mods' } } } dependencies { implementation("net.blay09.mods:balm-forge:${balm_version}") { changing = balm_version.contains('SNAPSHOT') } } ``` You will have to initialize your mod with Balm in each loader-specific entrypoint. Learn how your loader-specific entrypoints should look like in the [entrypoint](https://balm.twelveiterations.com/advanced/entrypoints) section. ## Using Balm for only some or one loader(s) That works too — just follow the setup guides and remove the loaders you don't want from the `include`s in your `settings.gradle` file. Everything else stays the same. ## I'm lost - what should I do? If you can't find the answer you're looking for in these pages, try looking at [Blay's various visible source mods](https://github.com/TwelveIterations){rel=""nofollow""}, or ask a question in the [Balm Developers Discord](https://discord.gg/36qHFMNgAh){rel=""nofollow""}. # Initializing your Mod ::note This page assumes you have a working multi-loader Gradle setup with Balm installed. See the [previous page](https://balm.twelveiterations.com/getting-started) if you don't. :: ## Initializing your Mod Your mod loader entrypoints will reference your mod's main class. This class usually just has a static `initialize(BalmRegistrars registrars)` method that you can use to register all content in your mod and define any event handlers you need. ```java [common/src/main/java/Waystones.java] public class Waystones { public static final String MOD_ID = "waystones"; public static void initialize(BalmRegistrars registrars) { // Register your content using the methods on the registrars instance // e.g. registrars.blocks(ModBlocks::initialize); // Register event handlers for any of Balm's common callbacks ServerPlayerCallback.Join.EVENT.register(player -> { System.out.println("Player " + player + " has just joined the game!"); }); } } ``` # Registering Content Content, such as Blocks and Items, is registered using helpers you can access through the `BalmRegistrars` instance that is passed to your [initializer](https://balm.twelveiterations.com/getting-started/mod). Balm provides various convenient helper methods for registering standard content, as well as the ability to register into any other registry using the generic `registrar(...)` method. ```java [common/src/main/java/Waystones.java] public class Waystones { public static DeferredBlock waystoneBlock; public static void initialize(BalmRegistrars registrars) { registrars.blocks(registrar -> { waystoneBlock = blocks.register("waystone", WaystoneBlock::new, it -> it.sound(SoundType.STONE).strength(5f, 2000f)).withItem(WaystoneBlockItem::new).asDeferredBlock(); }); } } ``` ### Structuring your Content As your mod grows, it often makes sense to split content registrations into their own classes depending on their type. ```java [common/src/main/java/ModBlocks.java] public class ModBlocks { public static void initialize(BalmBlockRegistrar blocks) { blocks.register(registrar -> { waystoneBlock = blocks.register("waystone", WaystoneBlock::new, it -> it.sound(SoundType.STONE) .strength(5f, 2000f) ).withItem(WaystoneBlockItem::new).asDeferredBlock(); }); } } ``` ```java [common/src/main/java/Waystones.java] public class Waystones { public static void initialize(BalmRegistrars registrars) { registrars.blocks(ModBlocks::initialize); } } ``` ## Caveat: Deferred Registrations In a multi-loader context, registrations are not necessarily instant across all loaders. While Fabric registers content the moment you make the call, NeoForge and Forge defer registrations until a later time. That's why anything you register with Balm will generally either provide you with a `Holder`, or something like `DeferredBlock` or `DeferredItem` rather than the `Block` or `Item` directly. Many Minecraft methods accept `Holder`s (or Balm's `DeferredBlock` and `DeferredItem` classes) just fine, but if you need to, you can access the underlying values using `Holder.value()` or e.g. `DeferredBlock.asBlock()`. `DeferredBlock` also comes with a useful `defaultBlockState()` method (since that is what you most commonly need a `Block` field for), and both `DeferredBlock` and `DeferredItem` have a `createStack(int count)` method to quickly get a new `ItemStack` of them. ::note Be careful not to access the underlying values too early, i.e. before the game has finished loading, or your mod may crash on NeoForge and Forge. :: ::note Avoid equality checks such as `itemStack.getItem() == ModItems.warpStone`, as they will always be false when using `DeferredItem`. Use `itemStack.is(ModItems.warpStone)` instead, or consider using an Item Tag where it makes sense. :: ## Using other Registries The most common registries are covered by specialized `Balm...Registrar` classes which not only provide useful helpers for registering things in a neat and streamlined manner, but may also take care of additional steps in the background that are needed to make your content function. Therefore, if Balm provides a specialized registrar for the registry you need, you should use it to avoid issues. For registries that are not covered by Balm, you can use the custom registrar method instead: ```java [common/src/java/main/ModSounds.java] public class ModSounds { public static Holder fridgeOpen; public static void initialize(BalmRegistrar.Scoped sounds) { fridgeOpen = sounds.register("fridge_open", SoundEvent::createVariableRangeEvent); } } ``` ```java [common/src/java/main/CookingForBlockheads.java] public class CookingForBlockheads { public static void initialize(BalmRegistrars registrars) { registrars.registrar(Registries.SOUND_EVENT, ModSounds::initialize); } } ``` # Reacting to Events Balm makes a set of callbacks for common events available to your mod, allowing you to hook into things such as block interactions, input handling, rendering, etc. Internally, these events map to existing mod loader events where possible, which means your mod will remain compatible with other mods the same way a mod-loader native mod would be. To register an event listener, find the callback you need (e.g. on the [Event Matrix](https://secret-ambert-359.notion.site/2ac242228e55805b89f7dbba3905b8a9){rel=""nofollow""}) and call `.register()` on the event you'd like to listen to. ```java ServerPlayerCallback.Join.EVENT.register(player -> { System.out.println("Player " + player + " has just joined the game!"); }); ``` Some callbacks have return values — you can check the interface you're implementing to see what the return value means; the method names are generally descriptive. ```java LivingEntityCallback.Damage.Before.EVENT.register((entity, damageSource, damageAmount) -> { if (entity instanceof Wolf) { return 0f; // Wolves are invincible! } return damageAmount; }); ``` Balm's goal is to provide a common callback for all events that are universally useful and supported by all the loaders. You can check the [Event Matrix](https://secret-ambert-359.notion.site/2ac242228e55805b89f7dbba3905b8a9){rel=""nofollow""} for the current status and how mod loader events map to Balm callbacks. # Using Mod Configs Balm provides an easy-to-use configuration system that hooks into native mod loader's configs where available and supports different Config GUI mods. To use Balm's configuration system, you need to: 1. Create your configuration class with fields for your categories and options 2. Register it with Balm in your mod's initializer ### Defining Configurations The annotation-based approach uses annotations on regular Java fields to define configuration properties. ```java @Config(value = "yourmodid", type = "common") public class MyAnnotationConfig { @Comment("Enables the special feature") public boolean enableFeature = true; @Comment("Maximum number of items") public int maxItems = 100; @Comment("Message shown to players") public String welcomeMessage = "Welcome to my mod!"; @Comment("Advanced configuration options") public AdvancedCategory advanced = new AdvancedCategory(); public static class AdvancedCategory { @Comment("Multiplier for movement speed") @Synced // This will sync to clients public float speedMultiplier = 1.0f; @NestedType(String.class) // Important to specify on Lists and Sets @Comment("List of allowed items") public List allowedItems = Arrays.asList("minecraft:diamond", "minecraft:gold_ingot"); } } ``` ### Registering Configurations Register your configuration class in your mod's initialization code: ```java // in your static initializer method: Balm.config().registerConfig(MyAnnotationConfig.class); // or as part of a BalmModule: @Override public void registerConfig(BalmConfig config) { config.registerConfig(MyAnnotationConfig.class); } ``` ### Accessing properties You can retrieve the current configuration with all fields set to their configured values. This config may differ from the local config, as it includes values that were synced from the server. ```java // Get the active config object MyAnnotationConfig config = Balm.config().getActiveConfig(MyAnnotationConfig.class); // Access properties boolean featureEnabled = config.enableFeature; int max = config.maxItems; float speed = config.advanced.speedMultiplier; ``` ### Updating properties To update a property, you should use the `updateLocalConfig()` method and update the field values of the given config object. ```java Balm.config().updateLocalConfig(MyAnnotationConfig.class, config -> { config.maxItems = 200; config.advanced.speedMultiplier = 1.5f; }); ``` ## Network Synchronization Properties marked as `@Synced` will be synced from server to clients when they join. These changes are temporary and only apply to the active config, not the local config. ## Loader-specific Differences On Forge and NeoForge, local configs are managed by the mod loader and Balm acts as an access and network synchronization layer. On Fabric, Balm manages local configs itself using a subset of the TOML format. The resulting TOML files are identical in both format and naming. However, Fabric uses the `StringRepresentable` of an enum for values while Neo/Forge use the enum's name (ignore-case). ::warning Supported Config Types Note that only the config types `common`, `client` and `server` are supported across all three mod loaders. While the Fabric runtime supports custom types as well, using them is not recommended unless you are building a mod exclusively for Fabric (they will crash on Neo/Forge). :: ::warning Configs may not be available in your initializer On Forge and NeoForge, configs are not loaded instantly. That means you will not have access to your common or client config within your mod initializer. You can use `Balm.config().onConfigAvailable` to defer code until your config has been loaded. On NeoForge and Fabric, you can use a config of type `startup` which will be loaded immediately, but this is not supported on Forge. :: ## Supported Config Screens Balm automatically integrates with Mod Menu on Fabric to provide support for the following configuration screen mods: - Cloth Config - Configured Since configs are managed through the default config system in NeoForge and Forge, any configuration screen mod that supports Neo/Forge's config system should work out of the box. ## Supported Property Types Balm supports the following property types: - `boolean` - `int` - `long` - `float` - `double` - `String` - `Identifier` (maps to `namespace:path` strings) - `any enum implementing StringRepresentable` (maps to enum's `getSerializedName()`) - `List` - `Set` If you need property types beyond these, or lists of complex objects, there's a good chance your use case would be better solved through a data pack or custom JSON files. # Data Generation Data Generation is an extremely useful tool that allows you to generate the correct JSON files needed for your blocks, items, tags, etc. It is recommended to setup Data Generation even for small mods - you will save yourself and your users a lot of time by ensuring all your JSON data is always up-to-date for the Minecraft version you're targeting. ## Using Data Generation The Balm Project Generator and [balm-mod](https://github.com/TwelveIterations/balm-mod){rel=""nofollow""} template come with a data generation setup already included. We use Fabric for data generation, as Fabric is usually the first to be available for snapshots. Generated files are saved into the `common` project, making them available on all loaders without needing data generator code for each loader. You can find the data generation classes under `fabric/src/main/java/.../datagen`. The template comes with data generators for recipes, loot tables, models and tags, which is the most common ones you'll need; but you can of course add additional classes by just adding them in the `ModDataGenerator` class. Run the `:fabric:runData` Gradle task to (re-)generate your resource and data files. ## Verifying Data Generation with GitHub Actions The [balm-mod](https://github.com/TwelveIterations/balm-mod){rel=""nofollow""} template also includes a `verify-resources` job in its GitHub Actions workflows, which will error if you forgot to run `:fabric:runData` or are missing language keys that were detected in use. If you used the Balm Project Generator, feel free to [copy those workflows](https://github.com/TwelveIterations/balm-mod/tree/26.2/.github/workflows){rel=""nofollow""} from the template repository. # Adding Dependencies If you want to interact with code from other mods, you usually need to add these projects to your development environment. You do this in Balm the same way you would in any other single-loader project, using the Gradle dependency system. Once dependencies are declared and you've refreshed Gradle in your IDE, you can access third party mod classes in the same way you access Minecraft code. You can even launch (and debug) the game with those mods installed from your IDE. ## Adding other mods to your development environment To add a mod dependency to your environment, you need to know the **Repository** and the **Artifact Descriptor** that it is deployed under. For example, Balm is located on the `https://maven.twelveiterations.com/repository/maven-public/` repository, and its artifact is located under e.g. `net.blay09.mods:balm-common:26.2.0.1`. ```groovy repositories { maven { url = 'https://example.com/repository/maven-public/' content { includeGroup 'com.example' } } } dependencies { compileOnly 'com.example:thirdpartymod:26.2.0.1' } ``` Third party repositories are not always easy to find, so the easiest way to quickly get a mod into your development environment is using [CurseMaven](https://www.cursemaven.com/){rel=""nofollow""}, which instead uses CurseForge project and file IDs to identify an artifact. CurseForge also provides a "Curse Maven Snippet" on the File Details page that you can use directly. ```groovy dependencies { compileOnly "curse.maven:waystones-245755:7844805" localRuntime "curse.maven:waystones-245755:7844805" } ``` ::warning CurseMaven works fine for many mods, but you may run into issues on more complex mod setups. If you run into errors about missing classes or missing mods on a CurseMaven dependency, try locating the mods' official Maven repository instead and define your dependency on that. :: The project templates come with a set of common repositories already set up, including CurseMaven - you can find them in `repositories.gradle`. Dependencies themselves should be defined in the `dependencies.gradle` file in `common` and the loader subfolders. ### Dependency Types The most common options for dependencies are `compileOnly`, `implementation` and `localRuntime`. - Start with `compileOnly`, which grants you access to another mod's classes in your code. - This is useful for when you have optional compatibility with a mod, such as integration code that only gets loaded when both mods are installed. - With this alone, the third party mod will **not** automatically be loaded when you launch the game from your IDE. - Only use `implementation`if your mod explicitly depends on the third party mod to run. - The third party mod will be loaded when you launch the game from your IDE, and declared as a transitive Maven dependency of your own mod. - For example, this is what's used to add Balm to your project. That way, any third party mods adding *your* mod as a dependency will also automatically pull in a Gradle dependency on Balm. - Use `localRuntime`when you want an optional mod to be loaded when you launch the game from your IDE. - Either used for mods that you just like to have in your development environment, or ones that you have soft dependencies for and are actively testing with. - The third party mod will be loaded when you launch the game from your IDE, but unlike `implementation` it will not be marked as a transitive dependency for your mod. ::note On Fabric versions before 26.1, you must prefix these with `mod*`, .e.g `modImplementation`. :: ## Dependencies in Common Code When adding support for other multiloader mods, it's usually best to do so in your `common` project so you don't have to duplicate your integration code. In order to do so, you may have to find a `mojmap` or `common` artifact of said mod. Those artifacts would generally only be found on the mods' real Maven repositories (i.e. not on CurseMaven). If all else fails, you can try depending on the NeoForge version of the mod in your common project. This **may or may not** work, depending on your Minecraft version. Starting with Minecraft 26.1 and the removal of obfuscation, this should hopefully be a lot easier now. ## Declaring Hard Dependencies Adding a Gradle dependency only pulls the mod into your development environment - it does not define the mod as a dependency on CurseForge, nor will it be considered a dependency when loading the game. If your mod actually requires the third party mod to be present, be sure to also add it as a dependency in your mod metadata files (e.g. `fabric.mod.json`), and when uploading files to CurseForge. See [Releasing your Mod](https://balm.twelveiterations.com/getting-started/release) for detailed information. ## Utilizing Soft Dependencies safely In most cases, you want other mods to be supported as soft dependencies only, i.e. your mod should still be able to run and function even if the third party mod is not installed. In order to ensure that, you must only load those mods' classes if the mod is actually present. Balm offers a helper for this: ```java Balm.initializeIfLoaded("othermodid", "com.example.yourmod.compat.OtherModIntegration"); ``` When the other mod is loaded, this method will construct an instance of your `OtherModIntegration` class. Make sure it has a public no-arg constructor! In that class, you can import packages and access classes from the third party mod safely, as long as you don't have any other references to `OtherModIntegration` anywhere in your code. # Releasing your Mod Congratulations on your new mod! Before releasing it into the wild, you should make sure some things are set up correctly so players won't run into issues. ### Mod Metadata When releasing your mod, make sure that your mod metadata files declare a dependency on Balm. If you used the Balm Project Generator or the [balm-mod](https://github.com/TwelveIterations/balm-mod){rel=""nofollow""} template repository, this is already the case. ```json [fabric/src/main/resources/fabric.mod.json] "depends": { "balm": ">=${balm_version}" } ``` ```toml [neoforge/src/main/resources/META-INF/neoforge.mods.toml] [[dependencies.yourmodid]] modId="balm" type="required" versionRange="[${balm_version},)" ordering="NONE" side="BOTH" ``` ```toml [forge/src/main/resources/META-INF/mods.toml] [[dependencies.yourmodid]] modId="balm" mandatory=true versionRange="[${balm_version},)" ordering="NONE" side="BOTH" ``` If you're adding these lines, make sure your Gradle setup correctly replaces the `${balm_version}` placeholder as well. ### File Dependencies You should also declare Balm as a `Required Dependency` on your CurseForge / Modrinth upload. That way players installing your mod with a Launcher will have Balm installed automatically alongside your mod. ## Mod Hosting Platforms Balm is available on [CurseForge](https://www.curseforge.com/minecraft/mc-mods/balm){rel=""nofollow""} and [Modrinth](https://modrinth.com/mod/balm){rel=""nofollow""}. Third Party Downloads are enabled, so Balm can be easily installed even outside of the official CurseForge / Modrinth Apps. ::note For Minecraft versions 1.21.1 and below, Fabric versions of Balm were on a [separate CurseForge page](https://www.curseforge.com/minecraft/mc-mods/balm-fabric){rel=""nofollow""}. If you are releasing for those versions, make sure to select the correct project for the file you are uploading. :: # Blocks ## Registering Blocks Blocks can be registered using a `BalmBlockRegistrar`. ```java public class ModBlocks { public static DeferredBlock yourBlock; public static void initialize(BalmBlockRegistrar blocks) { yourBlock = blocks.register("your_block", YourBlock::new, it -> it.sound(SoundType.WOOD).strength(2.5f)) .withDefaultItem() .asDeferredBlock(); } } ``` You can obtain a `BalmBlockRegistrar` through the `BalmRegistrars` instance passed to your mod initializer, or by registering your mod as a `BalmModule`. #### Using an Initializer ```java public class YourMod { public static void initialize(BalmRegistrars registrars) { registrars.blocks(ModBlocks::initialize); } } ``` #### Using `BalmModule` ```java public class YourMod implements BalmModule { @Override public void registerBlocks(BalmBlockRegistrar blocks) { ModBlocks.initialize(blocks); } } ``` ## Custom Block Items When using `withDefaultItem`, Balm will automatically register an item for your block based on the `BlockItem` class. You can specify your own class and item properties by using `withItem` instead. ```java public class ModBlocks { public static DeferredBlock yourBlock; public static void initialize(BalmBlockRegistrar blocks) { yourBlock = blocks.register("your_block", YourBlock::new, it -> it.sound(SoundType.WOOD).strength(2.5f)) .withItem(YourBlockItem::new) .asDeferredBlock(); } } ``` ## Discriminated Blocks Balm also provides a way to register blocks that are discriminated by a property, such as an enum like `DyeColor`. This is useful for registering multiple variants of a block that share the same properties. ```java public class ModBlocks { public static DiscriminatedBlocks ovens; public static void initialize(BalmBlockRegistrar blocks) { ovens = blocks.registerDiscriminated(DyeColor.values(), it -> DiscriminatedBlocks.prefix(it, "oven"), OvenBlock::new, it -> it.sound(SoundType.METAL).strength(5f, 10f)) .withDefaultItems() .asDiscriminatedBlocks(); } } ``` You can then easily look up the blocks using the discriminator value: ```java Block redOven = ovens.get(DyeColor.RED); ``` ## Deferred Blocks Registration of blocks is not always instant, as it may be delayed by the mod loader - that's why you can't get a proper Block instance right away. While blocks are technically safe to instantiate early and register later (and you could do that by passing a Supplier to an instance you constructed yourself), Balm chooses to instantiate the block only at the time of registration as that seems to be the most future-proof way that avoids caveats of incomplete registry states. Most of the time you don't actually need to store or access a block instance directly. Instead, Balm provides a `DeferredBlock` interface with: - `BlockLike`, a Balm interface that gives you access to `defaultBlockState()` - `createStack(int count)`, a utility for creating an item stack from the block - `asResourceKey()`, which returns the block's registry key - `asBlockItemId()`, which returns the block-item reference This should cover most uses of what would otherwise be a `Block` instance. `BlockLike` also provides `asBlock()` for APIs that require the resolved block. ::note Do not try to store `Block` instances in your ModBlocks class by using `asDeferredBlock().asBlock()` in your registration code, as that is too early to access the resolved block on NeoForge and Forge. Store a `DeferredBlock` instance or `Holder` instead and only resolve it once you truly need a `Block` instance. :: # Block Entities ## Registering Block Entities Block entities can be registered using a `BalmBlockEntityTypeRegistrar`. ```java public class ModBlockEntities { public static Holder> yourBlockEntity; public static void initialize(BalmBlockEntityTypeRegistrar blockEntities) { yourBlockEntity = blockEntities.register("your_block_entity", YourBlockEntity::new, ModBlocks.yourBlock).asHolder(); } } ``` You can obtain a `BalmBlockEntityTypeRegistrar` through the `BalmRegistrars` instance passed to your mod initializer, or by registering your mod as a `BalmModule`. #### Using an Initializer ```java public class YourMod { public static void initialize(BalmRegistrars registrars) { registrars.blockEntityTypes(ModBlockEntities::initialize); } } ``` #### Using `BalmModule` ```java public class YourMod implements BalmModule { @Override public void registerBlockEntityTypes(BalmBlockEntityTypeRegistrar blockEntities) { ModBlockEntities.initialize(blockEntities); } } ``` ## Using the Block Entity We're storing `Holder>` instead of `BlockEntityType` to account for delayed registration on some mod loaders. You can resolve the block entity type using `ModBlockEntities.yourBlockEntity.value()`. ::note Do not try to store `BlockEntityType` instances in your ModBlockEntities class by using `asHolder().value()` in your registration code, as that is too early to access the resolved block entity type on NeoForge and Forge. Store a `Holder>` instance instead and only resolve it once you truly need a `BlockEntityType` instance. :: # Items ## Registering Items Items can be registered using a `BalmItemRegistrar`. ```java public class ModItems { public static DeferredItem yourItem; public static void initialize(BalmItemRegistrar items) { yourItem = items.register("your_item", YourItem::new).asDeferredItem(); } } ``` You can obtain a `BalmItemRegistrar` through the `BalmRegistrars` instance passed to your mod initializer, or by registering your mod as a `BalmModule`. #### Using an Initializer ```java public class YourMod { public static void initialize(BalmRegistrars registrars) { registrars.items(ModItems::initialize); } } ``` #### Using `BalmModule` ```java public class YourMod implements BalmModule { @Override public void registerItems(BalmItemRegistrar items) { ModItems.initialize(items); } } ``` ## Block Items Block items can also be registered directly alongside the block using `withDefaultItem` or `withItem`. See [Blocks](https://balm.twelveiterations.com/blocks) for more information. ## Deferred Items Registration of items is not always instant, as it may be delayed by the mod loader - that's why you can't get a proper Item instance right away. While items are technically safe to instantiate early and register later (and you could do that by passing a Supplier to an instance you constructed yourself), Balm chooses to instantiate the item only at the time of registration as that seems to be the most future-proof way that avoids caveats of incomplete registry states (for example in regards to Data Components, that you would otherwise also need to ensure to be present and ready). Most of the time you don't actually need to store or access an item instance directly. Instead, Balm provides a `DeferredItem` interface with: - `ItemLike`, which can be used in calls where an `ItemLike` is expected - `createStack(int count)`, a utility for creating an item stack from the item - `asHolder()`, which returns the registered item as a `Holder` - `asResourceKey()`, which returns the item's registry key ::tip If you were using patterns like `itemStack.is(ModItems.yourItem)`, consider whether an item tag makes sense for your use case. Direct item comparisons often hardcode your mod to only your specific item and limit compatibility. :: This should cover most uses of what would otherwise be a `Item` instance. If you do need an Item instance after all (some data generators expect it), you can use `asItem()` in those calls to resolve the item. ::note Do not try to store `Item` instances in your ModItems class by using `asDeferredItem().asItem()` or `asHolder().value()` in your registration code, as that is too early to access the resolved item on NeoForge and Forge. Store a `DeferredItem` instance or `Holder` instead and only resolve it once you truly need an `Item` instance. :: # Data Components ## Registering Data Components Data components can be registered using a `BalmDataComponentTypeRegistrar`. ```java public class ModDataComponents { public static Holder> yourDataComponent; public static void initialize(BalmDataComponentTypeRegistrar dataComponentTypes) { yourDataComponent = dataComponentTypes.register("your_data_component", Codec.unit(Unit.INSTANCE)).asHolder(); } } ``` You can obtain a `BalmDataComponentTypeRegistrar` through the `BalmRegistrars` instance passed to your mod initializer, or by registering your mod as a `BalmModule`. #### Using an Initializer ```java public class YourMod { public static void initialize(BalmRegistrars registrars) { registrars.dataComponentTypes(ModDataComponents::initialize); } } ``` #### Using `BalmModule` ```java public class YourMod implements BalmModule { @Override public void registerDataComponents(BalmDataComponentTypeRegistrar dataComponentTypes) { ModDataComponents.initialize(dataComponentTypes); } } ``` # Creative Mode Tabs ## Registering Creative Mode Tabs Creative mode tabs can be registered using a `BalmCreativeModeTabRegistrar`. ```java public class ModCreativeModeTabs { public static void initialize(BalmCreativeModeTabRegistrar creativeModeTabs) { creativeModeTabs.register("your_creative_mode_tab", builder -> builder.title(Component.translatable("itemGroup.your_mod.your_creative_mode_tab")) .icon(() -> ModItems.yourItem.createStack()) .displayItems((parameters, output) -> { output.accept(ModItems.yourItem); output.accept(ModBlocks.yourBlock); }) ); } } ``` You can obtain a `BalmCreativeModeTabRegistrar` through the `BalmRegistrars` instance passed to your mod initializer, or by registering your mod as a `BalmModule`. #### Using an Initializer ```java public class YourMod { public static void initialize(BalmRegistrars registrars) { registrars.creativeModeTabs(ModCreativeModeTabs::initialize); } } ``` #### Using `BalmModule` ```java public class YourMod implements BalmModule { @Override public void registerCreativeModeTabs(BalmCreativeModeTabRegistrar creativeModeTabs) { ModCreativeModeTabs.initialize(creativeModeTabs); } } ``` # Menu Types ## Registering Menu Types Menu types can be registered using a `BalmMenuTypeRegistrar`. ```java public class ModMenuTypes { public static Holder> yourMenuType; public static void initialize(BalmMenuTypeRegistrar menuTypes) { yourMenuType = menuTypes.register("your_menu_type", new BalmMenuFactory() { @Override public YourMenu create(int syncId, Inventory inventory, YourMenu.Data data) { return new YourMenu(syncId, inventory, data); } @Override public StreamCodec getStreamCodec() { return YourMenu.Data.STREAM_CODEC.cast(); } }).asHolder(); } } ``` You can obtain a `BalmMenuTypeRegistrar` through the `BalmRegistrars` instance passed to your mod initializer, or by registering your mod as a `BalmModule`. #### Using an Initializer ```java public class YourMod { public static void initialize(BalmRegistrars registrars) { registrars.menuTypes(ModMenuTypes::initialize); } } ``` #### Using `BalmModule` ```java public class YourMod implements BalmModule { @Override public void registerMenuTypes(BalmMenuTypeRegistrar menuTypes) { ModMenuTypes.initialize(menuTypes); } } ``` ## Using the Menu Type You can then use the menu type in your menu class: ```java public class YourMenu extends Menu { public YourMenu(int syncId, Inventory inventory, Data data) { super(ModMenuTypes.yourMenuType.value(), syncId, inventory, data); } } ``` ::note Do not try to store `MenuType` instances in your ModMenuTypes class by using `asHolder().value()` in your registration code, as that is too early to access the resolved menu type on NeoForge and Forge. Store a `Holder` instead and only resolve it once you truly need a `MenuType` instance. :: # Menu Screens ## Registering Menu Screens Menu screens can be registered using a `BalmMenuScreenRegistrar`. ```java public class ModMenuScreens { public static void initialize(BalmMenuScreenRegistrar menuScreens) { menuScreens.register(ModMenuTypes.yourMenuType, YourMenuScreen::new); } } ``` You can obtain a `BalmMenuScreenRegistrar` through the `BalmClientRegistrars` instance passed to your client initializer, or by registering your mod as a `BalmClientModule`. #### Using an Initializer ```java public class YourModClient { public static void initialize(BalmClientRegistrars registrars) { registrars.menuScreens(ModMenuScreens::initialize); } } ``` #### Using `BalmClientModule` ```java public class YourModClient implements BalmClientModule { @Override public void registerMenuScreens(BalmMenuScreenRegistrar menuScreens) { ModMenuScreens.initialize(menuScreens); } } ``` # Entities ## Registering Entities Entities can be registered using a `BalmEntityTypeRegistrar`. ```java public class ModEntities { public static Holder> yourEntity; public static void initialize(BalmEntityTypeRegistrar entityTypes) { yourEntity = entityTypes.register("your_entity", () -> EntityType.Builder.of(YourEntity::new, MobCategory.MISC)) .withDefaultAttributes(it -> it.add(Attributes.ATTACK_DAMAGE, 2f)) .asHolder(); } } ``` You can obtain a `BalmEntityTypeRegistrar` through the `BalmRegistrars` instance passed to your mod initializer, or by registering your mod as a `BalmModule`. #### Using an Initializer ```java public class YourMod { public static void initialize(BalmRegistrars registrars) { registrars.entityTypes(ModEntities::initialize); } } ``` #### Using `BalmModule` ```java public class YourMod implements BalmModule { @Override public void registerEntityTypes(BalmEntityTypeRegistrar entityTypes) { ModEntities.initialize(entityTypes); } } ``` # Recipe Types ## Registering Recipe Types Recipe types and their related classes can be registered using a `BalmRecipeTypeRegistrar`. ```java public class ModRecipeTypes { public static DeferredRecipeType yourRecipes; public static void initialize(BalmRecipeTypeRegistrar recipeTypes) { yourRecipes = recipeTypes.register("your_recipes", YourRecipe.class) .withSerializer(YourRecipe.Serializer::new) .withRecipeBookCategory() .asDeferredRecipeType(); } } ``` You can obtain a `BalmRecipeTypeRegistrar` through the `BalmRegistrars` instance passed to your mod initializer, or by registering your mod as a `BalmModule`. #### Using an Initializer ```java public class YourMod { public static void initialize(BalmRegistrars registrars) { registrars.recipeTypes(ModRecipeTypes::initialize); } } ``` #### Using `BalmModule` ```java public class YourMod implements BalmModule { @Override public void registerRecipeTypes(BalmRecipeTypeRegistrar recipeTypes) { ModRecipeTypes.initialize(recipeTypes); } } ``` ## Using the Recipe Type We're storing `DeferredRecipeType` instead of `RecipeType` to account for delayed registration on some mod loaders. Conveniently, this object will also hold references to the serializer and recipe book category if specified. In your recipe class, you can then use your type as follows: ```java @Override public RecipeSerializer getSerializer() { return ModRecipeTypes.yourRecipes.serializer(); } @Override public RecipeType getType() { return ModRecipeTypes.yourRecipes.type(); } @Override public RecipeBookCategory recipeBookCategory() { return ModRecipeTypes.yourRecipes.category(); } ``` DeferredRecipeType also provides convenience functions for looking up recipes of its type: ```java Optional foundRecipe = ModRecipeTypes.yourRecipes.getRecipeFor(level, new SingleRecipeInput(itemStack)) ``` Note that this will always return an `Optional.empty()` on clients, as recipes are server-side only as of recent Minecraft versions. ::note Do not try to store `RecipeType` instances in your ModRecipeTypes class by using `asDeferredRecipeType().type()` or `asHolder().value()` in your registration code, as that is too early to access the resolved recipe type on NeoForge and Forge. Store a `DeferredRecipeType` instance or `Holder` instead and only resolve it once you truly need a `RecipeType` instance. :: # Commands ## Registering Commands Commands can be registered using `Balm.commands().register(...)`, which gives you access to Minecraft's Brigadier `CommandDispatcher`. ```java public class ModCommands { public static void initialize(BalmCommands commands) { commands.register(ModCommands::register); } private static void register(CommandDispatcher dispatcher) { dispatcher.register(Commands.literal("yourmod") .then(Commands.literal("ping") .executes(context -> { context.getSource().sendSuccess(() -> Component.literal("Pong"), false); return 1; })) ); } } ``` For more information on how to use Brigadier, your best option is to look at Vanilla commands similar to yours and see how they are registered. # Quick Move Stack Balm provides a utility for declaratively defining quick move behaviour in container menus. `QuickMove` lets you: - Define named slot ranges (e.g., `container`, `player`, `inventory`, `hotbar`). - Specify routing rules for how items move between these ranges. - Avoid having to implement your own error-prone `AbstractContainerMenu#quickMoveStack` implementation ## Usage This example defines a single slot named "input" with index 1, and three water bucket slots with indices [2, 3, 4]. It then defines two routes: - one that makes shift-clicking items in the inventory or hotbar move into the input slot if they're obsidian - and one that makes shift-clicking items in the inventory or hotbar move into the water bucket slots if they're water buckets QuickMove also provides a default route for moving items from the container slots to the player slots. ```java public class MyMenu extends AbstractContainerMenu { private final QuickMove.Routing quickMove; public MyMenu(int id, Inventory playerInv) { super(ModMenus.MY_MENU.get(), id); // ... add container slots and player slots ... this.quickMove = QuickMove.create(this, this::moveItemStackTo) .slot("input", 1) .slotRange("waterBuckets", 2, 5) .route(it -> it.is(Items.OBSIDIAN), QuickMove.PLAYER, "input") .route(it -> it.is(Items.WATER_BUCKET), QuickMove.PLAYER, "water") .build(); } @Override public ItemStack quickMoveStack(Player player, int index) { return quickMove.transfer(this, player, index); } } ``` The `create(AbstractContainerMenu, MoveItemStackTo)` variant assumes the last 36 slots belong to the player (27 inventory + 9 hotbar). If your layout differs, use `create(MoveItemStackTo)` and define all slot ranges manually. # Capabilities ## Overview Balm exposes Capabilities (Forge/NeoForge) and Block/Entity APIs (Fabric) through the `BalmCapabilities` service. ```java BalmCapabilities capabilities = Balm.capabilities(); ``` ## Default Capabilities By default, Balm registers capabilities for Minecraft's `Container` interface as well as Balm's `FluidTank` and `EnergyStorage` interfaces. These capabilities are automatically mapped to the mod-loader native capabilities: - The `Container` capability is exposed as `IItemHandler` on Forge and NeoForge, and as `ItemStorage` on Fabric - The `FluidTank` capability is exposed as `IFluidHandler` on Forge and NeoForge, and as `FluidStorage` on Fabric - The `EnergyStorage` capability is exposed as `IEnergyStorage` on Forge and NeoForge, and as a [Reborn Energy](https://github.com/TechReborn/Energy){rel=""nofollow""} (third party mod) `EnergyStorage` on Fabric ::info This mapping is unidirectional. That means if you expose a `Container` capability, it will be available to mods supporting `IItemHandler` or `ItemStorage`, but querying the `Container` capability will not provide you with other mod's `IItemHandler`s or `ItemStorage`s. It is recommended to use a [Mod Proxy](https://balm.twelveiterations.com/mod-proxy) or [Platform Proxy](https://balm.twelveiterations.com/platform-proxy) if you need to access a mod-loader specific capability. :: ### Exposing Default Capabilities The easiest way to provide these default capabilities is by implementing a respective provider interface on your block entity. - `Container` → `BalmContainerProvider` - `FluidTank` → `BalmFluidTankProvider` - `EnergyStorage` → `BalmEnergyStorageProvider` Block entities implementing these interfaces will automatically have them exposed as a capability. ```java public class YourStorageBlockEntity extends BlockEntity implements BalmContainerProvider { // ... @Override public Container getContainer() { return container; } // ... } ``` ## Retrieving an Existing Capability Type Most capability-related methods operate on a `CapabilityType`. If you are creating a capability yourself, you can use `registerType` (more on that below), otherwise you can use `getType` to lookup an already existing capability. ```java CapabilityType SOME_CAPABILITY = capabilities.getType(id("some_capability"), Block.class, YourCapability.class, Direction.class) ``` ::info Capabilities for Block Entities are registered under the `Block` scope. There is no separate `BlockEntity` scope. :: ::info Entity capabilities are all on the `Entity` scope; there is no separate scopes for `LivingEntity` or other subclasses. :: ::warning Caveat: On Forge, there is some additional code required before you can access a capability. See "Registering a Capability on Forge" below. :: ## Querying a Capability You can use `getCapability` to query for a capability on a block entity using a `CapabilityType` obtained through either `getType` or `registerType`. ```java EnergyStorage energyStorage = capabilities.getCapability(blockEntity, direction, CommonCapabilities.ENERGY_STORAGE); ``` ## Registering a Capability Provider Capability providers are responsible for exposing capabilities from your block entities. They are registered to specific block entity types. ::tip If you implement `BalmContainerProvider`, `BalmFluidTankProvider` or `BalmEnergyStorageProvider` in your block entity, you do not need to worry about exposing their respective capabilities manually. :: ```java capabilities.registerProvider(id("your_capability_provider"), SOME_CAPABILITY, (blockEntity, context) -> blockEntity instanceof YourBlockEntity yourBlockEntity ? yourBlockEntity.getSomeCapabilityImplementation() : null, () -> List.of( ModBlockEntities.yourBlockEntity.get() )); ``` ## Registering a Fallback Capability Provider You can also register a fallback capability provider. These providers will apply to **all** block entities. For example, Balm uses a fallback provider to automatically expose capabilities from block entities that implement the default interfaces (such as `BalmContainerProvider`). ```java capabilities.registerFallbackBlockEntityProvider(id("your_fallback_capability_provider"), SOME_CAPABILITY, (blockEntity, context) -> blockEntity instanceof YourBlockEntity yourBlockEntity ? yourBlockEntity.getSomeCapabilityImplementation() : null); ``` ## Registering a Capability If you're creating a capability for yourself or other mods to use, you need to register it. ```java title="With a nullable Direction as context" CapabilityType YOUR_CAPABILITY = capabilities.registerType(id("your_capability"), Block.class, YourCapability.class, Direction.class); ``` ```java title="Without a context" CapabilityType YOUR_CAPABILITY = capabilities.registerType(id("your_capability"), Block.class, YourCapability.class, Void.class); ``` ::warning Caveat: On Forge 26.1+, registration is only possible with a Forge-specific annotation. See below for more. :: ::warning Caveat: On Forge before 26.1, there is some additional code required to register a capability. See below for more. :: ## Caveat: Capabilities on Forge Starting in 26.1, registration on Forge is only possible with a Forge-specific annotation, and therefore no longer trivially achievable in common code. `registerType` will make a type known to Balm, but will no longer register it with Forge itself. Good luck (you're on your own for this)! Before 26.1, due to the way Forge's capabilities work, it is necessary to create a `CapabilityToken` and pre-register capabilities to Balm before calling `BalmCapabilities.registerType` or `BalmCapabilities.getType`. This step cannot be automated by Balm because Forge uses a transformer on `CapabilityToken` that needs the generic parameter to be statically defined at compile-time. ```java title="In your Forge entrypoint, before registering capabilities" final var forgeCapabilities = (ForgeBalmCapabilities) Balm.capabilities(); forgeCapabilities.preRegisterType(id("your_capability"), CapabilityManager.get(new CapabilityToken() { })); ``` ::tip If you use `BalmModule`, consider registering an additional module in your Forge entrypoint that runs before your common module. If you use a static intializer, simply make your `preRegisterType` calls before you call `initializeMod`. :: ## Caveat: Context for Fluid Tanks on older versions For backwards compatibility, Balm's default capability for fluid tanks (`balm:fluid_tank`) has a context of `Void.class`. Starting in Minecraft 1.21.5, the context has been changed to `Direction`. # Data Attachments ## Overview Data attachments let you store typed custom data directly on game objects, avoiding messy caches or mixins. They are useful for persisted pieces of state that belong to a player, entity, block entity, level or chunk. Balm exposes a common API over Fabric's and NeoForge's implementations. Data Attachments are not supported on Forge, as Forge does not provide an equivalent API. For attaching data to items, you should use [Data Components](https://balm.twelveiterations.com/content/data-components) instead. ## Defining the data Create an immutable class or record for your attachment. It will need a `Codec` if you want to have it saved. ```java [common/src/main/java/MyPlayerData.java] public record MyPlayerData(boolean example) { public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( Codec.BOOL.fieldOf("example").forGetter(MyPlayerData::example) ).apply(instance, MyPlayerData::new)); public static MyPlayerData createDefault() { return new MyPlayerData(false); } } ``` ::warning Attachment values should be immutable. Mutating an attached value in place may bypass change detection and cause its synchronized state to become outdated. Create a new value and pass it to `update()` whenever the data changes. :: ## Registering attachments Register attachment types through `BalmRegistrars.dataAttachmentTypes()`. The registration returns a `DataAttachmentLookup` that you keep around and use for reads and writes. ```java [common/src/main/java/ModDataAttachments.java] public class ModDataAttachments { public final DataAttachmentLookup playerData; public ModDataAttachments(BalmDataAttachmentTypeRegistrar registrar) { playerData = registrar .register("player_data", MyPlayerData.CODEC, MyPlayerData::createDefault, true) .asLookup(); } } ``` Then initialize it from your mod's common initialization code. ```java [common/src/main/java/MyMod.java] public class MyMod { public static final String MOD_ID = "mymod"; private static @Nullable ModDataAttachments dataAttachments; public static void initialize(BalmRegistrars registrars) { registrars.dataAttachmentTypes(registrar -> dataAttachments = new ModDataAttachments(registrar)); } public static ModDataAttachments dataAttachments() { return Objects.requireNonNull(dataAttachments); } } ``` ## Accessing attachments `DataAttachmentLookup` supports players, entities, block entities, levels and chunks. ```java DataAttachmentLookup lookup = MyMod.dataAttachments().playerData; MyPlayerData data = lookup.getOrCreate(player); if (!data.example()) { lookup.update(player, new MyPlayerData(true)); } if (lookup.has(player)) { MyPlayerData existing = lookup.get(player); } lookup.update(player, MyPlayerData.createDefault()); lookup.remove(player); ``` The same methods are available for other supported targets. ```java lookup.getOrCreate(entity); lookup.getOrCreate(blockEntity); lookup.getOrCreate(level); lookup.getOrCreate(chunk); ``` ## Builder options If you need more control when registering your attachment, use the builder overload. ```java playerData = registrar.register("player_data", builder -> builder .persistent(MyPlayerData.CODEC) .initializer(MyPlayerData::createDefault) .copyOnDeath() ).asLookup(); ``` The builder supports: - `persistent(codec)` to save and load the attachment value. - `initializer(supplier)` to create a default value for `getOrCreate()`. - `copyOnDeath()` to copy player data when the player respawns after death. - `networkSynchronized(streamCodec)` to synchronize attachment data to clients. - `networkSynchronized(streamCodec, predicate)` to synchronize only to players that match the predicate. # Mod Proxies ## Overview Mod proxies are a way of abstracting mod integrations away through a common interface and conditionally use different implementations based on which other mods are loaded in the environment. This enables seamless integration with third party mods without introducing hard dependencies or loader-specific code. Mod proxies provide: - Common Interfaces: Load different mod-specific implementations under a mod-independent interface - Fallbacks: Use a default implementation when no mod providing an implementation is present - Multiplexing: Define how to handle multiple supported mods being present (by default: uses the first defined) ## Usage ### Creating a Mod Proxy The most common use case is to create a specific implementation when a mod is present, with a fallback for when it's not: ```java MyInterface implementation = Balm.modProxy() .with("othermodid", "com.example.mod.compat.OtherModImplementation") .withFallback(new DefaultImplementation()) .build(); ``` This code will: 1. Check if `"othermodid"` is loaded 2. If loaded, instantiate the `OtherModImplementation` class 3. If not loaded, use the fallback implementation (`DefaultImplementation`) you provide ### Multiple Implementations You can chain multiple mod integrations: ```java MyInterface implementation = Balm.modProxy() .with("modid-a", "com.example.mod.compat.ModAImplementation") .with("modid-b", "com.example.mod.compat.ModBImplementation") .withFallback(new DefaultImplementation()) .build(); ``` By default, the first loaded mod's implementation (in order of `.with()` calls) will be used. If neither mod is loaded, the fallback is used. ### Multiplexing In some cases, you might want to specially handle multiple implementations being present instead of just using the first one: ```java MyInterface implementation = Balm.modProxy() .with("modid-a", "com.example.mod.ModAImplementation") .with("modid-b", "com.example.mod.ModBImplementation") .withMultiplexer(implementations -> new CombinedImplementation(implementations)) .withFallback(new DefaultImplementation()) .build(); ``` The multiplexer function receives a list of all loaded implementations and should return a single combined implementation. ### Lazy Building You can delay initialization of the mod proxy until it is accessed by using `buildLazily()`. This is recommended if you define a proxy outside of the typical mod lifecycle, such as in a static field initializer. Mod Proxies need to access the mod list when they are built, and there's no guarantee it's available by the time your class is loaded. ### Real World Examples Balm itself uses mod proxies for its Trinkets/Curios integration, as well as its Permissions support on Fabric. ```java BalmPermissions permissions = Balm.modProxy() .with("fabric-permissions-api-v0", "net.blay09.mods.balm.fabric.compat.FabricPermissionsAPIIntegration") .withFallback(new CommonBalmPermissions()) .build(); ``` This code creates a permissions system that integrates with fabric-permissions-api (third-party-mod) when available, but falls back to a standard implementation when the API isn't present. ### Requirements for Proxy Classes Implementation classes loaded through the mod proxy system must: - Implement the interface you're proxying - Have a public no-argument constructor ### Comparison with Other Proxy Types Balm also provides other proxy types: - [Platform Proxies](https://balm.twelveiterations.com/platform-proxy): For platform-specific code (e.g. NeoForge vs Fabric) - [Sided Proxies](https://balm.twelveiterations.com/sided-proxy): For client vs dedicated server code (only rarely needed nowadays) # Permission System ## Overview Balm's permission system provides a platform-agnostic way to handle permissions across the different mod loaders. It integrates with each platform's native permission systems when available, and falls back to a common implementation otherwise. ## Core Concepts ### Permission Identifiers Permissions are identified using `Identifier`s in the format `modid:permission.path`. #### Example: ```java Identifier PERMISSION_YOURCOMMAND = Identifier.fromNamespaceAndPath("yourmod", "command.yourcommand"); ``` ### Permission Context Permissions are resolved in a specific context, which can be: - A player context - A command source context (player, entity or command block) - An offline player (UUID only) ::warning Note that only player contexts are fully supported on all platforms. Most permission systems do not support non-player permissions and will instead fall back to the default resolver you specify when registering your permissions. Additionally, Balm does not support offline player permissions yet. :: ## Usage ### Registering Permissions ```java // For commands, there is a helper method to register a permission which will use the permission system for players and fall back to the regular Vanilla permission level check for non-players like command blocks. BalmCommands.registerPermission( Identifier.fromNamespaceAndPath("mymod", "command.feature"), Permissions.COMMANDS_GAMEMASTER ); // You can also register custom permissions. For example, the above helper method uses this under the hood: Balm.permissions().registerPermission( permission, (context) -> context.getCommandSource().map(it -> it.hasPermission(permissionLevel)).orElse(false) ); ``` ### Checking Permissions ```java // Check if a player has a permission boolean hasPermission = Balm.permissions().hasPermission(player, permission); // Check if a command source has a permission boolean hasPermission = Balm.permissions().hasPermission(commandSource, permission); ``` #### Requiring Permissions on Commands The `BalmCommands` interface provides helper methods for command permission checks: ```java // Require a single permission Commands.literal("mycommand") .requires(BalmCommands.requirePermission(PERMISSION_ID)) .executes(context -> { /* command logic */ }); // Require any of multiple permissions Commands.literal("mycommand") .requires(BalmCommands.requireAnyPermission(PERMISSION_A, PERMISSION_B)) .executes(context -> { /* command logic */ }); // Require all permissions Commands.literal("mycommand") .requires(BalmCommands.requireAllPermissions(PERMISSION_A, PERMISSION_B)) .executes(context -> { /* command logic */ }); ``` ### Platform Integrations Balm automatically integrates with: - [`fabric-permissions-api`](https://modrinth.com/mod/fabric-permissions-api){rel=""nofollow""} (third party mod) when it's present on Fabric - `PermissionAPI` on NeoForge and Forge - Fallback to your defined default resolvers / vanilla permission levels when no permission handler is installed # Declarative Configs ## Overview Balm offers two ways to define configurations: 1. Annotation-based Configuration - Using annotated Java classes 2. Declarative Configuration - Using a builder pattern This page is focused on **Declarative Configs**. Check out [Using Mod Configs](https://balm.twelveiterations.com/getting-started/config) for information on how to register configs using the annotation-based approach. Personally, I recommend using annotation-based configs because they are more readable. Annotation-based configs use the declarative system in the background, so both systems are managed the same in the background. Declarative Configs may be a good alternative for you if you are porting from Neo/Forge and don't want to fully rewrite your configs for Balm. ## Getting Started To use Balm's declarative configuration system, you need to: 1. Create your configuration schema 2. Register it with Balm in your mod's initializer or `registerConfig()` method ### Creating a configuration schema The declarative approach uses a builder pattern to define configuration properties. ```java public class MyDeclarativeConfig { // Create a schema with your mod's namespace and config type public static final ConfigSchemaBuilder schema = BalmConfigSchema.create( Identifier.fromNamespaceAndPath("yourmodid", "common") ); // Define a boolean property public static final ConfiguredBoolean enableFeature = schema .property("enableFeature") .comment("Enables the special feature") .boolOf(true); // default value // Define an integer property public static final ConfiguredInt maxItems = schema .property("maxItems") .comment("Maximum number of items") .intOf(100); // default value // Define a string property public static final ConfiguredString welcomeMessage = schema .property("welcomeMessage") .comment("Message shown to players") .stringOf("Welcome to my mod!"); // Create a category for related settings public static final MyCategory advancedSettings = schema .category("advanced") .comment("Advanced configuration options") .via(MyCategory::new); // Define a nested category public static class MyCategory extends BalmConfigCategoryInitializer { public final ConfiguredFloat speedMultiplier = category .property("speedMultiplier") .comment("Multiplier for movement speed") .synced() // This will sync to clients .floatOf(1.0f); public MyCategory(ConfigCategoryBuilder category) { super(category); } } } ``` ### Registering a configuration schema Register your configuration in your mod's initialization code: ```java // in your initializer: Balm.config().registerConfig(MyDeclarativeConfig.schema); // or as part of a BalmModule: @Override public void registerConfig(BalmConfig config) { config.registerConfig(MyDeclarativeConfig.schema); } ``` ### Accessing properties When accessing properties, Balm will automatically use the current active config. This config may differ from the local config, as it includes values that were synced from the server. ```java // Access a boolean value boolean featureEnabled = MyDeclarativeConfig.enableFeature.get(); // Access an integer value int max = MyDeclarativeConfig.maxItems.get(); // Access a category property float speed = MyDeclarativeConfig.advancedSettings.speedMultiplier.get(); ``` ### Updating properties To update a property, you can use the `set()` method - but you must remember to call `saveLocalConfig()` afterwards to apply the changes. ```java // Update a boolean value MyDeclarativeConfig.enableFeature.set(true); // Update an integer value MyDeclarativeConfig.maxItems.set(200); // Update a category property MyDeclarativeConfig.advancedSettings.speedMultiplier.set(1.5f); // Save the updated config Balm.config().saveLocalConfig(MyDeclarativeConfig.schema); ``` ## Network Synchronization of Properties Properties marked as `.synced()` will be synced from server to clients when they join. These changes are temporary and only apply to the active config, not the local config. ## More on Configs You can learn more about loader-specific differences, supported config screens and supported property types on the [annotation-based config guide](https://balm.twelveiterations.com/getting-started/config#loader-specific-differences). # Mod Entrypoints ## Overview In order for your mod to be loaded, you must create a mod-loader specific entrypoint inside each corresponding mod-loader subproject, and register your mod with Balm inside. ::tip If you use the [balm-mod](https://github.com/TwelveIterations/balm-mod){rel=""nofollow""} template repository, this setup is already done for you. :: To do that, you need to add calls to `Balm.initializeMod()` in each of your entrypoints - that is your `ModInitializer` on Fabric, and your `@Mod` class constructors in Neo/Forge. Similarly, your `ClientModInitializer` and your client-side `@Mod` class (NeoForge only) should use `BalmClient.initializeMod()`. On Forge, wrap this call in a `FMLEnvironment.dist.isClient()` check. Examples for each mod loader are provided below. ## NeoForge ### neoforge.mods.toml Your mod must come with a `neoforge.mods.toml` file that defines the metadata, dependencies and mixin configurations to load. Refer to the [NeoForge documentation](https://docs.neoforged.net/docs/gettingstarted/modfiles#neoforgemodstoml){rel=""nofollow""} to learn more. ### Common Entrypoint ```java [neoforge/src/main/java/NeoForgeWaystones.java] @Mod(Waystones.MOD_ID) public class NeoForgeWaystones { public NeoForgeWaystones(IEventBus modEventBus) { final var loadContext = new NeoForgeLoadContext(modEventBus); Balm.initializeMod(Waystones.MOD_ID, loadContext, Waystones::initialize); } } ``` ### Client Entrypoint ```java [neoforge/src/main/java/NeoForgeWaystonesClient.java] @Mod(value = Waystones.MOD_ID, dist = Dist.CLIENT) public class NeoForgeWaystonesClient { public NeoForgeWaystonesClient(IEventBus modEventBus) { final var loadContext = new NeoForgeLoadContext(modEventBus); BalmClient.initializeMod(Waystones.MOD_ID, loadContext, WaystonesClient::initialize); } } ``` ## Fabric ### fabric.mod.json Your mod must come with a `fabric.mod.json` file that defines the `main` and `client` entrypoints to your entrypoint classes as well as all other metadata. Refer to the [Fabric documentation](https://docs.fabricmc.net/develop/getting-started/project-structure#fabric-mod-json){rel=""nofollow""} to learn more. ::tip Balm provides a default [Mod Menu](https://modrinth.com/mod/modmenu){rel=""nofollow""} entrypoint to automatically expose your mod's configuration through supported config screens. You should only need to define a custom `modmenu` entrypoint if you want to provide your own configuration screen or make other changes. :: ### Common Entrypoint ```java [fabric/src/main/java/FabricWaystones.java] public class FabricWaystones implements ModInitializer { @Override public void onInitialize() { Balm.initializeMod(Waystones.MOD_ID, FabricLoadContext.INSTANCE, Waystones::initialize); } } ``` ### Client Entrypoint ```java [fabric/src/main/java/FabricWaystonesClient.java] public class FabricWaystonesClient implements ClientModInitializer { @Override public void onInitializeClient() { BalmClient.initializeMod(Waystones.MOD_ID, FabricLoadContext.INSTANCE, WaystonesClient::initialize); } } ``` ## Forge ### mods.toml Your mod must come with a `mods.toml` file that defines the metadata and dependencies to load. Refer to the [Forge documentation](https://docs.minecraftforge.net/en/latest/gettingstarted/modfiles/#modstoml){rel=""nofollow""} to learn more. Also note that Forge uses the `MixinConfigs` manifest attribute to define mixin configurations. On the template projects this is already taken care of, but otherwise you may have to adjust your gradle files to set the attribute accordingly. ### Common & Client Entrypoint ```java [forge/src/main/java/ForgeWaystones.java] @Mod(Waystones.MOD_ID) public class ForgeWaystones { final var loadContext = new ForgeLoadContext(context.getModEventBus()); Balm.initializeMod(Waystones.MOD_ID, loadContext, Waystones::initialize); if (FMLEnvironment.dist.isClient()) { BalmClient.initializeMod(Waystones.MOD_ID, loadContext, WaystonesClient::initialize); } } ``` ::tip This looks scary because it's accessing client-only code from a common context, but it's actually safe in modern Java as long as the methods involved are all static. :: ## Third Party Mod Compatibility The mod-loader specific entrypoints are also a good place to initialize addon compatibility classes that are only present in your mod-loader subprojects. You can use `Balm.initializeIfLoaded` to only load the given class if the mod is present, avoiding a hard dependency on the third party mod. ```java [Example] Balm.initializeIfLoaded("exnihilosequentia", "net.blay09.mods.excompressum.compat.ExNihiloSequentiaIntegration"); ``` ::warning Don't use `YourIntegration.class.getName()` as that will cause the class to be loaded even if the third party mod isn't present, which would lead to a crash if that class happens to access any classes from the third party mod. :: # Platform Proxies ## Overview Platform proxies are a way of abstracting platform-specific code away through a common interface from your common code. ::tip Most features you would normally need to access are already abstracted through other means using the Balm API, and there is a good chance you do not need to use platform proxies. They merely exist as a simple way to extend Balm's capabilities for things that are not already natively supported. :: With platform proxies, you can: - Define a common interface to abstract loader-specific code into common-accessible code - Provide implementations for your interface on each loader subproject individually - Have Balm automatically load the appropriate implementation when accessing your proxy ## Usage ```java // Define implementations for each platform MyInterface instance = Balm.platformProxy() .withFabric("com.example.mymod.fabric.FabricImplementation") .withForge("com.example.mymod.forge.ForgeImplementation") .withNeoForge("com.example.mymod.neoforge.NeoForgeImplementation") .build(); ``` ## Requirements for Proxy Classes Implementation classes loaded through the platform proxy system must: - Implement the interface you're proxying - Have a public no-argument constructor ## Platform Detection For the rare case where you need to handle some mod logic differently on one platform without needing to access loader-specific code, Balm also provides the current platform through `Balm.platform().name()` and its supported platforms in the `LoaderPlatforms` class. ```java if (Balm.platform().name().equals(LoaderPlatforms.FABRIC)) { // Do something that only happens on Fabric (but obviously, without having access to Fabric APIs since you're in common code) } ``` ### Comparison with Other Proxy Types Balm also provides other proxy types: - [Mod Proxies](https://balm.twelveiterations.com/mod-proxy): For mod-specific code (e.g. abstracting away different currency mods) - [Sided Proxies](https://balm.twelveiterations.com/sided-proxy): For client vs dedicated server code (only rarely needed nowadays) # Sided Proxies ## Overview Sided proxies are a way of safely calling client-only code in a common context. ::tip Balm provides its own sided proxy via `Balm.safeClientAccess()` that can be used for the most common use cases, such as getting the local player. :: With sided proxies, you can: - Define a common class that provides the default implementation to use (e.g. on servers) - Define a client sub-class that provides the implementation to use on clients ::warning Needing a sided proxy is often a sign that something is wrong in the design of your code. Consider refactoring to better separate your client code from your common code instead. :: ## Usage ```java MyCommonProxy sidedProxy = Balm.sidedProxy("com.example.mymod.MyCommonProxy","com.example.mymod.client.MyClientProxy").get(); ``` ## Requirements for Proxy Classes Proxy classes must - Have a public no-argument constructor The client proxy class must - extend the common proxy class ## Platform Detection If you have a `Level` available in your context, you should use `level.isClientSide` to determine whether you're acting on the client level or the server level. ::info Note that singleplayer has a server level too, since it is running an integrated server. :: For cases where you need to actually know whether the mod is running on a client or a dedicated server, you can check the environment: ```java if (Balm.platform().physicalSide() == BalmEnvironment.CLIENT) { // Do something that only happens on clients, but not on dedicated servers } ``` ::warning Be careful not to cause any client-only classes to be loaded when you use this pattern! It is safe to call a static method of a client-only class (as long as it doesn't take any client-only class parameters), but you cannot instantiate or access any instances of client-only classes. :: ### Comparison with Other Proxy Types Balm also provides other proxy types: - [Platform Proxies](https://balm.twelveiterations.com/platform-proxy): For platform-specific code (e.g. NeoForge vs Fabric) - [Mod Proxies](https://balm.twelveiterations.com/mod-proxy): For mod-specific code (e.g. abstracting away different currency mods) # Modules Balm offers an additional way of structuring your mod by implementing the `BalmModule` interface. This interface comes equipped with overridable methods for any of the registrars you may need. There is no functional difference to the standard way of initializing your mod, `BalmModule` is essentially just semantic sugar to bring more structure into the initialization process. The BalmModule approach may be useful for small mods or for mods with distinct thematic areas where it would make sense to split things into multiple modules. ## Defining a Module Modules need to implement at least `getId()` so they can be identified and attributed to a specific mod. The `path` is up to you to use. All other methods are defaulted to be empty. ```java [common/src/main/java/Waystones.java] public class Waystones implements BalmModule { public static final String MOD_ID = "waystones"; public static DeferredBlock waystoneBlock; @Override public Identifier getId() { return Identifier.fromNamespaceAndPath(MOD_ID, "common"); } @Override public void registerBlocks(BalmBlockRegistrar blocks) { waystoneBlock = blocks.register("waystone", WaystoneBlock::new, it -> it.sound(SoundType.STONE) .strength(5f, 2000f) ).withItem(WaystoneBlockItem::new).asDeferredBlock(); } @Override public void registerConfig(BalmConfig config) { config.registerConfig(WaystonesConfig.class); } @Override public void initialize() { ServerPlayerCallback.Join.EVENT.register(player -> { System.out.println("Player " + player + " has just joined the game!"); }); } } ``` ## Initializing your Mod You can pass an instance of the `BalmModule` interface to the `Balm.initializeMod()` method of your loader entrypoint to initialize your mod with a module. ```java [fabric/src/main/java/FabricWaystones.java] Balm.initializeMod(Waystones.MOD_ID, FabricLoadContext.INSTANCE, new Waystones()); ``` ## Initializing your mod with multiple modules You can also pass multiple modules to have them all registered. They will initialize in the order they were passed in. ```java [fabric/src/main/java/FabricWaystones.java] Balm.initializeMod(Waystones.MOD_ID, FabricLoadContext.INSTANCE, new Waystones(), new Sharestones()); ``` ## Registering a module manually You can call `BalmRegistrars.registerModule()` to load an additional module. ```java [common/src/main/java/Waystones.java] public static void initialize(BalmRegistrars registrars) { registrars.registerModule(new Sharestones()); } ``` ## Client Modules Client modules behave exactly the same except they use the `BalmClientModule` interface and are registered through `BalmClient`. # Loader Parity Balm generally attempts to behave identical across its supported loaders, but there are some cases where parity has not been reached. The following is an overview of all known and unresolved differences in feature support. | | NeoForge | Fabric | Forge | Notes | | --------------------------------------------------------- | -------- | ------ | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `BalmDataAttachmentTypeRegistrar` | ✅ | ✅ | ⛔ | Attempting to register or access on Forge will throw. | | `BalmCompostableRegistrar` | ✅ | ✅ | ⛔ | Attempting to register or access on Forge will throw. | | `BalmResourceReloadListenerRegistrar#addDependency` | ✅ | ✅ | ⛔ | Attempting to define a dependency on Forge will throw. | | `BalmClientResourceReloadListenerRegistrar#addDependency` | ✅ | ✅ | ⛔ | Attempting to define a dependency on Forge will throw. | | `BalmCapabilities#registerType` | ✅ | ✅ | ⛔ | `registerType` will make a type known to Balm, but registration on Forge is only possible with a Forge-specific annotation, and therefore no longer trivially achievable in common code. | | `BalmRegistrar#createCustomRegistry` | ✅ | ✅ | ⛔ | Balm does not support code-driven custom registries on Forge. | | `ScreenCallback.Render.AFTER_BACKGROUND` | ✅ | ✅ | ⚠️ | `mouseX`, `mouseY` and `tickDelta` are always `0` on Forge | | `ServerPlayerCallback.Respawn.EVENT` | ⚠️ | ✅ | ⚠️ | `oldPlayer` and `newPlayer` are always the same on Neo/Forge | | `ServerTickCallback.ServerEntityTick` | ✅ | ✅ | ⚠️ | Only fired for living entities on Forge; `BEFORE` and `AFTER` fire at the same time on Forge | | `BalmLootTables` | ⚠️ | ⚠️ | ⚠️ | Balm does not support ordering of individual loot modifiers at the moment - all Balm-registered modifiers are treated as one batch. | | `.asHolder()` | ✅ | ✅ | ⚠️ | Must only be called after deferred registration has finished on Forge. | # Minecraft 26.3 ::note These migration notes cover only the changes made to Balm APIs. For Minecraft and mod-loader specific migrations, check out their respective release announcements or primers. :: ## Overview The most notable change in Minecraft 26.3 is the switch from GLFW to SDL, as well as more things being migrated to use Data Components. ::warning If you are currently doing magic number comparisons like `event.button() == 0` or `button == 1` to test for left/right mouse buttons, you must update to use the appropriate `InputConstants` field instead. The numeric values have changed due to the migration to SDL. :: ### Version Catalog :latest-versions{minecraft="26.3"} ## ⚠️ Known Issues ## Breaking Changes ### Removed `BalmHooks#setBurnTime` Minecraft provides a Data Component `CookingFuel` now that you should use. ### Removed `BalmHooks#blockGrowFeature` This method was only used to fire an event on Forge. There is no direct replacement. If you have need for this, you can define a bi-directional event mapper or use a platform proxy. ### Removed `BalmCompostableRegistrar` Minecraft provides a Data Component `Compostable` now that you should use. ### Removed `BalmResourceReloadListenerRegistrar.VanillaKeys#advancements()` and `#recipes()` Recipes and Advancements appear to no longer use a resource reload listener. ### Replaced `BalmItemTags` with `ConventionalItemTags` Use `ConventionalItemTags` for conventional `c:` item tags. Most constants keep the same name, with the following exceptions: - `BalmItemTags.DIAMONDS` is now `ConventionalItemTags.DIAMOND_GEMS`. - `BalmItemTags.EMERALDS` is now `ConventionalItemTags.EMERALD_GEMS`. - `BalmItemTags.DYE_TAGS` is now `ConventionalItemTags.COLOR_DYES` and uses a `ColorCollection>` instead of an array. - `BalmItemTags.COOKING_OIL` has no replacement, since it was not an official conventional tag. Balm no longer ships the outdated `c:` tag data that previously populated these tags. Both Fabric and NeoForge already take care of that. ### Removed `BalmHooks#useFluidTank` This method has not been updated in years. There is no current equivalent in Balm; you would need to use a platform proxy to implement this logic yourself. There are plans to bring the [Transfer API](https://github.com/TwelveIterations/Balm/issues/263){rel=""nofollow""} to Balm, which would likely reintroduce an easier way to deal with fluid tank interactions. ### `BalmRegistrar#register` now returns `BalmHolderRegistration` The `register` methods on `BalmRegistrar` and `BalmRegistrar.Scoped` now return a `BalmHolderRegistration` instead of a `Holder` directly. Use the appropriate conversion method for the type you need: - `asHolder()` returns the registered value as a `Holder`. - `asSupplier()` returns it as a `Supplier`. - `asResourceKey()` returns its `ResourceKey`. For example, code that stores the result as a holder must now call `asHolder()`: ```java SPARK = spells.register("spark", id -> new ExampleSpell("Spark")).asHolder(); ``` # Minecraft 26.2 ::note These migration notes cover only the changes made to Balm APIs. For Minecraft and mod-loader specific migrations, check out their respective release announcements or primers. :: ## Overview Minecraft 26.2 is a fairly small update, most noticeably featuring some more rendering-related method renames, the introduction of `BlockItemId`, and the sealing of `Holder`, resulting in some changes to Balm's `DeferredBlock` and `DeferredItem` ### Version Catalog :latest-versions{minecraft="26.2"} ## ⚠️ Known Issues ### Some features are not available on Forge For a full list of features unsupported on Forge, check the new [Loader Parity](https://balm.twelveiterations.com/advanced/loader-parity) overview. If you depend on any of those features, my recommendation is to drop support for Forge. Balm won't be putting major efforts into feature parity with Forge. ## Breaking Changes ### Removed `DeferredHolder` and related workarounds Mojang has sealed `Holder` in this release, which makes it harder for other parties to create their own subclasses of it. This makes sense, as there is places where Holders are used as identity, meaning it is not a safe construct to subclass even if the Holder would be otherwise equal. In the past, Balm matched NeoForge by introducing its own `DeferredHolder` class and a workaround mixin for the equality checks, however, sticking to this solution would mean actively fighting Mojang on this design choice. Therefore, `DeferredHolder` has been removed from Balm and `DeferredItem` and `DeferredBlock` no longer implement `Holder`. This results in the following changes for uses of `asHolder()`: - On Fabric, nothing changes, because registry registrations are instant. `asHolder()` can be called instantly. - On NeoForge, the last I've read was that `DeferredHolder` was going to be kept, so similarly `asHolder()` can likely still be called instantly. - On Forge, there was never a native deferred `Holder` subclass, which means `asHolder()` was using Balm's now removed `DeferredHolder` wrapper. This means, in 26.2+, `asHolder()` on Forge must only be called after registries have loaded. It is recommended to switch to any of the other methods provided by the `*Registration` instance. Additionally, any Vanilla method that previously accepted a `DeferredItem` or `DeferredBlock` via `Holder` or `Holder` will no longer accept it directly. Use `.asItem()` or `.asBlock()` to resolve it when passing them. See [here](https://github.com/TwelveIterations/Waystones/commit/740a14a3d37ab8d666d9fd5ffc787596904aed8c#diff-102e8635ad98472451554bb8ab3dcd43ddb5d2e87c2d7f24663e5f2afe3c0d8d){rel=""nofollow""} for an example of this change. ### `RenderCallback.BlockHighlight` is now fired during extraction rather than rendering This makes the event follow Mojang's changes towards separate extraction/submit rendering stages, and makes it more specialized towards its purpose. See [here](https://github.com/TwelveIterations/KleeSlabs/commit/c3f40971183675ceaa64ea404af984224e625857#diff-4e3150b323dc3a62fa68be4696debf22649c0c8c517f6b717f5c0f2770a26b5d){rel=""nofollow""} for an example of how to update to the new event signature. ### `LivingEntityCallback.Death.Before` is now consistent between loaders and receives damage parameter Prior to this, Fabric would call `LivingEntityCallback.Death.Before` before totem activation, while Neo/Forge would fire it after totems were already checked. Fabric's approach seems more correct. Totems prevent death, therefore they should only be checked after a death has not been cancelled. To resolve this inconsistency, Balm now uses a supplemental event instead of relying on NeoForge's native event. This also includes a new `float damage` parameter added to its signature. ### `FluidTank` now supports multiple slots `FluidTank` is now slot-aware so Balm can expose multi-tank implementations correctly to Fabric, Forge and NeoForge's native fluid APIs. If you implement `FluidTank` yourself, update every method to receive a slot parameter and add `getSlotCount()`: ```diff -int fill(Fluid fluid, int maxFill, boolean simulate); +int fill(int slot, Fluid fluid, int maxFill, boolean simulate); -int drain(Fluid fluid, int maxDrain, boolean simulate); +int drain(int slot, Fluid fluid, int maxDrain, boolean simulate); -Fluid getFluid(); +Fluid getFluid(int slot); -void setFluid(Fluid fluid, int amount); +void setFluid(int slot, Fluid fluid, int amount); -int getAmount(); +int getAmount(int slot); -void setAmount(int amount); +void setAmount(int slot, int amount); -int getCapacity(); +int getCapacity(int slot); -boolean canDrain(Fluid fluid); +boolean canDrain(int slot, Fluid fluid); -boolean canFill(Fluid fluid); +boolean canFill(int slot, Fluid fluid); -boolean isEmpty(); +boolean isEmpty(int slot); + +int getSlotCount(); ``` For existing single-tank implementations, return `1` from `getSlotCount()` and treat slot `0` as the only valid slot. This is also what `DefaultFluidTank` does. Call sites using a `FluidTank` directly must now pass a slot, usually `0` for existing single-tank code: ```diff -tank.fill(fluid, amount, simulate); +tank.fill(0, fluid, amount, simulate); -tank.getFluid(); +tank.getFluid(0); ``` ### Removed deprecated `BalmBlockRegistrar#enableBlockDescriptionPrefixForItems` workaround This is now the default, as it should be for block items. You can use `Item.Properties#useItemDescriptionPrefix` to use an `item.` lang key instead on a case-by-base basis. ### Removed deprecated `BalmLootModifier` overload without `lootTableId` Use the version that takes a `lootTableId` instead. See [here](https://github.com/TwelveIterations/PantryForBlockheads/blob/26.2/common/src/main/java/net/blay09/mods/pantryforblockheads/loot/ModLootModifiers.java#L50){rel=""nofollow""} for an example. ### Removed deprecated `UnpackedLootTableHolder` This is no longer needed or useful now that `BalmLootModifier` receives a `lootTableId` parameter. See [here](https://github.com/TwelveIterations/PantryForBlockheads/blob/26.2/common/src/main/java/net/blay09/mods/pantryforblockheads/loot/ModLootModifiers.java#L50){rel=""nofollow""} for an example. ### Removed deprecated `Supplier`-based `#withItems` and `#withDefaultItems` overloads In order for Balm to set appropriate defaults without overwriting any consumer-made changes, you must now use the `Function` or `BiFunction` overloads that receive an existing `Item.Properties` instance. You don't *have* to use this instance if you need a separate one (e.g. a copy of an existing block's properties), but if you create your own instance, you must fully set it up yourself (including a `setId` call that Balm already does on the prepared instance). ### Removed deprecated `SimpleRecipeTransferRegistration` in favor of `IdentifiableRecipeTypeTransferRegistration` The previous implementation was unusable for anything other than custom recipe types also registered through the same layer. The new implementation correctly supports existing recipe types, so something like `registrar.registerRecipeTransferHandler(PortableCraftingMenu.class, ModMenus.portableCrafting, RecipeType.CRAFTING, 1, 9, 10, 36);` can now work correctly. ### Removed deprecated `prefix`/`suffix` methods in `DiscriminatedBlocks` and `DiscriminatedItems` The new variants `prefixWith`, `suffixWith` and `surroundWith` are a lot more clear about their intent and easier to use in general. See [here](https://github.com/TwelveIterations/Waystones/commit/740a14a3d37ab8d666d9fd5ffc787596904aed8c#diff-d77da83823773f30cbdb3a09d5cd1f5bf132966c2cb479987e3296104432964d){rel=""nofollow""} for an example of this change. ## Other Changes - Added `DeferredBlock#asBlockItemId`, useful for item tag generators - Added `DeferredBlock#asResourceKey` and `DeferredItem#asResourceKey` # Minecraft 26.1 ::note These migration notes cover only the changes made to Balm APIs. For Minecraft and mod-loader specific migrations, check out their respective release announcements or primers. :: ## Overview Minecraft 26.1 comes with a lot of toolchain changes, such as a new Loom plugin (`net.fabricmc.fabric-loom`), ForgeGradle 7, Java 25 and deobfuscated names. You will have to follow the individual mod loaders' migration guides to update your build setup. For reference, you can also compare against the [balm-mod](https://github.com/TwelveIterations/balm-mod){rel=""nofollow""} template repository and the commits made on the [26.1](https://github.com/TwelveIterations/balm-mod/commits/26.1/){rel=""nofollow""} branch to get an idea of what changes are required. In terms of code migrations, there are a few bigger updates in Minecraft (like the introduction of `ItemStackTemplate`), but Balm itself has not undergone any significant changes. ### Version Catalog :latest-versions{minecraft="26.1"} ## ⚠️ Known Issues ### Some features are not available on Forge For a full list of features unsupported on Forge, check the new [Loader Parity](https://balm.twelveiterations.com/advanced/loader-parity) overview. If you depend on any of those features, my recommendation is to drop support for Forge. Balm won't be putting major efforts into feature parity with Forge. ## Breaking Changes ### `NeoForgeLoadContext` now requires a `ModContainer` parameter This is mainly for future-proofing, to avoid potentially brittle static lookup over the mod list ```diff +// Add a ModContainer parameter to your NeoForge @Mod entrypoint constructor. NeoForge will pass it for you. -public NeoForgeYourMod(IEventBus modBus) { +public NeoForgeYourMod(ModContainer modContainer, IEventBus modBus) { ``` ```diff +// Pass the mod container into the NeoForgeLoadContext in your Balm.initializeMod and BalmClient.initializeMod calls -Balm.initializeMod("yourmod", new NeoForgeLoadContext(modBus), ...); +Balm.initializeMod("yourmod", new NeoForgeLoadContext(modContainer, modBus), ...); ``` ### `BalmVillagerTradeRegistrar` has been removed Minecraft supports data-driven villager trades now. ### `BalmBlockRenderTypeRegistrar` has been removed Minecraft automatically computes render layers from the block's textures now. ### `BalmBlockColorRegistrar#register(...)` now takes `List` instead of `BlockColor` Replace old block color registrations with new tint-source list registration. ## Other Changes - `DiscriminatedBlocks` and `DiscriminatedItems` removed `filterNonNullDiscriminatorEntries` and `filterNonNullDiscriminators`; use `sortedEntries(...)` / `sortedValues(...)` instead. Null discriminators now sort first, and blank discriminator values no longer generate prefixes/suffixes when using the helper methods. - `BalmItemRegistrar#registerDiscriminated(...)` was added, bringing discriminated item registration in line with block registration - Registry alias support was added across the generic and typed registrars via `addAlias(...)`. Use it for renamed IDs to preserve world/data compatibility when changing object names. - Config-screen selection is now configurable through `BalmConfig#setPreferredConfigScreen`. - Networking `sendTo*` behavior was tightened so packets are only sent when the channel is supported on the remote. # Minecraft 1.21.11 ::note These migration notes cover only the changes made to Balm APIs. For Minecraft and mod-loader specific migrations, check out their respective release announcements or primers. :: ## Overview In Minecraft 1.21.11, Balm's registry methods and event system have received major refactors. Almost all packages have been reorganized to be more consistent and align with Minecraft's package structure. The new registry methods severely clean up the registration calls as well as their internal implementations, matching the style that Minecraft uses to register its own blocks, adjusted to the needs of mod loaders and modder convenience. The new event system comes with better performance and better integration into mod loaders. For example, events now support phases on Fabric (priority equivalent on Neo/Forge), and the system supports firing back into the native mod loader bus for selected events, which can be used to e.g. notify other non-Balm mods about native events like block breaks. The new consistent package structure makes it easier to distinguish API classes from internal implementations and avoids ambiguities between Vanilla and mod loader concepts (e.g. there are two concepts of Permissions, one based on Vanilla's new 1.21.11 permission system, and one based on the Bukkit-esque permissio ecosystem, which are now clearly distinguished). This was the first refactor Balm has undergone since it started as a roughly put together library for my own mods four years ago. These changes resulted in many of Balm's internals being cleaned up and modernized to match the current modding ecosystem. ### Version Catalog :latest-versions{minecraft="1.21.11"} ## ⚠️ Known Issues ### Data Attachments are not available on Forge Forge does not have a native system for data attachments like Fabric and NeoForge. Balm may provide a basic implementation in the future, but right now attempting to access data attachments on Forge will crash. ### `/balm export icons` is not yet functional The command has not yet been updated to Minecraft's rendering changes. ## Breaking Changes ### Balm no longer has an additional Minecraft version suffix in its versions Instead of `21.11.1+1.21.11`, the version is now just `21.11.1`. You also no longer need to include Kuma in your build manually, as Balm correctly defines it as an exposed dependency now. ### Many `net.blay09.mods.balm` packages have moved I recommend cleaning out the broken imports and just reimporting using your IDE. ```diff - import net.blay09.mods.balm.Balm; + import net.blay09.mods.balm.api.Balm; - import net.blay09.mods.balm.api.module.BalmModule; + import net.blay09.mods.balm.platform.module.BalmModule; - import net.blay09.mods.balm.api.client.module.BalmClientModule; + import net.blay09.mods.balm.client.platform.module.BalmClientModule; - import net.blay09.mods.balm.api.block.entity.OnLoadHandler; + import net.blay09.mods.balm.world.level.block.entity.OnLoadHandler; - import net.blay09.mods.balm.api.loot.BalmLootModifier; + import net.blay09.mods.balm.world.level.storage.loot.BalmLootModifier; - import net.blay09.mods.balm.api.container.BalmContainerProvider; + import net.blay09.mods.balm.world.BalmContainerProvider; - import net.blay09.mods.balm.api.container.DefaultContainer; + import net.blay09.mods.balm.world.DefaultContainer; - import net.blay09.mods.balm.api.container.SubContainer; + import net.blay09.mods.balm.world.SubContainer; - import net.blay09.mods.balm.api.container.CombinedContainer; + import net.blay09.mods.balm.world.CombinedContainer; - import net.blay09.mods.balm.api.fluid.BalmFluidTankProvider; + import net.blay09.mods.balm.platform.fluid.BalmFluidTankProvider; - import net.blay09.mods.balm.api.fluid.DefaultFluidTank; + import net.blay09.mods.balm.platform.fluid.DefaultFluidTank; - import net.blay09.mods.balm.api.fluid.FluidTank; + import net.blay09.mods.balm.platform.fluid.FluidTank; - import net.blay09.mods.balm.api.menu.BalmMenuFactory; + import net.blay09.mods.balm.world.BalmMenuFactory; - import net.blay09.mods.balm.api.menu.BalmMenuProvider; + import net.blay09.mods.balm.world.BalmMenuProvider; - import net.blay09.mods.balm.common.CommonCapabilities; + import net.blay09.mods.balm.platform.capabilities.CommonCapabilities; - import net.blay09.mods.balm.api.tag.BalmItemTags; + import net.blay09.mods.balm.tags.BalmItemTags; - import net.blay09.mods.balm.api.energy.BalmEnergyStorageProvider; + import net.blay09.mods.balm.platform.energy.BalmEnergyStorageProvider; - import net.blay09.mods.balm.api.energy.DefaultEnergyStorage; + import net.blay09.mods.balm.platform.energy.DefaultEnergyStorage; - import net.blay09.mods.balm.api.energy.EnergyStorage; + import net.blay09.mods.balm.platform.energy.EnergyStorage; - import net.blay09.mods.balm.api.container.ContainerUtils; + import net.blay09.mods.balm.world.ContainerUtils; - import net.blay09.mods.balm.api.network.BalmNetworking; + import net.blay09.mods.balm.network.BalmNetworking; - import net.blay09.mods.balm.api.command.BalmCommands; + import net.blay09.mods.balm.commands.BalmCommands; - import net.blay09.mods.balm.api.config.reflection.Comment; + import net.blay09.mods.balm.platform.config.reflection.Comment; - import net.blay09.mods.balm.api.config.reflection.Config; + import net.blay09.mods.balm.platform.config.reflection.Config; - import net.blay09.mods.balm.api.config.reflection.NestedType; + import net.blay09.mods.balm.platform.config.reflection.NestedType; - import net.blay09.mods.balm.neoforge.NeoForgeLoadContext; + import net.blay09.mods.balm.neoforge.platform.runtime.NeoForgeLoadContext; ``` Event and Registry related methods have been moved elsewhere - see below. ### Many methods in `Balm` have been renamed ```diff - Balm.getConfig() + Balm.config() - Balm.getNetworking() + Balm.networking() - Balm.getPermissions() + Balm.permissions() - Balm.getHooks() + Balm.hooks() - Balm.getProxy() + Balm.safeClientAccess() ``` Event and Registry related methods have been moved elsewhere - see below. ### `EmptyLoadContext` has been removed in favor of loader-specific contexts Use `FabricLoadContext.INSTANCE` on Fabric, and construct a `NeoForgeLoadContext` or `ForgeLoadContext` on Neo/Forge. ### `BalmBlockEntity` has been removed in favor of `BalmBlockEntityUtils` This class was a relic from the past, was misleading developers to think they were required to use it, and couldn't be used when extending existing block entity classes. Therefore it has been removed. You can instead implement `getUpdateTag` and `getUpdatePacket` on your block entity by calling the respective `BalmBlockEntityUtils` method inside. You can call what used to be `sync()` via `BalmBlockEntityUtils.sync(BlockEntity)`. Once deobfuscation is gone for good, we may be able to provide an interface defaulting those methods (which isn't possible at the moment due to remapping). ### The old registry methods (`Balm.getBlocks()` etc.) have been removed in favor of the new `BalmRegistrars` See the docs on [adding content](https://balm.twelveiterations.com/content/blocks) for how to use the new registrars. `DeferredObject` no longer exists, you can use `DeferredBlock`, `DeferredItem` or `Holder` now using `.asDeferredBlock()` or `.asHolder()` etc. in registration. Most things that used to be a `Balm*` class like `BalmBlocks` have now become `BalmBlockRegistrar`. Your mod's initializer will be passed a `BalmRegistrars` or `BalmClientRegistrars` instance that provides access to all registrars. `BalmModule` implementations have their methods called with registrars as before. ### The old event system has been removed in favor of the new event mappers Instead of Balm keeping track of its own event bus, event mappers instead take your registrations (and invocations) and map them directly to the native mod loader events (or a Balm supplemental event in cases where mod loaders do not support the event). This allows for better compatibility and avoids wrapping every event in a Balm-specific class. These new events can be found in `*Callback` classes such as `BlockCallback.Use`. You can also search for `EventMapper.createUnbound` usages to find them all. Listeners are registered directly on these event mappers via `.register()` rather than using `Balm.getEvents().onEvent`. Event handlers receive the callback parameters directly, i.e. there is no longer an `event` object. If you're using a `BalmModule`, instead of defining handlers in `registerEvents` you can just do it in `initialize()` or anywhere else now. To define your own events, you can use `EventMapper.createBound()` with your existing event class, and fire them using its `.invoker()`. Note that these events won't fire on native mod loader buses anymore. Third party mods must instead use your event mapper directly to call `.register()` with their handler on them. If you do want to provide backwards compatibility or continue to have your mods available on mod-loader specific buses, you will have to use `EventMapper.createUnbound()` instead and then provide a mapping on each loader entrypoint using `.configureMapping()`. ### Kuma by default no longer fires screen handler events if a widget has focus This is to avoid key mappings from being handled when the user has a widget like a text field focused. You can override this behaviour by calling `.ignoreScreenFocus()` in your key mapping builder. ### `CustomRenderBoundingBox` has been removed This interface was not actually used anymore. # Minecraft 1.21.9 ::note These migration notes cover only the changes made to Balm APIs. For Minecraft and mod-loader specific migrations, check out their respective release announcements or primers. :: ## Overview In Minecraft 1.21.9, some deprecated methods regarding to de/serialization have been removed. Key categories are now a record instead of a plain string, and are now namespaced. ### Version Catalog :latest-versions{minecraft="1.21.9"} ## ⚠️ Known Issues ### `/balm export icons` is not yet functional The command has not yet been updated to Minecraft's rendering changes. ## Breaking Changes ### `BalmKeyMappings.registerKeyMapping` now takes a `KeyMapping.Category` instead of a string Minecraft provides a static `register` method to create these. Consider using [Kuma](https://github.com/TwelveIterations/KumaAPI){rel=""nofollow""} instead, which is embedded in Balm. ### Kuma's `overrideCategory` now takes a `KeyMapping.Category` instead of a string Minecraft provides a static `register` method to create these. ### Kuma's default key category is now `{modId}.default` (i.e. `key.categories.{modId}.default`) You will have to update your language files. This is because Minecraft now uses resource locations instead of plain strings for key categories. ### `Balm.lookupAllModPaths` has been removed in favor of `Balm.visitModResources` This was necessary as NeoForge no longer provides `Path`s for mod resources. The visitor receives a `ModResource` object for every item under the given path, which can be used to read the file (equivalent to doing `Files.walk` on the returned paths previously). While `lookupAllModPaths` looked up paths in all loaded mods and returned them mapped by mod id, `visitModResources` expects a mod id to be passed in. You can get a list of all loaded mod ids using `Balm.getLoadedPrimaryModIds`. This will only return real mod ids, it does not include "provided" ids by Fabric mods. ### `Balm.lookupModPath` has been removed in favor of `Balm.lookupModResource` As above, this will now return an optional `ModResource` object instead of a `Path`. ## Removals ### `DefaultContainer.deserialize` and `DefaultContainer.serialize` has been removed in favor of `ContainerHelper.loadAllItems` and `ContainerHelper.saveAllItems` Use `net.minecraft.world.ContainerHelper#loadAllItems(ValueInput, NonNullList)` and `net.minecraft.world.ContainerHelper#saveAllItems(ValueOutput, NonNullList)` instead, which are identical. ### `ImplementedContainer.deserializeInventory` and `ImplementedContainer.serializeInventory` have been removed in favor of `ContainerHelper.loadAllItems` and `ContainerHelper.saveAllItems` Use `net.minecraft.world.ContainerHelper#loadAllItems(ValueInput, NonNullList)` and `net.minecraft.world.ContainerHelper#saveAllItems(ValueOutput, NonNullList)` instead, which are identical. ### `DefaultFluidTank.deserialize` and `DefaultFluidTank.serialize` methods based on `CompoundTag` have been removed in favor of their `ValueInput` and `ValueOutput` variants Use the methods of same name that take a `ValueOutput` or `ValueInput` instead. ### `DefaultEnergyStorage.deserialize` and `DefaultEnergyStorage.serialize` have been removed in favor of their `ValueInput` and `ValueOutput` variants Use the methods of same name that take a `ValueOutput` or `ValueInput` instead. Note that, unlike the removed methods, these methods write into an `"Energy"` key instead of returning an `IntTag` directly. You may have to migrate data manually if you want to support upgrading older worlds. # Minecraft 1.21.6 ::note These migration notes cover only the changes made to Balm APIs. For Minecraft and mod-loader specific migrations, check out their respective release announcements or primers. :: ## Overview In Minecraft 1.21.6, Balm has cleaned up some method names and deprecated some serialization-related methods in favor of Minecraft's newer ValueInput and ValueOutput APIs. ### Version Catalog :latest-versions{minecraft="1.21.6"} ## ⚠️ Known Issues ### `/balm export icons` is not yet functional The command has not yet been updated to Minecraft's rendering changes. ## Minor Changes ### `BalmCapabilities.registerProvider` now takes a `Supplier>>` instead of `Supplier>>` This change was made because passed in types should be unique. ### `BalmEnvironment.SERVER` has been renamed to `BalmEnvironment.DEDICATED_SERVER` This change was made to avoid confusion with logical sides (i.e. ClientLevel vs ServerLevel). ### `BalmRenderers.setBlockRenderType` now takes a `ChunkSectionLayer` instead of `RenderType` This is to align with Minecraft's API changes. ### `BalmBlockEntity.writeUpdateTag` now takes a `ValueOutput` instead of `CompoundTag` This change was made to align with Minecraft's API changes. ## Deprecations ### `DefaultContainer.serialize` and `DefaultContainer.deserialize` have been deprecated Use `net.minecraft.world.ContainerHelper#loadAllItems(ValueInput, NonNullList)` and `net.minecraft.world.ContainerHelper#saveAllItems(ValueOutput, NonNullList)` instead, which are identical. ### `ImplementedContainer.deserializeInventory` and `ImplementedContainer.serializeInventory` have been deprecated Use `net.minecraft.world.ContainerHelper#loadAllItems(ValueInput, NonNullList)` and `net.minecraft.world.ContainerHelper#saveAllItems(ValueOutput, NonNullList)` instead, which are identical. ### `DefaultEnergyStorage.serialize()` and `DefaultEnergyStorage.deserialize()` have been deprecated Use the methods of same name that take a `ValueOutput` or `ValueInput` instead. Note that, unlike the removed methods, these methods write into an `"Energy"` key instead of returning an `IntTag` directly. You may have to migrate data manually if you want to support upgrading older worlds. ### `DefaultFluidTank.serialize()` and `DefaultFluidTank.deserialize()` have been deprecated Use the methods of same name that take a `ValueOutput` or `ValueInput` instead. ## Removals ### `Balm.initialize` has been removed in favor of `Balm.initializeMod` The methods are otherwise the same, this was just a name change for better clarity. ### `BalmClient.initialize` has been removed in favor of `BalmClient.initializeMod` The methods are otherwise the same, this was just a name change for better clarity. ### `TickType.Entity` has been removed in favor of `TickType.ServerEntity` and `TickType.ClientEntity` This change was made to match the other tick types in style. ### `SidedProxy.get()` has been removed in favor of `build` or `buildLazily` This change makes the intent over when the proxy is initialized more clear. ### `ListUtils` has been removed It was not used in Balm anymore and can easily be copied into individual projects if needed. # Minecraft 1.21.5 ::note These migration notes cover only the changes made to Balm APIs. For Minecraft and mod-loader specific migrations, check out their respective release announcements or primers. :: ## Overview In Minecraft 1.21.5, Balm has removed some long-standing deprecations, and refactored the way configs and capabilities are handled. ### Version Catalog :latest-versions{minecraft="1.21.5"} ## ⚠️ Known Issues ### `/balm export icons` is not yet functional The command has not yet been updated to Minecraft's rendering changes. ## Notable Additions and Changes ### `BalmModule` and `BalmClientModule`, an alternative way to initialize your mods Modules are a slightly cleaner way of interacting with Balm, where instead of accessing Balm services through the `Balm` helper class in your initializer, you instead override the appropriate `register*` method in your module and have it called by Balm at the appropriate time. Read through the [Modules](https://balm.twelveiterations.com/../concepts/modules) documentation to learn more. ### `BalmConfigSchema` for Declarative Configs Configs have been refactored for a much cleaner and more stable code-base, while also adding the ability to define a configuration programmatically instead of only statically through annotations. ::info Annotation-based Configs The previous annotation-based approach is still present and will remain, however some packages and annotations have been renamed for clarity. :: Balm now also supports different config types, although only `common` and `client` are universally supported across all mod loaders. ::tip The changes to `BalmConfig` and Declarative Configs have been backported to 1.20.1, 1.21.1 and 1.21.4. :: #### `SyncConfigMessage` has been removed Balm now automatically takes care of syncing mod's configs through its own packet. Mods no longer need to register a packet of their own. #### Added `BalmConfigSchema` as an alternative way to define configs Configs are now no longer directly tied to a given data class. Instead, their structure is defined in a schema, very similar to how config specs are built in Forge and NeoForge. However, the annotation-based approach remains and will continue to function. Schemas will be auto-generated for annotation-based configs so both can use the same code in the back. Read through the [Mod Configurations](https://balm.twelveiterations.com/../configuration) documentation for more information. #### Renamed some packages for annotation-based configs Annotations such as `@Config`, `@Comment` and `@Synced` are now available under the package `net.blay09.mods.balm.api.config.reflection`. #### `ExpectedType` has been renamed to `NestedType` The new name makes it more clear that the annotation is meant to be used for collections / generics. #### `BalmConfigData` has been removed The interface served no purpose beyond slightly better type-safety. Since mods generally only access their config classes in two places (`registerConfig` and `getActiveConfig`) where errors are easy to notice, the interface has been removed for simplicity. ### `BalmCapabilities` replaces `BalmProviders` The previous `BalmProviders` implementation was heavily biased towards Forge's approach to capabilities and had several shortcomings that resulted in a lot of mod-loader-specific boilerplate. It has been removed in favor the new `BalmCapabilities` service. With this new service, it is now possible to universally register capabilities, capability providers, and fallback providers through Balm. Read through the [Capabilities](https://balm.twelveiterations.com/../advanced/capabilities) documentation for more information. ::tip The new `BalmCapabilities` service has been backported to 1.20.1, 1.21.1 and 1.21.4. :: ## Minor Changes ### `BalmModels.loadModel` now returns a `BlockStateModel` `BlockStateModel` can be used in the same manner that `BakedModel` was, with the difference that you must use `collectParts(RandomSource)` to pass them into the `tesselateBlock` method. ::warning Extra Models are not currently supported on Fabric. They will always return the missing model. :: ### `BalmRenderers` and `BalmScreens` registration methods now require a `ResourceLocation` identifier While this identifier is not technically used by Minecraft or mod loaders, Balm needs it to attribute your objects to the appropriate mod. As a side-effect, this should also be more future-proof since more and more things are moving to registries or at least being identifiable. ### `FluidTank` and `EnergyStorage` are now an interface An interface was added as to not limit the respective Balm capabilities to only Balm's default implementation. In return, the default implementation has been renamed to `DefaultFluidTank` and `DefaultEnergyStorage`. If you are querying for a `FLUID_TANK` or `ENERGY_STORAGE` capability, you should handle the interface under the same name as previously. If you are defining your own fluid tank or energy storage, you should use `DefaultFluidTank` and `DefaultEnergyStorage` or a custom implementation. ### `isConnectedToServer` has been renamed to `isConnected` This method is available through `Balm.getProxy()` and can be used before sending packets to ensure they are only sent if the player is actually ingame. This was changed to avoid confusion on what this method does. Unlike the previous name may have suggested, it is not the equivalent of an `isMultiplayer()` method. `isConnected` checks if the client is connected to any server, be it the integrated singleplayer server, a LAN server, or a dedicated server. ## Caveats ### `Balm.registerConfig` may not load your config immediately anymore On Forge and NeoForge, `common` and `client` configs are no longer immediately available. Accessing the config in your initializer will result in a crash. You can instead use the new `ConfigLoadedEvent` to act only once the config is available. However, that may be too late if you need access to the configuration for e.g. setting up item components. **There is no good universal solution to this as of now.** ::note This was the case all the way back to 1.20.1, but it was not as noticeable because Balm would simply return the default values for a configuration. Newer versions of Balm will crash if the config is accessed too early. :: ### Reload Listeners can no longer be added directly to `ReloadableResourceManager` Adding reload listeners to the `ReloadableResourceManager` directly is no longer supported, as NeoForge had made the list immutable in favor of their registration event. Balm now provides an `addResourceReloadListener` method that uses the proper channels to register the listener on each mod loader. Functionally, this method behaves the same as registering a reload listener directly used to. ## Removals ### Removals in `BalmBlocks` #### `blockProperties()` has been removed Block Properties now require an identifier to be set, and mod-loader specific `BlockBehaviour.Properties` no longer exist, making this method obsolete. Instead, there is now a static `blockProperties(ResourceLocation)` utility method you can use. #### `registerBlock(Supplier, ResourceLocation)` has been removed Use `registerBlock(Function, ResourceLocation)` instead, which gives you access to the identifier you passed in (since you'll need it to construct your block properties). #### `registerBlockItem(Supplier, ResourceLocation)` and `registerBlockItem(Supplier, ResourceLocation, ResourceLocation)` has been removed Use `registerBlockItem(Function, ResourceLocation)` or `registerBlockItem(Function, ResourceLocation, ResourceLocation)` instead, which gives you access to the identifier you passed in (since you'll need it to construct your item properties). #### `register(Supplier, Supplier, ResourceLocation)` and `register(Supplier, Supplier, ResourceLocation, ResourceLocation)` has been removed Use `register(Function, BiFunction, ResourceLocation)` or `register(Function, BiFunction, ResourceLocation, ResourceLocation)` instead, which gives you access to the identifier you passed in, since you'll need it to construct your block and item properties. Additionally, the `BiFunction` for your block item also gets the constructed block passed in, simplifying registration further. ### Removals in `BalmItems` #### `itemProperties()` has been removed Item Properties now require an identifier to be set, and mod-loader specific `Item.Properties` no longer exist, making this method obsolete. Instead, there is now a static `itemProperties(ResourceLocation)` utility method you can use. #### `registerItem(Supplier, ResourceLocation)` and `registerItem(Supplier, ResourceLocation, ResourceLocation)` has been removed Use `registerItem(Function, ResourceLocation)` or `registerItem(Function, ResourceLocation, ResourceLocation)` instead, which gives you access to the identifier you passed in (since you'll need it to construct your item properties). ### Removals in `BalmNetworking` #### `openGui` has been removed in favor of `openMenu` The new method name more accurately describes what the method does and is functionally identical. #### `registerClientboundPacket` that takes `Consumer` and `Function` for encoding and decoding has been removed Minecraft is migrating its network protocol to use `StreamCodec`s, which are a composable way of declaring the structure of the data that gets sent and received. You can still use `StreamCodec.of(encoder, decoder)` to create a `StreamCodec` with imperative code, although it is recommended to migrate to using `StreamCodec.composite(...)`, as this declarative approach is less error-prone. ### Removals in `BalmHooks` #### `getBurnTime` has been removed You can instead use `level.fuelValues().burnDuration(itemStack)`. #### `isShield` has been removed You can instead check for the `DataComponents.BLOCKS_ATTACKS` data components. #### `isRepairable` has been removed You can instead check for the `DataComponents.REPAIRABLE` or `DataComponents.REPAIR_COST` data components. #### `getBlockReachDistance` has been removed You can instead query the `Attributes.BLOCK_INTERACTION_RANGE` attribute. ### Removals in `BalmRegistries` #### `getKey`, `getItemKeys` and `getItem`/... has been removed These methods are all available through the respective registry in `BuiltinRegistries` instead. #### `getItemTag` has been removed You can instead easily create a `TagKey` yourself: ```java TagKey yourTag = TagKey.create(Registries.ITEM, yourResourceLocation); ``` ### Removals in `BalmKeyMappings` #### Registration methods taking `KeyConflictContext` and `KeyModifier` have been removed Kuma is the preferred solution for dealing with special key mappings, which allows defining defaults as well as fallbacks should key modifiers not be supported by the mod loader. Kuma is already embedded in Balm, so you can safely depend on it without needing to jar-in-jar it. #### Modifier- and Context-aware `isActive*` methods have been removed Since Balm no longer provides the functionality for key conflict contexts and modifiers, you can now just use the existing methods from the `KeyMapping` to check for its state. If you use Kuma, you can access the state on the `ManagedKeyMapping`, or use the event handlers when registering your keys to define your logic. #### `ignoreConflicts(KeyMapping)` and related methods have been removed These methods were added to allow configuring key modifiers through dummy key mappings, but they were never really functional due to Minecraft's limitation of only supporting an input to be bound to a single key. In prior Balm versions, these methods already no-op'ed. #### `KeyConflictContext`, `KeyModifier` and `KeyModifiers` have been removed If you were using these classes, you should instead use Kuma or define your own. ### Removals in `BalmRecipes` #### `registerRecipeType` that takes both RecipeType and RecipeSerializer has been removed Use `registerRecipeType` and `registerRecipeSerializer` individually instead. ### Removal of `BalmTextures` This class was already empty and has not been needed for a while, since custom textures can be added to atlases through resource packs. ### Removal of `ExtractionAwareContainer` This interface was highly incompatible with other mods as the extraction checks were only performed when Balm's own `ContainerUtils` were used. Instead, you should implement Vanilla's `Container.canTakeItem` or `WorldlyContainer.canTakeItemThroughFace` in your container. # Minecraft 1.21.4 ::note These migration notes cover only the changes made to Balm APIs. For Minecraft and mod-loader specific migrations, check out their respective release announcements or primers. :: ## Overview Balm for Minecraft 1.21.4 has introduced a load context for Forge. ### Version Catalog :latest-versions{minecraft="1.21.4"} ## Notable Additions and Changes ### Balm now expects a `ForgeLoadContext` on Forge You must change your Forge entrypoint to pass in a `ForgeLoadContext` with the mod event bus from your `FMLJavaModLoadingContext`. ```java public ForgeWaystones(FMLJavaModLoadingContext context) { final var loadContext = new ForgeLoadContext(context.getModEventBus()); Balm.initializeMod(Waystones.MOD_ID, loadContext, Waystones::initialize); } ``` # Minecraft 1.21.1 ::note These migration notes cover only the changes made to Balm APIs. For Minecraft and mod-loader specific migrations, check out their respective release announcements or primers. :: ## Overview Balm for Minecraft 1.21.1 has introduced load contexts, an object that you must pass to `initializeMod` and differs per loader. ### Version Catalog :latest-versions{minecraft="1.21.1"} ## Notable Additions and Changes ### Load Contexts Balm now requires a load context to be passed in when calling `initializeMod` in your entrypoints. On Fabric and Forge, you can simply pass `EmptyLoadContext.INSTANCE` when calling `initializeMod`. On NeoForge, you must pass a `NeoForgeLoadContext` which holds the mod event bus that was passed into the constructor of your NeoForge entrypoint. ```java title="Example on Fabric" @Override public void onInitialize() { Balm.initializeMod(Waystones.MOD_ID, EmptyLoadContext.INSTANCE, Waystones::initialize); } ``` ```java title="Example on NeoForge" public NeoForgeWaystones(IEventBus modEventBus) { final var context = new NeoForgeLoadContext(modEventBus); Balm.initializeMod(Waystones.MOD_ID, context, Waystones::initialize); } ``` ### Changes to Registering Network Packets To align with Vanilla, the encoder in `registerClientboundPacket` has had its parameters swapped. It is now a `BiConsumer` where `T` is your packet object. Minecraft is also migrating its network protocol to use `StreamCodec`s, which are a composable way of declaring the structure of the data that gets sent and received. Therefore, the old methods that took a decoder and encoder function have been deprecated. You can still use `StreamCodec.of(encoder, decoder)` to create a `StreamCodec` with imperative code, although it is recommended to migrate to using `StreamCodec.composite(...)`, as this declarative approach is less error-prone.