Difference between revisions of "Dynamic Loot Modification"

From Forge Community Wiki
(Update to 1.19)
(Update to 1.19 Codec GLMs)
Line 11: Line 11:
 
#* The operational code that makes your modifier work and associated serializer.
 
#* The operational code that makes your modifier work and associated serializer.
  
Finally, the serializer for your operational class is [[Registration|registered]] as any other <code>ForgeRegistryEntry</code>.
+
Finally, the serializer for your operational class is [[Registration|registered]] as any other registry object.
  
 
==The global_loot_modifiers.json==
 
==The global_loot_modifiers.json==
Line 69: Line 69:
  
 
<syntaxhighlight lang="java">
 
<syntaxhighlight lang="java">
private static class WheatSeedsConverterModifier extends LootModifier {
+
/**
  private final int numSeedsToConvert;
+
    * When harvesting wheat with shears, this modifier is invoked via the wheat_harvest loot_modifier json<br/>
  private final Item itemToCheck;
+
     * This modifier checks how many seeds were harvested and turns X seeds into Y wheat (3:1)
  private final Item itemReward;
+
     *
  public WheatSeedsConverterModifier(LootItemCondition[] conditions, int numSeeds, Item itemCheck, Item reward) {
 
    super(conditions);
 
    numSeedsToConvert = numSeeds;
 
    itemToCheck = itemCheck;
 
    itemReward = reward;
 
  }
 
 
 
  @Nonnull
 
  @Override
 
  public List<ItemStack> doApply(List<ItemStack> generatedLoot, LootContext context) {
 
    /*
 
     * Additional conditions can be checked, though as much as possible should be parameterized via JSON data.
 
     * It is better to write a new LootItemCondition implementation than to do things here.
 
 
     */
 
     */
    int numSeeds = 0;
+
private static class WheatSeedsConverterModifier extends LootModifier
    for(ItemStack stack : generatedLoot) {
+
{
      if(stack.getItem() == itemToCheck)
+
        public static final Supplier<Codec<WheatSeedsConverterModifier>> CODEC = Suppliers.memoize(() ->
         numSeeds += stack.getCount();
+
         RecordCodecBuilder.create(inst -> codecStart(inst).and(
    }
+
                inst.group(
    if(numSeeds >= numSeedsToConvert) {
+
                        Codec.INT.fieldOf("numSeeds").forGetter(m -> m.numSeedsToConvert),
      generatedLoot.removeIf(x -> x.getItem() == itemToCheck);
+
                        ForgeRegistries.ITEMS.getCodec().fieldOf("seedItem").forGetter(m -> m.itemToCheck),
      generatedLoot.add(new ItemStack(itemReward, (numSeeds/numSeedsToConvert)));
+
                        ForgeRegistries.ITEMS.getCodec().fieldOf("replacement").forGetter(m -> m.itemReward)
      numSeeds = numSeeds % numSeedsToConvert;
+
                )).apply(inst, WheatSeedsConverterModifier::new)
      if(numSeeds > 0)
+
         ));
         generatedLoot.add(new ItemStack(itemToCheck, numSeeds));
 
    }
 
    return generatedLoot;
 
  }
 
  
  private static class Serializer extends GlobalLootModifierSerializer<WheatSeedsConverterModifier> {
+
        private final int numSeedsToConvert;
 +
        private final Item itemToCheck;
 +
        private final Item itemReward;
 +
        public WheatSeedsConverterModifier(LootItemCondition[] conditionsIn, int numSeeds, Item itemCheck,
 +
            Item reward)
 +
        {
 +
            super(conditionsIn);
 +
            numSeedsToConvert = numSeeds;
 +
            itemToCheck = itemCheck;
 +
            itemReward = reward;
 +
        }
  
    @Override
+
        @NotNull
    public WheatSeedsConverterModifier read(ResourceLocation name, JsonObject object, LootItemCondition[] conditions) {
+
        @Override
      int numSeeds = GsonHelper.getAsInt(object, "numSeeds");
+
        public ObjectArrayList<ItemStack> doApply(ObjectArrayList<ItemStack> generatedLoot, LootContext context) {
      Item seed = ForgeRegistries.ITEMS.getValue(new ResourceLocation((GsonHelper.getAsString(object, "seedItem"))));
+
            //
      Item wheat = ForgeRegistries.ITEMS.getValue(new ResourceLocation(GsonHelper.getAsString(object, "replacement")));
+
            // Additional conditions can be checked, though as much as possible should be parameterized via JSON data.
      return new WheatSeedsConverterModifier(conditions, numSeeds, seed, wheat);
+
            // It is better to write a new ILootCondition implementation than to do things here.
    }
+
            //
 +
            int numSeeds = 0;
 +
            generatedLoot.stream().filter(stack -> stack.getItem() == itemToCheck)
 +
                  .forEach(stack -> numSeeds+=stack.getCount())
 +
            if(numSeeds >= numSeedsToConvert) {
 +
                generatedLoot.removeIf(x -> x.getItem() == itemToCheck);
 +
                generatedLoot.add(new ItemStack(itemReward, (numSeeds/numSeedsToConvert)));
 +
                numSeeds = numSeeds%numSeedsToConvert;
 +
                if(numSeeds > 0)
 +
                    generatedLoot.add(new ItemStack(itemToCheck, numSeeds));
 +
            }
 +
            return generatedLoot;
 +
        }
  
    @Override
+
        @Override
    public JsonObject write(WheatSeedsConverterModifier instance) {
+
        public Codec<? extends IGlobalLootModifier> codec() {
      JsonObject json = makeConditions(instance.conditions);
+
            return CODEC.get();
      json.addProperty("numSeeds", instance.numSeedsToConvert);
+
        }
      json.addProperty("seedItem", ForgeRegistries.ITEMS.getKey(instance.itemToCheck).toString());
 
      json.addProperty("replacement", ForgeRegistries.ITEMS.getKey(instance.itemReward).toString());
 
      return json;
 
 
     }
 
     }
  }
 
}
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Line 128: Line 128:
 
This method is only called if the <code>conditions</code> specified return <code>true</code> and the modder is now able to make the modifications they desire. In this case we can see that the number of <code>itemToCheck</code> meets or exceeds the <code>numSeedsToConvert</code> before modifying the list by adding an <code>itemReward</code> and removing any excess <code>itemToCheck</code> stacks, matching the previously mentioned effects: ''When a wheat block is harvested with shears, if enough seeds are generated as loot, they are converted to additional wheat instead''.
 
This method is only called if the <code>conditions</code> specified return <code>true</code> and the modder is now able to make the modifications they desire. In this case we can see that the number of <code>itemToCheck</code> meets or exceeds the <code>numSeedsToConvert</code> before modifying the list by adding an <code>itemReward</code> and removing any excess <code>itemToCheck</code> stacks, matching the previously mentioned effects: ''When a wheat block is harvested with shears, if enough seeds are generated as loot, they are converted to additional wheat instead''.
  
Also take note of the <code>read</code> method in the serializer. The conditions are already deserialized for you and if you have no other data, simply <code>return new MyModifier(conditions)</code>. However, the full <code>JsonObject</code> is available if needed. The <code>write</code> method, on the other hand, is used for if you want to utilize <code>GlobalLootModifierProvider</code> for [[datageneration|data generation]].
+
Utilize <code>GlobalLootModifierProvider</code> for [[datageneration|data generation]].
  
 
Additional [https://github.com/MinecraftForge/MinecraftForge/blob/1.19.x/src/test/java/net/minecraftforge/debug/gameplay/loot/GlobalLootModifiersTest.java examples] can be found on the Forge Git repository, including silk touch and smelting effects.
 
Additional [https://github.com/MinecraftForge/MinecraftForge/blob/1.19.x/src/test/java/net/minecraftforge/debug/gameplay/loot/GlobalLootModifiersTest.java examples] can be found on the Forge Git repository, including silk touch and smelting effects.

Revision as of 17:52, 20 September 2022

Global Loot Modifiers are a data-driven method of handling modification of harvested drops without the need to overwrite dozens to hundreds of vanilla loot tables or to handle effects that would require interactions with another mod's loot tables without knowing what mods may be loaded. Global Loot Modifiers are also stacking, rather than last-load-wins as modifications to loot tables would be.

Registering a Global Loot Modifier

You will need 3 things:

  1. Create a global_loot_modifiers.json file at /data/forge/loot_modifiers/
    • This will tell Forge about your modifiers and works similar to tags.
  2. A serialized json representing your modifier at /data/<modID>/loot_modifiers/
    • This will contain all of the data about your modification and allows data packs to tweak your effect.
  3. A class that extends LootModifier
    • The operational code that makes your modifier work and associated serializer.

Finally, the serializer for your operational class is registered as any other registry object.

The global_loot_modifiers.json

All you need to add here are the file names of your loot modifiers. These are the names of the json files you have made in the loot_modifiers folder, in ResourceLocation format.

{
  "replace": false,
  "entries": [
    "global_loot_test:silk_touch_bamboo",
    "global_loot_test:smelting",
    "global_loot_test:wheat_harvest"
  ]
}

replace causes the cache of modifiers to be cleared fully when this asset loads (mods are loaded in an order that may be specified by a data pack). For modders you will want to use false while data pack makers may want to specify their overrides with true.

entries is an ordered list of the modifiers that will be loaded. Any modifier that is not listed will not be loaded and the ones listed are called in the order listed. This is primarily relevant to data pack makers for resolving conflicts between modifiers from separate mods.

The serialized json

This file contains all of the potential variables related to your modifier, including the conditions that must be met prior to modifying any loot as well as any other parameters your modifier might have. Avoid hard-coded values where ever possible so that data pack makers can adjust balance if they wish to.

{
  "conditions": [
    {
      "condition": "minecraft:match_tool",
      "predicate": {
        "item": "minecraft:shears"
      }
    },
    {
      "condition": "minecraft:block_state_property",
      "block": "minecraft:wheat"
    }
  ],
  "seedItem": "minecraft:wheat_seeds",
  "numSeeds": 3,
  "replacement": "minecraft:wheat"
}

In the above example, the modification only happens if the player harvests wheat when using shears (specified by the two conditions which are automatically ANDed together). The seedsItem and numSeeds values are then used to count how many seeds were generated by the vanilla loot table, and if matched, are substituted for an additional replacement item instead. The operation code will be shown below.


conditions is the only object needed by the system specification, everything else is the mod maker's data.

seedItem, numSeeds and replacement are NOT standard elements of Global Loot Modifiers.

They are deserialized manually in the next section.

The LootModifier Subclass

You will also need a static child class that extends GlobalLootModifierSerializer<T> where T is your LootModifier subclass in order to deserialize your json data file into operational code.

/**
     * When harvesting wheat with shears, this modifier is invoked via the wheat_harvest loot_modifier json<br/>
     * This modifier checks how many seeds were harvested and turns X seeds into Y wheat (3:1)
     *
     */
private static class WheatSeedsConverterModifier extends LootModifier
{
        public static final Supplier<Codec<WheatSeedsConverterModifier>> CODEC = Suppliers.memoize(() -> 
        RecordCodecBuilder.create(inst -> codecStart(inst).and(
                inst.group(
                        Codec.INT.fieldOf("numSeeds").forGetter(m -> m.numSeedsToConvert),
                        ForgeRegistries.ITEMS.getCodec().fieldOf("seedItem").forGetter(m -> m.itemToCheck),
                        ForgeRegistries.ITEMS.getCodec().fieldOf("replacement").forGetter(m -> m.itemReward)
                )).apply(inst, WheatSeedsConverterModifier::new)
        ));

        private final int numSeedsToConvert;
        private final Item itemToCheck;
        private final Item itemReward;
        public WheatSeedsConverterModifier(LootItemCondition[] conditionsIn, int numSeeds, Item itemCheck, 
             Item reward)
        {
            super(conditionsIn);
            numSeedsToConvert = numSeeds;
            itemToCheck = itemCheck;
            itemReward = reward;
        }

        @NotNull
        @Override
        public ObjectArrayList<ItemStack> doApply(ObjectArrayList<ItemStack> generatedLoot, LootContext context) {
            //
            // Additional conditions can be checked, though as much as possible should be parameterized via JSON data.
            // It is better to write a new ILootCondition implementation than to do things here.
            //
            int numSeeds = 0;
            generatedLoot.stream().filter(stack -> stack.getItem() == itemToCheck)
                   .forEach(stack -> numSeeds+=stack.getCount())
            if(numSeeds >= numSeedsToConvert) {
                generatedLoot.removeIf(x -> x.getItem() == itemToCheck);
                generatedLoot.add(new ItemStack(itemReward, (numSeeds/numSeedsToConvert)));
                numSeeds = numSeeds%numSeedsToConvert;
                if(numSeeds > 0)
                    generatedLoot.add(new ItemStack(itemToCheck, numSeeds));
            }
            return generatedLoot;
        }

        @Override
        public Codec<? extends IGlobalLootModifier> codec() {
            return CODEC.get();
        }
    }

The critical portion is the doApply method.

This method is only called if the conditions specified return true and the modder is now able to make the modifications they desire. In this case we can see that the number of itemToCheck meets or exceeds the numSeedsToConvert before modifying the list by adding an itemReward and removing any excess itemToCheck stacks, matching the previously mentioned effects: When a wheat block is harvested with shears, if enough seeds are generated as loot, they are converted to additional wheat instead.

Utilize GlobalLootModifierProvider for data generation.

Additional examples can be found on the Forge Git repository, including silk touch and smelting effects.