Skip to content

These docs were made completely by AI, so they might be right, or wrong, you'll need to test them yourself. This was made for a easier understanding of everything. So use at your own risk. If anything is wrong, please don't hurt to make a PR on the page you have a problem with. ON GITHUB

Custom Trades

This guide covers the villager trading system in LCE. We will look at how trade offers are built, how the Merchant interface works, every profession’s trade pool in detail, and how you can add your own trades or even create custom merchant entities.

Trading in LCE is built around a few key classes:

ClassRole
MerchantInterface that any tradeable entity implements
VillagerThe main entity that implements Merchant
MerchantRecipeA single trade offer (buy items in, sell item out)
MerchantRecipeListA list of recipes that a merchant offers
MerchantMenuThe container/UI for the trading screen
MerchantContainerHolds the 3 item slots (2 payment + 1 result)
MerchantResultSlotSpecial slot that handles payment deduction
ClientSideMerchantClient-side proxy used for rendering the trade UI

Any entity that wants to trade needs to implement the Merchant interface from Minecraft.World/Merchant.h:

class Merchant
{
public:
virtual void setTradingPlayer(shared_ptr<Player> player) = 0;
virtual shared_ptr<Player> getTradingPlayer() = 0;
virtual MerchantRecipeList *getOffers(shared_ptr<Player> forPlayer) = 0;
virtual void overrideOffers(MerchantRecipeList *recipeList) = 0;
virtual void notifyTrade(MerchantRecipe *activeRecipe) = 0;
virtual void notifyTradeUpdated(shared_ptr<ItemInstance> item) = 0;
virtual int getDisplayName() = 0;
};

Villager inherits from both AgableMob and Merchant:

class Villager : public AgableMob, public Npc, public Merchant

Each trade is a MerchantRecipe with up to 2 input items and 1 output item:

class MerchantRecipe
{
private:
shared_ptr<ItemInstance> buyA; // Required payment item
shared_ptr<ItemInstance> buyB; // Optional second payment item
shared_ptr<ItemInstance> sell; // What the player gets
int uses; // How many times this trade has been used
int maxUses; // Max uses before the trade locks (default: 7)
};

You can create recipes with different constructors:

// Simple: one item in, one item out
new MerchantRecipe(
shared_ptr<ItemInstance>(new ItemInstance(Item::emerald, 5)),
shared_ptr<ItemInstance>(new ItemInstance(Item::sword_diamond))
);
// Two items in, one item out
new MerchantRecipe(
shared_ptr<ItemInstance>(new ItemInstance(Item::book)),
shared_ptr<ItemInstance>(new ItemInstance(Item::emerald, 10)),
shared_ptr<ItemInstance>(new ItemInstance(Item::enchantedBook))
);
// With custom use limits
new MerchantRecipe(buyA, buyB, sell, 0 /* uses */, 20 /* maxUses */);

Trades have a limited number of uses. After uses >= maxUses, the trade is “deprecated” (locked out). The default max is 7 uses.

bool MerchantRecipe::isDeprecated()
{
return uses >= maxUses;
}

When a player buys the last item in a villager’s offer list, the villager starts a refresh timer. After about 2 seconds, it unlocks deprecated trades by adding more uses:

if (recipe->isDeprecated())
{
recipe->increaseMaxUses(random->nextInt(6) + random->nextInt(6) + 2);
}

This adds 2-12 extra uses to each locked trade.

Villagers have 5 professions, each with different trade pools:

ConstantValueSkin
PROFESSION_FARMER0Brown robe
PROFESSION_LIBRARIAN1White robe
PROFESSION_PRIEST2Purple robe
PROFESSION_SMITH3Black apron
PROFESSION_BUTCHER4White apron

When a player first interacts with a villager, getOffers() is called. If no offers exist yet, addOffers(1) generates them:

MerchantRecipeList *Villager::getOffers(shared_ptr<Player> forPlayer)
{
if (offers == NULL)
{
addOffers(1);
}
return offers;
}

The addOffers() method builds a full pool of possible trades based on profession, then picks from them. Here is how it works:

  1. Create a temporary newOffers list
  2. Add all possible trades for this profession (each with a random chance of appearing)
  3. Shuffle the list randomly
  4. Add addCount trades from the shuffled list to the villager’s permanent offers (only if they are new or better)
  5. If nothing was generated, fall back to a gold ingot trade-in

The addCount parameter is 1, meaning each call adds at most 1 new trade. The first time generates the full pool and picks 1. After that, each trade refresh adds 1 more.

Each trade in the pool has a base chance of appearing. The getRecipeChance() method applies a modifier and caps at 0.9:

float Villager::getRecipeChance(float baseChance)
{
float newChance = baseChance + baseRecipeChanceMod;
if (newChance > .9f)
{
return .9f - (newChance - .9f);
}
return newChance;
}

The baseRecipeChanceMod starts at 0 and can shift over time. If the modified chance would exceed 0.9, it wraps back down.

There are two types of trades:

Trade-In (addItemForTradeIn): Player gives items, gets 1 emerald. The quantity required comes from the MIN_MAX_VALUES table.

Purchase (addItemForPurchase): Player pays emeralds, gets an item. The cost comes from the MIN_MAX_PRICES table. Negative prices mean 1 emerald buys multiple items.

void Villager::addItemForPurchase(MerchantRecipeList *list, int itemId,
Random *random, float likelyHood)
{
if (random->nextFloat() < likelyHood)
{
int purchaseCost = getPurchaseCost(itemId, random);
shared_ptr<ItemInstance> rubyItem;
shared_ptr<ItemInstance> resultItem;
if (purchaseCost < 0)
{
// Negative: 1 emerald buys multiple items
rubyItem = shared_ptr<ItemInstance>(
new ItemInstance(Item::emerald_Id, 1, 0));
resultItem = shared_ptr<ItemInstance>(
new ItemInstance(itemId, -purchaseCost, 0));
}
else
{
// Positive: multiple emeralds buy 1 item
rubyItem = shared_ptr<ItemInstance>(
new ItemInstance(Item::emerald_Id, purchaseCost, 0));
resultItem = shared_ptr<ItemInstance>(
new ItemInstance(itemId, 1, 0));
}
list->push_back(new MerchantRecipe(rubyItem, resultItem));
}
}

Here is every trade for every profession, pulled directly from Villager::addOffers() in Villager.cpp.

Trade-Ins (player gives items, gets 1 emerald):

ItemQuantity per EmeraldBase Chance
Wheat18-2290%
Wool (any color)14-2250%
Raw Chicken14-1850%
Cooked Fish9-1340%

Purchases (player pays emeralds, gets items):

ItemPriceBase Chance
Bread1 emerald for 2-490%
Melon1 emerald for 4-830%
Apple1 emerald for 4-830%
Cookie1 emerald for 7-1030%
Shears3-4 emeralds30%
Flint & Steel3-4 emeralds30%
Cooked Chicken1 emerald for 6-830%
Arrow1 emerald for 8-1250%

Special Trade:

Input 1Input 2OutputChance
10 Gravel1 Emerald2-3 Flint50%

Trade-Ins:

ItemQuantity per EmeraldBase Chance
Paper24-3680%
Book11-1380%

Purchases:

ItemPriceBase Chance
Bookshelf3-4 emeralds80%
Glass1 emerald for 3-520%
Compass10-12 emeralds20%
Clock10-12 emeralds20%

Special Trade:

Input 1Input 2OutputChance
1 BookVariable emeraldsEnchanted Book7%

The enchanted book trade picks a random enchantment from Enchantment::validEnchantments at a random level between its min and max. The emerald cost is 2 + random(5 + level*10) + 3*level. So a level 1 enchantment costs 5-16 emeralds and a level 5 enchantment costs 17-67 emeralds.

if (random->nextFloat() < getRecipeChance(0.07f))
{
Enchantment *enchantment = Enchantment::validEnchantments[
random->nextInt(Enchantment::validEnchantments.size())];
int level = Mth::nextInt(random, enchantment->getMinLevel(),
enchantment->getMaxLevel());
shared_ptr<ItemInstance> book =
Item::enchantedBook->createForEnchantment(
new EnchantmentInstance(enchantment, level));
int cost = 2 + random->nextInt(5 + (level * 10)) + 3 * level;
newOffers->push_back(new MerchantRecipe(
shared_ptr<ItemInstance>(new ItemInstance(Item::book)),
shared_ptr<ItemInstance>(new ItemInstance(Item::emerald, cost)),
book));
}

The priest has no trade-ins. All trades are purchases.

Purchases:

ItemPriceBase Chance
Eye of Ender7-11 emeralds30%
XP Bottle1 emerald for 1-420%
Redstone1 emerald for 1-440%
Glowstone1 emerald for 1-330%

Special Trades (Enchanted Gear):

The priest can offer enchanted iron and diamond gear. There are 8 possible items:

ItemChance (each)
Iron Sword5%
Diamond Sword5%
Iron Chestplate5%
Diamond Chestplate5%
Iron Axe5%
Diamond Axe5%
Iron Pickaxe5%
Diamond Pickaxe5%

Each one that appears costs 2-4 emeralds and the item is enchanted at level 5-19 (5 + random(15)):

int enchantItems[] = {
Item::sword_iron_Id, Item::sword_diamond_Id,
Item::chestplate_iron_Id, Item::chestplate_diamond_Id,
Item::hatchet_iron_Id, Item::hatchet_diamond_Id,
Item::pickAxe_iron_Id, Item::pickAxe_diamond_Id
};
for (unsigned int i = 0; i < 8; ++i)
{
int id = enchantItems[i];
if (random->nextFloat() < getRecipeChance(.05f))
{
newOffers->push_back(new MerchantRecipe(
shared_ptr<ItemInstance>(new ItemInstance(id, 1, 0)),
shared_ptr<ItemInstance>(
new ItemInstance(Item::emerald, 2 + random->nextInt(3), 0)),
EnchantmentHelper::enchantItem(random,
shared_ptr<ItemInstance>(new ItemInstance(id, 1, 0)),
5 + random->nextInt(15))));
}
}

Trade-Ins:

ItemQuantity per EmeraldBase Chance
Coal16-2470%
Iron Ingot8-1050%
Gold Ingot8-1050%
Diamond4-650%

Purchases (Tools):

ItemPrice (emeralds)Base Chance
Iron Sword7-1150%
Diamond Sword12-1450%
Iron Axe6-830%
Diamond Axe9-1230%
Iron Pickaxe7-950%
Diamond Pickaxe10-1250%
Iron Shovel4-620%
Diamond Shovel7-820%
Iron Hoe4-620%
Diamond Hoe7-820%

Purchases (Armor):

ItemPrice (emeralds)Base Chance
Iron Boots4-620%
Diamond Boots7-820%
Iron Helmet4-620%
Diamond Helmet7-820%
Iron Chestplate10-1420%
Diamond Chestplate16-1920%
Iron Leggings8-1020%
Diamond Leggings11-1420%
Chain Boots5-710%
Chain Helmet5-710%
Chain Chestplate11-1510%
Chain Leggings9-1110%

The smith has the biggest trade pool. With 4 trade-ins and 22 purchases, there are 26 possible trades.

Trade-Ins:

ItemQuantity per EmeraldBase Chance
Coal16-2470%
Raw Porkchop14-1850%
Raw Beef14-1850%

Purchases:

ItemPriceBase Chance
Saddle6-8 emeralds10%
Leather Chestplate4-5 emeralds30%
Leather Boots2-4 emeralds30%
Leather Helmet2-4 emeralds30%
Leather Leggings2-4 emeralds30%
Cooked Porkchop1 emerald for 5-730%
Cooked Beef1 emerald for 5-730%

If the profession’s trade pool generates zero trades (all random chances failed), the code falls back to a guaranteed gold ingot trade-in:

if (newOffers->empty())
{
addItemForTradeIn(newOffers, Item::goldIngot_Id, random, 1.0f);
}

This uses 8-10 gold ingots per emerald and has a 100% chance of appearing.

How many items the villager wants per emerald:

ItemMinMax
Wheat1822
Wool1422
Raw Chicken1418
Cooked Fish913
Coal1624
Iron Ingot810
Gold Ingot810
Diamond46
Paper2436
Book1113
Ender Pearl34
Eye of Ender23
Raw Porkchop1418
Raw Beef1418
Wheat Seeds3448
Melon Seeds3038
Pumpkin Seeds3038
Rotten Flesh3664

Positive values are emeralds per item. Negative values mean 1 emerald buys that many items.

ItemMinMaxMeaning
Bread-4-22-4 bread per emerald
Melon-8-44-8 melon per emerald
Apple-8-44-8 apple per emerald
Cookie-10-77-10 cookies per emerald
Cooked Chicken-8-66-8 per emerald
Arrow-12-88-12 arrows per emerald
Cooked Porkchop-7-55-7 per emerald
Cooked Beef-7-55-7 per emerald
XP Bottle-4-11-4 per emerald
Redstone-4-11-4 per emerald
Glowstone-3-11-3 per emerald
Glass-5-33-5 per emerald
Shears343-4 emeralds each
Flint & Steel343-4 emeralds each
Iron Sword7117-11 emeralds
Diamond Sword121412-14 emeralds
Iron Axe686-8 emeralds
Diamond Axe9129-12 emeralds
Iron Pickaxe797-9 emeralds
Diamond Pickaxe101210-12 emeralds
Iron Shovel464-6 emeralds
Diamond Shovel787-8 emeralds
Iron Hoe464-6 emeralds
Diamond Hoe787-8 emeralds
Iron Boots464-6 emeralds
Diamond Boots787-8 emeralds
Iron Helmet464-6 emeralds
Diamond Helmet787-8 emeralds
Iron Chestplate101410-14 emeralds
Diamond Chestplate161916-19 emeralds
Iron Leggings8108-10 emeralds
Diamond Leggings111411-14 emeralds
Chain Boots575-7 emeralds
Chain Helmet575-7 emeralds
Chain Chestplate111511-15 emeralds
Chain Leggings9119-11 emeralds
Leather Chestplate454-5 emeralds
Leather Boots242-4 emeralds
Leather Helmet242-4 emeralds
Leather Leggings242-4 emeralds
Bookshelf343-4 emeralds
Saddle686-8 emeralds
Compass101210-12 emeralds
Clock101210-12 emeralds
Eye of Ender7117-11 emeralds

When a player right-clicks a villager, here is what happens:

  1. Villager::interact() sets the trading player and opens the trade UI:
bool Villager::interact(shared_ptr<Player> player)
{
// ...
if (!holdingSpawnEgg && isAlive() && !isTrading() && !isBaby())
{
if (!level->isClientSide)
{
setTradingPlayer(player);
player->openTrading(
dynamic_pointer_cast<Merchant>(shared_from_this()));
}
return true;
}
// ...
}
  1. A MerchantMenu is created with 3 slots (2 payment + 1 result) plus the player’s inventory.

  2. When the player places items in the payment slots, MerchantContainer::updateSellItem() searches for a matching recipe:

MerchantRecipe *recipeFor = offers->getRecipeFor(buyItem1, buyItem2,
selectionHint);
if (recipeFor != NULL && !recipeFor->isDeprecated())
{
activeRecipe = recipeFor;
setItem(MerchantMenu::RESULT_SLOT, recipeFor->getSellItem()->copy());
}
  1. When the player takes the result, MerchantResultSlot::onTake() deducts the payment items and calls merchant->notifyTrade().

  2. The TradeWithPlayerGoal AI goal keeps the villager still and facing the player during the trade. It stops when the player moves too far away (>4 blocks) or closes the menu.

After a player buys the last recipe in a villager’s list, the villager queues a refresh:

void Villager::notifyTrade(MerchantRecipe *activeRecipe)
{
activeRecipe->increaseUses();
// ...
if (activeRecipe->isSame(offers->at(offers->size() - 1)))
{
updateMerchantTimer = SharedConstants::TICKS_PER_SECOND * 2;
addRecipeOnUpdate = true;
// ...
}
// ...
}

After 2 seconds, the refresh runs in serverAiMobStep():

  • All deprecated recipes get 2-12 extra uses
  • A new recipe is added via addOffers(1)
  • The villager gets 10 seconds of Regeneration
  • If in a village, the last player to trade gets +1 reputation

Adding Custom Trades to Existing Villagers

Section titled “Adding Custom Trades to Existing Villagers”

The simplest way to add trades is to modify the addOffers() method in Villager.cpp. Add entries to the profession’s switch case:

case PROFESSION_SMITH:
// ... existing trades ...
// Add a new trade-in item (player gives items for emeralds)
addItemForTradeIn(newOffers, Item::myCustomIngot_Id, random,
getRecipeChance(.6f));
// Add a new purchase (player pays emeralds for items)
addItemForPurchase(newOffers, Item::myCustomSword_Id, random,
getRecipeChance(.4f));
// Add a complex three-item trade
if (random->nextFloat() < getRecipeChance(.3f))
{
newOffers->push_back(new MerchantRecipe(
shared_ptr<ItemInstance>(new ItemInstance(Item::emerald, 15)),
shared_ptr<ItemInstance>(new ItemInstance(Item::diamond, 3)),
shared_ptr<ItemInstance>(new ItemInstance(Item::myCustomArmor_Id, 1, 0))
));
}
break;

Don’t forget to register the price ranges in Villager::staticCtor():

void Villager::staticCtor()
{
// ... existing entries ...
// For trade-in items: how many items per emerald
MIN_MAX_VALUES[Item::myCustomIngot_Id] = pair<int,int>(6, 10);
// For purchases: emerald cost (negative = multi-item output)
MIN_MAX_PRICES[Item::myCustomSword_Id] = pair<int,int>(8, 12);
}

You can make any entity into a merchant. It needs to implement the Merchant interface and trigger the trade UI. Here is a skeleton:

#pragma once
#include "PathfinderMob.h"
#include "Merchant.h"
class MyMerchant : public PathfinderMob, public Merchant
{
public:
eINSTANCEOF GetType() { return eTYPE_MY_MERCHANT; }
static Entity *create(Level *level) { return new MyMerchant(level); }
MyMerchant(Level *level);
~MyMerchant();
// Merchant interface
void setTradingPlayer(shared_ptr<Player> player);
shared_ptr<Player> getTradingPlayer();
MerchantRecipeList *getOffers(shared_ptr<Player> forPlayer);
void overrideOffers(MerchantRecipeList *recipeList);
void notifyTrade(MerchantRecipe *activeRecipe);
void notifyTradeUpdated(shared_ptr<ItemInstance> item);
int getDisplayName();
bool interact(shared_ptr<Player> player);
virtual int getMaxHealth();
private:
weak_ptr<Player> tradingPlayer;
MerchantRecipeList *offers;
void buildOffers();
};
MyMerchant::MyMerchant(Level *level) : PathfinderMob(level)
{
this->defineSynchedData();
health = getMaxHealth();
offers = NULL;
tradingPlayer = weak_ptr<Player>();
// Add AI goals for a non-hostile NPC
goalSelector.addGoal(0, new FloatGoal(this));
goalSelector.addGoal(1, new TradeWithPlayerGoal(
reinterpret_cast<Villager*>(this)));
goalSelector.addGoal(2, new RandomStrollGoal(this, 0.3f));
goalSelector.addGoal(3, new LookAtPlayerGoal(this, typeid(Player), 8));
}
MyMerchant::~MyMerchant()
{
delete offers;
}
int MyMerchant::getMaxHealth() { return 30; }
bool MyMerchant::interact(shared_ptr<Player> player)
{
if (isAlive() && tradingPlayer.lock() == NULL)
{
if (!level->isClientSide)
{
setTradingPlayer(player);
player->openTrading(
dynamic_pointer_cast<Merchant>(shared_from_this()));
}
return true;
}
return PathfinderMob::interact(player);
}
void MyMerchant::buildOffers()
{
offers = new MerchantRecipeList();
// Sell diamonds for 5 emeralds
offers->push_back(new MerchantRecipe(
shared_ptr<ItemInstance>(new ItemInstance(Item::emerald, 5)),
shared_ptr<ItemInstance>(new ItemInstance(Item::diamond))
));
// Buy iron ingots: 10 iron = 1 emerald
offers->push_back(new MerchantRecipe(
shared_ptr<ItemInstance>(new ItemInstance(Item::ironIngot, 10)),
shared_ptr<ItemInstance>(new ItemInstance(Item::emerald))
));
// Three-slot trade: book + 20 emeralds = enchanted book
Enchantment *ench = Enchantment::sharpness;
shared_ptr<ItemInstance> enchBook =
Item::enchantedBook->createForEnchantment(
new EnchantmentInstance(ench, 3));
offers->push_back(new MerchantRecipe(
shared_ptr<ItemInstance>(new ItemInstance(Item::book)),
shared_ptr<ItemInstance>(new ItemInstance(Item::emerald, 20)),
enchBook
));
}
void MyMerchant::notifyTrade(MerchantRecipe *activeRecipe)
{
activeRecipe->increaseUses();
playSound(eSoundType_MOB_VILLAGER_YES,
getSoundVolume(), getVoicePitch());
}
void MyMerchant::notifyTradeUpdated(shared_ptr<ItemInstance> item) {}
void MyMerchant::overrideOffers(MerchantRecipeList *recipeList) {}
int MyMerchant::getDisplayName()
{
return IDS_MY_MERCHANT; // Your localized string ID
}

On the client side, LCE uses ClientSideMerchant as a proxy. It receives the offer list over the network and renders the trade UI. The server sends trade data through TradeItemPacket, which serializes the recipe list with MerchantRecipeList::writeToStream().

You don’t need to touch ClientSideMerchant for custom merchants. It works automatically as long as your entity implements Merchant and calls player->openTrading().

Trade offers are saved in NBT under the “Offers” tag. Each recipe stores its buy/sell items plus usage counts:

void Villager::addAdditonalSaveData(CompoundTag *tag)
{
AgableMob::addAdditonalSaveData(tag);
tag->putInt(L"Profession", getProfession());
tag->putInt(L"Riches", riches);
if (offers != NULL)
{
tag->putCompound(L"Offers", offers->createTag());
}
}

The recipe tag structure looks like:

Offers (CompoundTag)
Recipes (ListTag<CompoundTag>)
[0] (CompoundTag)
buy (CompoundTag) - ItemInstance data
sell (CompoundTag) - ItemInstance data
buyB (CompoundTag) - Optional second buy item
uses (Int) - Current use count
maxUses (Int) - Max before deprecated
ConstructorUse Case
MerchantRecipe(buy, sell)Simple 1-input trade
MerchantRecipe(buy, Item*)1-input, wraps Item in ItemInstance
MerchantRecipe(buy, Tile*)1-input, wraps Tile in ItemInstance
MerchantRecipe(buyA, buyB, sell)2-input trade
MerchantRecipe(buyA, buyB, sell, uses, maxUses)Full control
FileWhat it does
Minecraft.World/Merchant.hTrading interface
Minecraft.World/Villager.cppTrade generation, profession pools, price tables, addOffers()
Minecraft.World/MerchantRecipe.cppIndividual trade logic and use tracking
Minecraft.World/MerchantRecipeList.cppRecipe matching and serialization
Minecraft.World/MerchantMenu.cppTrading UI container
Minecraft.World/MerchantContainer.cppSlot management and recipe resolution
Minecraft.World/MerchantResultSlot.cppPayment deduction on trade completion
Minecraft.World/ClientSideMerchant.cppClient-side trade proxy
Minecraft.World/TradeWithPlayerGoal.cppAI that keeps villagers still while trading