From 8f8db7a604a86d17b88a621a18656e0791d87aeb Mon Sep 17 00:00:00 2001 From: shedaniel Date: Sun, 18 Dec 2022 19:34:26 +0800 Subject: Fix #668 --- .../shedaniel/rei/impl/client/view/ViewsImpl.java | 38 +++++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/view/ViewsImpl.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/view/ViewsImpl.java index cd44c7434..c42594b3b 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/view/ViewsImpl.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/view/ViewsImpl.java @@ -29,6 +29,8 @@ import com.google.common.collect.Maps; import it.unimi.dsi.fastutil.longs.Long2LongMap; import it.unimi.dsi.fastutil.longs.Long2LongMaps; import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; import me.shedaniel.rei.api.client.REIRuntime; import me.shedaniel.rei.api.client.config.ConfigObject; @@ -122,7 +124,7 @@ public class ViewsImpl implements Views { DisplayRegistry displayRegistry = DisplayRegistry.getInstance(); DisplaysHolder displaysHolder = ((DisplayRegistryImpl) displayRegistry).displaysHolder(); - Map, Set> result = Maps.newLinkedHashMap(); + Map, Set> result = Maps.newHashMap(); forCategories(processingVisibilityHandlers, filteringCategories, displayRegistry, result, (configuration, categoryId, displays, set) -> { if (categories.contains(categoryId)) { // If the category is in the search, add all displays for (Display display : displays) { @@ -217,17 +219,23 @@ public class ViewsImpl implements Views { } }); - Map, List> resultSpec = (Map, List>) (Map) new LinkedHashMap<>(); - for (CategoryRegistry.CategoryConfiguration configuration : CategoryRegistry.getInstance()) { - Set displays = result.get(configuration.getCategory()); - if (displays == null) continue; - resultSpec.put(configuration.getCategory(), new ArrayList<>(displays)); + // Merging displays + Stopwatch mergingStopwatch = Stopwatch.createStarted(), sortingStopwatch = Stopwatch.createUnstarted(); + Map, List> merged = (Map, List>) (Map) new LinkedHashMap<>(); + for (Map.Entry, Set> entry : result.entrySet()) { + merged.put(entry.getKey(), new ArrayList<>(entry.getValue())); } - // optimize displays + if (builder.isMergingDisplays() && ConfigObject.getInstance().doMergeDisplayUnderOne()) { - mergeAndOptimize(result, resultSpec); + mergeAndOptimize(result, merged); } + mergingStopwatch.stop(); + // Sorting displays + sortingStopwatch.start(); + Map, List> sorted = sortDisplays(merged); + sortingStopwatch.stop(); + String message = String.format("Built Recipe View in %s for %d categories, %d recipes for, %d usages for and %d live recipe generators.", stopwatch.stop(), categories.size(), recipesForStacks.size(), usagesForStacks.size(), generatorsCount); if (ConfigObject.getInstance().doDebugSearchTimeRequired()) { @@ -235,7 +243,19 @@ public class ViewsImpl implements Views { } else { InternalLogger.getInstance().trace(message); } - return resultSpec; + return sorted; + } + + private static Map, List> sortDisplays(Map, List> unsorted) { + Object2IntMap> categoryOrder = new Object2IntOpenHashMap<>(); + categoryOrder.defaultReturnValue(Integer.MAX_VALUE); + int i = 0; + for (CategoryRegistry.CategoryConfiguration configuration : CategoryRegistry.getInstance()) { + categoryOrder.put(configuration.getCategoryIdentifier(), i++); + } + Map, List> result = new TreeMap<>(Comparator.comparingInt(category -> categoryOrder.getInt(category.getCategoryIdentifier()))); + result.putAll(unsorted); + return result; } private static void forCategories(boolean processingVisibilityHandlers, Set> filteringCategories, DisplayRegistry displayRegistry, Map, Set> result, QuadConsumer, CategoryIdentifier, List, Set> displayConsumer) { -- cgit From e6687c01b254bf903d4895aa0bb631b1679d465c Mon Sep 17 00:00:00 2001 From: shedaniel Date: Sun, 18 Dec 2022 20:24:23 +0800 Subject: Close #913 --- .../java/me/shedaniel/rei/api/client/config/ConfigObject.java | 3 +++ .../me/shedaniel/rei/impl/client/config/ConfigObjectImpl.java | 11 +++++++++++ .../impl/client/gui/widget/CraftableFilterButtonWidget.java | 8 ++++++++ .../client/gui/widget/entrylist/EntryListSearchManager.java | 11 ++++++++++- .../shedaniel/rei/impl/client/search/AsyncSearchManager.java | 11 ++++------- .../main/resources/assets/roughlyenoughitems/lang/en_us.json | 4 ++++ 6 files changed, 40 insertions(+), 8 deletions(-) diff --git a/api/src/main/java/me/shedaniel/rei/api/client/config/ConfigObject.java b/api/src/main/java/me/shedaniel/rei/api/client/config/ConfigObject.java index 02afcd7f4..94e119eae 100644 --- a/api/src/main/java/me/shedaniel/rei/api/client/config/ConfigObject.java +++ b/api/src/main/java/me/shedaniel/rei/api/client/config/ConfigObject.java @@ -135,6 +135,9 @@ public interface ConfigObject { */ boolean isEntryListWidgetScrolled(); + @ApiStatus.Experimental + boolean isHidingEntryPanelIfIdle(); + /** * Returns whether REI should append mod names to tooltips. * diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/config/ConfigObjectImpl.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/config/ConfigObjectImpl.java index 47f84a7c4..2ef46744b 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/config/ConfigObjectImpl.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/config/ConfigObjectImpl.java @@ -60,6 +60,7 @@ import java.util.Map; @ApiStatus.Internal @Config(name = "roughlyenoughitems/config") @Environment(EnvType.CLIENT) +@SuppressWarnings("FieldMayBeFinal") public class ConfigObjectImpl implements ConfigObject, ConfigData { @ConfigEntry.Category("basics") @ConfigEntry.Gui.TransitiveObject @DontApplyFieldName public Basics basics = new Basics(); @@ -139,6 +140,15 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData { appearance.scrollingEntryListWidget = scrollingEntryListWidget; } + @Override + public boolean isHidingEntryPanelIfIdle() { + return appearance.hideEntryPanelIfIdle; + } + + public void setHidingEntryPanelIfIdle(boolean hideEntryPanelIfIdle) { + appearance.hideEntryPanelIfIdle = hideEntryPanelIfIdle; + } + @Override public boolean shouldAppendModNames() { return advanced.tooltips.appendModNames; @@ -625,6 +635,7 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData { @Comment("Declares the appearance of recipe's border.") @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON) private RecipeBorderType recipeBorder = RecipeBorderType.DEFAULT; @Comment("Declares whether entry panel is scrolled.") private boolean scrollingEntryListWidget = false; + @Comment("Declares whether entry panel should be invisible when not searching") private boolean hideEntryPanelIfIdle = false; public static class Layout { @Comment("Declares the position of the search field.") @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON) diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/widget/CraftableFilterButtonWidget.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/widget/CraftableFilterButtonWidget.java index a4926f524..d9e3f2531 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/widget/CraftableFilterButtonWidget.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/widget/CraftableFilterButtonWidget.java @@ -28,6 +28,7 @@ import dev.architectury.platform.Platform; import dev.architectury.utils.value.BooleanValue; import me.shedaniel.math.Point; import me.shedaniel.math.Rectangle; +import me.shedaniel.rei.api.client.REIRuntime; import me.shedaniel.rei.api.client.config.ConfigManager; import me.shedaniel.rei.api.client.config.ConfigObject; import me.shedaniel.rei.api.client.favorites.FavoriteMenuEntry; @@ -113,6 +114,13 @@ public class CraftableFilterButtonWidget { entries.add(new SubMenuEntry(Component.translatable("text.rei.config.menu.search_field.input_method"), createInputMethodEntries(access, applicableInputMethods))); } + entries.add(ToggleMenuEntry.of(new TranslatableComponent("text.rei.config.menu.search_field.hide_entry_panel_idle"), + config::isHidingEntryPanelIfIdle, + hideEntryPanelIfIdle -> { + config.setHidingEntryPanelIfIdle(hideEntryPanelIfIdle); + REIRuntime.getInstance().getOverlay().orElseThrow().queueReloadSearch(); + })); + return entries; } diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/widget/entrylist/EntryListSearchManager.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/widget/entrylist/EntryListSearchManager.java index 0b1bf2662..50fd71d78 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/widget/entrylist/EntryListSearchManager.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/widget/entrylist/EntryListSearchManager.java @@ -32,6 +32,7 @@ import me.shedaniel.rei.api.client.config.ConfigObject; import me.shedaniel.rei.api.client.gui.config.EntryPanelOrdering; import me.shedaniel.rei.api.client.registry.entry.CollapsibleEntryRegistry; import me.shedaniel.rei.api.client.registry.entry.EntryRegistry; +import me.shedaniel.rei.api.client.search.SearchFilter; import me.shedaniel.rei.api.client.view.Views; import me.shedaniel.rei.api.common.entry.EntryStack; import me.shedaniel.rei.api.common.entry.type.VanillaEntryTypes; @@ -67,7 +68,7 @@ public class EntryListSearchManager { public static final EntryListSearchManager INSTANCE = new EntryListSearchManager(); - private final AsyncSearchManager searchManager = new AsyncSearchManager(((EntryRegistryImpl) EntryRegistry.getInstance())::getPreFilteredComplexList, () -> { + private final AsyncSearchManager searchManager = new AsyncSearchManager(EntryListSearchManager::getAllEntriesContextually, () -> { boolean checkCraftable = ConfigManager.getInstance().isCraftableOnlyEnabled(); LongSet workingItems = checkCraftable ? new LongOpenHashSet() : null; if (checkCraftable) { @@ -78,6 +79,14 @@ public class EntryListSearchManager { return checkCraftable ? stack -> workingItems.contains(stack.hashExact()) : stack -> true; }, HashedEntryStackWrapper::normalize); + private static List> getAllEntriesContextually(SearchFilter filter) { + if (ConfigObject.getInstance().isHidingEntryPanelIfIdle() && filter.getFilter().isEmpty()) { + return List.of(); + } + + return EntryRegistry.getInstance().getPreFilteredList(); + } + public void update(String searchTerm, boolean ignoreLastSearch, Consumer | CollapsedStack*/ Object>> update) { Stopwatch stopwatch = Stopwatch.createStarted(); if (ignoreLastSearch) searchManager.markDirty(); diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/search/AsyncSearchManager.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/search/AsyncSearchManager.java index 8d1f6f056..381831c42 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/search/AsyncSearchManager.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/search/AsyncSearchManager.java @@ -41,21 +41,18 @@ import java.util.List; import java.util.Map; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiConsumer; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.function.UnaryOperator; +import java.util.function.*; public class AsyncSearchManager { private static final ExecutorService EXECUTOR_SERVICE = new ThreadCreator("REI-AsyncSearchManager").asService(Math.min(3, Runtime.getRuntime().availableProcessors())); - private final Supplier> stacksProvider; + private final Function> stacksProvider; private final Supplier> additionalPredicateSupplier; private final UnaryOperator transformer; private volatile Map.Entry, SearchFilter> last; public volatile ExecutorTuple executor; public volatile SearchFilter filter; - public AsyncSearchManager(Supplier> stacksProvider, Supplier> additionalPredicateSupplier, UnaryOperator transformer) { + public AsyncSearchManager(Function> stacksProvider, Supplier> additionalPredicateSupplier, UnaryOperator transformer) { this.stacksProvider = stacksProvider; this.additionalPredicateSupplier = additionalPredicateSupplier; this.transformer = transformer; @@ -123,7 +120,7 @@ public class AsyncSearchManager { Map.Entry, SearchFilter> last; last = this.last; return get(this.filter, this.additionalPredicateSupplier.get(), this.transformer, - this.stacksProvider.get(), last, this, executor, steps) + this.stacksProvider.apply(filter), last, this, executor, steps) .thenApply(entry -> { this.last = entry; return entry; diff --git a/runtime/src/main/resources/assets/roughlyenoughitems/lang/en_us.json b/runtime/src/main/resources/assets/roughlyenoughitems/lang/en_us.json index 1e3b150fa..54aa533c2 100755 --- a/runtime/src/main/resources/assets/roughlyenoughitems/lang/en_us.json +++ b/runtime/src/main/resources/assets/roughlyenoughitems/lang/en_us.json @@ -34,6 +34,7 @@ "text.rei.config.menu.config": "More Options...", "text.rei.config.menu.search_field.position": "Search Field Position...", "text.rei.config.menu.search_field.input_method": "Input Methods...", + "text.rei.config.menu.search_field.hide_entry_panel_idle": "Hide Entries when not Searching", "category.rei.crafting": "Crafting", "category.rei.smelting": "Smelting", "category.rei.smelting.fuel": "Fuel", @@ -334,6 +335,9 @@ "config.roughlyenoughitems.scrollingEntryListWidget": "Entry List Action:", "config.roughlyenoughitems.scrollingEntryListWidget.boolean.true": "Scrolled", "config.roughlyenoughitems.scrollingEntryListWidget.boolean.false": "Paginated", + "config.roughlyenoughitems.hideEntryPanelIfIdle": "Entry List When Not Searching:", + "config.roughlyenoughitems.hideEntryPanelIfIdle.boolean.true": "Invisible", + "config.roughlyenoughitems.hideEntryPanelIfIdle.boolean.false": "Visible", "config.roughlyenoughitems.horizontalEntriesBoundaries": "Horizontal Entries Boundaries:", "config.roughlyenoughitems.verticalEntriesBoundaries": "Vertical Entries Boundaries:", "config.roughlyenoughitems.horizontalEntriesBoundariesColumns": "Entries Columns Limit:", -- cgit From ffe21652b40a93a00f33a27a5ecf41479b48bcd9 Mon Sep 17 00:00:00 2001 From: shedaniel Date: Sun, 18 Dec 2022 20:50:56 +0800 Subject: Close #1131 --- .../client/registry/display/DisplayRegistry.java | 121 +++++++++++++++++++-- .../rei/plugin/client/DefaultClientPlugin.java | 23 +--- .../crafting/filler/TippedArrowRecipeFiller.java | 71 ++++++++++++ .../registry/display/DisplayRegistryImpl.java | 47 +++++--- 4 files changed, 221 insertions(+), 41 deletions(-) create mode 100644 default-plugin/src/main/java/me/shedaniel/rei/plugin/client/categories/crafting/filler/TippedArrowRecipeFiller.java diff --git a/api/src/main/java/me/shedaniel/rei/api/client/registry/display/DisplayRegistry.java b/api/src/main/java/me/shedaniel/rei/api/client/registry/display/DisplayRegistry.java index fd34e4e84..b708a2e31 100644 --- a/api/src/main/java/me/shedaniel/rei/api/client/registry/display/DisplayRegistry.java +++ b/api/src/main/java/me/shedaniel/rei/api/client/registry/display/DisplayRegistry.java @@ -212,7 +212,7 @@ public interface DisplayRegistry extends RecipeManagerContext { * Registers a display filler, to be filled during {@link #tryFillDisplay(Object)}. *

* Vanilla {@link Recipe} are by default filled, display filters - * can be used to automatically generate displaies for vanilla {@link Recipe}. + * can be used to automatically generate displays for vanilla {@link Recipe}. * * @param typeClass the type of {@code T} * @param filler the filler, taking a {@code T} and returning a {@code D} @@ -227,7 +227,23 @@ public interface DisplayRegistry extends RecipeManagerContext { * Registers a display filler, to be filled during {@link #tryFillDisplay(Object)}. *

* Vanilla {@link Recipe} are by default filled, display filters - * can be used to automatically generate displaies for vanilla {@link Recipe}. + * can be used to automatically generate displays for vanilla {@link Recipe}. + * + * @param typeClass the type of {@code T} + * @param filler the filler, taking a {@code T} and returning a {@code D} + * @param the type of object + * @param the type of display + */ + @ApiStatus.Experimental + default , D extends Display> void registerRecipesFiller(Class typeClass, RecipeType recipeType, Function> filler) { + registerRecipesFiller(typeClass, type -> Objects.equals(recipeType, type), filler); + } + + /** + * Registers a display filler, to be filled during {@link #tryFillDisplay(Object)}. + *

+ * Vanilla {@link Recipe} are by default filled, display filters + * can be used to automatically generate displays for vanilla {@link Recipe}. * * @param typeClass the type of {@code T} * @param filler the filler, taking a {@code T} and returning a {@code D} @@ -242,7 +258,23 @@ public interface DisplayRegistry extends RecipeManagerContext { * Registers a display filler, to be filled during {@link #tryFillDisplay(Object)}. *

* Vanilla {@link Recipe} are by default filled, display filters - * can be used to automatically generate displaies for vanilla {@link Recipe}. + * can be used to automatically generate displays for vanilla {@link Recipe}. + * + * @param typeClass the type of {@code T} + * @param filler the filler, taking a {@code T} and returning a {@code D} + * @param the type of object + * @param the type of display + */ + @ApiStatus.Experimental + default , D extends Display> void registerRecipesFiller(Class typeClass, Predicate> recipeType, Function> filler) { + registerRecipesFiller(typeClass, recipeType, Predicates.alwaysTrue(), filler); + } + + /** + * Registers a display filler, to be filled during {@link #tryFillDisplay(Object)}. + *

+ * Vanilla {@link Recipe} are by default filled, display filters + * can be used to automatically generate displays for vanilla {@link Recipe}. * * @param typeClass the type of {@code T} * @param filler the filler, taking a {@code T} and returning a {@code D} @@ -257,7 +289,23 @@ public interface DisplayRegistry extends RecipeManagerContext { * Registers a display filler, to be filled during {@link #tryFillDisplay(Object)}. *

* Vanilla {@link Recipe} are by default filled, display filters - * can be used to automatically generate displaies for vanilla {@link Recipe}. + * can be used to automatically generate displays for vanilla {@link Recipe}. + * + * @param typeClass the type of {@code T} + * @param filler the filler, taking a {@code T} and returning a {@code D} + * @param the type of object + * @param the type of display + */ + @ApiStatus.Experimental + default , D extends Display> void registerRecipesFiller(Class typeClass, Predicate> recipeType, Predicate predicate, Function> filler) { + registerDisplaysFiller(typeClass, recipe -> recipeType.test((RecipeType) recipe.getType()) && ((Predicate) predicate).test(recipe), filler); + } + + /** + * Registers a display filler, to be filled during {@link #tryFillDisplay(Object)}. + *

+ * Vanilla {@link Recipe} are by default filled, display filters + * can be used to automatically generate displays for vanilla {@link Recipe}. * * @param typeClass the type of {@code T} * @param filler the filler, taking a {@code T} and returning a {@code D} @@ -272,7 +320,23 @@ public interface DisplayRegistry extends RecipeManagerContext { * Registers a display filler, to be filled during {@link #tryFillDisplay(Object)}. *

* Vanilla {@link Recipe} are by default filled, display filters - * can be used to automatically generate displaies for vanilla {@link Recipe}. + * can be used to automatically generate displays for vanilla {@link Recipe}. + * + * @param typeClass the type of {@code T} + * @param filler the filler, taking a {@code T} and returning a {@code D} + * @param the type of object + * @param the type of display + */ + @ApiStatus.Experimental + default void registerDisplaysFiller(Class typeClass, Function> filler) { + registerDisplaysFiller(typeClass, Predicates.alwaysTrue(), filler); + } + + /** + * Registers a display filler, to be filled during {@link #tryFillDisplay(Object)}. + *

+ * Vanilla {@link Recipe} are by default filled, display filters + * can be used to automatically generate displays for vanilla {@link Recipe}. * * @param typeClass the type of {@code T} * @param predicate the predicate of {@code T} @@ -286,7 +350,22 @@ public interface DisplayRegistry extends RecipeManagerContext { * Registers a display filler, to be filled during {@link #tryFillDisplay(Object)}. *

* Vanilla {@link Recipe} are by default filled, display filters - * can be used to automatically generate displaies for vanilla {@link Recipe}. + * can be used to automatically generate displays for vanilla {@link Recipe}. + * + * @param typeClass the type of {@code T} + * @param predicate the predicate of {@code T} + * @param filler the filler, taking a {@code T} and returning a {@code D} + * @param the type of object + * @param the type of display + */ + @ApiStatus.Experimental + void registerDisplaysFiller(Class typeClass, Predicate predicate, Function> filler); + + /** + * Registers a display filler, to be filled during {@link #tryFillDisplay(Object)}. + *

+ * Vanilla {@link Recipe} are by default filled, display filters + * can be used to automatically generate displays for vanilla {@link Recipe}. * * @param typeClass the type of {@code T} * @param predicate the predicate of {@code T} and reason @@ -301,7 +380,22 @@ public interface DisplayRegistry extends RecipeManagerContext { * Registers a display filler, to be filled during {@link #tryFillDisplay(Object)}. *

* Vanilla {@link Recipe} are by default filled, display filters - * can be used to automatically generate displaies for vanilla {@link Recipe}. + * can be used to automatically generate displays for vanilla {@link Recipe}. + * + * @param typeClass the type of {@code T} + * @param predicate the predicate of {@code T} and reason + * @param filler the filler, taking a {@code T} and returning a {@code D} + * @param the type of object + * @param the type of display + */ + @ApiStatus.Experimental + void registerDisplaysFiller(Class typeClass, BiPredicate predicate, Function> filler); + + /** + * Registers a display filler, to be filled during {@link #tryFillDisplay(Object)}. + *

+ * Vanilla {@link Recipe} are by default filled, display filters + * can be used to automatically generate displays for vanilla {@link Recipe}. * * @param predicate the predicate of the object * @param filler the filler, taking an object and returning a {@code D} @@ -309,6 +403,19 @@ public interface DisplayRegistry extends RecipeManagerContext { */ void registerFiller(Predicate predicate, Function filler); + /** + * Registers a display filler, to be filled during {@link #tryFillDisplay(Object)}. + *

+ * Vanilla {@link Recipe} are by default filled, display filters + * can be used to automatically generate displays for vanilla {@link Recipe}. + * + * @param predicate the predicate of the object + * @param filler the filler, taking an object and returning a {@code D} + * @param the type of display + */ + @ApiStatus.Experimental + void registerDisplaysFiller(Predicate predicate, Function> filler); + /** * Tries to fill displays from {@code T}. * diff --git a/default-plugin/src/main/java/me/shedaniel/rei/plugin/client/DefaultClientPlugin.java b/default-plugin/src/main/java/me/shedaniel/rei/plugin/client/DefaultClientPlugin.java index bf1541003..50ffc35ef 100644 --- a/default-plugin/src/main/java/me/shedaniel/rei/plugin/client/DefaultClientPlugin.java +++ b/default-plugin/src/main/java/me/shedaniel/rei/plugin/client/DefaultClientPlugin.java @@ -29,6 +29,7 @@ import com.google.common.collect.Sets; import dev.architectury.event.EventResult; import dev.architectury.networking.NetworkManager; import dev.architectury.platform.Platform; +import it.unimi.dsi.fastutil.objects.Object2FloatMap; import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; import it.unimi.dsi.fastutil.objects.ReferenceSet; import me.shedaniel.math.Rectangle; @@ -55,6 +56,7 @@ import me.shedaniel.rei.plugin.client.categories.beacon.DefaultBeaconBaseCategor import me.shedaniel.rei.plugin.client.categories.beacon.DefaultBeaconPaymentCategory; import me.shedaniel.rei.plugin.client.categories.cooking.DefaultCookingCategory; import me.shedaniel.rei.plugin.client.categories.crafting.DefaultCraftingCategory; +import me.shedaniel.rei.plugin.client.categories.crafting.filler.TippedArrowRecipeFiller; import me.shedaniel.rei.plugin.client.categories.tag.DefaultTagCategory; import me.shedaniel.rei.plugin.client.exclusionzones.DefaultPotionEffectExclusionZones; import me.shedaniel.rei.plugin.client.exclusionzones.DefaultRecipeBookExclusionZones; @@ -73,7 +75,6 @@ import me.shedaniel.rei.plugin.common.displays.cooking.DefaultBlastingDisplay; import me.shedaniel.rei.plugin.common.displays.cooking.DefaultSmeltingDisplay; import me.shedaniel.rei.plugin.common.displays.cooking.DefaultSmokingDisplay; import me.shedaniel.rei.plugin.common.displays.crafting.DefaultCraftingDisplay; -import me.shedaniel.rei.plugin.common.displays.crafting.DefaultCustomDisplay; import me.shedaniel.rei.plugin.common.displays.tag.DefaultTagDisplay; import me.shedaniel.rei.plugin.common.displays.tag.TagNodes; import net.fabricmc.api.EnvType; @@ -253,25 +254,7 @@ public class DefaultClientPlugin implements REIClientPlugin, BuiltinClientPlugin for (Map.Entry entry : AbstractFurnaceBlockEntity.getFuel().entrySet()) { registry.add(new DefaultFuelDisplay(Collections.singletonList(EntryIngredients.of(entry.getKey())), Collections.emptyList(), entry.getValue())); } - EntryIngredient arrowStack = EntryIngredient.of(EntryStacks.of(Items.ARROW)); - ReferenceSet registeredPotions = new ReferenceOpenHashSet<>(); - EntryRegistry.getInstance().getEntryStacks().filter(entry -> entry.getValueType() == ItemStack.class && entry.castValue().getItem() == Items.LINGERING_POTION).forEach(entry -> { - ItemStack itemStack = (ItemStack) entry.getValue(); - Potion potion = PotionUtils.getPotion(itemStack); - if (registeredPotions.add(potion)) { - List input = new ArrayList<>(); - for (int i = 0; i < 4; i++) - input.add(arrowStack); - input.add(EntryIngredients.of(itemStack)); - for (int i = 0; i < 4; i++) - input.add(arrowStack); - ItemStack outputStack = new ItemStack(Items.TIPPED_ARROW, 8); - PotionUtils.setPotion(outputStack, potion); - PotionUtils.setCustomEffects(outputStack, PotionUtils.getCustomEffects(itemStack)); - EntryIngredient output = EntryIngredients.of(outputStack); - registry.add(new DefaultCustomDisplay(null, input, Collections.singletonList(output))); - } - }); + registry.registerRecipesFiller(TippedArrowRecipe.class, RecipeType.CRAFTING, new TippedArrowRecipeFiller()::apply); if (ComposterBlock.COMPOSTABLES.isEmpty()) { ComposterBlock.bootStrap(); } diff --git a/default-plugin/src/main/java/me/shedaniel/rei/plugin/client/categories/crafting/filler/TippedArrowRecipeFiller.java b/default-plugin/src/main/java/me/shedaniel/rei/plugin/client/categories/crafting/filler/TippedArrowRecipeFiller.java new file mode 100644 index 000000000..014799f6f --- /dev/null +++ b/default-plugin/src/main/java/me/shedaniel/rei/plugin/client/categories/crafting/filler/TippedArrowRecipeFiller.java @@ -0,0 +1,71 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022 shedaniel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.shedaniel.rei.plugin.client.categories.crafting.filler; + +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import it.unimi.dsi.fastutil.objects.ReferenceSet; +import me.shedaniel.rei.api.client.registry.entry.EntryRegistry; +import me.shedaniel.rei.api.common.display.Display; +import me.shedaniel.rei.api.common.entry.EntryIngredient; +import me.shedaniel.rei.api.common.util.EntryIngredients; +import me.shedaniel.rei.api.common.util.EntryStacks; +import me.shedaniel.rei.plugin.common.displays.crafting.DefaultCustomDisplay; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.alchemy.Potion; +import net.minecraft.world.item.alchemy.PotionUtils; +import net.minecraft.world.item.crafting.TippedArrowRecipe; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +public class TippedArrowRecipeFiller implements Function> { + @Override + public Collection apply(TippedArrowRecipe recipe) { + EntryIngredient arrowStack = EntryIngredient.of(EntryStacks.of(Items.ARROW)); + ReferenceSet registeredPotions = new ReferenceOpenHashSet<>(); + List displays = new ArrayList<>(); + + EntryRegistry.getInstance().getEntryStacks().filter(entry -> entry.getValueType() == ItemStack.class && entry.castValue().getItem() == Items.LINGERING_POTION).forEach(entry -> { + ItemStack itemStack = (ItemStack) entry.getValue(); + Potion potion = PotionUtils.getPotion(itemStack); + if (registeredPotions.add(potion)) { + List input = new ArrayList<>(); + for (int i = 0; i < 4; i++) + input.add(arrowStack); + input.add(EntryIngredients.of(itemStack)); + for (int i = 0; i < 4; i++) + input.add(arrowStack); + ItemStack outputStack = new ItemStack(Items.TIPPED_ARROW, 8); + PotionUtils.setPotion(outputStack, potion); + PotionUtils.setCustomEffects(outputStack, PotionUtils.getCustomEffects(itemStack)); + displays.add(new DefaultCustomDisplay(recipe, input, List.of(EntryIngredients.of(outputStack)))); + } + }); + + return displays; + } +} diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/registry/display/DisplayRegistryImpl.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/registry/display/DisplayRegistryImpl.java index f8f303408..655466211 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/registry/display/DisplayRegistryImpl.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/registry/display/DisplayRegistryImpl.java @@ -145,15 +145,32 @@ public class DisplayRegistryImpl extends RecipeManagerContextImpl typeClass.isInstance(o) && ((Predicate) predicate).test((T) o), o -> ((Function) filler).apply((T) o)); } + @Override + public void registerDisplaysFiller(Class typeClass, Predicate predicate, Function> filler) { + registerDisplaysFiller(o -> typeClass.isInstance(o) && ((Predicate) predicate).test((T) o), o -> ((Function>) filler).apply((T) o)); + } + @Override public void registerFiller(Class typeClass, BiPredicate predicate, Function filler) { - fillers.add(new DisplayFiller<>((o, s) -> typeClass.isInstance(o) && ((BiPredicate) predicate).test(o, s), (Function) filler)); + fillers.add(DisplayFiller.of((o, s) -> typeClass.isInstance(o) && ((BiPredicate) predicate).test(o, s), (Function) filler)); + InternalLogger.getInstance().debug("Added display filter: %s for %s", filler, typeClass.getName()); + } + + @Override + public void registerDisplaysFiller(Class typeClass, BiPredicate predicate, Function> filler) { + fillers.add(new DisplayFiller<>((o, s) -> typeClass.isInstance(o) && ((BiPredicate) predicate).test(o, s), (Function>) filler)); InternalLogger.getInstance().debug("Added display filter: %s for %s", filler, typeClass.getName()); } @Override public void registerFiller(Predicate predicate, Function filler) { - fillers.add(new DisplayFiller<>((o, s) -> ((Predicate) predicate).test(o), (Function) filler)); + fillers.add(DisplayFiller.of((o, s) -> ((Predicate) predicate).test(o), (Function) filler)); + InternalLogger.getInstance().debug("Added display filter: %s", filler); + } + + @Override + public void registerDisplaysFiller(Predicate predicate, Function> filler) { + fillers.add(new DisplayFiller<>((o, s) -> ((Predicate) predicate).test(o), (Function>) filler)); InternalLogger.getInstance().debug("Added display filter: %s", filler); } @@ -194,28 +211,27 @@ public class DisplayRegistryImpl extends RecipeManagerContextImpl Collection tryFillDisplay(T value, DisplayAdditionReason... reason) { if (value instanceof Display) return Collections.singleton((Display) value); - List displays = null; + List out = null; DisplayAdditionReasons reasons = reason.length == 0 ? DisplayAdditionReasons.Impl.EMPTY : new DisplayAdditionReasons.Impl(reason); for (DisplayFiller filler : fillers) { - Display display = tryFillDisplayGenerics(filler, value, reasons); - if (display != null) { - if (displays == null) displays = Collections.singletonList(display); - else { - if (!(displays instanceof ArrayList)) displays = new ArrayList<>(displays); - displays.add(display); + Collection displays = tryFillDisplayGenerics(filler, value, reasons); + if (displays != null && !displays.isEmpty()) { + if (out == null) out = new ArrayList<>(); + for (Display display : displays) { + if (display != null) out.add(display); } } } - if (displays != null) { - return displays; + if (out != null) { + return out; } return Collections.emptyList(); } - private D tryFillDisplayGenerics(DisplayFiller filler, Object value, DisplayAdditionReasons reasons) { + private Collection tryFillDisplayGenerics(DisplayFiller filler, Object value, DisplayAdditionReasons reasons) { try { if (filler.predicate.test(value, reasons)) { - return filler.mappingFunction.apply(value); + return (Collection) filler.mappingFunction.apply(value); } } catch (Throwable e) { throw new RuntimeException("Failed to fill displays!", e); @@ -233,7 +249,10 @@ public class DisplayRegistryImpl extends RecipeManagerContextImpl( BiPredicate predicate, - Function mappingFunction + Function> mappingFunction ) { + public static DisplayFiller of(BiPredicate predicate, Function mappingFunction) { + return new DisplayFiller<>(predicate, o -> Collections.singleton(mappingFunction.apply(o))); + } } } -- cgit From c93345b568ba0c6231986cac30485e689212c731 Mon Sep 17 00:00:00 2001 From: shedaniel Date: Sun, 25 Dec 2022 00:10:10 +0800 Subject: Implement Unified Ingredients --- .../rei/api/common/entry/EntryIngredient.java | 51 +++ .../entry/settings/EntryIngredientSetting.java | 33 ++ .../rei/api/common/util/CollectionUtils.java | 9 +- .../gui/screen/AbstractDisplayViewingScreen.java | 190 ++++++++++- .../gui/screen/CompositeDisplayViewingScreen.java | 1 + .../gui/screen/DefaultDisplayViewingScreen.java | 2 + .../rei/impl/client/gui/widget/EntryWidget.java | 96 +++--- .../client/util/AbstractIndexedCyclingList.java | 94 ++++++ .../rei/impl/client/util/ClientTickCounter.java | 40 +++ .../impl/client/util/ConcatenatedListIterator.java | 189 +++++++++++ .../rei/impl/client/util/CyclingList.java | 353 +++++++++++++++++++++ .../client/util/OriginalRetainingCyclingList.java | 176 ++++++++++ .../rei/impl/common/entry/EntryIngredientImpl.java | 67 +++- runtime/src/test/java/CyclingListTest.java | 338 ++++++++++++++++++++ 14 files changed, 1587 insertions(+), 52 deletions(-) create mode 100644 api/src/main/java/me/shedaniel/rei/api/common/entry/settings/EntryIngredientSetting.java create mode 100644 runtime/src/main/java/me/shedaniel/rei/impl/client/util/AbstractIndexedCyclingList.java create mode 100644 runtime/src/main/java/me/shedaniel/rei/impl/client/util/ClientTickCounter.java create mode 100644 runtime/src/main/java/me/shedaniel/rei/impl/client/util/ConcatenatedListIterator.java create mode 100644 runtime/src/main/java/me/shedaniel/rei/impl/client/util/CyclingList.java create mode 100644 runtime/src/main/java/me/shedaniel/rei/impl/client/util/OriginalRetainingCyclingList.java create mode 100644 runtime/src/test/java/CyclingListTest.java diff --git a/api/src/main/java/me/shedaniel/rei/api/common/entry/EntryIngredient.java b/api/src/main/java/me/shedaniel/rei/api/common/entry/EntryIngredient.java index f0c634b11..f21719676 100644 --- a/api/src/main/java/me/shedaniel/rei/api/common/entry/EntryIngredient.java +++ b/api/src/main/java/me/shedaniel/rei/api/common/entry/EntryIngredient.java @@ -23,13 +23,16 @@ package me.shedaniel.rei.api.common.entry; +import me.shedaniel.rei.api.common.entry.settings.EntryIngredientSetting; import me.shedaniel.rei.api.common.entry.type.EntryDefinition; import me.shedaniel.rei.impl.Internals; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.UUID; import java.util.function.Predicate; import java.util.function.UnaryOperator; import java.util.stream.Collector; @@ -198,6 +201,54 @@ public interface EntryIngredient extends List> { */ EntryIngredient map(UnaryOperator> transformer); + /** + * Returns the value of a {@link EntryIngredientSetting} of this {@link EntryIngredient}. + *

+ * This method returns {@code null} if the setting is not set. + * + * @param setting the setting to get + * @param the type of the setting + * @return the value of the setting + */ + @Nullable + @ApiStatus.Experimental + T getSetting(EntryIngredientSetting setting); + + /** + * Applies a setting to this {@link EntryIngredient}. + *

+ * It is generally not recommended to use this method, but to instead use the helper + * methods such as {@link EntryIngredient#unifyFocuses(EntryIngredient...)}. + * + * @param setting the setting to apply + * @param value the value of the setting to apply + * @param the type of the setting + * @return this {@link EntryStack} + */ + @ApiStatus.Experimental + EntryIngredient setting(EntryIngredientSetting setting, T value); + + /** + * Unifies focuses for the given {@link EntryIngredient}s, so the selection for these + * {@link EntryIngredient}s will be the same. + *

+ * For example, a recipe that accepts some type of banner pattern and will output a certain + * type of cloned banner pattern should have both these ingredients unified, such that + * the natural cycling of the ingredient should still make sure these two ingredients + * are matching. + *

+ * For that reason, all ingredients passed to this method must have the same size. + * + * @param ingredients the ingredients to unify + */ + @ApiStatus.Experimental + static void unifyFocuses(EntryIngredient... ingredients) { + UUID uuid = UUID.randomUUID(); + for (EntryIngredient ingredient : ingredients) { + ingredient.setting(EntryIngredientSetting.FOCUS_UUID, uuid); + } + } + @ApiStatus.NonExtendable interface Builder { Builder add(EntryStack stack); diff --git a/api/src/main/java/me/shedaniel/rei/api/common/entry/settings/EntryIngredientSetting.java b/api/src/main/java/me/shedaniel/rei/api/common/entry/settings/EntryIngredientSetting.java new file mode 100644 index 000000000..88766ff5d --- /dev/null +++ b/api/src/main/java/me/shedaniel/rei/api/common/entry/settings/EntryIngredientSetting.java @@ -0,0 +1,33 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022 shedaniel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.shedaniel.rei.api.common.entry.settings; + +import org.jetbrains.annotations.ApiStatus; + +import java.util.UUID; + +@ApiStatus.Experimental +public interface EntryIngredientSetting { + EntryIngredientSetting FOCUS_UUID = new EntryIngredientSetting<>() {}; +} diff --git a/api/src/main/java/me/shedaniel/rei/api/common/util/CollectionUtils.java b/api/src/main/java/me/shedaniel/rei/api/common/util/CollectionUtils.java index bd8376e53..3ac72cdf1 100644 --- a/api/src/main/java/me/shedaniel/rei/api/common/util/CollectionUtils.java +++ b/api/src/main/java/me/shedaniel/rei/api/common/util/CollectionUtils.java @@ -514,6 +514,10 @@ public class CollectionUtils { @SafeVarargs public static List concatUnmodifiable(List... lists) { + return new ListConcatenationView<>(Arrays.asList(lists)); + } + + public static List concatUnmodifiable(Iterable> lists) { return new ListConcatenationView<>(lists); } @@ -521,10 +525,9 @@ public class CollectionUtils { * A list which acts as view of the concatenation of a number of lists. */ private static class ListConcatenationView extends AbstractList { - private final List[] lists; + private final Iterable> lists; - @SafeVarargs - public ListConcatenationView(List... lists) { + public ListConcatenationView(Iterable> lists) { this.lists = lists; } diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/AbstractDisplayViewingScreen.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/AbstractDisplayViewingScreen.java index b2d26599a..91e4bc3af 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/AbstractDisplayViewingScreen.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/AbstractDisplayViewingScreen.java @@ -30,6 +30,9 @@ import com.mojang.datafixers.util.Pair; import com.mojang.math.Matrix4f; import dev.architectury.fluid.FluidStack; import dev.architectury.utils.value.IntValue; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; import me.shedaniel.math.Rectangle; import me.shedaniel.rei.api.client.REIRuntime; import me.shedaniel.rei.api.client.config.ConfigObject; @@ -45,7 +48,9 @@ import me.shedaniel.rei.api.client.registry.display.DisplayCategoryView; import me.shedaniel.rei.api.client.registry.entry.EntryRegistry; import me.shedaniel.rei.api.common.category.CategoryIdentifier; import me.shedaniel.rei.api.common.display.Display; +import me.shedaniel.rei.api.common.entry.EntryIngredient; import me.shedaniel.rei.api.common.entry.EntryStack; +import me.shedaniel.rei.api.common.entry.settings.EntryIngredientSetting; import me.shedaniel.rei.api.common.entry.type.EntryType; import me.shedaniel.rei.api.common.entry.type.VanillaEntryTypes; import me.shedaniel.rei.api.common.util.CollectionUtils; @@ -53,6 +58,8 @@ import me.shedaniel.rei.impl.client.REIRuntimeImpl; import me.shedaniel.rei.impl.client.gui.widget.EntryWidget; import me.shedaniel.rei.impl.client.gui.widget.TabContainerWidget; import me.shedaniel.rei.impl.client.gui.widget.entrylist.EntryListWidget; +import me.shedaniel.rei.impl.client.util.ClientTickCounter; +import me.shedaniel.rei.impl.client.util.CyclingList; import me.shedaniel.rei.impl.display.DisplaySpec; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; @@ -221,13 +228,30 @@ public abstract class AbstractDisplayViewingScreen extends Screen implements Dis private static void transformNotice(int marker, List setupDisplay, List> noticeStacks) { if (noticeStacks.isEmpty()) return; + Map, LongSet> noticeSet = new HashMap<>(); for (EntryWidget widget : Widgets.walk(setupDisplay, EntryWidget.class::isInstance)) { - if (widget.getNoticeMark() == marker && widget.getEntries().size() > 1) { + List> entries = widget.getEntries(); + if (widget.getNoticeMark() == marker && entries.size() > 1) { for (EntryStack noticeStack : noticeStacks) { - EntryStack stack = CollectionUtils.findFirstOrNullEqualsExact(widget.getEntries(), noticeStack); + EntryStack stack = CollectionUtils.findFirstOrNullEqualsExact(entries, noticeStack); if (stack != null) { widget.clearStacks(); widget.entry(stack); + if (entries instanceof EntryIngredient ingredient) noticeSet.computeIfAbsent(stack, $ -> new LongOpenHashSet()) + .add(hashFocusIngredient(ingredient)); + break; + } + } + } + } + for (EntryWidget widget : Widgets.walk(setupDisplay, EntryWidget.class::isInstance)) { + List> entries = widget.getEntries(); + if (widget.getNoticeMark() != marker && entries.size() > 1 && entries instanceof EntryIngredient ingredient) { + long hashFocusIngredient = hashFocusIngredient(ingredient); + for (Map.Entry, LongSet> entry : noticeSet.entrySet()) { + if (entry.getValue().contains(hashFocusIngredient)) { + widget.clearStacks(); + widget.entry(entry.getKey()); break; } } @@ -235,18 +259,31 @@ public abstract class AbstractDisplayViewingScreen extends Screen implements Dis } } + @SuppressWarnings("RedundantCast") protected void transformFiltering(List setupDisplay) { for (EntryWidget widget : Widgets.walk(setupDisplay, EntryWidget.class::isInstance)) { if (widget.getEntries().size() > 1) { Collection> refiltered = EntryRegistry.getInstance().refilterNew(false, widget.getEntries()); - if (!refiltered.isEmpty()) { + EntryIngredient asEntryIngredient = widget.getEntries() instanceof EntryIngredient ingredient ? ingredient : null; + if (!refiltered.isEmpty() && !widget.getEntries().equals(refiltered)) { widget.clearStacks(); - widget.entries(refiltered); + EntryIngredient newIngredient = EntryIngredient.of(refiltered); + if (asEntryIngredient != null && (Object) asEntryIngredient.getSetting(EntryIngredientSetting.FOCUS_UUID) instanceof UUID uuid) { + newIngredient.setting(EntryIngredientSetting.FOCUS_UUID, + new UUID(uuid.getMostSignificantBits() ^ refiltered.size(), uuid.getLeastSignificantBits() ^ refiltered.size())); + } + widget.entries(newIngredient); } } } } + protected static long hashFocusIngredient(EntryIngredient ingredient) { + UUID uuid = ingredient.getSetting(EntryIngredientSetting.FOCUS_UUID); + if (uuid == null) return System.identityHashCode(ingredient); + return uuid.hashCode() ^ ingredient.size(); + } + protected void setupTags(List widgets) { outer: for (EntryWidget widget : Widgets.walk(widgets, EntryWidget.class::isInstance)) { @@ -283,6 +320,32 @@ public abstract class AbstractDisplayViewingScreen extends Screen implements Dis } } + protected void unifyIngredients(List widgets) { + Map> slots = new TreeMap<>(Comparator.comparingLong(AbstractDisplayViewingScreen::hashFocusIngredient)); + for (EntryWidget slot : Widgets.walk(widgets, EntryWidget.class::isInstance)) { + CyclingList> entries = slot.getBackingCyclingEntries(); + if (entries.get() instanceof EntryIngredient ingredient) { + slots.computeIfAbsent(ingredient, key -> new ArrayList<>()).add(slot); + } + } + for (Map.Entry> entry : slots.entrySet()) { + List slotList = entry.getValue(); + if (slotList.size() > 1) { + List>> all = new ArrayList<>(); + Limiter>> limiter = new TickCountLimiter<>(all); + for (EntryWidget slot : slotList) { + CyclingList> limited; + CyclingList> backing = slot.getBackingCyclingEntries(); + if (backing instanceof CyclingList.Mutable> mutable) + limited = new LimitedCyclingList.Mutable<>(mutable, limiter); + else limited = new LimitedCyclingList<>(backing, limiter); + slot.entries(limited); + all.add(backing); + } + } + } + } + private static final int MAX_WIDTH = 200; private void addCyclingTooltip(EntryWidget widget) { @@ -443,4 +506,123 @@ public abstract class AbstractDisplayViewingScreen extends Screen implements Dis public boolean charTyped(char character, int modifiers) { return super.charTyped(character, modifiers) || (getOverlay().charTyped(character, modifiers) && handleFocuses()); } + + private interface Limiter { + boolean canExecute(T t); + + List getEntries(); + } + + private static class TickCountLimiter implements Limiter { + private int ticks = -1; + private final List list; + private final Set set = new ReferenceOpenHashSet<>(); + + public TickCountLimiter(List list) { + this.list = list; + } + + @Override + public boolean canExecute(T t) { + int currentTick = ClientTickCounter.getTicks(); + if (this.ticks != currentTick) { + this.ticks = currentTick; + this.set.clear(); + } + return this.set.add(t); + } + + @Override + public List getEntries() { + return list; + } + } + + private static class LimitedCyclingList implements CyclingList { + protected final CyclingList provider; + private final Limiter> limiter; + + public LimitedCyclingList(CyclingList provider, Limiter> limiter) { + this.provider = provider; + this.limiter = limiter; + } + + @Override + public T peek() { + return provider.peek(); + } + + @Override + public void resetToStart() { + provider.resetToStart(); + } + + @Override + public int size() { + return provider.size(); + } + + @Override + public int currentIndex() { + return provider.currentIndex(); + } + + @Override + public T previous() { + if (this.limiter.canExecute(provider)) { + for (CyclingList list : this.limiter.getEntries()) { + list.previous(); + } + } + + return provider.peek(); + } + + @Override + public int nextIndex() { + return provider.nextIndex(); + } + + @Override + public int previousIndex() { + return provider.previousIndex(); + } + + @Override + public T next() { + if (this.limiter.canExecute(provider)) { + for (CyclingList list : this.limiter.getEntries()) { + list.next(); + } + } + + return provider.peek(); + } + + @Override + public List get() { + return provider.get(); + } + + private static class Mutable extends LimitedCyclingList implements CyclingList.Mutable { + public Mutable(CyclingList.Mutable provider, Limiter> limiter) { + super(provider, limiter); + } + + @Override + public void add(T entry) { + ((CyclingList.Mutable) provider).add(entry); + } + + @Override + public void addAll(Collection entries) { + ((CyclingList.Mutable) provider).addAll(entries); + } + + @Override + public void clear() { + ((CyclingList.Mutable) provider).clear(); + } + } + } } diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/CompositeDisplayViewingScreen.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/CompositeDisplayViewingScreen.java index c0e9e2b01..f92275424 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/CompositeDisplayViewingScreen.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/CompositeDisplayViewingScreen.java @@ -148,6 +148,7 @@ public class CompositeDisplayViewingScreen extends AbstractDisplayViewingScreen transformFiltering(setupDisplay); transformIngredientNotice(setupDisplay, ingredientStackToNotice); transformResultNotice(setupDisplay, resultStackToNotice); + unifyIngredients(setupDisplay); for (EntryWidget widget : Widgets.walk(widgets, EntryWidget.class::isInstance)) { widget.removeTagMatch = true; } diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/DefaultDisplayViewingScreen.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/DefaultDisplayViewingScreen.java index c4535757a..b47c9d825 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/DefaultDisplayViewingScreen.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/DefaultDisplayViewingScreen.java @@ -255,6 +255,7 @@ public class DefaultDisplayViewingScreen extends AbstractDisplayViewingScreen { transformFiltering(setupDisplay); transformIngredientNotice(setupDisplay, ingredientStackToNotice); transformResultNotice(setupDisplay, resultStackToNotice); + unifyIngredients(setupDisplay); for (EntryWidget widget : Widgets.walk(widgets, EntryWidget.class::isInstance)) { widget.removeTagMatch = true; } @@ -446,6 +447,7 @@ public class DefaultDisplayViewingScreen extends AbstractDisplayViewingScreen { transformFiltering(setupDisplay); transformIngredientNotice(setupDisplay, ingredientStackToNotice); transformResultNotice(setupDisplay, resultStackToNotice); + unifyIngredients(setupDisplay); for (EntryWidget widget : Widgets.walk(widgets(), EntryWidget.class::isInstance)) { widget.removeTagMatch = true; } diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/widget/EntryWidget.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/widget/EntryWidget.java index f5369814c..be2de31eb 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/widget/EntryWidget.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/widget/EntryWidget.java @@ -44,6 +44,7 @@ import me.shedaniel.rei.api.client.gui.screen.DisplayScreen; import me.shedaniel.rei.api.client.gui.widgets.Slot; import me.shedaniel.rei.api.client.gui.widgets.Tooltip; import me.shedaniel.rei.api.client.gui.widgets.TooltipContext; +import me.shedaniel.rei.api.client.gui.widgets.Widgets; import me.shedaniel.rei.api.client.overlay.ScreenOverlay; import me.shedaniel.rei.api.client.registry.category.CategoryRegistry; import me.shedaniel.rei.api.client.registry.display.DisplayRegistry; @@ -63,6 +64,8 @@ import me.shedaniel.rei.impl.client.gui.dragging.CurrentDraggingStack; import me.shedaniel.rei.impl.client.gui.widget.favorites.FavoritesListWidget; import me.shedaniel.rei.impl.client.registry.display.DisplayRegistryImpl; import me.shedaniel.rei.impl.client.registry.display.DisplaysHolder; +import me.shedaniel.rei.impl.client.util.CyclingList; +import me.shedaniel.rei.impl.client.util.OriginalRetainingCyclingList; imp