From a56baa875630ffac06e421a7389854b5301ed7f0 Mon Sep 17 00:00:00 2001 From: shedaniel Date: Sun, 21 Feb 2021 22:33:45 +0800 Subject: More Signed-off-by: shedaniel --- .../java/me/shedaniel/rei/impl/PluginManager.java | 627 +++++++++++++++++++++ 1 file changed, 627 insertions(+) create mode 100644 runtime/src/main/java/me/shedaniel/rei/impl/PluginManager.java (limited to 'runtime/src/main/java/me/shedaniel/rei/impl/PluginManager.java') diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/PluginManager.java b/runtime/src/main/java/me/shedaniel/rei/impl/PluginManager.java new file mode 100644 index 000000000..41da643a7 --- /dev/null +++ b/runtime/src/main/java/me/shedaniel/rei/impl/PluginManager.java @@ -0,0 +1,627 @@ +/* + * 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); + } + } +} -- cgit