diff --git a/Common/src/main/java/mezz/jei/common/async/JeiStartTask.java b/Common/src/main/java/mezz/jei/common/async/JeiStartTask.java new file mode 100644 index 000000000..2d85a49ad --- /dev/null +++ b/Common/src/main/java/mezz/jei/common/async/JeiStartTask.java @@ -0,0 +1,37 @@ +package mezz.jei.common.async; + +public class JeiStartTask extends Thread { + private boolean isCancelled = false; + + public JeiStartTask(Runnable target) { + super(target, "JEI Start"); + setDaemon(true); + } + + public void cancelStart() { + isCancelled = true; + } + + public static void interruptIfCanceled() { + Thread t = Thread.currentThread(); + if (t instanceof JeiStartTask startTask) { + if (startTask.isCancelled) { + throw new JeiAsyncStartInterrupt(); + } + } + } + + @Override + public void run() { + try { + super.run(); + } catch (JeiAsyncStartInterrupt ignored) { + + } + } + + private static final class JeiAsyncStartInterrupt extends Error { + public JeiAsyncStartInterrupt() { + } + } +} diff --git a/Common/src/main/java/mezz/jei/common/config/ClientConfig.java b/Common/src/main/java/mezz/jei/common/config/ClientConfig.java index 5a29c912e..0debb50a0 100644 --- a/Common/src/main/java/mezz/jei/common/config/ClientConfig.java +++ b/Common/src/main/java/mezz/jei/common/config/ClientConfig.java @@ -17,6 +17,7 @@ public final class ClientConfig implements IClientConfig { private final Supplier centerSearchBarEnabled; private final Supplier lowMemorySlowSearchEnabled; private final Supplier cheatToHotbarUsingHotkeysEnabled; + private final Supplier asyncLoadingEnabled; private final Supplier addBookmarksToFront; private final Supplier lookupFluidContents; private final Supplier giveMode; @@ -65,6 +66,13 @@ public ClientConfig(IConfigSchemaBuilder schema) { "Max. recipe gui height" ); + IConfigCategoryBuilder loading = schema.addCategory("loading"); + asyncLoadingEnabled = loading.addBoolean( + "asyncLoadingEnabled", + false, + "Whether JEI should load asynchronously, so that it finishes loading after world join." + ); + IConfigCategoryBuilder sorting = schema.addCategory("sorting"); ingredientSorterStages = sorting.addList( "IngredientSortStages", @@ -98,6 +106,11 @@ public boolean isCheatToHotbarUsingHotkeysEnabled() { return cheatToHotbarUsingHotkeysEnabled.get(); } + @Override + public boolean getAsyncLoadingEnabled() { + return asyncLoadingEnabled.get(); + } + @Override public boolean isAddingBookmarksToFront() { return addBookmarksToFront.get(); diff --git a/Common/src/main/java/mezz/jei/common/config/IClientConfig.java b/Common/src/main/java/mezz/jei/common/config/IClientConfig.java index bcb99c5a4..b6cc8f550 100644 --- a/Common/src/main/java/mezz/jei/common/config/IClientConfig.java +++ b/Common/src/main/java/mezz/jei/common/config/IClientConfig.java @@ -13,6 +13,8 @@ public interface IClientConfig { boolean isCheatToHotbarUsingHotkeysEnabled(); + boolean getAsyncLoadingEnabled(); + boolean isAddingBookmarksToFront(); boolean isLookupFluidContents(); diff --git a/Common/src/main/java/mezz/jei/common/util/ErrorUtil.java b/Common/src/main/java/mezz/jei/common/util/ErrorUtil.java index 3a205aaec..bcc7610ba 100644 --- a/Common/src/main/java/mezz/jei/common/util/ErrorUtil.java +++ b/Common/src/main/java/mezz/jei/common/util/ErrorUtil.java @@ -10,7 +10,6 @@ import net.minecraft.CrashReport; import net.minecraft.CrashReportCategory; import net.minecraft.ReportedException; -import net.minecraft.client.Minecraft; import net.minecraft.core.NonNullList; import net.minecraft.core.registries.Registries; import net.minecraft.nbt.CompoundTag; @@ -135,19 +134,6 @@ public static void checkNotNull(Collection values, String name) { } } - @SuppressWarnings("ConstantConditions") - public static void assertMainThread() { - Minecraft minecraft = Minecraft.getInstance(); - if (minecraft != null && !minecraft.isSameThread()) { - Thread currentThread = Thread.currentThread(); - throw new IllegalStateException( - "A JEI API method is being called by another mod from the wrong thread:\n" + - currentThread + "\n" + - "It must be called on the main thread by using Minecraft.addScheduledTask." - ); - } - } - public static ReportedException createRenderIngredientException(Throwable throwable, final T ingredient, IIngredientManager ingredientManager) { CrashReport crashreport = CrashReport.forThrowable(throwable, "Rendering ingredient"); CrashReportCategory ingredientCategory = crashreport.addCategory("Ingredient being rendered"); diff --git a/CommonApi/src/main/java/mezz/jei/api/IModPlugin.java b/CommonApi/src/main/java/mezz/jei/api/IModPlugin.java index eda5cae04..2ef236062 100644 --- a/CommonApi/src/main/java/mezz/jei/api/IModPlugin.java +++ b/CommonApi/src/main/java/mezz/jei/api/IModPlugin.java @@ -16,9 +16,14 @@ import mezz.jei.api.registration.IVanillaCategoryExtensionRegistration; import mezz.jei.api.runtime.IJeiRuntime; + /** * The main class to implement to create a JEI plugin. Everything communicated between a mod and JEI is through this class. - * IModPlugins must have the {@link JeiPlugin} annotation to get loaded by JEI. + * + * In a Forge environment, IModPlugins must have the {@link JeiPlugin} annotation to get loaded by JEI. + * + * In a Fabric environment, IModPlugins must be declared under `entrypoints.jei_mod_plugin` in `fabric.mod.json`. + * See the Fabric Wiki for more information. */ public interface IModPlugin { @@ -109,7 +114,11 @@ default void registerAdvanced(IAdvancedRegistration registration) { /** * Override the default JEI runtime. + * + * @since 12.0.2 + * @deprecated this has moved to {@link IRuntimePlugin} */ + @Deprecated(since = "13.2.0", forRemoval = true) default void registerRuntime(IRuntimeRegistration registration) { } diff --git a/CommonApi/src/main/java/mezz/jei/api/IRuntimePlugin.java b/CommonApi/src/main/java/mezz/jei/api/IRuntimePlugin.java new file mode 100644 index 000000000..2db299a41 --- /dev/null +++ b/CommonApi/src/main/java/mezz/jei/api/IRuntimePlugin.java @@ -0,0 +1,58 @@ +package mezz.jei.api; + +import mezz.jei.api.registration.IRuntimeRegistration; +import mezz.jei.api.runtime.IJeiRuntime; +import net.minecraft.resources.ResourceLocation; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +/** + * A runtime plugin is used to override the default JEI runtime. + * Only one runtime plugin will be used, so if you create one then JEI's will be deactivated. + * This is intended for mods that implement a GUI that completely replaces JEI's. + * + * In a Forge environment, IRuntimePlugins must have the {@link JeiPlugin} annotation to get loaded by JEI. + * + * In a Fabric environment, IModPlugins must be declared under `entrypoints.jei_runtime_plugin` in `fabric.mod.json`. + * See the Fabric Wiki for more information. + * + * @since 13.2.0 + */ +public interface IRuntimePlugin { + + /** + * The unique ID for this mod plugin. + * The namespace should be your mod's modId. + * + * @since 13.2.0 + */ + ResourceLocation getPluginUid(); + + /** + * Override the default JEI runtime. + * + * @since 13.2.0 + */ + default CompletableFuture registerRuntime(IRuntimeRegistration registration, Executor clientExecutor) { + return CompletableFuture.completedFuture(null); + } + + /** + * Called when JEI's runtime features are available, after all mods have registered. + * + * @since 13.2.0 + */ + default CompletableFuture onRuntimeAvailable(IJeiRuntime jeiRuntime, Executor clientExecutor) { + return CompletableFuture.completedFuture(null); + } + + /** + * Called when JEI's runtime features are no longer available, after a user quits or logs out of a world. + * + * @since 13.2.0 + */ + default CompletableFuture onRuntimeUnavailable(Executor clientExecutor) { + return CompletableFuture.completedFuture(null); + } +} diff --git a/Fabric/src/main/java/mezz/jei/fabric/events/JeiLifecycleEvents.java b/Fabric/src/main/java/mezz/jei/fabric/events/JeiLifecycleEvents.java index 1bed2f100..e7a8a89f3 100644 --- a/Fabric/src/main/java/mezz/jei/fabric/events/JeiLifecycleEvents.java +++ b/Fabric/src/main/java/mezz/jei/fabric/events/JeiLifecycleEvents.java @@ -36,6 +36,14 @@ public class JeiLifecycleEvents { } }); + public static final Event CLIENT_TICK_END = + EventFactory.createArrayBacked(Runnable.class, callbacks -> () -> { + for (Runnable callback : callbacks) { + callback.run(); + } + }); + + @Environment(EnvType.CLIENT) @FunctionalInterface public interface RegisterResourceReloadListener { diff --git a/Fabric/src/main/java/mezz/jei/fabric/mixin/EffectRenderingInventoryScreenMixin.java b/Fabric/src/main/java/mezz/jei/fabric/mixin/EffectRenderingInventoryScreenMixin.java index f0cb49dbb..037a33625 100644 --- a/Fabric/src/main/java/mezz/jei/fabric/mixin/EffectRenderingInventoryScreenMixin.java +++ b/Fabric/src/main/java/mezz/jei/fabric/mixin/EffectRenderingInventoryScreenMixin.java @@ -2,7 +2,7 @@ import mezz.jei.api.runtime.IIngredientListOverlay; import mezz.jei.api.runtime.IJeiRuntime; -import mezz.jei.fabric.plugins.fabric.FabricGuiPlugin; +import mezz.jei.fabric.plugins.fabric.FabricRuntimePlugin; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.client.gui.screens.inventory.EffectRenderingInventoryScreen; import net.minecraft.network.chat.Component; @@ -25,7 +25,7 @@ public EffectRenderingInventoryScreenMixin(AbstractContainerMenu menu, Inventory at = @At("STORE") ) public boolean modifyHasRoom(boolean bl) { - boolean ingredientListDisplayed = FabricGuiPlugin.getRuntime() + boolean ingredientListDisplayed = FabricRuntimePlugin.getRuntime() .map(IJeiRuntime::getIngredientListOverlay) .map(IIngredientListOverlay::isListDisplayed) .orElse(false); diff --git a/Fabric/src/main/java/mezz/jei/fabric/mixin/MinecraftMixin.java b/Fabric/src/main/java/mezz/jei/fabric/mixin/MinecraftMixin.java index 265dadaf4..3a7086f5a 100644 --- a/Fabric/src/main/java/mezz/jei/fabric/mixin/MinecraftMixin.java +++ b/Fabric/src/main/java/mezz/jei/fabric/mixin/MinecraftMixin.java @@ -48,4 +48,12 @@ public void beforeInitialResourceReload(GameConfig gameConfig, CallbackInfo ci) public void clearLevel(Screen screen, CallbackInfo ci) { JeiLifecycleEvents.GAME_STOP.invoker().run(); } + + @Inject( + method = "tick", + at = @At("TAIL") + ) + private void jeiOnTickEnd(CallbackInfo ci) { + JeiLifecycleEvents.CLIENT_TICK_END.invoker().run(); + } } diff --git a/Fabric/src/main/java/mezz/jei/fabric/plugins/fabric/FabricGuiPlugin.java b/Fabric/src/main/java/mezz/jei/fabric/plugins/fabric/FabricRuntimePlugin.java similarity index 57% rename from Fabric/src/main/java/mezz/jei/fabric/plugins/fabric/FabricGuiPlugin.java rename to Fabric/src/main/java/mezz/jei/fabric/plugins/fabric/FabricRuntimePlugin.java index 6ae9f2c82..f472cfc20 100644 --- a/Fabric/src/main/java/mezz/jei/fabric/plugins/fabric/FabricGuiPlugin.java +++ b/Fabric/src/main/java/mezz/jei/fabric/plugins/fabric/FabricRuntimePlugin.java @@ -1,12 +1,11 @@ package mezz.jei.fabric.plugins.fabric; -import mezz.jei.api.IModPlugin; +import mezz.jei.api.IRuntimePlugin; import mezz.jei.api.JeiPlugin; import mezz.jei.api.constants.ModIds; import mezz.jei.api.registration.IRuntimeRegistration; import mezz.jei.api.runtime.IJeiRuntime; import mezz.jei.fabric.startup.EventRegistration; -import mezz.jei.gui.startup.JeiEventHandlers; import mezz.jei.gui.startup.JeiGuiStarter; import net.minecraft.resources.ResourceLocation; import org.apache.logging.log4j.LogManager; @@ -14,9 +13,11 @@ import org.jetbrains.annotations.Nullable; import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; @JeiPlugin -public class FabricGuiPlugin implements IModPlugin { +public class FabricRuntimePlugin implements IRuntimePlugin { private static final Logger LOGGER = LogManager.getLogger(); private static @Nullable IJeiRuntime runtime; @@ -24,25 +25,27 @@ public class FabricGuiPlugin implements IModPlugin { @Override public ResourceLocation getPluginUid() { - return new ResourceLocation(ModIds.JEI_ID, "fabric_gui"); + return new ResourceLocation(ModIds.JEI_ID, "fabric_runtime"); } @Override - public void registerRuntime(IRuntimeRegistration registration) { - JeiEventHandlers eventHandlers = JeiGuiStarter.start(registration); - eventRegistration.setEventHandlers(eventHandlers); + public CompletableFuture registerRuntime(IRuntimeRegistration registration, Executor clientExecutor) { + return JeiGuiStarter.start(registration, clientExecutor) + .thenAccept(eventRegistration::setEventHandlers); } @Override - public void onRuntimeAvailable(IJeiRuntime jeiRuntime) { + public CompletableFuture onRuntimeAvailable(IJeiRuntime jeiRuntime, Executor clientExecutor) { runtime = jeiRuntime; + return CompletableFuture.completedFuture(null); } @Override - public void onRuntimeUnavailable() { + public CompletableFuture onRuntimeUnavailable(Executor clientExecutor) { runtime = null; LOGGER.info("Stopping JEI GUI"); eventRegistration.clear(); + return CompletableFuture.completedFuture(null); } public static Optional getRuntime() { diff --git a/Fabric/src/main/java/mezz/jei/fabric/startup/ClientLifecycleHandler.java b/Fabric/src/main/java/mezz/jei/fabric/startup/ClientLifecycleHandler.java index f0dc5ec74..e254d1d45 100644 --- a/Fabric/src/main/java/mezz/jei/fabric/startup/ClientLifecycleHandler.java +++ b/Fabric/src/main/java/mezz/jei/fabric/startup/ClientLifecycleHandler.java @@ -1,6 +1,5 @@ package mezz.jei.fabric.startup; -import mezz.jei.api.IModPlugin; import mezz.jei.common.Internal; import mezz.jei.common.config.IServerConfig; import mezz.jei.common.network.ClientPacketRouter; @@ -16,8 +15,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.util.List; - public class ClientLifecycleHandler { private static final Logger LOGGER = LogManager.getLogger(); @@ -34,9 +31,9 @@ public ClientLifecycleHandler(IServerConfig serverConfig) { ClientPacketRouter packetRouter = new ClientPacketRouter(serverConnection, serverConfig); ClientNetworkHandler.registerClientPacketHandler(packetRouter); - List plugins = FabricPluginFinder.getModPlugins(); - StartData startData = new StartData( - plugins, + FabricPluginFinder pluginFinder = new FabricPluginFinder(); + StartData startData = StartData.create( + pluginFinder, serverConnection, keyMappings ); @@ -54,6 +51,7 @@ public void registerEvents() { }) ); JeiLifecycleEvents.GAME_STOP.register(this::stopJei); + JeiLifecycleEvents.CLIENT_TICK_END.register(this.jeiStarter::tick); } public ResourceManagerReloadListener getReloadListener() { diff --git a/Fabric/src/main/java/mezz/jei/fabric/startup/EventRegistration.java b/Fabric/src/main/java/mezz/jei/fabric/startup/EventRegistration.java index 532d74dc5..6bdd0cbba 100644 --- a/Fabric/src/main/java/mezz/jei/fabric/startup/EventRegistration.java +++ b/Fabric/src/main/java/mezz/jei/fabric/startup/EventRegistration.java @@ -38,6 +38,10 @@ private void registerEvents() { ScreenEvents.BEFORE_INIT.register((client, screen, scaledWidth, scaledHeight) -> registerScreenEvents(screen) ); + Screen currentScreen = Minecraft.getInstance().screen; + if (currentScreen != null) { + registerScreenEvents(currentScreen); + } JeiCharTypedEvents.BEFORE_CHAR_TYPED.register(this::beforeCharTyped); ScreenEvents.AFTER_INIT.register(this::afterInit); JeiScreenEvents.AFTER_RENDER_BACKGROUND.register(this::afterRenderBackground); diff --git a/Fabric/src/main/java/mezz/jei/fabric/startup/FabricPluginFinder.java b/Fabric/src/main/java/mezz/jei/fabric/startup/FabricPluginFinder.java index fea8151f5..4c0ca4c6f 100644 --- a/Fabric/src/main/java/mezz/jei/fabric/startup/FabricPluginFinder.java +++ b/Fabric/src/main/java/mezz/jei/fabric/startup/FabricPluginFinder.java @@ -1,27 +1,33 @@ package mezz.jei.fabric.startup; import mezz.jei.api.IModPlugin; +import mezz.jei.api.IRuntimePlugin; +import mezz.jei.library.startup.IPluginFinder; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.entrypoint.EntrypointContainer; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; -public final class FabricPluginFinder { - private FabricPluginFinder() { +public final class FabricPluginFinder implements IPluginFinder { + private static final Map, String> entryPointKeys = Map.of( + IModPlugin.class, "jei_mod_plugin", + IRuntimePlugin.class, "jei_runtime_plugin" + ); - } - - public static List getModPlugins() { - return getInstances("jei_mod_plugin", IModPlugin.class); - } + @Override + public List getPlugins(Class pluginClass) { + String entryPointKey = entryPointKeys.get(pluginClass); + if (entryPointKey == null) { + throw new IllegalArgumentException("FabricPluginFinder does not support " + pluginClass); + } - @SuppressWarnings("SameParameterValue") - private static List getInstances(String entrypointContainerKey, Class instanceClass) { FabricLoader fabricLoader = FabricLoader.getInstance(); - List> pluginContainers = fabricLoader.getEntrypointContainers(entrypointContainerKey, instanceClass); + List> pluginContainers = fabricLoader.getEntrypointContainers(entryPointKey, pluginClass); return pluginContainers.stream() .map(EntrypointContainer::getEntrypoint) .collect(Collectors.toList()); } + } diff --git a/Fabric/src/main/resources/fabric.mod.json b/Fabric/src/main/resources/fabric.mod.json index ac998e5d2..1144d975b 100644 --- a/Fabric/src/main/resources/fabric.mod.json +++ b/Fabric/src/main/resources/fabric.mod.json @@ -25,11 +25,13 @@ "mezz.jei.fabric.JustEnoughItemsClient" ], "jei_mod_plugin": [ - "mezz.jei.library.plugins.vanilla.VanillaPlugin", - "mezz.jei.library.plugins.jei.JeiInternalPlugin", + "mezz.jei.gui.plugins.JeiGuiPlugin", "mezz.jei.library.plugins.debug.JeiDebugPlugin", - "mezz.jei.fabric.plugins.fabric.FabricGuiPlugin", - "mezz.jei.gui.plugins.JeiGuiPlugin" + "mezz.jei.library.plugins.jei.JeiInternalPlugin", + "mezz.jei.library.plugins.vanilla.VanillaPlugin" + ], + "jei_runtime_plugin": [ + "mezz.jei.fabric.plugins.fabric.FabricRuntimePlugin" ] }, "mixins": [ diff --git a/Forge/src/main/java/mezz/jei/forge/JustEnoughItemsClient.java b/Forge/src/main/java/mezz/jei/forge/JustEnoughItemsClient.java index 69e20c218..ebf7a1cd0 100644 --- a/Forge/src/main/java/mezz/jei/forge/JustEnoughItemsClient.java +++ b/Forge/src/main/java/mezz/jei/forge/JustEnoughItemsClient.java @@ -1,6 +1,5 @@ package mezz.jei.forge; -import mezz.jei.api.IModPlugin; import mezz.jei.common.Internal; import mezz.jei.common.config.IServerConfig; import mezz.jei.common.gui.textures.Textures; @@ -18,7 +17,6 @@ import net.minecraftforge.client.event.RegisterKeyMappingsEvent; import java.util.HashSet; -import java.util.List; import java.util.Set; public class JustEnoughItemsClient { @@ -41,16 +39,16 @@ public JustEnoughItemsClient( ClientPacketRouter packetRouter = new ClientPacketRouter(serverConnection, serverConfig); networkHandler.registerClientPacketHandler(packetRouter); - List plugins = ForgePluginFinder.getModPlugins(); - StartData startData = new StartData( - plugins, + ForgePluginFinder forgePluginFinder = new ForgePluginFinder(); + StartData startData = StartData.create( + forgePluginFinder, serverConnection, keyMappings ); JeiStarter jeiStarter = new JeiStarter(startData); - this.startEventObserver = new StartEventObserver(jeiStarter::start, jeiStarter::stop); + this.startEventObserver = new StartEventObserver(jeiStarter::start, jeiStarter::stop, jeiStarter::tick); this.startEventObserver.register(subscriptions); } diff --git a/Forge/src/main/java/mezz/jei/forge/plugins/forge/ForgeGuiPlugin.java b/Forge/src/main/java/mezz/jei/forge/plugins/forge/ForgeRuntimePlugin.java similarity index 58% rename from Forge/src/main/java/mezz/jei/forge/plugins/forge/ForgeGuiPlugin.java rename to Forge/src/main/java/mezz/jei/forge/plugins/forge/ForgeRuntimePlugin.java index b19fed470..5593267d3 100644 --- a/Forge/src/main/java/mezz/jei/forge/plugins/forge/ForgeGuiPlugin.java +++ b/Forge/src/main/java/mezz/jei/forge/plugins/forge/ForgeRuntimePlugin.java @@ -1,44 +1,48 @@ package mezz.jei.forge.plugins.forge; -import mezz.jei.api.IModPlugin; +import mezz.jei.api.IRuntimePlugin; import mezz.jei.api.JeiPlugin; import mezz.jei.api.constants.ModIds; import mezz.jei.api.registration.IRuntimeRegistration; import mezz.jei.forge.events.RuntimeEventSubscriptions; import mezz.jei.forge.startup.EventRegistration; -import mezz.jei.gui.startup.JeiEventHandlers; import mezz.jei.gui.startup.JeiGuiStarter; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.common.MinecraftForge; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + @JeiPlugin -public class ForgeGuiPlugin implements IModPlugin { +public class ForgeRuntimePlugin implements IRuntimePlugin { private static final Logger LOGGER = LogManager.getLogger(); private final RuntimeEventSubscriptions runtimeSubscriptions = new RuntimeEventSubscriptions(MinecraftForge.EVENT_BUS); @Override public ResourceLocation getPluginUid() { - return new ResourceLocation(ModIds.JEI_ID, "forge_gui"); + return new ResourceLocation(ModIds.JEI_ID, "forge_runtime"); } @Override - public void registerRuntime(IRuntimeRegistration registration) { + public CompletableFuture registerRuntime(IRuntimeRegistration registration, Executor clientExecutor) { if (!runtimeSubscriptions.isEmpty()) { LOGGER.error("JEI GUI is already running."); runtimeSubscriptions.clear(); } - JeiEventHandlers eventHandlers = JeiGuiStarter.start(registration); - - EventRegistration.registerEvents(runtimeSubscriptions, eventHandlers); + return JeiGuiStarter.start(registration, clientExecutor) + .thenAcceptAsync(eventHandlers -> { + EventRegistration.registerEvents(runtimeSubscriptions, eventHandlers); + }, clientExecutor); } @Override - public void onRuntimeUnavailable() { + public CompletableFuture onRuntimeUnavailable(Executor clientExecutor) { LOGGER.info("Stopping JEI GUI"); runtimeSubscriptions.clear(); + return CompletableFuture.completedFuture(null); } } diff --git a/Forge/src/main/java/mezz/jei/forge/startup/ForgePluginFinder.java b/Forge/src/main/java/mezz/jei/forge/startup/ForgePluginFinder.java index 32e7a6182..3cbf34595 100644 --- a/Forge/src/main/java/mezz/jei/forge/startup/ForgePluginFinder.java +++ b/Forge/src/main/java/mezz/jei/forge/startup/ForgePluginFinder.java @@ -1,37 +1,28 @@ package mezz.jei.forge.startup; -import java.lang.reflect.Constructor; -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; - +import mezz.jei.api.JeiPlugin; +import mezz.jei.library.startup.IPluginFinder; import net.minecraftforge.fml.ModList; import net.minecraftforge.forgespi.language.ModFileScanData; - -import mezz.jei.api.IModPlugin; -import mezz.jei.api.JeiPlugin; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.objectweb.asm.Type; -public final class ForgePluginFinder { - private static final Logger LOGGER = LogManager.getLogger(); - - private ForgePluginFinder() { +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; - } +public final class ForgePluginFinder implements IPluginFinder { + private static final Logger LOGGER = LogManager.getLogger(); - public static List getModPlugins() { - return getInstances(JeiPlugin.class, IModPlugin.class); - } + private final LinkedHashSet pluginClassNames; - @SuppressWarnings("SameParameterValue") - private static List getInstances(Class annotationClass, Class instanceClass) { - Type annotationType = Type.getType(annotationClass); + public ForgePluginFinder() { + Type annotationType = Type.getType(JeiPlugin.class); List allScanData = ModList.get().getAllScanData(); - Set pluginClassNames = new LinkedHashSet<>(); + this.pluginClassNames = new LinkedHashSet<>(); for (ModFileScanData scanData : allScanData) { Iterable annotations = scanData.getAnnotations(); for (ModFileScanData.AnnotationData a : annotations) { @@ -41,15 +32,21 @@ private static List getInstances(Class annotationClass, Class insta } } } + } + + @Override + public List getPlugins(Class pluginClass) { List instances = new ArrayList<>(); for (String className : pluginClassNames) { try { Class asmClass = Class.forName(className); - Class asmInstanceClass = asmClass.asSubclass(instanceClass); - Constructor constructor = asmInstanceClass.getDeclaredConstructor(); - T instance = constructor.newInstance(); - instances.add(instance); - } catch (ReflectiveOperationException | LinkageError e) { + if (pluginClass.isAssignableFrom(asmClass)) { + Class asmInstanceClass = asmClass.asSubclass(pluginClass); + Constructor constructor = asmInstanceClass.getDeclaredConstructor(); + T instance = constructor.newInstance(); + instances.add(instance); + } + } catch (ReflectiveOperationException | ClassCastException | LinkageError e) { LOGGER.error("Failed to load: {}", className, e); } } diff --git a/Forge/src/main/java/mezz/jei/forge/startup/StartEventObserver.java b/Forge/src/main/java/mezz/jei/forge/startup/StartEventObserver.java index 13ea02768..67b453bc5 100644 --- a/Forge/src/main/java/mezz/jei/forge/startup/StartEventObserver.java +++ b/Forge/src/main/java/mezz/jei/forge/startup/StartEventObserver.java @@ -10,6 +10,7 @@ import net.minecraftforge.client.event.RecipesUpdatedEvent; import net.minecraftforge.client.event.ScreenEvent; import net.minecraftforge.event.TagsUpdatedEvent; +import net.minecraftforge.event.TickEvent; import net.minecraftforge.eventbus.api.Event; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -37,11 +38,13 @@ private enum State { private final Set> observedEvents = new HashSet<>(); private final Runnable startRunnable; private final Runnable stopRunnable; + private final Runnable tickRunnable; private State state = State.DISABLED; - public StartEventObserver(Runnable startRunnable, Runnable stopRunnable) { + public StartEventObserver(Runnable startRunnable, Runnable stopRunnable, Runnable tickRunnable) { this.startRunnable = startRunnable; this.stopRunnable = stopRunnable; + this.tickRunnable = tickRunnable; } public void register(PermanentEventSubscriptions subscriptions) { @@ -79,6 +82,12 @@ public void register(PermanentEventSubscriptions subscriptions) { } } }); + + subscriptions.register(TickEvent.ClientTickEvent.class, event -> { + if(event.phase == TickEvent.Phase.END && this.state == State.JEI_STARTED) { + this.tickRunnable.run(); + } + }); } /** diff --git a/Forge/src/test/java/mezz/jei/test/IngredientFilterTest.java b/Forge/src/test/java/mezz/jei/test/IngredientFilterTest.java index 197650d79..51aedfb0c 100644 --- a/Forge/src/test/java/mezz/jei/test/IngredientFilterTest.java +++ b/Forge/src/test/java/mezz/jei/test/IngredientFilterTest.java @@ -1,5 +1,6 @@ package mezz.jei.test; +import com.google.common.util.concurrent.MoreExecutors; import mezz.jei.api.helpers.IColorHelper; import mezz.jei.api.helpers.IModIdHelper; import mezz.jei.api.ingredients.IIngredientRenderer; @@ -7,9 +8,9 @@ import mezz.jei.api.runtime.IEditModeConfig; import mezz.jei.api.runtime.IIngredientManager; import mezz.jei.api.runtime.IIngredientVisibility; -import mezz.jei.common.util.Translator; -import mezz.jei.common.config.IClientToggleState; import mezz.jei.common.config.IClientConfig; +import mezz.jei.common.config.IClientToggleState; +import mezz.jei.common.util.Translator; import mezz.jei.gui.filter.FilterTextSource; import mezz.jei.gui.filter.IFilterTextSource; import mezz.jei.gui.ingredients.IIngredientSorter; @@ -24,13 +25,13 @@ import mezz.jei.library.ingredients.subtypes.SubtypeManager; import mezz.jei.library.load.registration.IngredientManagerBuilder; import mezz.jei.test.lib.TestClientConfig; +import mezz.jei.test.lib.TestClientToggleState; import mezz.jei.test.lib.TestColorHelper; import mezz.jei.test.lib.TestIngredient; import mezz.jei.test.lib.TestIngredientFilterConfig; import mezz.jei.test.lib.TestIngredientHelper; import mezz.jei.test.lib.TestModIdHelper; import mezz.jei.test.lib.TestPlugin; -import mezz.jei.test.lib.TestClientToggleState; import net.minecraft.core.NonNullList; import net.minecraft.network.chat.Component; import net.minecraft.util.StringUtil; @@ -44,6 +45,8 @@ import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; public class IngredientFilterTest { private static final int EXTRA_INGREDIENT_COUNT = 5; @@ -61,8 +64,9 @@ public class IngredientFilterTest { private FilterTextSource filterTextSource; @BeforeEach - public void setup() { + public void setup() throws ExecutionException, InterruptedException { TestPlugin testPlugin = new TestPlugin(); + Executor clientExecutor = MoreExecutors.directExecutor(); SubtypeInterpreters subtypeInterpreters = new SubtypeInterpreters(); SubtypeManager subtypeManager = new SubtypeManager(subtypeInterpreters); @@ -92,11 +96,11 @@ public void setup() { ingredientFilterConfig, ingredientManager, ingredientListSorter, - baseList, modIdHelper, ingredientVisibility, colorHelper ); + this.ingredientFilter.addIngredientsAsync(baseList, clientExecutor).get(); this.ingredientManager.registerIngredientListener(blacklist); this.ingredientManager.registerIngredientListener(ingredientFilter); diff --git a/Forge/src/test/java/mezz/jei/test/lib/TestClientConfig.java b/Forge/src/test/java/mezz/jei/test/lib/TestClientConfig.java index fbb39cc60..e03b84b9b 100644 --- a/Forge/src/test/java/mezz/jei/test/lib/TestClientConfig.java +++ b/Forge/src/test/java/mezz/jei/test/lib/TestClientConfig.java @@ -28,6 +28,11 @@ public boolean isCheatToHotbarUsingHotkeysEnabled() { return false; } + @Override + public boolean getAsyncLoadingEnabled() { + return false; + } + @Override public boolean isAddingBookmarksToFront() { return false; diff --git a/Gui/src/main/java/mezz/jei/gui/ingredients/IListElementInfo.java b/Gui/src/main/java/mezz/jei/gui/ingredients/IListElementInfo.java index 0eb89b0e7..1d3e67864 100644 --- a/Gui/src/main/java/mezz/jei/gui/ingredients/IListElementInfo.java +++ b/Gui/src/main/java/mezz/jei/gui/ingredients/IListElementInfo.java @@ -37,5 +37,4 @@ public interface IListElementInfo { void setSortedIndex(int sortIndex); int getSortedIndex(); - } diff --git a/Gui/src/main/java/mezz/jei/gui/ingredients/IngredientFilter.java b/Gui/src/main/java/mezz/jei/gui/ingredients/IngredientFilter.java index b4cddf555..402e02be2 100644 --- a/Gui/src/main/java/mezz/jei/gui/ingredients/IngredientFilter.java +++ b/Gui/src/main/java/mezz/jei/gui/ingredients/IngredientFilter.java @@ -1,5 +1,6 @@ package mezz.jei.gui.ingredients; +import com.google.common.collect.Lists; import mezz.jei.api.helpers.IColorHelper; import mezz.jei.api.helpers.IModIdHelper; import mezz.jei.api.ingredients.IIngredientHelper; @@ -9,9 +10,9 @@ import mezz.jei.api.runtime.IIngredientManager; import mezz.jei.api.runtime.IIngredientVisibility; import mezz.jei.common.config.DebugConfig; -import mezz.jei.common.util.Translator; import mezz.jei.common.config.IClientConfig; import mezz.jei.common.config.IIngredientFilterConfig; +import mezz.jei.common.util.Translator; import mezz.jei.gui.filter.IFilterTextSource; import mezz.jei.gui.overlay.IIngredientGridSource; import mezz.jei.gui.search.ElementPrefixParser; @@ -34,9 +35,13 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import java.util.stream.Stream; public class IngredientFilter implements IIngredientGridSource, IIngredientManager.IIngredientListener { @@ -64,7 +69,6 @@ public IngredientFilter( IIngredientFilterConfig config, IIngredientManager ingredientManager, IIngredientSorter sorter, - NonNullList> ingredients, IModIdHelper modIdHelper, IIngredientVisibility ingredientVisibility, IColorHelper colorHelper @@ -82,19 +86,42 @@ public IngredientFilter( this.elementSearch = new ElementSearch(this.elementPrefixParser); } - LOGGER.info("Adding {} ingredients", ingredients.size()); - ingredients.stream() - .map(i -> ListElementInfo.create(i, ingredientManager, modIdHelper)) - .flatMap(Optional::stream) - .forEach(this::addIngredient); - LOGGER.info("Added {} ingredients", ingredients.size()); - this.filterTextSource.addListener(filterText -> { ingredientListCached = null; notifyListenersOfChange(); }); } + public CompletableFuture addIngredientsAsync( + NonNullList> ingredients, + Executor clientExecutor + ) { + int ingredientCount = ingredients.size(); + LOGGER.info("Adding {} ingredients", ingredientCount); + List> elementInfos = ingredients.stream() + .map(i -> ListElementInfo.create(i, ingredientManager, modIdHelper)) + .flatMap(Optional::stream) + .collect(Collectors.toList()); + + int batchSize = 1000; + AtomicInteger addedTotal = new AtomicInteger(0); + Stream> futures = Lists.partition(elementInfos, batchSize) + .stream() + .map(batch -> + CompletableFuture.runAsync(() -> { + for (IListElementInfo elementInfo : batch) { + this.addIngredient(elementInfo); + } + int added = addedTotal.addAndGet(batch.size()); + if (added % (10 * batchSize) == 0 || added == ingredientCount) { + LOGGER.info("Added {}/{} ingredients", added, ingredientCount); + } + }, clientExecutor) + ); + + return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)); + } + public void addIngredient(IListElementInfo info) { IListElement element = info.getElement(); updateHiddenState(element); diff --git a/Gui/src/main/java/mezz/jei/gui/ingredients/ListElementInfo.java b/Gui/src/main/java/mezz/jei/gui/ingredients/ListElementInfo.java index 7239fd90b..9b6a69ef7 100644 --- a/Gui/src/main/java/mezz/jei/gui/ingredients/ListElementInfo.java +++ b/Gui/src/main/java/mezz/jei/gui/ingredients/ListElementInfo.java @@ -6,8 +6,8 @@ import mezz.jei.api.ingredients.IIngredientRenderer; import mezz.jei.api.ingredients.ITypedIngredient; import mezz.jei.api.runtime.IIngredientManager; -import mezz.jei.common.util.Translator; import mezz.jei.common.config.IIngredientFilterConfig; +import mezz.jei.common.util.Translator; import net.minecraft.resources.ResourceLocation; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -158,5 +158,4 @@ public void setSortedIndex(int sortIndex) { public int getSortedIndex() { return sortedIndex; } - } diff --git a/Gui/src/main/java/mezz/jei/gui/startup/JeiGuiStarter.java b/Gui/src/main/java/mezz/jei/gui/startup/JeiGuiStarter.java index 7da2605e3..594c9e5c9 100644 --- a/Gui/src/main/java/mezz/jei/gui/startup/JeiGuiStarter.java +++ b/Gui/src/main/java/mezz/jei/gui/startup/JeiGuiStarter.java @@ -55,11 +55,13 @@ import org.apache.logging.log4j.Logger; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; public class JeiGuiStarter { private static final Logger LOGGER = LogManager.getLogger(); - public static JeiEventHandlers start(IRuntimeRegistration registration) { + public static CompletableFuture start(IRuntimeRegistration registration, Executor clientExecutor) { LOGGER.info("Starting JEI GUI"); LoggedTimer timer = new LoggedTimer(); @@ -111,112 +113,118 @@ public static JeiEventHandlers start(IRuntimeRegistration registration) { ingredientFilterConfig, ingredientManager, ingredientSorter, - ingredientList, modIdHelper, ingredientVisibility, colorHelper ); - ingredientManager.registerIngredientListener(ingredientFilter); - ingredientVisibility.registerListener(ingredientFilter::onIngredientVisibilityChanged); timer.stop(); - IIngredientFilter ingredientFilterApi = new IngredientFilterApi(ingredientFilter, filterTextSource); - registration.setIngredientFilter(ingredientFilterApi); - - CheatUtil cheatUtil = new CheatUtil(ingredientManager); - IngredientListOverlay ingredientListOverlay = OverlayHelper.createIngredientListOverlay( - ingredientManager, - screenHelper, - ingredientFilter, - filterTextSource, - modIdHelper, - keyMappings, - ingredientListConfig, - clientConfig, - toggleState, - editModeConfig, - serverConnection, - ingredientFilterConfig, - textures, - colorHelper, - cheatUtil - ); - registration.setIngredientListOverlay(ingredientListOverlay); - - BookmarkList bookmarkList = new BookmarkList(ingredientManager, bookmarkConfig, clientConfig); - bookmarkConfig.loadBookmarks(ingredientManager, bookmarkList); - - BookmarkOverlay bookmarkOverlay = OverlayHelper.createBookmarkOverlay( - ingredientManager, - screenHelper, - bookmarkList, - modIdHelper, - keyMappings, - bookmarkListConfig, - editModeConfig, - ingredientFilterConfig, - clientConfig, - toggleState, - serverConnection, - textures, - colorHelper, - cheatUtil - ); - registration.setBookmarkOverlay(bookmarkOverlay); - - GuiEventHandler guiEventHandler = new GuiEventHandler( - screenHelper, - bookmarkOverlay, - ingredientListOverlay - ); - - RecipesGui recipesGui = new RecipesGui( - recipeManager, - recipeTransferManager, - ingredientManager, - modIdHelper, - clientConfig, - textures, - keyMappings, - focusFactory - ); - registration.setRecipesGui(recipesGui); - - CombinedRecipeFocusSource recipeFocusSource = new CombinedRecipeFocusSource( - recipesGui, - ingredientListOverlay, - bookmarkOverlay, - new GuiContainerWrapper(screenHelper) - ); - - List charTypedHandlers = List.of( - ingredientListOverlay - ); - - UserInputRouter userInputRouter = new UserInputRouter( - new EditInputHandler(recipeFocusSource, toggleState, editModeConfig), - ingredientListOverlay.createInputHandler(), - bookmarkOverlay.createInputHandler(), - new FocusInputHandler(recipeFocusSource, recipesGui, focusFactory, clientConfig, ingredientManager), - new BookmarkInputHandler(recipeFocusSource, bookmarkList), - new GlobalInputHandler(toggleState), - new GuiAreaInputHandler(screenHelper, recipesGui, focusFactory) - ); - - DragRouter dragRouter = new DragRouter( - ingredientListOverlay.createDragHandler(), - bookmarkOverlay.createDragHandler() - ); - ClientInputHandler clientInputHandler = new ClientInputHandler( - charTypedHandlers, - userInputRouter, - dragRouter, - keyMappings - ); - - return new JeiEventHandlers( - guiEventHandler, - clientInputHandler - ); + timer.start("Adding ingredients"); + return ingredientFilter.addIngredientsAsync(ingredientList, clientExecutor) + .thenApplyAsync((v) -> { + timer.stop(); + + ingredientManager.registerIngredientListener(ingredientFilter); + ingredientVisibility.registerListener(ingredientFilter::onIngredientVisibilityChanged); + + IIngredientFilter ingredientFilterApi = new IngredientFilterApi(ingredientFilter, filterTextSource); + registration.setIngredientFilter(ingredientFilterApi); + + CheatUtil cheatUtil = new CheatUtil(ingredientManager); + IngredientListOverlay ingredientListOverlay = OverlayHelper.createIngredientListOverlay( + ingredientManager, + screenHelper, + ingredientFilter, + filterTextSource, + modIdHelper, + keyMappings, + ingredientListConfig, + clientConfig, + toggleState, + editModeConfig, + serverConnection, + ingredientFilterConfig, + textures, + colorHelper, + cheatUtil + ); + registration.setIngredientListOverlay(ingredientListOverlay); + + BookmarkList bookmarkList = new BookmarkList(ingredientManager, bookmarkConfig, clientConfig); + bookmarkConfig.loadBookmarks(ingredientManager, bookmarkList); + + BookmarkOverlay bookmarkOverlay = OverlayHelper.createBookmarkOverlay( + ingredientManager, + screenHelper, + bookmarkList, + modIdHelper, + keyMappings, + bookmarkListConfig, + editModeConfig, + ingredientFilterConfig, + clientConfig, + toggleState, + serverConnection, + textures, + colorHelper, + cheatUtil + ); + registration.setBookmarkOverlay(bookmarkOverlay); + + GuiEventHandler guiEventHandler = new GuiEventHandler( + screenHelper, + bookmarkOverlay, + ingredientListOverlay + ); + + RecipesGui recipesGui = new RecipesGui( + recipeManager, + recipeTransferManager, + ingredientManager, + modIdHelper, + clientConfig, + textures, + keyMappings, + focusFactory + ); + registration.setRecipesGui(recipesGui); + + CombinedRecipeFocusSource recipeFocusSource = new CombinedRecipeFocusSource( + recipesGui, + ingredientListOverlay, + bookmarkOverlay, + new GuiContainerWrapper(screenHelper) + ); + + List charTypedHandlers = List.of( + ingredientListOverlay + ); + + UserInputRouter userInputRouter = new UserInputRouter( + new EditInputHandler(recipeFocusSource, toggleState, editModeConfig), + ingredientListOverlay.createInputHandler(), + bookmarkOverlay.createInputHandler(), + new FocusInputHandler(recipeFocusSource, recipesGui, focusFactory, clientConfig, ingredientManager), + new BookmarkInputHandler(recipeFocusSource, bookmarkList), + new GlobalInputHandler(toggleState), + new GuiAreaInputHandler(screenHelper, recipesGui, focusFactory) + ); + + DragRouter dragRouter = new DragRouter( + ingredientListOverlay.createDragHandler(), + bookmarkOverlay.createDragHandler() + ); + ClientInputHandler clientInputHandler = new ClientInputHandler( + charTypedHandlers, + userInputRouter, + dragRouter, + keyMappings + ); + + return new JeiEventHandlers( + guiEventHandler, + clientInputHandler + ); + }, clientExecutor); } } diff --git a/Library/src/main/java/mezz/jei/library/ingredients/IngredientManager.java b/Library/src/main/java/mezz/jei/library/ingredients/IngredientManager.java index 7670e9c1f..789aa1b10 100644 --- a/Library/src/main/java/mezz/jei/library/ingredients/IngredientManager.java +++ b/Library/src/main/java/mezz/jei/library/ingredients/IngredientManager.java @@ -73,11 +73,9 @@ public Collection> getRegisteredIngredientTypes() { } @Override - public void addIngredientsAtRuntime(IIngredientType ingredientType, Collection ingredients) { - ErrorUtil.assertMainThread(); + public synchronized void addIngredientsAtRuntime(IIngredientType ingredientType, Collection ingredients) { ErrorUtil.checkNotNull(ingredientType, "ingredientType"); ErrorUtil.checkNotEmpty(ingredients, "ingredients"); - IngredientInfo ingredientInfo = this.registeredIngredients.getIngredientInfo(ingredientType); LOGGER.info("Ingredients are being added at runtime: {} {}", ingredients.size(), ingredientType.getIngredientClass().getName()); @@ -119,11 +117,9 @@ public Optional> getIngredientTypeChecked(Class void removeIngredientsAtRuntime(IIngredientType ingredientType, Collection ingredients) { - ErrorUtil.assertMainThread(); + public synchronized void removeIngredientsAtRuntime(IIngredientType ingredientType, Collection ingredients) { ErrorUtil.checkNotNull(ingredientType, "ingredientType"); ErrorUtil.checkNotEmpty(ingredients, "ingredients"); - IngredientInfo ingredientInfo = this.registeredIngredients.getIngredientInfo(ingredientType); LOGGER.info("Ingredients are being removed at runtime: {} {}", ingredients.size(), ingredientType.getIngredientClass().getName()); diff --git a/Library/src/main/java/mezz/jei/library/load/PluginCaller.java b/Library/src/main/java/mezz/jei/library/load/PluginCaller.java index 7fb0605bc..cd1dd7888 100644 --- a/Library/src/main/java/mezz/jei/library/load/PluginCaller.java +++ b/Library/src/main/java/mezz/jei/library/load/PluginCaller.java @@ -2,36 +2,101 @@ import com.google.common.base.Stopwatch; import mezz.jei.api.IModPlugin; +import mezz.jei.api.IRuntimePlugin; +import mezz.jei.common.async.JeiStartTask; +import mezz.jei.library.startup.ClientTaskExecutor; import net.minecraft.resources.ResourceLocation; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; public class PluginCaller { private static final Logger LOGGER = LogManager.getLogger(); + private final List plugins; + private final IRuntimePlugin runtimePlugin; + private final ClientTaskExecutor clientExecutor; - public static void callOnPlugins(String title, List plugins, Consumer func) { + public PluginCaller( + List plugins, + IRuntimePlugin runtimePlugin, + ClientTaskExecutor clientExecutor + ) { + this.plugins = plugins; + this.runtimePlugin = runtimePlugin; + this.clientExecutor = clientExecutor; + } + + private void callSync( + String title, + PluginCallerTimer timer, + List plugins, + Function uidFunc, + Consumer func + ) { + Set erroredPlugins = ConcurrentHashMap.newKeySet(); + Stream runnables = plugins.stream() + .map(plugin -> { + return () -> { + try { + ResourceLocation pluginUid = uidFunc.apply(plugin); + try (var ignored = timer.begin(title, pluginUid)) { + clientExecutor.runAsync(() -> func.accept(plugin)); + } catch (RuntimeException | LinkageError e) { + LOGGER.error("Caught an error from mod plugin: {} {}", plugin.getClass(), pluginUid, e); + erroredPlugins.add(plugin); + } + } catch (RuntimeException e) { + LOGGER.error("Caught an error from mod plugin: {}", plugin.getClass(), e); + erroredPlugins.add(plugin); + } + }; + }); + + clientExecutor.runAsync(runnables); + + plugins.removeAll(erroredPlugins); + } + + public void callOnPlugins( + String title, + Consumer func + ) { + JeiStartTask.interruptIfCanceled(); + LOGGER.info("{}...", title); + Stopwatch stopwatch = Stopwatch.createStarted(); + + try (PluginCallerTimer timer = new PluginCallerTimer()) { + callSync( + title, + timer, + plugins, + IModPlugin::getPluginUid, + func + ); + } + + LOGGER.info("{} took {}", title, stopwatch); + } + + public void callOnRuntimePlugin( + String title, + Function> asyncFun + ) { LOGGER.info("{}...", title); Stopwatch stopwatch = Stopwatch.createStarted(); try (PluginCallerTimer timer = new PluginCallerTimer()) { - List erroredPlugins = new ArrayList<>(); - - for (IModPlugin plugin : plugins) { - try { - ResourceLocation pluginUid = plugin.getPluginUid(); - timer.begin(title, pluginUid); - func.accept(plugin); - timer.end(); - } catch (RuntimeException | LinkageError e) { - LOGGER.error("Caught an error from mod plugin: {} {}", plugin.getClass(), plugin.getPluginUid(), e); - erroredPlugins.add(plugin); - } + ResourceLocation pluginUid = runtimePlugin.getPluginUid(); + try (var ignored = timer.begin(title, pluginUid)) { + clientExecutor.runAsync(() -> asyncFun.apply(runtimePlugin)); } - plugins.removeAll(erroredPlugins); } LOGGER.info("{} took {}", title, stopwatch); diff --git a/Library/src/main/java/mezz/jei/library/load/PluginCallerTimer.java b/Library/src/main/java/mezz/jei/library/load/PluginCallerTimer.java index 1d71247af..cc70e2b38 100644 --- a/Library/src/main/java/mezz/jei/library/load/PluginCallerTimer.java +++ b/Library/src/main/java/mezz/jei/library/load/PluginCallerTimer.java @@ -1,15 +1,16 @@ package mezz.jei.library.load; import net.minecraft.resources.ResourceLocation; -import org.jetbrains.annotations.Nullable; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class PluginCallerTimer implements AutoCloseable { private final ScheduledExecutorService executor; - private @Nullable PluginCallerTimerRunnable runnable; + private final Set refs = ConcurrentHashMap.newKeySet(); public PluginCallerTimer() { this.executor = Executors.newSingleThreadScheduledExecutor(); @@ -17,24 +18,39 @@ public PluginCallerTimer() { } private synchronized void run() { - if (this.runnable != null) { - this.runnable.check(); - } + refs.stream() + .map(r -> r.runnable) + .forEach(PluginCallerTimerRunnable::check); } - public synchronized void begin(String title, ResourceLocation pluginUid) { - this.runnable = new PluginCallerTimerRunnable(title, pluginUid); + public synchronized Ref begin(String title, ResourceLocation pluginUid) { + PluginCallerTimerRunnable runnable = new PluginCallerTimerRunnable(title, pluginUid); + Ref ref = new Ref(runnable); + refs.add(ref); + return ref; } - public synchronized void end() { - if (this.runnable != null) { - this.runnable.stop(); - this.runnable = null; - } + private synchronized boolean end(Ref ref) { + return refs.remove(ref); } @Override - public void close() { + public synchronized void close() { this.executor.shutdown(); } + + public final class Ref implements AutoCloseable { + public final PluginCallerTimerRunnable runnable; + + public Ref(PluginCallerTimerRunnable runnable) { + this.runnable = runnable; + } + + @Override + public void close() { + if (end(this)) { + this.runnable.stop(); + } + } + } } diff --git a/Library/src/main/java/mezz/jei/library/load/PluginCallerTimerRunnable.java b/Library/src/main/java/mezz/jei/library/load/PluginCallerTimerRunnable.java index 34ab9a35e..219a817c9 100644 --- a/Library/src/main/java/mezz/jei/library/load/PluginCallerTimerRunnable.java +++ b/Library/src/main/java/mezz/jei/library/load/PluginCallerTimerRunnable.java @@ -1,5 +1,6 @@ package mezz.jei.library.load; +import mezz.jei.common.config.DebugConfig; import net.minecraft.resources.ResourceLocation; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -40,7 +41,7 @@ public void check() { public void stop() { Duration elapsed = Duration.ofNanos(System.nanoTime() - this.startTime); - if (elapsed.toMillis() > startReportDurationMs) { + if (elapsed.toMillis() > startReportDurationMs || DebugConfig.isDebugModeEnabled()) { LOGGER.info("{}: {} took {}", title, pluginUid, toHumanString(elapsed)); } } diff --git a/Library/src/main/java/mezz/jei/library/load/PluginHelper.java b/Library/src/main/java/mezz/jei/library/load/PluginHelper.java index 97381fe20..ece9b5b3e 100644 --- a/Library/src/main/java/mezz/jei/library/load/PluginHelper.java +++ b/Library/src/main/java/mezz/jei/library/load/PluginHelper.java @@ -19,13 +19,10 @@ public static void sortPlugins(List plugins, VanillaPlugin vanillaPl } } - public static Optional getPluginWithClass(Class pluginClass, List modPlugins) { - for (IModPlugin modPlugin : modPlugins) { - if (pluginClass.isInstance(modPlugin)) { - T cast = pluginClass.cast(modPlugin); - return Optional.of(cast); - } - } - return Optional.empty(); + public static Optional getPluginWithClass(Class pluginClass, List modPlugins) { + return modPlugins.stream() + .filter(pluginClass::isInstance) + .map(pluginClass::cast) + .findFirst(); } } diff --git a/Library/src/main/java/mezz/jei/library/load/PluginLoader.java b/Library/src/main/java/mezz/jei/library/load/PluginLoader.java index 7a914783b..8346f09e1 100644 --- a/Library/src/main/java/mezz/jei/library/load/PluginLoader.java +++ b/Library/src/main/java/mezz/jei/library/load/PluginLoader.java @@ -2,7 +2,6 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableTable; -import mezz.jei.api.IModPlugin; import mezz.jei.api.helpers.IColorHelper; import mezz.jei.api.helpers.IJeiHelpers; import mezz.jei.api.helpers.IModIdHelper; @@ -18,10 +17,11 @@ import mezz.jei.api.runtime.IScreenHelper; import mezz.jei.common.Internal; import mezz.jei.common.gui.textures.Textures; +import mezz.jei.common.network.IConnectionToServer; import mezz.jei.common.platform.IPlatformFluidHelperInternal; import mezz.jei.common.platform.Services; -import mezz.jei.core.util.LoggedTimer; import mezz.jei.common.util.StackHelper; +import mezz.jei.core.util.LoggedTimer; import mezz.jei.library.config.IModIdFormatConfig; import mezz.jei.library.config.RecipeCategorySortingConfig; import mezz.jei.library.focus.FocusFactory; @@ -44,7 +44,7 @@ import mezz.jei.library.recipes.RecipeManager; import mezz.jei.library.recipes.RecipeManagerInternal; import mezz.jei.library.runtime.JeiHelpers; -import mezz.jei.library.startup.StartData; +import mezz.jei.library.startup.ClientTaskExecutor; import mezz.jei.library.transfer.RecipeTransferHandlerHelper; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.inventory.AbstractContainerMenu; @@ -53,27 +53,43 @@ import java.util.List; public class PluginLoader { - private final StartData data; + private final IConnectionToServer serverConnection; + private final PluginCaller pluginCaller; + private final ClientTaskExecutor clientExecutor; private final LoggedTimer timer; private final IIngredientManager ingredientManager; private final JeiHelpers jeiHelpers; - public PluginLoader(StartData data, IModIdFormatConfig modIdFormatConfig, IColorHelper colorHelper) { - this.data = data; + public PluginLoader( + IConnectionToServer serverConnection, + PluginCaller pluginCaller, + IModIdFormatConfig modIdFormatConfig, + IColorHelper colorHelper, + ClientTaskExecutor clientExecutor + ) { + this.serverConnection = serverConnection; + this.pluginCaller = pluginCaller; + this.clientExecutor = clientExecutor; this.timer = new LoggedTimer(); IPlatformFluidHelperInternal fluidHelper = Services.PLATFORM.getFluidHelper(); - List plugins = data.plugins(); SubtypeRegistration subtypeRegistration = new SubtypeRegistration(); - PluginCaller.callOnPlugins("Registering item subtypes", plugins, p -> p.registerItemSubtypes(subtypeRegistration)); - PluginCaller.callOnPlugins("Registering fluid subtypes", plugins, p -> - p.registerFluidSubtypes(subtypeRegistration, fluidHelper) + pluginCaller.callOnPlugins( + "Registering item subtypes", + p -> p.registerItemSubtypes(subtypeRegistration) + ); + pluginCaller.callOnPlugins( + "Registering fluid subtypes", + p -> p.registerFluidSubtypes(subtypeRegistration, fluidHelper) ); SubtypeInterpreters subtypeInterpreters = subtypeRegistration.getInterpreters(); SubtypeManager subtypeManager = new SubtypeManager(subtypeInterpreters); IngredientManagerBuilder ingredientManagerBuilder = new IngredientManagerBuilder(subtypeManager, colorHelper); - PluginCaller.callOnPlugins("Registering ingredients", plugins, p -> p.registerIngredients(ingredientManagerBuilder)); + pluginCaller.callOnPlugins( + "Registering ingredients", + p -> p.registerIngredients(ingredientManagerBuilder) + ); this.ingredientManager = ingredientManagerBuilder.build(); StackHelper stackHelper = new StackHelper(subtypeManager); @@ -84,45 +100,62 @@ public PluginLoader(StartData data, IModIdFormatConfig modIdFormatConfig, IColor } @Unmodifiable - private List> createRecipeCategories(List plugins, VanillaPlugin vanillaPlugin) { + private List> createRecipeCategories(VanillaPlugin vanillaPlugin) { RecipeCategoryRegistration recipeCategoryRegistration = new RecipeCategoryRegistration(jeiHelpers); - PluginCaller.callOnPlugins("Registering categories", plugins, p -> p.registerCategories(recipeCategoryRegistration)); + pluginCaller.callOnPlugins( + "Registering categories", + p -> p.registerCategories(recipeCategoryRegistration) + ); CraftingRecipeCategory craftingCategory = vanillaPlugin.getCraftingCategory() .orElseThrow(() -> new NullPointerException("vanilla crafting category")); VanillaCategoryExtensionRegistration vanillaCategoryExtensionRegistration = new VanillaCategoryExtensionRegistration(craftingCategory, jeiHelpers); - PluginCaller.callOnPlugins("Registering vanilla category extensions", plugins, p -> p.registerVanillaCategoryExtensions(vanillaCategoryExtensionRegistration)); + pluginCaller.callOnPlugins( + "Registering vanilla category extensions", + p -> p.registerVanillaCategoryExtensions(vanillaCategoryExtensionRegistration) + ); return recipeCategoryRegistration.getRecipeCategories(); } - public IScreenHelper createGuiScreenHelper(List plugins, IJeiHelpers jeiHelpers) { + public IScreenHelper createGuiScreenHelper(IJeiHelpers jeiHelpers) { GuiHandlerRegistration guiHandlerRegistration = new GuiHandlerRegistration(jeiHelpers); - PluginCaller.callOnPlugins("Registering gui handlers", plugins, p -> p.registerGuiHandlers(guiHandlerRegistration)); + pluginCaller.callOnPlugins( + "Registering gui handlers", + p -> p.registerGuiHandlers(guiHandlerRegistration) + ); return guiHandlerRegistration.createGuiScreenHelper(ingredientManager); } - public ImmutableTable, RecipeType, IRecipeTransferHandler> createRecipeTransferHandlers(List plugins) { + public ImmutableTable, RecipeType, IRecipeTransferHandler> createRecipeTransferHandlers() { IStackHelper stackHelper = jeiHelpers.getStackHelper(); IRecipeTransferHandlerHelper handlerHelper = new RecipeTransferHandlerHelper(stackHelper); - RecipeTransferRegistration recipeTransferRegistration = new RecipeTransferRegistration(stackHelper, handlerHelper, this.jeiHelpers, data.serverConnection()); - PluginCaller.callOnPlugins("Registering recipes transfer handlers", plugins, p -> p.registerRecipeTransferHandlers(recipeTransferRegistration)); + RecipeTransferRegistration recipeTransferRegistration = new RecipeTransferRegistration(stackHelper, handlerHelper, jeiHelpers, serverConnection); + pluginCaller.callOnPlugins( + "Registering recipes transfer handlers", + p -> p.registerRecipeTransferHandlers(recipeTransferRegistration) + ); return recipeTransferRegistration.getRecipeTransferHandlers(); } public RecipeManager createRecipeManager( - List plugins, VanillaPlugin vanillaPlugin, RecipeCategorySortingConfig recipeCategorySortingConfig, IModIdHelper modIdHelper, IIngredientVisibility ingredientVisibility ) { - List> recipeCategories = createRecipeCategories(plugins, vanillaPlugin); + List> recipeCategories = createRecipeCategories(vanillaPlugin); RecipeCatalystRegistration recipeCatalystRegistration = new RecipeCatalystRegistration(ingredientManager, jeiHelpers); - PluginCaller.callOnPlugins("Registering recipe catalysts", plugins, p -> p.registerRecipeCatalysts(recipeCatalystRegistration)); + pluginCaller.callOnPlugins( + "Registering recipe catalysts", + p -> p.registerRecipeCatalysts(recipeCatalystRegistration) + ); ImmutableListMultimap> recipeCatalysts = recipeCatalystRegistration.getRecipeCatalysts(); AdvancedRegistration advancedRegistration = new AdvancedRegistration(jeiHelpers); - PluginCaller.callOnPlugins("Registering advanced plugins", plugins, p -> p.registerAdvanced(advancedRegistration)); + pluginCaller.callOnPlugins( + "Registering advanced plugins", + p -> p.registerAdvanced(advancedRegistration) + ); List recipeManagerPlugins = advancedRegistration.getRecipeManagerPlugins(); timer.start("Building recipe registry"); @@ -138,10 +171,13 @@ public RecipeManager createRecipeManager( VanillaRecipeFactory vanillaRecipeFactory = new VanillaRecipeFactory(ingredientManager); RecipeRegistration recipeRegistration = new RecipeRegistration(jeiHelpers, ingredientManager, ingredientVisibility, vanillaRecipeFactory, recipeManagerInternal); - PluginCaller.callOnPlugins("Registering recipes", plugins, p -> p.registerRecipes(recipeRegistration)); + pluginCaller.callOnPlugins( + "Registering recipes", + p -> p.registerRecipes(recipeRegistration) + ); Textures textures = Internal.getTextures(); - return new RecipeManager(recipeManagerInternal, modIdHelper, ingredientManager, textures, ingredientVisibility); + return new RecipeManager(recipeManagerInternal, modIdHelper, ingredientManager, textures, ingredientVisibility, clientExecutor); } public IIngredientManager getIngredientManager() { diff --git a/Library/src/main/java/mezz/jei/library/plugins/debug/JeiDebugPlugin.java b/Library/src/main/java/mezz/jei/library/plugins/debug/JeiDebugPlugin.java index 1f02f1c7a..d1df62074 100644 --- a/Library/src/main/java/mezz/jei/library/plugins/debug/JeiDebugPlugin.java +++ b/Library/src/main/java/mezz/jei/library/plugins/debug/JeiDebugPlugin.java @@ -23,7 +23,6 @@ import mezz.jei.common.platform.IPlatformRegistry; import mezz.jei.common.platform.IPlatformScreenHelper; import mezz.jei.common.platform.Services; -import mezz.jei.common.util.ErrorUtil; import mezz.jei.common.util.MathUtil; import mezz.jei.library.plugins.jei.ingredients.DebugIngredient; import mezz.jei.library.plugins.jei.ingredients.DebugIngredientHelper; @@ -236,7 +235,6 @@ private void registerRecipeCatalysts(IRecipeCatalystRegistration registratio @Override public void onRuntimeAvailable(IJeiRuntime jeiRuntime) { if (DebugConfig.isDebugModeEnabled()) { - ErrorUtil.assertMainThread(); if (debugRecipeCategory != null) { debugRecipeCategory.setRuntime(jeiRuntime); } diff --git a/Library/src/main/java/mezz/jei/library/recipes/RecipeManager.java b/Library/src/main/java/mezz/jei/library/recipes/RecipeManager.java index da27011c7..0a505492e 100644 --- a/Library/src/main/java/mezz/jei/library/recipes/RecipeManager.java +++ b/Library/src/main/java/mezz/jei/library/recipes/RecipeManager.java @@ -18,6 +18,7 @@ import mezz.jei.common.util.ErrorUtil; import mezz.jei.library.gui.ingredients.RecipeSlot; import mezz.jei.library.gui.recipes.RecipeLayout; +import mezz.jei.library.startup.ClientTaskExecutor; import net.minecraft.resources.ResourceLocation; import java.util.Collection; @@ -31,13 +32,22 @@ public class RecipeManager implements IRecipeManager { private final IIngredientManager ingredientManager; private final Textures textures; private final IIngredientVisibility ingredientVisibility; - - public RecipeManager(RecipeManagerInternal internal, IModIdHelper modIdHelper, IIngredientManager ingredientManager, Textures textures, IIngredientVisibility ingredientVisibility) { + private final ClientTaskExecutor clientExecutor; + + public RecipeManager( + RecipeManagerInternal internal, + IModIdHelper modIdHelper, + IIngredientManager ingredientManager, + Textures textures, + IIngredientVisibility ingredientVisibility, + ClientTaskExecutor clientExecutor + ) { this.internal = internal; this.modIdHelper = modIdHelper; this.ingredientManager = ingredientManager; this.textures = textures; this.ingredientVisibility = ingredientVisibility; + this.clientExecutor = clientExecutor; } @Override @@ -60,9 +70,7 @@ public IRecipeCatalystLookup createRecipeCatalystLookup(RecipeType recipeType public void addRecipes(RecipeType recipeType, List recipes) { ErrorUtil.checkNotNull(recipeType, "recipeType"); ErrorUtil.checkNotNull(recipes, "recipes"); - ErrorUtil.assertMainThread(); - - internal.addRecipes(recipeType, recipes); + clientExecutor.runAsync(() -> internal.addRecipes(recipeType, recipes)); } @Override @@ -92,30 +100,26 @@ public IRecipeSlotDrawable createRecipeSlotDrawable(RecipeIngredientRole role, L public void hideRecipes(RecipeType recipeType, Collection recipes) { ErrorUtil.checkNotNull(recipes, "recipe"); ErrorUtil.checkNotNull(recipeType, "recipeType"); - ErrorUtil.assertMainThread(); - internal.hideRecipes(recipeType, recipes); + clientExecutor.runAsync(() -> internal.hideRecipes(recipeType, recipes)); } @Override public void unhideRecipes(RecipeType recipeType, Collection recipes) { ErrorUtil.checkNotNull(recipes, "recipe"); ErrorUtil.checkNotNull(recipeType, "recipeType"); - ErrorUtil.assertMainThread(); - internal.unhideRecipes(recipeType, recipes); + clientExecutor.runAsync(() -> internal.unhideRecipes(recipeType, recipes)); } @Override public void hideRecipeCategory(RecipeType recipeType) { ErrorUtil.checkNotNull(recipeType, "recipeType"); - ErrorUtil.assertMainThread(); - internal.hideRecipeCategory(recipeType); + clientExecutor.runAsync(() -> internal.hideRecipeCategory(recipeType)); } @Override public void unhideRecipeCategory(RecipeType recipeType) { ErrorUtil.checkNotNull(recipeType, "recipeType"); - ErrorUtil.assertMainThread(); - internal.unhideRecipeCategory(recipeType); + clientExecutor.runAsync(() -> internal.unhideRecipeCategory(recipeType)); } @Override diff --git a/Library/src/main/java/mezz/jei/library/recipes/collect/RecipeTypeDataMap.java b/Library/src/main/java/mezz/jei/library/recipes/collect/RecipeTypeDataMap.java index 164f2ea07..7a49dacb1 100644 --- a/Library/src/main/java/mezz/jei/library/recipes/collect/RecipeTypeDataMap.java +++ b/Library/src/main/java/mezz/jei/library/recipes/collect/RecipeTypeDataMap.java @@ -7,11 +7,13 @@ import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.Unmodifiable; +import javax.annotation.concurrent.ThreadSafe; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +@ThreadSafe public class RecipeTypeDataMap { @Unmodifiable private final Map> uidMap; diff --git a/Library/src/main/java/mezz/jei/library/startup/ClientTaskExecutor.java b/Library/src/main/java/mezz/jei/library/startup/ClientTaskExecutor.java new file mode 100644 index 000000000..fdfeef020 --- /dev/null +++ b/Library/src/main/java/mezz/jei/library/startup/ClientTaskExecutor.java @@ -0,0 +1,85 @@ +package mezz.jei.library.startup; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.Minecraft; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public final class ClientTaskExecutor { + private final InternalExecutor executor = new InternalExecutor(); + + public void tick() { + executor.tick(); + } + + public void runAsync(Stream runnables) { + Stream> futures = runnables.map(r -> CompletableFuture.runAsync(r, executor)); + CompletableFuture future = CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)); + join(future); + } + + public void runAsync(Runnable runnable) { + CompletableFuture future = CompletableFuture.runAsync(runnable, executor); + join(future); + } + + public void runAsync(Supplier> supplier) { + CompletableFuture future = CompletableFuture.supplyAsync(supplier, executor) + .thenCompose(f -> f); + join(future); + } + + @SuppressWarnings("UnusedReturnValue") + private T join(CompletableFuture future) { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.isSameThread()) { + minecraft.managedBlock(() -> { + if (future.isDone()) { + return true; + } + tick(); + return false; + }); + } + return future.join(); + } + + public InternalExecutor getExecutor() { + return executor; + } + + private static final class InternalExecutor implements Executor { + private static final long TICK_BUDGET = TimeUnit.MILLISECONDS.toNanos(2); + + private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>(); + + public void tick() { + final long startTime = System.nanoTime(); + do { + Runnable r = this.taskQueue.poll(); + if (r != null) { + r.run(); + } else { + return; + } + } while ((System.nanoTime() - startTime) < TICK_BUDGET); + } + + @Override + public void execute(Runnable runnable) { + if (RenderSystem.isOnRenderThreadOrInit()) { + // we can't queue on the client render thread, + // it would block forever waiting for the next tick to happen + runnable.run(); + } else { + taskQueue.add(runnable); + } + } + } + +} diff --git a/Library/src/main/java/mezz/jei/library/startup/IPluginFinder.java b/Library/src/main/java/mezz/jei/library/startup/IPluginFinder.java new file mode 100644 index 000000000..a501fbabd --- /dev/null +++ b/Library/src/main/java/mezz/jei/library/startup/IPluginFinder.java @@ -0,0 +1,7 @@ +package mezz.jei.library.startup; + +import java.util.List; + +public interface IPluginFinder { + List getPlugins(Class pluginClass); +} diff --git a/Library/src/main/java/mezz/jei/library/startup/JeiStarter.java b/Library/src/main/java/mezz/jei/library/startup/JeiStarter.java index 5b6cf1a41..0dc1f32dd 100644 --- a/Library/src/main/java/mezz/jei/library/startup/JeiStarter.java +++ b/Library/src/main/java/mezz/jei/library/startup/JeiStarter.java @@ -11,16 +11,17 @@ import mezz.jei.api.runtime.IIngredientVisibility; import mezz.jei.api.runtime.IScreenHelper; import mezz.jei.common.Internal; +import mezz.jei.common.async.JeiStartTask; import mezz.jei.common.config.ConfigManager; import mezz.jei.common.config.DebugConfig; +import mezz.jei.common.config.IClientConfig; import mezz.jei.common.config.IClientToggleState; import mezz.jei.common.config.JeiClientConfigs; +import mezz.jei.common.config.file.ConfigSchemaBuilder; import mezz.jei.common.config.file.FileWatcher; +import mezz.jei.common.config.file.IConfigSchemaBuilder; import mezz.jei.common.platform.Services; -import mezz.jei.common.util.ErrorUtil; import mezz.jei.core.util.LoggedTimer; -import mezz.jei.common.config.file.ConfigSchemaBuilder; -import mezz.jei.common.config.file.IConfigSchemaBuilder; import mezz.jei.library.color.ColorHelper; import mezz.jei.library.config.ColorNameConfig; import mezz.jei.library.config.EditModeConfig; @@ -50,7 +51,6 @@ public final class JeiStarter { private static final Logger LOGGER = LogManager.getLogger(); private final StartData data; - private final List plugins; private final VanillaPlugin vanillaPlugin; private final ModIdFormatConfig modIdFormatConfig; private final ColorNameConfig colorNameConfig; @@ -58,11 +58,15 @@ public final class JeiStarter { @SuppressWarnings("FieldCanBeLocal") private final FileWatcher fileWatcher = new FileWatcher("JEI Config File Watcher"); private final ConfigManager configManager; + private final ClientTaskExecutor clientExecutor; + private final PluginCaller pluginCaller; + private final JeiClientConfigs jeiClientConfigs; + + private JeiStartTask currentStartTask = null; public JeiStarter(StartData data) { - ErrorUtil.checkNotEmpty(data.plugins(), "plugins"); this.data = data; - this.plugins = data.plugins(); + List plugins = data.plugins(); this.vanillaPlugin = PluginHelper.getPluginWithClass(VanillaPlugin.class, plugins) .orElseThrow(() -> new IllegalStateException("vanilla plugin not found")); JeiInternalPlugin jeiInternalPlugin = PluginHelper.getPluginWithClass(JeiInternalPlugin.class, plugins) @@ -85,32 +89,64 @@ public JeiStarter(StartData data) { this.colorNameConfig = new ColorNameConfig(colorFileBuilder); colorFileBuilder.build().register(fileWatcher, configManager); - JeiClientConfigs jeiClientConfigs = new JeiClientConfigs(configDir.resolve("jei-client.ini")); - jeiClientConfigs.register(fileWatcher, configManager); + this.jeiClientConfigs = new JeiClientConfigs(configDir.resolve("jei-client.ini")); + this.jeiClientConfigs.register(fileWatcher, configManager); Internal.setJeiClientConfigs(jeiClientConfigs); fileWatcher.start(); this.recipeCategorySortingConfig = new RecipeCategorySortingConfig(configDir.resolve("recipe-category-sort-order.ini")); - PluginCaller.callOnPlugins("Sending ConfigManager", plugins, p -> p.onConfigManagerAvailable(configManager)); + this.clientExecutor = new ClientTaskExecutor(); + this.pluginCaller = new PluginCaller( + data.plugins(), + data.runtimePlugin(), + clientExecutor + ); + + pluginCaller.callOnPlugins( + "Sending ConfigManager", + p -> p.onConfigManagerAvailable(configManager) + ); } + /** + * Starts JEI, either synchronously or asynchronously depending on config. Should only be called from + * the main thread. + */ public void start() { + if (currentStartTask != null) { + LOGGER.error("JEI start requested but it is already starting."); + return; + } Minecraft minecraft = Minecraft.getInstance(); if (minecraft.level == null) { LOGGER.error("Failed to start JEI, there is no Minecraft client level."); return; } + IClientConfig clientConfig = jeiClientConfigs.getClientConfig(); + if (clientConfig.getAsyncLoadingEnabled()) { + currentStartTask = new JeiStartTask(this::doActualStart); + currentStartTask.start(); + } else { + doActualStart(); + } + } + + private void doActualStart() { LoggedTimer totalTime = new LoggedTimer(); - totalTime.start("Starting JEI"); + if (Thread.currentThread() instanceof JeiStartTask) { + totalTime.start("Starting JEI asynchronously"); + } else { + totalTime.start("Starting JEI synchronously"); + } IColorHelper colorHelper = new ColorHelper(colorNameConfig); IClientToggleState toggleState = Internal.getClientToggleState(); - PluginLoader pluginLoader = new PluginLoader(data, modIdFormatConfig, colorHelper); + PluginLoader pluginLoader = new PluginLoader(data.serverConnection(), pluginCaller, modIdFormatConfig, colorHelper, clientExecutor); JeiHelpers jeiHelpers = pluginLoader.getJeiHelpers(); IModIdHelper modIdHelper = jeiHelpers.getModIdHelper(); @@ -130,19 +166,18 @@ public void start() { ); RecipeManager recipeManager = pluginLoader.createRecipeManager( - plugins, vanillaPlugin, recipeCategorySortingConfig, modIdHelper, ingredientVisibility ); ImmutableTable, RecipeType, IRecipeTransferHandler> recipeTransferHandlers = - pluginLoader.createRecipeTransferHandlers(plugins); + pluginLoader.createRecipeTransferHandlers(); IRecipeTransferManager recipeTransferManager = new RecipeTransferManager(recipeTransferHandlers); LoggedTimer timer = new LoggedTimer(); timer.start("Building runtime"); - IScreenHelper screenHelper = pluginLoader.createGuiScreenHelper(plugins, jeiHelpers); + IScreenHelper screenHelper = pluginLoader.createGuiScreenHelper(jeiHelpers); RuntimeRegistration runtimeRegistration = new RuntimeRegistration( recipeManager, @@ -153,7 +188,15 @@ public void start() { recipeTransferManager, screenHelper ); - PluginCaller.callOnPlugins("Registering Runtime", plugins, p -> p.registerRuntime(runtimeRegistration)); + //noinspection removal + pluginCaller.callOnPlugins( + "Registering Runtime (legacy)", + p -> p.registerRuntime(runtimeRegistration) + ); + pluginCaller.callOnRuntimePlugin( + "Registering Runtime", + p -> p.registerRuntime(runtimeRegistration, clientExecutor.getExecutor()) + ); JeiRuntime jeiRuntime = new JeiRuntime( recipeManager, @@ -172,14 +215,36 @@ public void start() { ); timer.stop(); - PluginCaller.callOnPlugins("Sending Runtime", plugins, p -> p.onRuntimeAvailable(jeiRuntime)); + pluginCaller.callOnPlugins( + "Sending Runtime", + p -> p.onRuntimeAvailable(jeiRuntime) + ); + pluginCaller.callOnRuntimePlugin( + "Sending Runtime to Runtime Plugin", + p -> p.onRuntimeAvailable(jeiRuntime, clientExecutor.getExecutor()) + ); totalTime.stop(); } public void stop() { LOGGER.info("Stopping JEI"); - List plugins = data.plugins(); - PluginCaller.callOnPlugins("Sending Runtime Unavailable", plugins, IModPlugin::onRuntimeUnavailable); + if (currentStartTask != null) { + currentStartTask.cancelStart(); + Minecraft.getInstance().managedBlock(() -> !currentStartTask.isAlive()); + currentStartTask = null; + } + pluginCaller.callOnPlugins( + "Sending Runtime Unavailable", + IModPlugin::onRuntimeUnavailable + ); + pluginCaller.callOnRuntimePlugin( + "Sending Runtime Unavailable to Runtime Plugin", + p -> p.onRuntimeUnavailable(clientExecutor.getExecutor()) + ); + } + + public void tick() { + this.clientExecutor.tick(); } } diff --git a/Library/src/main/java/mezz/jei/library/startup/StartData.java b/Library/src/main/java/mezz/jei/library/startup/StartData.java index 64e6e16f6..e7da2ced5 100644 --- a/Library/src/main/java/mezz/jei/library/startup/StartData.java +++ b/Library/src/main/java/mezz/jei/library/startup/StartData.java @@ -1,14 +1,62 @@ package mezz.jei.library.startup; import mezz.jei.api.IModPlugin; +import mezz.jei.api.IRuntimePlugin; +import mezz.jei.api.constants.ModIds; import mezz.jei.common.input.IInternalKeyMappings; import mezz.jei.common.network.IConnectionToServer; +import net.minecraft.resources.ResourceLocation; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; public record StartData( List plugins, + IRuntimePlugin runtimePlugin, IConnectionToServer serverConnection, IInternalKeyMappings keyBindings ) { + private static final Logger LOGGER = LogManager.getLogger(); + + public static StartData create( + IPluginFinder pluginFinder, + IConnectionToServer connectionToServer, + IInternalKeyMappings keyBindings + ) { + List runtimePlugins = pluginFinder.getPlugins(IRuntimePlugin.class); + if (runtimePlugins.size() > 1) { + // Only one runtime plugin should be active at a time. + // If a mod has registered one, it gets priority over JEI's. + runtimePlugins = runtimePlugins.stream() + .filter(r -> !r.getPluginUid().getNamespace().equals(ModIds.JEI_ID)) + .sorted() + .toList(); + } + + IRuntimePlugin runtimePlugin = runtimePlugins.get(0); + if (runtimePlugins.size() > 1) { + LOGGER.warn( + """ + Multiple runtime plugins have been registered but only one can be used. + Chosen runtime plugin: {} + Ignored runtime plugins: [{}]""", + runtimePlugin.getPluginUid(), + runtimePlugins.stream() + .filter(r -> !Objects.equals(r, runtimePlugin)) + .map(r -> r.getPluginUid()) + .map(ResourceLocation::toString) + .collect(Collectors.joining(", ")) + ); + } + + return new StartData( + pluginFinder.getPlugins(IModPlugin.class), + runtimePlugin, + connectionToServer, + keyBindings + ); + } } diff --git a/gradle.properties b/gradle.properties index be2cfcbcf..ed16a989a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -38,7 +38,7 @@ curseHomepageUrl=https://www.curseforge.com/minecraft/mc-mods/jei jUnitVersion=5.8.2 # Version -specificationVersion=13.1.0 +specificationVersion=13.2.0 # Workaround for Spotless bug # https://github.com/diffplug/spotless/issues/834