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

FFUtilities — Modding API

FFUtilities is a shared library for Fabric, NeoForge and Forge. Package root: com.fyxe.ffutilities.api. The same API is built for every target below; gameplay mods bundle the jar for their target with Jar-in-Jar, so players do not need a separate download. Keep the required ffutilities dependency in your loader metadata (neoforge.mods.toml, mods.toml or fabric.mod.json); the embedded mod satisfies it.

Each target is published to the Fyxe Gitea Maven registry (or a sibling local publication) as com.fyxe.ffutilities:ffutilities-<minecraft>-<loader>:<version>:

MinecraftLoadersArtifacts
1.20.1Fabric, Forgeffutilities-1.20.1-fabric, ffutilities-1.20.1-forge
1.20.6Fabric, NeoForgeffutilities-1.20.6-fabric, ffutilities-1.20.6-neoforge
1.21.1Fabric, NeoForgeffutilities-1.21.1-fabric, ffutilities-1.21.1-neoforge (also published as ffutilities, the coordinate used before 0.2.8)
1.21.11Fabric, NeoForgeffutilities-1.21.11-fabric, ffutilities-1.21.11-neoforge

Gradle Kotlin DSL, one target per build script (ffutilities holds the pinned version, for example 0.2.8):

// NeoForge (ModDevGradle)
val ffutilitiesJar = "com.fyxe.ffutilities:ffutilities-1.21.1-neoforge:$ffutilities"
implementation(ffutilitiesJar)
jarJar(ffutilitiesJar) {
    version {
        strictly("[$ffutilities,0.3.0)")
        prefer(ffutilities)
    }
}

// Forge 1.20.1 (ModDevGradle Legacy: modImplementation remaps the SRG jar for development)
val ffutilitiesJar = "com.fyxe.ffutilities:ffutilities-1.20.1-forge:$ffutilities"
modImplementation(ffutilitiesJar)
jarJar(ffutilitiesJar) {
    version {
        strictly("[$ffutilities,0.3.0)")
        prefer(ffutilities)
    }
}

// Fabric (Loom). Forge Config API Port comes along as a dependency, from
// maven("https://raw.githubusercontent.com/Fuzss/modresources/main/maven/")
val ffutilitiesJar = "com.fyxe.ffutilities:ffutilities-1.21.1-fabric:$ffutilities"
modImplementation(ffutilitiesJar)
include(ffutilitiesJar)

In a Groovy build script the NeoForge form jarJar(implementation("...")) { version { ... } } also works.

Differences between targets

Core utilities

ClassPublic helpersUse
Idsof(namespace,path), of(id), tryParse(raw), vanilla(path), ofKey(resourceKey)Build or parse resource IDs; vanilla and ofKey (0.2.11) give a minecraft: ID and the ID behind a registry key.
ModsisLoaded, ifLoaded, getIfLoadedGuard optional integrations.
SoftReflectfindClass, findMethod, findFirstMethod, findAccessibleMethod, enumConstant, invokeStatic, invokeReflect into optional classes. 0.2.11: the first of several renamed methods, a method you can call on another mod's non-public implementation class, and enum constants by name.
compat.BetterCombatisLoaded, isWeapon(stack), isHoldingWeapon(player), weaponCategory(stack), comboCount(player)Whether Better Combat swings an item, its weapon category ("dagger", "claymore") and the swing index of a player's current combo, read through reflection; false, empty or 0 without Better Combat (0.2.10).
compat.EpicFightisLoaded, isEpicFightDamage(source), usedItem(source), attackAnimation(source), weaponCategory(stack), weaponPosition(source, target)What an Epic Fight attack hit with, read through reflection: the item of the hand that swung (the off-hand item for an off-hand swing), the animation's ID, Epic Fight's weapon category in lower case ("longsword", "dagger"; empty for no weapon) and where the phase's collider was when it hit, in world coordinates. Empty or false without Epic Fight or for other damage (0.2.12).
compat.AccessoriesisAnyLoaded, equippedStacks(entity), hasAny(entity)Stacks worn in Accessories, Curios or Trinkets slots, read through reflection. Only the first installed one is read (Accessories, then Curios, then Trinkets), because Accessories' compatibility layers show its slots through the other two APIs (0.2.11).
compat.ToughAsNailsisLoaded(), isTemperatureEnabled(), playerTemperature(player), temperatureAt(level, pos)Read-only reflection bridge (0.2.14). Returns Optional<ToughAsNails.TemperatureLevel> with ICY, COLD, NEUTRAL, WARM or HOT. Player reads the current body level. Position reads TAN's local level. Empty for absent, disabled or unavailable APIs and unknown categories. No TAN dependency; these are categories, not degrees. The enabled setting is checked on every call.
compat.SablerefreshInventoryMass(subLevel, contentsMass)Optional Sable 2.x bridge (0.2.14). Call on the server thread immediately before ServerSubLevel.updateMergedMassData. The ToDoubleFunction<BlockEntity> callback returns contents mass in Sable units. Adds deltas to the existing self tracker at container block centers; Sable merges and uploads mass and inertia afterward. Tracker rebuilds start afresh. Only loaded, solid plot block inventories are included; passengers and detached kinematic contraptions are excluded. API checked against Sable 2.0.5. Changed APIs log once and disable the bridge.
compat.ColdSweatisLoaded, bodyTemperature(entity), worldTemperature(entity), temperatureAt(level, pos)Cold Sweat temperatures in its own units as an OptionalDouble, empty without Cold Sweat (0.2.11).
PlayerUtilisSurvivalLike, isGameplayTarget, profileName, blockReachApply survival/creative targeting rules; the account name and block reach on every version (0.2.11).
WeatherUtilprecipitationAt, precipitationOn, isRainingOn, isSnowingOnTell whether rain or snow falls on a block or entity from open sky (0.2.6).
DebugLoginfo, debugLog only when the supplied toggle is enabled.
ResourceLocation id = Ids.of("mymod", "widget");
if (Mods.isLoaded("someothermod")) {
    // Access optional integration code only after the load check.
}
// 0.2.10: Better Combat's weapon category, with no dependency on Better Combat.
String category = BetterCombat.weaponCategory(player.getMainHandItem()).orElse("none");
if (BetterCombat.isHoldingWeapon(player)) {
    int swing = BetterCombat.comboCount(player); // 0 for the first swing of a combo
}
// 0.2.14: optional temperature categories (not degrees).
ToughAsNails.playerTemperature(player).ifPresent(level -> {
    // Apply your mod's own rules for ICY, COLD, NEUTRAL, WARM or HOT.
});
// import com.fyxe.ffutilities.api.compat.ToughAsNails;

// 0.2.12: an Epic Fight hit, read in a damage event: the item that swung and where its blade was.
ItemStack weapon = EpicFight.usedItem(source).orElse(attacker.getMainHandItem());
Optional<Vec3> blade = EpicFight.weaponPosition(source, victim);
// 0.2.6: rain or snow reaching an entity from open sky (top of hitbox, then feet).
if (WeatherUtil.isSnowingOn(entity)) {
    // Pile snow on it.
} else if (WeatherUtil.precipitationAt(level, pos) == Biome.Precipitation.RAIN) {
    // Rain falls on this block.
}

Version-proof wrappers (0.2.11)

These wrap vanilla code that changed between Minecraft 1.20.1 and 1.21.11, so a mod calls one method instead of writing its own Stonecutter conditional. Each behaves the same on every target.

ClassPublic helpersWhat changed between versions
nbt.NbtUtilgetInt, getLong, getFloat, getDouble, getBoolean, getString (each with an optional fallback), getCompound, getCompounds, getStrings, getDoubles, keys1.21.5 made CompoundTag getters return Optionals. A missing key or a value of the wrong type gives the fallback everywhere.
entity.AttributeUtilholder(id), exists, instance(entity, id or holder), operation(index), modifier, getModifier, hasModifier, modifierAmount, setTransient, setPermanent, removeModifier, amount, modifierUuidAttributes moved behind holders in 1.20.5 and modifiers are keyed by ID from 1.21. Before 1.21 a modifier is keyed by modifierUuid(id). setTransient and setPermanent replace a modifier with the same ID instead of throwing.
entity.EffectUtilholder(id), exists, newInstance, add, get, has, removeMob effects moved behind holders in 1.20.5. An effect ID that is not registered counts as absent.
command.CommandUtilhasPermission(source, level), isGameMaster, requires(level)1.21.11 replaced numeric permission levels. Levels stay 1 to 4 (MODERATOR, GAME_MASTER, ADMIN, OWNER).
player.InventoryUtilmainItems, armorItems (feet first), offhandItems, allItems, selectedSlot, containedItems(stack), blockItems(blockEntity) (0.2.14)Block inventories read physical Container slots first, then unsided Forge/NeoForge capabilities or Fabric block storage, without combining adjacent chests or counting multiple sides. Unsupported storage returns an empty list. 1.21.5 replaced the inventory's list fields; 1.20.5 moved shulker box and bundle contents to components. On 1.20.1 a shulker box that never held anything has no contents (empty Optional); newer versions give an empty list.
item.FoodUtilfood(stack), isFoodFood became a component in 1.20.5 with absolute saturation; FoodValues always holds the absolute amount (bread: 5 nutrition, 6.0 saturation).
world.WorldUtilminY, hasMoon, moonPhase, isFullMoonThe build height getter was renamed in 1.21.2; 1.21.11 moved the moon into environment attributes. Moon phase 0 is the full moon.
client.ClientInputhasShiftDown, hasControlDown, hasAltDown, shows(tooltipMode)The key checks moved from Screen to Minecraft. Client only.
client.GuiUtilblitTexture, withScaledPose, tooltip1.21.11 blits take a render pipeline and tooltips are drawn at the end of the frame; 1.21.6 made the GUI pose 2D. Client only.
ResourceLocation tired = Ids.of("mymod", "tired");
AttributeInstance stamina = AttributeUtil.instance(player, Ids.of("somemod", "max_stamina"));
AttributeUtil.setTransient(stamina, tired, -0.2, AttributeUtil.operation(2)); // null-safe, replaces an older one
EffectUtil.add(player, Ids.vanilla("slowness"), 100, 0);
int level = NbtUtil.getInt(tag, "level", 1);
int nutrition = FoodUtil.food(stack).map(FoodUtil.FoodValues::nutrition).orElse(0);
dispatcher.register(Commands.literal("mymod").requires(CommandUtil.requires(CommandUtil.GAME_MASTER)));

On 1.21.11 the type is called Identifier instead of ResourceLocation.

Config and data

ConfigValidators validates resource locations, tags, registry IDs, weighted entries, keyed numbers, booleans, nonblank strings, and hex colors (hexColor, 0.2.10). KeyedEntries.split separates a config entry at the last occurrence of a chosen character. Blank lines and comments (# followed by whitespace, or ##) are skipped, while tag keys such as #minecraft:skeletons=#6E5134 are kept. ConfigLists parses lists into maps of string or resource ID keys to doubles, integers, or booleans; freezeStringDoubles, freezeStringInts, and freezeStringBooleans return immutable snapshots.

ConfigValidators.keyed(separator, keyCheck, valueCheck) (0.2.11) builds a validator for your own key=value lines, with ConfigValidators.isDouble and isInt for values: keyed('=', RegistryMatcher::isValid, ConfigValidators::isDouble) accepts #minecraft:logs=2.5. ConfigValidators.expression("speed", "weight") accepts equations that parse and use only those variables.

RegistryUtil.item, .block, and .fluid return Optional lookups by ID. SimpleCodecReloadListener<T> is a base for datapack reload listeners backed by a codec; loaded() returns the current keyed entries. Keep domain-specific rules in the gameplay mod.

RegistryFilter, RegistryValues and RegistryMatcher (0.2.5, api.registry) read config entries written as an ID (minecraft:cow), a tag (#minecraft:logs) or a whole namespace (somemod:*). A filter's blacklist always wins and an empty whitelist allows everything; keyed values return the most specific match (ID, then tag, then namespace). Tag membership is read live, so datapack reloads apply. ConfigValidators.registryMatcher validates the entry format.

IdKeywords<V> (0.2.10, api.registry) maps words in IDs to values from config lines such as dagger=stab, for guessing what an unlisted modded item is. Names are split into words at anything other than a letter or digit; a keyword matches a word that equals it or ends with it (sword matches iron_longsword), a keyword with underscores matches those words in a row, and the longest keyword wins (pickaxe beats axe). IdKeywords.isKeyword validates keys.

IdKeywords<String> kinds = IdKeywords.parse(List.of("dagger=stab", "axe=chop", "pickaxe=dig"), '=', v -> v);
kinds.match(Ids.of("somemod", "iron_greataxe")); // Optional[chop]
kinds.match("diamond_pickaxe");                   // Optional[dig]
RegistryFilter<Block> blocks = RegistryFilter.of(Registries.BLOCK, WHITELIST.get(), BLACKLIST.get());
RegistryValues<Block, Double> mess = RegistryValues.parse(Registries.BLOCK, MULTIPLIERS.get(), '=', Double::valueOf);
if (blocks.allows(state.getBlockHolder())) {
    double multiplier = mess.getOrDefault(state.getBlockHolder(), 1.0); // "minecraft:mud=3.0", "#minecraft:dirt=1.25"
}

Math, time, and selection

MathUtil provides numeric clamp, lerp, inverseLerp, and stageIndex, plus (0.2.11) remap, remapClamped, approach (move toward a target by at most a step) and smoothstep. WeightedRandom.pick(items, weightFn, random) selects by integer weights. TickGate.every and everyStaggered support periodic work without a timer per entity.

ExpressionEvaluator.evaluate(expression, vars) evaluates configurable arithmetic with named variables (keys in lower case) and returns NaN when the text is malformed or a variable is missing. It supports + - * / %, ^ (power, right to left, so -2^2 is -4), parentheses and the functions abs, min, max (any number of arguments), clamp(x, low, high), pow, sqrt, floor, ceil and round. From 0.2.11, compile parses once into a reusable Expression with evaluate(vars) and variables(), tryCompile returns an Optional, and isValid(expression, allowedVariables) checks config input. Text after a complete expression (1 2) is now an error.

// Parse when the config loads, evaluate every tick.
Expression drain = ExpressionEvaluator.compile(DRAIN_EQUATION.get()); // "base * (1 + weight / 100) ^ 2"
double value = drain.evaluate(Map.of("base", 0.5, "weight", 40.0)); // 0.98

TimeFormat formats seconds or ticks as minutes and seconds, and (0.2.11) duration / ticksAsDuration give the two largest units: 45s, 2m 5s, 1h 2m, 3d 4h. TextUtil (0.2.11) has roman (4 is IV, 1 to 3999), decimal(value, maxDecimals) and percent (no trailing zeros, always a dot: decimal(2.50, 2) is 2.5), and copyable(text) / copyOnClick(style, text) for chat text that copies itself when clicked.

ColorUtil.parseHex reads #RRGGBB (opaque) or #AARRGGBB with a fallback color; ColorUtil.tryParseHex (0.2.10) returns an empty OptionalInt for malformed input instead, and ColorUtil.isHex checks a value. 0.2.11 adds toHex(argb, withAlpha), channel getters alpha, red, green, blue, builders argb, rgb, withAlpha, and mixing with fadeAlpha, lerp, scaleRgb and multiply (straight-alpha 0xAARRGGBB, channels clamped).

HUD and client helpers

HudPlacement.anchorX, anchorY, and relativeText calculate positions from anchor enums, offsets, and layout; clusterOffset(index, spacing, orientation, direction) (0.2.11) gives the {dx, dy} of the n-th icon in a row or column. ClientInput and GuiUtil (above) cover modifier keys, texture blits, scaled drawing and tooltips. HudTextStyle and related enums describe bold, outline, shadow, alignment, and expansion. HudTextRenderer.draw and ConfigScreens.register (see above) live under api.client; access those only from client code. TooltipMode.shouldShow(shiftDown) controls tooltip visibility.

FFUtilities does not define injury, spoilage, rarity, or encumbrance rules. Use each gameplay mod's own API for those features.

Sable cargo mass

A consumer calls the bridge from its optional Sable server physics hook, immediately before ServerSubLevel.updateMergedMassData. This example supplies total stack weight with FFItemWeight's public API. FFItemWeight already installs this integration automatically; do not register another weight provider for the same body.

// import com.fyxe.ffutilities.api.compat.Sable;
// import com.fyxe.ffutilities.api.player.InventoryUtil;
// import com.fyxe.ffitemweight.api.ItemWeightAPI;
Sable.refreshInventoryMass(subLevel, blockEntity ->
    InventoryUtil.blockItems(blockEntity).stream()
        .mapToDouble(ItemWeightAPI::getWeight).sum());