package com.xfestudio.mydimension.builder;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.entity.item.ItemEntity;
import java.util.ArrayList;
import java.util.List;
/**
* Ordered material access shared by layer and blueprint jobs. The anchor implementation installs the
* remote bridge during common setup; keeping the bridge here also makes a failed third-party handler
* incapable of preventing the player inventory fallbacks from running.
*/
public final class BuilderMaterials {
public interface RemoteBridge {
Extraction extract(ServerPlayer player, ItemStack scepter, ItemStack requested, int amount);
/** Returns the part that could not be stored. */
ItemStack insert(ServerPlayer player, ItemStack scepter, ItemStack stack);
/**
* Stores a whole operation's output while preserving one remainder per input value.
* Implementations backed by remote endpoints should override this so leases and
* capabilities are resolved once per operation instead of once per dropped stack.
*/
default List<ItemStack> insertAll(ServerPlayer player, ItemStack scepter,
List<ItemStack> stacks) {
List<ItemStack> remainders = new ArrayList<>(stacks.size());
for (ItemStack stack : stacks) {
if (stack.isEmpty()) {
remainders.add(ItemStack.EMPTY);
continue;
}
ItemStack original = stack.copy();
try {
remainders.add(BuilderMaterials.normalizeRemoteRemainder(
original, insert(player, scepter, original.copy())));
} catch (RuntimeException ignored) {
// Isolate one endpoint failure without forgetting remainders from values that
// were already committed successfully earlier in this operation.
remainders.add(original);
}
}
return List.copyOf(remainders);
}
}
public record Extraction(int count, List<ItemStack> stacks) {
public Extraction {
count = Math.max(0, count);
stacks = stacks.stream().filter(stack -> !stack.isEmpty()).map(ItemStack::copy).toList();
}
public static Extraction empty() {
return new Extraction(0, List.of());
}
}
private static final RemoteBridge NO_REMOTE = new RemoteBridge() {
@Override
public Extraction extract(ServerPlayer player, ItemStack scepter, ItemStack requested, int amount) {
return Extraction.empty();
}
@Override
public ItemStack insert(ServerPlayer player, ItemStack scepter, ItemStack stack) {
return stack.copy();
}
};
private static volatile RemoteBridge remote = NO_REMOTE;
private BuilderMaterials() {
}
public static void installRemoteBridge(RemoteBridge bridge) {
remote = bridge == null ? NO_REMOTE : bridge;
}
public static Extraction extract(ServerPlayer player, ItemStack scepter, ItemStack requested, int amount) {
return extract(player, scepter, requested, amount, true);
}
private static Extraction extract(ServerPlayer player, ItemStack scepter, ItemStack requested, int amount,
boolean allowOffhand) {
if (amount <= 0 || requested.isEmpty()) return Extraction.empty();
List<ItemStack> debits = new ArrayList<>();
int remaining = amount;
try {
Extraction fromRemote = remote.extract(player, scepter, requested, remaining);
int accepted = Math.min(remaining, Math.max(0, fromRemote.count()));
if (accepted > 0) {
remaining -= accepted;
appendUpTo(debits, fromRemote.stacks(), accepted);
}
} catch (RuntimeException ignored) {
// A broken capability is isolated to its endpoint; local inventory remains usable.
}
remaining = takeSlotRange(player, requested, remaining, 9, 35, debits, -1);
remaining = takeSlotRange(player, requested, remaining, 0, 8, debits,
player.getInventory().selected);
if (allowOffhand && remaining > 0) {
ItemStack offhand = player.getOffhandItem();
if (matches(offhand, requested) && offhand != scepter) {
int taken = Math.min(remaining, offhand.getCount());
debits.add(offhand.copyWithCount(taken));
offhand.shrink(taken);
remaining -= taken;
}
}
return new Extraction(amount - remaining, debits);
}
/** Stores in anchor order first, then the player's ordinary inventory. */
public static List<ItemStack> insert(ServerPlayer player, ItemStack scepter, List<ItemStack> values) {
return insert(player, scepter, values, true);
}
/** Transaction bookkeeping uses this variant so an exact offhand tool is never replaced by a drop. */
public static List<ItemStack> insertPreservingOffhand(ServerPlayer player, ItemStack scepter,
List<ItemStack> values) {
return insert(player, scepter, values, false);
}
private static List<ItemStack> insert(ServerPlayer player, ItemStack scepter, List<ItemStack> values,
boolean allowOffhand) {
List<ItemStack> aligned = insertAligned(player, scepter, values, allowOffhand);
List<ItemStack> overflow = new ArrayList<>(aligned.size());
for (ItemStack remainder : aligned) {
if (!remainder.isEmpty()) overflow.add(remainder.copy());
}
return overflow;
}
/**
* Stores all values in one material-access session and returns a remainder at
* every corresponding input index. Keeping the alignment lets demolition
* put overflow back at the exact block that produced it without reopening
* every remote anchor for every block.
*/
public static List<ItemStack> insertAligned(ServerPlayer player, ItemStack scepter,
List<ItemStack> values) {
return insertAligned(player, scepter, values, true);
}
/** Aligned batch variant that reserves the offhand for the demolition tool state. */
public static List<ItemStack> insertAlignedPreservingOffhand(ServerPlayer player, ItemStack scepter,
List<ItemStack> values) {
return insertAligned(player, scepter, values, false);
}
private static List<ItemStack> insertAligned(ServerPlayer player, ItemStack scepter,
List<ItemStack> values, boolean allowOffhand) {
List<ItemStack> offered = values.stream().map(stack -> stack.isEmpty()
? ItemStack.EMPTY : stack.copy()).toList();
List<ItemStack> remoteRemainders;
try {
List<ItemStack> remoteInput = offered.stream()
.map(stack -> stack.isEmpty() ? ItemStack.EMPTY : stack.copy()).toList();
remoteRemainders = remote.insertAll(player, scepter, remoteInput);
if (remoteRemainders == null || remoteRemainders.size() != offered.size()) {
remoteRemainders = offered;
}
} catch (RuntimeException ignored) {
remoteRemainders = offered;
}
List<ItemStack> remainders = new ArrayList<>(offered.size());
for (int index = 0; index < offered.size(); index++) {
ItemStack value = offered.get(index);
if (value.isEmpty()) {
remainders.add(ItemStack.EMPTY);
continue;
}
ItemStack remainder = normalizeRemoteRemainder(value, remoteRemainders.get(index));
remainder = insertSlotRange(player, remainder, 9, 35, -1);
remainder = insertSlotRange(player, remainder, 0, 8, player.getInventory().selected);
if (allowOffhand && !remainder.isEmpty()) remainder = insertOffhand(player, remainder, scepter);
remainders.add(remainder.isEmpty() ? ItemStack.EMPTY : remainder.copy());
}
return List.copyOf(remainders);
}
/** Rejects malformed handler output before it can duplicate, inflate or transform an item. */
private static ItemStack normalizeRemoteRemainder(ItemStack offered, ItemStack returned) {
if (offered.isEmpty()) return ItemStack.EMPTY;
if (returned == null || !returned.isEmpty()
&& (!matches(returned, offered) || returned.getCount() > offered.getCount())) {
return offered.copy();
}
return returned.isEmpty() ? ItemStack.EMPTY : returned.copy();
}
/** Conservative exact removal used before undoing demolition. */
public static boolean canAndRemove(ServerPlayer player, ItemStack scepter, List<ItemStack> values) {
return canAndRemove(player, scepter, values, List.of());
}
public static boolean canAndRemove(ServerPlayer player, ItemStack scepter, List<ItemStack> values,
List<ItemEntity> transactionEntities) {
return canAndRemove(player, scepter, values, transactionEntities, false);
}
/** Build replacement credits may occupy the offhand and therefore participate in exact reclaim. */
public static boolean canAndRemoveIncludingOffhand(ServerPlayer player, ItemStack scepter,
List<ItemStack> values,
List<ItemEntity> transactionEntities) {
return canAndRemove(player, scepter, values, transactionEntities, true);
}
private static boolean canAndRemove(ServerPlayer player, ItemStack scepter, List<ItemStack> values,
List<ItemEntity> transactionEntities, boolean allowOffhand) {
List<ItemStack> required = merge(values);
List<ItemStack> removed = new ArrayList<>();
List<EntityDebit> entityDebits = new ArrayList<>();
for (ItemStack wanted : required) {
// The offhand is separately fingerprinted and restored as tool state; accepting it as a drop
// debit would let undo consume the tool and then recreate it from offhandBefore.
Extraction extraction = extract(player, scepter, wanted, wanted.getCount(), allowOffhand);
removed.addAll(extraction.stacks());
int remaining = wanted.getCount() - extraction.count();
for (ItemEntity entity : transactionEntities) {
if (remaining <= 0 || entity.isRemoved() || !matches(entity.getItem(), wanted)) continue;
int alreadyPlanned = entityDebits.stream().filter(debit -> debit.entity == entity)
.mapToInt(debit -> debit.count).sum();
int take = Math.min(remaining, Math.max(0, entity.getItem().getCount() - alreadyPlanned));
if (take > 0) {
entityDebits.add(new EntityDebit(entity, take));
remaining -= take;
}
}
if (remaining > 0) {
// Roll back every debit before reporting the transaction conflict.
List<ItemStack> overflow = insert(player, scepter, removed, allowOffhand);
for (ItemStack stack : overflow) player.drop(stack, false);
return false;
}
}
for (EntityDebit debit : entityDebits) {
ItemStack stack = debit.entity.getItem();
stack.shrink(debit.count);
if (stack.isEmpty()) debit.entity.discard();
}
return true;
}
private static int takeSlotRange(ServerPlayer player, ItemStack requested, int remaining,
int first, int last, List<ItemStack> result, int excluded) {
for (int slot = first; slot <= last && remaining > 0; slot++) {
if (slot == excluded) continue;
ItemStack found = player.getInventory().getItem(slot);
if (!matches(found, requested)) continue;
int taken = Math.min(remaining, found.getCount());
result.add(found.copyWithCount(taken));
found.shrink(taken);
remaining -= taken;
}
return remaining;
}
private static ItemStack insertSlotRange(ServerPlayer player, ItemStack offered,
int first, int last, int excluded) {
ItemStack remaining = offered.copy();
// Fill compatible stacks before empty slots, while preserving the requested slot-class priority.
for (int pass = 0; pass < 2 && !remaining.isEmpty(); pass++) {
for (int slot = first; slot <= last && !remaining.isEmpty(); slot++) {
if (slot == excluded) continue;
ItemStack current = player.getInventory().getItem(slot);
if (pass == 0) {
if (!matches(current, remaining)) continue;
int room = Math.min(current.getMaxStackSize(), player.getInventory().getMaxStackSize())
- current.getCount();
if (room <= 0) continue;
int moved = Math.min(room, remaining.getCount());
current.grow(moved);
remaining.shrink(moved);
} else if (current.isEmpty()) {
int moved = Math.min(remaining.getCount(), remaining.getMaxStackSize());
player.getInventory().setItem(slot, remaining.copyWithCount(moved));
remaining.shrink(moved);
}
}
}
return remaining;
}
private static ItemStack insertOffhand(ServerPlayer player, ItemStack offered, ItemStack scepter) {
ItemStack remaining = offered.copy();
ItemStack current = player.getOffhandItem();
if (current == scepter) return remaining;
if (current.isEmpty()) {
int moved = Math.min(remaining.getCount(), remaining.getMaxStackSize());
player.setItemInHand(net.minecraft.world.InteractionHand.OFF_HAND, remaining.copyWithCount(moved));
remaining.shrink(moved);
} else if (matches(current, remaining)) {
int room = current.getMaxStackSize() - current.getCount();
int moved = Math.min(Math.max(0, room), remaining.getCount());
current.grow(moved);
remaining.shrink(moved);
}
return remaining;
}
private static boolean matches(ItemStack left, ItemStack right) {
return !left.isEmpty() && ItemStack.isSameItemSameTags(left, right);
}
private static void appendUpTo(List<ItemStack> target, List<ItemStack> values, int maximum) {
int remaining = maximum;
for (ItemStack value : values) {
if (remaining <= 0) break;
int count = Math.min(remaining, value.getCount());
if (count > 0) target.add(value.copyWithCount(count));
remaining -= count;
}
}
private static List<ItemStack> merge(List<ItemStack> values) {
List<ItemStack> result = new ArrayList<>();
outer:
for (ItemStack value : values) {
if (value.isEmpty()) continue;
for (ItemStack existing : result) {
if (matches(existing, value)) {
existing.grow(value.getCount());
continue outer;
}
}
result.add(value.copy());
}
return result;
}
private record EntityDebit(ItemEntity entity, int count) { }
}
package com.xfestudio.mydimension.builder;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.entity.item.ItemEntity;
import java.util.ArrayList;
import java.util.List;
/**
* Ordered material access shared by layer and blueprint jobs. The anchor implementation installs the
* remote bridge during common setup; keeping the bridge here also makes a failed third-party handler
* incapable of preventing the player inventory fallbacks from running.
*/
public final class BuilderMaterials {
public interface RemoteBridge {
Extraction extract(ServerPlayer player, ItemStack scepter, ItemStack requested, int amount);
/** Returns the part that could not be stored. */
ItemStack insert(ServerPlayer player, ItemStack scepter, ItemStack stack);
/**
* Stores a whole operation's output while preserving one remainder per input value.
* Implementations backed by remote endpoints should override this so leases and
* capabilities are resolved once per operation instead of once per dropped stack.
*/
default List<ItemStack> insertAll(ServerPlayer player, ItemStack scepter,
List<ItemStack> stacks) {
List<ItemStack> remainders = new ArrayList<>(stacks.size());
for (ItemStack stack : stacks) {
if (stack.isEmpty()) {
remainders.add(ItemStack.EMPTY);
continue;
}
ItemStack original = stack.copy();
try {
remainders.add(BuilderMaterials.normalizeRemoteRemainder(
original, insert(player, scepter, original.copy())));
} catch (RuntimeException ignored) {
// Isolate one endpoint failure without forgetting remainders from values that
// were already committed successfully earlier in this operation.
remainders.add(original);
}
}
return List.copyOf(remainders);
}
}
public record Extraction(int count, List<ItemStack> stacks) {
public Extraction {
count = Math.max(0, count);
stacks = stacks.stream().filter(stack -> !stack.isEmpty()).map(ItemStack::copy).toList();
}
public static Extraction empty() {
return new Extraction(0, List.of());
}
}
private static final RemoteBridge NO_REMOTE = new RemoteBridge() {
@Override
public Extraction extract(ServerPlayer player, ItemStack scepter, ItemStack requested, int amount) {
return Extraction.empty();
}
@Override
public ItemStack insert(ServerPlayer player, ItemStack scepter, ItemStack stack) {
return stack.copy();
}
};
private static volatile RemoteBridge remote = NO_REMOTE;
private BuilderMaterials() {
}
public static void installRemoteBridge(RemoteBridge bridge) {
remote = bridge == null ? NO_REMOTE : bridge;
}
public static Extraction extract(ServerPlayer player, ItemStack scepter, ItemStack requested, int amount) {
return extract(player, scepter, requested, amount, true);
}
private static Extraction extract(ServerPlayer player, ItemStack scepter, ItemStack requested, int amount,
boolean allowOffhand) {
if (amount <= 0 || requested.isEmpty()) return Extraction.empty();
List<ItemStack> debits = new ArrayList<>();
int remaining = amount;
try {
Extraction fromRemote = remote.extract(player, scepter, requested, remaining);
int accepted = Math.min(remaining, Math.max(0, fromRemote.count()));
if (accepted > 0) {
remaining -= accepted;
appendUpTo(debits, fromRemote.stacks(), accepted);
}
} catch (RuntimeException ignored) {
// A broken capability is isolated to its endpoint; local inventory remains usable.
}
remaining = takeSlotRange(player, requested, remaining, 9, 35, debits, -1);
remaining = takeSlotRange(player, requested, remaining, 0, 8, debits,
player.getInventory().selected);
if (allowOffhand && remaining > 0) {
ItemStack offhand = player.getOffhandItem();
if (matches(offhand, requested) && offhand != scepter) {
int taken = Math.min(remaining, offhand.getCount());
debits.add(offhand.copyWithCount(taken));
offhand.shrink(taken);
remaining -= taken;
}
}
return new Extraction(amount - remaining, debits);
}
/** Stores in anchor order first, then the player's ordinary inventory. */
public static List<ItemStack> insert(ServerPlayer player, ItemStack scepter, List<ItemStack> values) {
return insert(player, scepter, values, true);
}
/** Transaction bookkeeping uses this variant so an exact offhand tool is never replaced by a drop. */
public static List<ItemStack> insertPreservingOffhand(ServerPlayer player, ItemStack scepter,
List<ItemStack> values) {
return insert(player, scepter, values, false);
}
private static List<ItemStack> insert(ServerPlayer player, ItemStack scepter, List<ItemStack> values,
boolean allowOffhand) {
List<ItemStack> aligned = insertAligned(player, scepter, values, allowOffhand);
List<ItemStack> overflow = new ArrayList<>(aligned.size());
for (ItemStack remainder : aligned) {
if (!remainder.isEmpty()) overflow.add(remainder.copy());
}
return overflow;
}
/**
* Stores all values in one material-access session and returns a remainder at
* every corresponding input index. Keeping the alignment lets demolition
* put overflow back at the exact block that produced it without reopening
* every remote anchor for every block.
*/
public static List<ItemStack> insertAligned(ServerPlayer player, ItemStack scepter,
List<ItemStack> values) {
return insertAligned(player, scepter, values, true);
}
/** Aligned batch variant that reserves the offhand for the demolition tool state. */
public static List<ItemStack> insertAlignedPreservingOffhand(ServerPlayer player, ItemStack scepter,
List<ItemStack> values) {
return insertAligned(player, scepter, values, false);
}
private static List<ItemStack> insertAligned(ServerPlayer player, ItemStack scepter,
List<ItemStack> values, boolean allowOffhand) {
List<ItemStack> offered = values.stream().map(stack -> stack.isEmpty()
? ItemStack.EMPTY : stack.copy()).toList();
List<ItemStack> remoteRemainders;
try {
List<ItemStack> remoteInput = offered.stream()
.map(stack -> stack.isEmpty() ? ItemStack.EMPTY : stack.copy()).toList();
remoteRemainders = remote.insertAll(player, scepter, remoteInput);
if (remoteRemainders == null || remoteRemainders.size() != offered.size()) {
remoteRemainders = offered;
}
} catch (RuntimeException ignored) {
remoteRemainders = offered;
}
List<ItemStack> remainders = new ArrayList<>(offered.size());
for (int index = 0; index < offered.size(); index++) {
ItemStack value = offered.get(index);
if (value.isEmpty()) {
remainders.add(ItemStack.EMPTY);
continue;
}
ItemStack remainder = normalizeRemoteRemainder(value, remoteRemainders.get(index));
remainder = insertSlotRange(player, remainder, 9, 35, -1);
remainder = insertSlotRange(player, remainder, 0, 8, player.getInventory().selected);
if (allowOffhand && !remainder.isEmpty()) remainder = insertOffhand(player, remainder, scepter);
remainders.add(remainder.isEmpty() ? ItemStack.EMPTY : remainder.copy());
}
return List.copyOf(remainders);
}
/** Rejects malformed handler output before it can duplicate, inflate or transform an item. */
private static ItemStack normalizeRemoteRemainder(ItemStack offered, ItemStack returned) {
if (offered.isEmpty()) return ItemStack.EMPTY;
if (returned == null || !returned.isEmpty()
&& (!matches(returned, offered) || returned.getCount() > offered.getCount())) {
return offered.copy();
}
return returned.isEmpty() ? ItemStack.EMPTY : returned.copy();
}
/** Conservative exact removal used before undoing demolition. */
public static boolean canAndRemove(ServerPlayer player, ItemStack scepter, List<ItemStack> values) {
return canAndRemove(player, scepter, values, List.of());
}
public static boolean canAndRemove(ServerPlayer player, ItemStack scepter, List<ItemStack> values,
List<ItemEntity> transactionEntities) {
return canAndRemove(player, scepter, values, transactionEntities, false);
}
/** Build replacement credits may occupy the offhand and therefore participate in exact reclaim. */
public static boolean canAndRemoveIncludingOffhand(ServerPlayer player, ItemStack scepter,
List<ItemStack> values,
List<ItemEntity> transactionEntities) {
return canAndRemove(player, scepter, values, transactionEntities, true);
}
private static boolean canAndRemove(ServerPlayer player, ItemStack scepter, List<ItemStack> values,
List<ItemEntity> transactionEntities, boolean allowOffhand) {
List<ItemStack> required = merge(values);
List<ItemStack> removed = new ArrayList<>();
List<EntityDebit> entityDebits = new ArrayList<>();
for (ItemStack wanted : required) {
// The offhand is separately fingerprinted and restored as tool state; accepting it as a drop
// debit would let undo consume the tool and then recreate it from offhandBefore.
Extraction extraction = extract(player, scepter, wanted, wanted.getCount(), allowOffhand);
removed.addAll(extraction.stacks());
int remaining = wanted.getCount() - extraction.count();
for (ItemEntity entity : transactionEntities) {
if (remaining <= 0 || entity.isRemoved() || !matches(entity.getItem(), wanted)) continue;
int alreadyPlanned = entityDebits.stream().filter(debit -> debit.entity == entity)
.mapToInt(debit -> debit.count).sum();
int take = Math.min(remaining, Math.max(0, entity.getItem().getCount() - alreadyPlanned));
if (take > 0) {
entityDebits.add(new EntityDebit(entity, take));
remaining -= take;
}
}
if (remaining > 0) {
// Roll back every debit before reporting the transaction conflict.
List<ItemStack> overflow = insert(player, scepter, removed, allowOffhand);
for (ItemStack stack : overflow) player.drop(stack, false);
return false;
}
}
for (EntityDebit debit : entityDebits) {
ItemStack stack = debit.entity.getItem();
stack.shrink(debit.count);
if (stack.isEmpty()) debit.entity.discard();
}
return true;
}
private static int takeSlotRange(ServerPlayer player, ItemStack requested, int remaining,
int first, int last, List<ItemStack> result, int excluded) {
for (int slot = first; slot <= last && remaining > 0; slot++) {
if (slot == excluded) continue;
ItemStack found = player.getInventory().getItem(slot);
if (!matches(found, requested)) continue;
int taken = Math.min(remaining, found.getCount());
result.add(found.copyWithCount(taken));
found.shrink(taken);
remaining -= taken;
}
return remaining;
}
private static ItemStack insertSlotRange(ServerPlayer player, ItemStack offered,
int first, int last, int excluded) {
ItemStack remaining = offered.copy();
// Fill compatible stacks before empty slots, while preserving the requested slot-class priority.
for (int pass = 0; pass < 2 && !remaining.isEmpty(); pass++) {
for (int slot = first; slot <= last && !remaining.isEmpty(); slot++) {
if (slot == excluded) continue;
ItemStack current = player.getInventory().getItem(slot);
if (pass == 0) {
if (!matches(current, remaining)) continue;
int room = Math.min(current.getMaxStackSize(), player.getInventory().getMaxStackSize())
- current.getCount();
if (room <= 0) continue;
int moved = Math.min(room, remaining.getCount());
current.grow(moved);
remaining.shrink(moved);
} else if (current.isEmpty()) {
int moved = Math.min(remaining.getCount(), remaining.getMaxStackSize());
player.getInventory().setItem(slot, remaining.copyWithCount(moved));
remaining.shrink(moved);
}
}
}
return remaining;
}
private static ItemStack insertOffhand(ServerPlayer player, ItemStack offered, ItemStack scepter) {
ItemStack remaining = offered.copy();
ItemStack current = player.getOffhandItem();
if (current == scepter) return remaining;
if (current.isEmpty()) {
int moved = Math.min(remaining.getCount(), remaining.getMaxStackSize());
player.setItemInHand(net.minecraft.world.InteractionHand.OFF_HAND, remaining.copyWithCount(moved));
remaining.shrink(moved);
} else if (matches(current, remaining)) {
int room = current.getMaxStackSize() - current.getCount();
int moved = Math.min(Math.max(0, room), remaining.getCount());
current.grow(moved);
remaining.shrink(moved);
}
return remaining;
}
private static boolean matches(ItemStack left, ItemStack right) {
return !left.isEmpty() && ItemStack.isSameItemSameTags(left, right);
}
private static void appendUpTo(List<ItemStack> target, List<ItemStack> values, int maximum) {
int remaining = maximum;
for (ItemStack value : values) {
if (remaining <= 0) break;
int count = Math.min(remaining, value.getCount());
if (count > 0) target.add(value.copyWithCount(count));
remaining -= count;
}
}
private static List<ItemStack> merge(List<ItemStack> values) {
List<ItemStack> result = new ArrayList<>();
outer:
for (ItemStack value : values) {
if (value.isEmpty()) continue;
for (ItemStack existing : result) {
if (matches(existing, value)) {
existing.grow(value.getCount());
continue outer;
}
}
result.add(value.copy());
}
return result;
}
private record EntityDebit(ItemEntity entity, int count) { }
}