aboutsummaryrefslogtreecommitdiff
path: root/runtime/src
diff options
context:
space:
mode:
authorshedaniel <daniel@shedaniel.me>2022-12-25 00:10:10 +0800
committershedaniel <daniel@shedaniel.me>2024-04-16 00:38:17 +0900
commitc93345b568ba0c6231986cac30485e689212c731 (patch)
treef99a81395cf624315f4de3e56296cfa811668902 /runtime/src
parentffe21652b40a93a00f33a27a5ecf41479b48bcd9 (diff)
downloadRoughlyEnoughItems-c93345b568ba0c6231986cac30485e689212c731.tar.gz
RoughlyEnoughItems-c93345b568ba0c6231986cac30485e689212c731.tar.bz2
RoughlyEnoughItems-c93345b568ba0c6231986cac30485e689212c731.zip
Implement Unified Ingredients
Diffstat (limited to 'runtime/src')
-rw-r--r--runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/AbstractDisplayViewingScreen.java190
-rw-r--r--runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/CompositeDisplayViewingScreen.java1
-rw-r--r--runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/DefaultDisplayViewingScreen.java2
-rw-r--r--runtime/src/main/java/me/shedaniel/rei/impl/client/gui/widget/EntryWidget.java96
-rw-r--r--runtime/src/main/java/me/shedaniel/rei/impl/client/util/AbstractIndexedCyclingList.java94
-rw-r--r--runtime/src/main/java/me/shedaniel/rei/impl/client/util/ClientTickCounter.java40
-rw-r--r--runtime/src/main/java/me/shedaniel/rei/impl/client/util/ConcatenatedListIterator.java189
-rw-r--r--runtime/src/main/java/me/shedaniel/rei/impl/client/util/CyclingList.java353
-rw-r--r--runtime/src/main/java/me/shedaniel/rei/impl/client/util/OriginalRetainingCyclingList.java176
-rw-r--r--runtime/src/main/java/me/shedaniel/rei/impl/common/entry/EntryIngredientImpl.java67
-rw-r--r--runtime/src/test/java/CyclingListTest.java338
11 files changed, 1497 insertions, 49 deletions
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<? extends GuiEventListener> setupDisplay, List<EntryStack<?>> noticeStacks) {
if (noticeStacks.isEmpty())
return;
+ Map<EntryStack<?>, LongSet> noticeSet = new HashMap<>();
for (EntryWidget widget : Widgets.<EntryWidget>walk(setupDisplay, EntryWidget.class::isInstance)) {
- if (widget.getNoticeMark() == marker && widget.getEntries().size() > 1) {
+ List<EntryStack<?>> 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.<EntryWidget>walk(setupDisplay, EntryWidget.class::isInstance)) {
+ List<EntryStack<?>> entries = widget.getEntries();
+ if (widget.getNoticeMark() != marker && entries.size() > 1 && entries instanceof EntryIngredient ingredient) {
+ long hashFocusIngredient = hashFocusIngredient(ingredient);
+ for (Map.Entry<EntryStack<?>, 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<? extends GuiEventListener> setupDisplay) {
for (EntryWidget widget : Widgets.<EntryWidget>walk(setupDisplay, EntryWidget.class::isInstance)) {
if (widget.getEntries().size() > 1) {
Collection<EntryStack<?>> 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<Widget> widgets) {
outer:
for (EntryWidget widget : Widgets.<EntryWidget>walk(widgets, EntryWidget.class::isInstance)) {
@@ -283,6 +320,32 @@ public abstract class AbstractDisplayViewingScreen extends Screen implements Dis
}
}
+ protected void unifyIngredients(List<Widget> widgets) {
+ Map<EntryIngredient, List<EntryWidget>> slots = new TreeMap<>(Comparator.comparingLong(AbstractDisplayViewingScreen::hashFocusIngredient));
+ for (EntryWidget slot : Widgets.<EntryWidget>walk(widgets, EntryWidget.class::isInstance)) {
+ CyclingList<EntryStack<?>> entries = slot.getBackingCyclingEntries();
+ if (entries.get() instanceof EntryIngredient ingredient) {
+ slots.computeIfAbsent(ingredient, key -> new ArrayList<>()).add(slot);
+ }
+ }
+ for (Map.Entry<EntryIngredient, List<EntryWidget>> entry : slots.entrySet()) {
+ List<EntryWidget> slotList = entry.getValue();
+ if (slotList.size() > 1) {
+ List<CyclingList<EntryStack<?>>> all = new ArrayList<>();
+ Limiter<CyclingList<EntryStack<?>>> limiter = new TickCountLimiter<>(all);
+ for (EntryWidget slot : slotList) {
+ CyclingList<EntryStack<?>> limited;
+ CyclingList<EntryStack<?>> backing = slot.getBackingCyclingEntries();
+ if (backing instanceof CyclingList.Mutable<EntryStack<?>> 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<T> {
+ boolean canExecute(T t);
+
+ List<T> getEntries();
+ }
+
+ private static class TickCountLimiter<T> implements Limiter<T> {
+ private int ticks = -1;
+ private final List<T> list;
+ private final Set<T> set = new ReferenceOpenHashSet<>();
+
+ public TickCountLimiter(List<T> 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<T> getEntries() {
+ return list;
+ }
+ }
+
+ private static class LimitedCyclingList<T> implements CyclingList<T> {
+ protected final CyclingList<T> provider;
+ private final Limiter<CyclingList<T>> limiter;
+
+ public LimitedCyclingList(CyclingList<T> provider, Limiter<CyclingList<T>> 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<T> 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<T> list : this.limiter.getEntries()) {
+ list.next();
+ }
+ }
+
+ return provider.peek();
+ }
+
+ @Override
+ public List<T> get() {
+ return provider.get();
+ }
+
+ private static class Mutable<T> extends LimitedCyclingList<T> implements CyclingList.Mutable<T> {
+ public Mutable(CyclingList.Mutable<T> provider, Limiter<CyclingList<T>> limiter) {
+ super(provider, limiter);
+ }
+
+ @Override
+ public void add(T entry) {
+ ((CyclingList.Mutable<T>) provider).add(entry);
+ }
+
+ @Override
+ public void addAll(Collection<? extends T> entries) {
+ ((CyclingList.Mutable<T>) provider).addAll(entries);
+ }
+
+ @Override
+ public void clear() {
+ ((CyclingList.Mutable<T>) 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.<EntryWidget>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.<EntryWidget>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.<EntryWidget>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,11 +89,9 @@ 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;
protected boolean tooltips = 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<EntryStack<?>> entryStacks;
+ private final Rectangle bounds;
+ private final OriginalRetainingCyclingList<EntryStack<?>> stacks = new OriginalRetainingCyclingList<>(EntryStack::empty);
+ private long lastCycleTime = -1;
@Nullable
private Set<UnaryOperator<Tooltip>> 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<? extends EntryStack<?>> 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<EntryStack<?>> stacks) {
+ this.getCyclingEntries().setBacking(stacks);
+ if (removeTagMatch) tagMatch = null;
+ return this;
}
- protected long getCyclingInterval() {
- return 1000;
+ public CyclingList<EntryStack<?>> getBackingCyclingEntries() {
+ return this.stacks.getBacking();
+ }
+
+ public OriginalRetainingCyclingList<EntryStack<?>> 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<EntryStack<?>> 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.<EntryWidget>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.<EntryWidget>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<Screen> 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<T> implements CyclingList<T> {
+ 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<T> extends AbstractIndexedCyclingList<T> implements CyclingList.Mutable<T> {
+ }
+}
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<T> implements CyclingList<T> {
+ private static final int HEAD_FIRST = -1, HEAD_LAST = -2, TAIL_FIRST = -3, TAIL_LAST = -4;
+ private final List<T> listView;
+ private final CyclingList<T> head, tail;
+ private int position = HEAD_FIRST;
+
+ public ConcatenatedListIterator(CyclingList<T> head, CyclingList<T> 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 =