/* * 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.impl; import com.google.common.base.Stopwatch; import com.google.common.collect.*; import me.shedaniel.math.Rectangle; import me.shedaniel.rei.RoughlyEnoughItemsCore; import me.shedaniel.rei.api.*; import me.shedaniel.rei.api.fluid.FluidSupportProvider; import me.shedaniel.rei.api.ingredient.EntryIngredient; import me.shedaniel.rei.api.ingredient.EntryStack; import me.shedaniel.rei.api.ingredient.util.EntryStacks; import me.shedaniel.rei.api.plugins.REIPlugin; import me.shedaniel.rei.api.registry.CategoryRegistry; import me.shedaniel.rei.api.registry.EntryRegistry; import me.shedaniel.rei.api.registry.ParentReloadable; import me.shedaniel.rei.api.registry.Reloadable; import me.shedaniel.rei.api.registry.display.Display; import me.shedaniel.rei.api.registry.display.DisplayCategory; import me.shedaniel.rei.api.registry.screens.ExclusionZones; import me.shedaniel.rei.api.registry.screens.OverlayDecider; import me.shedaniel.rei.api.registry.screens.ScreenRegistry; import me.shedaniel.rei.api.subsets.SubsetsRegistry; import me.shedaniel.rei.api.util.CollectionUtils; import me.shedaniel.rei.impl.subsets.SubsetsRegistryImpl; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.Util; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.InteractionResult; import net.minecraft.world.InteractionResultHolder; import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.item.crafting.RecipeManager; import org.apache.commons.lang3.tuple.MutablePair; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @ApiStatus.Internal @Environment(EnvType.CLIENT) public class PluginManager implements ParentReloadable { private final List reloadables = new ArrayList<>(); private final List focusedStackProviders = Lists.newArrayList(); private final List autoTransferHandlers = Lists.newArrayList(); private final List> recipeFunctions = Lists.newArrayList(); private final Multimap, ClickAreaHandler> screenClickAreas = HashMultimap.create(); private final List displayVisibilityHandlers = Lists.newArrayList(); private final List> liveDisplayGenerators = Lists.newArrayList(); private RecipeManager recipeManager; private boolean arePluginsLoading = false; public PluginManager() { reloadables.add(CategoryRegistry.getInstance()); } @Override public List> findCraftableEntriesByItems(Iterable> inventoryItems) { List> craftables = new ArrayList<>(); for (List value : recipeDisplays.values()) for (Display display : Lists.newArrayList(value)) { int slotsCraftable = 0; List requiredInput = display.getRequiredEntries(); for (EntryIngredient slot : requiredInput) { if (slot.isEmpty()) { slotsCraftable++; continue; } back: for (EntryStack possibleType : inventoryItems) { for (EntryStack slotPossible : slot) if (EntryStacks.equalsIgnoreCount(possibleType, slotPossible)) { slotsCraftable++; break back; } } } if (slotsCraftable == display.getRequiredEntries().size()) display.getResultingEntries().stream().flatMap(Collection::stream).collect(Collectors.toCollection(() -> craftables)); } return craftables.stream().distinct().collect(Collectors.toList()); } @Override public boolean arePluginsLoading() { return arePluginsLoading; } @Override public void registerDisplay(Display display) { ResourceLocation identifier = Objects.requireNonNull(display.getCategoryIdentifier()); if (!recipeDisplays.containsKey(identifier)) throw new IllegalArgumentException("Unable to identify category: " + identifier.toString()); recipeCount.increment(); recipeDisplays.get(identifier).add(display); } private void registerDisplay(ResourceLocation categoryIdentifier, Display display, int index) { if (!recipeDisplays.containsKey(categoryIdentifier)) throw new IllegalArgumentException("Unable to identify category: " + categoryIdentifier.toString()); recipeCount.increment(); recipeDisplays.get(categoryIdentifier).add(index, display); } @Override public Map, List> buildMapFor(ClientHelper.ViewSearchBuilder builder) { Stopwatch stopwatch = Stopwatch.createStarted(); Set categories = builder.getCategories(); List> recipesFor = builder.getRecipesFor(); List> usagesFor = builder.getUsagesFor(); Map, List> result = Maps.newLinkedHashMap(); for (Map.Entry, ResourceLocation> entry : this.categories.entrySet()) { DisplayCategory category = entry.getKey(); ResourceLocation categoryId = entry.getValue(); List allRecipesFromCategory = getAllRecipesFromCategory(category); Set set = Sets.newLinkedHashSet(); if (categories.contains(categoryId)) { for (Display display : allRecipesFromCategory) { if (isDisplayVisible(display)) { set.add(display); } } if (!set.isEmpty()) { CollectionUtils.getOrPutEmptyList(result, category).addAll(set); } continue; } for (Display display : allRecipesFromCategory) { if (!isDisplayVisible(display)) continue; if (!recipesFor.isEmpty()) { back: for (List> results : display.getResultingEntries()) { for (EntryStack otherEntry : results) { for (EntryStack stack : recipesFor) { if (EntryStacks.equalsIgnoreCount(otherEntry, stack)) { set.add(display); break back; } } } } } if (!usagesFor.isEmpty()) { back: for (List> input : display.getInputEntries()) { for (EntryStack otherEntry : input) { for (EntryStack stack : usagesFor) { if (EntryStacks.equalsIgnoreCount(otherEntry, stack)) { set.add(display); break back; } } } } } } for (EntryStack stack : usagesFor) { if (isStackWorkStationOfCategory(categoryId, stack)) { set.addAll(CollectionUtils.filter(allRecipesFromCategory, this::isDisplayVisible)); break; } } if (!set.isEmpty()) { CollectionUtils.getOrPutEmptyList(result, category).addAll(set); } } for (LiveDisplayGenerator liveDisplayGenerator : liveDisplayGenerators) { Set set = Sets.newLinkedHashSet(); for (EntryStack stack : recipesFor) { Optional> recipeForDisplays = liveDisplayGenerator.getRecipeFor(stack); if (recipeForDisplays.isPresent()) { for (Display display : recipeForDisplays.get()) { if (isDisplayVisible(display)) set.add(display); } } } for (EntryStack stack : usagesFor) { Optional> usageForDisplays = liveDisplayGenerator.getUsageFor(stack); if (usageForDisplays.isPresent()) { for (Display display : usageForDisplays.get()) { if (isDisplayVisible(display)) set.add(display); } } } Optional> displaysGenerated = liveDisplayGenerator.getDisplaysGenerated(builder); if (displaysGenerated.isPresent()) { for (Display display : displaysGenerated.get()) { if (isDisplayVisible(display)) set.add(display); } } if (!set.isEmpty()) { CollectionUtils.getOrPutEmptyList(result, getCategory(liveDisplayGenerator.getCategoryIdentifier())).addAll(set); } } String message = String.format("Built Recipe View in %s for %d categories, %d recipes for, %d usages for and %d live recipe generators.", stopwatch.stop().toString(), categories.size(), recipesFor.size(), usagesFor.size(), liveDisplayGenerators.size()); if (ConfigObject.getInstance().doDebugSearchTimeRequired()) { RoughlyEnoughItemsCore.LOGGER.info(message); } else { RoughlyEnoughItemsCore.LOGGER.trace(message); } return result; } @Override public Map, List> getRecipesFor(EntryStack stack) { return buildMapFor(ClientHelper.ViewSearchBuilder.builder().addRecipesFor(stack)); } @Override public DisplayCategory getCategory(ResourceLocation identifier) { return categories.inverse().get(identifier); } @Override public RecipeManager getRecipeManager() { return recipeManager; } private boolean isStackWorkStationOfCategory(ResourceLocation category, EntryStack stack) { for (List> stacks : getWorkingStations(category)) { for (EntryStack entryStack : stacks) { if (EntryStacks.equalsFuzzy(entryStack, stack)) return true; } } return false; } @Override public Map, List> getUsagesFor(EntryStack stack) { return buildMapFor(ClientHelper.ViewSearchBuilder.builder().addUsagesFor(stack)); } @Override public List> getAllCategories() { return Lists.newArrayList(categories.keySet()); } @Override public Optional getAutoCraftButtonArea(DisplayCategory category) { if (!autoCraftAreaSupplierMap.containsKey(category.getIdentifier())) return Optional.ofNullable(bounds -> new Rectangle(bounds.getMaxX() - 16, bounds.getMaxY() - 16, 10, 10)); return Optional.ofNullable(autoCraftAreaSupplierMap.get(category.getIdentifier())); } @Override public void registerAutoCraftButtonArea(ResourceLocation category, ButtonAreaSupplier rectangle) { if (rectangle == null) { autoCraftAreaSupplierMap.remove(category); } else autoCraftAreaSupplierMap.put(category, rectangle); } private void startSection(MutablePair sectionData, String section) { sectionData.setRight(section); RoughlyEnoughItemsCore.LOGGER.debug("Reloading Section: \"%s\"", section); sectionData.getLeft().reset().start(); } private void endSection(MutablePair sectionData) { sectionData.getLeft().stop(); String section = sectionData.getRight(); RoughlyEnoughItemsCore.LOGGER.debug("Reloading Section: \"%s\" done in %s", section, sectionData.getLeft().toString()); sectionData.getLeft().reset(); } private void pluginSection(MutablePair sectionData, String sectionName, List list, Consumer consumer) { for (REIPlugin plugin : list) { startSection(sectionData, sectionName + " for " + plugin.getPluginName()); try { consumer.accept(plugin); } catch (Throwable e) { RoughlyEnoughItemsCore.LOGGER.error(plugin.getPluginName() + " plugin failed to " + sectionName + "!", e); } endSection(sectionData); } } public void tryRecipesLoaded(RecipeManager recipeManager) { try { recipesLoaded(recipeManager); } catch (Throwable throwable) { throwable.printStackTrace(); } arePluginsLoading = false; } public void recipesLoaded(RecipeManager recipeManager) { long startTime = Util.getMillis(); MutablePair sectionData = new MutablePair<>(Stopwatch.createUnstarted(), ""); startSection(sectionData, "reset-data"); for (Reloadable reloadable : reloadables) { reloadable.resetData(); } arePluginsLoading = true; ScreenHelper.clearLastRecipeScreenData(); recipeCount.setValue(0); this.recipeManager = recipeManager; this.recipeDisplays.clear(); this.categories.clear(); this.autoCraftAreaSupplierMap.clear(); this.screenClickAreas.clear(); this.recipeFunctions.clear(); this.displayVisibilityHandlers.clear(); this.liveDisplayGenerators.clear(); this.autoTransferHandlers.clear(); this.focusedStackProviders.clear(); ScreenRegistryImpl displayHelper = (ScreenRegistryImpl) ScreenRegistry.getInstance(); EntryRegistryImpl entryRegistry = (EntryRegistryImpl) EntryRegistry.getInstance(); EntryTypeRegistryImpl entryTypeRegistry = EntryTypeRegistryImpl.getInstance(); entryTypeRegistry.reset(); FavoriteEntryTypeRegistryImpl.getInstance().clear(); ((SubsetsRegistryImpl) SubsetsRegistry.getInstance()).reset(); ((FluidSupportProviderImpl) FluidSupportProvider.getInstance()).reset(); displayHelper.resetData(); List plugins = RoughlyEnoughItemsCore.getPlugins(); plugins.sort(Comparator.comparingInt(REIPluginEntry::getPriority).reversed()); RoughlyEnoughItemsCore.LOGGER.info("Reloading REI, registered %d plugins: %s", plugins.size(), plugins.stream().map(REIPluginEntry::getPluginName).collect(Collectors.joining(", "))); Collections.reverse(plugins); entryRegistry.resetToReloadStart(); List reiPlugins = new ArrayList<>(); endSection(sectionData); for (REIPluginEntry plugin : plugins) { startSection(sectionData, "pre-register for " + plugin.getPluginName()); try { if (plugin instanceof REIPlugin) { ((REIPlugin) plugin).preRegister(); reiPlugins.add((REIPlugin) plugin); } } catch (Throwable e) { RoughlyEnoughItemsCore.LOGGER.error(plugin.getPluginName() + " plugin failed to pre register!", e); } endSection(sectionData); } pluginSection(sectionData, "register-entry-types", reiPlugins, plugin -> plugin.registerEntryTypes(entryTypeRegistry)); pluginSection(sectionData, "register-bounds", reiPlugins, plugin -> plugin.registerScreens(displayHelper)); pluginSection(sectionData, "register-entries", reiPlugins, plugin -> plugin.registerEntries(entryRegistry)); pluginSection(sectionData, "register-categories", reiPlugins, plugin -> plugin.registerCategories(this)); pluginSection(sectionData, "register-displays", reiPlugins, plugin -> plugin.registerDisplays(this)); pluginSection(sectionData, "register-others", reiPlugins, plugin -> plugin.registerOthers(this)); pluginSection(sectionData, "post-register", reiPlugins, REIPlugin::postRegister); startSection(sectionData, "recipe-functions"); if (!recipeFunctions.isEmpty()) { List> allSortedRecipes = getAllSortedRecipes(); for (int i = allSortedRecipes.size() - 1; i >= 0; i--) { Recipe recipe = allSortedRecipes.get(i); for (RecipeFunction recipeFunction : recipeFunctions) { try { if (recipeFunction.recipeFilter.test(recipe)) { registerDisplay(recipeFunction.category, recipeFunction.get(recipe), 0); } } catch (Throwable e) { RoughlyEnoughItemsCore.LOGGER.error("Failed to add recipes!", e); } } } } endSection(sectionData); startSection(sectionData, "fill-handlers"); if (getDisplayVisibilityHandlers().isEmpty()) registerRecipeVisibilityHandler(new DisplayVisibilityHandler() { @Override public InteractionResult handleDisplay(DisplayCategory category, Display display) { return InteractionResult.SUCCESS; } @Override public float getPriority() { return -1f; } }); registerFocusedStackProvider(new FocusedStackProvider() { @Override @NotNull public InteractionResultHolder> provide(Screen screen) { if (screen instanceof AbstractContainerScreen) { AbstractContainerScreen containerScreen = (AbstractContainerScreen) screen; if (containerScreen.hoveredSlot != null && !containerScreen.hoveredSlot.getItem().isEmpty()) return InteractionResultHolder.success(EntryStacks.of(containerScreen.hoveredSlot.getItem())); } return InteractionResultHolder.pass(EntryStack.empty()); } @Override public double getPriority() { return -1.0; } }); displayHelper.registerHandler(new OverlayDecider() { @Override public boolean isHandingScreen(Class screen) { return true; } @Override public InteractionResult shouldScreenBeOverlaid(Class screen) { return AbstractContainerScreen.class.isAssignableFrom(screen) ? InteractionResult.SUCCESS : InteractionResult.PASS; } @Override public float getPriority() { return -10; } }); endSection(sectionData); // Clear Cache REIHelper.getInstance().getOverlay().ifPresent(REIOverlay::queueReloadOverlay); startSection(sectionData, "entry-registry-finalise"); // Finish Reload entryRegistry.finishReload(); endSection(sectionData); startSection(sectionData, "entry-registry-refilter"); arePluginsLoading = false; entryRegistry.refilter(); endSection(sectionData); startSection(sectionData, "finalizing"); // Clear Cache Again! REIHelper.getInstance().getOverlay().ifPresent(REIOverlay::queueReloadOverlay); displayVisibilityHandlers.sort(Comparator.reverseOrder()); endSection(sectionData); long usedTime = Util.getMillis() - startTime; RoughlyEnoughItemsCore.LOGGER.info("Reloaded %d stack entries, %d recipes displays, %d exclusion zones suppliers, %d overlay deciders, %d visibility handlers and %d categories (%s) in %dms.", entryRegistry.getEntryStacks().count(), recipeCount.getValue(), ExclusionZones.getInstance().getZonesCount(), displayHelper.getAllOverlayDeciders().size(), getDisplayVisibilityHandlers().size(), categories.size(), categories.keySet().stream().map(DisplayCategory::getTitle).map(Component::getString).collect(Collectors.joining(", ")), usedTime); } @Override public AutoTransferHandler registerAutoCraftingHandler(AutoTransferHandler handler) { autoTransferHandlers.add(handler); autoTransferHandlers.sort(Comparator.reverseOrder()); return handler; } @Override public void registerFocusedStackProvider(FocusedStackProvider provider) { focusedStackProviders.add(provider); focusedStackProviders.sort(Comparator.reverseOrder()); } @Override @Nullable public EntryStack getScreenFocusedStack(Screen screen) { for (FocusedStackProvider provider : focusedStackProviders) { InteractionResultHolder> result = Objects.requireNonNull(provider.provide(screen)); if (result.getResult() == InteractionResult.SUCCESS) { if (!result.getObject().isEmpty()) return result.getObject(); return null; } else if (result.getResult() == InteractionResult.FAIL) return null; } return null; } @Override public List getSortedAutoCraftingHandler() { return autoTransferHandlers; } @Override public int getDisplayCount() { return recipeCount.getValue(); } @Override public List> getAllSortedRecipes() { return getRecipeManager().getRecipes().parallelStream().sorted(RECIPE_COMPARATOR).collect(Collectors.toList()); } @Override public Map, List> getAllRecipes() { return buildMapFor(ClientHelper.ViewSearchBuilder.builder().addAllCategories()); } @Override public Map, List> getAllRecipesNoHandlers() { Map, List> result = Maps.newLinkedHashMap(); for (Map.Entry, ResourceLocation> entry : categories.entrySet()) { DisplayCategory category = entry.getKey(); ResourceLocation categoryId = entry.getValue(); List displays = recipeDisplays.get(categoryId); if (displays != null && !displays.isEmpty()) { result.put(category, displays); } } return result; } @Override public List getAllRecipesFromCategory(DisplayCategory category) { return recipeDisplays.get(category.getIdentifier()); } @Override public void registerRecipeVisibilityHandler(DisplayVisibilityHandler visibilityHandler) { displayVisibilityHandlers.add(visibilityHandler); } @Override public void unregisterRecipeVisibilityHandler(DisplayVisibilityHandler visibilityHandler) { displayVisibilityHandlers.remove(visibilityHandler); } @Override public List getDisplayVisibilityHandlers() { return Collections.unmodifiableList(displayVisibilityHandlers); } @Override public boolean isDisplayNotVisible(Display display) { return !isDisplayVisible(display); } @Override public boolean isDisplayVisible(Display display) { DisplayCategory category = getCategory(display.getCategoryIdentifier()); try { for (DisplayVisibilityHandler displayVisibilityHandler : displayVisibilityHandlers) { InteractionResult visibility = displayVisibilityHandler.handleDisplay(category, display); if (visibility != InteractionResult.PASS) return visibility == InteractionResult.SUCCESS; } } catch (Throwable throwable) { RoughlyEnoughItemsCore.LOGGER.error("Failed to check if the recipe is visible!", throwable); } return true; } @Override public > void registerContainerClickArea(ScreenClickAreaProvider rectangleSupplier, Class screenClass, ResourceLocation... categories) { registerClickArea(screen -> { Rectangle rectangle = rectangleSupplier.provide(screen).clone(); rectangle.translate(screen.leftPos, screen.topPos); return rectangle; }, screenClass, categories); } @Override public void registerClickArea(ScreenClickAreaProvider rectangleSupplier, Class screenClass, ResourceLocation... categories) { registerClickArea(screenClass, rectangleSupplier.toHandler(() -> categories)); } @Override public void registerClickArea(Class screenClass, ClickAreaHandler handler) { this.screenClickAreas.put(screenClass, handler); } @Override public > void registerRecipes(ResourceLocation category, Predicate> recipeFilter, Function mappingFunction) { recipeFunctions.add(new RecipeFunction<>(category, recipeFilter, mappingFunction)); } @Override public void registerLiveRecipeGenerator(LiveDisplayGenerator liveDisplayGenerator) { liveDisplayGenerators.add((LiveDisplayGenerator) liveDisplayGenerator); } public Multimap, ClickAreaHandler> getClickAreas() { return screenClickAreas; } private static class RecipeFunction> { private ResourceLocation category; private Predicate> recipeFilter; private Function mappingFunction; public RecipeFunction(ResourceLocation category, Predicate> recipeFilter, Function mappingFunction) { this.category = category; this.recipeFilter = recipeFilter; this.mappingFunction = mappingFunction; } private Display get(A screen) { return mappingFunction.apply((T) screen); } } }