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 ID | ffsoilfertility |
| API package | com.fyxe.ffsoilfertility.api (plus com.fyxe.ffsoilfertility.SoilQuality and SoilParams) |
| Loaders | Fabric, Forge (1.20.1) and NeoForge; see the Overview for the supported Minecraft versions |
| Soil quality | SoilQuality.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
- Quality is a function of position. It depends on the block's X and Z, the biome and the synced
SoilParams, so it can be read on both sides. It never loads chunks. - Server and client differ. The server value includes registered
SoilQualityModifiers; the client value never does (it drives the grass tint and hoe readout). - No field, no data. When a side has no soil field (no world started, or a client on a server without the mod), every query reports
NORMALandgetFertilityreturns 0. CheckisActive(level)first. - Call the API on the thread that owns the level.
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.
| Method | Returns | Notes |
|---|---|---|
isEnabled() | boolean | Quality can still be read while the mod is disabled |
isActive(Level) | boolean | False means every query reports NORMAL |
getParams(Level) | SoilParams | Null when not active |
getQuality(Level, BlockPos) | SoilQuality | Never null |
getBaseQuality(Level, BlockPos) | SoilQuality | Never null |
getFertility(Level, BlockPos) | double | 0 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);
}
});
}
}
| Event | Cancellable | Side | When |
|---|---|---|---|
SoilGrowthEvent | Yes | Server | Before each affected block's random tick |
SoilBonemealEvent | No | Server | Bone meal is about to advance an affected crop |
SoilGrowthParticlesEvent | Yes | Server | An affected block just grew and particles are enabled for its quality |
SoilFieldChangedEvent | No | Server and client | A 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 / setter | Meaning |
|---|---|
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 / setter | Meaning |
|---|---|
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
- Package
com.fyxe.ffsoilfertility.api,SoilQualityand theSoilParamsrecord are public API. SoilFertilityAPI.applyModifiersis public only for the mod's own lookup; do not call it.- Config classes,
FFSoilFertilityMod,SoilField, mixins and the platform packages are internal.