diff --git a/api/src/main/java/me/shedaniel/rei/api/client/gui/widgets/Slot.java b/api/src/main/java/me/shedaniel/rei/api/client/gui/widgets/Slot.java index 02527dafd..c968e685f 100644 --- a/api/src/main/java/me/shedaniel/rei/api/client/gui/widgets/Slot.java +++ b/api/src/main/java/me/shedaniel/rei/api/client/gui/widgets/Slot.java @@ -25,10 +25,12 @@ import me.shedaniel.math.Rectangle; import me.shedaniel.rei.api.common.entry.EntryStack; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import java.util.Collection; import java.util.List; +import java.util.function.Consumer; public abstract class Slot extends WidgetWithBounds { public static final byte UN_MARKED = 0; @@ -133,6 +135,10 @@ public final Slot disableBackground() { public abstract Slot entries(Collection> stacks); + @ApiStatus.Experimental + @ApiStatus.Internal + public abstract Slot withEntriesListener(Consumer listener); + public abstract EntryStack getCurrentEntry(); public abstract List> getEntries(); diff --git a/api/src/main/java/me/shedaniel/rei/api/common/plugins/PluginView.java b/api/src/main/java/me/shedaniel/rei/api/common/plugins/PluginView.java index b1072cb21..0e9918c66 100644 --- a/api/src/main/java/me/shedaniel/rei/api/common/plugins/PluginView.java +++ b/api/src/main/java/me/shedaniel/rei/api/common/plugins/PluginView.java @@ -24,9 +24,9 @@ package me.shedaniel.rei.api.common.plugins; import me.shedaniel.rei.api.client.plugins.REIClientPlugin; -import me.shedaniel.rei.api.common.registry.ReloadStage; import me.shedaniel.rei.impl.ClientInternals; import me.shedaniel.rei.impl.Internals; +import me.shedaniel.rei.impl.common.plugins.PluginReloadContext; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import org.jetbrains.annotations.ApiStatus; @@ -62,18 +62,25 @@ public void registerPlugin(REIPluginProvider plugin) { } @Override - public void pre(ReloadStage stage) { - PluginView.this.pre(stage); + public void pre(PluginReloadContext context) throws InterruptedException { + PluginView.this.pre(context); } @Override - public void post(ReloadStage stage) { - PluginView.this.post(stage); + public void reload(PluginReloadContext context) throws InterruptedException { + PluginView.this.reload(context); + } + + @Override + public void post(PluginReloadContext context) throws InterruptedException { + PluginView.this.post(context); } }; } - void pre(ReloadStage stage); + void pre(PluginReloadContext context) throws InterruptedException; + + void reload(PluginReloadContext context) throws InterruptedException; - void post(ReloadStage stage); + void post(PluginReloadContext context) throws InterruptedException; } diff --git a/api/src/main/java/me/shedaniel/rei/impl/common/plugins/PluginReloadContext.java b/api/src/main/java/me/shedaniel/rei/impl/common/plugins/PluginReloadContext.java new file mode 100644 index 000000000..0481b38d0 --- /dev/null +++ b/api/src/main/java/me/shedaniel/rei/impl/common/plugins/PluginReloadContext.java @@ -0,0 +1,48 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 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.common.plugins; + +import me.shedaniel.rei.api.common.registry.ReloadStage; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public interface PluginReloadContext { + ReloadStage stage(); + + ReloadInterruptionContext interruptionContext(); + + static PluginReloadContext of(ReloadStage stage, ReloadInterruptionContext interruptionContext) { + return new PluginReloadContext() { + @Override + public ReloadStage stage() { + return stage; + } + + @Override + public ReloadInterruptionContext interruptionContext() { + return interruptionContext; + } + }; + } +} diff --git a/api/src/main/java/me/shedaniel/rei/impl/common/plugins/ReloadInterruptionContext.java b/api/src/main/java/me/shedaniel/rei/impl/common/plugins/ReloadInterruptionContext.java new file mode 100644 index 000000000..19201b20b --- /dev/null +++ b/api/src/main/java/me/shedaniel/rei/impl/common/plugins/ReloadInterruptionContext.java @@ -0,0 +1,63 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 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.common.plugins; + +import me.shedaniel.rei.impl.common.InternalLogger; +import org.jetbrains.annotations.ApiStatus; + +@FunctionalInterface +@ApiStatus.Internal +public interface ReloadInterruptionContext { + boolean isInterrupted(); + + default void checkInterrupted() throws InterruptedException { + if (isInterrupted()) { + InternalLogger.getInstance().debug("Plugin reload interrupted!"); + throw new InterruptedException(); + } + } + + default ReloadInterruptionContext withJob(Runnable ifInterrupted) { + return new ReloadInterruptionContext() { + @Override + public boolean isInterrupted() { + return ReloadInterruptionContext.this.isInterrupted(); + } + + @Override + public void checkInterrupted() throws InterruptedException { + try { + ReloadInterruptionContext.this.checkInterrupted(); + } catch (InterruptedException e) { + ifInterrupted.run(); + throw e; + } + } + }; + } + + static ReloadInterruptionContext ofNever() { + return () -> false; + } +} diff --git a/default-plugin/src/main/java/me/shedaniel/rei/plugin/client/categories/DefaultSmithingCategory.java b/default-plugin/src/main/java/me/shedaniel/rei/plugin/client/categories/DefaultSmithingCategory.java index 982221600..a7546390c 100644 --- a/default-plugin/src/main/java/me/shedaniel/rei/plugin/client/categories/DefaultSmithingCategory.java +++ b/default-plugin/src/main/java/me/shedaniel/rei/plugin/client/categories/DefaultSmithingCategory.java @@ -27,15 +27,22 @@ import me.shedaniel.math.Point; import me.shedaniel.math.Rectangle; import me.shedaniel.rei.api.client.gui.Renderer; +import me.shedaniel.rei.api.client.gui.widgets.Slot; import me.shedaniel.rei.api.client.gui.widgets.Widget; import me.shedaniel.rei.api.client.gui.widgets.Widgets; import me.shedaniel.rei.api.client.registry.display.DisplayCategory; import me.shedaniel.rei.api.common.category.CategoryIdentifier; +import me.shedaniel.rei.api.common.display.basic.BasicDisplay; +import me.shedaniel.rei.api.common.entry.EntryIngredient; +import me.shedaniel.rei.api.common.entry.EntryStack; import me.shedaniel.rei.api.common.util.EntryStacks; import me.shedaniel.rei.plugin.common.BuiltinPlugin; import me.shedaniel.rei.plugin.common.displays.DefaultSmithingDisplay; +import net.minecraft.core.RegistryAccess; import net.minecraft.network.chat.Component; import net.minecraft.world.level.block.Blocks; +import org.apache.commons.lang3.mutable.MutableBoolean; +import org.jetbrains.annotations.ApiStatus; import java.util.List; @@ -65,14 +72,23 @@ public List setupDisplay(DefaultSmithingDisplay display, Rectangle bound widgets.add(Widgets.createArrow(new Point(startPoint.x + 27 + offsetX, startPoint.y + 4))); widgets.add(Widgets.createResultSlotBackground(new Point(startPoint.x + 61 + offsetX, startPoint.y + 5))); if (!legacy) { - widgets.add(Widgets.createSlot(new Point(startPoint.x + 4 - 18 * 2 + offsetX, startPoint.y + 5)).entries(display.getInputEntries().get(0)).markInput()); - widgets.add(Widgets.createSlot(new Point(startPoint.x + 4 - 18 + offsetX, startPoint.y + 5)).entries(display.getInputEntries().get(1)).markInput()); - widgets.add(Widgets.createSlot(new Point(startPoint.x + 4 + offsetX, startPoint.y + 5)).entries(display.getInputEntries().get(2)).markInput()); + Slot templateSlot, baseSlot, additionSlot, resultSlot; + MutableBoolean dirty = new MutableBoolean(true); + widgets.add(templateSlot = Widgets.createSlot(new Point(startPoint.x + 4 - 18 * 2 + offsetX, startPoint.y + 5)).entries(display.getInputEntries().get(0)).withEntriesListener(slot -> dirty.setTrue()).markInput()); + widgets.add(baseSlot = Widgets.createSlot(new Point(startPoint.x + 4 - 18 + offsetX, startPoint.y + 5)).entries(display.getInputEntries().get(1)).withEntriesListener(slot -> dirty.setTrue()).markInput()); + widgets.add(additionSlot = Widgets.createSlot(new Point(startPoint.x + 4 + offsetX, startPoint.y + 5)).entries(display.getInputEntries().get(2)).withEntriesListener(slot -> dirty.setTrue()).markInput()); + widgets.add(resultSlot = Widgets.createSlot(new Point(startPoint.x + 61 + offsetX, startPoint.y + 5)).entries(display.getOutputEntries().get(0)).disableBackground().markOutput()); + widgets.add(Widgets.createDrawableWidget((graphics, mouseX, mouseY, delta) -> { + if (dirty.booleanValue()) { + resultSlot.clearEntries().entries(getOutput(display, BasicDisplay.registryAccess(), templateSlot.getCurrentEntry(), baseSlot.getCurrentEntry(), additionSlot.getCurrentEntry())); + dirty.setFalse(); + } + })); } else { widgets.add(Widgets.createSlot(new Point(startPoint.x + 4 - 22 + offsetX, startPoint.y + 5)).entries(display.getInputEntries().get(0)).markInput()); widgets.add(Widgets.createSlot(new Point(startPoint.x + 4 + offsetX, startPoint.y + 5)).entries(display.getInputEntries().get(1)).markInput()); + widgets.add(Widgets.createSlot(new Point(startPoint.x + 61 + offsetX, startPoint.y + 5)).entries(display.getOutputEntries().get(0)).disableBackground().markOutput()); } - widgets.add(Widgets.createSlot(new Point(startPoint.x + 61 + offsetX, startPoint.y + 5)).entries(display.getOutputEntries().get(0)).disableBackground().markOutput()); return widgets; } @@ -80,4 +96,14 @@ public List setupDisplay(DefaultSmithingDisplay display, Rectangle bound public int getDisplayHeight() { return 36; } + + @ApiStatus.Experimental + private static EntryIngredient getOutput(DefaultSmithingDisplay display, RegistryAccess registryAccess, EntryStack template, EntryStack base, EntryStack addition) { + if (display.getType() == DefaultSmithingDisplay.SmithingRecipeType.TRIM) { + EntryIngredient output = DefaultSmithingDisplay.getTrimmingOutput(registryAccess, template, base, addition); + if (!output.isEmpty()) return output; + } + + return display.getOutputEntries().get(0); + } } diff --git a/default-plugin/src/main/java/me/shedaniel/rei/plugin/common/displays/DefaultSmithingDisplay.java b/default-plugin/src/main/java/me/shedaniel/rei/plugin/common/displays/DefaultSmithingDisplay.java index 0c45a469b..7b610b9b7 100644 --- a/default-plugin/src/main/java/me/shedaniel/rei/plugin/common/displays/DefaultSmithingDisplay.java +++ b/default-plugin/src/main/java/me/shedaniel/rei/plugin/common/displays/DefaultSmithingDisplay.java @@ -26,6 +26,8 @@ import me.shedaniel.rei.api.common.category.CategoryIdentifier; import me.shedaniel.rei.api.common.display.basic.BasicDisplay; import me.shedaniel.rei.api.common.entry.EntryIngredient; +import me.shedaniel.rei.api.common.entry.EntryStack; +import me.shedaniel.rei.api.common.entry.type.VanillaEntryTypes; import me.shedaniel.rei.api.common.util.EntryIngredients; import me.shedaniel.rei.api.common.util.EntryStacks; import me.shedaniel.rei.plugin.common.BuiltinPlugin; @@ -38,12 +40,16 @@ import net.minecraft.world.item.crafting.SmithingTransformRecipe; import net.minecraft.world.item.crafting.SmithingTrimRecipe; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Optional; public class DefaultSmithingDisplay extends BasicDisplay { + @Nullable + private final SmithingRecipeType type; + @ApiStatus.Experimental public DefaultSmithingDisplay(SmithingTransformRecipe recipe) { this( @@ -82,25 +88,15 @@ public static List from(SmithingTrimRecipe recipe) { .orElse(null); if (trimMaterial == null) continue; - ArmorTrim armorTrim = new ArmorTrim(trimMaterial, trimPattern); - EntryIngredient.Builder baseItems = EntryIngredient.builder(), outputItems = EntryIngredient.builder(); - for (ItemStack item : recipe.base.getItems()) { - Optional trim = ArmorTrim.getTrim(registryAccess, item); - if (trim.isEmpty() || !trim.get().hasPatternAndMaterial(trimPattern, trimMaterial)) { - ItemStack newItem = item.copy(); - newItem.setCount(1); - if (ArmorTrim.setTrim(registryAccess, newItem, armorTrim)) { - baseItems.add(EntryStacks.of(item.copy())); - outputItems.add(EntryStacks.of(newItem)); - } - } - } + EntryIngredient baseIngredient = EntryIngredients.ofIngredient(recipe.base); + EntryIngredient templateOutput = baseIngredient.isEmpty() ? EntryIngredient.empty() + : getTrimmingOutput(registryAccess, EntryStacks.of(templateItem), baseIngredient.get(0), EntryStacks.of(additionStack)); + displays.add(new DefaultSmithingDisplay(List.of( EntryIngredients.of(templateItem), - baseItems.build(), + baseIngredient, EntryIngredients.of(additionStack) - ), List.of(outputItems.build()), - Optional.ofNullable(recipe.getId()))); + ), List.of(templateOutput), SmithingRecipeType.TRIM, Optional.ofNullable(recipe.getId()))); } } return displays; @@ -115,7 +111,13 @@ public DefaultSmithingDisplay(SmithingRecipe recipe, List input } public DefaultSmithingDisplay(List inputs, List outputs, Optional location) { + this(inputs, outputs, null, location); + } + + @ApiStatus.Experimental + public DefaultSmithingDisplay(List inputs, List outputs, @Nullable SmithingRecipeType type, Optional location) { super(inputs, outputs, location); + this.type = type; } @Override @@ -123,7 +125,47 @@ public CategoryIdentifier getCategoryIdentifier() { return BuiltinPlugin.SMITHING; } + @ApiStatus.Experimental + @Nullable + public SmithingRecipeType getType() { + return type; + } + public static BasicDisplay.Serializer serializer() { - return BasicDisplay.Serializer.ofSimple(DefaultSmithingDisplay::new); + return BasicDisplay.Serializer.of((input, output, id, tag) -> { + SmithingRecipeType type = tag.contains("Type") ? SmithingRecipeType.valueOf(tag.getString("Type")) : null; + return new DefaultSmithingDisplay(input, output, type, id); + }, (display, tag) -> { + if (display.type != null) tag.putString("Type", display.type.name()); + }); + } + + @ApiStatus.Experimental + public enum SmithingRecipeType { + TRIM, + TRANSFORM + } + + @ApiStatus.Experimental + @ApiStatus.Internal + public static EntryIngredient getTrimmingOutput(RegistryAccess registryAccess, EntryStack template, EntryStack base, EntryStack addition) { + if (template.getType() != VanillaEntryTypes.ITEM || base.getType() != VanillaEntryTypes.ITEM || addition.getType() != VanillaEntryTypes.ITEM) return EntryIngredient.empty(); + ItemStack templateItem = template.castValue(); + ItemStack baseItem = base.castValue(); + ItemStack additionItem = addition.castValue(); + Holder.Reference trimPattern = TrimPatterns.getFromTemplate(registryAccess, templateItem) + .orElse(null); + if (trimPattern == null) return EntryIngredient.empty(); + Holder.Reference trimMaterial = TrimMaterials.getFromIngredient(registryAccess, additionItem) + .orElse(null); + if (trimMaterial == null) return EntryIngredient.empty(); + ArmorTrim armorTrim = new ArmorTrim(trimMaterial, trimPattern); + Optional trim = ArmorTrim.getTrim(registryAccess, baseItem); + if (trim.isPresent() && trim.get().hasPatternAndMaterial(trimPattern, trimMaterial)) return EntryIngredient.empty(); + ItemStack newItem = baseItem.copy(); + newItem.setCount(1); + if (ArmorTrim.setTrim(registryAccess, newItem, armorTrim)) { + return EntryIngredients.of(newItem); + } else return EntryIngredient.empty(); } } diff --git a/fabric/src/main/resources/roughlyenoughitems.accessWidener b/fabric/src/main/resources/roughlyenoughitems.accessWidener index 84831b501..fda98792a 100644 --- a/fabric/src/main/resources/roughlyenoughitems.accessWidener +++ b/fabric/src/main/resources/roughlyenoughitems.accessWidener @@ -41,4 +41,6 @@ accessible field net/minecraft/world/item/crafting/SmithingTrimRecipe template L accessible field net/minecraft/world/item/crafting/SmithingTrimRecipe base Lnet/minecraft/world/item/crafting/Ingredient; accessible field net/minecraft/world/item/crafting/SmithingTrimRecipe addition Lnet/minecraft/world/item/crafting/Ingredient; accessible field net/minecraft/world/item/CreativeModeTab displayItemsGenerator Lnet/minecraft/world/item/CreativeModeTab$DisplayItemsGenerator; -accessible class net/minecraft/world/item/CreativeModeTab$ItemDisplayBuilder \ No newline at end of file +accessible class net/minecraft/world/item/CreativeModeTab$ItemDisplayBuilder +accessible field net/minecraft/client/multiplayer/ClientLevel connection Lnet/minecraft/client/multiplayer/ClientPacketListener; +accessible field net/minecraft/client/multiplayer/MultiPlayerGameMode connection Lnet/minecraft/client/multiplayer/ClientPacketListener; \ No newline at end of file diff --git a/forge/src/main/resources/META-INF/accesstransformer.cfg b/forge/src/main/resources/META-INF/accesstransformer.cfg index 082c7dea2..6eb5af3b2 100644 --- a/forge/src/main/resources/META-INF/accesstransformer.cfg +++ b/forge/src/main/resources/META-INF/accesstransformer.cfg @@ -48,3 +48,5 @@ public-f net.minecraft.client.gui.font.CodepointMap f_283938_ # blockMap public-f net.minecraft.client.gui.font.CodepointMap f_283773_ # blockConstructor public net.minecraft.world.item.CreativeModeTab f_256824_ # displayItemsGenerator public net.minecraft.world.item.CreativeModeTab$ItemDisplayBuilder +public net.minecraft.client.multiplayer.ClientLevel f_104561_ # connection +public net.minecraft.client.multiplayer.MultiPlayerGameMode f_105190_ # connection \ No newline at end of file diff --git a/runtime/src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java b/runtime/src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java index 732ca00cd..3f99fa7cb 100644 --- a/runtime/src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java +++ b/runtime/src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java @@ -29,13 +29,10 @@ import dev.architectury.registry.ReloadListenerRegistry; import dev.architectury.utils.Env; import dev.architectury.utils.EnvExecutor; -import me.shedaniel.cloth.clothconfig.shadowed.blue.endless.jankson.annotation.Nullable; import me.shedaniel.rei.api.common.entry.type.EntryType; -import me.shedaniel.rei.api.common.plugins.PluginManager; import me.shedaniel.rei.api.common.plugins.PluginView; import me.shedaniel.rei.api.common.plugins.REIPlugin; import me.shedaniel.rei.api.common.plugins.REIServerPlugin; -import me.shedaniel.rei.api.common.registry.ReloadStage; import me.shedaniel.rei.impl.Internals; import me.shedaniel.rei.impl.common.InternalLogger; import me.shedaniel.rei.impl.common.category.CategoryIdentifierImpl; @@ -53,6 +50,8 @@ import me.shedaniel.rei.impl.common.logging.performance.PerformanceLogger; import me.shedaniel.rei.impl.common.logging.performance.PerformanceLoggerImpl; import me.shedaniel.rei.impl.common.plugins.PluginManagerImpl; +import me.shedaniel.rei.impl.common.plugins.ReloadInterruptionContext; +import me.shedaniel.rei.impl.common.plugins.ReloadManagerImpl; import me.shedaniel.rei.impl.common.registry.RecipeManagerContextImpl; import me.shedaniel.rei.impl.common.transfer.MenuInfoRegistryImpl; import me.shedaniel.rei.impl.common.transfer.SlotAccessorRegistryImpl; @@ -135,7 +134,7 @@ public static void attachCommonInternals() { UnaryOperator.identity(), new EntryTypeRegistryImpl(), new EntrySettingsAdapterRegistryImpl(), - new RecipeManagerContextImpl<>(RecipeManagerContextImpl.supplier()), + new RecipeManagerContextImpl<>(), new ItemComparatorRegistryImpl(), new FluidComparatorRegistryImpl(), new DisplaySerializerRegistryImpl(), @@ -147,28 +146,6 @@ public static void attachCommonInternals() { new SlotAccessorRegistryImpl()), "serverPluginManager"); } - public static void _reloadPlugins(@Nullable ReloadStage stage) { - if (stage == null) { - for (ReloadStage reloadStage : ReloadStage.values()) { - _reloadPlugins(reloadStage); - } - return; - } - try { - for (PluginManager> instance : PluginManager.getActiveInstances()) { - instance.view().pre(stage); - } - for (PluginManager> instance : PluginManager.getActiveInstances()) { - instance.startReload(stage); - } - for (PluginManager> instance : PluginManager.getActiveInstances()) { - instance.view().post(stage); - } - } catch (Throwable throwable) { - throwable.printStackTrace(); - } - } - public void onInitialize() { PluginDetector detector = getPluginDetector(); detector.detectCommonPlugins(); @@ -179,8 +156,7 @@ public void onInitialize() { MutableLong lastReload = new MutableLong(-1); ReloadListenerRegistry.register(PackType.SERVER_DATA, (preparationBarrier, resourceManager, profilerFiller, profilerFiller2, executor, executor2) -> { return preparationBarrier.wait(Unit.INSTANCE).thenRunAsync(() -> { - PERFORMANCE_LOGGER.clear(); - RoughlyEnoughItemsCore._reloadPlugins(null); + ReloadManagerImpl.reloadPlugins(null, ReloadInterruptionContext.ofNever()); }, executor2); }); } diff --git a/runtime/src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCoreClient.java b/runtime/src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCoreClient.java index de49c9a29..576193baf 100644 --- a/runtime/src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCoreClient.java +++ b/runtime/src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCoreClient.java @@ -24,13 +24,12 @@ package me.shedaniel.rei; import com.google.common.collect.Lists; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.serialization.DataResult; import dev.architectury.event.Event; import dev.architectury.event.EventFactory; import dev.architectury.event.EventResult; import dev.architectury.event.events.client.ClientGuiEvent; +import dev.architectury.event.events.client.ClientPlayerEvent; import dev.architectury.event.events.client.ClientRecipeUpdateEvent; import dev.architectury.event.events.client.ClientScreenInputEvent; import dev.architectury.networking.NetworkManager; @@ -67,7 +66,6 @@ import me.shedaniel.rei.impl.client.entry.renderer.EntryRendererRegistryImpl; import me.shedaniel.rei.impl.client.favorites.DelegatingFavoriteEntryProviderImpl; import me.shedaniel.rei.impl.client.favorites.FavoriteEntryTypeRegistryImpl; -import me.shedaniel.rei.impl.client.gui.ScreenOverlayImpl; import me.shedaniel.rei.impl.client.gui.modules.entries.SubMenuEntry; import me.shedaniel.rei.impl.client.gui.modules.entries.ToggleMenuEntry; import me.shedaniel.rei.impl.client.gui.widget.InternalWidgets; @@ -89,6 +87,8 @@ import me.shedaniel.rei.impl.common.entry.type.collapsed.CollapsibleEntryRegistryImpl; import me.shedaniel.rei.impl.common.entry.type.types.EmptyEntryDefinition; import me.shedaniel.rei.impl.common.plugins.PluginManagerImpl; +import me.shedaniel.rei.impl.common.plugins.ReloadManagerImpl; +import me.shedaniel.rei.impl.common.util.InstanceHelper; import me.shedaniel.rei.impl.common.util.IssuesDetector; import me.shedaniel.rei.plugin.test.REITestPlugin; import net.fabricmc.api.EnvType; @@ -121,7 +121,6 @@ import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.List; -import java.util.concurrent.*; import java.util.function.BiFunction; import java.util.function.BooleanSupplier; import java.util.function.Function; @@ -133,15 +132,6 @@ public class RoughlyEnoughItemsCoreClient { public static final Event PRE_UPDATE_RECIPES = EventFactory.createLoop(); public static final Event POST_UPDATE_TAGS = EventFactory.createLoop(); public static boolean isLeftMousePressed = false; - private static final ExecutorService RELOAD_PLUGINS = Executors.newSingleThreadScheduledExecutor(task -> { - Thread thread = new Thread(task, "REI-ReloadPlugins"); - thread.setDaemon(true); - thread.setUncaughtExceptionHandler(($, exception) -> { - InternalLogger.getInstance().throwException(exception); - }); - return thread; - }); - private static final List> RELOAD_TASKS = new CopyOnWriteArrayList<>(); public static void attachClientInternals() { InternalWidgets.attach(); @@ -317,24 +307,25 @@ private static boolean _shouldReturn(Screen screen) { private void registerEvents() { Minecraft client = Minecraft.getInstance(); final ResourceLocation recipeButtonTex = new ResourceLocation("textures/gui/recipe_button.png"); - MutableLong startReload = new MutableLong(-1); MutableLong endReload = new MutableLong(-1); PRE_UPDATE_RECIPES.register(recipeManager -> { - RoughlyEnoughItemsCore.PERFORMANCE_LOGGER.clear(); - reloadPlugins(startReload, ReloadStage.START); + reloadPlugins(null, ReloadStage.START); }); ClientRecipeUpdateEvent.EVENT.register(recipeManager -> { reloadPlugins(endReload, ReloadStage.END); }); + ClientPlayerEvent.CLIENT_PLAYER_QUIT.register(player -> { + InternalLogger.getInstance().debug("Player quit, clearing reload tasks!"); + endReload.setValue(-1); + ReloadManagerImpl.terminateReloadTasks(); + }); ClientGuiEvent.INIT_PRE.register((screen, access) -> { List stages = ((PluginManagerImpl>) PluginManager.getInstance()).getObservedStages(); if (Minecraft.getInstance().level != null && Minecraft.getInstance().player != null && stages.contains(ReloadStage.START) && !stages.contains(ReloadStage.END) && !PluginManager.areAnyReloading() && screen instanceof AbstractContainerScreen) { - for (Future task : RELOAD_TASKS) { - if (!task.isDone()) { - return EventResult.pass(); - } + if (ReloadManagerImpl.countRunningReloadTasks() > 0) { + return EventResult.pass(); } InternalLogger.getInstance().error("Detected missing stage: END! This is possibly due to issues during client recipe reload! REI will force a reload of the recipes now!"); @@ -473,27 +464,12 @@ public static boolean resetFocused(Screen screen) { public static void reloadPlugins(MutableLong lastReload, @Nullable ReloadStage start) { if (Minecraft.getInstance().level == null) return; if (lastReload != null) { - if (lastReload.getValue() > 0 && System.currentTimeMillis() - lastReload.getValue() <= 5000) { + if (lastReload.getValue() > 0 && System.currentTimeMillis() - lastReload.getValue() <= 1000) { InternalLogger.getInstance().warn("Suppressing Reload Plugins of stage " + start); return; } lastReload.setValue(System.currentTimeMillis()); } - InternalLogger.getInstance().debug("Starting Reload Plugins of stage " + start, new Throwable()); - if (ConfigObject.getInstance().doesRegisterRecipesInAnotherThread()) { - Future[] futures = new Future[1]; - CompletableFuture future = CompletableFuture.runAsync(() -> RoughlyEnoughItemsCore._reloadPlugins(start), RELOAD_PLUGINS) - .whenComplete((unused, throwable) -> { - // Remove the future from the list of futures - if (futures[0] != null) { - RELOAD_TASKS.remove(futures[0]); - futures[0] = null; - } - }); - futures[0] = future; - RELOAD_TASKS.add(future); - } else { - RoughlyEnoughItemsCore._reloadPlugins(start); - } + ReloadManagerImpl.reloadPlugins(start, () -> InstanceHelper.connectionFromClient() == null); } } diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigGroups.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigGroups.java index 28b83e639..b206fd6e5 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigGroups.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigGroups.java @@ -116,7 +116,8 @@ static OptionGroup make(String id) { OptionGroup DEBUG_PERFORMANCE = make("debug.performance") .add(PLUGINS_PERFORMANCE) .add(SEARCH_PERFORMANCE) - .add(ENTRY_LIST_PERFORMANCE); + .add(ENTRY_LIST_PERFORMANCE) + .add(DISPLAY_REGISTRY_ANALYSIS); OptionGroup RESET_RELOAD = make("reset.reload") .add(RELOAD_PLUGINS) .add(RELOAD_SEARCH); diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigOptions.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigOptions.java index 30340bfba..97119df2e 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigOptions.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigOptions.java @@ -24,7 +24,6 @@ package me.shedaniel.rei.impl.client.gui.config.options; import me.shedaniel.clothconfig2.api.ModifierKeyCode; -import me.shedaniel.rei.RoughlyEnoughItemsCore; import me.shedaniel.rei.RoughlyEnoughItemsCoreClient; import me.shedaniel.rei.api.client.config.entry.EntryStackProvider; import me.shedaniel.rei.api.client.gui.config.*; @@ -37,6 +36,7 @@ import me.shedaniel.rei.impl.client.config.entries.FilteringEntry; import me.shedaniel.rei.impl.client.gui.config.REIConfigScreen; import me.shedaniel.rei.impl.client.gui.config.options.configure.PanelBoundariesConfiguration; +import me.shedaniel.rei.impl.client.gui.performance.DisplayRegistryInfoScreen; import me.shedaniel.rei.impl.client.gui.performance.PerformanceScreen; import me.shedaniel.rei.impl.client.gui.screen.ConfigReloadingScreen; import me.shedaniel.rei.impl.client.gui.screen.collapsible.CollapsibleEntriesScreen; @@ -246,9 +246,11 @@ static CompositeOption make(String id, Function bind .enabledDisabled(); CompositeOption ENTRY_LIST_PERFORMANCE = make("debug.entry_list_performance", i -> i.advanced.layout.debugRenderTimeRequired, (i, v) -> i.advanced.layout.debugRenderTimeRequired = v) .enabledDisabled(); + CompositeOption DISPLAY_REGISTRY_ANALYSIS = make("debug.display_registry_analysis", i -> null, (i, v) -> new Object()) + .details((access, option, onClose) -> Minecraft.getInstance().setScreen(new DisplayRegistryInfoScreen(onClose))) + .requiresLevel(); CompositeOption RELOAD_PLUGINS = make("reset.reload_plugins", i -> null, (i, v) -> new Object()) .reload((access, option, onClose) -> { - RoughlyEnoughItemsCore.PERFORMANCE_LOGGER.clear(); RoughlyEnoughItemsCoreClient.reloadPlugins(null, null); while (!PluginManager.areAnyReloading()) { try { diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/performance/DisplayRegistryInfoScreen.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/performance/DisplayRegistryInfoScreen.java new file mode 100644 index 000000000..f0cb9de14 --- /dev/null +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/performance/DisplayRegistryInfoScreen.java @@ -0,0 +1,176 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 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.gui.performance; + +import com.mojang.blaze3d.systems.RenderSystem; +import me.shedaniel.clothconfig2.gui.widget.DynamicElementListWidget; +import me.shedaniel.math.Rectangle; +import me.shedaniel.rei.api.client.registry.display.DisplayRegistry; +import me.shedaniel.rei.api.common.category.CategoryIdentifier; +import me.shedaniel.rei.api.common.display.Display; +import me.shedaniel.rei.api.common.util.CollectionUtils; +import me.shedaniel.rei.impl.client.gui.modules.Menu; +import me.shedaniel.rei.impl.client.gui.modules.entries.ToggleMenuEntry; +import me.shedaniel.rei.impl.client.gui.screen.ScreenWithMenu; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FormattedCharSequence; + +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Stream; + +@Environment(EnvType.CLIENT) +public class DisplayRegistryInfoScreen extends ScreenWithMenu { + private Runnable onClose; + + public DisplayRegistryInfoScreen(Runnable onClose) { + super(Component.translatable("text.rei.display_registry_analysis")); + this.onClose = onClose; + } + + private ListWidget list; + private SortType sortType = SortType.ID; + + @Override + public void init() { + { + Component backText = Component.literal("↩ ").append(Component.translatable("gui.back")); + addRenderableWidget(new Button(4, 4, Minecraft.getInstance().font.width(backText) + 10, 20, backText, button -> { + this.onClose.run(); + this.onClose = null; + }, Supplier::get) { + }); + } + { + Component text = Component.translatable("text.rei.sort"); + Rectangle bounds = new Rectangle(this.width - 4 - Minecraft.getInstance().font.width(text) - 10, 4, Minecraft.getInstance().font.width(text) + 10, 20); + addRenderableWidget(new Button(bounds.x, bounds.y, bounds.width, bounds.height, text, button -> { + this.setMenu(new Menu(bounds, CollectionUtils.map(SortType.values(), type -> { + return ToggleMenuEntry.of(Component.translatable("text.rei.sort.by", type.name().toLowerCase(Locale.ROOT)), () -> false, o -> { + this.closeMenu(); + this.sortType = type; + this.init(this.minecraft, this.width, this.height); + }); + }), false)); + }, Supplier::get) { + }); + } + list = new ListWidget(); + list.addItem(new EntryImpl(Component.literal("Total Displays"), DisplayRegistry.getInstance().displaySize())); + sort(DisplayRegistry.getInstance().getAll().entrySet().stream()) + .forEach(entry -> { + list.addItem(new EntryImpl(entry.getKey(), entry.getValue().size())); + }); + addWidget(list); + } + + private Stream, List>> sort(Stream, List>> stream) { + return switch (sortType) { + case COUNT -> stream.sorted(Comparator., List>>comparingInt(value -> value.getValue().size()).reversed()); + case ID -> stream.sorted(Comparator.comparing(value -> value.getKey().toString())); + }; + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + renderDirtBackground(graphics); + list.render(graphics, mouseX, mouseY, delta); + graphics.drawString(this.font, this.title.getVisualOrderText(), (int) (this.width / 2.0F - this.font.width(this.title) / 2.0F), 12, -1); + super.render(graphics, mouseX, mouseY, delta); + } + + public static abstract class ListEntry extends DynamicElementListWidget.ElementEntry { + } + + private class ListWidget extends DynamicElementListWidget { + public ListWidget() { + super(DisplayRegistryInfoScreen.this.minecraft, DisplayRegistryInfoScreen.this.width, DisplayRegistryInfoScreen.this.height, 30, DisplayRegistryInfoScreen.this.height, Screen.BACKGROUND_LOCATION); + } + + @Override + public int getItemWidth() { + return width; + } + + @Override + public int addItem(ListEntry item) { + return super.addItem(item); + } + + @Override + protected int getScrollbarPosition() { + return width - 6; + } + } + + public static class EntryImpl extends ListEntry { + private final Component component; + public final int count; + + public EntryImpl(CategoryIdentifier identifier, int count) { + this(Component.literal(identifier.getIdentifier().toString()), count); + } + + public EntryImpl(Component component, int count) { + this.component = component; + this.count = count; + } + + @Override + public void render(GuiGraphics graphics, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isHovered, float delta) { + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + graphics.drawString(Minecraft.getInstance().font, this.component.getVisualOrderText(), x + 4, y + 6, -1); + FormattedCharSequence rightText = Component.translatable("text.rei.display_registry_analysis.displays", count).getVisualOrderText(); + graphics.drawString(Minecraft.getInstance().font, rightText, x + entryWidth - 6 - 8 - Minecraft.getInstance().font.width(rightText), y + 6, -1); + } + + @Override + public int getItemHeight() { + return 24; + } + + @Override + public List children() { + return Collections.emptyList(); + } + + @Override + public List narratables() { + return Collections.emptyList(); + } + } + + private enum SortType { + COUNT, + ID + } +} diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/performance/PerformanceScreen.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/performance/PerformanceScreen.java index 21c8c20c9..f96f7bc5f 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/performance/PerformanceScreen.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/performance/PerformanceScreen.java @@ -25,11 +25,16 @@ import com.mojang.datafixers.util.Pair; import me.shedaniel.clothconfig2.gui.widget.DynamicElementListWidget; +import me.shedaniel.math.Rectangle; import me.shedaniel.rei.RoughlyEnoughItemsCore; import me.shedaniel.rei.api.common.plugins.REIPlugin; import me.shedaniel.rei.api.common.plugins.REIPluginProvider; +import me.shedaniel.rei.api.common.util.CollectionUtils; +import me.shedaniel.rei.impl.client.gui.modules.Menu; +import me.shedaniel.rei.impl.client.gui.modules.entries.ToggleMenuEntry; import me.shedaniel.rei.impl.client.gui.performance.entry.PerformanceEntryImpl; import me.shedaniel.rei.impl.client.gui.performance.entry.SubCategoryListEntry; +import me.shedaniel.rei.impl.client.gui.screen.ScreenWithMenu; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.Minecraft; @@ -48,7 +53,7 @@ import static java.util.concurrent.TimeUnit.*; @Environment(EnvType.CLIENT) -public class PerformanceScreen extends Screen { +public class PerformanceScreen extends ScreenWithMenu { private Runnable onClose; public PerformanceScreen(Runnable onClose) { @@ -57,6 +62,7 @@ public PerformanceScreen(Runnable onClose) { } private PerformanceEntryListWidget list; + private SortType sortType = SortType.ORDER; /* * Copyright (C) 2008 The Guava Authors @@ -142,8 +148,22 @@ public void init() { this.onClose = null; }, Supplier::get) {}); } + { + Component text = Component.translatable("text.rei.sort"); + Rectangle bounds = new Rectangle(this.width - 4 - Minecraft.getInstance().font.width(text) - 10, 4, Minecraft.getInstance().font.width(text) + 10, 20); + addRenderableWidget(new Button(bounds.x, bounds.y, bounds.width, bounds.height, text, button -> { + this.setMenu(new Menu(bounds, CollectionUtils.map(SortType.values(), type -> { + return ToggleMenuEntry.of(Component.translatable("text.rei.sort.by", type.name().toLowerCase(Locale.ROOT)), () -> false, o -> { + this.closeMenu(); + this.sortType = type; + this.init(this.minecraft, this.width, this.height); + }); + }), false)); + }, Supplier::get) {}); + } list = new PerformanceEntryListWidget(); long[] totalTime = {0}; + List subCategories = new ArrayList<>(); RoughlyEnoughItemsCore.PERFORMANCE_LOGGER.getStages().forEach((stage, inner) -> { List entries = new ArrayList<>(); inner.times().forEach((obj, time) -> { @@ -158,9 +178,15 @@ public void init() { entries.add(new PerformanceEntryImpl(Component.literal("Miscellaneous Operations"), inner.totalNano() - separateTime)); } totalTime[0] += Math.max(inner.totalNano(), separateTime); - entries.sort(Comparator.comparingLong(value -> value.time).reversed()); - list.addItem(new SubCategoryListEntry(Component.literal(stage), (List) (List) entries, Math.max(inner.totalNano(), separateTime), false)); + if (this.sortType == SortType.DURATION) { + entries.sort(Comparator.comparingLong(value -> value.time).reversed()); + } + subCategories.add(new SubCategoryListEntry(Component.literal(stage), (List) (List) entries, Math.max(inner.totalNano(), separateTime), false)); }); + if (this.sortType == SortType.DURATION) { + subCategories.sort(Comparator.comparingLong(SubCategoryListEntry::getTotalTime).reversed()); + } + subCategories.forEach(list::addItem); list.children().add(0, new PerformanceEntryImpl(Component.literal("Total Load Time"), totalTime[0])); addWidget(list); } @@ -212,4 +238,9 @@ protected int getScrollbarPosition() { return width - 6; } } + + private enum SortType { + ORDER, + DURATION + } } diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/performance/entry/SubCategoryListEntry.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/performance/entry/SubCategoryListEntry.java index 322c6cf5a..191192692 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/performance/entry/SubCategoryListEntry.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/performance/entry/SubCategoryListEntry.java @@ -156,6 +156,10 @@ public List narratables() { return Collections.emptyList(); } + public long getTotalTime() { + return totalTime; + } + public class CategoryLabelWidget implements GuiEventListener { private final Rectangle rectangle = new Rectangle(); diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/ScreenWithMenu.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/ScreenWithMenu.java new file mode 100644 index 000000000..c117f70e4 --- /dev/null +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/screen/ScreenWithMenu.java @@ -0,0 +1,89 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 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.gui.screen; + +import me.shedaniel.rei.impl.client.gui.modules.Menu; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; + +public class ScreenWithMenu extends Screen { + @Nullable + private Menu menu; + + protected ScreenWithMenu(Component component) { + super(component); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + super.render(graphics, mouseX, mouseY, delta); + if (this.menu != null) { + graphics.pose().pushPose(); + graphics.pose().translate(0, 0, 400); + this.menu.render(graphics, mouseX, mouseY, delta); + graphics.pose().popPose(); + } + } + + protected void setMenu(@Nullable Menu menu) { + this.menu = menu; + } + + protected void closeMenu() { + this.menu = null; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (this.menu != null) { + if (!this.menu.mouseClicked(mouseX, mouseY, button)) + this.menu = null; + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (this.menu != null && this.menu.mouseReleased(mouseX, mouseY, button)) + return true; + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount) { + if (this.menu != null && this.menu.mouseScrolled(mouseX, mouseY, amount)) + return true; + return super.mouseScrolled(mouseX, mouseY, amount); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + if (this.menu != null && this.menu.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) + return true; + return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); + } +} 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 fcaba58aa..f6a64683e 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 @@ -84,6 +84,7 @@ import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.function.Consumer; import java.util.function.Supplier; import java.util.function.UnaryOperator; import java.util.stream.Collectors; @@ -271,6 +272,12 @@ public EntryWidget entries(Collection> stacks) { return this; } + @Override + public Slot withEntriesListener(Consumer listener) { + this.getCyclingEntries().addListener($ -> listener.accept(this)); + return this; + } + public Slot entries(CyclingList> stacks) { this.getCyclingEntries().setBacking(stacks); if (removeTagMatch) tagMatch = null; diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/registry/display/DisplayCache.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/registry/display/DisplayCache.java index b65aba97c..9c725efc4 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/registry/display/DisplayCache.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/registry/display/DisplayCache.java @@ -34,6 +34,10 @@ import java.util.Set; public interface DisplayCache { + int cachedSize(); + + int notCachedSize(); + boolean doesCache(); boolean isCached(Display display); diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/registry/display/DisplayCacheImpl.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/registry/display/DisplayCacheImpl.java index 80a73f82e..4c995c9b0 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/registry/display/DisplayCacheImpl.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/registry/display/DisplayCacheImpl.java @@ -53,6 +53,16 @@ public DisplayCacheImpl(boolean init) { this.displaysByOutput = createSetMultimap(); } + @Override + public int cachedSize() { + return this.displaysCached.size(); + } + + @Override + public int notCachedSize() { + return this.displaysNotCached.size(); + } + @Override public boolean doesCache() { return this.cache; @@ -66,7 +76,7 @@ public boolean isCached(Display display) { @Override public void add(Display display) { if (this.cache) { - if (!preprocessed) { + if (!this.preprocessed) { this.displaysNotCached.add(display); } else { this.process(display); @@ -80,7 +90,7 @@ public void add(Display display) { @Override public boolean remove(Display display) { if (this.cache) { - if (!preprocessed) { + if (!this.preprocessed) { return this.displaysNotCached.remove(display); } else { boolean removed = this.displaysCached.remove(display); @@ -106,7 +116,7 @@ public boolean remove(Display display) { @Override public void endReload() { if (this.cache) { - if (preprocessed) { + if (this.preprocessed) { InternalLogger.getInstance().error("DisplayCache#endReload called after preprocessed!"); } 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 d0d36130c..7ec01a9f2 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 @@ -23,7 +23,9 @@ package me.shedaniel.rei.impl.client.registry.display; -import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; import dev.architectury.event.EventResult; import me.shedaniel.rei.api.client.plugins.REIClientPlugin; import me.shedaniel.rei.api.client.registry.category.CategoryRegistry; @@ -36,6 +38,7 @@ import me.shedaniel.rei.api.common.category.CategoryIdentifier; import me.shedaniel.rei.api.common.display.Display; import me.shedaniel.rei.api.common.plugins.PluginManager; +import me.shedaniel.rei.api.common.registry.ReloadStage; import me.shedaniel.rei.impl.common.InternalLogger; import me.shedaniel.rei.impl.common.registry.RecipeManagerContextImpl; import net.minecraft.world.item.crafting.Recipe; @@ -55,10 +58,6 @@ public class DisplayRegistryImpl extends RecipeManagerContextImpl category = (DisplayCategory) CategoryRegistry.getInstance().get(display.getCategoryIdentifier()).getCategory(); - Preconditions.checkNotNull(category, "Failed to resolve category: " + display.getCategoryIdentifier()); + return isDisplayVisible(category, display); + } + + public boolean isDisplayVisible(DisplayCategory category, Display display) { + if (category == null) throw new NullPointerException("Failed to resolve category: " + display.getCategoryIdentifier()); for (DisplayVisibilityPredicate predicate : visibilityPredicates) { try { EventResult result = predicate.handleDisplay(category, display); @@ -187,13 +190,8 @@ public void startReload() { @Override public void endReload() { - if (!fillers.isEmpty()) { - List> allSortedRecipes = getAllSortedRecipes(); - for (int i = allSortedRecipes.size() - 1; i >= 0; i--) { - Recipe recipe = allSortedRecipes.get(i); - addWithReason(recipe, DisplayAdditionReason.RECIPE_MANAGER); - } - } + InternalLogger.getInstance().debug("Found preliminary %d displays", displaySize()); + fillSortedRecipes(); for (CategoryIdentifier identifier : getAll().keySet()) { if (CategoryRegistry.getInstance().tryGet(identifier).isEmpty()) { @@ -201,21 +199,59 @@ public void endReload() { } } - List failedDisplays = new ArrayList<>(); + removeFailedDisplays(); + this.displaysHolder.endReload(); + InternalLogger.getInstance().debug("%d displays registration have completed", displaySize()); + } + + private void fillSortedRecipes() { + Stopwatch stopwatch = Stopwatch.createStarted(); + int lastSize = displaySize(); + if (!fillers.isEmpty()) { + List> allSortedRecipes = getAllSortedRecipes(); + for (int i = allSortedRecipes.size() - 1; i >= 0; i--) { + Recipe recipe = allSortedRecipes.get(i); + try { + addWithReason(recipe, DisplayAdditionReason.RECIPE_MANAGER); + } catch (Throwable e) { + InternalLogger.getInstance().error("Failed to fill display for recipe: %s [%s]", recipe, recipe.getId(), e); + } + } + } + InternalLogger.getInstance().debug("Filled %d displays from recipe manager in %s", displaySize() - lastSize, stopwatch.stop()); + } + + private void removeFailedDisplays() { + Multimap, Display> failedDisplays = Multimaps.newListMultimap(new HashMap<>(), ArrayList::new); for (List displays : getAll().values()) { for (Display display : displays) { if (!DisplayValidator.validate(display)) { - failedDisplays.add(display); + failedDisplays.put(display.getCategoryIdentifier(), display); } } } - for (Display display : failedDisplays) { - this.displaysHolder.remove(display); - } - - this.displaysHolder.endReload(); - InternalLogger.getInstance().debug("Registered %d displays", displaySize()); + InternalLogger.getInstance().debug("Removing %d failed displays" + (!failedDisplays.isEmpty() ? ":" : ""), failedDisplays.size()); + failedDisplays.asMap().entrySet().stream() + .sorted(Comparator.comparing(entry -> entry.getKey().toString())) + .forEach(entry -> { + InternalLogger.getInstance().debug("- %s: %d failed display" + (entry.getValue().size() == 1 ? "" : "s"), entry.getKey(), entry.getValue().size()); + for (Display display : entry.getValue()) { + this.displaysHolder.remove(display); + } + }); + } + + @Override + public void postStage(ReloadStage stage) { + if (stage != ReloadStage.END) return; + InternalLogger.getInstance().debug("Registered displays report (%d displays, %d cached / %d not cached)" + (displaySize() > 0 ? ":" : ""), + displaySize(), displaysHolder().cache().cachedSize(), displaysHolder().cache().notCachedSize()); + getAll().entrySet().stream() + .sorted(Comparator.comparing(entry -> entry.getKey().toString())) + .forEach(entry -> { + InternalLogger.getInstance().debug("- %s: %d display" + (entry.getValue().size() == 1 ? "" : "s"), entry.getKey(), entry.getValue().size()); + }); } public DisplaysHolder displaysHolder() { diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/util/OriginalRetainingCyclingList.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/util/OriginalRetainingCyclingList.java index aeb557d45..50627c2a1 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/util/OriginalRetainingCyclingList.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/util/OriginalRetainingCyclingList.java @@ -26,16 +26,15 @@ import com.google.common.collect.Iterables; import org.jetbrains.annotations.Nullable; -import java.util.AbstractList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; +import java.util.*; +import java.util.function.Consumer; import java.util.function.Supplier; public class OriginalRetainingCyclingList implements CyclingList.Mutable { private final Supplier empty; @Nullable private CyclingList backing = null; + private List>> listeners = List.of(); public OriginalRetainingCyclingList(Supplier empty) { this.empty = empty; @@ -56,7 +55,9 @@ public T peek() { @Override public T previous() { if (this.backing == null) return empty.get(); - return this.backing.previous(); + T previous = this.backing.previous(); + notifyListeners(); + return previous; } @Override @@ -72,7 +73,9 @@ public int previousIndex() { @Override public T next() { if (this.backing == null) return empty.get(); - return this.backing.next(); + T next = this.backing.next(); + notifyListeners(); + return next; } @Override @@ -86,11 +89,16 @@ public void add(T entry) { mutable.add(entry); this.backing = mutable; } + + notifyListeners(); } @Override public void resetToStart() { - if (this.backing != null) this.backing.resetToStart(); + if (this.backing != null) { + this.backing.resetToStart(); + notifyListeners(); + }; } @Override @@ -118,6 +126,8 @@ public void addAll(Collection entries) { mutable.addAll(entries); this.backing = mutable; } + + notifyListeners(); } } @@ -128,10 +138,13 @@ public void clear() { } else { this.backing = null; } + + notifyListeners(); } public void setBacking(@Nullable CyclingList backing) { this.backing = backing; + notifyListeners(); } private static AbstractList getListFromCollection(Collection entries) { @@ -173,4 +186,19 @@ public CyclingList getBacking() { if (this.backing == null) return CyclingList.of(this.empty); return this.backing; } + + public void addListener(Consumer> listener) { + if (this.listeners instanceof ArrayList>> list) { + list.add(listener); + } else { + this.listeners = new ArrayList<>(this.listeners); + this.listeners.add(listener); + } + } + + private void notifyListeners() { + for (Consumer> listener : this.listeners) { + listener.accept(this); + } + } } 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 2d2e6fada..0047638aa 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 @@ -111,7 +111,7 @@ private static Map, List> _buildMapFor(ViewSearc 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) { - if (!processingVisibilityHandlers || displayRegistry.isDisplayVisible(display)) { + if (!processingVisibilityHandlers || ((DisplayRegistryImpl) displayRegistry).isDisplayVisible(configuration.getCategory(), display)) { set.add(display); } } @@ -121,7 +121,7 @@ private static Map, List> _buildMapFor(ViewSearc return; } for (Display display : displays) { - if (processingVisibilityHandlers && !displayRegistry.isDisplayVisible(display)) continue; + if (processingVisibilityHandlers && !((DisplayRegistryImpl) displayRegistry).isDisplayVisible(configuration.getCategory(), display)) continue; if (!recipesForStacks.isEmpty()) { if (isRecipesFor(displaysHolder, recipesForStacks, display)) { set.add(display); @@ -171,7 +171,7 @@ private static Map, List> _buildMapFor(ViewSearc forCategories(processingVisibilityHandlers, filteringCategories, displayRegistry, result, (configuration, categoryId, displays, set) -> { if (categories.contains(categoryId)) return; for (Display display : displays) { - if (processingVisibilityHandlers && !displayRegistry.isDisplayVisible(display)) continue; + if (processingVisibilityHandlers && !((DisplayRegistryImpl) displayRegistry).isDisplayVisible(configuration.getCategory(), display)) continue; if (!recipesForStacksWildcard.isEmpty()) { if (isRecipesFor(displaysHolder, recipesForStacksWildcard, display)) { set.add(display); @@ -193,7 +193,7 @@ private static Map, List> _buildMapFor(ViewSearc if (isStackWorkStationOfCategory(configuration, usagesFor)) { categories.add(categoryId); if (processingVisibilityHandlers) { - set.addAll(CollectionUtils.filterToSet(displays, displayRegistry::isDisplayVisible)); + set.addAll(CollectionUtils.filterToSet(displays, display -> ((DisplayRegistryImpl) displayRegistry).isDisplayVisible(configuration.getCategory(), display))); } else { set.addAll(displays); } diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/common/plugins/PluginManagerImpl.java b/runtime/src/main/java/me/shedaniel/rei/impl/common/plugins/PluginManagerImpl.java index d5140f2c8..b0d7abb52 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/common/plugins/PluginManagerImpl.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/common/plugins/PluginManagerImpl.java @@ -38,12 +38,11 @@ import me.shedaniel.rei.api.common.plugins.REIPluginProvider; import me.shedaniel.rei.api.common.registry.ReloadStage; import me.shedaniel.rei.api.common.registry.Reloadable; -import me.shedaniel.rei.api.common.util.CollectionUtils; import me.shedaniel.rei.impl.common.InternalLogger; import me.shedaniel.rei.impl.common.logging.performance.PerformanceLogger; import net.minecraft.client.Minecraft; import net.minecraft.server.MinecraftServer; -import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; @@ -52,7 +51,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; import java.util.function.UnaryOperator; -import java.util.stream.Stream; @ApiStatus.Internal public class PluginManagerImpl

> implements PluginManager

, PluginView

{ @@ -62,7 +60,7 @@ public class PluginManagerImpl

> implements PluginManager< private final UnaryOperator> view; @Nullable private ReloadStage reloading = null; - private List observedStages = new ArrayList<>(); + private final List observedStages = new ArrayList<>(); private final List> plugins = new ArrayList<>(); private final Stopwatch reloadStopwatch = Stopwatch.createUnstarted(); private boolean forcedMainThread; @@ -127,15 +125,7 @@ public FluentIterable

getPlugins() { return FluentIterable.concat(Iterables.transform(plugins, REIPluginProvider::provide)); } - private static class PluginWrapper

> { - private final P plugin; - private final REIPluginProvider

provider; - - public PluginWrapper(P plugin, REIPluginProvider

provider) { - this.plugin = plugin; - this.provider = provider; - } - + private record PluginWrapper

>(P plugin, REIPluginProvider

provider) { public double getPriority() { return plugin.getPriority(); } @@ -143,7 +133,7 @@ public double getPriority() { public String getPluginProviderName() { String providerName = provider.getPluginProviderName(); - if (provider.provide().size() >= 1) { + if (!provider.provide().isEmpty()) { String pluginName = plugin.getPluginProviderName(); if (!providerName.equals(pluginName)) { @@ -158,32 +148,36 @@ public String getPluginProviderName() { @SuppressWarnings("RedundantTypeArguments") public FluentIterable> getPluginWrapped() { return FluentIterable.>concat(Iterables., Iterable>>transform(plugins, input -> Iterables.>transform(input.provide(), - plugin -> new PluginWrapper(plugin, input)))); + plugin -> new PluginWrapper<>(plugin, input)))); } private class SectionClosable implements Closeable { - private ReloadStage stage; - private MutablePair sectionData; + private final PluginReloadContext context; + private final String section; + private final Stopwatch stopwatch; - public SectionClosable(ReloadStage stage, String section) { - this.stage = stage; - this.sectionData = new MutablePair<>(Stopwatch.createUnstarted(), ""); - sectionData.setRight(section); - InternalLogger.getInstance().trace("[" + name(pluginClass) + " " + stage + "] Reloading Section: \"%s\"", section); - sectionData.getLeft().reset().start(); + public SectionClosable(PluginReloadContext context, String section) { + this.context = context; + this.section = section; + this.stopwatch = Stopwatch.createStarted(); + InternalLogger.getInstance().trace("[" + name(pluginClass) + " " + context.stage() + "] Reloading Section: \"%s\"", section); } @Override public void close() { - sectionData.getLeft().stop(); - String section = sectionData.getRight(); - InternalLogger.getInstance().trace("[" + name(pluginClass) + " " + stage + "] Reloading Section: \"%s\" done in %s", section, sectionData.getLeft().toString()); - sectionData.getLeft().reset(); + this.stopwatch.stop(); + InternalLogger.getInstance().trace("[" + name(pluginClass) + " " + context.stage() + "] Reloading Section: \"%s\" done in %s", this.section, this.stopwatch); + this.stopwatch.reset(); + try { + context.interruptionContext().checkInterrupted(); + } catch (InterruptedException exception) { + ExceptionUtils.rethrow(exception); + } } } - private SectionClosable section(ReloadStage stage, String section) { - return new SectionClosable(stage, section); + private SectionClosable section(PluginReloadContext context, String section) { + return new SectionClosable(context, section); } @FunctionalInterface @@ -191,9 +185,9 @@ private interface SectionPluginSink { void accept(boolean respectMainThread, Runnable task); } - private void pluginSection(ReloadStage stage, String sectionName, List> list, @Nullable Reloadable reloadable, BiConsumer, SectionPluginSink> consumer) { + private void pluginSection(PluginReloadContext context, String sectionName, List> list, @Nullable Reloadable reloadable, BiConsumer, SectionPluginSink> consumer) throws InterruptedException { for (PluginWrapper

wrapper : list) { - try (SectionClosable section = section(stage, sectionName + wrapper.getPluginProviderName() + "/")) { + try (SectionClosable section = section(context, sectionName + wrapper.getPluginProviderName() + "/")) { consumer.accept(wrapper, (respectMainThread, runnable) -> { if (!respectMainThread || reloadable == null || !wrapper.plugin.shouldBeForcefullyDoneOnMainThread(reloadable)) { runnable.run(); @@ -213,6 +207,7 @@ private void pluginSection(ReloadStage stage, String sectionName, List this.reloading = null)); + List> plugins = new ArrayList<>(getPluginWrapped().toList()); plugins.sort(Comparator.comparingDouble(PluginWrapper

::getPriority).reversed()); Collections.reverse(plugins); InternalLogger.getInstance().debug("========================================"); - InternalLogger.getInstance().debug(name(pluginClass) + " starting pre-reload for " + stage + "."); + InternalLogger.getInstance().debug(name(pluginClass) + " starting pre-reload for " + context.stage() + "."); InternalLogger.getInstance().debug("Reloadables (%d):".formatted(reloadables.size())); for (Reloadable

reloadable : reloadables) { InternalLogger.getInstance().debug(" - " + name(reloadable.getClass())); @@ -250,47 +247,51 @@ public void pre(ReloadStage stage) { this.forceMainThreadStopwatch.reset(); this.reloadStopwatch.reset().start(); this.observedStages.clear(); - this.observedStages.add(stage); - try (SectionClosable preRegister = section(stage, "pre-register/"); + this.observedStages.add(context.stage()); + try (SectionClosable preRegister = section(context, "pre-register/"); PerformanceLogger.Plugin perfLogger = RoughlyEnoughItemsCore.PERFORMANCE_LOGGER.stage("Pre Registration")) { - pluginSection(stage, "pre-register/", plugins, null, (plugin, sink) -> { + pluginSection(context, "pre-register/", plugins, null, (plugin, sink) -> { try (PerformanceLogger.Plugin.Inner inner = perfLogger.plugin(new Pair<>(plugin.provider, plugin.plugin))) { sink.accept(false, () -> { - ((REIPlugin

) plugin.plugin).preStage(this, stage); + ((REIPlugin

) plugin.plugin).preStage(this, context.stage()); }); } }); + } catch (InterruptedException exception) { + throw exception; } catch (Throwable throwable) { - this.reloading = null; - new RuntimeException("Failed to run pre registration").printStackTrace(); + InternalLogger.getInstance().throwException(new RuntimeException("Failed to run pre registration in stage [" + context.stage() + "]")); } - try (SectionClosable preStageAll = section(stage, "pre-stage/"); - PerformanceLogger.Plugin perfLogger = RoughlyEnoughItemsCore.PERFORMANCE_LOGGER.stage("Pre Stage " + stage.name())) { + try (SectionClosable preStageAll = section(context, "pre-stage/"); + PerformanceLogger.Plugin perfLogger = RoughlyEnoughItemsCore.PERFORMANCE_LOGGER.stage("Pre Stage " + context.stage().name())) { for (Reloadable

reloadable : reloadables) { Class reloadableClass = reloadable.getClass(); - try (SectionClosable preStage = section(stage, "pre-stage/" + name(reloadableClass) + "/"); + try (SectionClosable preStage = section(context, "pre-stage/" + name(reloadableClass) + "/"); PerformanceLogger.Plugin.Inner inner = perfLogger.stage(name(reloadableClass))) { - reloadable.preStage(stage); + reloadable.preStage(context.stage()); } catch (Throwable throwable) { - throwable.printStackTrace(); + if (throwable instanceof InterruptedException) throw (InterruptedException) throwable; + InternalLogger.getInstance().error("Failed to run pre registration task for reloadable [" + name(reloadableClass) + "] in stage [" + context.stage() + "]", throwable); } } } this.reloading = null; this.reloadStopwatch.stop(); InternalLogger.getInstance().debug("========================================"); - InternalLogger.getInstance().debug(name(pluginClass) + " finished pre-reload for " + stage + " in " + reloadStopwatch + "."); + InternalLogger.getInstance().debug(name(pluginClass) + " finished pre-reload for " + context.stage() + " in " + reloadStopwatch + "."); InternalLogger.getInstance().debug("========================================"); } @Override - public void post(ReloadStage stage) { - this.reloading = stage; + public void post(PluginReloadContext context0) throws InterruptedException { + this.reloading = context0.stage(); + PluginReloadContext context = PluginReloadContext.of(context0.stage(), context0.interruptionContext().withJob(() -> this.reloading = null)); + List> plugins = new ArrayList<>(getPluginWrapped().toList()); plugins.sort(Comparator.comparingDouble(PluginWrapper

::getPriority).reversed()); Collections.reverse(plugins); InternalLogger.getInstance().debug("========================================"); - InternalLogger.getInstance().debug(name(pluginClass) + " starting post-reload for " + stage + "."); + InternalLogger.getInstance().debug(name(pluginClass) + " starting post-reload for " + context.stage() + "."); InternalLogger.getInstance().debug("Reloadables (%d):".formatted(reloadables.size())); for (Reloadable

reloadable : reloadables) { InternalLogger.getInstance().debug(" - " + name(reloadable.getClass())); @@ -302,28 +303,29 @@ public void post(ReloadStage stage) { InternalLogger.getInstance().debug("========================================"); this.reloadStopwatch.start(); Stopwatch postStopwatch = Stopwatch.createStarted(); - try (SectionClosable postRegister = section(stage, "post-register/"); + try (SectionClosable postRegister = section(context, "post-register/"); PerformanceLogger.Plugin perfLogger = RoughlyEnoughItemsCore.PERFORMANCE_LOGGER.stage("Post Registration")) { - pluginSection(stage, "post-register/", plugins, null, (plugin, sink) -> { + pluginSection(context, "post-register/", plugins, null, (plugin, sink) -> { try (PerformanceLogger.Plugin.Inner inner = perfLogger.plugin(new Pair<>(plugin.provider, plugin.plugin))) { sink.accept(false, () -> { - ((REIPlugin

) plugin.plugin).postStage(this, stage); + ((REIPlugin

) plugin.plugin).postStage(this, context.stage()); }); } }); } catch (Throwable throwable) { - this.reloading = null; - new RuntimeException("Failed to run post registration").printStackTrace(); + if (throwable instanceof InterruptedException) throw (InterruptedException) throwable; + InternalLogger.getInstance().throwException(new RuntimeException("Failed to run post registration in stage [" + context.stage() + "]")); } - try (SectionClosable postStageAll = section(stage, "post-stage/"); - PerformanceLogger.Plugin perfLogger = RoughlyEnoughItemsCore.PERFORMANCE_LOGGER.stage("Pre Stage " + stage.name())) { + try (SectionClosable postStageAll = section(context, "post-stage/"); + PerformanceLogger.Plugin perfLogger = RoughlyEnoughItemsCore.PERFORMANCE_LOGGER.stage("Pre Stage " + context.stage().name())) { for (Reloadable

reloadable : reloadables) { Class reloadableClass = reloadable.getClass(); - try (SectionClosable postStage = section(stage, "post-stage/" + name(reloadableClass) + "/"); + try (SectionClosable postStage = section(context, "post-stage/" + name(reloadableClass) + "/"); PerformanceLogger.Plugin.Inner inner = perfLogger.stage(name(reloadableClass))) { - reloadable.postStage(stage); + reloadable.postStage(context.stage()); } catch (Throwable throwable) { - throwable.printStackTrace(); + if (throwable instanceof InterruptedException) throw (InterruptedException) throwable; + InternalLogger.getInstance().error("Failed to run post registration task for reloadable [" + name(reloadableClass) + "] in stage [" + context.stage() + "]", throwable); } } } @@ -331,7 +333,7 @@ public void post(ReloadStage stage) { this.reloadStopwatch.stop(); postStopwatch.stop(); InternalLogger.getInstance().debug("========================================"); - InternalLogger.getInstance().info(name(pluginClass) + " finished post-reload for " + stage + " in " + postStopwatch + ", totaling " + reloadStopwatch + "."); + InternalLogger.getInstance().info(name(pluginClass) + " finished post-reload for " + context.stage() + " in " + postStopwatch + ", totaling " + reloadStopwatch + "."); if (forcedMainThread) { InternalLogger.getInstance().warn("Forcing plugins to run on main thread took " + forceMainThreadStopwatch); } @@ -346,11 +348,21 @@ private static String name(Class clazz) { @Override public void startReload(ReloadStage stage) { + try { + reload(PluginReloadContext.of(stage, ReloadInterruptionContext.ofNever())); + } catch (InterruptedException e) { + ExceptionUtils.rethrow(e); + } + } + + @Override + public void reload(PluginReloadContext context0) throws InterruptedException { try { this.reloadStopwatch.start(); Stopwatch reloadingStopwatch = Stopwatch.createStarted(); - reloading = stage; - + this.reloading = context0.stage(); + PluginReloadContext context = PluginReloadContext.of(context0.stage(), context0.interruptionContext().withJob(() -> this.reloading = null)); + // Sort Plugins List> plugins = new ArrayList<>(getPluginWrapped().toList()); plugins.sort(Comparator.comparingDouble(PluginWrapper

::getPriority).reversed()); @@ -359,7 +371,7 @@ public void startReload(ReloadStage stage) { // Pre Reload String line = new String[]{"*", "=", "#", "@", "%", "~", "O", "-", "+"}[new Random().nextInt(9)].repeat(40); InternalLogger.getInstance().info(line); - InternalLogger.getInstance().info(name(pluginClass) + " starting main-reload for " + stage + "."); + InternalLogger.getInstance().info(name(pluginClass) + " starting main-reload for " + context.stage() + "."); InternalLogger.getInstance().debug("Reloadables (%d):".formatted(reloadables.size())); for (Reloadable

reloadable : reloadables) { InternalLogger.getInstance().debug(" - " + name(reloadable.getClass())); @@ -370,57 +382,58 @@ public void startReload(ReloadStage stage) { } InternalLogger.getInstance().info(line); - try (SectionClosable startReloadAll = section(stage, "start-reload/"); + try (SectionClosable startReloadAll = section(context, "start-reload/"); PerformanceLogger.Plugin perfLogger = RoughlyEnoughItemsCore.PERFORMANCE_LOGGER.stage("Reload Initialization")) { for (Reloadable

reloadable : reloadables) { Class reloadableClass = reloadable.getClass(); - try (SectionClosable startReload = section(stage, "start-reload/" + name(reloadableClass) + "/"); + try (SectionClosable startReload = section(context, "start-reload/" + name(reloadableClass) + "/"); PerformanceLogger.Plugin.Inner inner = perfLogger.stage(name(reloadableClass))) { - reloadable.startReload(stage); + reloadable.startReload(context.stage()); } catch (Throwable throwable) { - throwable.printStackTrace(); + if (throwable instanceof InterruptedException) throw (InterruptedException) throwable; + InternalLogger.getInstance().error("Failed to run start-reload task for reloadable [" + name(reloadableClass) + "] in stage [" + context.stage() + "]", throwable); } } } // Reload InternalLogger.getInstance().debug("========================================"); - InternalLogger.getInstance().debug(name(pluginClass) + " started main-reload for " + stage + "."); + InternalLogger.getInstance().debug(name(pluginClass) + " started main-reload for " + context.stage() + "."); InternalLogger.getInstance().debug("========================================"); for (Reloadable

reloadable : getReloadables()) { Class reloadableClass = reloadable.getClass(); - try (SectionClosable reloadablePlugin = section(stage, "reloadable-plugin/" + name(reloadableClass) + "/"); + try (SectionClosable reloadablePlugin = section(context, "reloadable-plugin/" + name(reloadableClass) + "/"); PerformanceLogger.Plugin perfLogger = RoughlyEnoughItemsCore.PERFORMANCE_LOGGER.stage(name(reloadableClass))) { try (PerformanceLogger.Plugin.Inner inner = perfLogger.stage("reloadable-plugin/" + name(reloadableClass) + "/prompt-others-before")) { for (Reloadable

listener : reloadables) { try { - listener.beforeReloadable(stage, reloadable); + listener.beforeReloadable(context.stage(), reloadable); } catch (Throwable throwable) { - throwable.printStackTrace(); + InternalLogger.getInstance().error("Failed to prompt others before reloadable [" + name(reloadableClass) + "] in stage [" + context.stage() + "]", throwable); } } } - pluginSection(stage, "reloadable-plugin/" + name(reloadableClass) + "/", plugins, reloadable, (plugin, sink) -> { + pluginSection(context, "reloadable-plugin/" + name(reloadableClass) + "/", plugins, reloadable, (plugin, sink) -> { try (PerformanceLogger.Plugin.Inner inner = perfLogger.plugin(new Pair<>(plugin.provider, plugin.plugin))) { sink.accept(true, () -> { for (Reloadable

listener : reloadables) { try { - listener.beforeReloadablePlugin(stage, reloadable, plugin.plugin); + listener.beforeReloadablePlugin(context.stage(), reloadable, plugin.plugin); } catch (Throwable throwable) { - throwable.printStackTrace(); + InternalLogger.getInstance().error("Failed to run pre-reloadable task for " + plugin.getPluginProviderName() + " before reloadable [" + name(reloadableClass) + "] in stage [" + context.stage() + "]", throwable); } } try { - reloadable.acceptPlugin(plugin.plugin, stage); + reloadable.acceptPlugin(plugin.plugin, context.stage()); } finally { for (Reloadable

listener : reloadables) { try { - listener.afterReloadablePlugin(stage, reloadable, plugin.plugin); + listener.afterReloadablePlugin(context.stage(), reloadable, plugin.plugin); } catch (Throwable throwable) { - throwable.printStackTrace(); + InternalLogger.getInstance().error("Failed to run post-reloadable task for " + plugin.getPluginProviderName() + " after reloadable [" + name(reloadableClass) + "] in stage [" + context.stage() + "]", throwable); } } } @@ -431,9 +444,9 @@ public void startReload(ReloadStage stage) { try (PerformanceLogger.Plugin.Inner inner = perfLogger.stage("reloadable-plugin/" + name(reloadableClass) + "/prompt-others-after")) { for (Reloadable

listener : reloadables) { try { - listener.afterReloadable(stage, reloadable); + listener.afterReloadable(context.stage(), reloadable); } catch (Throwable throwable) { - throwable.printStackTrace(); + InternalLogger.getInstance().error("Failed to prompt others after reloadable [" + name(reloadableClass) + "] in stage [" + context.stage() + "]", throwable); } } } @@ -442,28 +455,30 @@ public void startReload(ReloadStage stage) { // Post Reload InternalLogger.getInstance().debug("========================================"); - InternalLogger.getInstance().debug(name(pluginClass) + " ending main-reload for " + stage + "."); + InternalLogger.getInstance().debug(name(pluginClass) + " ending main-reload for " + context.stage() + "."); InternalLogger.getInstance().debug("========================================"); - try (SectionClosable endReloadAll = section(stage, "end-reload/"); + try (SectionClosable endReloadAll = section(context, "end-reload/"); PerformanceLogger.Plugin perfLogger = RoughlyEnoughItemsCore.PERFORMANCE_LOGGER.stage("Reload Finalization")) { for (Reloadable

reloadable : reloadables) { Class reloadableClass = reloadable.getClass(); - try (SectionClosable endReload = section(stage, "end-reload/" + name(reloadableClass) + "/"); + try (SectionClosable endReload = section(context, "end-reload/" + name(reloadableClass) + "/"); PerformanceLogger.Plugin.Inner inner = perfLogger.stage(name(reloadableClass))) { - reloadable.endReload(stage); + reloadable.endReload(context.stage()); } catch (Throwable throwable) { - throwable.printStackTrace(); + if (throwable instanceof InterruptedException) throw (InterruptedException) throwable; + InternalLogger.getInstance().error("Failed to run end-reload task for reloadable [" + name(reloadableClass) + "] in stage [" + context.stage() + "]", throwable); } } } this.reloadStopwatch.stop(); InternalLogger.getInstance().debug("========================================"); - InternalLogger.getInstance().debug(name(pluginClass) + " ended main-reload for " + stage + " in " + reloadingStopwatch.stop() + "."); + InternalLogger.getInstance().debug(name(pluginClass) + " ended main-reload for " + context.stage() + " in " + reloadingStopwatch.stop() + "."); InternalLogger.getInstance().debug("========================================"); } catch (Throwable throwable) { - throwable.printStackTrace(); + if (throwable instanceof InterruptedException) throw (InterruptedException) throwable; + InternalLogger.getInstance().error("Failed to run reload task in stage [" + context0.stage() + "]", throwable); } finally { reloading = null; } diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/common/plugins/ReloadManagerImpl.java b/runtime/src/main/java/me/shedaniel/rei/impl/common/plugins/ReloadManagerImpl.java new file mode 100644 index 000000000..a9590131f --- /dev/null +++ b/runtime/src/main/java/me/shedaniel/rei/impl/common/plugins/ReloadManagerImpl.java @@ -0,0 +1,211 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 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.common.plugins; + +import com.google.common.base.Stopwatch; +import com.google.common.base.Suppliers; +import dev.architectury.platform.Platform; +import dev.architectury.utils.Env; +import me.shedaniel.rei.RoughlyEnoughItemsCore; +import me.shedaniel.rei.api.client.config.ConfigObject; +import me.shedaniel.rei.api.common.plugins.PluginManager; +import me.shedaniel.rei.api.common.plugins.REIPlugin; +import me.shedaniel.rei.api.common.registry.ReloadStage; +import me.shedaniel.rei.api.common.util.CollectionUtils; +import me.shedaniel.rei.impl.common.InternalLogger; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.concurrent.*; +import java.util.function.Supplier; + +@ApiStatus.Internal +public class ReloadManagerImpl { + private static final Supplier RELOAD_PLUGINS = Suppliers.memoize(() -> Executors.newSingleThreadScheduledExecutor(task -> { + Thread thread = new Thread(task, "REI-ReloadPlugins"); + thread.setDaemon(true); + thread.setUncaughtExceptionHandler(($, exception) -> { + if (exception instanceof InterruptedException) { + InternalLogger.getInstance().debug("Interrupted while reloading plugins, could be caused by a new request to reload plugins!", new UncaughtException(exception)); + return; + } + + InternalLogger.getInstance().throwException(new UncaughtException(exception)); + }); + return thread; + })); + + private static final List RELOAD_TASKS = new CopyOnWriteArrayList<>(); + + private static class Task { + private final Future future; + private boolean interrupted = false; + private boolean completed = false; + + public Task(Future future) { + this.future = future; + } + } + + private static Executor executor() { + if (usesREIThread()) { + return RELOAD_PLUGINS.get(); + } else { + return runnable -> { + try { + runnable.run(); + } catch (Throwable throwable) { + InternalLogger.getInstance().throwException(throwable); + } + }; + } + } + + private static boolean usesREIThread() { + if (Platform.getEnvironment() == Env.CLIENT) { + return usesREIThreadClient(); + } else { + return false; + } + } + + @Environment(EnvType.CLIENT) + private static boolean usesREIThreadClient() { + return ConfigObject.getInstance().doesRegisterRecipesInAnotherThread(); + } + + public static int countRunningReloadTasks() { + return CollectionUtils.sumInt(RELOAD_TASKS, task -> !task.future.isDone() || !task.completed ? 1 : 0); + } + + public static int countUninterruptedRunningReloadTasks() { + return CollectionUtils.sumInt(RELOAD_TASKS, task -> !task.interrupted && (!task.future.isDone() || !task.completed) ? 1 : 0); + } + + public static void reloadPlugins(@Nullable ReloadStage start, ReloadInterruptionContext interruptionContext) { + InternalLogger.getInstance().debug("Starting Reload Plugins of stage " + start, new Throwable()); + if (usesREIThread()) { + if ((start == ReloadStage.START || start == null) && countRunningReloadTasks() > 0) { + InternalLogger.getInstance().warn("Trying to start reload plugins of stage %s but found %d existing reload task(s)!", start, countRunningReloadTasks()); + terminateReloadTasks(); + } + + if (!RELOAD_TASKS.isEmpty()) { + InternalLogger.getInstance().warn("Found %d existing reload task(s) after trying to terminate them!", RELOAD_TASKS.size()); + } + + Task[] task = new Task[1]; + Future future = CompletableFuture.runAsync(() -> reloadPlugins0(start, () -> interruptionContext.isInterrupted() || (task[0] != null && task[0].interrupted)), executor()) + .whenComplete((unused, throwable) -> { + // Remove the future from the list of futures + if (task[0] != null) { + task[0].completed = true; + RELOAD_TASKS.remove(task[0]); + task[0] = null; + } + }); + task[0] = new Task(future); + RELOAD_TASKS.add(task[0]); + } else { + reloadPlugins0(start, interruptionContext); + } + } + + private static void reloadPlugins0(@Nullable ReloadStage stage, ReloadInterruptionContext interruptionContext) { + if (stage == null) { + for (ReloadStage reloadStage : ReloadStage.values()) { + reloadPlugins0(reloadStage, interruptionContext); + } + } else { + reloadPlugins0(PluginReloadContext.of(stage, interruptionContext)); + } + } + + private static void reloadPlugins0(PluginReloadContext context) { + if (context.stage() == ReloadStage.START) RoughlyEnoughItemsCore.PERFORMANCE_LOGGER.clear(); + try { + for (PluginManager> instance : PluginManager.getActiveInstances()) { + instance.view().pre(context); + } + for (PluginManager> instance : PluginManager.getActiveInstances()) { + instance.view().reload(context); + } + for (PluginManager> instance : PluginManager.getActiveInstances()) { + instance.view().post(context); + } + } catch (InterruptedException e) { + InternalLogger.getInstance().debug("Interrupted while reloading plugins, could be caused by a new request to reload plugins!", e); + } catch (Throwable throwable) { + InternalLogger.getInstance().throwException(throwable); + } + } + + public static void terminateReloadTasks() { + if (countUninterruptedRunningReloadTasks() == 0) { + InternalLogger.getInstance().debug("Did not fulfill the request of termination of REI reload tasks because there are no uninterrupted running tasks. This is not an error."); + RELOAD_TASKS.clear(); + return; + } + + InternalLogger.getInstance().debug("Requested the termination of REI reload tasks."); + + for (Task task : RELOAD_TASKS) { + task.interrupted = true; + } + + long startTerminateTime = System.currentTimeMillis(); + Stopwatch stopwatch = Stopwatch.createStarted(); + while (countRunningReloadTasks() > 0) { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + InternalLogger.getInstance().error("Thread interrupted while waiting for reload tasks to terminate!", e); + } + + if (System.currentTimeMillis() - startTerminateTime > 5000) { + InternalLogger.getInstance().error("Took too long to terminate reload tasks (over 5 seconds)! Now forcefully terminating them!"); + for (Task task : RELOAD_TASKS) { + task.future.cancel(Platform.isFabric()); + } + break; + } + } + + if (countRunningReloadTasks() == 0) { + RELOAD_TASKS.clear(); + InternalLogger.getInstance().debug("Successfully terminated reload tasks in %s", stopwatch.stop()); + } else { + InternalLogger.getInstance().error("Failed to terminate reload tasks! Found %d running tasks!", countRunningReloadTasks()); + } + } + + private static class UncaughtException extends Exception { + public UncaughtException(Throwable cause) { + super(cause); + } + } +} diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/common/registry/RecipeManagerContextImpl.java b/runtime/src/main/java/me/shedaniel/rei/impl/common/registry/RecipeManagerContextImpl.java index da48a31ae..e1979f11a 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/common/registry/RecipeManagerContextImpl.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/common/registry/RecipeManagerContextImpl.java @@ -23,34 +23,21 @@ package me.shedaniel.rei.impl.common.registry; -import dev.architectury.utils.EnvExecutor; -import dev.architectury.utils.GameInstance; import me.shedaniel.rei.api.common.plugins.REIPlugin; import me.shedaniel.rei.api.common.registry.RecipeManagerContext; -import net.minecraft.client.Minecraft; +import me.shedaniel.rei.impl.common.util.InstanceHelper; import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.item.crafting.RecipeManager; import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.function.Supplier; import java.util.stream.Collectors; public class RecipeManagerContextImpl

> implements RecipeManagerContext

{ private static final Comparator> RECIPE_COMPARATOR = Comparator.comparing((Recipe o) -> o.getId().getNamespace()).thenComparing(o -> o.getId().getPath()); - private final Supplier recipeManager; private List> sortedRecipes = null; - public RecipeManagerContextImpl(Supplier recipeManager) { - this.recipeManager = recipeManager; - } - - public static Supplier supplier() { - return () -> EnvExecutor.getEnvSpecific(() -> () -> Minecraft.getInstance().getConnection().getRecipeManager(), - () -> () -> GameInstance.getServer().getRecipeManager()); - } - @Override public List> getAllSortedRecipes() { if (sortedRecipes == null) { @@ -62,7 +49,7 @@ public List> getAllSortedRecipes() { @Override public RecipeManager getRecipeManager() { - return recipeManager.get(); + return InstanceHelper.getInstance().recipeManager(); } @Override diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/common/util/InstanceHelper.java b/runtime/src/main/java/me/shedaniel/rei/impl/common/util/InstanceHelper.java new file mode 100644 index 000000000..e83d72467 --- /dev/null +++ b/runtime/src/main/java/me/shedaniel/rei/impl/common/util/InstanceHelper.java @@ -0,0 +1,125 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 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.common.util; + +import dev.architectury.platform.Platform; +import dev.architectury.utils.Env; +import dev.architectury.utils.GameInstance; +import me.shedaniel.rei.impl.common.InternalLogger; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.crafting.RecipeManager; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.lang.ref.WeakReference; + +@ApiStatus.Internal +public final class InstanceHelper { + private static final InstanceHelper INSTANCE = new InstanceHelper(); + private WeakReference registryAccessRef; + private WeakReference recipeManagerRef; + private boolean warnedRegistryAccess; + private boolean warnedRecipeManager; + + public static InstanceHelper getInstance() { + return INSTANCE; + } + + public RegistryAccess registryAccess() { + RegistryAccess access = this.registryAccessRef == null ? null : this.registryAccessRef.get(); + if (access != null) { + return access; + } + + if (Platform.getEnvironment() == Env.CLIENT) { + access = registryAccessFromClient(); + } else if (GameInstance.getServer() != null) { + access = GameInstance.getServer().registryAccess(); + } + + if (access == null && !this.warnedRegistryAccess) { + this.warnedRegistryAccess = true; + + InternalLogger.getInstance().throwException(new IllegalStateException("Cannot get registry access!")); + return RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); + } + + return access; + } + + public RecipeManager recipeManager() { + RecipeManager manager = this.recipeManagerRef == null ? null : this.recipeManagerRef.get(); + if (manager != null) { + return manager; + } + + if (Platform.getEnvironment() == Env.CLIENT) { + manager = recipeManagerFromClient(); + } else if (GameInstance.getServer() != null) { + manager = GameInstance.getServer().getRecipeManager(); + } + + if (manager == null && !this.warnedRegistryAccess) { + this.warnedRegistryAccess = true; + + throw new IllegalStateException("Cannot get recipe manager!"); + } + + return manager; + } + + @Environment(EnvType.CLIENT) + @Nullable + public static ClientPacketListener connectionFromClient() { + if (Minecraft.getInstance().level != null) { + return Minecraft.getInstance().level.connection; + } else if (Minecraft.getInstance().getConnection() != null) { + return Minecraft.getInstance().getConnection(); + } else if (Minecraft.getInstance().gameMode != null) { + // Sometimes the packet is sent way too fast and is between the connection and the level, better safe than sorry + return Minecraft.getInstance().gameMode.connection; + } + + return null; + } + + @Environment(EnvType.CLIENT) + private static RegistryAccess registryAccessFromClient() { + ClientPacketListener connection = connectionFromClient(); + if (connection == null) return null; + return connection.registryAccess(); + } + + @Environment(EnvType.CLIENT) + private static RecipeManager recipeManagerFromClient() { + ClientPacketListener connection = connectionFromClient(); + if (connection == null) return null; + return connection.getRecipeManager(); + } +} 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 0146868cf..5568968b6 100755 --- a/runtime/src/main/resources/assets/roughlyenoughitems/lang/en_us.json +++ b/runtime/src/main/resources/assets/roughlyenoughitems/lang/en_us.json @@ -79,6 +79,10 @@ "text.rei.tag_accept": "Tag: #%s", "text.rei.missing": "Missing following:", "text.rei.performance": "Performance Analysis", + "text.rei.display_registry_analysis": "Display Registry Analysis", + "text.rei.display_registry_analysis.displays": "%d display(s)", + "text.rei.sort": "Sort", + "text.rei.sort.by": "Sort by %s", "text.rei.addons": "REI Addons", "text.rei.shapeless": "Shapeless", "text.rei.crafting.firework.gunpowder.amount": "The amount of gunpowder affects the flight duration of the firework.", @@ -451,6 +455,8 @@ "config.rei.options.debug.search_performance.desc": "Verbose console print out for analysing search performance.", "config.rei.options.debug.entry_list_performance": "Entry List Performance", "config.rei.options.debug.entry_list_performance.desc": "Display the time used to render entries on the entry list.", + "config.rei.options.debug.display_registry_analysis": "Display Registry Analysis", + "config.rei.options.debug.display_registry_analysis.desc": "Break-down in registration of displays in each category.", "config.rei.options.groups.reset.reload": "Reload", "config.rei.options.reset.reload_plugins": "Reload Plugins", "config.rei.options.reset.reload_plugins.desc": "Resets the current loaded data and reload all data and plugins.",