Skip to content

Addon Development

Warlon Mhite edited this page Nov 4, 2026 · 2 revisions

Home › Developers › Addon Development

How to write a mod that works with Hempdustry 2: getting it into your build, the four addon events on Fabric and on NeoForge, the types they hand you, strains and components from code, and exactly what you may rely on. For mod authors. If your idea needs no code — a strain, a recipe, a balance change — a datapack does it: see Datapacks.

On this page: Getting the mod into your build · Declaring the dependency · The addon events · On Fabric · On NeoForge · The types the events hand you · Strains from code · Item components from code · Ids, tags and data · What you can rely on

Getting the mod into your build

Hempdustry 2.1 is built for each loader and each Minecraft version separately — eight jars, the same features:

Loader Minecraft 26.3 Minecraft 26.2 Minecraft 1.21.11 Minecraft 1.21.1
Fabric hempdustry-fabric-2.1.0+26.3.jar hempdustry-fabric-2.1.0+26.2.jar hempdustry-fabric-2.1.0+1.21.11.jar hempdustry-fabric-2.1.0+1.21.1.jar
NeoForge hempdustry-neoforge-2.1.0+26.3.jar hempdustry-neoforge-2.1.0+26.2.jar hempdustry-neoforge-2.1.0+1.21.11.jar hempdustry-neoforge-2.1.0+1.21.1.jar

(The 2.0.x releases were Fabric only, named hempdustry-<version>+<minecraft>.jar.) Build against the jar of your own loader and Minecraft version. A Fabric jar and a NeoForge jar are separate builds of the mod, and an addon compiled against one does not load against the other.

The release jar (works today)

Download the jar for your loader from GitHub releases or CurseForge and put it in your project, say in libs/.

Fabric, with Loom — the Fabric jar is in intermediary names like every released Fabric mod, so Loom remaps it to whatever mappings you use:

dependencies {
    // Compile against Hempdustry...
    modCompileOnly files("libs/hempdustry-fabric-2.1.0+1.21.11.jar")
    // ...and load it in your development client and server. Fabric API is needed at run time too.
    modLocalRuntime files("libs/hempdustry-fabric-2.1.0+1.21.11.jar")
}

Use modImplementation instead of the pair if your addon cannot run without Hempdustry.

Fabric on Minecraft 26.x — the game ships unobfuscated, Loom for 26.x (net.fabricmc.fabric-loom) remaps nothing, and the Fabric jar is in Mojang's names, so it goes on the plain configurations: compileOnly and localRuntime (or implementation), as on NeoForge below.

NeoForge, with ModDevGradle — the NeoForge jar is in Mojang's names, which is what NeoForge runs on, so it needs no remapping:

configurations {
    // What the development runs load and your published jar does not depend on.
    localRuntime
    runtimeClasspath.extendsFrom localRuntime
}

dependencies {
    compileOnly files("libs/hempdustry-neoforge-2.1.0+1.21.11.jar")
    localRuntime files("libs/hempdustry-neoforge-2.1.0+1.21.11.jar")
}

That is the same arrangement Hempdustry's own NeoForge build uses for its recipe viewers. NeoForge needs nothing else at run time.

Your local maven, from the source (Fabric)

To build against an unreleased version, or to get the sources jar beside the classes, build the mod yourself (Building from Source; JDK 21) at the release you want and publish it to your local maven:

./gradlew :fabric:publishToMavenLocal

That publishes com.warlonmhite.hempdustry:hempdustry-fabric:<version>, with the version as the mod reports it (2.1.0+1.21.11), plus its sources:

repositories {
    mavenLocal()
}

dependencies {
    modImplementation "com.warlonmhite.hempdustry:hempdustry-fabric:2.1.0+1.21.11"
}

The NeoForge project has no maven publication; use the jar.

Not live yet

  • Modrinth's maven (https://api.modrinth.com/maven), which serves every file uploaded to Modrinth, will work once the Modrinth listing clears review. It is still in review, and the beta is not uploaded there. The coordinates will be on this page when they exist; an older snippet naming maven.modrinth:hempdustry:… does not resolve today.
  • GitHub Packages: the build can publish there, but no release has been, and reading from GitHub Packages needs a GitHub token. Don't depend on it yet.

Names in the snippets

Hempdustry is written in Mojang's names (with Parchment parameter names on 1.21.x; 26.x ships its own), on every loader and every Minecraft version, and so is every snippet on this page. On Minecraft 1.21.11 and 26.x an id is Identifier; on 1.21.1 it is ResourceLocation. On Fabric with Yarn mappings the vanilla names differ (PlayerEntity for Player, World for Level); Hempdustry's own names do not.

Declaring the dependency

Say so in your mod's metadata, so a launcher and the loader can tell a player what is missing. The id is hempdustry.

Fabric, fabric.mod.json — use depends for a required dependency, recommends or suggests for an optional one:

"depends": {
  "hempdustry": ">=2.1.0-beta"
}

Fabric's version predicates follow SemVer, where a pre-release sorts before its release: >=2.1.0 does not match 2.1.0-beta+1.21.11, >=2.1.0-beta does.

NeoForge, META-INF/neoforge.mods.toml:

[[dependencies.youraddon]]
modId = "hempdustry"
type = "required"          # or "optional"
versionRange = "[2.1.0-beta,)"
ordering = "AFTER"
side = "BOTH"

Hempdustry runs on both client and server, so an addon that touches it usually does too.

Tip: for an optional integration, keep every reference to Hempdustry's classes in a class of its own and load that class only when the mod is present — FabricLoader.getInstance().isModLoaded("hempdustry") on Fabric, ModList.get().isLoaded("hempdustry") on NeoForge. A class that names HempdustryEvents fails to load where Hempdustry is not installed.

The addon events

Four events, the same on both loaders, so another mod can react to Hempdustry — or refuse a hit — without a mixin:

Fabric (HempdustryEvents) NeoForge (posted on NeoForge.EVENT_BUS) When What you get
ALLOW_SMOKE AllowSmokeEvent before a hit, before anything is spent player, contents, device stack — return false / cancel the event to refuse the hit
AFTER_SMOKE AfterSmokeEvent after a hit has landed player, contents, device stack
AFTER_EAT AfterEatEvent when a player eats an edible player, potency tier, quality
AFTER_INFUSE AfterInfuseEvent when a batch of Cannabutter is collected from an Infuser level, position, strength, quality

The details every listener needs:

  • Server thread only. Every event is fired on the server thread. A listener may read and change the world directly; it must not block.
  • ALLOW_SMOKE comes before anything happens. A refused hit costs the player nothing — no effects, no cooldown, no bud, no durability — and the item simply does nothing, as when it is empty. It is asked once per hit: for a Bong, whose hit is held, at the end of the draw. The check is on the server, so a player's arm may still swing.
  • AFTER_SMOKE comes last: effects applied (or a green-out rolled instead of them), sound played, cooldown not yet started. The device stack is still loaded at that point, but a spliff's contents are about to be gone, so read the contents from the event, not from the stack. It fires for every spliff, pipe, bong and vaporizer hit.
  • AFTER_EAT fires at the moment of eating, not when the edible kicks in. The effects are queued behind an onset of up to 4:20 (a server can scale it) and then arrive in stages, so a listener that wants to act when the high lands should schedule its own delay. The tier is the edible's potency, 1–4. It fires for the six edibles — Space Cookie, Space Brownie, a slice of Space Cake, Cannabutter Toast, Dawamesk and Bucket of Bhang — and not for an undosed one, or for the plain hemp-seed foods.
  • AFTER_INFUSE fires however the batch leaves the tub — a player, a hopper, a pipe or the tub's own spout. It is the last moment the batch's numbers exist: strength (how much hemp went in, 1–24) and quality.
  • Consumption has two doors. A hit is AFTER_SMOKE, an edible is AFTER_EAT. A tolerance system that listens only to the first counts every joint and misses every brownie.

On Fabric

com.warlonmhite.hempdustry.api.HempdustryEvents: four Fabric API Events. Register in your mod initializer:

import com.warlonmhite.hempdustry.api.HempdustryEvents;
import net.fabricmc.api.ModInitializer;
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.Level;

public final class YourAddon implements ModInitializer {
    @Override
    public void onInitialize() {
        // A veto: no smoking in the Nether. Any listener returning false refuses the hit.
        HempdustryEvents.ALLOW_SMOKE.register((player, contents, device) ->
                !player.level().dimension().equals(Level.NETHER));

        // After a hit: contents is what was smoked, read from here rather than from the stack.
        HempdustryEvents.AFTER_SMOKE.register((player, contents, device) ->
                player.displayClientMessage(Component.literal(
                        "Dose " + contents.dose() + " of ").append(contents.loadName()), true));

        // An edible eaten: tier 1-4 and the butter's quality.
        HempdustryEvents.AFTER_EAT.register((player, tier, quality) ->
                player.displayClientMessage(Component.literal(
                        "Tier " + tier + ", " + quality.getSerializedName()), true));

        // A batch collected from an Infuser at pos.
        HempdustryEvents.AFTER_INFUSE.register((level, pos, strength, quality) -> {
            // strength: 1-24 hemp; quality: the grade it was collected at
        });
    }
}

The listener interfaces are HempdustryEvents.AllowSmoke (boolean allowSmoke(Player, SmokeContents, ItemStack)), AfterSmoke (void afterSmoke(Player, SmokeContents, ItemStack)), EdibleEaten (void onEaten(Player, int tier, Quality)) and CannabutterInfused (void onInfused(Level, BlockPos, int strength, Quality)), if you prefer a class to a lambda.

On NeoForge

New in 2.1. The same four as NeoForge events in the same package, com.warlonmhite.hempdustry.api, posted on NeoForge.EVENT_BUS (the game bus, not your mod bus):

Event Getters Cancellable
AllowSmokeEvent getPlayer(), getContents(), getDevice() yes — cancel it to refuse the hit
AfterSmokeEvent getPlayer(), getContents(), getDevice() no
AfterEatEvent getPlayer(), getTier(), getQuality() no
AfterInfuseEvent getLevel(), getPos(), getStrength(), getQuality() no
import com.warlonmhite.hempdustry.api.AfterEatEvent;
import com.warlonmhite.hempdustry.api.AfterInfuseEvent;
import com.warlonmhite.hempdustry.api.AfterSmokeEvent;
import com.warlonmhite.hempdustry.api.AllowSmokeEvent;
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.Level;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.ModList;
import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.common.NeoForge;

@Mod("youraddon")
public final class YourAddon {
    public YourAddon(IEventBus modBus) {
        if (ModList.get().isLoaded("hempdustry")) {
            HempdustryHooks.register();
        }
    }
}

final class HempdustryHooks {
    static void register() {
        NeoForge.EVENT_BUS.addListener(HempdustryHooks::onAllowSmoke);
        NeoForge.EVENT_BUS.addListener(HempdustryHooks::onAfterSmoke);
        NeoForge.EVENT_BUS.addListener(HempdustryHooks::onAfterEat);
        NeoForge.EVENT_BUS.addListener(HempdustryHooks::onAfterInfuse);
    }

    private static void onAllowSmoke(AllowSmokeEvent event) {
        if (event.getPlayer().level().dimension().equals(Level.NETHER)) {
            event.setCanceled(true);
        }
    }

    private static void onAfterSmoke(AfterSmokeEvent event) {
        event.getPlayer().displayClientMessage(Component.literal(
                "Dose " + event.getContents().dose() + " of ").append(event.getContents().loadName()), true);
    }

    private static void onAfterEat(AfterEatEvent event) {
        event.getPlayer().displayClientMessage(Component.literal(
                "Tier " + event.getTier() + ", " + event.getQuality().getSerializedName()), true);
    }

    private static void onAfterInfuse(AfterInfuseEvent event) {
        // event.getStrength(): 1-24 hemp; event.getQuality(): the grade
    }
}

The types the events hand you

The events pass three of the mod's own types. They are covered by the same promise as the events (below).

com.warlonmhite.hempdustry.item.custom.SmokeContents — what is loaded in a spliff, pipe, bong, vaporizer or moon rock; the value of the hempdustry:smoke_contents component.

Member Gives
entries() the list of SmokeContents.Entry(Holder<Strain> strain, int count): each strain and its dose. One entry, or two for a bud with a pinch of hash or a moon rock
dose() the total dose, every entry together
primaryStrain(), primaryCount() the entry with the most (a tie goes to the plant) and its count — the strain the item is named and tinted after. null and 0 only for an empty load, which no event carries
hashAdditive() Optional of the strain with no plant riding with a plant strain, if there is one
isEmpty(), isBlend() nothing loaded; more than one entry
color() the load's tint, the entries' colours weighted by count
loadName() the name of what is loaded, as a translatable Component
effects(int durationTicks, boolean onExhale, RandomGenerator random) fresh effect instances for one hit of this load, before the server's config
buffBonus() 1 when a bud is loaded with a concentrate, else 0
SmokeContents.of(…), EMPTY, CODEC, PACKET_CODEC building one, and its codecs

com.warlonmhite.hempdustry.strain.Strain — one strain, a record in the hempdustry:strain registry. Its components are the JSON fields of a strain file: translationKey(), color(), modelIndex(), seedItem() (Optional<Item>), buds(), wildFlower() (Optional<Block>), greenOutFactor(), dosePerItem(), smokeEffects() (a list of Strain.SmokeEffect) and coughFactor(). wildFlower().isPresent() is the mod's own test for "this strain grew on a plant"; hash, rosin, scorched hemp and schwag answer no. Strain.SmokeEffect is effect(), baseAmplifier(), scales(), durationFactor(), onExhale(), chance() and maxAmplifier().

com.warlonmhite.hempdustry.item.custom.Quality — a Cannabutter grade: ROUGH, STANDARD, CLEAN or PERFECT, lowest first. getSerializedName() is the name in data ("rough" …), getTranslationKey() its lang key, next() the grade above (null at the top). The potency tier is a plain int, 1–4.

The 2.0 bridges

2.1 made a strain's seeds and flower optional, because the hash family has neither. So that an addon built against 2.0 still links, 2.0's members are still there, deprecated:

2.0 member (deprecated) Says Use instead
Strain.seeds() the seed item, or Items.AIR for a strain with no plant — never null seedItem(), an Optional<Item>
Strain.flower() the wild flower block, or Blocks.AIR wildFlower(), an Optional<Block>
new Strain(key, color, modelIndex, seeds, buds, flower, effects) a plant strain with every 2.1 field at its default the canonical constructor
Strain.effects(int dose, int durationTicks), SmokeContents.effects(int durationTicks) the whole hit — the hit's and the exhale's effects together, every effect with a chance taken as landing (a chance of zero still never does) effects(…, boolean onExhale, RandomGenerator)

The older constructors of Strain.SmokeEffect are kept too. New code should use the 2.1 members.

Strains from code

Strains are a synced datapack registry, Strain.REGISTRY_KEY (hempdustry:strain), loaded when a world opens and sent to every client. Read them through the world's registry access, never cache them across worlds, and remember a datapack may have changed or added any of them.

import com.warlonmhite.hempdustry.strain.Strain;
import net.minecraft.core.Holder;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;

static final ResourceKey<Strain> PURPLE_KUSH =
        ResourceKey.create(Strain.REGISTRY_KEY, Identifier.fromNamespaceAndPath("hempdustry", "indica"));

// level is any Level, or a server's registryAccess()
Optional<Holder.Reference<Strain>> kush = Strain.registry(level.registryAccess()).get(PURPLE_KUSH);
List<Holder.Reference<Strain>> every = Strain.all(level.registryAccess());     // sorted: model_index, then id
Optional<Holder.Reference<Strain>> fromItem = Strain.fromBuds(level.registryAccess(), stack.getItem());

// In AFTER_SMOKE: did this hit hold Purple Kush?
boolean kushHit = contents.entries().stream().anyMatch(entry -> entry.strain().is(PURPLE_KUSH));

The strain ids are on Datapacks — indica is Purple Kush, sativa Lemon Haze, beldia Beldía. Build the keys yourself as above: the mod's own constants (ModStrains) are internal.

Shipping strains of your own is data, even from a mod: put the JSON in your jar at data/<your mod id>/hempdustry/strain/<id>.json, pointing at your own items (Datapacks). Your strain packs into every device with no code. To give its spliff art of its own, use a model_index of 100 or more and ship the model, the texture and an entry in the spliff's item definition (Resource Packs); 1–99 are Hempdustry's, and the log warns about a clash.

Item components from code

The component ids are frozen; the class that registers them, ModComponents, is not. To read a component without depending on internals, look its type up by id once the game has started:

import com.warlonmhite.hempdustry.item.custom.SmokeContents;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.item.ItemStack;

@SuppressWarnings("unchecked")
static SmokeContents smokeContents(ItemStack stack) {
    return BuiltInRegistries.DATA_COMPONENT_TYPE
            .getOptional(Identifier.fromNamespaceAndPath("hempdustry", "smoke_contents"))
            .map(type -> stack.get((DataComponentType<SmokeContents>) type))
            .orElse(SmokeContents.EMPTY);
}

It answers SmokeContents.EMPTY for a stack with nothing loaded. On Minecraft 1.21.1 the id is a ResourceLocation (ResourceLocation.fromNamespaceAndPath). The six components, their value types and what carries them are on Datapacks: smoke_contents (SmokeContents), charges (Integer), cooldown_until (Long), strength (Integer), quality (Quality) and potency (Integer).

Ids, tags and data

Much of what an addon wants needs no code at all, and these are as stable as the API:

  • Registry ids — hempdustry:indica_buds, hempdustry:hemp_press, hempdustry:smoke … — never change or go away. Refer to the mod's content by id rather than by its Java fields.
  • Tags — another mod's milk in #hempdustry:milk_buckets, a forge or crucible in #hempdustry:heat_sources, a knife in #hempdustry:hash_cutters, a lamp in #hempdustry:grow_lamps, siftable plant matter in #hempdustry:siftable/*, and the mod's own items in the c: convention tags. The full list: Tags.
  • Recipe types — hempdustry:decarboxylating, infusing and pressing take your items by recipe, the crafting types infused_shaped and infused_shapeless make your own edibles carry potency and quality (Datapacks).
  • Advancement triggers — hempdustry:smoke and hempdustry:harvest_hemp, filterable by strain, device, light and bed (Datapacks).
  • Balance data — devices, edibles and the green-out are datapack files (Datapacks).

What you can rely on

  • Frozen: the com.warlonmhite.hempdustry.api package, and the types its signatures name — SmokeContents (and its Entry), Strain (and its SmokeEffect) and Quality — with every public member they have. From the release that ships it, each is additive-only: members and events may be added, never removed or re-shaped, deprecated bridges included. A release is checked against the last one for exactly this before it goes out.
  • Within a loader. Fabric's HempdustryEvents has been frozen since 2.0; the NeoForge events are frozen from their first public build, 2.1.0-beta. The two are different classes and the promise is about each loader's own: a Fabric jar and a NeoForge jar are never interchangeable.
  • The same API on every Minecraft version, written for each one's names.
  • Internal: everything else. ModItems, ModBlocks, ModComponents, ModStrains, the smoking and edible code, the block entities, the platform layer — any of it may be renamed or rewritten in any release. Please don't mixin into it: a refactor on our side would then break your mod, which is the failure the API exists to prevent. If you need a hook the API does not have, open an issue and ask for it.

What is frozen beyond code — ids, block states, components, data formats, asset paths — is on Stability Promise.

See also: Datapacks · Stability Promise · Building from Source · Tags

Clone this wiki locally