diff --git a/build.gradle b/build.gradle index 8090fcb..65e3389 100644 --- a/build.gradle +++ b/build.gradle @@ -83,6 +83,14 @@ repositories { name 'jared' url 'https://maven.blamejared.com/' } + maven { + name "EMI" + url "https://maven.terraformersmc.com/" + } + maven { + name "REI" + url "https://maven.shedaniel.me/" + } } dependencies { @@ -133,6 +141,12 @@ dependencies { //runtimeOnly fg.deobf("top.theillusivec4.curios:curios-forge:${project.curios_version}") // https://maven.theillusivec4.top/top/theillusivec4/curios/curios-forge/ compileOnly fg.deobf("top.theillusivec4.curios:curios-forge:${project.curios_version}:api") + + compileOnly fg.deobf("dev.emi:emi-forge:${emi_version}") // https://maven.terraformersmc.com/releases/dev/emi/emi-forge + + compileOnly fg.deobf("me.shedaniel:RoughlyEnoughItems-forge:${rei_version}") // https://maven.shedaniel.me/me/shedaniel/RoughlyEnoughItems-forge/ or just look on CurseForge by modloader and MC version... + implementation fg.deobf("me.shedaniel.cloth:cloth-config-forge:$cloth_config_version"); + implementation fg.deobf("dev.architectury:architectury-forge:$architectury_version"); } minecraft { @@ -143,6 +157,11 @@ minecraft { workingDirectory project.file('run') //property 'forge.logging.markers', 'REGISTRIES,REGISTRYDUMP' property 'forge.logging.console.level', 'debug' + + // Required for EMI + property 'mixin.env.remapRefMap', 'true' + property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg" + mods { integratedterminalscompat { source sourceSets.main @@ -157,6 +176,11 @@ minecraft { server { workingDirectory project.file('run') property 'forge.logging.console.level', 'debug' + + // Required for EMI + property 'mixin.env.remapRefMap', 'true' + property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg" + mods { integratedterminalscompat { source sourceSets.main diff --git a/gradle.properties b/gradle.properties index 7365493..5015b5b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,15 +1,19 @@ mod_version=1.0.0 minecraft_version=1.20.1 forge_version=47.0.1 -cyclopscore_version=1.18.4-340 +cyclopscore_version=1.19.4-497 release_type=release fingerprint=bd0353b3e8a2810d60dd584e256e364bc3bedd44 integrateddynamics_version=1.20.1-1.16.7-575 -integratedterminals_version=1.20.1-1.4.8-281 +integratedterminals_version=1.20.1-1.5.1-387 commoncapabilities_version=1.20.1-2.9.0-98 curios_version=1.19.4-5.1.5.3 jei_version=1.20.1-forge:15.1.0.19 +emi_version=1.1.16+1.20.1 +rei_version=12.1.780 +cloth_config_version=11.1.136 +architectury_version=9.2.14 # Workaround for Spotless bug # https://github.com/diffplug/spotless/issues/834 diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/common/RecipeInputSlot.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/common/RecipeInputSlot.java new file mode 100644 index 0000000..dd225c6 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/common/RecipeInputSlot.java @@ -0,0 +1,12 @@ +package org.cyclops.integratedterminalscompat.modcompat.common; + +import net.minecraft.world.item.ItemStack; + +/** + * @author rubensworks + */ +public interface RecipeInputSlot extends Iterable { + + public boolean isEmpty(); + +} diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/common/RecipeTransferHelpers.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/common/RecipeTransferHelpers.java new file mode 100644 index 0000000..e910a39 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/common/RecipeTransferHelpers.java @@ -0,0 +1,212 @@ +package org.cyclops.integratedterminalscompat.modcompat.common; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Streams; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.items.wrapper.InvWrapper; +import org.apache.commons.lang3.tuple.Pair; +import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; +import org.cyclops.commoncapabilities.ingredient.storage.IngredientComponentStorageWrapperHandlerItemStack; +import org.cyclops.cyclopscore.ingredient.collection.IIngredientCollectionMutable; +import org.cyclops.cyclopscore.ingredient.collection.IngredientCollectionHelpers; +import org.cyclops.cyclopscore.ingredient.collection.IngredientCollectionPrototypeMap; +import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabCommon; +import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentClient; +import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentItemStackCrafting; +import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentItemStackCraftingCommon; +import org.cyclops.integratedterminals.inventory.container.ContainerTerminalStorageBase; +import org.cyclops.integratedterminalscompat.IntegratedTerminalsCompat; +import org.cyclops.integratedterminalscompat.network.packet.TerminalStorageIngredientItemStackCraftingGridSetRecipe; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Common helpers for JEI/EMI/REI + * @author rubensworks + */ +public class RecipeTransferHelpers { + + // The amount of seconds recipeErrors will be cached for transferRecipe + private static final long RECIPE_ERROR_CACHE_TIME = 60; + private static final Cache>> recipeErrorCache = CacheBuilder.newBuilder() + .expireAfterAccess(RECIPE_ERROR_CACHE_TIME, TimeUnit.SECONDS) + .build(); + + public static Optional>> getTabs(ContainerTerminalStorageBase container) { + if (Objects.equals(container.getSelectedTab(), TerminalStorageTabIngredientComponentItemStackCrafting.NAME.toString())) { + ITerminalStorageTabCommon tabCommon = container.getTabCommon(container.getSelectedTab()); + TerminalStorageTabIngredientComponentItemStackCraftingCommon tabCommonCrafting = + (TerminalStorageTabIngredientComponentItemStackCraftingCommon) tabCommon; + TerminalStorageTabIngredientComponentClient tabClient = (TerminalStorageTabIngredientComponentClient) + container.getTabClient(container.getSelectedTab()); + return Optional.of(Pair.of(tabCommonCrafting, tabClient)); + } + return Optional.empty(); + } + + public static Optional> getMissingItems(Object cacheKey, ContainerTerminalStorageBase container, Iterable recipeInputSlots, Player player, TerminalStorageTabIngredientComponentItemStackCraftingCommon tabCommonCrafting, TerminalStorageTabIngredientComponentClient tabClient, Function itemStackToMatchCondition, Supplier getId, Consumer onChangeId) { + Callable>> missingItemsSupplier = + () -> getMissingItemsUncached(container, recipeInputSlots, player, tabCommonCrafting, itemStackToMatchCondition, onChangeId); + if (getId.get() != tabClient.getLastChangeId()) { + // Clear cache when storage contents changed + recipeErrorCache.invalidateAll(); + } + try { + return recipeErrorCache.get(cacheKey, (Callable) missingItemsSupplier); + } catch (ExecutionException e) { + // Throw exceptions from missingItemsSupplier + throw new RuntimeException(e); + } + } + + public static Optional> getMissingItemsUncached(ContainerTerminalStorageBase container, Iterable recipeInputSlots, Player player, TerminalStorageTabIngredientComponentItemStackCraftingCommon tabCommonCrafting, Function itemStackToMatchCondition, Consumer onChangeId) { + TerminalStorageTabIngredientComponentClient tabClient = (TerminalStorageTabIngredientComponentClient) + container.getTabClient(container.getSelectedTab()); + + // Check in the player inventory and local client view if the required recipe ingredients are available + + // Build crafting grid index + IIngredientCollectionMutable hayStackCraftingGrid = new IngredientCollectionPrototypeMap<>(IngredientComponent.ITEMSTACK); + for (int slot = 0; slot < tabCommonCrafting.getInventoryCrafting().getContainerSize(); slot++) { + hayStackCraftingGrid.add(tabCommonCrafting.getInventoryCrafting().getItem(slot)); + } + + // Build player inventory index + IIngredientCollectionMutable hayStackPlayer = new IngredientCollectionPrototypeMap<>(IngredientComponent.ITEMSTACK); + hayStackPlayer.addAll(player.getInventory().items); + + // Build local client view of storage + List> unfilteredIngredients = tabClient + .getUnfilteredIngredientsView(container.getSelectedChannel()); + IIngredientCollectionMutable hayStack = IngredientCollectionHelpers.createCollapsedCollection(IngredientComponent.ITEMSTACK); + IIngredientCollectionMutable hayStackCraftable = IngredientCollectionHelpers.createCollapsedCollection(IngredientComponent.ITEMSTACK); + hayStack.addAll(unfilteredIngredients + .stream() + .filter(i -> i.getCraftingOption() == null) + .map(TerminalStorageTabIngredientComponentClient.InstanceWithMetadata::getInstance) + .collect(Collectors.toList())); + hayStackCraftable.addAll(unfilteredIngredients + .stream() + .filter(i -> i.getCraftingOption() != null) + .map(TerminalStorageTabIngredientComponentClient.InstanceWithMetadata::getInstance) + .collect(Collectors.toList())); + + List slotsMissingItems = Lists.newArrayList(); + List slotsMissingCraftableItems = Lists.newArrayList(); + for (T slot : recipeInputSlots) { + if (!slot.isEmpty()) { + boolean found = false; + boolean craftable = false; + for (ItemStack itemStack : slot) { + int matchCondition = itemStackToMatchCondition.apply(itemStack); + + // First check in the crafting grid + if (hayStackCraftingGrid.contains(itemStack, matchCondition)) { + hayStackPlayer.remove(itemStack); + found = true; + break; + } + + // Then check in player inventory + if (hayStackPlayer.contains(itemStack, matchCondition)) { + hayStackPlayer.remove(itemStack); + found = true; + break; + } + + // Then check the storage + if (hayStack.contains(itemStack, matchCondition)) { + hayStack.remove(itemStack); + found = true; + break; + } + + // Also check if this item could be craftable + if (hayStackCraftable.contains(itemStack, matchCondition)) { + craftable = true; + } + } + + if (!found) { + if (craftable) { + slotsMissingCraftableItems.add(slot); + } else { + slotsMissingItems.add(slot); + } + } + } + } + + onChangeId.accept(tabClient.getLastChangeId()); + if (!slotsMissingItems.isEmpty() || !slotsMissingCraftableItems.isEmpty()) { + return Optional.of(new RecipeTransferResult(slotsMissingItems, slotsMissingCraftableItems)); + } + + return Optional.empty(); + } + + public static void transferRecipe(ContainerTerminalStorageBase container, Iterable recipeInputSlots, Player player, TerminalStorageTabIngredientComponentItemStackCraftingCommon tabCommonCrafting, Function itemStackToMatchCondition, boolean maxTransfer) { + IngredientComponentStorageWrapperHandlerItemStack.ComponentStorageWrapper playerInventory = + new IngredientComponentStorageWrapperHandlerItemStack.ComponentStorageWrapper(IngredientComponent.ITEMSTACK, new InvWrapper(player.getInventory())); + + // Send a packet to the server if the recipe effectively needs to be applied to the grid + Map> slottedIngredientsFromPlayer = Maps.newHashMap(); + Map>> slottedIngredientsFromStorage = Maps.newHashMap(); + int slotOffset = tabCommonCrafting.getSlotCrafting().index; + int slotId = 0; + for (T recipeSlot : recipeInputSlots) { + if (!recipeSlot.isEmpty()) { + boolean found = false; + + // First check if we can transfer from the player inventory + // No need to check the crafting grid, as the server will first clear the grid into the storage in TerminalStorageIngredientItemStackCraftingGridSetRecipe + for (ItemStack itemStack : recipeSlot) { + int matchCondition = itemStackToMatchCondition.apply(itemStack); + + if (!playerInventory.extract(itemStack, matchCondition, true).isEmpty()) { + found = true; + + // Move from player to crafting grid + ItemStack extracted = playerInventory.extract(itemStack, matchCondition, false); + Slot slot = container.getSlot(slotId + slotOffset); + slot.set(extracted); + + // Do the exact same thing server-side + slottedIngredientsFromPlayer.put(slotId, Pair.of(itemStack, itemStackToMatchCondition.apply(itemStack))); + + break; + } + } + + if (!found) { + // Otherwise, request them from the storage + slottedIngredientsFromStorage.put(slotId, Streams.stream(recipeSlot) + .map(itemStack -> Pair.of(itemStack, itemStackToMatchCondition.apply(itemStack))) + .collect(Collectors.toList())); + } + } + slotId++; + } + + IntegratedTerminalsCompat._instance.getPacketHandler().sendToServer( + new TerminalStorageIngredientItemStackCraftingGridSetRecipe(container.getSelectedTab(), + container.getSelectedChannel(), maxTransfer, slottedIngredientsFromPlayer, slottedIngredientsFromStorage, AbstractContainerScreen.hasControlDown())); + } + +} diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/common/RecipeTransferResult.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/common/RecipeTransferResult.java new file mode 100644 index 0000000..2266188 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/common/RecipeTransferResult.java @@ -0,0 +1,68 @@ +package org.cyclops.integratedterminalscompat.modcompat.common; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import org.cyclops.cyclopscore.helper.Helpers; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author rubensworks + */ +public class RecipeTransferResult { + + public static final boolean HAS_CMD = System.getProperty("os.name").equals("Mac OS X"); + + public static final int SLOT_COLOR_MISSING = Helpers.RGBAToInt(255, 0, 0, 100); + public static final int SLOT_COLOR_CRAFTABLE = Helpers.RGBAToInt(0, 0, 255, 100); + + public static final int HIGHLIGHT_COLOR_FAIL = Helpers.RGBAToInt(255, 0, 0, 100); + public static final int HIGHLIGHT_COLOR_CRAFTABLE = Helpers.RGBAToInt(0, 0, 255, 100); + public static final int HIGHLIGHT_COLOR_CRAFTABLE_PARTIAL = Helpers.RGBAToInt(255, 125, 0, 100); + + private final List message = new ArrayList<>(); + private final Collection slotsMissing; + private final Collection slotsCraftable; + private final int color; + + public RecipeTransferResult(Collection slotsMissing, Collection slotsCraftable) { + this.slotsMissing = slotsMissing; + this.slotsCraftable = slotsCraftable; + if (slotsCraftable.isEmpty() && slotsMissing.isEmpty()) { + this.color = HIGHLIGHT_COLOR_CRAFTABLE; + } else if (slotsMissing.isEmpty()) { + // Missing items, but they are all craftable + this.message.add(Component.translatable("gui.integratedterminalscompat.terminal_storage.jei.transfer.craftable").withStyle(ChatFormatting.RED)); + this.message.add(Component.translatable(HAS_CMD ? "gui.integratedterminalscompat.terminal_storage.jei.transfer.craft.info_cmd" : "gui.integratedterminalscompat.terminal_storage.jei.transfer.craft.info").withStyle(ChatFormatting.ITALIC)); + this.color = HIGHLIGHT_COLOR_CRAFTABLE; + } else if (!slotsCraftable.isEmpty()) { + // Missing items, but only some of them are craftable + this.message.add(Component.translatable("gui.integratedterminalscompat.terminal_storage.jei.transfer.craftable_partial").withStyle(ChatFormatting.RED)); + this.message.add(Component.translatable(HAS_CMD ? "gui.integratedterminalscompat.terminal_storage.jei.transfer.craft.info_cmd" : "gui.integratedterminalscompat.terminal_storage.jei.transfer.craft.info").withStyle(ChatFormatting.ITALIC)); + this.color = HIGHLIGHT_COLOR_CRAFTABLE_PARTIAL; + } else { + // Missing items, and none are craftable + this.message.add(Component.translatable("gui.integratedterminalscompat.terminal_storage.jei.transfer.missing").withStyle(ChatFormatting.ITALIC)); + this.color = HIGHLIGHT_COLOR_FAIL; + } + } + + public int getButtonHighlightColor() { + return this.color; + } + + public List getMessage() { + return message; + } + + public Collection getSlotsCraftable() { + return slotsCraftable; + } + + public Collection getSlotsMissing() { + return slotsMissing; + } + +} diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/terminalstorage/button/TerminalButtonItemStackCraftingGridJeiSearchSync.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/common/button/TerminalButtonItemStackCraftingGridSearchSync.java similarity index 73% rename from src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/terminalstorage/button/TerminalButtonItemStackCraftingGridJeiSearchSync.java rename to src/main/java/org/cyclops/integratedterminalscompat/modcompat/common/button/TerminalButtonItemStackCraftingGridSearchSync.java index f242604..68bbf04 100644 --- a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/terminalstorage/button/TerminalButtonItemStackCraftingGridJeiSearchSync.java +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/common/button/TerminalButtonItemStackCraftingGridSearchSync.java @@ -1,4 +1,4 @@ -package org.cyclops.integratedterminalscompat.modcompat.jei.terminalstorage.button; +package org.cyclops.integratedterminalscompat.modcompat.common.button; import net.minecraft.ChatFormatting; import net.minecraft.nbt.CompoundTag; @@ -8,6 +8,7 @@ import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import org.cyclops.cyclopscore.client.gui.component.button.ButtonImage; +import org.cyclops.cyclopscore.client.gui.image.Image; import org.cyclops.integratedterminals.api.terminalstorage.ITerminalButton; import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabClient; import org.cyclops.integratedterminals.client.gui.image.Images; @@ -19,23 +20,27 @@ import java.util.List; /** - * A button for toggling JEI search box sync. + * A button for toggling JEI/EMI search box sync. * @author rubensworks */ -public class TerminalButtonItemStackCraftingGridJeiSearchSync +public class TerminalButtonItemStackCraftingGridSearchSync implements ITerminalButton, TerminalStorageTabIngredientComponentCommon, ButtonImage> { + private final String mod; private final TerminalStorageState state; private final String buttonName; private final ITerminalStorageTabClient clientTab; + private final Image image; private boolean active; - public TerminalButtonItemStackCraftingGridJeiSearchSync(TerminalStorageState state, ITerminalStorageTabClient clientTab) { + public TerminalButtonItemStackCraftingGridSearchSync(String mod, TerminalStorageState state, ITerminalStorageTabClient clientTab, Image image) { + this.mod = mod; this.state = state; - this.buttonName = "itemstack_grid_jeisearchsync"; + this.buttonName = "itemstack_grid_" + mod + "searchsync"; this.clientTab = clientTab; + this.image = image; reloadFromState(); } @@ -54,10 +59,10 @@ public void reloadFromState() { @OnlyIn(Dist.CLIENT) public ButtonImage createButton(int x, int y) { return new ButtonImage(x, y, - Component.translatable("gui.integratedterminalscompat.terminal_storage.craftinggrid.jeisync"), + Component.translatable("gui.integratedterminalscompat.terminal_storage.craftinggrid." + mod + "sync"), (b) -> {}, active ? Images.BUTTON_BACKGROUND_ACTIVE : Images.BUTTON_BACKGROUND_INACTIVE, - Images.BUTTON_MIDDLE_JEI_SYNC); + this.image); } @Override @@ -71,13 +76,13 @@ public void onClick(TerminalStorageTabIngredientComponentClient clientTab, @Override public String getTranslationKey() { - return "gui.integratedterminalscompat.terminal_storage.craftinggrid.jeisync"; + return "gui.integratedterminalscompat.terminal_storage.craftinggrid." + mod + "sync"; } @Override @OnlyIn(Dist.CLIENT) public void getTooltip(Player player, TooltipFlag tooltipFlag, List lines) { - lines.add(Component.translatable("gui.integratedterminalscompat.terminal_storage.craftinggrid.jeisync.info").withStyle(ChatFormatting.GRAY)); + lines.add(Component.translatable("gui.integratedterminalscompat.terminal_storage.craftinggrid." + mod + "sync.info").withStyle(ChatFormatting.GRAY)); lines.add(Component.translatable( active ? "general.cyclopscore.info.enabled" : "general.cyclopscore.info.disabled") .withStyle(ChatFormatting.ITALIC)); @@ -86,4 +91,13 @@ public void getTooltip(Player player, TooltipFlag tooltipFlag, List l public boolean isActive() { return active; } + + public static boolean isSearchSynced(ITerminalStorageTabClient clientTab) { + for (ITerminalButton button : clientTab.getButtons()) { + if (button instanceof TerminalButtonItemStackCraftingGridSearchSync) { + return ((TerminalButtonItemStackCraftingGridSearchSync) button).isActive(); + } + } + return false; + } } diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/emi/EmiIntegratedTerminalsConfig.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/emi/EmiIntegratedTerminalsConfig.java new file mode 100644 index 0000000..01e0eb4 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/emi/EmiIntegratedTerminalsConfig.java @@ -0,0 +1,110 @@ +package org.cyclops.integratedterminalscompat.modcompat.emi; + +import dev.emi.emi.api.EmiEntrypoint; +import dev.emi.emi.api.EmiPlugin; +import dev.emi.emi.api.EmiRegistry; +import dev.emi.emi.api.stack.Comparison; +import dev.emi.emi.registry.EmiComparisonDefaults; +import dev.emi.emi.screen.EmiScreenManager; +import net.minecraft.client.Minecraft; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.client.event.ScreenEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import org.cyclops.commoncapabilities.api.capability.itemhandler.ItemMatch; +import org.cyclops.cyclopscore.client.gui.component.input.WidgetTextFieldExtended; +import org.cyclops.integratedterminals.RegistryEntries; +import org.cyclops.integratedterminals.api.terminalstorage.event.TerminalStorageScreenSizeEvent; +import org.cyclops.integratedterminals.api.terminalstorage.event.TerminalStorageTabClientLoadButtonsEvent; +import org.cyclops.integratedterminals.api.terminalstorage.event.TerminalStorageTabClientSearchFieldUpdateEvent; +import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; +import org.cyclops.integratedterminals.client.gui.image.Images; +import org.cyclops.integratedterminalscompat.modcompat.common.button.TerminalButtonItemStackCraftingGridSearchSync; +import org.cyclops.integratedterminalscompat.modcompat.emi.terminalstorage.TerminalStorageEmiRecipeHandler; +import org.cyclops.integratedterminalscompat.modcompat.emi.terminalstorage.TerminalStorageEmiStackProvider; + +/** + * @author rubensworks + */ +@EmiEntrypoint +public class EmiIntegratedTerminalsConfig implements EmiPlugin { + + private boolean loaded = false; + private boolean wasEmiVisible = false; + + public EmiIntegratedTerminalsConfig() { + MinecraftForge.EVENT_BUS.register(this); + } + + public static int getItemStackMatchCondition(ItemStack itemStack) { + Comparison comparison = EmiComparisonDefaults.get(itemStack.getItem()); + return comparison == Comparison.DEFAULT_COMPARISON ? ItemMatch.ITEM : ItemMatch.ITEM | ItemMatch.TAG; + } + + @Override + public void register(EmiRegistry emiRegistry) { + loaded = true; + + emiRegistry.addRecipeHandler(RegistryEntries.CONTAINER_PART_TERMINAL_STORAGE_PART, new TerminalStorageEmiRecipeHandler<>()); + emiRegistry.addRecipeHandler(RegistryEntries.CONTAINER_PART_TERMINAL_STORAGE_ITEM, new TerminalStorageEmiRecipeHandler<>()); + emiRegistry.addGenericStackProvider(new TerminalStorageEmiStackProvider()); + } + + @SubscribeEvent + public void onTerminalStorageButtons(TerminalStorageTabClientLoadButtonsEvent event) { + if (this.loaded && !event.getButtons().stream() + .anyMatch((button) -> button instanceof TerminalButtonItemStackCraftingGridSearchSync)) { + event.getButtons().add(new TerminalButtonItemStackCraftingGridSearchSync( + "emi", event.getContainer().getGuiState(), event.getClientTab(), Images.BUTTON_MIDDLE_EMI_SYNC)); + } + } + + @SubscribeEvent + public void onTerminalStorageScreenSize(TerminalStorageScreenSizeEvent event) { + if (this.loaded) { + try { + boolean isOpen = !EmiScreenManager.isDisabled(); + boolean wasJeiVisiblePrevious = wasEmiVisible; + if (isOpen) { + wasEmiVisible = true; + event.setHeight(event.getHeight() - 20); + event.setWidth(event.getWidth() - 170); + } else { + wasEmiVisible = false; + } + + // Re-init screen if JEI was just made (in)visible + if (wasJeiVisiblePrevious != wasEmiVisible) { + ((ContainerScreenTerminalStorage) Minecraft.getInstance().screen).init(); + } + } catch (NoClassDefFoundError | ClassCastException e) { + // Do nothing when we detect some JEI API issues + } + } + } + + @SubscribeEvent + public void onSearchFieldUpdated(TerminalStorageTabClientSearchFieldUpdateEvent event) { + // Copy the terminal search box contents into the JEI search box. + if (!EmiScreenManager.isDisabled() && TerminalButtonItemStackCraftingGridSearchSync.isSearchSynced(event.getClientTab())) { + EmiScreenManager.search.setValue(event.getSearchString() + ""); + } + } + + @SubscribeEvent + public void onKeyTyped(ScreenEvent.KeyReleased.Post event) { + // Copy the JEI search box contents into the terminal search box. + if (event.getScreen() instanceof ContainerScreenTerminalStorage) { + ContainerScreenTerminalStorage gui = ((ContainerScreenTerminalStorage) event.getScreen()); + if (!EmiScreenManager.isDisabled() && EmiScreenManager.search.isFocused()) { + gui.getSelectedClientTab().ifPresent(tab -> { + if (TerminalButtonItemStackCraftingGridSearchSync.isSearchSynced(tab)) { + WidgetTextFieldExtended fieldSearch = gui.getFieldSearch(); + fieldSearch.setValue(EmiScreenManager.search.getValue()); + tab.setInstanceFilter(gui.getMenu().getSelectedChannel(), fieldSearch.getValue() + ""); + } + }); + } + } + } +} diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/emi/terminalstorage/RecipeInputSlotEmiIngredient.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/emi/terminalstorage/RecipeInputSlotEmiIngredient.java new file mode 100644 index 0000000..c459af8 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/emi/terminalstorage/RecipeInputSlotEmiIngredient.java @@ -0,0 +1,41 @@ +package org.cyclops.integratedterminalscompat.modcompat.emi.terminalstorage; + +import com.google.common.collect.Lists; +import dev.emi.emi.api.stack.EmiIngredient; +import dev.emi.emi.api.stack.EmiStack; +import net.minecraft.world.item.ItemStack; +import org.cyclops.integratedterminalscompat.modcompat.common.RecipeInputSlot; +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; + +/** + * @author rubensworks + */ +public class RecipeInputSlotEmiIngredient implements RecipeInputSlot { + + private final boolean input; + private final EmiIngredient emiIngredient; + + public RecipeInputSlotEmiIngredient(EmiIngredient emiIngredient, boolean input) { + this.emiIngredient = emiIngredient; + this.input = input; + } + + @NotNull + @Override + public Iterator iterator() { + if (!this.input) { + return Lists.newArrayList().iterator(); + } + return emiIngredient.getEmiStacks().stream() + .map(EmiStack::getItemStack) + .filter(stack -> !stack.isEmpty()) + .iterator(); + } + + @Override + public boolean isEmpty() { + return !this.input || emiIngredient.isEmpty(); + } +} diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/emi/terminalstorage/RecipeInputSlotEmiSlotWidget.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/emi/terminalstorage/RecipeInputSlotEmiSlotWidget.java new file mode 100644 index 0000000..de829dd --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/emi/terminalstorage/RecipeInputSlotEmiSlotWidget.java @@ -0,0 +1,43 @@ +package org.cyclops.integratedterminalscompat.modcompat.emi.terminalstorage; + +import com.google.common.collect.Lists; +import dev.emi.emi.api.stack.EmiStack; +import dev.emi.emi.api.widget.SlotWidget; +import net.minecraft.world.item.ItemStack; +import org.cyclops.integratedterminalscompat.modcompat.common.RecipeInputSlot; +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; + +/** + * @author rubensworks + */ +public class RecipeInputSlotEmiSlotWidget implements RecipeInputSlot { + + private final SlotWidget slotWidget; + + public RecipeInputSlotEmiSlotWidget(SlotWidget slotWidget) { + this.slotWidget = slotWidget; + } + + public SlotWidget getSlotWidget() { + return slotWidget; + } + + @NotNull + @Override + public Iterator iterator() { + if (slotWidget.getRecipe() != null) { + return Lists.newArrayList().iterator(); + } + return slotWidget.getStack().getEmiStacks().stream() + .map(EmiStack::getItemStack) + .filter(stack -> !stack.isEmpty()) + .iterator(); + } + + @Override + public boolean isEmpty() { + return slotWidget.getRecipe() != null || slotWidget.getStack().isEmpty(); + } +} diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/emi/terminalstorage/TerminalStorageEmiRecipeHandler.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/emi/terminalstorage/TerminalStorageEmiRecipeHandler.java new file mode 100644 index 0000000..1d35042 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/emi/terminalstorage/TerminalStorageEmiRecipeHandler.java @@ -0,0 +1,167 @@ +package org.cyclops.integratedterminalscompat.modcompat.emi.terminalstorage; + +import com.google.common.collect.Lists; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.emi.emi.api.recipe.EmiCraftingRecipe; +import dev.emi.emi.api.recipe.EmiPlayerInventory; +import dev.emi.emi.api.recipe.EmiRecipe; +import dev.emi.emi.api.recipe.handler.EmiCraftContext; +import dev.emi.emi.api.recipe.handler.EmiRecipeHandler; +import dev.emi.emi.api.stack.EmiIngredient; +import dev.emi.emi.api.stack.EmiStack; +import dev.emi.emi.api.widget.Bounds; +import dev.emi.emi.api.widget.RecipeFillButtonWidget; +import dev.emi.emi.api.widget.SlotWidget; +import dev.emi.emi.api.widget.Widget; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; +import net.minecraft.world.item.ItemStack; +import org.apache.commons.lang3.tuple.Pair; +import org.cyclops.cyclopscore.datastructure.Wrapper; +import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentClient; +import org.cyclops.integratedterminals.inventory.container.ContainerTerminalStorageBase; +import org.cyclops.integratedterminalscompat.modcompat.common.RecipeTransferHelpers; +import org.cyclops.integratedterminalscompat.modcompat.common.RecipeTransferResult; +import org.cyclops.integratedterminalscompat.modcompat.emi.EmiIntegratedTerminalsConfig; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author rubensworks + */ +public class TerminalStorageEmiRecipeHandler> implements EmiRecipeHandler { + + private int previousChangeId; + + @Override + public EmiPlayerInventory getInventory(AbstractContainerScreen screen) { + T container = screen.getMenu(); + return RecipeTransferHelpers.getTabs(container) + .map(tabs -> { + List> unfilteredIngredients = tabs.getValue() + .getUnfilteredIngredientsView(container.getSelectedChannel()); + return new EmiPlayerInventory(unfilteredIngredients.stream() + .filter(ingredient -> ingredient.getCraftingOption() == null) + .map(TerminalStorageTabIngredientComponentClient.InstanceWithMetadata::getInstance) + .map(EmiStack::of) + .collect(Collectors.toList())); + }) + .orElseGet(() -> new EmiPlayerInventory(Lists.newArrayList())); + } + + @Override + public boolean supportsRecipe(EmiRecipe emiRecipe) { + return emiRecipe instanceof EmiCraftingRecipe; + } + + @Override + public boolean canCraft(EmiRecipe emiRecipe, EmiCraftContext emiCraftContext) { + return true; + } + + @Override + public boolean craft(EmiRecipe recipe, EmiCraftContext context) { + T container = context.getScreen().getMenu(); + RecipeTransferHelpers.getTabs(container) + .ifPresent(tabs -> { + // Determine input slots + Collection recipeInputSlots = Lists.newArrayList(); + for (EmiIngredient input : recipe.getOutputs()) { + recipeInputSlots.add(new RecipeInputSlotEmiIngredient(input, false)); + } + for (EmiIngredient input : recipe.getInputs()) { + recipeInputSlots.add(new RecipeInputSlotEmiIngredient(input, true)); + } + + // Transfer recipe + RecipeTransferHelpers.transferRecipe( + container, + recipeInputSlots, + Minecraft.getInstance().player, + tabs.getLeft(), + EmiIntegratedTerminalsConfig::getItemStackMatchCondition, + false + ); + }); + return true; + } + + @Override + public List getTooltip(EmiRecipe recipe, EmiCraftContext context) { + T container = context.getScreen().getMenu(); + return RecipeTransferHelpers.getTabs(container) + .map(tabs -> { + // Determine input slots + Collection recipeInputSlots = Lists.newArrayList(); + for (EmiIngredient input : recipe.getInputs()) { + recipeInputSlots.add(new RecipeInputSlotEmiIngredient(input, true)); + } + + // Calculate missing items + return RecipeTransferHelpers.getMissingItems( + Pair.of("tooltip", recipe), + container, + recipeInputSlots, + Minecraft.getInstance().player, + tabs.getLeft(), + tabs.getRight(), + EmiIntegratedTerminalsConfig::getItemStackMatchCondition, + () -> previousChangeId, + id -> previousChangeId = id + ) + .map(result -> result.getMessage().stream().map(c -> ClientTooltipComponent.create(c.getVisualOrderText())).collect(Collectors.toList())) + .orElseGet(List::of); + }) + .orElseGet(List::of); + } + + @Override + public void render(EmiRecipe recipe, EmiCraftContext context, List widgets, GuiGraphics guiGraphics) { + T container = context.getScreen().getMenu(); + RecipeTransferHelpers.getTabs(container) + .ifPresent(tabs -> { + // Determine input slots + Collection recipeInputSlots = Lists.newArrayList(); + Wrapper fillButton = new Wrapper<>(); + for (Widget widget : widgets) { + if (widget instanceof SlotWidget slotWidget) { + recipeInputSlots.add(new RecipeInputSlotEmiSlotWidget(slotWidget)); + } + if (widget instanceof RecipeFillButtonWidget fillButtonWidget) { + fillButton.set(fillButtonWidget); + } + } + + // Calculate missing items + RecipeTransferHelpers.getMissingItems( + Pair.of("render", recipe), + container, + recipeInputSlots, + Minecraft.getInstance().player, + tabs.getLeft(), + tabs.getRight(), + EmiIntegratedTerminalsConfig::getItemStackMatchCondition, + () -> previousChangeId, + id -> previousChangeId = id + ).ifPresent(transferResult -> { + // Render overlay on slots + for (RecipeInputSlotEmiSlotWidget slot : transferResult.getSlotsMissing()) { + Bounds bounds = slot.getSlotWidget().getBounds(); + guiGraphics.fill(bounds.x(), bounds.y(), bounds.x() + bounds.width(), bounds.y() + bounds.height(), RecipeTransferResult.SLOT_COLOR_MISSING); + } + for (RecipeInputSlotEmiSlotWidget slot : transferResult.getSlotsCraftable()) { + Bounds bounds = slot.getSlotWidget().getBounds(); + guiGraphics.fill(bounds.x(), bounds.y(), bounds.x() + bounds.width(), bounds.y() + bounds.height(), RecipeTransferResult.SLOT_COLOR_CRAFTABLE); + } + + // Render overlay on button + Bounds bounds = fillButton.get().getBounds(); + guiGraphics.fill(bounds.x(), bounds.y(), bounds.x() + bounds.width(), bounds.y() + bounds.height(), transferResult.getButtonHighlightColor()); + }); + }); + } +} diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/emi/terminalstorage/TerminalStorageEmiStackProvider.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/emi/terminalstorage/TerminalStorageEmiStackProvider.java new file mode 100644 index 0000000..4227cb9 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/emi/terminalstorage/TerminalStorageEmiStackProvider.java @@ -0,0 +1,43 @@ +package org.cyclops.integratedterminalscompat.modcompat.emi.terminalstorage; + +import dev.emi.emi.api.EmiStackProvider; +import dev.emi.emi.api.stack.EmiStack; +import dev.emi.emi.api.stack.EmiStackInteraction; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; +import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; +import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentClient; + +import java.util.Optional; + +/** + * @author rubensworks + */ +public class TerminalStorageEmiStackProvider implements EmiStackProvider { + @Override + public EmiStackInteraction getStackAt(Screen screen, int mouseX, int mouseY) { + if (screen instanceof ContainerScreenTerminalStorage containerScreen) { + return createClickableIngredient(containerScreen, mouseX, mouseY); + } + return EmiStackInteraction.EMPTY; + } + + private EmiStackInteraction createClickableIngredient( + ContainerScreenTerminalStorage containerScreen, int mouseX, int mouseY) { + int slotIndex = containerScreen.getStorageSlotIndexAtPosition(mouseX, mouseY); + @SuppressWarnings("unchecked") // Cast is safe due to filter + Optional> tabOptional = containerScreen.getSelectedClientTab() + .filter(TerminalStorageTabIngredientComponentClient.class::isInstance) + .map(TerminalStorageTabIngredientComponentClient.class::cast); + if(slotIndex >= 0 && tabOptional.isPresent()) { + TerminalStorageTabIngredientComponentClient tab = tabOptional.get(); + int channel = containerScreen.getMenu().getSelectedChannel(); + Optional instanceOptional = tab.getSlotInstance(channel, slotIndex); + return instanceOptional + .map(instance -> new EmiStackInteraction(instance instanceof ItemStack itemStack ? EmiStack.of(itemStack) : (instance instanceof FluidStack fluidStack ? EmiStack.of(fluidStack.getFluid(), fluidStack.getAmount()) : EmiStack.EMPTY), null, false)) + .orElse(EmiStackInteraction.EMPTY); + } + return EmiStackInteraction.EMPTY; + } +} diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/JEIIntegratedTerminalsConfig.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/JEIIntegratedTerminalsConfig.java index 8d1160a..fd2221e 100644 --- a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/JEIIntegratedTerminalsConfig.java +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/JEIIntegratedTerminalsConfig.java @@ -21,19 +21,18 @@ import org.cyclops.commoncapabilities.api.capability.itemhandler.ItemMatch; import org.cyclops.cyclopscore.client.gui.component.input.WidgetTextFieldExtended; import org.cyclops.integratedterminals.RegistryEntries; -import org.cyclops.integratedterminals.api.terminalstorage.ITerminalButton; -import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabClient; import org.cyclops.integratedterminals.api.terminalstorage.event.TerminalStorageScreenSizeEvent; import org.cyclops.integratedterminals.api.terminalstorage.event.TerminalStorageTabClientLoadButtonsEvent; import org.cyclops.integratedterminals.api.terminalstorage.event.TerminalStorageTabClientSearchFieldUpdateEvent; import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; +import org.cyclops.integratedterminals.client.gui.image.Images; import org.cyclops.integratedterminals.inventory.container.ContainerTerminalStorageItem; import org.cyclops.integratedterminals.inventory.container.ContainerTerminalStoragePart; import org.cyclops.integratedterminals.part.PartTypes; import org.cyclops.integratedterminalscompat.Reference; +import org.cyclops.integratedterminalscompat.modcompat.common.button.TerminalButtonItemStackCraftingGridSearchSync; import org.cyclops.integratedterminalscompat.modcompat.jei.terminalstorage.TerminalStorageGuiHandler; import org.cyclops.integratedterminalscompat.modcompat.jei.terminalstorage.TerminalStorageRecipeTransferHandler; -import org.cyclops.integratedterminalscompat.modcompat.jei.terminalstorage.button.TerminalButtonItemStackCraftingGridJeiSearchSync; /** * Helper for registering JEI manager. @@ -72,11 +71,9 @@ public void registerIngredients(IModIngredientRegistration registration) { @Override public void registerRecipeTransferHandlers(IRecipeTransferRegistration registration) { registration.addUniversalRecipeTransferHandler( - new TerminalStorageRecipeTransferHandler<>(registration.getTransferHelper(), - ContainerTerminalStoragePart.class, RegistryEntries.CONTAINER_PART_TERMINAL_STORAGE_PART)); + new TerminalStorageRecipeTransferHandler<>(ContainerTerminalStoragePart.class, RegistryEntries.CONTAINER_PART_TERMINAL_STORAGE_PART)); registration.addUniversalRecipeTransferHandler( - new TerminalStorageRecipeTransferHandler<>(registration.getTransferHelper(), - ContainerTerminalStorageItem.class, RegistryEntries.CONTAINER_PART_TERMINAL_STORAGE_ITEM)); + new TerminalStorageRecipeTransferHandler<>(ContainerTerminalStorageItem.class, RegistryEntries.CONTAINER_PART_TERMINAL_STORAGE_ITEM)); } @Override @@ -104,10 +101,10 @@ public void onRuntimeAvailable(IJeiRuntime jeiRuntime) { @SubscribeEvent public void onTerminalStorageButtons(TerminalStorageTabClientLoadButtonsEvent event) { - if (!event.getButtons().stream() - .anyMatch((button) -> button instanceof TerminalButtonItemStackCraftingGridJeiSearchSync)) { - event.getButtons().add(new TerminalButtonItemStackCraftingGridJeiSearchSync( - event.getContainer().getGuiState(), event.getClientTab())); + if (jeiRuntime != null && !event.getButtons().stream() + .anyMatch((button) -> button instanceof TerminalButtonItemStackCraftingGridSearchSync)) { + event.getButtons().add(new TerminalButtonItemStackCraftingGridSearchSync( + "jei", event.getContainer().getGuiState(), event.getClientTab(), Images.BUTTON_MIDDLE_JEI_SYNC)); } } @@ -134,19 +131,10 @@ public void onTerminalStorageScreenSize(TerminalStorageScreenSizeEvent event) { } } - protected boolean isSearchSynced(ITerminalStorageTabClient clientTab) { - for (ITerminalButton button : clientTab.getButtons()) { - if (button instanceof TerminalButtonItemStackCraftingGridJeiSearchSync) { - return ((TerminalButtonItemStackCraftingGridJeiSearchSync) button).isActive(); - } - } - return false; - } - @SubscribeEvent public void onSearchFieldUpdated(TerminalStorageTabClientSearchFieldUpdateEvent event) { // Copy the terminal search box contents into the JEI search box. - if (isSearchSynced(event.getClientTab())) { + if (jeiRuntime != null && TerminalButtonItemStackCraftingGridSearchSync.isSearchSynced(event.getClientTab())) { jeiRuntime.getIngredientFilter().setFilterText(event.getSearchString() + ""); } } @@ -156,9 +144,9 @@ public void onKeyTyped(ScreenEvent.KeyReleased.Post event) { // Copy the JEI search box contents into the terminal search box. if (event.getScreen() instanceof ContainerScreenTerminalStorage) { ContainerScreenTerminalStorage gui = ((ContainerScreenTerminalStorage) event.getScreen()); - if (jeiRuntime.getIngredientListOverlay().hasKeyboardFocus()) { + if (jeiRuntime != null && jeiRuntime.getIngredientListOverlay().hasKeyboardFocus()) { gui.getSelectedClientTab().ifPresent(tab -> { - if (isSearchSynced(tab)) { + if (TerminalButtonItemStackCraftingGridSearchSync.isSearchSynced(tab)) { WidgetTextFieldExtended fieldSearch = gui.getFieldSearch(); fieldSearch.setValue(jeiRuntime.getIngredientFilter().getFilterText()); tab.setInstanceFilter(gui.getMenu().getSelectedChannel(), fieldSearch.getValue() + ""); diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/RecipeInputSlotJei.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/RecipeInputSlotJei.java new file mode 100644 index 0000000..71b6d01 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/RecipeInputSlotJei.java @@ -0,0 +1,44 @@ +package org.cyclops.integratedterminalscompat.modcompat.jei; + +import com.google.common.collect.Lists; +import mezz.jei.api.gui.ingredient.IRecipeSlotView; +import mezz.jei.api.ingredients.ITypedIngredient; +import mezz.jei.api.recipe.RecipeIngredientRole; +import net.minecraft.world.item.ItemStack; +import org.cyclops.integratedterminalscompat.modcompat.common.RecipeInputSlot; +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; +import java.util.stream.Stream; + +/** + * @author rubensworks + */ +public class RecipeInputSlotJei implements RecipeInputSlot { + + private final IRecipeSlotView slotView; + + public RecipeInputSlotJei(IRecipeSlotView slotView) { + this.slotView = slotView; + } + + @Override + public boolean isEmpty() { + return this.slotView.getRole() != RecipeIngredientRole.INPUT || this.slotView.isEmpty(); + } + + public IRecipeSlotView getSlotView() { + return slotView; + } + + @NotNull + @Override + public Iterator iterator() { + if (this.slotView.getRole() != RecipeIngredientRole.INPUT) { + return Lists.newArrayList().iterator(); + } + return ((Stream>) (Stream) slotView.getAllIngredients()) + .map(ITypedIngredient::getIngredient) + .iterator(); + } +} diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/RecipeTransferErrorTransferResult.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/RecipeTransferErrorTransferResult.java new file mode 100644 index 0000000..8b914df --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/RecipeTransferErrorTransferResult.java @@ -0,0 +1,46 @@ +package org.cyclops.integratedterminalscompat.modcompat.jei; + +import mezz.jei.api.gui.ingredient.IRecipeSlotsView; +import mezz.jei.api.recipe.transfer.IRecipeTransferError; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import org.cyclops.integratedterminalscompat.modcompat.common.RecipeTransferResult; + +import java.util.stream.Stream; + +/** + * @author rubensworks + */ +public class RecipeTransferErrorTransferResult implements IRecipeTransferError { + + private final RecipeTransferResult result; + + public RecipeTransferErrorTransferResult(RecipeTransferResult result) { + this.result = result; + } + + @Override + public Type getType() { + return Type.COSMETIC; + } + + @Override + public int getButtonHighlightColor() { + return this.result.getButtonHighlightColor(); + } + + @Override + public void showError(GuiGraphics guiGraphics, int mouseX, int mouseY, IRecipeSlotsView recipeSlotsView, int recipeX, int recipeY) { + guiGraphics.renderComponentTooltip(Minecraft.getInstance().font, Stream.concat(Stream.of(Component.translatable("jei.tooltip.transfer")), this.result.getMessage().stream()).toList(), mouseX, mouseY); + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(recipeX, recipeY, 0.0); + for (RecipeInputSlotJei slot : this.result.getSlotsMissing()) { + slot.getSlotView().drawHighlight(guiGraphics, RecipeTransferResult.SLOT_COLOR_MISSING); + } + for (RecipeInputSlotJei slot : this.result.getSlotsCraftable()) { + slot.getSlotView().drawHighlight(guiGraphics, RecipeTransferResult.SLOT_COLOR_CRAFTABLE); + } + guiGraphics.pose().popPose(); + } +} diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/terminalstorage/TerminalStorageRecipeTransferHandler.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/terminalstorage/TerminalStorageRecipeTransferHandler.java index 4880f34..91f485c 100644 --- a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/terminalstorage/TerminalStorageRecipeTransferHandler.java +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/jei/terminalstorage/TerminalStorageRecipeTransferHandler.java @@ -1,73 +1,43 @@ package org.cyclops.integratedterminalscompat.modcompat.jei.terminalstorage; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; import com.google.common.collect.Lists; -import com.google.common.collect.Maps; import mezz.jei.api.constants.RecipeTypes; import mezz.jei.api.constants.VanillaTypes; import mezz.jei.api.gui.ingredient.IRecipeSlotView; import mezz.jei.api.gui.ingredient.IRecipeSlotsView; -import mezz.jei.api.ingredients.ITypedIngredient; import mezz.jei.api.recipe.RecipeIngredientRole; import mezz.jei.api.recipe.RecipeType; import mezz.jei.api.recipe.transfer.IRecipeTransferError; import mezz.jei.api.recipe.transfer.IRecipeTransferHandler; -import mezz.jei.api.recipe.transfer.IRecipeTransferHandlerHelper; import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.MenuType; -import net.minecraft.world.inventory.Slot; -import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.CraftingRecipe; -import net.minecraftforge.items.wrapper.InvWrapper; -import org.apache.commons.lang3.tuple.Pair; -import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent; -import org.cyclops.commoncapabilities.ingredient.storage.IngredientComponentStorageWrapperHandlerItemStack; -import org.cyclops.cyclopscore.ingredient.collection.IIngredientCollectionMutable; -import org.cyclops.cyclopscore.ingredient.collection.IngredientCollectionHelpers; -import org.cyclops.cyclopscore.ingredient.collection.IngredientCollectionPrototypeMap; import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabCommon; import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentClient; import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentItemStackCrafting; import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentItemStackCraftingCommon; import org.cyclops.integratedterminals.inventory.container.ContainerTerminalStorageBase; -import org.cyclops.integratedterminalscompat.IntegratedTerminalsCompat; +import org.cyclops.integratedterminalscompat.modcompat.common.RecipeTransferHelpers; import org.cyclops.integratedterminalscompat.modcompat.jei.JEIIntegratedTerminalsConfig; -import org.cyclops.integratedterminalscompat.network.packet.TerminalStorageIngredientItemStackCraftingGridSetRecipe; +import org.cyclops.integratedterminalscompat.modcompat.jei.RecipeInputSlotJei; +import org.cyclops.integratedterminalscompat.modcompat.jei.RecipeTransferErrorTransferResult; import javax.annotation.Nullable; -import java.util.List; -import java.util.Map; +import java.util.Collection; import java.util.Objects; import java.util.Optional; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.stream.Stream; /** * Handles recipe clicking from JEI. * @author rubensworks */ public class TerminalStorageRecipeTransferHandler> implements IRecipeTransferHandler { - // The amount of seconds recipeErrors will be cached for transferRecipe - private static final long RECIPE_ERROR_CACHE_TIME = 60; - private final IRecipeTransferHandlerHelper recipeTransferHandlerHelper; private final Class clazz; private final MenuType menuType; + private int previousChangeId; - private long previousChangeId; - private final Cache> recipeErrorCache = CacheBuilder.newBuilder() - .expireAfterAccess(RECIPE_ERROR_CACHE_TIME, TimeUnit.SECONDS) - .build(); - - - public TerminalStorageRecipeTransferHandler(IRecipeTransferHandlerHelper recipeTransferHandlerHelper, Class clazz, MenuType menuType) { - this.recipeTransferHandlerHelper = recipeTransferHandlerHelper; + public TerminalStorageRecipeTransferHandler(Class clazz, MenuType menuType) { this.clazz = clazz; this.menuType = menuType; } @@ -99,71 +69,17 @@ public IRecipeTransferError transferRecipe(T container, CraftingRecipe recipe, I if (!doTransfer) { TerminalStorageTabIngredientComponentClient tabClient = (TerminalStorageTabIngredientComponentClient) container.getTabClient(container.getSelectedTab()); - Callable> missingItemsSupplier = - () -> getMissingItems(container, recipe, recipeLayout, player, tabCommonCrafting); - if (previousChangeId != tabClient.getLastChangeId()) { - // Clear cache when storage contents changed - recipeErrorCache.invalidateAll(); - } - try { - return recipeErrorCache.get(recipe, missingItemsSupplier).orElse(null); - } catch (ExecutionException e) { - // Throw exceptions from missingItemsSupplier - throw new RuntimeException(e); - } + return getMissingItems(container, recipe, recipeLayout, player, tabCommonCrafting, tabClient) + .orElse(null); } else { - IngredientComponentStorageWrapperHandlerItemStack.ComponentStorageWrapper playerInventory = - new IngredientComponentStorageWrapperHandlerItemStack.ComponentStorageWrapper(IngredientComponent.ITEMSTACK, new InvWrapper(player.getInventory())); - - // Send a packet to the server if the recipe effectively needs to be applied to the grid - Map> slottedIngredientsFromPlayer = Maps.newHashMap(); - Map>> slottedIngredientsFromStorage = Maps.newHashMap(); - int slotOffset = tabCommonCrafting.getSlotCrafting().index; - int slotId = 0; - for (IRecipeSlotView slotView : recipeLayout.getSlotViews()) { - if (!slotView.isEmpty() && slotView.getRole() == RecipeIngredientRole.INPUT) { - ITypedIngredient typedIngredient = slotView.getAllIngredients().findFirst().get(); - if (typedIngredient.getType() == VanillaTypes.ITEM_STACK) { - boolean found = false; - - // First check if we can transfer from the player inventory - // No need to check the crafting grid, as the server will first clear the grid into the storage in TerminalStorageIngredientItemStackCraftingGridSetRecipe - Set allIngredients = ((Stream>) (Stream) slotView.getAllIngredients()) - .map(ITypedIngredient::getIngredient) - .collect(Collectors.toSet()); - for (ItemStack itemStack : allIngredients) { - int matchCondition = JEIIntegratedTerminalsConfig.getItemStackMatchCondition(itemStack); - - if (!playerInventory.extract(itemStack, matchCondition, true).isEmpty()) { - found = true; - - // Move from player to crafting grid - ItemStack extracted = playerInventory.extract(itemStack, matchCondition, false); - Slot slot = container.getSlot(slotId + slotOffset); - slot.set(extracted); - - // Do the exact same thing server-side - slottedIngredientsFromPlayer.put(slotId, Pair.of(itemStack, JEIIntegratedTerminalsConfig.getItemStackMatchCondition(itemStack))); - - break; - } - } - - if (!found) { - // Otherwise, request them from the storage - slottedIngredientsFromStorage.put(slotId, allIngredients - .stream() - .map(itemStack -> Pair.of(itemStack, JEIIntegratedTerminalsConfig.getItemStackMatchCondition(itemStack))) - .collect(Collectors.toList())); - } - } - } - slotId++; - } - - IntegratedTerminalsCompat._instance.getPacketHandler().sendToServer( - new TerminalStorageIngredientItemStackCraftingGridSetRecipe(container.getSelectedTab(), - container.getSelectedChannel(), maxTransfer, slottedIngredientsFromPlayer, slottedIngredientsFromStorage)); + RecipeTransferHelpers.transferRecipe( + container, + getRecipeInputSlots(recipeLayout), + player, + tabCommonCrafting, + JEIIntegratedTerminalsConfig::getItemStackMatchCondition, + maxTransfer + ); return null; } } @@ -171,78 +87,28 @@ public IRecipeTransferError transferRecipe(T container, CraftingRecipe recipe, I return new TransferError(); } - private Optional getMissingItems(T container, CraftingRecipe recipe, IRecipeSlotsView recipeLayout, Player player, TerminalStorageTabIngredientComponentItemStackCraftingCommon tabCommonCrafting) { - TerminalStorageTabIngredientComponentClient tabClient = (TerminalStorageTabIngredientComponentClient) - container.getTabClient(container.getSelectedTab()); - - // Check in the player inventory and local client view if the required recipe ingredients are available - - // Build crafting grid index - IIngredientCollectionMutable hayStackCraftingGrid = new IngredientCollectionPrototypeMap<>(IngredientComponent.ITEMSTACK); - for (int slot = 0; slot < tabCommonCrafting.getInventoryCrafting().getContainerSize(); slot++) { - hayStackCraftingGrid.add(tabCommonCrafting.getInventoryCrafting().getItem(slot)); - } - - // Build player inventory index - IIngredientCollectionMutable hayStackPlayer = new IngredientCollectionPrototypeMap<>(IngredientComponent.ITEMSTACK); - hayStackPlayer.addAll(player.getInventory().items); - - // Build local client view of storage - List> unfilteredIngredients = tabClient - .getUnfilteredIngredientsView(container.getSelectedChannel()); - IIngredientCollectionMutable hayStack = IngredientCollectionHelpers.createCollapsedCollection(IngredientComponent.ITEMSTACK); - hayStack.addAll(unfilteredIngredients - .stream() - .filter(i -> i.getCraftingOption() == null) - .map(TerminalStorageTabIngredientComponentClient.InstanceWithMetadata::getInstance) - .collect(Collectors.toList())); - - List slotsMissingItems = Lists.newArrayList(); + private Collection getRecipeInputSlots(IRecipeSlotsView recipeLayout) { + Collection recipeInputSlots = Lists.newArrayList(); for (IRecipeSlotView slotView : recipeLayout.getSlotViews()) { - if (!slotView.isEmpty() && slotView.getRole() == RecipeIngredientRole.INPUT) { - ITypedIngredient typedIngredient = slotView.getAllIngredients().findFirst().get(); - if (typedIngredient.getType() == VanillaTypes.ITEM_STACK) { - boolean found = false; - for (ItemStack itemStack : ((Stream>) (Stream) slotView.getAllIngredients()) - .map(ITypedIngredient::getIngredient) - .collect(Collectors.toSet())) { - int matchCondition = JEIIntegratedTerminalsConfig.getItemStackMatchCondition(itemStack); - - // First check in the crafting grid - if (hayStackCraftingGrid.contains(itemStack, matchCondition)) { - hayStackPlayer.remove(itemStack); - found = true; - break; - } - - // Then check in player inventory - if (hayStackPlayer.contains(itemStack, matchCondition)) { - hayStackPlayer.remove(itemStack); - found = true; - break; - } - - // Then check the storage - if (hayStack.contains(itemStack, matchCondition)) { - hayStack.remove(itemStack); - found = true; - break; - } - } - if (!found) { - slotsMissingItems.add(slotView); - } - } + if (slotView.getRole() != RecipeIngredientRole.INPUT || slotView.isEmpty() || slotView.getAllIngredients().findFirst().get().getType() == VanillaTypes.ITEM_STACK) { + recipeInputSlots.add(new RecipeInputSlotJei(slotView)); } } + return recipeInputSlots; + } - previousChangeId = tabClient.getLastChangeId(); - if (!slotsMissingItems.isEmpty()) { - Component message = Component.translatable("jei.tooltip.error.recipe.transfer.missing"); - return Optional.of(recipeTransferHandlerHelper.createUserErrorForMissingSlots(message, slotsMissingItems)); - } - - return Optional.empty(); + private Optional getMissingItems(T container, CraftingRecipe recipe, IRecipeSlotsView recipeLayout, Player player, TerminalStorageTabIngredientComponentItemStackCraftingCommon tabCommonCrafting, TerminalStorageTabIngredientComponentClient tabClient) { + return RecipeTransferHelpers.getMissingItems( + recipe, + container, + getRecipeInputSlots(recipeLayout), + player, + tabCommonCrafting, + tabClient, + JEIIntegratedTerminalsConfig::getItemStackMatchCondition, + () -> previousChangeId, + id -> previousChangeId = id + ).map(RecipeTransferErrorTransferResult::new); } public static class TransferError implements IRecipeTransferError { diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/rei/RecipeInputSlotRei.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/rei/RecipeInputSlotRei.java new file mode 100644 index 0000000..28ff67c --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/rei/RecipeInputSlotRei.java @@ -0,0 +1,45 @@ +package org.cyclops.integratedterminalscompat.modcompat.rei; + +import com.google.common.collect.Lists; +import me.shedaniel.rei.api.common.entry.EntryIngredient; +import net.minecraft.world.item.ItemStack; +import org.cyclops.integratedterminalscompat.modcompat.common.RecipeInputSlot; +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; + +/** + * @author rubensworks + */ +public class RecipeInputSlotRei implements RecipeInputSlot { + + private final EntryIngredient ingredient; + private final boolean input; + private final int index; + + public RecipeInputSlotRei(EntryIngredient ingredient, boolean input, int index) { + this.ingredient = ingredient; + this.input = input; + this.index = index; + } + + public int getIndex() { + return index; + } + + @Override + public boolean isEmpty() { + return !this.input || this.ingredient.isEmpty(); + } + + @NotNull + @Override + public Iterator iterator() { + if (!this.input) { + return Lists.newArrayList().iterator(); + } + return this.ingredient.stream() + .map(i -> i.castValue()) + .iterator(); + } +} diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/rei/ReiIntegratedTerminalsConfig.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/rei/ReiIntegratedTerminalsConfig.java new file mode 100644 index 0000000..464d185 --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/rei/ReiIntegratedTerminalsConfig.java @@ -0,0 +1,122 @@ +package org.cyclops.integratedterminalscompat.modcompat.rei; + +import me.shedaniel.rei.api.client.REIRuntime; +import me.shedaniel.rei.api.client.plugins.REIClientPlugin; +import me.shedaniel.rei.api.client.registry.screen.ScreenRegistry; +import me.shedaniel.rei.api.client.registry.transfer.TransferHandlerRegistry; +import me.shedaniel.rei.api.common.entry.comparison.ItemComparatorRegistry; +import me.shedaniel.rei.forge.REIPluginClient; +import net.minecraft.client.Minecraft; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.client.event.ScreenEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import org.cyclops.commoncapabilities.api.capability.itemhandler.ItemMatch; +import org.cyclops.cyclopscore.client.gui.component.input.WidgetTextFieldExtended; +import org.cyclops.integratedterminals.api.terminalstorage.event.TerminalStorageScreenSizeEvent; +import org.cyclops.integratedterminals.api.terminalstorage.event.TerminalStorageTabClientLoadButtonsEvent; +import org.cyclops.integratedterminals.api.terminalstorage.event.TerminalStorageTabClientSearchFieldUpdateEvent; +import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; +import org.cyclops.integratedterminals.client.gui.image.Images; +import org.cyclops.integratedterminalscompat.modcompat.common.button.TerminalButtonItemStackCraftingGridSearchSync; +import org.cyclops.integratedterminalscompat.modcompat.rei.terminalstorage.TerminalStorageReiFocusedStackProvider; +import org.cyclops.integratedterminalscompat.modcompat.rei.terminalstorage.TerminalStorageReiTransferHandler; + +/** + * @author rubensworks + */ +@REIPluginClient +public class ReiIntegratedTerminalsConfig implements REIClientPlugin { + + private static ItemComparatorRegistry itemComparatorRegistry; + + public static int getItemStackMatchCondition(ItemStack itemStack) { + // By default, REI ignores NBT when matching items, unless sub type info is set. + + // Ideally, we would have to do a plain item extraction, and filter the items that match the subtype string, + // but just using the heuristic that the existence of sub type info implies NBT matching seems to work out so far. + // So if we would run into problems with this, this filtering is what we'd need to do. + + return !itemComparatorRegistry.containsComparator(itemStack.getItem()) ? ItemMatch.ITEM : ItemMatch.ITEM | ItemMatch.TAG; + } + + private boolean loaded = false; + private boolean wasReiVisible = false; + + public ReiIntegratedTerminalsConfig() { + MinecraftForge.EVENT_BUS.register(this); + } + + @Override + public void registerItemComparators(ItemComparatorRegistry registry) { + itemComparatorRegistry = registry; + } + + @Override + public void registerScreens(ScreenRegistry registry) { + loaded = true; + registry.registerFocusedStack(new TerminalStorageReiFocusedStackProvider()); + } + + @Override + public void registerTransferHandlers(TransferHandlerRegistry registry) { + registry.register(new TerminalStorageReiTransferHandler()); + } + + @SubscribeEvent + public void onTerminalStorageButtons(TerminalStorageTabClientLoadButtonsEvent event) { + if (this.loaded && !event.getButtons().stream() + .anyMatch((button) -> button instanceof TerminalButtonItemStackCraftingGridSearchSync)) { + event.getButtons().add(new TerminalButtonItemStackCraftingGridSearchSync( + "rei", event.getContainer().getGuiState(), event.getClientTab(), Images.BUTTON_MIDDLE_REI_SYNC)); + } + } + + @SubscribeEvent + public void onTerminalStorageScreenSize(TerminalStorageScreenSizeEvent event) { + if (this.loaded) { + try { + boolean isOpen = REIRuntime.getInstance().getOverlay().isPresent(); + boolean wasJeiVisiblePrevious = wasReiVisible; + if (isOpen) { + wasReiVisible = true; + event.setWidth(event.getWidth() - 170); + } else { + wasReiVisible = false; + } + + // Re-init screen if JEI was just made (in)visible + if (wasJeiVisiblePrevious != wasReiVisible) { + ((ContainerScreenTerminalStorage) Minecraft.getInstance().screen).init(); + } + } catch (NoClassDefFoundError | ClassCastException e) { + // Do nothing when we detect some JEI API issues + } + } + } + + @SubscribeEvent + public void onSearchFieldUpdated(TerminalStorageTabClientSearchFieldUpdateEvent event) { + // Copy the terminal search box contents into the JEI search box. + if (REIRuntime.getInstance().getOverlay().isPresent() && TerminalButtonItemStackCraftingGridSearchSync.isSearchSynced(event.getClientTab())) { + REIRuntime.getInstance().getSearchTextField().setText(event.getSearchString() + ""); + } + } + + @SubscribeEvent + public void onKeyTyped(ScreenEvent.KeyReleased.Post event) { + // Copy the JEI search box contents into the terminal search box. + if (event.getScreen() instanceof ContainerScreenTerminalStorage) { + ContainerScreenTerminalStorage gui = ((ContainerScreenTerminalStorage) event.getScreen()); + if (REIRuntime.getInstance().getOverlay().isPresent() && REIRuntime.getInstance().getSearchTextField().isFocused()) { + gui.getSelectedClientTab().ifPresent(tab -> { + if (TerminalButtonItemStackCraftingGridSearchSync.isSearchSynced(tab)) { + WidgetTextFieldExtended fieldSearch = gui.getFieldSearch(); + fieldSearch.setValue(REIRuntime.getInstance().getSearchTextField().getText()); + tab.setInstanceFilter(gui.getMenu().getSelectedChannel(), fieldSearch.getValue() + ""); + } + }); + } + } + } +} diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/rei/terminalstorage/TerminalStorageReiFocusedStackProvider.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/rei/terminalstorage/TerminalStorageReiFocusedStackProvider.java new file mode 100644 index 0000000..914baff --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/rei/terminalstorage/TerminalStorageReiFocusedStackProvider.java @@ -0,0 +1,45 @@ +package org.cyclops.integratedterminalscompat.modcompat.rei.terminalstorage; + +import dev.architectury.event.CompoundEventResult; +import me.shedaniel.math.Point; +import me.shedaniel.rei.api.client.registry.screen.FocusedStackProvider; +import me.shedaniel.rei.api.common.entry.EntryStack; +import me.shedaniel.rei.api.common.util.EntryStacks; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; +import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage; +import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentClient; + +import java.util.Optional; + +/** + * @author rubensworks + */ +public class TerminalStorageReiFocusedStackProvider implements FocusedStackProvider { + @Override + public CompoundEventResult> provide(Screen screen, Point point) { + if (screen instanceof ContainerScreenTerminalStorage containerScreen) { + return createClickableIngredient(containerScreen, point.x, point.y); + } + return CompoundEventResult.pass(); + } + + private CompoundEventResult> createClickableIngredient( + ContainerScreenTerminalStorage containerScreen, int mouseX, int mouseY) { + int slotIndex = containerScreen.getStorageSlotIndexAtPosition(mouseX, mouseY); + @SuppressWarnings("unchecked") // Cast is safe due to filter + Optional> tabOptional = containerScreen.getSelectedClientTab() + .filter(TerminalStorageTabIngredientComponentClient.class::isInstance) + .map(TerminalStorageTabIngredientComponentClient.class::cast); + if(slotIndex >= 0 && tabOptional.isPresent()) { + TerminalStorageTabIngredientComponentClient tab = tabOptional.get(); + int channel = containerScreen.getMenu().getSelectedChannel(); + Optional instanceOptional = tab.getSlotInstance(channel, slotIndex); + return instanceOptional + .map(instance -> CompoundEventResult.interruptTrue(instance instanceof ItemStack itemStack ? EntryStacks.of(itemStack) : (instance instanceof FluidStack fluidStack ? EntryStacks.of(fluidStack.getFluid(), fluidStack.getAmount()) : EntryStack.empty()))) + .orElse(CompoundEventResult.pass()); + } + return CompoundEventResult.pass(); + } +} diff --git a/src/main/java/org/cyclops/integratedterminalscompat/modcompat/rei/terminalstorage/TerminalStorageReiTransferHandler.java b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/rei/terminalstorage/TerminalStorageReiTransferHandler.java new file mode 100644 index 0000000..40ec6ce --- /dev/null +++ b/src/main/java/org/cyclops/integratedterminalscompat/modcompat/rei/terminalstorage/TerminalStorageReiTransferHandler.java @@ -0,0 +1,119 @@ +package org.cyclops.integratedterminalscompat.modcompat.rei.terminalstorage; + +import com.google.common.collect.Lists; +import me.shedaniel.math.Rectangle; +import me.shedaniel.rei.api.client.gui.widgets.Slot; +import me.shedaniel.rei.api.client.gui.widgets.Widget; +import me.shedaniel.rei.api.client.registry.transfer.TransferHandler; +import me.shedaniel.rei.api.common.entry.EntryIngredient; +import me.shedaniel.rei.plugin.common.BuiltinPlugin; +import me.shedaniel.rei.plugin.common.displays.crafting.DefaultCraftingDisplay; +import net.minecraft.network.chat.Component; +import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabCommon; +import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentClient; +import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentItemStackCrafting; +import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentItemStackCraftingCommon; +import org.cyclops.integratedterminals.inventory.container.ContainerTerminalStorageBase; +import org.cyclops.integratedterminalscompat.modcompat.common.RecipeTransferHelpers; +import org.cyclops.integratedterminalscompat.modcompat.common.RecipeTransferResult; +import org.cyclops.integratedterminalscompat.modcompat.rei.RecipeInputSlotRei; +import org.cyclops.integratedterminalscompat.modcompat.rei.ReiIntegratedTerminalsConfig; + +import java.util.Collection; +import java.util.Objects; + +/** + * @author rubensworks + */ +public class TerminalStorageReiTransferHandler implements TransferHandler { + + private int previousChangeId; + + @Override + public Result handle(Context context) { + if (context.getDisplay().getCategoryIdentifier().equals(BuiltinPlugin.CRAFTING) && + context.getDisplay() instanceof DefaultCraftingDisplay displayCrafting && + context.getMenu() instanceof ContainerTerminalStorageBase container && + Objects.equals(container.getSelectedTab(), TerminalStorageTabIngredientComponentItemStackCrafting.NAME.toString())) { + ITerminalStorageTabCommon tabCommon = container.getTabCommon(container.getSelectedTab()); + TerminalStorageTabIngredientComponentItemStackCraftingCommon tabCommonCrafting = + (TerminalStorageTabIngredientComponentItemStackCraftingCommon) tabCommon; + + if (context.isActuallyCrafting()) { + RecipeTransferHelpers.transferRecipe( + container, + getRecipeInputSlots(displayCrafting), + context.getMinecraft().player, + tabCommonCrafting, + ReiIntegratedTerminalsConfig::getItemStackMatchCondition, + context.isStackedCrafting() + ); + return Result.createSuccessful().blocksFurtherHandling(); + } else { + TerminalStorageTabIngredientComponentClient tabClient = (TerminalStorageTabIngredientComponentClient) + container.getTabClient(container.getSelectedTab()); + return RecipeTransferHelpers.getMissingItems( + displayCrafting.getOptionalRecipe().orElse(null), + container, + getRecipeInputSlots(displayCrafting), + context.getMinecraft().player, + tabCommonCrafting, + tabClient, + ReiIntegratedTerminalsConfig::getItemStackMatchCondition, + () -> previousChangeId, + id -> previousChangeId = id + ).map(transferResult -> Result.createSuccessful() + .color(transferResult.getButtonHighlightColor()) + .tooltip(transferResult.getMessage().stream().reduce(Component.empty(), (c1, c2) -> c1.copy().append("\n").append(c2))) + .renderer((guiGraphics, mouseX, mouseY, v, widgets, rectangle, display) -> { + int index = 0; + for (Widget widget : widgets) { + if (widget instanceof Slot widgetSlot && widgetSlot.getNoticeMark() == Slot.INPUT) { + // Determine if this slot requires any highlighting + int color = -1; + for (RecipeInputSlotRei slot : transferResult.getSlotsMissing()) { + if (slot.getIndex() == index) { + color = RecipeTransferResult.SLOT_COLOR_MISSING; + break; + } + } + for (RecipeInputSlotRei slot : transferResult.getSlotsCraftable()) { + if (slot.getIndex() == index) { + color = RecipeTransferResult.SLOT_COLOR_CRAFTABLE; + break; + } + } + + // Draw the highlight + if (color != -1) { + Rectangle bounds = widgetSlot.getInnerBounds(); + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(0, 0, 20); + guiGraphics.fill(bounds.x, bounds.y, bounds.getMaxX(), bounds.getMaxY(), color); + guiGraphics.pose().popPose(); + } + + index++; + } + } + + }) + .blocksFurtherHandling()) + .orElse(Result.createSuccessful().blocksFurtherHandling()); + } + } + return Result.createNotApplicable(); + } + + private Collection getRecipeInputSlots(DefaultCraftingDisplay context) { + Collection recipeInputSlots = Lists.newArrayList(); + for (EntryIngredient outputEntry : context.getOutputEntries()) { + recipeInputSlots.add(new RecipeInputSlotRei(outputEntry, false, 0)); + } + int i = 0; + for (EntryIngredient outputEntry : context.getOrganisedInputEntries(3, 3)) { + recipeInputSlots.add(new RecipeInputSlotRei(outputEntry, true, i++)); + } + return recipeInputSlots; + } +} diff --git a/src/main/java/org/cyclops/integratedterminalscompat/network/packet/TerminalStorageIngredientItemStackCraftingGridSetRecipe.java b/src/main/java/org/cyclops/integratedterminalscompat/network/packet/TerminalStorageIngredientItemStackCraftingGridSetRecipe.java index bfa395b..53258dd 100644 --- a/src/main/java/org/cyclops/integratedterminalscompat/network/packet/TerminalStorageIngredientItemStackCraftingGridSetRecipe.java +++ b/src/main/java/org/cyclops/integratedterminalscompat/network/packet/TerminalStorageIngredientItemStackCraftingGridSetRecipe.java @@ -2,11 +2,11 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; -import net.minecraft.world.entity.player.Player; +import net.minecraft.network.FriendlyByteBuf; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; -import net.minecraft.network.FriendlyByteBuf; import net.minecraft.world.level.Level; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @@ -18,11 +18,17 @@ import org.cyclops.cyclopscore.network.CodecField; import org.cyclops.cyclopscore.network.PacketCodec; import org.cyclops.integratedterminals.api.terminalstorage.ITerminalStorageTabCommon; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.CraftingJobStartException; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingOption; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalCraftingPlan; +import org.cyclops.integratedterminals.api.terminalstorage.crafting.ITerminalStorageTabIngredientCraftingHandler; import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentItemStackCraftingCommon; import org.cyclops.integratedterminals.core.terminalstorage.TerminalStorageTabIngredientComponentServer; +import org.cyclops.integratedterminals.core.terminalstorage.crafting.TerminalStorageTabIngredientCraftingHandlers; import org.cyclops.integratedterminals.inventory.container.ContainerTerminalStorageBase; import org.cyclops.integratedterminals.network.packet.TerminalStorageIngredientItemStackCraftingGridClear; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -41,6 +47,8 @@ public class TerminalStorageIngredientItemStackCraftingGridSetRecipe extends Pac private boolean maxTransfer; private Map> slottedIngredientsFromPlayer; private Map>> slottedIngredientsFromStorage; + @CodecField + private boolean triggerCraftingJobs; public TerminalStorageIngredientItemStackCraftingGridSetRecipe() { @@ -48,12 +56,14 @@ public TerminalStorageIngredientItemStackCraftingGridSetRecipe() { public TerminalStorageIngredientItemStackCraftingGridSetRecipe(String tabId, int channel, boolean maxTransfer, Map> slottedIngredientsFromPlayer, - Map>> slottedIngredientsFromStorage) { + Map>> slottedIngredientsFromStorage, + boolean triggerCraftingJobs) { this.tabId = tabId; this.channel = channel; this.maxTransfer = maxTransfer; this.slottedIngredientsFromPlayer = slottedIngredientsFromPlayer; this.slottedIngredientsFromStorage = slottedIngredientsFromStorage; + this.triggerCraftingJobs = triggerCraftingJobs; } @Override @@ -160,6 +170,31 @@ public void actionServer(Level world, ServerPlayer player) { } if (!extracted.isEmpty()) { slot.set(extracted); + } else if (this.triggerCraftingJobs) { + // Trigger crafting job if enabled + boolean startedJob = false; + for (ITerminalStorageTabIngredientCraftingHandler handler : TerminalStorageTabIngredientCraftingHandlers.REGISTRY.getHandlers()) { + for (Pair stackEntry : entry.getValue()) { + for (ITerminalCraftingOption craftingOption : (Collection>) handler.getCraftingOptionsWithOutput(tabServerCrafting, channel, stackEntry.getLeft(), stackEntry.getRight())) { + ITerminalCraftingPlan craftingPlan = handler.calculateCraftingPlan(tabServerCrafting.getNetwork(), channel, craftingOption, 1); + if (craftingPlan.getStatus().isValid()) { + try { + handler.startCraftingJob(tabServerCrafting.getNetwork(), channel, craftingPlan, player); + startedJob = true; + break; + } catch (CraftingJobStartException e) { + // Ignore jobs that could not start + } + } + } + if (startedJob) { + break; + } + } + if (startedJob) { + break; + } + } } } } diff --git a/src/main/resources/assets/integratedterminalscompat/lang/en_us.json b/src/main/resources/assets/integratedterminalscompat/lang/en_us.json index 0316784..97eee07 100644 --- a/src/main/resources/assets/integratedterminalscompat/lang/en_us.json +++ b/src/main/resources/assets/integratedterminalscompat/lang/en_us.json @@ -1,5 +1,14 @@ { "_comment": "Gui", "gui.integratedterminalscompat.terminal_storage.craftinggrid.jeisync": "JEI Search Sync", - "gui.integratedterminalscompat.terminal_storage.craftinggrid.jeisync.info": "Synchronize search box with JEI." + "gui.integratedterminalscompat.terminal_storage.craftinggrid.jeisync.info": "Synchronize search box with JEI.", + "gui.integratedterminalscompat.terminal_storage.craftinggrid.emisync": "EMI Search Sync", + "gui.integratedterminalscompat.terminal_storage.craftinggrid.emisync.info": "Synchronize search box with EMI.", + "gui.integratedterminalscompat.terminal_storage.craftinggrid.reisync": "REI Search Sync", + "gui.integratedterminalscompat.terminal_storage.craftinggrid.reisync.info": "Synchronize search box with REI.", + "gui.integratedterminalscompat.terminal_storage.jei.transfer.craftable": "Auto-craftable items", + "gui.integratedterminalscompat.terminal_storage.jei.transfer.craftable_partial": "Partially auto-craftable items", + "gui.integratedterminalscompat.terminal_storage.jei.transfer.missing": "Missing items", + "gui.integratedterminalscompat.terminal_storage.jei.transfer.craft.info": "Ctrl+click to trigger auto-crafting", + "gui.integratedterminalscompat.terminal_storage.jei.transfer.craft.info_cmd": "Cmd+click to trigger auto-crafting" } \ No newline at end of file