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
- Register a POI as a job site
- Create the profession
- Create the trades
- Create a texture
- 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" }