diff --git a/src/main/java/meteordevelopment/meteorclient/commands/Commands.java b/src/main/java/meteordevelopment/meteorclient/commands/Commands.java index 8764f20636..abf72e7b7b 100644 --- a/src/main/java/meteordevelopment/meteorclient/commands/Commands.java +++ b/src/main/java/meteordevelopment/meteorclient/commands/Commands.java @@ -36,6 +36,7 @@ public static void init() { add(new DamageCommand()); add(new DropCommand()); add(new EnchantCommand()); + add(new OptimizeEnchantsCommand()); add(new FakePlayerCommand()); add(new FriendsCommand()); add(new CommandsCommand()); diff --git a/src/main/java/meteordevelopment/meteorclient/commands/arguments/EnchantmentLevelArgumentType.java b/src/main/java/meteordevelopment/meteorclient/commands/arguments/EnchantmentLevelArgumentType.java new file mode 100644 index 0000000000..c97f047cbf --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/commands/arguments/EnchantmentLevelArgumentType.java @@ -0,0 +1,93 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.commands.arguments; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.enchantment.Enchantment; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.text.Text; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class EnchantmentLevelArgumentType implements ArgumentType { + private final String enchantmentArgName; + + + public EnchantmentLevelArgumentType(String enchantmentArgName) { + this.enchantmentArgName = enchantmentArgName; + } + + public static EnchantmentLevelArgumentType enchantmentLevel(String enchantmentArgName) { + return new EnchantmentLevelArgumentType(enchantmentArgName); + } + + + @Override + public Integer parse(StringReader reader) throws CommandSyntaxException { + int start = reader.getCursor(); + int level = reader.readInt(); + + if (level < 1) { + reader.setCursor(start); + throw new CommandSyntaxException( + new DynamicCommandExceptionType(obj -> Text.literal("Level must be at least 1")), + Text.literal("Level must be at least 1") + ); + } + + return level; + } + + @Override + public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { + try { + // Try to get the enchantment from the previous argument + RegistryEntry.Reference enchantment = + RegistryEntryReferenceArgumentType.getEnchantment(context, enchantmentArgName); + + int maxLevel = enchantment.value().getMaxLevel(); + String enchantName = enchantment.value().description().getString(); + + // Build suggestions based on max level + // Suggest 1 through maxLevel + for (int i = 1; i <= Math.min(maxLevel, 10); i++) { + builder.suggest(i); + } + + // TODO: this isn't working, only the above suggestions show up; overengineering? + // Add a tooltip showing the valid range + String remaining = builder.getRemaining(); + if (!remaining.isEmpty()) { + try { + int typedLevel = Integer.parseInt(remaining); + if (typedLevel > maxLevel) { + // Show error in suggestions + builder.suggest(maxLevel, Text.literal("§c" + enchantName + " max: " + maxLevel)); + } + } catch (NumberFormatException ignored) { + // Command handler highlights invalid input + } + } + + return builder.buildFuture(); + } catch (Exception e) { + return Suggestions.empty(); + } + } + + @Override + public Collection getExamples() { + return List.of("1", "2", "3", "4", "5"); + } +} diff --git a/src/main/java/meteordevelopment/meteorclient/commands/commands/OptimizeEnchantsCommand.java b/src/main/java/meteordevelopment/meteorclient/commands/commands/OptimizeEnchantsCommand.java new file mode 100644 index 0000000000..2c09bb99fa --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/commands/commands/OptimizeEnchantsCommand.java @@ -0,0 +1,196 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.commands.commands; + +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import meteordevelopment.meteorclient.commands.Command; +import meteordevelopment.meteorclient.commands.arguments.EnchantmentLevelArgumentType; +import meteordevelopment.meteorclient.commands.arguments.RegistryEntryReferenceArgumentType; +import meteordevelopment.meteorclient.utils.misc.EnchantmentOptimizer; +import meteordevelopment.meteorclient.utils.player.ChatUtils; +import net.minecraft.command.CommandSource; +import net.minecraft.command.argument.ItemStackArgumentType; +import net.minecraft.enchantment.Enchantment; +import net.minecraft.item.Item; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.util.ArrayList; +import java.util.List; + +public class OptimizeEnchantsCommand extends Command { + public OptimizeEnchantsCommand() { + super("optimize-enchant", "Calculates the optimal order to apply enchantments for minimum XP cost.", "eopt"); + } + + @Override + public void build(LiteralArgumentBuilder builder) { + // TODO: The optimizer supports book-only mode (item=null) for combining enchanted books, + // but this command currently requires an item argument, so item will never be null. + // Should we add an item-less version of the command for book-only optimization? + + // TODO: should we restrict the available items to only those that can be enchanted? + // e.g. armors, weapons, tools, books, etc. + builder.then(argument("item", ItemStackArgumentType.itemStack(REGISTRY_ACCESS)) + .then(buildEnchantmentChain(1, 20)) + // TODO: what should the max depth be? Idk how many enchantments on a single item MC supports. + ); + } + + /** + * Recursively builds a chain of enchantment arguments. + * Each enchantment requires a name and level, and can optionally chain to the next. + */ + private RequiredArgumentBuilder buildEnchantmentChain(int index, int maxDepth) { + String enchantArg = "enchantment" + index; + String levelArg = "level" + index; + + var enchantmentArg = argument(enchantArg, RegistryEntryReferenceArgumentType.enchantment()); + var levelArgBuilder = argument(levelArg, EnchantmentLevelArgumentType.enchantmentLevel(enchantArg)) + .executes(context -> { + executeOptimization(context, index); + return SINGLE_SUCCESS; + }); + + if (index < maxDepth) { + levelArgBuilder.then(buildEnchantmentChain(index + 1, maxDepth)); + } + + return enchantmentArg.then(levelArgBuilder); + } + + /** + * Extracts all enchantments from context and runs optimization. + */ + private void executeOptimization(CommandContext context, int enchantmentCount) { + try { + Item item = getItem(context); + List enchants = new ArrayList<>(); + + for (int i = 1; i <= enchantmentCount; i++) { + String enchantArg = "enchantment" + i; + String levelArg = "level" + i; + + try { + RegistryEntry.Reference enchantment = + RegistryEntryReferenceArgumentType.getEnchantment(context, enchantArg); + int level = IntegerArgumentType.getInteger(context, levelArg); + + // TODO: keep validation or allow arbitary levels? MC supports up to 255. + // Validate level against enchantment's max level + int maxLevel = enchantment.value().getMaxLevel(); + if (level > maxLevel) { + String enchantName = enchantment.value().description().getString(); + error("Enchantment (highlight)%s(default) has max level (highlight)%d(default), but you specified (highlight)%d(default).", + enchantName, maxLevel, level); + return; + } + + enchants.add(new EnchantmentOptimizer.EnchantmentEntry(enchantment, level)); + } catch (IllegalArgumentException e) { + // Argument doesn't exist, we've reached the end + break; + } + } + + if (enchants.isEmpty()) { + error("No enchantments specified."); + return; + } + + optimize(item, enchants); + } catch (Exception e) { + error("Failed to parse enchantments: %s", e.getMessage()); + } + } + + private Item getItem(CommandContext context) { + try { + var itemArg = ItemStackArgumentType.getItemStackArgument(context, "item"); + return itemArg.getItem(); + } catch (Exception e) { + return null; + } + } + + private void optimize(Item item, List enchants) { + try { + // Create optimizer from current registry + var registry = mc.getNetworkHandler().getRegistryManager().getOrThrow(RegistryKeys.ENCHANTMENT); + EnchantmentOptimizer.OptimizationResult result = EnchantmentOptimizer.create(registry).optimize(item, enchants); + + // Display header + String itemName = item != null ? item.getName().getString() : "Book"; + ChatUtils.info("=== Enchantment Optimization for %s ===", itemName); + info("Total Cost: (highlight)%d levels(default) (%d XP)", result.totalLevels(), result.totalXp()); + + if (result.instructions().isEmpty()) { + info("No combinations needed - single enchantment only."); + return; + } + + info("Steps:"); + + // Display steps + for (int i = 0; i < result.instructions().size(); i++) { + EnchantmentOptimizer.Instruction instr = result.instructions().get(i); + + MutableText stepText = Text.literal(String.format(" %d. ", i + 1)).formatted(Formatting.GRAY); + stepText.append(Text.literal("Combine ").formatted(Formatting.GRAY)); + stepText.append(formatItem(instr.left()).copy().formatted(Formatting.YELLOW)); + stepText.append(Text.literal(" with ").formatted(Formatting.GRAY)); + stepText.append(formatItem(instr.right()).copy().formatted(Formatting.AQUA)); + + ChatUtils.sendMsg(stepText); + + info(" Cost: (highlight)%d levels(default) (%d XP), Prior Work Penalty: %d", + instr.levels(), + instr.xp(), + instr.priorWorkPenalty() + ); + } + + } catch (Exception e) { + error("Failed to optimize enchantments: %s", e.getMessage()); + } + } + + private Text formatItem(EnchantmentOptimizer.Combination comb) { + if (comb.item != null) { + return comb.item.getName(); + } + + if (comb.enchantment != null) { + String enchName = comb.enchantment.value().description().getString(); + String level = romanNumeral(comb.level); + return Text.literal(enchName + " " + level + " Book"); + } + + return Text.literal("Combined Item"); + } + + private String romanNumeral(int num) { + return switch (num) { + case 1 -> "I"; + case 2 -> "II"; + case 3 -> "III"; + case 4 -> "IV"; + case 5 -> "V"; + case 6 -> "VI"; + case 7 -> "VII"; + case 8 -> "VIII"; + case 9 -> "IX"; + case 10 -> "X"; + default -> String.valueOf(num); + }; + } +} diff --git a/src/main/java/meteordevelopment/meteorclient/utils/misc/EnchantmentOptimizer.java b/src/main/java/meteordevelopment/meteorclient/utils/misc/EnchantmentOptimizer.java new file mode 100644 index 0000000000..817800c464 --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/utils/misc/EnchantmentOptimizer.java @@ -0,0 +1,442 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.utils.misc; + +import it.unimi.dsi.fastutil.ints.*; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.enchantment.Enchantment; +import net.minecraft.item.Item; +import net.minecraft.registry.Registry; +import net.minecraft.registry.entry.RegistryEntry; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.*; +import java.util.stream.Collectors; + +@NullMarked +public class EnchantmentOptimizer { + private static final int MAXIMUM_MERGE_LEVELS = 39; + + private final Object2IntMap> enchantmentIds; + private final int[] enchantmentWeights; + private final Map> memoCache; + + public EnchantmentOptimizer(List> enchantments) { + this.enchantmentIds = new Object2IntOpenHashMap<>(); + this.enchantmentWeights = new int[enchantments.size()]; + this.memoCache = new HashMap<>(); + + int id = 0; + for (RegistryEntry entry : enchantments) { + enchantmentIds.put(entry, id); + int anvilCost = entry.value().getAnvilCost(); + + /* + NOTE: Minecraft's anvil code (AnvilScreenHandler.updateResult) divides the anvil cost by 2 + when merging with enchanted books: "if (bl) { s = Math.max(1, s / 2); }" + where bl = itemStack3.contains(DataComponentTypes.STORED_ENCHANTMENTS) + Since we're optimizing book-based enchanting, we divide by 2 to match in-game behavior + */ + enchantmentWeights[id] = Math.max(1, anvilCost / 2); + + id++; + } + } + + public record EnchantmentEntry(RegistryEntry enchantment, int level) { + } + + public OptimizationResult optimize(@Nullable Item item, List enchants) { + memoCache.clear(); + + // Create enchantment objects + List enchantObjs = enchants.stream() + .map(e -> { + int id = enchantmentIds.getOrDefault(e.enchantment(), -1); + if (id == -1) { + throw new IllegalArgumentException("Unknown enchantment: " + e.enchantment().getKey().orElseThrow().getValue()); + } + int value = e.level() * enchantmentWeights[id]; + IntList ids = IntLists.singleton(id); + ItemObject obj = new ItemObject(ItemType.BOOK, value, ids); + obj.combination = new Combination(e.enchantment(), e.level()); + return obj; + }) + .collect(Collectors.toCollection(ArrayList::new)); + + // Find most expensive enchant + int mostExpensiveIdx = findMostExpensive(enchantObjs); + + // Create base item + ItemObject baseItem; + if (item == null) { // Book-only mode + ItemObject expensive = enchantObjs.get(mostExpensiveIdx); + IntList ids = IntLists.singleton(expensive.enchantIds.getInt(0)); + baseItem = new ItemObject(ItemType.ENCHANTED_BOOK, expensive.value, ids); + baseItem.combination = expensive.combination; + enchantObjs.remove(mostExpensiveIdx); + // Find the next most expensive after removing the first + mostExpensiveIdx = findMostExpensive(enchantObjs); + } else { + baseItem = new ItemObject(ItemType.ITEM, 0, new IntArrayList()); + baseItem.combination = new Combination(item); + } + + if (enchantObjs.isEmpty()) { + return new OptimizationResult(baseItem, List.of(), 0, 0); + } + + // Merge base with most expensive + ItemObject merged = new MergeEnchants(baseItem, enchantObjs.get(mostExpensiveIdx)); + // Override the left combination with a fresh one that has value=0, priorWork=0 + // This ensures the base item doesn't contribute to the value calculation + if (item != null) { + merged.combination.left = new Combination(item); + } + enchantObjs.remove(mostExpensiveIdx); + + // Find optimal combination + List allObjs = new ArrayList<>(enchantObjs); + allObjs.add(merged); + + Int2ObjectMap cheapestItems = cheapestItemsFromList(allObjs); + + // Select cheapest by total XP + ItemObject cheapest = cheapestItems.values().stream() + .min(Comparator.comparingInt(a -> a.totalXp)) + .orElseThrow(); + + List instructions = getInstructions(cheapest.combination); + + int maxLevels = instructions.stream().mapToInt(i -> i.levels).sum(); + int maxXp = experience(maxLevels); + + return new OptimizationResult(cheapest, instructions, maxLevels, maxXp); + } + + private int findMostExpensive(List items) { + int maxIdx = 0; + for (int i = 1; i < items.size(); i++) { + if (items.get(i).value > items.get(maxIdx).value) { + maxIdx = i; + } + } + return maxIdx; + } + + + private Int2ObjectMap cheapestItemsFromList(List items) { + ResultKey key = ResultKey.fromItems(items); + Int2ObjectMap cached = memoCache.get(key); + if (cached != null) return cached; + + Int2ObjectMap result = switch (items.size()) { + case 1 -> Int2ObjectMaps.singleton(items.getFirst().priorWork, items.getFirst()); + case 2 -> { + ItemObject cheapest = cheapestItemFromItems(items.getFirst(), items.get(1)); + yield Int2ObjectMaps.singleton(cheapest.priorWork, cheapest); + } + default -> cheapestItemsFromListN(items, items.size() / 2); + }; + + memoCache.put(key, result); + return result; + } + + private ItemObject cheapestItemFromItems(ItemObject left, ItemObject right) { + if (left.type == ItemType.ITEM) return new MergeEnchants(left, right); + if (right.type == ItemType.ITEM) return new MergeEnchants(right, left); + + ItemObject normal = null; + ItemObject reversed = null; + + try { + normal = new MergeEnchants(left, right); + } catch (MergeLevelsTooExpensiveException ignored) { + // Ignore too expensive merges + } + + try { + reversed = new MergeEnchants(right, left); + } catch (MergeLevelsTooExpensiveException ignored) { + // Ignore too expensive merges + } + + if (normal == null && reversed == null) { + throw new IllegalStateException("Both merge attempts were too expensive"); + } + + if (normal == null) return reversed; + if (reversed == null) return normal; + + // Both merges succeeded - they have same priorWork, so compareCheapest cannot return null + return compareCheapest(normal, reversed); + } + + private Int2ObjectMap cheapestItemsFromListN(List items, int maxSubcount) { + Int2ObjectMap cheapestWork2Item = new Int2ObjectOpenHashMap<>(); + + for (int subcount = 1; subcount <= maxSubcount; subcount++) { + for (List leftItems : combinations(items, subcount)) { + List rightItems = new ArrayList<>(items); + rightItems.removeAll(leftItems); + + Int2ObjectMap leftWork2Item = cheapestItemsFromList(leftItems); + Int2ObjectMap rightWork2Item = cheapestItemsFromList(rightItems); + Int2ObjectMap newWork2Item = cheapestItemsFromDictionaries(leftWork2Item, rightWork2Item); + + for (Int2ObjectMap.Entry entry : newWork2Item.int2ObjectEntrySet()) { + cheapestWork2Item.merge(entry.getIntKey(), entry.getValue(), this::compareCheapest); + } + } + } + return cheapestWork2Item; + } + + private ItemObject compareCheapest(ItemObject item1, ItemObject item2) { + // This method assumes both items have the same priorWork (enforced by callers using work-indexed maps) + // If they somehow differ, we can't meaningfully compare them + if (item1.priorWork != item2.priorWork) { + throw new IllegalStateException("Items must have same priorWork: " + item1.priorWork + " vs " + item2.priorWork); + } + + // Prefer lower value (fewer enchantment levels) + if (item1.value != item2.value) return item1.value < item2.value ? item1 : item2; + + // If value is equal, prefer lower total XP cost + return item1.totalXp <= item2.totalXp ? item1 : item2; + } + + private Int2ObjectMap cheapestItemsFromDictionaries( + Int2ObjectMap left, Int2ObjectMap right) { + Int2ObjectMap cheapest = new Int2ObjectOpenHashMap<>(); + + for (ItemObject leftItem : left.values()) { + for (ItemObject rightItem : right.values()) { + try { + Int2ObjectMap newWork2Item = cheapestItemsFromList(List.of(leftItem, rightItem)); + + for (Int2ObjectMap.Entry entry : newWork2Item.int2ObjectEntrySet()) { + cheapest.merge(entry.getIntKey(), entry.getValue(), this::compareCheapest); + } + } catch (MergeLevelsTooExpensiveException ignored) { + // Ignore too expensive merges + } + } + } + return removeExpensiveCandidates(cheapest); + } + + private Int2ObjectMap removeExpensiveCandidates(Int2ObjectMap work2Item) { + Int2ObjectMap result = new Int2ObjectOpenHashMap<>(); + int cheapestValue = Integer.MAX_VALUE; + + for (Int2ObjectMap.Entry entry : work2Item.int2ObjectEntrySet()) { + if (entry.getValue().value < cheapestValue) { + result.put(entry.getIntKey(), entry.getValue()); + cheapestValue = entry.getValue().value; + } + } + return result; + } + + private List getInstructions(Combination comb) { + List instructions = new ArrayList<>(); + extractInstructions(comb, instructions); + return instructions; + } + + private void extractInstructions(Combination comb, List instructions) { + if (comb.left != null && comb.left.left != null) extractInstructions(comb.left, instructions); + if (comb.right != null && comb.right.left != null) extractInstructions(comb.right, instructions); + + if (comb.left != null && comb.right != null) { + instructions.add(new Instruction(comb.left, comb.right, comb.mergeCost, + experience(comb.mergeCost), (1 << comb.priorWork) - 1)); + } + } + + private static int experience(int level) { + if (level == 0) return 0; + if (level <= 16) return level * level + 6 * level; + if (level <= 31) return (int) (2.5 * level * level - 40.5 * level + 360); + return (int) (4.5 * level * level - 162.5 * level + 2220); + } + + private static List> combinations(List set, int k) { + if (k > set.size() || k <= 0) return List.of(); + if (k == set.size()) return List.of(new ArrayList<>(set)); + if (k == 1) return set.stream().map(List::of).toList(); + + List> combs = new ArrayList<>(); + for (int i = 0; i < set.size() - k + 1; i++) { + T head = set.get(i); + List> tailCombs = combinations(set.subList(i + 1, set.size()), k - 1); + for (List tail : tailCombs) { + List combination = new ArrayList<>(tail.size() + 1); + combination.add(head); + combination.addAll(tail); + combs.add(combination); + } + } + return combs; + } + + // Data classes + + public enum ItemType { + ITEM, // The actual item being enchanted + BOOK, // Enchanted book + ENCHANTED_BOOK // Book that's being used as base (for book-only mode) + } + + public static class ItemObject { + ItemType type; + IntList enchantIds; + Combination combination; + int priorWork; + int value; + int totalXp; + + ItemObject(ItemType type, int value, IntList enchantIds) { + this.type = type; + this.value = value; + this.enchantIds = new IntArrayList(enchantIds); + this.combination = new Combination(); + this.priorWork = 0; + this.totalXp = 0; + } + } + + public static class MergeEnchants extends ItemObject { + MergeEnchants(ItemObject left, ItemObject right) { + super(left.type, left.value + right.value, new IntArrayList()); + + int mergeCost = right.value + (1 << left.priorWork) - 1 + (1 << right.priorWork) - 1; + if (mergeCost > MAXIMUM_MERGE_LEVELS) { + throw new MergeLevelsTooExpensiveException(); + } + + this.enchantIds.addAll(left.enchantIds); + this.enchantIds.addAll(right.enchantIds); + this.priorWork = Math.max(left.priorWork, right.priorWork) + 1; + this.totalXp = left.totalXp + right.totalXp + experience(mergeCost); + this.combination = new Combination(left.combination, right.combination, mergeCost, this.priorWork, this.value); + } + } + + public static class Combination { + public @Nullable Combination left; + public @Nullable Combination right; + public int mergeCost; + public int priorWork; + public int value; + public @Nullable Item item; // For base item + public @Nullable RegistryEntry enchantment; // For enchanted book + public int level; + + Combination() { + this.left = null; + this.right = null; + this.item = null; + this.enchantment = null; + } + + Combination(RegistryEntry enchantment, int level) { + this.left = null; + this.right = null; + this.item = null; + this.enchantment = enchantment; + this.level = level; + } + + Combination(Item item) { + this.left = null; + this.right = null; + this.item = item; + this.enchantment = null; + } + + Combination(Combination left, Combination right, int mergeCost, int priorWork, int value) { + this.left = left; + this.right = right; + this.mergeCost = mergeCost; + this.priorWork = priorWork; + this.value = value; + this.item = null; + this.enchantment = null; + } + } + + public record Instruction( + Combination left, + Combination right, + int levels, + int xp, + int priorWorkPenalty + ) { + } + + public record OptimizationResult( + ItemObject finalItem, + List instructions, + int totalLevels, + int totalXp + ) { + } + + private record ResultKey(List hashes) { + static ResultKey fromItems(List items) { + return new ResultKey(items.stream() + .map(ItemHash::new) + .sorted() + .toList()); + } + } + + private record ItemHash(ItemType itemType, IntList sortedEnchants, int priorWork) implements Comparable { + ItemHash(ItemObject item) { + this(item.type, createSortedEnchants(item.enchantIds), item.priorWork); + } + + private static IntList createSortedEnchants(IntList enchantIds) { + IntList sorted = new IntArrayList(enchantIds); + sorted.sort(IntComparators.NATURAL_COMPARATOR); + return sorted; + } + + @Override + public int compareTo(ItemHash o) { + int c = itemType.compareTo(o.itemType); + if (c != 0) return c; + c = Integer.compare(priorWork, o.priorWork); + if (c != 0) return c; + + // Compare sizes first + c = Integer.compare(sortedEnchants.size(), o.sortedEnchants.size()); + if (c != 0) return c; + + // Then element by element + for (int i = 0; i < sortedEnchants.size(); i++) { + c = Integer.compare(sortedEnchants.getInt(i), o.sortedEnchants.getInt(i)); + if (c != 0) return c; + } + return 0; + } + } + + private static class MergeLevelsTooExpensiveException extends RuntimeException { + } + + // Static factory method to create optimizer from registry + public static EnchantmentOptimizer create(Registry registry) { + // streamEntries returns RegistryEntry.Reference, which extends RegistryEntry + List> enchantments = new ArrayList<>(registry.streamEntries().toList()); + return new EnchantmentOptimizer(enchantments); + } +}