浏览代码

Error Screen when Fabric API is not installed

Signed-off-by: shedaniel <daniel@shedaniel.me>
shedaniel 5 年之前
父节点
当前提交
4c51884bcd

+ 2 - 2
gradle.properties

@@ -1,4 +1,4 @@
-mod_version=4.1.5-unstable
+mod_version=4.1.6-unstable
 minecraft_version=20w14a
 yarn_version=20w14a+build.2
 fabricloader_version=0.7.9+build.190
@@ -6,7 +6,7 @@ cloth_events_version=2.0.2-unstable
 cloth_config_version=3.3.1-unstable
 modmenu_version=1.11.0+build.2
 fabric_api=0.5.7+build.314-1.16
-autoconfig1u=2.0
+autoconfig1u=2.0.1
 api_include=me.shedaniel.cloth:cloth-events,me.shedaniel.cloth:config-2,me.sargunvohra.mcmods:autoconfig1u,org.jetbrains:annotations
 api_exculde=
 #api_include=me.shedaniel.cloth:cloth-events,me.shedaniel.cloth:config-2,me.sargunvohra.mcmods:autoconfig1u,org.jetbrains:annotations,net.fabricmc.fabric-api:fabric-

+ 38 - 30
src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java

@@ -26,6 +26,7 @@ package me.shedaniel.rei;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import me.shedaniel.cloth.hooks.ClothClientHooks;
+import me.shedaniel.math.api.Executor;
 import me.shedaniel.rei.api.*;
 import me.shedaniel.rei.api.plugins.REIPluginV0;
 import me.shedaniel.rei.gui.ContainerScreenOverlay;
@@ -180,39 +181,46 @@ public class RoughlyEnoughItemsCore implements ClientModInitializer {
                 RoughlyEnoughItemsCore.LOGGER.error("REI plugin from " + modContainer.getMetadata().getId() + " is not loaded because it is too old!");
         }
         
-        ClientSidePacketRegistry.INSTANCE.register(RoughlyEnoughItemsNetwork.CREATE_ITEMS_MESSAGE_PACKET, (packetContext, packetByteBuf) -> {
-            ItemStack stack = packetByteBuf.readItemStack();
-            String player = packetByteBuf.readString(32767);
-            packetContext.getPlayer().addMessage(new LiteralText(I18n.translate("text.rei.cheat_items").replaceAll("\\{item_name}", SearchArgument.tryGetItemStackName(stack.copy())).replaceAll("\\{item_count}", stack.copy().getCount() + "").replaceAll("\\{player_name}", player)), false);
-        });
-        ClientSidePacketRegistry.INSTANCE.register(RoughlyEnoughItemsNetwork.NOT_ENOUGH_ITEMS_PACKET, (packetContext, packetByteBuf) -> {
-            Screen currentScreen = MinecraftClient.getInstance().currentScreen;
-            if (currentScreen instanceof CraftingScreen) {
-                RecipeBookWidget recipeBookGui = ((RecipeBookProvider) currentScreen).getRecipeBookWidget();
-                RecipeBookGhostSlots ghostSlots = ((RecipeBookGuiHooks) recipeBookGui).rei_getGhostSlots();
-                ghostSlots.reset();
-                
-                List<List<ItemStack>> input = Lists.newArrayList();
-                int mapSize = packetByteBuf.readInt();
-                for (int i = 0; i < mapSize; i++) {
-                    List<ItemStack> list = Lists.newArrayList();
-                    int count = packetByteBuf.readInt();
-                    for (int j = 0; j < count; j++) {
-                        list.add(packetByteBuf.readItemStack());
+        boolean networkingLoaded = FabricLoader.getInstance().isModLoaded("fabric-networking-v0");
+        if (!networkingLoaded) {
+            RoughlyEnoughItemsState.failedToLoad("Fabric API is not installed!", "https://www.curseforge.com/minecraft/mc-mods/fabric-api/files/all");
+            return;
+        }
+        Executor.run(() -> () -> {
+            ClientSidePacketRegistry.INSTANCE.register(RoughlyEnoughItemsNetwork.CREATE_ITEMS_MESSAGE_PACKET, (packetContext, packetByteBuf) -> {
+                ItemStack stack = packetByteBuf.readItemStack();
+                String player = packetByteBuf.readString(32767);
+                packetContext.getPlayer().addMessage(new LiteralText(I18n.translate("text.rei.cheat_items").replaceAll("\\{item_name}", SearchArgument.tryGetItemStackName(stack.copy())).replaceAll("\\{item_count}", stack.copy().getCount() + "").replaceAll("\\{player_name}", player)), false);
+            });
+            ClientSidePacketRegistry.INSTANCE.register(RoughlyEnoughItemsNetwork.NOT_ENOUGH_ITEMS_PACKET, (packetContext, packetByteBuf) -> {
+                Screen currentScreen = MinecraftClient.getInstance().currentScreen;
+                if (currentScreen instanceof CraftingScreen) {
+                    RecipeBookWidget recipeBookGui = ((RecipeBookProvider) currentScreen).getRecipeBookWidget();
+                    RecipeBookGhostSlots ghostSlots = ((RecipeBookGuiHooks) recipeBookGui).rei_getGhostSlots();
+                    ghostSlots.reset();
+                    
+                    List<List<ItemStack>> input = Lists.newArrayList();
+                    int mapSize = packetByteBuf.readInt();
+                    for (int i = 0; i < mapSize; i++) {
+                        List<ItemStack> list = Lists.newArrayList();
+                        int count = packetByteBuf.readInt();
+                        for (int j = 0; j < count; j++) {
+                            list.add(packetByteBuf.readItemStack());
+                        }
+                        input.add(list);
                     }
-                    input.add(list);
-                }
-                
-                ghostSlots.addSlot(Ingredient.ofItems(Items.STONE), 381203812, 12738291);
-                CraftingScreenHandler screenHandler = ((CraftingScreen) currentScreen).getScreenHandler();
-                for (int i = 0; i < input.size(); i++) {
-                    List<ItemStack> stacks = input.get(i);
-                    if (!stacks.isEmpty()) {
-                        Slot slot = screenHandler.getSlot(i + screenHandler.getCraftingResultSlotIndex() + 1);
-                        ghostSlots.addSlot(Ingredient.ofStacks(stacks.toArray(new ItemStack[0])), slot.x, slot.y);
+                    
+                    ghostSlots.addSlot(Ingredient.ofItems(Items.STONE), 381203812, 12738291);
+                    CraftingScreenHandler screenHandler = ((CraftingScreen) currentScreen).getScreenHandler();
+                    for (int i = 0; i < input.size(); i++) {
+                        List<ItemStack> stacks = input.get(i);
+                        if (!stacks.isEmpty()) {
+                            Slot slot = screenHandler.getSlot(i + screenHandler.getCraftingResultSlotIndex() + 1);
+                            ghostSlots.addSlot(Ingredient.ofStacks(stacks.toArray(new ItemStack[0])), slot.x, slot.y);
+                        }
                     }
                 }
-            }
+            });
         });
     }
     

+ 67 - 58
src/main/java/me/shedaniel/rei/RoughlyEnoughItemsNetwork.java

@@ -26,6 +26,7 @@ package me.shedaniel.rei;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import io.netty.buffer.Unpooled;
+import me.shedaniel.math.api.Executor;
 import me.shedaniel.rei.server.InputSlotCrafter;
 import net.fabricmc.api.ModInitializer;
 import net.fabricmc.fabric.api.network.ServerSidePacketRegistry;
@@ -39,6 +40,7 @@ import net.minecraft.server.network.ServerPlayerEntity;
 import net.minecraft.text.TranslatableText;
 import net.minecraft.util.Formatting;
 import net.minecraft.util.Identifier;
+
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
@@ -53,71 +55,78 @@ public class RoughlyEnoughItemsNetwork implements ModInitializer {
     
     @Override
     public void onInitialize() {
-        FabricLoader.getInstance().getEntrypoints("rei_containers", Runnable.class).forEach(Runnable::run);
-        ServerSidePacketRegistry.INSTANCE.register(DELETE_ITEMS_PACKET, (packetContext, packetByteBuf) -> {
-            ServerPlayerEntity player = (ServerPlayerEntity) packetContext.getPlayer();
-            if (player.getServer().getPermissionLevel(player.getGameProfile()) < player.getServer().getOpPermissionLevel()) {
-                player.addMessage(new TranslatableText("text.rei.no_permission_cheat").formatted(Formatting.RED), false);
-                return;
-            }
-            if (!player.inventory.getCursorStack().isEmpty())
-                player.inventory.setCursorStack(ItemStack.EMPTY);
-        });
-        ServerSidePacketRegistry.INSTANCE.register(CREATE_ITEMS_PACKET, (packetContext, packetByteBuf) -> {
-            ServerPlayerEntity player = (ServerPlayerEntity) packetContext.getPlayer();
-            if (player.getServer().getPermissionLevel(player.getGameProfile()) < player.getServer().getOpPermissionLevel()) {
-                player.addMessage(new TranslatableText("text.rei.no_permission_cheat").formatted(Formatting.RED), false);
-                return;
-            }
-            ItemStack stack = packetByteBuf.readItemStack();
-            if (player.inventory.insertStack(stack.copy())) {
-                ServerSidePacketRegistry.INSTANCE.sendToPlayer(player, RoughlyEnoughItemsNetwork.CREATE_ITEMS_MESSAGE_PACKET, new PacketByteBuf(Unpooled.buffer()).writeItemStack(stack.copy()).writeString(player.getEntityName(), 32767));
-            } else
-                player.addMessage(new TranslatableText("text.rei.failed_cheat_items"), false);
-        });
-        ServerSidePacketRegistry.INSTANCE.register(MOVE_ITEMS_PACKET, (packetContext, packetByteBuf) -> {
-            Identifier category = packetByteBuf.readIdentifier();
-            ServerPlayerEntity player = (ServerPlayerEntity) packetContext.getPlayer();
-            ScreenHandler screenHandler = player.currentScreenHandler;
-            PlayerScreenHandler playerScreenHandler = player.playerScreenHandler;
-            try {
-                boolean shift = packetByteBuf.readBoolean();
-                Map<Integer, List<ItemStack>> input = Maps.newHashMap();
-                int mapSize = packetByteBuf.readInt();
-                for (int i = 0; i < mapSize; i++) {
-                    List<ItemStack> list = Lists.newArrayList();
-                    int count = packetByteBuf.readInt();
-                    for (int j = 0; j < count; j++) {
-                        list.add(packetByteBuf.readItemStack());
-                    }
-                    input.put(i, list);
+        boolean loaded = FabricLoader.getInstance().isModLoaded("fabric-networking-v0");
+        if (!loaded) {
+            RoughlyEnoughItemsState.failedToLoad("Fabric API is not installed!", "https://www.curseforge.com/minecraft/mc-mods/fabric-api/files/all");
+            return;
+        }
+        Executor.run(() -> () -> {
+            FabricLoader.getInstance().getEntrypoints("rei_containers", Runnable.class).forEach(Runnable::run);
+            ServerSidePacketRegistry.INSTANCE.register(DELETE_ITEMS_PACKET, (packetContext, packetByteBuf) -> {
+                ServerPlayerEntity player = (ServerPlayerEntity) packetContext.getPlayer();
+                if (player.getServer().getPermissionLevel(player.getGameProfile()) < player.getServer().getOpPermissionLevel()) {
+                    player.addMessage(new TranslatableText("text.rei.no_permission_cheat").formatted(Formatting.RED), false);
+                    return;
                 }
+                if (!player.inventory.getCursorStack().isEmpty())
+                    player.inventory.setCursorStack(ItemStack.EMPTY);
+            });
+            ServerSidePacketRegistry.INSTANCE.register(CREATE_ITEMS_PACKET, (packetContext, packetByteBuf) -> {
+                ServerPlayerEntity player = (ServerPlayerEntity) packetContext.getPlayer();
+                if (player.getServer().getPermissionLevel(player.getGameProfile()) < player.getServer().getOpPermissionLevel()) {
+                    player.addMessage(new TranslatableText("text.rei.no_permission_cheat").formatted(Formatting.RED), false);
+                    return;
+                }
+                ItemStack stack = packetByteBuf.readItemStack();
+                if (player.inventory.insertStack(stack.copy())) {
+                    ServerSidePacketRegistry.INSTANCE.sendToPlayer(player, RoughlyEnoughItemsNetwork.CREATE_ITEMS_MESSAGE_PACKET, new PacketByteBuf(Unpooled.buffer()).writeItemStack(stack.copy()).writeString(player.getEntityName(), 32767));
+                } else
+                    player.addMessage(new TranslatableText("text.rei.failed_cheat_items"), false);
+            });
+            ServerSidePacketRegistry.INSTANCE.register(MOVE_ITEMS_PACKET, (packetContext, packetByteBuf) -> {
+                Identifier category = packetByteBuf.readIdentifier();
+                ServerPlayerEntity player = (ServerPlayerEntity) packetContext.getPlayer();
+                ScreenHandler screenHandler = player.currentScreenHandler;
+                PlayerScreenHandler playerScreenHandler = player.playerScreenHandler;
                 try {
-                    InputSlotCrafter.start(category, screenHandler, player, input, shift);
-                } catch (InputSlotCrafter.NotEnoughMaterialsException e) {
-                    if (!(screenHandler instanceof AbstractRecipeScreenHandler))
-                        return;
-                    PacketByteBuf buf = new PacketByteBuf(Unpooled.buffer());
-                    buf.writeInt(input.size());
-                    input.entrySet().stream().sorted(Comparator.comparingInt(Map.Entry::getKey)).forEach(entry -> {
-                        List<ItemStack> stacks = entry.getValue();
-                        buf.writeInt(stacks.size());
-                        for (ItemStack stack : stacks) {
-                            buf.writeItemStack(stack);
+                    boolean shift = packetByteBuf.readBoolean();
+                    Map<Integer, List<ItemStack>> input = Maps.newHashMap();
+                    int mapSize = packetByteBuf.readInt();
+                    for (int i = 0; i < mapSize; i++) {
+                        List<ItemStack> list = Lists.newArrayList();
+                        int count = packetByteBuf.readInt();
+                        for (int j = 0; j < count; j++) {
+                            list.add(packetByteBuf.readItemStack());
+                        }
+                        input.put(i, list);
+                    }
+                    try {
+                        InputSlotCrafter.start(category, screenHandler, player, input, shift);
+                    } catch (InputSlotCrafter.NotEnoughMaterialsException e) {
+                        if (!(screenHandler instanceof AbstractRecipeScreenHandler))
+                            return;
+                        PacketByteBuf buf = new PacketByteBuf(Unpooled.buffer());
+                        buf.writeInt(input.size());
+                        input.entrySet().stream().sorted(Comparator.comparingInt(Map.Entry::getKey)).forEach(entry -> {
+                            List<ItemStack> stacks = entry.getValue();
+                            buf.writeInt(stacks.size());
+                            for (ItemStack stack : stacks) {
+                                buf.writeItemStack(stack);
+                            }
+                        });
+                        if (ServerSidePacketRegistry.INSTANCE.canPlayerReceive(player, NOT_ENOUGH_ITEMS_PACKET)) {
+                            ServerSidePacketRegistry.INSTANCE.sendToPlayer(player, NOT_ENOUGH_ITEMS_PACKET, buf);
                         }
-                    });
-                    if (ServerSidePacketRegistry.INSTANCE.canPlayerReceive(player, NOT_ENOUGH_ITEMS_PACKET)) {
-                        ServerSidePacketRegistry.INSTANCE.sendToPlayer(player, NOT_ENOUGH_ITEMS_PACKET, buf);
+                    } catch (IllegalStateException e) {
+                        player.sendMessage(new TranslatableText(e.getMessage()).formatted(Formatting.RED));
+                    } catch (Exception e) {
+                        player.sendMessage(new TranslatableText("error.rei.internal.error", e.getMessage()).formatted(Formatting.RED));
+                        e.printStackTrace();
                     }
-                } catch (IllegalStateException e) {
-                    player.sendMessage(new TranslatableText(e.getMessage()).formatted(Formatting.RED));
                 } catch (Exception e) {
-                    player.sendMessage(new TranslatableText("error.rei.internal.error", e.getMessage()).formatted(Formatting.RED));
                     e.printStackTrace();
                 }
-            } catch (Exception e) {
-                e.printStackTrace();
-            }
+            });
         });
     }
     

+ 60 - 0
src/main/java/me/shedaniel/rei/RoughlyEnoughItemsState.java

@@ -0,0 +1,60 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 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;
+
+import net.fabricmc.api.EnvType;
+import net.fabricmc.loader.api.FabricLoader;
+import net.minecraft.util.Pair;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+@ApiStatus.Internal
+public class RoughlyEnoughItemsState {
+    private RoughlyEnoughItemsState() {}
+    
+    private static List<Pair<String, String>> failedToLoad = new ArrayList<>();
+    private static Set<String> failedToLoadSet = new LinkedHashSet<>();
+    
+    public static void failedToLoad(String reason) {
+        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.SERVER || FabricLoader.getInstance().isDevelopmentEnvironment())
+            throw new RuntimeException(reason);
+        if (RoughlyEnoughItemsState.failedToLoadSet.add(reason + " " + null))
+            RoughlyEnoughItemsState.failedToLoad.add(new Pair<>(reason, null));
+    }
+    
+    public static void failedToLoad(String reason, String link) {
+        if (FabricLoader.getInstance().getEnvironmentType() == EnvType.SERVER || FabricLoader.getInstance().isDevelopmentEnvironment())
+            throw new RuntimeException(reason + " " + link);
+        if (RoughlyEnoughItemsState.failedToLoadSet.add(reason + " " + link))
+            RoughlyEnoughItemsState.failedToLoad.add(new Pair<>(reason, link));
+    }
+    
+    public static List<Pair<String, String>> getFailedToLoad() {
+        return failedToLoad;
+    }
+}

+ 248 - 0
src/main/java/me/shedaniel/rei/gui/FailedToLoadScreen.java

@@ -0,0 +1,248 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 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.gui;
+
+import me.shedaniel.clothconfig2.gui.widget.DynamicNewSmoothScrollingEntryListWidget;
+import me.shedaniel.rei.RoughlyEnoughItemsState;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawableHelper;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.widget.AbstractButtonWidget;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.sound.PositionedSoundInstance;
+import net.minecraft.sound.SoundEvents;
+import net.minecraft.text.LiteralText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Lazy;
+import net.minecraft.util.Pair;
+import net.minecraft.util.Util;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+@ApiStatus.Internal
+public class FailedToLoadScreen extends Screen {
+    public static final Lazy<FailedToLoadScreen> INSTANCE = new Lazy<>(FailedToLoadScreen::new);
+    private AbstractButtonWidget buttonExit;
+    private StringEntryListWidget listWidget;
+    
+    private FailedToLoadScreen() {
+        super(new LiteralText("REI has failed to init"));
+    }
+    
+    @Override
+    public boolean shouldCloseOnEsc() {
+        return false;
+    }
+    
+    @Override
+    protected void init() {
+        children.add(listWidget = new StringEntryListWidget(client, width, height, 32, height - 32));
+        listWidget.max = 80;
+        listWidget.creditsClearEntries();
+        listWidget.creditsAddEntry(new EmptyItem());
+        for (Pair<String, String> pair : RoughlyEnoughItemsState.getFailedToLoad()) {
+            listWidget.creditsAddEntry(new TextItem(pair.getLeft()));
+            if (pair.getRight() != null)
+                listWidget.creditsAddEntry(this.new LinkItem(pair.getRight()));
+            for (int i = 0; i < 2; i++) {
+                listWidget.creditsAddEntry(new EmptyItem());
+            }
+        }
+        for (StringItem child : listWidget.children()) {
+            listWidget.max = Math.max(listWidget.max, child.getWidth());
+        }
+        children.add(buttonExit = new ButtonWidget(width / 2 - 100, height - 26, 200, 20, "Exit", button -> {
+            MinecraftClient.getInstance().scheduleStop();
+        }));
+    }
+    
+    @Override
+    public boolean mouseScrolled(double double_1, double double_2, double double_3) {
+        return listWidget.mouseScrolled(double_1, double_2, double_3) || super.mouseScrolled(double_1, double_2, double_3);
+    }
+    
+    @Override
+    public void render(int int_1, int int_2, float float_1) {
+        this.renderDirtBackground(0);
+        this.listWidget.render(int_1, int_2, float_1);
+        this.drawCenteredString(this.textRenderer, this.title.asFormattedString(), this.width / 2, 16, 16777215);
+        super.render(int_1, int_2, float_1);
+        this.buttonExit.render(int_1, int_2, float_1);
+    }
+    
+    private static class StringEntryListWidget extends DynamicNewSmoothScrollingEntryListWidget<StringItem> {
+        private boolean inFocus;
+        private int max = 80;
+        
+        public StringEntryListWidget(MinecraftClient client, int width, int height, int startY, int endY) {
+            super(client, width, height, startY, endY, DrawableHelper.BACKGROUND_TEXTURE);
+        }
+        
+        @Override
+        public boolean changeFocus(boolean boolean_1) {
+            if (!this.inFocus && this.getItemCount() == 0) {
+                return false;
+            } else {
+                this.inFocus = !this.inFocus;
+                if (this.inFocus && this.getFocused() == null && this.getItemCount() > 0) {
+                    this.moveSelection(1);
+                } else if (this.inFocus && this.getFocused() != null) {
+                    this.moveSelection(0);
+                }
+                
+                return this.inFocus;
+            }
+        }
+        
+        public void creditsClearEntries() {
+            clearItems();
+        }
+        
+        private StringItem rei_getEntry(int int_1) {
+            return this.children().get(int_1);
+        }
+        
+        public void creditsAddEntry(StringItem entry) {
+            addItem(entry);
+        }
+        
+        @Override
+        public int getItemWidth() {
+            return max;
+        }
+        
+        @Override
+        protected int getScrollbarPosition() {
+            return width - 40;
+        }
+    }
+    
+    private abstract static class StringItem extends DynamicNewSmoothScrollingEntryListWidget.Entry<StringItem> {
+        public abstract int getWidth();
+    }
+    
+    private static class EmptyItem extends StringItem {
+        @Override
+        public void render(int i, int i1, int i2, int i3, int i4, int i5, int i6, boolean b, float v) {
+        
+        }
+        
+        @Override
+        public int getItemHeight() {
+            return 5;
+        }
+        
+        @Override
+        public int getWidth() {
+            return 0;
+        }
+    }
+    
+    private static class TextItem extends StringItem {
+        private String text;
+        
+        public TextItem(Text textComponent) {
+            this(textComponent.asFormattedString());
+        }
+        
+        public TextItem(String text) {
+            this.text = text;
+        }
+        
+        @Override
+        public void render(int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
+            MinecraftClient.getInstance().textRenderer.drawWithShadow(text, x + 5, y, -1);
+        }
+        
+        @Override
+        public int getItemHeight() {
+            return 12;
+        }
+        
+        @Override
+        public boolean changeFocus(boolean boolean_1) {
+            return false;
+        }
+        
+        @Override
+        public int getWidth() {
+            return MinecraftClient.getInstance().textRenderer.getStringWidth(text) + 10;
+        }
+    }
+    
+    private class LinkItem extends StringItem {
+        private String text;
+        private boolean contains;
+        
+        public LinkItem(Text textComponent) {
+            this(textComponent.asFormattedString());
+        }
+        
+        public LinkItem(String text) {
+            this.text = text;
+        }
+        
+        @Override
+        public void render(int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
+            contains = mouseX >= x && mouseX <= x + entryWidth && mouseY >= y && mouseY <= y + entryHeight;
+            if (contains) {
+                FailedToLoadScreen.this.renderTooltip("Click to open link.", mouseX, mouseY);
+                MinecraftClient.getInstance().textRenderer.drawWithShadow("§n" + text, x + 5, y, 0xff1fc3ff);
+            } else {
+                MinecraftClient.getInstance().textRenderer.drawWithShadow(text, x + 5, y, 0xff1fc3ff);
+            }
+        }
+        
+        @Override
+        public int getItemHeight() {
+            return 12;
+        }
+        
+        @Override
+        public boolean changeFocus(boolean boolean_1) {
+            return false;
+        }
+        
+        @Override
+        public int getWidth() {
+            return MinecraftClient.getInstance().textRenderer.getStringWidth(text) + 10;
+        }
+        
+        @Override
+        public boolean mouseClicked(double mouseX, double mouseY, int button) {
+            if (contains && button == 0) {
+                MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
+                try {
+                    Util.getOperatingSystem().open(new URI(text));
+                    return true;
+                } catch (URISyntaxException e) {
+                    e.printStackTrace();
+                }
+            }
+            return false;
+        }
+    }
+}

+ 41 - 32
src/main/java/me/shedaniel/rei/impl/ClientHelperImpl.java

@@ -28,8 +28,10 @@ import io.netty.buffer.Unpooled;
 import me.sargunvohra.mcmods.autoconfig1u.annotation.ConfigEntry;
 import me.shedaniel.clothconfig2.api.FakeModifierKeyCodeAdder;
 import me.shedaniel.clothconfig2.api.ModifierKeyCode;
+import me.shedaniel.math.api.Executor;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.RoughlyEnoughItemsNetwork;
+import me.shedaniel.rei.RoughlyEnoughItemsState;
 import me.shedaniel.rei.api.*;
 import me.shedaniel.rei.gui.PreRecipeViewingScreen;
 import me.shedaniel.rei.gui.RecipeScreen;
@@ -285,41 +287,48 @@ public class ClientHelperImpl implements ClientHelper, ClientModInitializer {
     
     @Override
     public void registerFabricKeyBinds() {
-        String category = "key.rei.category";
-        if (!FabricLoader.getInstance().isModLoaded("amecs")) {
-            try {
-                ConfigObjectImpl.General general = ConfigObject.getInstance().getGeneral();
-                ConfigObjectImpl.General instance = general.getClass().getConstructor().newInstance();
-                for (Field declaredField : general.getClass().getDeclaredFields()) {
-                    if (declaredField.getType() == ModifierKeyCode.class && !declaredField.isAnnotationPresent(ConfigEntry.Gui.Excluded.class)) {
-                        declaredField.setAccessible(true);
-                        FakeModifierKeyCodeAdder.INSTANCE.registerModifierKeyCode(category, "config.roughlyenoughitems." + declaredField.getName(), () -> {
-                            try {
-                                ModifierKeyCode code = (ModifierKeyCode) declaredField.get(general);
-                                return code == null ? ModifierKeyCode.unknown() : code;
-                            } catch (Exception e) {
-                                throw new RuntimeException(e);
-                            }
-                        }, () -> {
-                            try {
-                                return (ModifierKeyCode) declaredField.get(instance);
-                            } catch (IllegalAccessException e) {
-                                throw new RuntimeException(e);
-                            }
-                        }, keyCode -> {
-                            try {
-                                declaredField.set(general, keyCode);
-                            } catch (IllegalAccessException e) {
-                                throw new RuntimeException(e);
-                            }
-                        });
+        boolean keybindingsLoaded = FabricLoader.getInstance().isModLoaded("fabric-keybindings-v0");
+        if (!keybindingsLoaded) {
+            RoughlyEnoughItemsState.failedToLoad("Fabric API is not installed!", "https://www.curseforge.com/minecraft/mc-mods/fabric-api/files/all");
+            return;
+        }
+        Executor.run(() -> () -> {
+            String category = "key.rei.category";
+            if (!FabricLoader.getInstance().isModLoaded("amecs")) {
+                try {
+                    ConfigObjectImpl.General general = ConfigObject.getInstance().getGeneral();
+                    ConfigObjectImpl.General instance = general.getClass().getConstructor().newInstance();
+                    for (Field declaredField : general.getClass().getDeclaredFields()) {
+                        if (declaredField.getType() == ModifierKeyCode.class && !declaredField.isAnnotationPresent(ConfigEntry.Gui.Excluded.class)) {
+                            declaredField.setAccessible(true);
+                            FakeModifierKeyCodeAdder.INSTANCE.registerModifierKeyCode(category, "config.roughlyenoughitems." + declaredField.getName(), () -> {
+                                try {
+                                    ModifierKeyCode code = (ModifierKeyCode) declaredField.get(general);
+                                    return code == null ? ModifierKeyCode.unknown() : code;
+                                } catch (Exception e) {
+                                    throw new RuntimeException(e);
+                                }
+                            }, () -> {
+                                try {
+                                    return (ModifierKeyCode) declaredField.get(instance);
+                                } catch (IllegalAccessException e) {
+                                    throw new RuntimeException(e);
+                                }
+                            }, keyCode -> {
+                                try {
+                                    declaredField.set(general, keyCode);
+                                } catch (IllegalAccessException e) {
+                                    throw new RuntimeException(e);
+                                }
+                            });
+                        }
                     }
+                    KeyBindingRegistryImpl.INSTANCE.addCategory(category);
+                } catch (Throwable throwable) {
+                    throwable.printStackTrace();
                 }
-                KeyBindingRegistryImpl.INSTANCE.addCategory(category);
-            } catch (Throwable throwable) {
-                throwable.printStackTrace();
             }
-        }
+        });
     }
     
 }

+ 17 - 4
src/main/java/me/shedaniel/rei/impl/ScreenHelper.java

@@ -27,16 +27,20 @@ import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import me.shedaniel.cloth.hooks.ClothClientHooks;
+import me.shedaniel.math.api.Executor;
+import me.shedaniel.rei.RoughlyEnoughItemsState;
 import me.shedaniel.rei.api.ConfigManager;
 import me.shedaniel.rei.api.ConfigObject;
 import me.shedaniel.rei.api.REIHelper;
 import me.shedaniel.rei.api.widgets.Tooltip;
 import me.shedaniel.rei.gui.ContainerScreenOverlay;
+import me.shedaniel.rei.gui.FailedToLoadScreen;
 import me.shedaniel.rei.gui.OverlaySearchField;
 import me.shedaniel.rei.gui.RecipeScreen;
 import me.shedaniel.rei.gui.widget.TextFieldWidget;
 import net.fabricmc.api.ClientModInitializer;
 import net.fabricmc.fabric.api.event.client.ClientTickCallback;
+import net.fabricmc.loader.api.FabricLoader;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.screen.Screen;
 import net.minecraft.client.gui.screen.ingame.HandledScreen;
@@ -190,13 +194,22 @@ public class ScreenHelper implements ClientModInitializer, REIHelper {
     @Override
     public void onInitializeClient() {
         ClothClientHooks.SCREEN_INIT_PRE.register((client, screen, screenHooks) -> {
-            if (lastHandledScreen != screen && screen instanceof HandledScreen)
+            if (!RoughlyEnoughItemsState.getFailedToLoad().isEmpty() && !(screen instanceof FailedToLoadScreen)) {
+                client.openScreen(FailedToLoadScreen.INSTANCE.get());
+            } else if (lastHandledScreen != screen && screen instanceof HandledScreen)
                 lastHandledScreen = (HandledScreen<?>) screen;
             return ActionResult.PASS;
         });
-        ClientTickCallback.EVENT.register(minecraftClient -> {
-            if (isOverlayVisible() && getSearchField() != null)
-                getSearchField().tick();
+        boolean loaded = FabricLoader.getInstance().isModLoaded("fabric-events-lifecycle-v0");
+        if (!loaded) {
+            RoughlyEnoughItemsState.failedToLoad("Fabric API is not installed!", "https://www.curseforge.com/minecraft/mc-mods/fabric-api/files/all");
+            return;
+        }
+        Executor.run(() -> () -> {
+            ClientTickCallback.EVENT.register(minecraftClient -> {
+                if (isOverlayVisible() && getSearchField() != null)
+                    getSearchField().tick();
+            });
         });
     }
 }