diff --git a/fabric-data-generation-api-v1/build.gradle b/fabric-data-generation-api-v1/build.gradle index e9cd3fc74c..2524d917d7 100644 --- a/fabric-data-generation-api-v1/build.gradle +++ b/fabric-data-generation-api-v1/build.gradle @@ -10,6 +10,7 @@ moduleDependencies(project, [ testDependencies(project, [ ':fabric-item-group-api-v1', + ':fabric-loot-api-v3', ':fabric-object-builder-api-v1' ]) diff --git a/fabric-data-generation-api-v1/src/testmod/generated/data/fabric-data-gen-api-v1-testmod/fabric/loot_modifier/modifier_a.json b/fabric-data-generation-api-v1/src/testmod/generated/data/fabric-data-gen-api-v1-testmod/fabric/loot_modifier/modifier_a.json new file mode 100644 index 0000000000..fa36aecfe0 --- /dev/null +++ b/fabric-data-generation-api-v1/src/testmod/generated/data/fabric-data-gen-api-v1-testmod/fabric/loot_modifier/modifier_a.json @@ -0,0 +1,34 @@ +{ + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:apple" + } + ], + "rolls": 1.0 + } + ], + "target": { + "type": "fabric:require_all", + "children": [ + { + "type": "fabric:source", + "sources": "any_builtin" + }, + { + "type": "fabric:loot_table_id", + "loot_tables": [ + "minecraft:blocks/acacia_button" + ] + } + ] + } +} \ No newline at end of file diff --git a/fabric-data-generation-api-v1/src/testmod/generated/data/fabric-data-gen-api-v1-testmod/fabric/loot_modifier/modifier_b.json b/fabric-data-generation-api-v1/src/testmod/generated/data/fabric-data-gen-api-v1-testmod/fabric/loot_modifier/modifier_b.json new file mode 100644 index 0000000000..2d0fe1b4e4 --- /dev/null +++ b/fabric-data-generation-api-v1/src/testmod/generated/data/fabric-data-gen-api-v1-testmod/fabric/loot_modifier/modifier_b.json @@ -0,0 +1,54 @@ +{ + "functions": [ + { + "add": false, + "count": 10.0, + "function": "minecraft:set_count" + } + ], + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:golden_apple" + } + ], + "rolls": 1.0 + } + ], + "target": { + "type": "fabric:require_any", + "children": [ + { + "type": "fabric:loot_table_id", + "loot_tables": [ + "minecraft:blocks/anvil" + ] + }, + { + "type": "fabric:require_all", + "children": [ + { + "type": "fabric:loot_table_id", + "loot_tables": [ + "minecraft:entities/allay" + ] + }, + { + "type": "fabric:source", + "sources": [ + "data_pack" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/fabric-data-generation-api-v1/src/testmod/java/net/fabricmc/fabric/test/datagen/DataGeneratorTestEntrypoint.java b/fabric-data-generation-api-v1/src/testmod/java/net/fabricmc/fabric/test/datagen/DataGeneratorTestEntrypoint.java index 8b8a77ff89..bd2de502c3 100644 --- a/fabric-data-generation-api-v1/src/testmod/java/net/fabricmc/fabric/test/datagen/DataGeneratorTestEntrypoint.java +++ b/fabric-data-generation-api-v1/src/testmod/java/net/fabricmc/fabric/test/datagen/DataGeneratorTestEntrypoint.java @@ -77,7 +77,9 @@ import net.minecraft.world.level.storage.loot.LootPool; import net.minecraft.world.level.storage.loot.LootTable; import net.minecraft.world.level.storage.loot.entries.LootItem; +import net.minecraft.world.level.storage.loot.functions.SetItemCountFunction; import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets; +import net.minecraft.world.level.storage.loot.predicates.ExplosionCondition; import net.minecraft.world.level.storage.loot.predicates.LootItemBlockStatePropertyCondition; import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; import net.minecraft.world.level.storage.loot.providers.number.ConstantValue; @@ -96,6 +98,9 @@ import net.fabricmc.fabric.api.datagen.v1.provider.FabricRecipeProvider; import net.fabricmc.fabric.api.datagen.v1.provider.FabricTagProvider; import net.fabricmc.fabric.api.datagen.v1.provider.SimpleFabricLootTableProvider; +import net.fabricmc.fabric.api.loot.v3.LootModifier; +import net.fabricmc.fabric.api.loot.v3.LootModifierTarget; +import net.fabricmc.fabric.api.loot.v3.LootTableSource; import net.fabricmc.fabric.api.recipe.v1.ingredient.DefaultCustomIngredients; import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition; import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditions; @@ -123,6 +128,7 @@ public void onInitializeDataGenerator(FabricDataGenerator dataGenerator) { pack.addProvider(JapaneseLangProvider::new); pack.addProvider(TestDynamicRegistryProvider::new); pack.addProvider(TestPredicateProvider::new); + pack.addProvider(TestLootModifierProvider::new); pack.addProvider(TestCustomCodecProvider::new); TestBlockTagProvider blockTagProvider = pack.addProvider(TestBlockTagProvider::new); @@ -496,6 +502,44 @@ public String getName() { } } + private static class TestLootModifierProvider extends FabricCodecDataProvider { + private TestLootModifierProvider(FabricDataOutput dataOutput, CompletableFuture registriesFuture) { + super(dataOutput, registriesFuture, PackOutput.Target.DATA_PACK, LootModifier.DATA_DIRECTORY, LootModifier.CODEC); + } + + @Override + protected void configure(BiConsumer provider, HolderLookup.Provider lookup) { + LootModifier modifierA = LootModifier.builder() + .target(LootModifierTarget.all(LootModifierTarget.builtinSource(), LootModifierTarget.lootTable(net.minecraft.world.level.block.Blocks.ACACIA_BUTTON.getLootTable().orElseThrow()))) + .pools(LootPool.lootPool() + .add(LootItem.lootTableItem(Items.APPLE)) + .when(ExplosionCondition.survivesExplosion()) + .build()) + .build(); + LootModifier modifierB = LootModifier.builder() + .target(LootModifierTarget.any( + LootModifierTarget.lootTable(net.minecraft.world.level.block.Blocks.ANVIL), + LootModifierTarget.all( + LootModifierTarget.lootTable(EntityType.ALLAY), + LootModifierTarget.source(LootTableSource.DATA_PACK) + ) + )) + .pools(LootPool.lootPool() + .add(LootItem.lootTableItem(Items.GOLDEN_APPLE)) + .when(ExplosionCondition.survivesExplosion())) + .functions(SetItemCountFunction.setCount(ConstantValue.exactly(10))) + .build(); + + provider.accept(Identifier.fromNamespaceAndPath(MOD_ID, "modifier_a"), modifierA); + provider.accept(Identifier.fromNamespaceAndPath(MOD_ID, "modifier_b"), modifierB); + } + + @Override + public String getName() { + return "Loot Modifiers"; + } + } + private static class TestCustomCodecProvider extends FabricCodecDataProvider { private TestCustomCodecProvider(FabricDataOutput dataOutput, CompletableFuture registriesFuture) { super(dataOutput, registriesFuture, PackOutput.Target.DATA_PACK, "biome_entry", Entry.CODEC); diff --git a/fabric-loot-api-v3/build.gradle b/fabric-loot-api-v3/build.gradle index f2db1b51aa..38f49d78ad 100644 --- a/fabric-loot-api-v3/build.gradle +++ b/fabric-loot-api-v3/build.gradle @@ -2,5 +2,6 @@ version = getSubprojectVersion(project) moduleDependencies(project, [ 'fabric-api-base', + 'fabric-registry-sync-v0', 'fabric-resource-loader-v0' ]) diff --git a/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/LootModifier.java b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/LootModifier.java new file mode 100644 index 0000000000..7bd7348d86 --- /dev/null +++ b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/LootModifier.java @@ -0,0 +1,164 @@ +package net.fabricmc.fabric.api.loot.v3; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import com.mojang.serialization.Codec; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +import net.minecraft.world.level.storage.loot.LootPool; +import net.minecraft.world.level.storage.loot.functions.LootItemFunction; + +import net.fabricmc.fabric.impl.loot.LootModifierImpl; + +/** + * A data-driven modifier for loot tables. + * Loot modifiers can add new {@linkplain LootPool pools} and {@linkplain LootItemFunction functions} + * to loot tables without overriding loot table data files. + * + *

Loot modifiers can be defined in the {@link #DATA_DIRECTORY data/[namespace]/fabric/loot_modifier} + * directory as JSON files with the following format: TODO add format + * {@snippet lang=json : + * "something" + * } + * You can also generate loot modifier files using {@link net.fabricmc.fabric.api.datagen.v1.provider.FabricCodecDataProvider + * FabricCodecDataProvider}. + * New modifiers can be created with a {@linkplain #builder() builder}. + * + *

Loot modifiers determine which loot tables they modify by using a {@link LootModifierTarget}. + * You can use them to target specific loot tables or only builtin loot tables, just like with {@link LootTableEvents#MODIFY}. + */ +@ApiStatus.NonExtendable +public interface LootModifier { + /** + * The data directory for loot modifiers. + */ + String DATA_DIRECTORY = "fabric/loot_modifier"; + + /** + * The loot modifier codec. + */ + Codec CODEC = LootModifierImpl.CODEC; + + /** + * {@return the target of this loot modifier} + */ + LootModifierTarget target(); + + /** + * {@return the pools added by this loot modifier} + */ + List pools(); + + /** + * {@return the functions added by this loot modifier} + */ + List functions(); + + /** + * {@return a new loot modifier builder} + */ + static Builder builder() { + return new Builder(); + } + + /** + * A builder for {@link LootModifier}. + */ + final class Builder { + private @Nullable LootModifierTarget target; + private final List pools = new ArrayList<>(); + private final List functions = new ArrayList<>(); + + /** + * Sets the loot modifier target. + * + * @param target the target + * @return this builder + */ + public Builder target(LootModifierTarget target) { + Objects.requireNonNull(target, "Loot modifier target cannot be null"); + this.target = target; + return this; + } + + /** + * Adds pools to this builder. + * + * @param pools the pools to add + * @return this builder + */ + public Builder pools(LootPool.Builder... pools) { + return pools(Arrays.stream(pools).map(LootPool.Builder::build).toList()); + } + + /** + * Adds pools to this builder. + * + * @param pools the pools to add + * @return this builder + */ + public Builder pools(LootPool... pools) { + return pools(Arrays.asList(pools)); + } + + /** + * Adds pools to this builder. + * + * @param pools the pools to add + * @return this builder + */ + public Builder pools(Collection pools) { + this.pools.addAll(pools); + return this; + } + + /** + * Adds functions to this builder. + * + * @param functions the functions to add + * @return this builder + */ + public Builder functions(LootItemFunction.Builder... functions) { + return functions(Arrays.stream(functions).map(LootItemFunction.Builder::build).toList()); + } + + /** + * Adds functions to this builder. + * + * @param functions the functions to add + * @return this builder + */ + public Builder functions(LootItemFunction... functions) { + return functions(Arrays.asList(functions)); + } + + /** + * Adds functions to this builder. + * + * @param functions the functions to add + * @return this builder + */ + public Builder functions(Collection functions) { + this.functions.addAll(functions); + return this; + } + + // TODO: test datagenning + + /** + * Builds a loot modifier from this builder. + * + * @return the created modifier + * @throws NullPointerException if the target hasn't been set + */ + public LootModifier build() { + Objects.requireNonNull(target, "Loot modifier target not set"); + return new LootModifierImpl(target, pools, functions); + } + } +} diff --git a/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/LootModifierTarget.java b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/LootModifierTarget.java new file mode 100644 index 0000000000..f0635e8999 --- /dev/null +++ b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/LootModifierTarget.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.loot.v3; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; + +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.storage.loot.LootTable; + +import net.fabricmc.fabric.impl.loot.LootModifierTargets; + +/** + * A loot modifier target checks which loot tables should be modified + * by a {@linkplain LootModifier loot modifier}. + * + *

There are builtin targets for {@linkplain #lootTable(ResourceKey...) specific loot table ids}, + * {@linkplain #source(LootTableSource...) specific sources} and combining different targets together + * ({@link #all(LootModifierTarget...) all()} and {@link #any(LootModifierTarget...) any()}). + * + *

New target types can be registered by registering the codec using {@link #registerType(Identifier, MapCodec)}. + */ +public interface LootModifierTarget { + /** + * The loot modifier target codec. + */ + Codec CODEC = LootModifierTargets.TARGET_CODEC; + + /** + * Registers a new loot modifier target type. + * + * @param id the id + * @param codec the map codec for the targets + */ + static void registerType(Identifier id, MapCodec codec) { + LootModifierTargets.registerType(id, codec); + } + + /** + * Checks whether a loot table should be modified. + * + * @param key the loot table's key + * @param source the loot table's source + * @return {@code true} if the table should be modified, {@code false} otherwise + */ + boolean shouldModify(ResourceKey key, LootTableSource source); + + /** + * {@return the map codec of this target} + */ + MapCodec codec(); + + /** + * Returns a loot modifier target that matches specific loot table keys. + * + * @param tables the keys to match + * @return the target + */ + @SafeVarargs + static LootModifierTarget lootTable(ResourceKey... tables) { + return lootTable(Arrays.asList(tables)); + } + + /** + * Returns a loot modifier target that matches specific block loot tables. + * + * @param blocks the blocks to match + * @return the target + */ + static LootModifierTarget lootTable(Block... blocks) { + return lootTable(Arrays.stream(blocks).map(block -> block.getLootTable().orElseThrow()).toList()); + } + + /** + * Returns a loot modifier target that matches specific entity loot tables. + * + * @param entityTypes the entity types to match + * @return the target + */ + static LootModifierTarget lootTable(EntityType... entityTypes) { + return lootTable(Arrays.stream(entityTypes).map(type -> type.getDefaultLootTable().orElseThrow()).toList()); + } + + /** + * Returns a loot modifier target that matches specific loot table keys. + * + * @param tables the keys to match + * @return the target + */ + static LootModifierTarget lootTable(Collection> tables) { + return new LootModifierTargets.LootTableId(List.copyOf(tables)); + } + + /** + * Returns a loot modifier target that requires any child target to match. + * + * @param children the child targets, cannot be empty + * @return the target + */ + static LootModifierTarget any(LootModifierTarget... children) { + return any(Arrays.asList(children)); + } + + /** + * Returns a loot modifier target that requires any child target to match. + * + * @param children the child targets, cannot be empty + * @return the target + */ + static LootModifierTarget any(Collection children) { + return new LootModifierTargets.RequireAny(List.copyOf(children)); + } + + /** + * Returns a loot modifier target that requires all child targets to match. + * + * @param children the child targets, cannot be empty + * @return the target + */ + static LootModifierTarget all(LootModifierTarget... children) { + return all(Arrays.asList(children)); + } + + /** + * Returns a loot modifier target that requires all child targets to match. + * + * @param children the child targets, cannot be empty + * @return the target + */ + static LootModifierTarget all(Collection children) { + return new LootModifierTargets.RequireAll(List.copyOf(children)); + } + + /** + * Returns a loot modifier target that matches specific {@linkplain LootTableSource loot table sources}. + * + * @param sources the sources to match + * @return the target + */ + static LootModifierTarget source(LootTableSource... sources) { + return source(Arrays.asList(sources)); + } + + /** + * Returns a loot modifier target that matches {@linkplain LootTableSource#isBuiltin() builtin} loot table sources. + * + * @return the target + */ + static LootModifierTarget builtinSource() { + return source(LootModifierTargets.Source.BUILTIN_SOURCES); + } + + /** + * Returns a loot modifier target that matches specific {@linkplain LootTableSource loot table sources}. + * + * @param sources the sources to match + * @return the target + */ + static LootModifierTarget source(Collection sources) { + return new LootModifierTargets.Source(Set.copyOf(sources)); + } +} diff --git a/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/LootTableEvents.java b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/LootTableEvents.java index 9278138a6d..5b88a36ba7 100644 --- a/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/LootTableEvents.java +++ b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/LootTableEvents.java @@ -85,6 +85,8 @@ private LootTableEvents() { * } * }); * } + * + * @see LootModifier data-driven loot modifiers */ public static final Event MODIFY = EventFactory.createArrayBacked(Modify.class, listeners -> (key, tableBuilder, source, registries) -> { for (Modify listener : listeners) { diff --git a/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/LootTableSource.java b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/LootTableSource.java index 6e2af89159..cd54ae0f68 100644 --- a/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/LootTableSource.java +++ b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/LootTableSource.java @@ -16,14 +16,18 @@ package net.fabricmc.fabric.api.loot.v3; +import com.mojang.serialization.Codec; + +import net.minecraft.util.StringRepresentable; + /** * Describes where a loot table has been loaded from. */ -public enum LootTableSource { +public enum LootTableSource implements StringRepresentable { /** * A loot table loaded from the default data pack. */ - VANILLA(true), + VANILLA(true, "vanilla"), /** * A loot table loaded from mods' bundled resources. @@ -31,22 +35,29 @@ public enum LootTableSource { *

This includes the additional builtin data packs registered by mods * with Fabric Resource Loader. */ - MOD(true), + MOD(true, "mod"), /** * A loot table loaded from an external data pack. */ - DATA_PACK(false), + DATA_PACK(false, "data_pack"), /** * A loot table created in {@link LootTableEvents#REPLACE}. */ - REPLACED(false); + REPLACED(false, "replaced"); + + /** + * A codec that serializes sources in their {@link StringRepresentable} format (the enum name in lowercase). + */ + public static final Codec CODEC = StringRepresentable.fromEnum(LootTableSource::values); private final boolean builtin; + private final String id; - LootTableSource(boolean builtin) { + LootTableSource(boolean builtin, String id) { this.builtin = builtin; + this.id = id; } /** @@ -60,4 +71,9 @@ public enum LootTableSource { public boolean isBuiltin() { return builtin; } + + @Override + public String getSerializedName() { + return id; + } } diff --git a/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/package-info.java b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/package-info.java index 0f25106661..8a5baf7baa 100644 --- a/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/package-info.java +++ b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/api/loot/v3/package-info.java @@ -25,6 +25,10 @@ * {@link net.fabricmc.fabric.api.loot.v3.LootTableSource}. This is useful when you only want to modify * loot tables from mods or vanilla, but not user-created data packs. * + *

Modifiers

+ * {@linkplain net.fabricmc.fabric.api.loot.v3.LootModifier Loot modifiers} can do simple additions to loot tables + * directly from data packs without having to use code. They can add new loot pools and loot item functions. + * *

Extended loot table and pool builders

* This API has injected interfaces to add useful methods to * {@linkplain net.fabricmc.fabric.api.loot.v3.FabricLootTableBuilder loot table} and diff --git a/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/impl/loot/LootModifierImpl.java b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/impl/loot/LootModifierImpl.java new file mode 100644 index 0000000000..e1367c26be --- /dev/null +++ b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/impl/loot/LootModifierImpl.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.loot; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.FileToIdConverter; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.RegistryOps; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener; +import net.minecraft.world.level.storage.loot.LootPool; +import net.minecraft.world.level.storage.loot.LootTable; +import net.minecraft.world.level.storage.loot.functions.LootItemFunction; +import net.minecraft.world.level.storage.loot.functions.LootItemFunctions; + +import net.fabricmc.fabric.api.loot.v3.FabricLootTableBuilder; +import net.fabricmc.fabric.api.loot.v3.LootModifier; +import net.fabricmc.fabric.api.loot.v3.LootModifierTarget; +import net.fabricmc.fabric.api.loot.v3.LootTableEvents; +import net.fabricmc.fabric.api.loot.v3.LootTableSource; + +public record LootModifierImpl(LootModifierTarget target, List pools, List functions) implements LootModifier { + private static final Logger LOGGER = LoggerFactory.getLogger(LootModifierImpl.class); + + public static final Codec CODEC = RecordCodecBuilder.create(builder -> builder.group( + LootModifierTarget.CODEC.fieldOf("target").forGetter(LootModifier::target), + LootPool.CODEC.listOf().optionalFieldOf("pools", List.of()).forGetter(LootModifier::pools), + LootItemFunctions.ROOT_CODEC.listOf().optionalFieldOf("functions", List.of()).forGetter(LootModifier::functions) + ).apply(builder, LootModifierImpl::new)); + + public static void modifyLootTables(ResourceManager resourceManager, RegistryOps registryOps, HolderLookup.Provider registries, Map lootTables) { + Map modifiers = loadLootModifiers(resourceManager, registryOps); + lootTables.replaceAll((id, table) -> modifyLootTable(modifiers, registries, id, table)); + } + + private static Map loadLootModifiers(ResourceManager resourceManager, RegistryOps registryOps) { + Map modifiers = new HashMap<>(); + SimpleJsonResourceReloadListener.scanDirectory(resourceManager, FileToIdConverter.json(LootModifier.DATA_DIRECTORY), registryOps, CODEC, modifiers); + LOGGER.debug("Loaded {} loot modifiers", modifiers.size()); + return modifiers; + } + + private static LootTable modifyLootTable(Map modifiers, HolderLookup.Provider registries, Identifier id, LootTable table) { + ResourceKey key = ResourceKey.create(Registries.LOOT_TABLE, id); + // Populated inside SimpleJsonResourceReloadListenerMixin + LootTableSource source = LootUtil.SOURCES.get().getOrDefault(id, LootTableSource.DATA_PACK); + // Invoke the REPLACE event for the current loot table. + LootTable replacement = LootTableEvents.REPLACE.invoker().replaceLootTable(key, table, source, registries); + + if (replacement != null) { + // Set the loot table to MODIFY to be the replacement loot table. + // The MODIFY event will also see it as a replaced loot table via the source. + table = replacement; + source = LootTableSource.REPLACED; + } + + // Turn the current table into a modifiable builder, apply loot modifiers and invoke the MODIFY event. + LootTable.Builder builder = FabricLootTableBuilder.copyOf(table); + + for (LootModifier modifier : modifiers.values()) { + applyModifier(builder, key, source, modifier); + } + + LootTableEvents.MODIFY.invoker().modifyLootTable(key, builder, source, registries); + return builder.build(); + } + + private static void applyModifier(LootTable.Builder builder, ResourceKey key, LootTableSource source, LootModifier modifier) { + if (modifier.target().shouldModify(key, source)) { + builder.pools(modifier.pools()); + builder.apply(modifier.functions()); + } + } +} diff --git a/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/impl/loot/LootModifierTargets.java b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/impl/loot/LootModifierTargets.java new file mode 100644 index 0000000000..47dd86e1f2 --- /dev/null +++ b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/impl/loot/LootModifierTargets.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.loot; + +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import com.google.common.base.Preconditions; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.storage.loot.LootTable; + +import net.fabricmc.fabric.api.loot.v3.LootModifierTarget; +import net.fabricmc.fabric.api.loot.v3.LootTableSource; + +public final class LootModifierTargets { + // TODO: or registry? + private static final BiMap> TARGET_CODECS = HashBiMap.create(); + + public static final Codec TARGET_CODEC = Identifier.CODEC.dispatch( + target -> TARGET_CODECS.inverse().get(target.codec()), + TARGET_CODECS::get + ); + + static { + registerType(Identifier.fromNamespaceAndPath("fabric", "require_all"), RequireAll.MAP_CODEC); + registerType(Identifier.fromNamespaceAndPath("fabric", "require_any"), RequireAny.MAP_CODEC); + registerType(Identifier.fromNamespaceAndPath("fabric", "loot_table_id"), LootTableId.MAP_CODEC); + registerType(Identifier.fromNamespaceAndPath("fabric", "source"), Source.MAP_CODEC); + } + + public static void registerType(Identifier id, MapCodec codec) { + Objects.requireNonNull(id, "Loot modifier target id cannot be null"); + Objects.requireNonNull(codec, "Loot modifier target codec cannot be null"); + + if (TARGET_CODECS.putIfAbsent(id, codec) != null) { + throw new IllegalStateException("Loot modifier target " + id + " has already been registered"); + } + } + + public record RequireAll(List children) implements LootModifierTarget { + public static final MapCodec MAP_CODEC = TARGET_CODEC.listOf(1, Integer.MAX_VALUE).fieldOf("children").xmap(RequireAll::new, RequireAll::children); + + public RequireAll { + Preconditions.checkArgument(!children.isEmpty(), "require_all loot modifier target must have children"); + } + + @Override + public boolean shouldModify(ResourceKey key, LootTableSource source) { + for (LootModifierTarget child : children) { + if (!child.shouldModify(key, source)) { + return false; + } + } + + return true; + } + + @Override + public MapCodec codec() { + return MAP_CODEC; + } + } + + public record RequireAny(List children) implements LootModifierTarget { + public static final MapCodec MAP_CODEC = TARGET_CODEC.listOf(1, Integer.MAX_VALUE).fieldOf("children").xmap(RequireAny::new, RequireAny::children); + + public RequireAny { + Preconditions.checkArgument(!children.isEmpty(), "require_any loot modifier target must have children"); + } + + @Override + public boolean shouldModify(ResourceKey key, LootTableSource source) { + for (LootModifierTarget child : children) { + if (child.shouldModify(key, source)) { + return true; + } + } + + return false; + } + + @Override + public MapCodec codec() { + return MAP_CODEC; + } + } + + public record LootTableId(List> lootTables) implements LootModifierTarget { + public static final MapCodec MAP_CODEC = ResourceKey.codec(Registries.LOOT_TABLE).listOf().fieldOf("loot_tables").xmap(LootTableId::new, LootTableId::lootTables); + + @Override + public boolean shouldModify(ResourceKey key, LootTableSource source) { + return lootTables.contains(key); + } + + @Override + public MapCodec codec() { + return MAP_CODEC; + } + } + + public record Source(Set sources) implements LootModifierTarget { + private static final String BUILTIN_SOURCES_KEY = "any_builtin"; + public static final Set BUILTIN_SOURCES = Set.of(LootTableSource.VANILLA, LootTableSource.MOD); + + private static final Codec> SOURCE_LIST_CODEC = + Codec.either( + LootTableSource.CODEC.listOf().xmap(Set::copyOf, List::copyOf), + Codec.stringResolver(sources -> BUILTIN_SOURCES.equals(sources) ? BUILTIN_SOURCES_KEY : null, key -> BUILTIN_SOURCES_KEY.equals(key) ? BUILTIN_SOURCES : null) + ) + .xmap(Either::unwrap, sources -> BUILTIN_SOURCES.equals(sources) ? Either.right(sources) : Either.left(sources)); + public static final MapCodec MAP_CODEC = SOURCE_LIST_CODEC.fieldOf("sources").xmap(Source::new, Source::sources); + + @Override + public boolean shouldModify(ResourceKey key, LootTableSource source) { + return sources.contains(source); + } + + @Override + public MapCodec codec() { + return MAP_CODEC; + } + } +} diff --git a/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/mixin/loot/ReloadableServerRegistriesMixin.java b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/mixin/loot/ReloadableServerRegistriesMixin.java index 5e22fb5e4c..b0eff891de 100644 --- a/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/mixin/loot/ReloadableServerRegistriesMixin.java +++ b/fabric-loot-api-v3/src/main/java/net/fabricmc/fabric/mixin/loot/ReloadableServerRegistriesMixin.java @@ -41,17 +41,15 @@ import net.minecraft.core.registries.Registries; import net.minecraft.resources.Identifier; import net.minecraft.resources.RegistryOps; -import net.minecraft.resources.ResourceKey; import net.minecraft.server.RegistryLayer; import net.minecraft.server.ReloadableServerRegistries; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.world.level.storage.loot.LootDataType; import net.minecraft.world.level.storage.loot.LootTable; -import net.fabricmc.fabric.api.loot.v3.FabricLootTableBuilder; import net.fabricmc.fabric.api.loot.v3.LootTableEvents; -import net.fabricmc.fabric.api.loot.v3.LootTableSource; import net.fabricmc.fabric.impl.loot.FabricLootTable; +import net.fabricmc.fabric.impl.loot.LootModifierImpl; import net.fabricmc.fabric.impl.loot.LootUtil; /** @@ -63,52 +61,29 @@ abstract class ReloadableServerRegistriesMixin { * Due to possible cross-thread handling, this uses WeakHashMap instead of ThreadLocal. */ @Unique - private static final WeakHashMap, HolderLookup.Provider> WRAPPERS = new WeakHashMap<>(); + private static final WeakHashMap, HolderLookup.Provider> REGISTRIES_BY_OPS = new WeakHashMap<>(); @WrapOperation(method = "reload", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/HolderLookup$Provider;createSerializationContext(Lcom/mojang/serialization/DynamicOps;)Lnet/minecraft/resources/RegistryOps;")) private static RegistryOps storeOps(HolderLookup.Provider registries, DynamicOps ops, Operation> original) { RegistryOps created = original.call(registries, ops); - WRAPPERS.put(created, registries); + REGISTRIES_BY_OPS.put(created, registries); return created; } @WrapOperation(method = "reload", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenApplyAsync(Ljava/util/function/Function;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;")) private static CompletableFuture> removeOps(CompletableFuture>> future, Function>, ? extends LayeredRegistryAccess> fn, Executor executor, Operation>> original, @Local RegistryOps ops) { return original.call(future.thenApply(v -> { - WRAPPERS.remove(ops); + REGISTRIES_BY_OPS.remove(ops); return v; }), fn, executor); } + @SuppressWarnings("unchecked") @Inject(method = "method_61240", at = @At(value = "INVOKE", target = "Ljava/util/Map;forEach(Ljava/util/function/BiConsumer;)V")) private static void modifyLootTable(LootDataType lootDataType, ResourceManager resourceManager, RegistryOps registryOps, CallbackInfoReturnable> cir, @Local Map map) { - map.replaceAll((identifier, t) -> modifyLootTable(t, identifier, registryOps)); - } - - @Unique - private static T modifyLootTable(T value, Identifier id, RegistryOps ops) { - if (!(value instanceof LootTable table)) return value; - - ResourceKey key = ResourceKey.create(Registries.LOOT_TABLE, id); - // Populated above. - HolderLookup.Provider registries = WRAPPERS.get(ops); - // Populated inside SimpleJsonResourceReloadListenerMixin - LootTableSource source = LootUtil.SOURCES.get().getOrDefault(id, LootTableSource.DATA_PACK); - // Invoke the REPLACE event for the current loot table. - LootTable replacement = LootTableEvents.REPLACE.invoker().replaceLootTable(key, table, source, registries); - - if (replacement != null) { - // Set the loot table to MODIFY to be the replacement loot table. - // The MODIFY event will also see it as a replaced loot table via the source. - table = replacement; - source = LootTableSource.REPLACED; + if (lootDataType.registryKey() == (Object) Registries.LOOT_TABLE) { + LootModifierImpl.modifyLootTables(resourceManager, registryOps, REGISTRIES_BY_OPS.get(registryOps), (Map) map); } - - // Turn the current table into a modifiable builder and invoke the MODIFY event. - LootTable.Builder builder = FabricLootTableBuilder.copyOf(table); - LootTableEvents.MODIFY.invoker().modifyLootTable(key, builder, source, registries); - - return (T) builder.build(); } @SuppressWarnings("unchecked") diff --git a/fabric-loot-api-v3/src/testmod/java/net/fabricmc/fabric/test/loot/LootGameTest.java b/fabric-loot-api-v3/src/testmod/java/net/fabricmc/fabric/test/loot/LootGameTest.java index be3a6af35d..5b615347ce 100644 --- a/fabric-loot-api-v3/src/testmod/java/net/fabricmc/fabric/test/loot/LootGameTest.java +++ b/fabric-loot-api-v3/src/testmod/java/net/fabricmc/fabric/test/loot/LootGameTest.java @@ -109,4 +109,13 @@ public void testInlineTableModifyDrops(GameTestHelper context) { context.assertTrue(seenAtStart < seenAtEnd, Component.literal("inline loot table should've been processed by MODIFY_DROPS")); context.succeed(); } + + @GameTest + public void testLootModifier(GameTestHelper helper) { + // Green wool should drop 10 green wools and 10 poisonous potatoes + LootTableDrops drops = LootTableDrops.block(helper, Blocks.GREEN_WOOL).drop(); + drops.assertContains(new ItemStack(Items.GREEN_WOOL, 10)); + drops.assertContains(new ItemStack(Items.POISONOUS_POTATO, 10)); + helper.succeed(); + } } diff --git a/fabric-loot-api-v3/src/testmod/java/net/fabricmc/fabric/test/loot/LootTableDrops.java b/fabric-loot-api-v3/src/testmod/java/net/fabricmc/fabric/test/loot/LootTableDrops.java index c707a73c0d..a0d6a86e8d 100644 --- a/fabric-loot-api-v3/src/testmod/java/net/fabricmc/fabric/test/loot/LootTableDrops.java +++ b/fabric-loot-api-v3/src/testmod/java/net/fabricmc/fabric/test/loot/LootTableDrops.java @@ -108,6 +108,11 @@ public static Builder entity(GameTestHelper context, EntityType type) { .set(LootContextParams.DAMAGE_SOURCE, context.getLevel().damageSources().generic()); } + @Override + public String toString() { + return name.getString() + ": " + stacks; + } + public static final class Builder { private final GameTestHelper testContext; private final Component name; diff --git a/fabric-loot-api-v3/src/testmod/resources/data/fabric-loot-api-v3-testmod/fabric/loot_modifier/green_wool_potatoes.json b/fabric-loot-api-v3/src/testmod/resources/data/fabric-loot-api-v3-testmod/fabric/loot_modifier/green_wool_potatoes.json new file mode 100644 index 0000000000..a607fa5bd7 --- /dev/null +++ b/fabric-loot-api-v3/src/testmod/resources/data/fabric-loot-api-v3-testmod/fabric/loot_modifier/green_wool_potatoes.json @@ -0,0 +1,37 @@ +{ + "target": { + "type": "fabric:require_all", + "children": [ + { + "type": "fabric:loot_table_id", + "loot_tables": ["minecraft:blocks/green_wool"] + }, + { + "type": "fabric:source", + "sources": "any_builtin" + } + ] + }, + "pools": [ + { + "rolls": 1, + "entries": [ + { + "type": "minecraft:item", + "name": "minecraft:poisonous_potato" + } + ], + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ] + } + ], + "functions": [ + { + "function": "minecraft:set_count", + "count": 10 + } + ] +}