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(-) (limited to 'runtime/src/main/java/me') 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 --- .../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 ++++------- 4 files changed, 33 insertions(+), 8 deletions(-) (limited to 'runtime/src/main/java/me') 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; -- cgit From ffe21652b40a93a00f33a27a5ecf41479b48bcd9 Mon Sep 17 00:00:00 2001 From: shedaniel Date: Sun, 18 Dec 2022 20:50:56 +0800 Subject: Close #1131 --- .../registry/display/DisplayRegistryImpl.java | 47 +++++++++++++++------- 1 file changed, 33 insertions(+), 14 deletions(-) (limited to 'runtime/src/main/java/me') 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 --- .../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 +++- 10 files changed, 1159 insertions(+), 49 deletions(-) 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 (limited to 'runtime/src/main/java/me') 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; import me.shedaniel.rei.impl.client.view.ViewsImpl; import net.minecraft.ChatFormatting; import net.minecraft.CrashReport; @@ -77,7 +80,6 @@ import net.minecraft.client.resources.sounds.SimpleSoundInstance; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.sounds.SoundEvents; -import net.minecraft.util.Mth; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; @@ -87,10 +89,8 @@ import java.util.function.UnaryOperator; import java.util.stream.Collectors; import java.util.stream.Stream; +@SuppressWarnings("UnstableApiUsage") public class EntryWidget extends Slot implements DraggableStackProviderWidget { - @ApiStatus.Internal - public static long stackDisplayOffset = 0; - @ApiStatus.Internal private byte noticeMark = 0; protected boolean highlight = true; @@ -99,8 +99,9 @@ public class EntryWidget extends Slot implements DraggableStackProviderWidget { protected boolean interactable = true; protected boolean interactableFavorites = true; protected boolean wasClicked = false; - private Rectangle bounds; - private List> entryStacks; + private final Rectangle bounds; + private final OriginalRetainingCyclingList> stacks = new OriginalRetainingCyclingList<>(EntryStack::empty); + private long lastCycleTime = -1; @Nullable private Set> tooltipProcessors; public ResourceLocation tagMatch; @@ -117,7 +118,6 @@ public class EntryWidget extends Slot implements DraggableStackProviderWidget { public EntryWidget(Rectangle bounds) { this.bounds = bounds; - this.entryStacks = Collections.emptyList(); } @Override @@ -244,60 +244,64 @@ public class EntryWidget extends Slot implements DraggableStackProviderWidget { return background; } - public EntryWidget clearStacks() { - entryStacks = Collections.emptyList(); + @Override + public Slot clearEntries() { + this.getCyclingEntries().clear(); + if (removeTagMatch) tagMatch = null; return this; } - @Override - public Slot clearEntries() { - return clearStacks(); + public EntryWidget clearStacks() { + return (EntryWidget) this.clearEntries(); } @Override public EntryWidget entry(EntryStack stack) { Objects.requireNonNull(stack); - if (entryStacks.isEmpty()) { - entryStacks = Collections.singletonList(stack); - } else { - if (!(entryStacks instanceof ArrayList)) { - entryStacks = new ArrayList<>(entryStacks); - } - entryStacks.add(stack); - } + this.getCyclingEntries().add(stack); if (removeTagMatch) tagMatch = null; return this; } @Override public EntryWidget entries(Collection> stacks) { - if (!stacks.isEmpty()) { - if (!(entryStacks instanceof ArrayList)) { - entryStacks = new ArrayList<>(entryStacks); - } - entryStacks.addAll(stacks); - if (removeTagMatch) tagMatch = null; - } + Objects.requireNonNull(stacks); + this.getCyclingEntries().addAll(stacks); + if (removeTagMatch) tagMatch = null; return this; } - @Override - public EntryStack getCurrentEntry() { - int size = entryStacks.size(); - if (size == 0) - return EntryStack.empty(); - if (size == 1) - return entryStacks.get(0); - return entryStacks.get(Mth.floor(((System.currentTimeMillis() + stackDisplayOffset) / getCyclingInterval() % (double) size))); + public Slot entries(CyclingList> stacks) { + this.getCyclingEntries().setBacking(stacks); + if (removeTagMatch) tagMatch = null; + return this; } - protected long getCyclingInterval() { - return 1000; + public CyclingList> getBackingCyclingEntries() { + return this.stacks.getBacking(); + } + + public OriginalRetainingCyclingList> getCyclingEntries() { + return this.stacks; + } + + @Override + public EntryStack getCurrentEntry() { + if (this.lastCycleTime == -1) this.lastCycleTime = System.currentTimeMillis(); + if (System.currentTimeMillis() > this.lastCycleTime + getCyclingInterval()) { + this.lastCycleTime = System.currentTimeMillis(); + this.getCyclingEntries().next(); + } + return this.getCyclingEntries().peek(); } @Override public List> getEntries() { - return entryStacks; + return getCyclingEntries().get(); + } + + protected long getCyclingInterval() { + return 1000; } @Override @@ -562,12 +566,18 @@ public class EntryWidget extends Slot implements DraggableStackProviderWidget { @Override public boolean mouseScrolled(double mouseX, double mouseY, double amount) { - if (REIRuntimeImpl.isWithinRecipeViewingScreen && entryStacks.size() > 1 && containsMouse(mouseX, mouseY)) { + if (REIRuntimeImpl.isWithinRecipeViewingScreen && this.getCyclingEntries().get().size() > 1 && containsMouse(mouseX, mouseY)) { if (amount < 0) { - EntryWidget.stackDisplayOffset = ((System.currentTimeMillis() + stackDisplayOffset) / 1000 - 1) * 1000; + for (EntryWidget slot : Widgets.walk(minecraft.screen.children(), EntryWidget.class::isInstance)) { + slot.getCyclingEntries().previous(); + slot.lastCycleTime = System.currentTimeMillis(); + } return true; } else if (amount > 0) { - EntryWidget.stackDisplayOffset = ((System.currentTimeMillis() + stackDisplayOffset) / 1000 + 1) * 1000; + for (EntryWidget slot : Widgets.walk(minecraft.screen.children(), EntryWidget.class::isInstance)) { + slot.getCyclingEntries().next(); + slot.lastCycleTime = System.currentTimeMillis(); + } return true; } } @@ -708,7 +718,7 @@ public class EntryWidget extends Slot implements DraggableStackProviderWidget { public DraggableStack getHoveredStack(DraggingContext context, double mouseX, double mouseY) { if (!getCurrentEntry().isEmpty() && containsMouse(mouseX, mouseY)) { return new DraggableStack() { - EntryStack stack = getCurrentEntry().copy() + final EntryStack stack = getCurrentEntry().copy() .removeSetting(EntryStack.Settings.RENDERER) .removeSetting(EntryStack.Settings.FLUID_RENDER_RATIO); @@ -750,7 +760,7 @@ public class EntryWidget extends Slot implements DraggableStackProviderWidget { category.setDetail("Highlight enabled", () -> String.valueOf(isHighlightEnabled())); category.setDetail("Tooltip enabled", () -> String.valueOf(isTooltipsEnabled())); category.setDetail("Background enabled", () -> String.valueOf(isBackgroundEnabled())); - category.setDetail("Entries count", () -> String.valueOf(entryStacks.size())); + category.setDetail("Entries count", () -> String.valueOf(getEntries().size())); EntryStack currentEntry = getCurrentEntry(); CrashReportCategory entryCategory = report.addCategory("Current Rendering Entry"); diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/util/AbstractIndexedCyclingList.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/util/AbstractIndexedCyclingList.java new file mode 100644 index 000000000..199afbb29 --- /dev/null +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/util/AbstractIndexedCyclingList.java @@ -0,0 +1,94 @@ +/* + * 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.impl.client.util; + +import com.google.common.annotations.GwtCompatible; + +@GwtCompatible +abstract class AbstractIndexedCyclingList implements CyclingList { + private int position = 0; + + protected abstract T get(int index); + + protected abstract T empty(); + + @Override + public T peek() { + int size = size(); + + if (size == 0) { + return empty(); + } else { + return get(Math.floorMod(position, size)); + } + } + + @Override + public T next() { + int size = size(); + + if (size == 0) { + return empty(); + } else { + int tmp = position; + position = Math.floorMod(++tmp, size); + return get(Math.floorMod(tmp, size)); + } + } + + @Override + public int currentIndex() { + return position; + } + + @Override + public int nextIndex() { + return Math.floorMod(position + 1, size()); + } + + @Override + public T previous() { + int size = size(); + + if (size == 0) { + return empty(); + } else { + position = Math.floorMod(--position, size); + return get(position); + } + } + + @Override + public int previousIndex() { + return Math.floorMod(position - 1, size()); + } + + @Override + public void resetToStart() { + position = 0; + } + + public static abstract class Mutable extends AbstractIndexedCyclingList implements CyclingList.Mutable { + } +} diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/util/ClientTickCounter.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/util/ClientTickCounter.java new file mode 100644 index 000000000..03e2d9de8 --- /dev/null +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/util/ClientTickCounter.java @@ -0,0 +1,40 @@ +/* + * 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.impl.client.util; + +import dev.architectury.event.events.client.ClientTickEvent; + +public class ClientTickCounter { + private static int ticks = 0; + + static { + ClientTickEvent.CLIENT_POST.register(tick -> { + ticks++; + }); + } + + public static int getTicks() { + return ticks; + } +} diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/util/ConcatenatedListIterator.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/util/ConcatenatedListIterator.java new file mode 100644 index 000000000..eedd2b0cf --- /dev/null +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/util/ConcatenatedListIterator.java @@ -0,0 +1,189 @@ +/* + * 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.impl.client.util; + +import me.shedaniel.rei.api.common.util.CollectionUtils; +import org.spongepowered.include.com.google.common.collect.Iterators; + +import java.util.List; + +public abstract class ConcatenatedListIterator implements CyclingList { + private static final int HEAD_FIRST = -1, HEAD_LAST = -2, TAIL_FIRST = -3, TAIL_LAST = -4; + private final List listView; + private final CyclingList head, tail; + private int position = HEAD_FIRST; + + public ConcatenatedListIterator(CyclingList head, CyclingList tail) { + this.listView = CollectionUtils.concatUnmodifiable(() -> Iterators.forArray(head.get(), tail.get())); + this.head = head; + this.tail = tail; + this.head.resetToStart(); + this.tail.resetToStart(); + } + + protected abstract T empty(); + + @Override + public T peek() { + int p = currentIndex(); + int neededHeadPos = Math.min(p, head.size() - 1); + int neededTailPos = Math.max(p - head.size(), 0); + while (head.currentIndex() != neededHeadPos) { + head.next(); + } + while (tail.currentIndex() != neededTailPos) { + tail.next(); + } + return p < head.size() ? head.peek() : tail.peek(); + } + + @Override + public T next() { + position = nextIndex(); + int neededHeadPos = Math.min(position, head.size() - 1); + int neededTailPos = Math.max(position - head.size(), 0); + while (head.currentIndex() != neededHeadPos) { + head.next(); + } + while (tail.currentIndex() != neededTailPos) { + tail.next(); + } + T t = position < head.size() ? head.peek() : tail.peek(); + position = normalizeIndex(position); + return t; + } + + @Override + public T previous() { + position = previousIndex(); + int neededHeadPos = Math.min(position, head.size() - 1); + int neededTailPos = Math.max(position - head.size(), 0); + while (head.currentIndex() != neededHeadPos) { + head.previous(); + } + while (tail.currentIndex() != neededTailPos) { + tail.previous(); + } + T t = position < head.size() ? head.peek() : tail.peek(); + position = normalizeIndex(position); + return t; + } + + private int normalizeIndex(int index) { + if (index == 0) return HEAD_FIRST; + int hSize = head.size(); + if (index == hSize - 1) return HEAD_LAST; + if (index == hSize) return TAIL_FIRST; + if (index == hSize + tail.size() - 1) return TAIL_LAST; + return index; + } + + @Override + public int currentIndex() { + int size = size(); + int tmp = switch (position) { + case HEAD_FIRST -> 0; + case HEAD_LAST -> head.size() - 1; + case TAIL_FIRST -> tail.size() > 0 ? head.size() : 0; + case TAIL_LAST -> size - 1; + default -> position; + }; + return Math.floorMod(tmp, size); + } + + @Override + public int nextIndex() { + int size = size(); + int tmp = switch (position) { + case HEAD_FIRST -> 1; + case HEAD_LAST -> { + if (tail.size() > 0) { + yield head.size(); + } else if (head.size() > 0) { + yield 0; + } else { + yield 1; + } + } + case TAIL_FIRST -> { + if (tail.size() > 0) { + yield head.size() + 1; + } else if (head.size() > 0) { + yield 0; + } else { + yield 1; + } + } + case TAIL_LAST -> size > 0 ? 0 : 1; + default -> position + 1; + }; + return Math.floorMod(tmp, size); + } + + @Override + public int previousIndex() { + int size = size(); + int tmp = switch (position) { + case HEAD_FIRST -> size - 1; + case HEAD_LAST -> { + if (head.size() > 0) { + yield head.size() - 2; + } else if (tail.size() > 0) { + yield tail.size() - 1; + } else { + yield -1; + } + } + case TAIL_FIRST -> head.size() - 1; + case TAIL_LAST -> { + if (size > 0) { + yield size - 2; + } else { + yield -1; + } + } + default -> position - 1; + }; + return Math.floorMod(tmp, size); + } + + @Override + public List get() { + if (tail.size() == 0) return head.get(); + if (head.size() == 0) return tail.get(); + return listView; + } + + @Override + public int size() { + return head.size() + tail.size(); + } + + @Override + public void resetToStart() { + head.resetToStart(); + tail.resetToStart(); + position = 0; + } +} \ No newline at end of file diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/util/CyclingList.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/util/CyclingList.java new file mode 100644 index 000000000..2fb060d7a --- /dev/null +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/util/CyclingList.java @@ -0,0 +1,353 @@ +/* + * 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.impl.client.util; + +import org.jetbrains.annotations.ApiStatus; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +@ApiStatus.Experimental +public interface CyclingList extends Supplier> { + default T peek() { + next(); + return previous(); + } + + default boolean hasNext() { + int nextIndex = nextIndex(); + return currentIndex() < nextIndex && nextIndex < size(); + } + + default boolean hasPrevious() { + int previousIndex = previousIndex(); + return previousIndex >= 0 && currentIndex() > previousIndex; + } + + T next(); + + T previous(); + + int nextIndex(); + + int previousIndex(); + + void resetToStart(); + + int size(); + + int currentIndex(); + + @ApiStatus.Experimental + interface Mutable extends CyclingList { + void add(T entry); + + void addAll(Collection entries); + + void clear(); + } + + static CyclingList of(List list, Supplier empty) { + return new AbstractIndexedCyclingList<>() { + @Override + public List get() { + return list; + } + + @Override + protected T get(int index) { + return list.get(index); + } + + @Override + protected T empty() { + return empty.get(); + } + + @Override + public int size() { + return list.size(); + } + }; + } + + static CyclingList.Mutable ofMutable(List list, Supplier empty) { + return new AbstractIndexedCyclingList.Mutable<>() { + @Override + public List get() { + return list; + } + + @Override + protected T get(int index) { + return list.get(index); + } + + @Override + protected T empty() { + return empty.get(); + } + + @Override + public int size() { + return list.size(); + } + + @Override + public void add(T entry) { + list.add(entry); + } + + @Override + public void addAll(Collection entries) { + list.addAll(entries); + } + + @Override + public void clear() { + list.clear(); + } + }; + } + + static CyclingList of(Supplier empty) { + return new CyclingList<>() { + @Override + public List get() { + return List.of(); + } + + @Override + public T peek() { + return empty.get(); + } + + @Override + public void resetToStart() { + } + + @Override + public int size() { + return 0; + } + + @Override + public int currentIndex() { + return 0; + } + + @Override + public T next() { + return empty.get(); + } + + @Override + public T previous() { + return empty.get(); + } + + @Override + public int nextIndex() { + return 1; + } + + @Override + public int previousIndex() { + return -1; + } + }; + } + + static CyclingList.Mutable ofMutable(Supplier empty) { + return new Mutable<>() { + private List list; + private CyclingList provider; + + + @Override + public List get() { + return this.list == null ? List.of() : this.list; + } + + @Override + public T peek() { + if (this.provider == null) return empty.get(); + return this.provider.peek(); + } + + @Override + public T previous() { + if (this.provider == null) return empty.get(); + return this.provider.previous(); + } + + @Override + public int nextIndex() { + return this.provider == null ? 1 : this.provider.nextIndex(); + } + + @Override + public int previousIndex() { + return this.provider == null ? -1 : this.provider.previousIndex(); + } + + @Override + public T next() { + if (this.provider == null) return empty.get(); + return this.provider.next(); + } + + @Override + public void add(T entry) { + if (this.list == null || this.list.isEmpty()) { + this.list = Collections.singletonList(entry); + this.provider = CyclingList.ofMutable(this.list, empty); + } else { + if (!(this.list instanceof ArrayList)) { + this.list = new ArrayList<>(this.list); + this.provider = CyclingList.ofMutable(this.list, empty); + } + this.list.add(entry); + } + } + + @Override + public void resetToStart() { + if (this.provider != null) { + this.provider.resetToStart(); + } + } + + @Override + public int size() { + return this.list == null ? 0 : th