This guide describes the public API in the current source version 0.4.0. Use the dependency guidance below and check that the mod is loaded when integrating conditionally.

FFSoilFertility — Modding API

Guide for other mods (and AI agents) integrating with FFSoilFertility (ffsoilfertility).

Mod IDffsoilfertility
API packagecom.fyxe.ffsoilfertility.api (plus com.fyxe.ffsoilfertility.SoilQuality and SoilParams)
LoadersFabric, Forge (1.20.1) and NeoForge; see the Overview for the supported Minecraft versions
Soil qualitySoilQuality.LOW, NORMAL, HIGH

Dependency (optional, NeoForge neoforge.mods.toml):

[[dependencies.yourmod]]
    modId="ffsoilfertility"
    type="optional"
    versionRange="[0.4.0,)"
    ordering="NONE"
    side="BOTH"

On Fabric, list "ffsoilfertility": ">=0.4.0" under suggests (or depends when required) in fabric.mod.json. Forge 1.20.1 (mods.toml) uses mandatory=false in place of type="optional". The API first shipped in 0.4.0. Each target is published as com.fyxe.ffsoilfertility:ffsoilfertility-<minecraft>-<loader>:<version>. Check ModList.get().isLoaded("ffsoilfertility") on NeoForge or Forge, FabricLoader.getInstance().isModLoaded("ffsoilfertility") on Fabric, or FFUtilities' Mods.isLoaded, before touching API classes.


Core concepts


Reading soil quality

import com.fyxe.ffsoilfertility.SoilQuality;
import com.fyxe.ffsoilfertility.api.SoilFertilityAPI;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;

boolean on = SoilFertilityAPI.isEnabled();                 // master config switch
boolean active = SoilFertilityAPI.isActive(level);         // this side has a soil field
SoilQuality q = SoilFertilityAPI.getQuality(level, pos);   // server: with modifiers; client: base only
SoilQuality base = SoilFertilityAPI.getBaseQuality(level, pos);   // ignores modifiers
double fertility = SoilFertilityAPI.getFertility(level, pos);     // std deviations; below 0 is poorer

getFertility includes the biome's climate bias. Low quality is at or below the field's low threshold and High at or above its high threshold. getParams(level) returns the SoilParams record the field was built from, or null when isActive is false.

MethodReturnsNotes
isEnabled()booleanQuality can still be read while the mod is disabled
isActive(Level)booleanFalse means every query reports NORMAL
getParams(Level)SoilParamsNull when not active
getQuality(Level, BlockPos)SoilQualityNever null
getBaseQuality(Level, BlockPos)SoilQualityNever null
getFertility(Level, BlockPos)double0 when not active

Growth rules

import java.util.OptionalInt;

double mult = SoilFertilityAPI.getGrowthMultiplier(SoilQuality.HIGH);   // config value for a quality
double here = SoilFertilityAPI.getGrowthMultiplier(level, pos);        // for the soil at pos
OptionalInt limit = SoilFertilityAPI.getBonemealStageLimit(SoilQuality.LOW);   // empty = not limited
boolean affected = SoilFertilityAPI.isAffected(level.getBlockState(pos));
boolean crops = SoilFertilityAPI.affectsCrops();
boolean saplings = SoilFertilityAPI.affectsSaplings();

A multiplier of 1 is vanilla, below 1 skips random ticks and above 1 adds extra ticks. isAffected is true when soil quality changes how that block grows (crops and saplings, per the config and block tags).


Area queries

import java.util.Map;
import java.util.Optional;

// Count qualities on a grid: radius 64, one sample every 8 blocks
Map<SoilQuality, Integer> counts = SoilFertilityAPI.sampleArea(level, pos, 64, 8);
int highColumns = counts.get(SoilQuality.HIGH);

// Nearest High soil within 128 blocks, searched on a 4 block grid
Optional<BlockPos> nearest = SoilFertilityAPI.findNearest(level, pos, SoilQuality.HIGH, 128, 4);

Both use getQuality, so modifiers count on the server. radius is clamped to MAX_SEARCH_RADIUS (256), and step is raised when needed so at most MAX_SAMPLES (16384) positions are read. Every quality is a key of the sampleArea result. findNearest returns a grid position at the origin's Y, up to step / 2 blocks from the true nearest column, or empty when nothing was found.


Quality modifiers (server)

A SoilQualityModifier adjusts the quality the server uses for gameplay: growth speed, bone meal limits, particles and the server-side getQuality. Modifiers run in registration order. Return the given quality (or null) to leave it as is. They run on every quality lookup, including every random tick of an affected block, so keep them cheap. They do not change the grass tint or client queries.

import com.fyxe.ffsoilfertility.SoilQuality;
import com.fyxe.ffsoilfertility.api.SoilFertilityAPI;
import com.fyxe.ffsoilfertility.api.SoilQualityModifier;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;

// Soil within one block of a composter is always High
SoilQualityModifier nearComposter = (level, pos, quality) -> {
    if (quality == SoilQuality.HIGH) return quality;
    BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
    for (int dx = -1; dx <= 1; dx++) {
        for (int dz = -1; dz <= 1; dz++) {
            cursor.set(pos.getX() + dx, pos.getY(), pos.getZ() + dz);
            if (level.hasChunkAt(cursor) && level.getBlockState(cursor).is(Blocks.COMPOSTER)) {
                return SoilQuality.HIGH;
            }
        }
    }
    return quality;
};

SoilFertilityAPI.registerQualityModifier(nearComposter);
// Later, if needed:
SoilFertilityAPI.unregisterQualityModifier(nearComposter);

Events

All events extend SoilEvent. On every loader listen with SoilApiEvents.register(EventType.class, listener); listeners run in registration order on the thread that posts the event. On NeoForge the same events are also posted on NeoForge.EVENT_BUS (SoilEvent extends NeoForge's Event). Fabric and Forge have no separate bus, so use SoilApiEvents there. Cancellable events implement Cancellable, which is NeoForge's ICancellableEvent on NeoForge and a plain interface with isCanceled / setCanceled elsewhere.

Fabric

import com.fyxe.ffsoilfertility.api.SoilApiEvents;
import com.fyxe.ffsoilfertility.api.SoilGrowthEvent;
import net.fabricmc.api.ModInitializer;

public class YourMod implements ModInitializer {
    @Override
    public void onInitialize() {
        SoilApiEvents.register(SoilGrowthEvent.class, event -> {
            if (event.getLevel().isRaining()) {
                event.setGrowthMultiplier(event.getOriginalMultiplier() * 1.5);
            }
        });
    }
}

Forge (1.20.1)

import com.fyxe.ffsoilfertility.api.SoilApiEvents;
import com.fyxe.ffsoilfertility.api.SoilGrowthEvent;
import net.minecraftforge.fml.common.Mod;

@Mod("yourmod")
public class YourMod {
    public YourMod() {
        // Forge does not receive these on its event bus; register with SoilApiEvents.
        SoilApiEvents.register(SoilGrowthEvent.class, event -> {
            if (event.getLevel().isRaining()) {
                event.setGrowthMultiplier(event.getOriginalMultiplier() * 1.5);
            }
        });
    }
}

NeoForge

import com.fyxe.ffsoilfertility.SoilQuality;
import com.fyxe.ffsoilfertility.api.SoilApiEvents;
import com.fyxe.ffsoilfertility.api.SoilGrowthEvent;
import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.common.NeoForge;

@Mod("yourmod")
public class YourMod {
    public YourMod() {
        // Either form works on NeoForge.
        SoilApiEvents.register(SoilGrowthEvent.class, event -> {
            if (event.getLevel().isRaining()) {
                event.setGrowthMultiplier(event.getOriginalMultiplier() * 1.5);
            }
        });
        NeoForge.EVENT_BUS.addListener((SoilGrowthEvent event) -> {
            if (event.getQuality() == SoilQuality.LOW && event.getLevel().isNight()) {
                event.setCanceled(true);
            }
        });
    }
}
EventCancellableSideWhen
SoilGrowthEventYesServerBefore each affected block's random tick
SoilBonemealEventNoServerBone meal is about to advance an affected crop
SoilGrowthParticlesEventYesServerAn affected block just grew and particles are enabled for its quality
SoilFieldChangedEventNoServer and clientA soil field was built with different parameters

SoilGrowthEvent

Fired before an affected block gets its random tick. Cancel to skip this tick entirely, or change the multiplier for just this tick (below 1 is the chance the tick happens, above 1 adds extra ticks). Not fired for the extra ticks the mod itself adds on high quality soil, nor while the mod is disabled.

Getter / setterMeaning
getLevel() / getPos() / getState()ServerLevel, block position and state
getQuality()Soil quality, after modifiers
getOriginalMultiplier()The multiplier the config gives this quality
getGrowthMultiplier() / setGrowthMultiplier(double)Current value; setter clamps to 0 to 32 and ignores NaN and infinities

SoilBonemealEvent

Fired when bone meal (a player, a dispenser or any other caller of the crop code) is about to advance an affected crop, with the stages after the bone meal limit. Not fired while the mod is disabled.

SoilApiEvents.register(SoilBonemealEvent.class, event -> {
    // Lift the limit on High soil
    if (event.getQuality() == SoilQuality.HIGH) {
        event.setStages(event.getVanillaStages());
    }
});
Getter / setterMeaning
getLevel() / getPos() / getState() / getQuality()Where and on what soil
getVanillaStages()Stages vanilla rolled, before any limit
getLimitedStages()Stages after the config limit, before listeners
getStages() / setStages(int)Result; setter clamps to 0 to 64

SoilGrowthParticlesEvent

Fired when an affected block just grew and the config has particles enabled for its quality. Cancel to show none, or replace the particle and count.

import net.minecraft.core.particles.ParticleTypes;

SoilApiEvents.register(SoilGrowthParticlesEvent.class, event -> {
    if (event.getQuality() == SoilQuality.HIGH) {
        event.setParticle(ParticleTypes.HAPPY_VILLAGER);
        event.setCount(8);
    }
});

Getters: getLevel(), getPos(), getQuality(), getParticle() and getCount(). setParticle(SimpleParticleType) ignores null; setCount(int) clamps to 0 to 256.

SoilFieldChangedEvent

Fired after a soil field was built with parameters different from the previous one: on the server when a world starts or the config changes the soil settings, and on the client when the server's parameters arrive. Quality at every position may have changed, so drop cached results. Check isClientSide() to tell which thread it runs on.

SoilApiEvents.register(SoilFieldChangedEvent.class, event -> {
    if (event.isClientSide()) {
        myClientCache.clear();
    }
    SoilParams params = event.getParams();   // com.fyxe.ffsoilfertility.SoilParams
});

Stability