Villager Profession Checklist

These are all the steps you need to add a new villager profession to Minecraft using Forge. At the time of writing I was using Forge for 1.20.2, but this should also apply to 1.19.2, 1.20.1, and NeoForge 1.20.4.

Checklist


  1. Register a POI as a job site
  2. Create the profession
  3. Create the trades
  4. Create a texture
  5. Hero of the Village Rewards

Point of Interest


The POI registers a block as something that a villager’s AI will notice. Later you use this when registering a new villager profession. A basic implementation for a POI Type Registry will look something like this:

/**
 * Register POIs for use by the AI.
 */
public class PoiTypeRegistry {

    // An instance of a deferred registry we use to register.
    private final DeferredRegister<PoiType> deferredRegister;

    // The lepidopterist's job block.
    private RegistryObject<PoiType> lepidopterist;

    /**
     * Construction
     * @param modEventBus The event bus to register with.
     */
    public PoiTypeRegistry(IEventBus modEventBus) {
        this.deferredRegister = DeferredRegister.create(ForgeRegistries.POI_TYPES, ButterfliesMod.MOD_ID);
        this.deferredRegister.register(modEventBus);
    }

    /**
     * Register the POI types.
     * @param blockRegistry The block registry.
     */
    public void initialise(BlockRegistry blockRegistry) {
        lepidopterist =deferredRegister.register("lepidopterist",
                () -> new PoiType(getBlockStates(blockRegistry.getButterflyFeeder()), 1, 1));
    }

    /**
     * Accessor to the lepidopterist POI.
     * @return The POI Type.
     */
    public RegistryObject<PoiType> getLepidopterist() {
        return lepidopterist;
    }

    /**
     * Helper method to get a set of block states.
     * @param block The block to get the block states for.
     * @return The set of block states.
     */
    private Set<BlockState> getBlockStates(RegistryObject<Block> block) {
        return ImmutableSet.copyOf(block.get().getStateDefinition().getPossibleStates());
    }
}

Tag

To register the POI as a job site, it needs to be added to the acquirable job sites tag. Create or modify acquirable_job_site.json under resources/data/minecraft/tags/point_of_interest_type/ that looks something like this:

{
  "values": [
    "butterflies:lepidopterist"
  ]
}

Profession


To add a villager profession we need to register it like everything else. We don’t need to add any new classes, just a registry that looks something like this:

/**
 * Register professions to be used by villagers.
 */
public class VillagerProfessionRegistry {

    // An instance of a deferred registry we use to register.
    private final DeferredRegister<VillagerProfession> deferredRegister;

    // The lepidopterist profession.
    private RegistryObject<VillagerProfession> lepidopterist;

    /**
     * Construction
     * @param modEventBus The event bus to register with.
     */
    public VillagerProfessionRegistry(IEventBus modEventBus) {
        this.deferredRegister = DeferredRegister.create(ForgeRegistries.VILLAGER_PROFESSIONS, ButterfliesMod.MOD_ID);
        this.deferredRegister.register(modEventBus);
    }

    /**
     * Register the professions.
     * @param poiTypeRegistry The POI Type registry.
     */
    public void initialise(PoiTypeRegistry poiTypeRegistry) {
        lepidopterist = deferredRegister.register("lepidopterist",
                () -> new VillagerProfession(
                        "lepidopterist",
                        x -> x.get() == poiTypeRegistry.getLepidopterist().get(),
                        x -> x.get() == poiTypeRegistry.getLepidopterist().get(),
                        ImmutableSet.of(),
                        ImmutableSet.of(),
                        SoundEvents.VILLAGER_WORK_CLERIC));
    }

    /**
     * Accessor to the lepidopterist profession.
     * @return The POI Type.
     */
    public RegistryObject<VillagerProfession> getLepidopterist() {
        return lepidopterist;
    }
}

Localisation

A new localisation string will need to be added. The key will be entity.minecraft.villager.<MOD_ID>.<PROFESSION_ID>. For example:

{
  "entity.minecraft.villager.butterflies.lepidopterist": "Lepidopterist"
}

Trades


To set up trades for the new profession you can listen for the VillagerTradesEvent. Check for the profession you created, and add your new trades to the lists contained in the event. There are 5 lists, one for each level of the villager. A very simple implementation might look like this:

/**
 * Listens for events based around villagers.
 */
public class VillageEventListener {

    // Registries.
    private final ItemRegistry itemRegistry;
    private final VillagerProfessionRegistry villagerProfessionRegistry;

    /**
     * Construction
     * @param forgeEventBus The event bus to register with.
     */
    public VillageEventListener(IEventBus forgeEventBus,
                                ItemRegistry itemRegistry,
                                VillagerProfessionRegistry villagerProfessionRegistry) {
        forgeEventBus.register(this);
        forgeEventBus.addListener(this::onVillagerTrades);

        this.itemRegistry = itemRegistry;
        this.villagerProfessionRegistry = villagerProfessionRegistry;
    }

    /**
     * Used to add/modify trades to professions.
     * @param event The event information.
     */
    private void onVillagerTrades(VillagerTradesEvent event) {
        if (event.getType() == villagerProfessionRegistry.getLepidopterist().get()) {
            Int2ObjectMap<List<VillagerTrades.ItemListing>> trades = event.getTrades();

            Collection<ButterflyData> butterflies = ButterflyData.getButterflyDataCollection();

            List<VillagerTrades.ItemListing> tradesLevel1 = trades.get(1);
            List<VillagerTrades.ItemListing> tradesLevel3 = trades.get(3);

            tradesLevel1.add(new SellingItemTrade(itemRegistry.getEmptyButterflyNet().get(), 5, 1, 1));
            tradesLevel3.add(new SellingItemTrade(itemRegistry.getSilk().get(), 32, 8, 10));
    }
}

Trade Helper Classes

Creating trades can be confusing, so most people set up helper classes. These classes make creating basic trades a lot easier. There are several copies of this implementation on GitHub already, but these are the ones I’ve been using. For trades where the village will buy from the player:

/**
 * Represents an offer to buy an item.
 */
public class BuyingItemTrade implements VillagerTrades.ItemListing {
    private final Item wantedItem;
    private final int count;
    private final int maxUses;
    private final int xpValue;
    private final float priceMultiplier;

    /**
     * Construction.
     * @param wantedItem The item the trader wants.
     * @param countIn The number of items the trader wants.
     * @param maxUsesIn The maximum number of trades.
     * @param xpValueIn The XP awarded for completing the trade.
     */
    public BuyingItemTrade(ItemLike wantedItem,
                           int countIn,
                           int maxUsesIn,
                           int xpValueIn) {
        this.wantedItem = wantedItem.asItem();
        this.count = countIn;
        this.maxUses = maxUsesIn;
        this.xpValue = xpValueIn;
        this.priceMultiplier = 0.05F;
    }

    @Override
    public MerchantOffer getOffer(@NotNull Entity trader,
                                  @NotNull RandomSource rand) {
        ItemStack stack = new ItemStack(this.wantedItem, this.count);
        return new MerchantOffer(stack,
                new ItemStack(Items.EMERALD),
                this.maxUses,
                this.xpValue,
                this.priceMultiplier);
    }
}

For trades where the villager is selling something to the player:

/**
 * Creates a trade for selling.
 */
public class SellingItemTrade implements VillagerTrades.ItemListing {
    private final ItemStack givenItem;
    private final int emeraldCount;
    private final int sellingItemCount;
    private final int maxUses;
    private final int xpValue;
    private final float priceMultiplier;

    /**
     * Constructor
     * @param givenItem The item to sell.
     * @param emeraldCount The base cost in emeralds.
     * @param sellingItemCount The number of items to sell.
     * @param xpValue The XP awarded to the villager on a successful trade.
     */
    public SellingItemTrade(ItemLike givenItem,
                            int emeraldCount,
                            int sellingItemCount,
                            int xpValue) {
        this(new ItemStack(givenItem), emeraldCount, sellingItemCount, 12, xpValue);
    }

    /**
     * Constructor
     * @param givenItem The item to sell.
     * @param emeraldCount The base cost in emeralds.
     * @param sellingItemCount The number of items to sell.
     * @param maxUses The maximum number of trades.
     * @param xpValue The XP awarded to the villager on a successful trade.
     */
    public SellingItemTrade(ItemStack givenItem,
                            int emeraldCount,
                            int sellingItemCount,
                            int maxUses,
                            int xpValue) {
        this(givenItem, emeraldCount, sellingItemCount, maxUses, xpValue, 0.2F);
    }

    /**
     * Constructor
     * @param givenItem The item to sell.
     * @param emeraldCount The base cost in emeralds.
     * @param sellingItemCount The number of items to sell.
     * @param maxUses The maximum number of trades.
     * @param xpValue The XP awarded to the villager on a successful trade.
     * @param priceMultiplier The amount the price is modified by annoying or
     *                        helping the villager.
     */
    public SellingItemTrade(ItemStack givenItem,
                            int emeraldCount,
                            int sellingItemCount,
                            int maxUses,
                            int xpValue,
                            float priceMultiplier) {
        this.givenItem = givenItem;
        this.emeraldCount = emeraldCount;
        this.sellingItemCount = sellingItemCount;
        this.maxUses = maxUses;
        this.xpValue = xpValue;
        this.priceMultiplier = priceMultiplier;
    }

    /**
     * Get an offer for the trader to use.
     * @param trader The trader requesting the offer.
     * @param rand The RNG.
     * @return The offer to the player.
     */
    @Override
    public MerchantOffer getOffer(@NotNull Entity trader,
                                  @NotNull RandomSource rand) {
        return new MerchantOffer(new ItemStack(Items.EMERALD, this.emeraldCount),
                new ItemStack(this.givenItem.getItem(), this.sellingItemCount),
                this.maxUses,
                this.xpValue,
                this.priceMultiplier);
    }
}

Textures


You need to do is create a texture for your new profession. You can use the vanilla textures as a base for the layout. Once you have created the texture you need to place it in two places:

  • /resources/assets/<MOD_ID>/textures/entity/villager/profession/
  • /resources/assets/<MOD_ID>/textures/entity/zombie_villager/profession/

Zombie Villager

Don’t forget the zombie village texture! If you want the zombie version to look different for any reason you can, of course, use a different texture.

Hero of the Village Rewards


If you want the villager to reward the player for saving them from a raid, you need to add a loot table for these rewards. They will go in your resources folder under:

  • /resources/data/<MOD_ID>/loot_tables/gameplay/hero_of_the_village/

A typical JSON file with rewards might look something like this:

{
  "type": "minecraft:gift",
  "pools": [
    {
      "bonus_rolls": 0.0,
      "entries": [
        {
          "type": "minecraft:item",
          "name": "butterflies:silk"
        },
        {
          "type": "minecraft:item",
          "name": "butterflies:butterfly_net"
        }
      ],
      "rolls": 1.0
    }
  ],
  "random_sequence": "minecraft:gameplay/hero_of_the_village/lepidopterist_gift"
}