package net.minecraft.world.level.storage.loot; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.mojang.logging.LogUtils; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Consumer; import net.minecraft.Util; import net.minecraft.core.Holder; import net.minecraft.core.registries.Registries; import net.minecraft.resources.RegistryFileCodec; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; import net.minecraft.util.context.ContextKeySet; import net.minecraft.world.Container; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.storage.loot.functions.FunctionUserBuilder; import net.minecraft.world.level.storage.loot.functions.LootItemFunction; import net.minecraft.world.level.storage.loot.functions.LootItemFunctions; import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets; import org.slf4j.Logger; public class LootTable { private static final Logger LOGGER = LogUtils.getLogger(); public static final Codec> KEY_CODEC = ResourceKey.codec(Registries.LOOT_TABLE); public static final ContextKeySet DEFAULT_PARAM_SET = LootContextParamSets.ALL_PARAMS; public static final long RANDOMIZE_SEED = 0L; public static final Codec DIRECT_CODEC = Codec.lazyInitialized( () -> RecordCodecBuilder.create( instance -> instance.group( LootContextParamSets.CODEC.lenientOptionalFieldOf("type", DEFAULT_PARAM_SET).forGetter(lootTable -> lootTable.paramSet), ResourceLocation.CODEC.optionalFieldOf("random_sequence").forGetter(lootTable -> lootTable.randomSequence), LootPool.CODEC.listOf().optionalFieldOf("pools", List.of()).forGetter(lootTable -> lootTable.pools), LootItemFunctions.ROOT_CODEC.listOf().optionalFieldOf("functions", List.of()).forGetter(lootTable -> lootTable.functions) ) .apply(instance, LootTable::new) ) ); public static final Codec> CODEC = RegistryFileCodec.create(Registries.LOOT_TABLE, DIRECT_CODEC); public static final LootTable EMPTY = new LootTable(LootContextParamSets.EMPTY, Optional.empty(), List.of(), List.of()); private final ContextKeySet paramSet; private final Optional randomSequence; private final List pools; private final List functions; private final BiFunction compositeFunction; LootTable(ContextKeySet paramSet, Optional randomSequence, List pools, List functions) { this.paramSet = paramSet; this.randomSequence = randomSequence; this.pools = pools; this.functions = functions; this.compositeFunction = LootItemFunctions.compose(functions); } public static Consumer createStackSplitter(ServerLevel level, Consumer output) { return itemStack -> { if (itemStack.isItemEnabled(level.enabledFeatures())) { if (itemStack.getCount() < itemStack.getMaxStackSize()) { output.accept(itemStack); } else { int i = itemStack.getCount(); while (i > 0) { ItemStack itemStack2 = itemStack.copyWithCount(Math.min(itemStack.getMaxStackSize(), i)); i -= itemStack2.getCount(); output.accept(itemStack2); } } } }; } public void getRandomItemsRaw(LootParams params, Consumer output) { this.getRandomItemsRaw(new LootContext.Builder(params).create(this.randomSequence), output); } /** * Generate items to the given Consumer, ignoring maximum stack size. */ public void getRandomItemsRaw(LootContext context, Consumer output) { LootContext.VisitedEntry visitedEntry = LootContext.createVisitedEntry(this); if (context.pushVisitedElement(visitedEntry)) { Consumer consumer = LootItemFunction.decorate(this.compositeFunction, output, context); for (LootPool lootPool : this.pools) { lootPool.addRandomItems(consumer, context); } context.popVisitedElement(visitedEntry); } else { LOGGER.warn("Detected infinite loop in loot tables"); } } public void getRandomItems(LootParams params, long seed, Consumer output) { this.getRandomItemsRaw( new LootContext.Builder(params).withOptionalRandomSeed(seed).create(this.randomSequence), createStackSplitter(params.getLevel(), output) ); } public void getRandomItems(LootParams params, Consumer output) { this.getRandomItemsRaw(params, createStackSplitter(params.getLevel(), output)); } /** * Generate random items to the given Consumer, ensuring they do not exceed their maximum stack size. */ public void getRandomItems(LootContext contextData, Consumer output) { this.getRandomItemsRaw(contextData, createStackSplitter(contextData.getLevel(), output)); } public ObjectArrayList getRandomItems(LootParams params, RandomSource random) { return this.getRandomItems(new LootContext.Builder(params).withOptionalRandomSource(random).create(this.randomSequence)); } public ObjectArrayList getRandomItems(LootParams params, long seed) { return this.getRandomItems(new LootContext.Builder(params).withOptionalRandomSeed(seed).create(this.randomSequence)); } public ObjectArrayList getRandomItems(LootParams params) { return this.getRandomItems(new LootContext.Builder(params).create(this.randomSequence)); } /** * Generate random items to a List. */ private ObjectArrayList getRandomItems(LootContext context) { ObjectArrayList objectArrayList = new ObjectArrayList<>(); this.getRandomItems(context, objectArrayList::add); return objectArrayList; } public ContextKeySet getParamSet() { return this.paramSet; } /** * Validate this LootTable using the given ValidationContext. */ public void validate(ValidationContext validator) { for (int i = 0; i < this.pools.size(); i++) { ((LootPool)this.pools.get(i)).validate(validator.forChild(".pools[" + i + "]")); } for (int i = 0; i < this.functions.size(); i++) { ((LootItemFunction)this.functions.get(i)).validate(validator.forChild(".functions[" + i + "]")); } } public void fill(Container container, LootParams params, long seed) { LootContext lootContext = new LootContext.Builder(params).withOptionalRandomSeed(seed).create(this.randomSequence); ObjectArrayList objectArrayList = this.getRandomItems(lootContext); RandomSource randomSource = lootContext.getRandom(); List list = this.getAvailableSlots(container, randomSource); this.shuffleAndSplitItems(objectArrayList, list.size(), randomSource); for (ItemStack itemStack : objectArrayList) { if (list.isEmpty()) { LOGGER.warn("Tried to over-fill a container"); return; } if (itemStack.isEmpty()) { container.setItem((Integer)list.remove(list.size() - 1), ItemStack.EMPTY); } else { container.setItem((Integer)list.remove(list.size() - 1), itemStack); } } } /** * Shuffles items by changing their order and splitting stacks */ private void shuffleAndSplitItems(ObjectArrayList stacks, int emptySlotsCount, RandomSource random) { List list = Lists.newArrayList(); Iterator iterator = stacks.iterator(); while (iterator.hasNext()) { ItemStack itemStack = (ItemStack)iterator.next(); if (itemStack.isEmpty()) { iterator.remove(); } else if (itemStack.getCount() > 1) { list.add(itemStack); iterator.remove(); } } while (emptySlotsCount - stacks.size() - list.size() > 0 && !list.isEmpty()) { ItemStack itemStack2 = (ItemStack)list.remove(Mth.nextInt(random, 0, list.size() - 1)); int i = Mth.nextInt(random, 1, itemStack2.getCount() / 2); ItemStack itemStack3 = itemStack2.split(i); if (itemStack2.getCount() > 1 && random.nextBoolean()) { list.add(itemStack2); } else { stacks.add(itemStack2); } if (itemStack3.getCount() > 1 && random.nextBoolean()) { list.add(itemStack3); } else { stacks.add(itemStack3); } } stacks.addAll(list); Util.shuffle(stacks, random); } private List getAvailableSlots(Container inventory, RandomSource random) { ObjectArrayList objectArrayList = new ObjectArrayList<>(); for (int i = 0; i < inventory.getContainerSize(); i++) { if (inventory.getItem(i).isEmpty()) { objectArrayList.add(i); } } Util.shuffle(objectArrayList, random); return objectArrayList; } public static LootTable.Builder lootTable() { return new LootTable.Builder(); } public static class Builder implements FunctionUserBuilder { private final ImmutableList.Builder pools = ImmutableList.builder(); private final ImmutableList.Builder functions = ImmutableList.builder(); private ContextKeySet paramSet = LootTable.DEFAULT_PARAM_SET; private Optional randomSequence = Optional.empty(); public LootTable.Builder withPool(LootPool.Builder lootPool) { this.pools.add(lootPool.build()); return this; } public LootTable.Builder setParamSet(ContextKeySet paramSet) { this.paramSet = paramSet; return this; } public LootTable.Builder setRandomSequence(ResourceLocation randomSequence) { this.randomSequence = Optional.of(randomSequence); return this; } public LootTable.Builder apply(LootItemFunction.Builder builder) { this.functions.add(builder.build()); return this; } public LootTable.Builder unwrap() { return this; } public LootTable build() { return new LootTable(this.paramSet, this.randomSequence, this.pools.build(), this.functions.build()); } } }