diff --git a/patches/minecraft/net/minecraft/world/entity/npc/VillagerTrades.java.patch b/patches/minecraft/net/minecraft/world/entity/npc/VillagerTrades.java.patch new file mode 100644 index 0000000000..d33b723dad --- /dev/null +++ b/patches/minecraft/net/minecraft/world/entity/npc/VillagerTrades.java.patch @@ -0,0 +1,21 @@ +--- a/net/minecraft/world/entity/npc/VillagerTrades.java ++++ b/net/minecraft/world/entity/npc/VillagerTrades.java +@@ -1366,6 +_,7 @@ + private final int villagerXp; + + public EmeraldsForVillagerTypeItem(int p_35669_, int p_35670_, int p_35671_, Map p_35672_) { ++ if (false) // FORGE: Modders can add custom villager types, so remove this validation + BuiltInRegistries.VILLAGER_TYPE.stream().filter(p_35680_ -> !p_35672_.containsKey(p_35680_)).findAny().ifPresent(p_327046_ -> { + throw new IllegalStateException("Missing trade for villager type: " + BuiltInRegistries.VILLAGER_TYPE.getKey(p_327046_)); + }); +@@ -1379,7 +_,9 @@ + @Override + public MerchantOffer getOffer(Entity p_219685_, RandomSource p_219686_) { + if (p_219685_ instanceof VillagerDataHolder villagerdataholder) { +- ItemCost itemcost = new ItemCost(this.trades.get(villagerdataholder.getVillagerData().getType()), this.cost); ++ Item item = this.trades.get(villagerdataholder.getVillagerData().getType()); ++ if (item == null) return null; ++ ItemCost itemcost = new ItemCost(item, this.cost); // FORGE: Account for modded villager types by returning null if there is no trade + return new MerchantOffer(itemcost, new ItemStack(Items.EMERALD), this.maxUses, this.villagerXp, 0.05F); + } else { + return null; diff --git a/patches/minecraft/net/minecraft/world/entity/npc/VillagerType.java.patch b/patches/minecraft/net/minecraft/world/entity/npc/VillagerType.java.patch new file mode 100644 index 0000000000..ea3609eac6 --- /dev/null +++ b/patches/minecraft/net/minecraft/world/entity/npc/VillagerType.java.patch @@ -0,0 +1,12 @@ +--- a/net/minecraft/world/entity/npc/VillagerType.java ++++ b/net/minecraft/world/entity/npc/VillagerType.java +@@ -69,4 +_,9 @@ + public static VillagerType byBiome(Holder p_204074_) { + return p_204074_.unwrapKey().map(BY_BIOME::get).orElse(PLAINS); + } ++ ++ /** FORGE: Registers the VillagerType that will spawn in the given biome. This method should be called during FMLCommonSetupEvent using event.enqueueWork() */ ++ public static void registerBiomeType(ResourceKey biomeKey, VillagerType villagerType) { ++ BY_BIOME.put(biomeKey, villagerType); ++ } + } diff --git a/src/test/java/net/minecraftforge/debug/gameplay/villager/VillagerTypeTestMod.java b/src/test/java/net/minecraftforge/debug/gameplay/villager/VillagerTypeTestMod.java new file mode 100644 index 0000000000..a9b84decb0 --- /dev/null +++ b/src/test/java/net/minecraftforge/debug/gameplay/villager/VillagerTypeTestMod.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.debug.gameplay.villager; + +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.entity.npc.VillagerTrades; +import net.minecraft.world.entity.npc.VillagerType; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.gametest.GameTestHolder; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.RegistryObject; +import net.minecraftforge.test.BaseTestMod; + +import java.util.Map; + +@GameTestHolder("forge." + VillagerTypeTestMod.MOD_ID) +@Mod(VillagerTypeTestMod.MOD_ID) +public class VillagerTypeTestMod extends BaseTestMod { + public static final String MOD_ID = "villager_type_test"; + + private static final DeferredRegister VILLAGER_TYPES = DeferredRegister.create(Registries.VILLAGER_TYPE, MOD_ID); + + private static final RegistryObject TEST_VILLAGER_TYPE = VILLAGER_TYPES.register("test_villager_type", () -> new VillagerType("test_villager_type")); + + public VillagerTypeTestMod(FMLJavaModLoadingContext context) { + context.getModEventBus().addListener(this::onCommonSetup); + } + + private void onCommonSetup(final FMLCommonSetupEvent event) { + event.enqueueWork(() -> VillagerType.registerBiomeType(Biomes.PLAINS, TEST_VILLAGER_TYPE.get())); + } + + @GameTest(template = "forge:empty3x3x3") + public static void biome_type(GameTestHelper helper) { + RegistryAccess access = helper.getLevel().registryAccess(); + VillagerType type = access.registryOrThrow(Registries.VILLAGER_TYPE).get(TEST_VILLAGER_TYPE.getId()); + if (type == null) + helper.fail("Failed to find test_villager_type"); + helper.assertValueEqual(type, TEST_VILLAGER_TYPE.get(), "Loaded entry does not contain expected value"); + + Holder biome = access.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS); + helper.assertValueEqual(VillagerType.byBiome(biome), TEST_VILLAGER_TYPE.get(), "VillagerType.byBiome did not return the expected value"); + + helper.succeed(); + } + + /** Test verifies NPE not thrown when looking up a villager type in the trade that doesn't contain that type.*/ + @GameTest(template = "forge:empty3x3x3") + public static void emeralds_for_villager_type(GameTestHelper helper) { + var trade = new VillagerTrades.EmeraldsForVillagerTypeItem(1, 12, 30, Map.of(TEST_VILLAGER_TYPE.get(), Items.DIRT)); + var rnd = helper.getLevel().getRandom(); + + // Should be a successful trade for dirt for our test villager + var test = new Villager(EntityType.VILLAGER, helper.getLevel(), TEST_VILLAGER_TYPE.get()); + var test_offer = trade.getOffer(test, rnd); + helper.assertFalse(test_offer == null, "Failed to retreive trade value for test profession"); + helper.assertValueEqual(test_offer.getItemCostA().itemStack().getItem(), Items.DIRT, "Offer did not return the expected item"); + + var plains = new Villager(EntityType.VILLAGER, helper.getLevel(), VillagerType.PLAINS); + // This will NPE on unpatched code, we need to test that it returns null correctly + var plains_offer = trade.getOffer(plains, rnd); + helper.assertTrue(plains_offer == null, "Offer should not be available for a plains villager"); + + helper.succeed(); + } +}