Mercantile

Developers

Mercantile exposes a stable, read-only API and two server-side events so other mods can read the villager economy — reputation, trade locks, sentry golems — without a hard dependency. Every read accessor is server-side authoritative.

Soft Dependency Setup

The API lives in com.rfizzle.mercantile.api — the only package another mod should reference. Everything outside it is internal and may change in any release. Every type in the package carries Mercantile's own @Stable marker (a local annotation per the Concord API Standard v1 no-shared-jar rule, not a shared library type): its signature survives minor and patch releases.

Compile against Mercantile with modCompileOnly so it stays optional at runtime, then guard every call with a mod-loaded check.

Gradle:

dependencies {
    modCompileOnly "maven.modrinth:mercantile:<version>"
}

Guard calls so your mod loads cleanly whether or not Mercantile is present:

if (FabricLoader.getInstance().isModLoaded("mercantile")) {
    // safe to touch the Mercantile API here
}

Reading Reputation & Villager State

MercantileAPI is a static, read-only facade over Mercantile's server-side state. Nothing on it mutates that state, and every accessor is authoritative on the server only — the backing entity and player data attachments are internal, so consume this facade rather than reaching for them.

  • int getReputation(ServerPlayer player) — the player's current reputation score, clamped to [-200, 1500]. A player with no reputation history yet scores 0 (NEUTRAL).
  • ReputationTier getReputationTier(ServerPlayer player) — the standing derived from that score.
  • boolean isSentryGolem(Entity entity) — true for an iron golem spawned and maintained by a Sentry Pylon (temporary, pylon-bound, despawns when the pylon runs out of fuel).
  • boolean isProfessionLocked(Villager villager) — the villager-level lock. Mercantile locks a villager's profession permanently after its first trade, so it can no longer lose its trades by losing its workstation.
  • boolean isTradeLocked(Villager villager, MerchantOffer offer) — the per-offer lock. An offer locks the first time it is traded and stays locked when the villager's trade pool is cycled. Offers are matched by item/count identity, so an equivalent offer object matches the stored lock even if it is a different instance.
import com.rfizzle.mercantile.api.MercantileAPI;
import com.rfizzle.mercantile.api.ReputationTier;

if (FabricLoader.getInstance().isModLoaded("mercantile")) {
    int score = MercantileAPI.getReputation(serverPlayer);
    ReputationTier tier = MercantileAPI.getReputationTier(serverPlayer);

    // gate perks by comparing tiers, not raw scores
    if (tier.ordinal() <= ReputationTier.TRUSTED.ordinal()) {
        // TRUSTED or better (HONORED) — grant a bonus
    }

    boolean sentry = MercantileAPI.isSentryGolem(entity);
    boolean locked = MercantileAPI.isProfessionLocked(villager);
}

The ReputationTier Enum

ReputationTier is the six standings a player can hold, declared best to worst: HONORED, TRUSTED, LIKED, NEUTRAL, DISTRUSTED, REVILED. The constants, their relative ordering, and the enum's public methods are stable across minor and patch releases.

  • ReputationTier fromScore(int score) — the tier a given score falls into (the same mapping getReputationTier uses).
  • int minScore() — the lowest score in the tier's range.
  • Component displayName() — the localized standing name (Honored, Trusted, …) for display.
  • int priceModifierForScore(int score, int basePrice) — the reputation price adjustment for a base price: negative for a discount at the friendly tiers, positive for the markup that ramps across DISTRUSTED, zero at NEUTRAL.

Compare tiers, don't hardcode scores. Exact score thresholds are gameplay tuning and may shift in a minor release; the tier constants and their ordering will not. Gate your logic on the tier or on minScore(), never on a literal like 300.

Events

Two standard array-backed Fabric Events fire server-side only. A listener that throws is caught and logged by Mercantile — it cannot corrupt the reputation or trade flow, but a thrown exception may stop listeners registered after it from seeing that event.

ReputationChangedCallback.EVENTonReputationChanged(ServerPlayer player, int oldScore, int newScore) fires whenever a player's score actually changes. It is not invoked when a change is fully absorbed by score clamping, and not by the one-shot legacy save-format migration.

TradeExecutedCallback.EVENTonTradeExecuted(ServerPlayer player, AbstractVillager villager, MerchantOffer offer) fires after a completed trade with a villager or a wandering trader — after the merchant's use count, XP, and Mercantile's own bookkeeping have been applied. A bulk trade (shift-click) fires the event once per executed trade.

import com.rfizzle.mercantile.api.ReputationChangedCallback;
import com.rfizzle.mercantile.api.TradeExecutedCallback;

if (FabricLoader.getInstance().isModLoaded("mercantile")) {
    ReputationChangedCallback.EVENT.register((player, oldScore, newScore) -> {
        // react to the standing change (fires only on a real change)
    });

    TradeExecutedCallback.EVENT.register((player, villager, offer) -> {
        // villager is a Villager or WanderingTrader;
        // fires once per executed trade, bulk trades included
    });
}

HUD Coordination

Two accessors implement the Concord HUD-STANDARD §6 stacking contract so a lower-priority HUD element from another mod can offset itself past Mercantile's reputation badge. They are reflection-backed into the client overlay, so they are safe to call unconditionally from common code on either physical side.

  • boolean isHudVisible() — whether Mercantile's reputation HUD element is currently being drawn. Sentinel: false on a dedicated server, when the HUD is config-disabled, or when it is hidden (F1, an open screen, spectator, the death screen, no villager in range).
  • int getHudHeight() — the element's current height contribution in pixels for a lower slot to clear. Sentinel: 0 whenever isHudVisible() is false; 22 (a 20px standard element plus a 2px gap) when visible.

These are for rendering coordination only — the sentinels make them safe to read from anywhere, but never drive gameplay logic off them.

Notes

  • The API package is com.rfizzle.mercantile.api — everything outside it (attachments, managers, mixins) is internal and may change without notice.
  • Every read accessor on MercantileAPI is server-side authoritative; the reputation and trade-state reads are meaningless on a bare client.
  • ReputationChangedCallback and TradeExecutedCallback are standard Fabric Events, not a custom event bus.
  • A breaking change to any @Stable signature requires a major version bump plus a changelog entry naming the broken signature.

Building an integration and something's missing? Open an issue — the API surface grows based on real integration needs.