Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ public class GeneralConfig extends DummyConfig {
@ConfigurableProperty(category = "general", comment = "If the search box and button states should be synchronized between the item storage and crafting tabs.", isCommandable = true, configLocation = ModConfig.Type.CLIENT)
public static boolean syncItemStorageAndCraftingTabStates = true;

@ConfigurableProperty(category = "general", comment = "If storage terminal interactions should be shown immediately, before the server confirms them.", isCommandable = true, configLocation = ModConfig.Type.CLIENT)
public static boolean guiStoragePredictInteractions = true;

@ConfigurableProperty(category = "general", comment = "If shift-clicking on the crafting terminal's crafting result slot should only produce a single result.", isCommandable = true, configLocation = ModConfig.Type.CLIENT)
public static boolean shiftClickCraftingResultLimit = false;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.cyclops.integratedterminals.api.ingredient;

import com.google.common.collect.Iterables;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.world.entity.player.Player;
Expand All @@ -14,12 +15,16 @@
import org.cyclops.commoncapabilities.api.ingredient.IIngredientMatcher;
import org.cyclops.commoncapabilities.api.ingredient.IngredientComponent;
import org.cyclops.commoncapabilities.api.ingredient.storage.IIngredientComponentStorage;
import org.cyclops.cyclopscore.ingredient.collection.IIngredientCollapsedCollectionMutable;
import org.cyclops.cyclopscore.ingredient.collection.IngredientCollectionHelpers;
import org.cyclops.cyclopscore.ingredient.storage.IngredientComponentStorageCollectionWrapper;
import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage;
import org.cyclops.integratedterminals.core.terminalstorage.query.SearchMode;

import javax.annotation.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Predicate;

/**
Expand Down Expand Up @@ -180,6 +185,93 @@ public default void insertMaxIntoContainer(IIngredientComponentStorage<T, M> sto
*/
public T insertIntoContainer(IIngredientComponentStorage<T, M> storage, AbstractContainerMenu container, int containerSlot, T maxInstance, @Nullable Player player, boolean transferFullSelection);

/**
* Simulate {@link #insertMaxIntoContainer(IIngredientComponentStorage, AbstractContainerMenu, int, int, Object)}
* against the client-side container, so that its effect can be shown before the server confirms it.
*
* The container is modified, the storage is not, as the client has no storage to modify.
*
* This is part of this capability, and not a helper next to its only caller,
* so that a handler whose movements can not run client-side can opt out of being predicted
* by returning an empty instance here.
*
* @param container The client-side container to insert to.
* @param containerSlotStart The container slot to start from.
* @param containerSlotEnd The container slot to end at (exclusive).
* @param instance The instance to move.
* @param availableQuantity The quantity that is expected to be available in the storage.
* @return The instance quantity that would be moved.
*/
public default T predictInsertMaxIntoContainer(AbstractContainerMenu container, int containerSlotStart,
int containerSlotEnd, T instance, long availableQuantity) {
return predictMovement(availableQuantity, instance, (storage, movedInstance) ->
insertMaxIntoContainer(storage, container, containerSlotStart, containerSlotEnd, movedInstance));
}

/**
* Simulate {@link #insertIntoContainer(IIngredientComponentStorage, AbstractContainerMenu, int, Object, Player, boolean)}
* against the client-side container, so that its effect can be shown before the server confirms it.
*
* The container is modified, the storage is not, as the client has no storage to modify.
* No player is passed, so that the container slot contents are never picked up by the prediction.
* The server may still do so, in which case the prediction simply moves nothing.
*
* @param container The client-side container to insert to.
* @param containerSlot The container slot to insert to.
* @param maxInstance The instance to move.
* @param transferFullSelection If the selected stack should be moved fully.
* @param availableQuantity The quantity that is expected to be available in the storage.
* @return The instance quantity that would be moved.
*/
public default T predictInsertIntoContainer(AbstractContainerMenu container, int containerSlot, T maxInstance,
boolean transferFullSelection, long availableQuantity) {
return predictMovement(availableQuantity, maxInstance, (storage, movedInstance) ->
insertIntoContainer(storage, container, containerSlot, movedInstance, null, transferFullSelection));
}

/**
* Simulate {@link #extractMaxFromContainerSlot(IIngredientComponentStorage, AbstractContainerMenu, int, Inventory, int)}
* against the client-side container, so that its effect can be shown before the server confirms it.
*
* The container is modified, the storage is not, as the client has no storage to modify.
*
* @param container The client-side container to extract from.
* @param containerSlot The container slot to extract from.
* @param playerInventory The active player inventory.
* @param limit The max limit. -1 is no limit.
* @return The instance quantity that would be moved.
*/
public default T predictExtractMaxFromContainerSlot(AbstractContainerMenu container, int containerSlot,
Inventory playerInventory, int limit) {
IIngredientCollapsedCollectionMutable<T, M> collection = IngredientCollectionHelpers
.createCollapsedCollection(getComponent());
extractMaxFromContainerSlot(new IngredientComponentStorageCollectionWrapper<>(collection),
container, containerSlot, playerInventory, limit);
return Iterables.getFirst(collection, getComponent().getMatcher().getEmptyInstance());
}

/**
* Run the given movement against a storage that holds the given available quantity of the given instance,
* and determine how much was taken out of it.
*
* The storage that is simulated has no rate limit, while the network may have one,
* in which case the prediction moves more than the server will.
* The client can not know that limit, so such a prediction is left to expire.
*/
private T predictMovement(long availableQuantity, T instance,
BiConsumer<IIngredientComponentStorage<T, M>, T> movement) {
IIngredientMatcher<T, M> matcher = getComponent().getMatcher();
if (availableQuantity <= 0) {
return matcher.getEmptyInstance();
}
IIngredientCollapsedCollectionMutable<T, M> collection = IngredientCollectionHelpers
.createCollapsedCollection(getComponent());
collection.add(matcher.withQuantity(instance, availableQuantity));
// The movement may modify the instance it is given, so it never gets the caller's instance
movement.accept(new IngredientComponentStorageCollectionWrapper<>(collection), matcher.copy(instance));
return matcher.withQuantity(instance, availableQuantity - collection.getQuantity(instance));
}

/**
* Move the ingredient in the active player stack to the storage.
* @param storage The storage to insert to.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,22 @@ public boolean handleClick(AbstractContainerMenu container, int channel, int hov
boolean hasClickedOutside, boolean hasClickedInStorage, int hoveredContainerSlot,
boolean isQuickMove);

/**
* If {@link #handleClick} should already be called when the mouse button goes down,
* instead of when it is released.
*
* Clicks are handled on release by default, as the press may also be the start of a drag.
* Tabs can return true for the clicks that can never start a drag, so that they are applied instantly.
* This is only called when the player's cursor is empty.
*
* @param channel The active channel.
* @param hoveringStorageSlot The storage slot id that is being hovered. -1 if none.
* @return If the click should be handled on press.
*/
public default boolean isClickHandledOnPress(int channel, int hoveringStorageSlot) {
return false;
}

/**
* Called when a mouse scroll happens in a gui.
* @param container The active container.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.cyclops.integratedterminals.capability.ingredient.sorter.FluidStackQuantitySorter;
import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage;
import org.cyclops.integratedterminals.client.gui.tooltip.TooltipRenderHelpers;
import org.cyclops.integratedterminals.core.terminalstorage.query.IngredientQueryMatchers;
import org.cyclops.integratedterminals.core.terminalstorage.query.SearchMode;

import javax.annotation.Nullable;
Expand Down Expand Up @@ -233,14 +234,15 @@ public void drainActivePlayerStackQuantity(Inventory playerInventory, AbstractCo
@Override
@OnlyIn(Dist.CLIENT)
public Predicate<FluidStack> getInstanceFilterPredicate(SearchMode searchMode, String query) {
Predicate<String> matcher = IngredientQueryMatchers.containsQuery(query);
return switch (searchMode) {
case MOD -> i -> BuiltInRegistries.FLUID.getKey(i.getFluid()).getNamespace()
.toLowerCase(Locale.ENGLISH).matches(".*" + query + ".*");
case MOD -> i -> matcher.test(BuiltInRegistries.FLUID.getKey(i.getFluid()).getNamespace()
.toLowerCase(Locale.ENGLISH));
case TOOLTIP -> i -> false; // Fluids have no tooltip
case TAG -> i -> i.getFluid().builtInRegistryHolder().tags()
.filter(tag -> tag.location().toString().toLowerCase(Locale.ENGLISH).matches(".*" + query + ".*"))
.filter(tag -> matcher.test(tag.location().toString().toLowerCase(Locale.ENGLISH)))
.anyMatch(tag -> !BuiltInRegistries.FLUID.getTag(tag).isEmpty());
case DEFAULT -> i -> i != null && i.getHoverName().getString().toLowerCase(Locale.ENGLISH).matches(".*" + query + ".*");
case DEFAULT -> i -> i != null && matcher.test(i.getHoverName().getString().toLowerCase(Locale.ENGLISH));
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.cyclops.integratedterminals.capability.ingredient.sorter.ItemStackQuantitySorter;
import org.cyclops.integratedterminals.client.gui.container.ContainerScreenTerminalStorage;
import org.cyclops.integratedterminals.client.gui.tooltip.TooltipRenderHelpers;
import org.cyclops.integratedterminals.core.terminalstorage.query.IngredientQueryMatchers;
import org.cyclops.integratedterminals.core.terminalstorage.query.SearchMode;
import org.lwjgl.opengl.GL11;

Expand Down Expand Up @@ -243,16 +244,16 @@ public void drainActivePlayerStackQuantity(Inventory playerInventory, AbstractCo
@Override
@OnlyIn(Dist.CLIENT)
public Predicate<ItemStack> getInstanceFilterPredicate(SearchMode searchMode, String query) {
Predicate<String> matcher = IngredientQueryMatchers.containsQuery(query);
return switch (searchMode) {
case MOD -> i -> Optional.ofNullable(i.getItem().getCreatorModId(i))
.orElse("minecraft").toLowerCase(Locale.ENGLISH)
.matches(".*" + query + ".*");
case MOD -> i -> matcher.test(Optional.ofNullable(i.getItem().getCreatorModId(i))
.orElse("minecraft").toLowerCase(Locale.ENGLISH));
case TOOLTIP -> i -> i.getTooltipLines(Item.TooltipContext.of(Minecraft.getInstance().player.registryAccess()), Minecraft.getInstance().player, TooltipFlag.Default.NORMAL).stream()
.anyMatch(s -> s.getString().toLowerCase(Locale.ENGLISH).matches(".*" + query + ".*"));
.anyMatch(s -> matcher.test(s.getString().toLowerCase(Locale.ENGLISH)));
case TAG -> i -> i.getItem().builtInRegistryHolder().tags()
.filter(tag -> tag.location().toString().toLowerCase(Locale.ENGLISH).matches(".*" + query + ".*"))
.filter(tag -> matcher.test(tag.location().toString().toLowerCase(Locale.ENGLISH)))
.anyMatch(tag -> !BuiltInRegistries.ITEM.getTag(tag).isEmpty());
case DEFAULT -> i -> i.getHoverName().getString().toLowerCase(Locale.ENGLISH).matches(".*" + query + ".*");
case DEFAULT -> i -> matcher.test(i.getHoverName().getString().toLowerCase(Locale.ENGLISH));
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,22 @@ && mouseX > getGuiLeft() + TAB_OFFSET_X
}
});

// Handle clicks that don't have to wait for the mouse button to be released
if (this.clicked && tabOptional.isPresent() && getMenu().getCarried().isEmpty()) {
ITerminalStorageTabClient<?> tab = tabOptional.get();
int slot = getStorageSlotIndexAtPosition(mouseX, mouseY);
if (tab.isClickHandledOnPress(getMenu().getSelectedChannel(), slot)) {
this.clicked = false; // To avoid handling this click again on mouse release
Slot playerSlot = getSlotUnderMouse();
if (tab.handleClick(getMenu(), getMenu().getSelectedChannel(), slot, mouseButton,
this.hasClickedOutside(mouseX, mouseY, this.leftPos, this.topPos, mouseButton),
this.hasClickedInStorage(mouseX, mouseY),
playerSlot != null ? playerSlot.index : -1, false)) {
return true;
}
}
}

return super.mouseClicked(mouseX, mouseY, mouseButton);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.cyclops.integratedterminals.core.terminalstorage;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;

import java.util.List;
import java.util.Map;

/**
* Helpers for telling the server which container slots a client-side prediction changed.
*
* This is the same reconciliation that vanilla container clicks use:
* the client sends the slots it changed, and the server only sends back the ones it disagrees with.
*
* @author rubensworks
*/
public final class ContainerHelpers {

private ContainerHelpers() {
}

/**
* @param container A container.
* @return A copy of the contents of all slots in the given container.
*/
public static List<ItemStack> copyContents(AbstractContainerMenu container) {
List<ItemStack> contents = Lists.newArrayListWithExpectedSize(container.slots.size());
for (Slot slot : container.slots) {
contents.add(slot.getItem().copy());
}
return contents;
}

/**
* Determine which slots of the given container changed since the given contents were copied.
* @param contentsBefore The contents from {@link #copyContents(AbstractContainerMenu)}.
* @param container The container, after it was changed.
* @return The new contents of the changed slots, by slot id.
*/
public static Map<Integer, ItemStack> getChangedContents(List<ItemStack> contentsBefore,
AbstractContainerMenu container) {
Map<Integer, ItemStack> changed = Maps.newHashMap();
for (int i = 0; i < contentsBefore.size(); i++) {
ItemStack after = container.getSlot(i).getItem();
if (!ItemStack.matches(contentsBefore.get(i), after)) {
changed.put(i, after.copy());
}
}
return changed;
}

}
Loading
Loading