From 9a2741128a78bb52eba50a631126e090a5a2abd8 Mon Sep 17 00:00:00 2001 From: miozune Date: Sat, 26 Nov 2022 01:45:28 +0900 Subject: Rewrite GUIs with ModularUI (#1381) * Base work for ModularUI compat * Remove useless interface * Add almost all the widgets * Invert method * Refactor NEI stack placement positions * NEI handlers on ModularUI * Add some more docs * AdvDebugStructureWriter * Fix NEI progressbar not working * PrimitiveBlastFurnace * clean * derp * clean * spotlessApply * Boilers * Buffers * clean * N by N slots containers * Fix boilers not having bucket interaction Put opening UI to individual MetaTEs * Maintenance Hatch * clean * spotlessApply * Add dependency * IndustrialApiary * Adapt to ModularUI change * Base work for covers & fix crash with MP * Fix crash with server * Rewrite base work for covers * Send initial cover data on cover GUI open so that the time of showing incorrect data will be eliminated * Covers part 1 * Rename package: ModularUI -> modularui * Rename class: GT_UIInfo -> GT_UIInfos * Fix build * Covers part2 * Fix missing client check with tile UI & fix title overlap * CoverTabLine * Move cover window creators to inner class * Fix crash with null base TE * Close GUI when tile is broken * Color cover window with tile colorization * Change signature of addUIWidgets * FluidFilter cover, FluidDisplaySlotWidget, BasicTank, BasicGenerator, Output Hatch, MicrowaveEnergyTransmitter, Teleporter, DigitalChest, DigitalTank * Add title tab * Move package: modularui -> modularui/widget * Programmed circuit + IConfigurationCircuitSupport * clean * VolumetricFlask * Remove integrated circuit overlay from recipe input slots * Input Hatch & Quadruple Input Hatch * Multiblock * Deprecate old cover GUI * BasicMachines * Finish BasicMachine & NEI * Expand DTPF NEI to 9 slots * Fix ME input bus on MP * Move AESlotWidget to public class * Move GT_Recipe_Map constructors with mNEIUnificateOutput to setter method * Move SteamTexture.Variant to outer enum * Switch to remote repository * oops * Update MUI * Update MUI * Minor refactor for change amount buttons * Display items and fluids that exceed usual count * blah * use +=, why didn't I do this * Update MUI * Move ModularUI to Base (#1510) * Move ModularUI to Base * Move most of the ModularUI functionality to `BaseTileEntity` (and `CoverableTileEntity`) * `CommonMetaTileEntity` delegates ato the MetaTileEntity * Added several interfaces (with defaults) to indicate if a tile/metatile override/implement certain behaviors. * Moved `IConfigurationCircuitSupport` interface such that it will work with BaseTileEntity or a MetaTileEntity * Address reviews Co-authored-by: miozune * Update MUI * Minor changes to NEI * Return :facepalm: * IGetTabIconSet override * Some more changes to NEI * Merge texture getter interfaces to new class GUITextureSet * Remove BBF structure picture as it's auto-buildable now * Make unified title tab style of texture angular * Expose some boiler texture getters for addon * Fix crash with cover GUI on pipe * Lower the number of recipe per page for DTPF & update MUI * Update MUI * Fix crash with middle-clicking slot on circuit selection GUI * Fix circuit selection window not syncing item from base machine * Merge GT_NEI_AssLineHandler into GT_NEI_DefaultHandler * Update MUI * Add in TecTech multi message * Allow changing the way of binding player inventory * Update MUI * Update MUI * Update MUI * Update MUI * Update MUI * Make MUI non-transitive to allow addons to use their own version * Force enable mixin * Format fluid amount tooltip * Add GUITextureSet.STEAM * Add guard against null ModularWindow creation * Add constructors for Muffler Hatch with inventory * Fix output slot on digital chest and tank allowing insertion * Don't log null ModularWindow * Add default implementation for IHasWorldObjectAndCoords#openGUI * Make openGTTileEntityUI accept MultiTE & cleanup Co-authored-by: Jason Mitchell --- .../common/gui/modularui/widget/AESlotWidget.java | 40 ++ .../modularui/widget/CoverCycleButtonWidget.java | 89 ++++ .../widget/CoverDataControllerWidget.java | 138 ++++++ .../CoverDataFollower_CycleButtonWidget.java | 38 ++ .../widget/CoverDataFollower_SlotWidget.java | 102 +++++ .../widget/CoverDataFollower_TextFieldWidget.java | 110 +++++ .../CoverDataFollower_ToggleButtonWidget.java | 84 ++++ .../gui/modularui/widget/DataControllerWidget.java | 162 +++++++ .../modularui/widget/FluidDisplaySlotWidget.java | 474 +++++++++++++++++++++ .../modularui/widget/ItemWatcherSlotWidget.java | 47 ++ 10 files changed, 1284 insertions(+) create mode 100644 src/main/java/gregtech/common/gui/modularui/widget/AESlotWidget.java create mode 100644 src/main/java/gregtech/common/gui/modularui/widget/CoverCycleButtonWidget.java create mode 100644 src/main/java/gregtech/common/gui/modularui/widget/CoverDataControllerWidget.java create mode 100644 src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_CycleButtonWidget.java create mode 100644 src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_SlotWidget.java create mode 100644 src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_TextFieldWidget.java create mode 100644 src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_ToggleButtonWidget.java create mode 100644 src/main/java/gregtech/common/gui/modularui/widget/DataControllerWidget.java create mode 100644 src/main/java/gregtech/common/gui/modularui/widget/FluidDisplaySlotWidget.java create mode 100644 src/main/java/gregtech/common/gui/modularui/widget/ItemWatcherSlotWidget.java (limited to 'src/main/java/gregtech/common/gui/modularui/widget') diff --git a/src/main/java/gregtech/common/gui/modularui/widget/AESlotWidget.java b/src/main/java/gregtech/common/gui/modularui/widget/AESlotWidget.java new file mode 100644 index 0000000000..f3620d3234 --- /dev/null +++ b/src/main/java/gregtech/common/gui/modularui/widget/AESlotWidget.java @@ -0,0 +1,40 @@ +package gregtech.common.gui.modularui.widget; + +import appeng.client.render.AppEngRenderItem; +import appeng.core.AELog; +import appeng.util.Platform; +import com.gtnewhorizons.modularui.common.internal.wrapper.BaseSlot; +import com.gtnewhorizons.modularui.common.internal.wrapper.ModularGui; +import com.gtnewhorizons.modularui.common.widget.SlotWidget; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.renderer.entity.RenderItem; +import net.minecraft.inventory.Slot; + +public class AESlotWidget extends SlotWidget { + + public AESlotWidget(BaseSlot slot) { + super(slot); + } + + @Override + @SideOnly(Side.CLIENT) + protected void drawSlot(Slot slotIn) { + final AppEngRenderItem aeRenderItem = new AppEngRenderItem(); + final RenderItem pIR = this.setItemRender(aeRenderItem); + try { + aeRenderItem.setAeStack(Platform.getAEStackInSlot(slotIn)); + super.drawSlot(slotIn, false); + } catch (final Exception err) { + AELog.warn("[AppEng] AE prevented crash while drawing slot: " + err); + } + this.setItemRender(pIR); + } + + @SideOnly(Side.CLIENT) + private RenderItem setItemRender(final RenderItem item) { + final RenderItem ri = ModularGui.getItemRenderer(); + ModularGui.setItemRenderer(item); + return ri; + } +} diff --git a/src/main/java/gregtech/common/gui/modularui/widget/CoverCycleButtonWidget.java b/src/main/java/gregtech/common/gui/modularui/widget/CoverCycleButtonWidget.java new file mode 100644 index 0000000000..599ed28a5f --- /dev/null +++ b/src/main/java/gregtech/common/gui/modularui/widget/CoverCycleButtonWidget.java @@ -0,0 +1,89 @@ +package gregtech.common.gui.modularui.widget; + +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.drawable.UITexture; +import com.gtnewhorizons.modularui.common.widget.CycleButtonWidget; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import gregtech.api.gui.modularui.GT_UITextures; +import org.lwjgl.opengl.GL11; + +/** + * Fires click action on mouse release, not on press. + * Draws different backgrounds depending on whether the mouse is being pressed or the widget is hovered. + */ +public class CoverCycleButtonWidget extends CycleButtonWidget { + + private static final UITexture BUTTON_NORMAL_NOT_PRESSED = + GT_UITextures.BUTTON_COVER_NORMAL.getSubArea(0, 0, 1, 0.5f); + private static final UITexture BUTTON_NORMAL_PRESSED = GT_UITextures.BUTTON_COVER_NORMAL.getSubArea(0, 0.5f, 1, 1); + private static final UITexture BUTTON_HOVERED_NOT_PRESSED = + GT_UITextures.BUTTON_COVER_NORMAL_HOVERED.getSubArea(0, 0, 1, 0.5f); + private static final UITexture BUTTON_HOVERED_PRESSED = + GT_UITextures.BUTTON_COVER_NORMAL_HOVERED.getSubArea(0, 0.5f, 1, 1); + + private boolean clickPressed; + + private static final int TOOLTIP_DELAY = 5; + + public CoverCycleButtonWidget() { + setSize(16, 16); + setTooltipShowUpDelay(TOOLTIP_DELAY); + } + + @Override + public ClickResult onClick(int buttonId, boolean doubleClick) { + updateState(); + if (!canClick()) return ClickResult.REJECT; + clickPressed = true; + return ClickResult.SUCCESS; + } + + @Override + public boolean onClickReleased(int buttonId) { + clickPressed = false; + updateState(); + if (!isHovering() || !canClick()) return false; + return onClickImpl(buttonId); + } + + protected boolean onClickImpl(int buttonId) { + super.onClick(buttonId, false); + return true; + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + protected boolean canClick() { + return true; + } + + @SideOnly(Side.CLIENT) + protected void updateState() {} + + public boolean isClickPressed() { + return clickPressed; + } + + @Override + public void drawBackground(float partialTicks) { + GL11.glColor4f(1, 1, 1, 1); + super.drawBackground(partialTicks); + } + + @Override + public IDrawable[] getBackground() { + if (isHovering()) { + if (clickPressed) { + return new IDrawable[] {BUTTON_HOVERED_PRESSED}; + } else { + return new IDrawable[] {BUTTON_HOVERED_NOT_PRESSED}; + } + } else { + if (clickPressed) { + return new IDrawable[] {BUTTON_NORMAL_PRESSED}; + } else { + return new IDrawable[] {BUTTON_NORMAL_NOT_PRESSED}; + } + } + } +} diff --git a/src/main/java/gregtech/common/gui/modularui/widget/CoverDataControllerWidget.java b/src/main/java/gregtech/common/gui/modularui/widget/CoverDataControllerWidget.java new file mode 100644 index 0000000000..d28117054a --- /dev/null +++ b/src/main/java/gregtech/common/gui/modularui/widget/CoverDataControllerWidget.java @@ -0,0 +1,138 @@ +package gregtech.common.gui.modularui.widget; + +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.common.internal.network.NetworkUtils; +import gregtech.api.gui.modularui.IDataFollowerWidget; +import gregtech.api.util.GT_CoverBehaviorBase; +import gregtech.api.util.ISerializableObject; +import java.io.IOException; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import net.minecraft.network.PacketBuffer; + +public class CoverDataControllerWidget extends DataControllerWidget { + + protected final GT_CoverBehaviorBase coverBehavior; + + /** + * @param dataGetter () -> cover data this widget handles + * @param dataSetter data to set -> if setting cover data is successful + * @param coverBehavior cover this widget handles data update + */ + public CoverDataControllerWidget( + Supplier dataGetter, Function dataSetter, GT_CoverBehaviorBase coverBehavior) { + super(dataGetter, dataSetter); + this.coverBehavior = coverBehavior; + } + + @Override + public > CoverDataControllerWidget addFollower( + W widget, Function dataToStateGetter, BiFunction dataUpdater, Consumer applyForWidget) { + super.addFollower(widget, dataToStateGetter, dataUpdater, applyForWidget); + return this; + } + + @Override + protected void writeToPacket(PacketBuffer buffer, T data) { + try { + NetworkUtils.writeNBTBase(buffer, data.saveDataToNBT()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + protected T readFromPacket(PacketBuffer buffer) throws IOException { + return coverBehavior.createDataObject(NetworkUtils.readNBTBase(buffer)); + } + + /** + * Uses int index to determine toggle button behaviors. + */ + public static class CoverDataIndexedControllerWidget_ToggleButtons + extends CoverDataControllerWidget { + + private final BiFunction dataToStateGetter; + private final BiFunction dataUpdater; + + /** + * @param coverDataGetter () -> cover data this widget handles + * @param coverDataSetter data to set -> if setting cover data is successful + * @param coverBehavior cover this widget handles data update + * @param dataToStateGetter (index of button, given cover data) -> button state + * @param dataUpdater (index of button, current cover data) -> new cover data + */ + public CoverDataIndexedControllerWidget_ToggleButtons( + Supplier coverDataGetter, + Function coverDataSetter, + GT_CoverBehaviorBase coverBehavior, + BiFunction dataToStateGetter, + BiFunction dataUpdater) { + super(coverDataGetter, coverDataSetter, coverBehavior); + this.dataToStateGetter = dataToStateGetter; + this.dataUpdater = dataUpdater; + } + + /** + * @param index index of widget to add + * @param widget widget to add + * @param applyForWidget methods to call for widget to add + */ + public > + CoverDataIndexedControllerWidget_ToggleButtons addToggleButton( + int index, W widget, Consumer> applyForWidget) { + addFollower( + widget, + data -> dataToStateGetter.apply(index, data), + (data, state) -> dataUpdater.apply(index, data), + applyForWidget); + return this; + } + } + + /** + * Uses int index to determine cycle button behaviors. + */ + public static class CoverDataIndexedControllerWidget_CycleButtons + extends CoverDataControllerWidget { + + private final BiFunction dataToStateGetter; + private final BiFunction dataUpdater; + + /** + * @param coverDataGetter () -> cover data this widget handles + * @param coverDataSetter data to set -> if setting cover data is successful + * @param coverBehavior cover this widget handles data update + * @param dataToStateGetter (index of button, given cover data) -> button state + * @param dataUpdater (index of button, current cover data) -> new cover data + */ + public CoverDataIndexedControllerWidget_CycleButtons( + Supplier coverDataGetter, + Function coverDataSetter, + GT_CoverBehaviorBase coverBehavior, + BiFunction dataToStateGetter, + BiFunction dataUpdater) { + super(coverDataGetter, coverDataSetter, coverBehavior); + this.dataToStateGetter = dataToStateGetter; + this.dataUpdater = dataUpdater; + } + + /** + * @param index index of widget to add + * @param widget widget to add + * @param applyForWidget methods to call for the widget to add + */ + public > + CoverDataIndexedControllerWidget_CycleButtons addCycleButton( + int index, W widget, Consumer> applyForWidget) { + addFollower( + widget, + data -> dataToStateGetter.apply(index, data), + (data, state) -> dataUpdater.apply(index, data), + applyForWidget); + return this; + } + } +} diff --git a/src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_CycleButtonWidget.java b/src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_CycleButtonWidget.java new file mode 100644 index 0000000000..d07165cc6e --- /dev/null +++ b/src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_CycleButtonWidget.java @@ -0,0 +1,38 @@ +package gregtech.common.gui.modularui.widget; + +import gregtech.api.gui.modularui.IDataFollowerWidget; +import gregtech.api.util.ISerializableObject; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Determines button state with cover data. + */ +public class CoverDataFollower_CycleButtonWidget extends CoverCycleButtonWidget + implements IDataFollowerWidget { + + private Function dataToStateGetter; + + public CoverDataFollower_CycleButtonWidget() { + super(); + setGetter(() -> 0); // fake getter; used only for init + setSynced(false, false); + } + + @Override + public CoverDataFollower_CycleButtonWidget setDataToStateGetter(Function dataToStateGetter) { + this.dataToStateGetter = dataToStateGetter; + return this; + } + + @Override + public CoverDataFollower_CycleButtonWidget setStateSetter(Consumer setter) { + super.setSetter(setter); + return this; + } + + @Override + public void updateState(T data) { + setState(dataToStateGetter.apply(data), false, false); + } +} diff --git a/src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_SlotWidget.java b/src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_SlotWidget.java new file mode 100644 index 0000000000..c09c5b5279 --- /dev/null +++ b/src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_SlotWidget.java @@ -0,0 +1,102 @@ +package gregtech.common.gui.modularui.widget; + +import com.gtnewhorizons.modularui.api.forge.IItemHandlerModifiable; +import com.gtnewhorizons.modularui.api.widget.Interactable; +import com.gtnewhorizons.modularui.common.internal.wrapper.BaseSlot; +import com.gtnewhorizons.modularui.common.widget.SlotWidget; +import gregtech.api.gui.modularui.IDataFollowerWidget; +import gregtech.api.util.ISerializableObject; +import java.io.IOException; +import java.util.function.Consumer; +import java.util.function.Function; +import net.minecraft.item.ItemStack; + +public class CoverDataFollower_SlotWidget extends SlotWidget + implements IDataFollowerWidget { + + private Function dataToStateGetter; + private Consumer dataSetter; + + public CoverDataFollower_SlotWidget(BaseSlot slot) { + super(slot); + } + + public CoverDataFollower_SlotWidget(IItemHandlerModifiable handler, int index, boolean phantom) { + this(new BaseSlot(handler, index, phantom)); + } + + public CoverDataFollower_SlotWidget(IItemHandlerModifiable handler, int index) { + this(handler, index, false); + } + + @Override + public CoverDataFollower_SlotWidget setDataToStateGetter(Function dataToStateGetter) { + this.dataToStateGetter = dataToStateGetter; + return this; + } + + @Override + public CoverDataFollower_SlotWidget setStateSetter(Consumer setter) { + this.dataSetter = setter; + return this; + } + + @Override + public void updateState(T data) { + getMcSlot().putStack(dataToStateGetter.apply(data)); + } + + @Override + public void detectAndSendChanges(boolean init) {} + + // Slot sync is handled differently from other DataFollowers, + // so we need to also sync slot content directly to server. + + @Override + public ClickResult onClick(int buttonId, boolean doubleClick) { + if (interactionDisabled) return ClickResult.REJECT; + if (isPhantom()) { + ClickData clickData = ClickData.create(buttonId, doubleClick); + syncToServer(2, clickData::writeToPacket); + phantomClick(clickData); + dataSetter.accept(getMcSlot().getStack()); + return ClickResult.ACCEPT; + } + return ClickResult.REJECT; + } + + @Override + public boolean onMouseScroll(int direction) { + if (interactionDisabled) return false; + if (isPhantom()) { + if (Interactable.hasShiftDown()) { + direction *= 8; + } + final int finalDirection = direction; + syncToServer(3, buffer -> buffer.writeVarIntToBuffer(finalDirection)); + phantomScroll(finalDirection); + dataSetter.accept(getMcSlot().getStack()); + return true; + } + return false; + } + + @Override + public boolean handleDragAndDrop(ItemStack draggedStack, int button) { + if (interactionDisabled) return false; + if (!isPhantom()) return false; + ClickData clickData = ClickData.create(button, false); + syncToServer(5, buffer -> { + try { + clickData.writeToPacket(buffer); + buffer.writeItemStackToBuffer(draggedStack); + } catch (IOException e) { + e.printStackTrace(); + } + }); + phantomClick(clickData, draggedStack); + dataSetter.accept(getMcSlot().getStack()); + draggedStack.stackSize = 0; + return true; + } +} diff --git a/src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_TextFieldWidget.java b/src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_TextFieldWidget.java new file mode 100644 index 0000000000..9130f8e3d0 --- /dev/null +++ b/src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_TextFieldWidget.java @@ -0,0 +1,110 @@ +package gregtech.common.gui.modularui.widget; + +import com.gtnewhorizons.modularui.api.math.Alignment; +import com.gtnewhorizons.modularui.api.math.Color; +import com.gtnewhorizons.modularui.api.math.MathExpression; +import com.gtnewhorizons.modularui.common.widget.textfield.TextFieldWidget; +import gregtech.api.gui.modularui.GT_UITextures; +import gregtech.api.gui.modularui.IDataFollowerWidget; +import gregtech.api.util.ISerializableObject; +import java.util.function.Consumer; +import java.util.function.Function; +import net.minecraft.client.gui.GuiScreen; + +public class CoverDataFollower_TextFieldWidget extends TextFieldWidget + implements IDataFollowerWidget { + + private Function dataToStateGetter; + + public CoverDataFollower_TextFieldWidget() { + super(); + setGetter(() -> ""); // fake getter; used only for init + setSynced(false, false); + setTextColor(Color.WHITE.dark(1)); + setTextAlignment(Alignment.CenterLeft); + setBackground(GT_UITextures.BACKGROUND_TEXT_FIELD.withOffset(-1, -1, 2, 2)); + } + + @Override + public void onPostInit() { + // Widget#onPostInit is called earlier than IDataFollowerWidget#onPostInit, + // so we make sure cursor is set after text is set + super.onPostInit(); + + // On first call #handler does not contain text. + // On second call, it contains correct text to update #lastText, + // but #shouldGetFocus call is skipped by Cursor#updateFocused, + // so we need to manually call this. + if (focusOnGuiOpen) { + shouldGetFocus(); + } + } + + @Override + public CoverDataFollower_TextFieldWidget setDataToStateGetter(Function dataToStateGetter) { + this.dataToStateGetter = dataToStateGetter; + return this; + } + + @Override + public CoverDataFollower_TextFieldWidget setStateSetter(Consumer setter) { + super.setSetter(setter); + return this; + } + + @Override + public void updateState(T data) { + setText(dataToStateGetter.apply(data)); + } + + public CoverDataFollower_TextFieldWidget setOnScrollNumbers(int baseStep, int ctrlStep, int shiftStep) { + setOnScrollNumbers((val, direction) -> { + int step = (GuiScreen.isShiftKeyDown() ? shiftStep : GuiScreen.isCtrlKeyDown() ? ctrlStep : baseStep) + * direction; + try { + val = Math.addExact(val, step); + } catch (ArithmeticException ignored) { + val = Integer.MAX_VALUE; + } + return val; + }); + return this; + } + + public CoverDataFollower_TextFieldWidget setOnScrollNumbers() { + return setOnScrollNumbers(1, 50, 1000); + } + + public CoverDataFollower_TextFieldWidget setOnScrollText(int baseStep, int ctrlStep, int shiftStep) { + setOnScroll((text, direction) -> { + int val = (int) MathExpression.parseMathExpression(text, -1); + int step = (GuiScreen.isShiftKeyDown() ? shiftStep : GuiScreen.isCtrlKeyDown() ? ctrlStep : baseStep) + * direction; + try { + val = Math.addExact(val, step); + } catch (ArithmeticException ignored) { + val = Integer.MAX_VALUE; + } + return TextFieldWidget.format.format(val); + }); + return this; + } + + public CoverDataFollower_TextFieldWidget setOnScrollText() { + return setOnScrollText(1, 5, 50); + } + + public CoverDataFollower_TextFieldWidget setOnScrollNumbersLong(long baseStep, long ctrlStep, long shiftStep) { + setOnScrollNumbersLong((val, direction) -> { + long step = (GuiScreen.isShiftKeyDown() ? shiftStep : GuiScreen.isCtrlKeyDown() ? ctrlStep : baseStep) + * direction; + try { + val = Math.addExact(val, step); + } catch (ArithmeticException ignored) { + val = Long.MAX_VALUE; + } + return val; + }); + return this; + } +} diff --git a/src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_ToggleButtonWidget.java b/src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_ToggleButtonWidget.java new file mode 100644 index 0000000000..8e091d7bc6 --- /dev/null +++ b/src/main/java/gregtech/common/gui/modularui/widget/CoverDataFollower_ToggleButtonWidget.java @@ -0,0 +1,84 @@ +package gregtech.common.gui.modularui.widget; + +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import gregtech.api.gui.modularui.GT_UITextures; +import gregtech.api.gui.modularui.IDataFollowerWidget; +import gregtech.api.util.ISerializableObject; +import java.util.function.Consumer; +import java.util.function.Function; + +public class CoverDataFollower_ToggleButtonWidget extends CoverCycleButtonWidget + implements IDataFollowerWidget { + + private Function dataToStateGetter; + + public CoverDataFollower_ToggleButtonWidget() { + super(); + setGetter(() -> 0); // fake getter; used only for init + setSynced(false, false); + setLength(2); + } + + @Override + public CoverDataFollower_ToggleButtonWidget setDataToStateGetter(Function dataToStateGetter) { + this.dataToStateGetter = dataToStateGetter; + return this; + } + + @Override + public CoverDataFollower_ToggleButtonWidget setStateSetter(Consumer setter) { + super.setSetter(val -> setter.accept(val == 1)); + return this; + } + + @Override + public void updateState(T data) { + setState(dataToStateGetter.apply(data) ? 1 : 0, false, false); + } + + public CoverDataFollower_ToggleButtonWidget setToggleTexture(IDrawable active, IDrawable inactive) { + setTextureGetter(state -> state == 1 ? active : inactive); + return this; + } + + public static CoverDataFollower_ToggleButtonWidget ofCheckAndCross() { + return new CoverDataFollower_ToggleButtonWidget() + .setToggleTexture(GT_UITextures.OVERLAY_BUTTON_CHECKMARK, GT_UITextures.OVERLAY_BUTTON_CROSS); + } + + public static CoverDataFollower_ToggleButtonWidget ofCheck() { + return new CoverDataFollower_ToggleButtonWidget() + .setToggleTexture(GT_UITextures.OVERLAY_BUTTON_CHECKMARK, GT_UITextures.TRANSPARENT); + } + + public static CoverDataFollower_ToggleButtonWidget ofRedstone() { + return new CoverDataFollower_ToggleButtonWidget() + .setToggleTexture(GT_UITextures.OVERLAY_BUTTON_REDSTONE_ON, GT_UITextures.OVERLAY_BUTTON_REDSTONE_OFF); + } + + public static CoverDataFollower_ToggleButtonWidget ofDisableable() { + return new CoverDataFollower_DisableableToggleButtonWidget<>(); + } + + /** + * Disables clicking if button is already pressed. + */ + public static class CoverDataFollower_DisableableToggleButtonWidget + extends CoverDataFollower_ToggleButtonWidget { + + public CoverDataFollower_DisableableToggleButtonWidget() { + super(); + } + + @Override + protected boolean canClick() { + return getState() == 0; + } + + @Override + public IDrawable[] getBackground() { + if (!canClick()) return new IDrawable[] {GT_UITextures.BUTTON_COVER_NORMAL_DISABLED}; + return super.getBackground(); + } + } +} diff --git a/src/main/java/gregtech/common/gui/modularui/widget/DataControllerWidget.java b/src/main/java/gregtech/common/gui/modularui/widget/DataControllerWidget.java new file mode 100644 index 0000000000..f29b8eeaf9 --- /dev/null +++ b/src/main/java/gregtech/common/gui/modularui/widget/DataControllerWidget.java @@ -0,0 +1,162 @@ +package gregtech.common.gui.modularui.widget; + +import com.gtnewhorizons.modularui.api.widget.ISyncedWidget; +import com.gtnewhorizons.modularui.api.widget.Widget; +import com.gtnewhorizons.modularui.common.internal.network.NetworkUtils; +import com.gtnewhorizons.modularui.common.widget.MultiChildWidget; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import gregtech.api.gui.modularui.IDataFollowerWidget; +import gregtech.api.util.ISerializableObject; +import java.io.IOException; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import net.minecraft.network.PacketBuffer; + +/** + * Controls state of child widgets with specific data, and allows centralized control of multiple widgets. + * e.g. clicking button B will set machine mode to B, so button A, whose state is bound to the mode, + * will be automatically deactivated by this widget. + *
This widget wraps data and handles validation, e.g. tell client to close GUI when tile is broken or cover is removed. + *
Data can be anything, e.g. {@link ISerializableObject} or machine recipe mode. + * @param Data type stored in this widget + * @see IDataFollowerWidget + */ +public abstract class DataControllerWidget extends MultiChildWidget implements ISyncedWidget { + + private final Supplier dataGetter; + private final Function dataSetter; + + protected T lastData; + + private boolean needsUpdate; + + /** + * @param dataGetter () -> data this widget handles + * @param dataSetter data to set -> if setting data is successful + */ + public DataControllerWidget(Supplier dataGetter, Function dataSetter) { + this.dataGetter = dataGetter; + this.dataSetter = dataSetter; + } + + protected T getLastData() { + return lastData; + } + + @Override + public void onPostInit() { + super.onPostInit(); + // client _should_ have received initial cover data from `GT_UIInfos#openCoverUI` + lastData = dataGetter.get(); + if (NetworkUtils.isClient()) { + updateChildren(true); + } + } + + @Override + public void detectAndSendChanges(boolean init) { + T actualValue = dataGetter.get(); + if (actualValue == null) { + // data is in invalid state e.g. tile is broken, cover is removed + getWindow().tryClose(); + return; + } + if (init || !actualValue.equals(getLastData())) { + // init sync or someone else edited data + lastData = actualValue; + syncDataToClient(actualValue); + } + } + + protected void syncDataToClient(T data) { + syncToClient(0, buffer -> writeToPacket(buffer, data)); + } + + protected void syncDataToServer(T data) { + syncToServer(0, buffer -> writeToPacket(buffer, data)); + updateChildren(); + } + + @Override + public void readOnClient(int id, PacketBuffer buf) throws IOException { + if (id == 0) { + lastData = readFromPacket(buf); + dataSetter.apply(getLastData()); + updateChildren(); + } + } + + @Override + public void readOnServer(int id, PacketBuffer buf) throws IOException { + if (id == 0) { + lastData = readFromPacket(buf); + if (dataSetter.apply(getLastData())) { + markForUpdate(); + } else { + getWindow().closeWindow(); + } + } + } + + @SuppressWarnings("unchecked") + @SideOnly(Side.CLIENT) + protected void updateChildren(boolean postInit) { + for (Widget child : getChildren()) { + if (child instanceof IDataFollowerWidget) { + ((IDataFollowerWidget) child).updateState(getLastData()); + if (postInit) { + ((IDataFollowerWidget) child).onPostInit(); + } + } + } + } + + @SideOnly(Side.CLIENT) + protected void updateChildren() { + updateChildren(false); + } + + protected abstract void writeToPacket(PacketBuffer buffer, T data); + + protected abstract T readFromPacket(PacketBuffer buffer) throws IOException; + + @Override + public void markForUpdate() { + needsUpdate = true; + } + + @Override + public void unMarkForUpdate() { + needsUpdate = false; + } + + @Override + public boolean isMarkedForUpdate() { + return needsUpdate; + } + + /** + * @param widget widget to add that implements {@link IDataFollowerWidget} + * @param dataToStateGetter given data -> state of the widget to add + * @param dataUpdater (current data, state of the widget to add) -> new data to set + * @param applyForWidget methods to call for the widget to add + * @param state type stored in the widget to add + * @param widget type to add + */ + public > DataControllerWidget addFollower( + W widget, Function dataToStateGetter, BiFunction dataUpdater, Consumer applyForWidget) { + widget.setDataToStateGetter(dataToStateGetter); + widget.setStateSetter(state -> { + T newData = dataUpdater.apply(getLastData(), state); + lastData = newData; + dataSetter.apply(getLastData()); + syncDataToServer(newData); + }); + applyForWidget.accept(widget); + addChild(widget); + return this; + } +} diff --git a/src/main/java/gregtech/common/gui/modularui/widget/FluidDisplaySlotWidget.java b/src/main/java/gregtech/common/gui/modularui/widget/FluidDisplaySlotWidget.java new file mode 100644 index 0000000000..99200e5e8d --- /dev/null +++ b/src/main/java/gregtech/common/gui/modularui/widget/FluidDisplaySlotWidget.java @@ -0,0 +1,474 @@ +package gregtech.common.gui.modularui.widget; + +import com.gtnewhorizons.modularui.api.forge.IItemHandlerModifiable; +import com.gtnewhorizons.modularui.common.internal.wrapper.BaseSlot; +import com.gtnewhorizons.modularui.common.widget.SlotWidget; +import gregtech.GT_Mod; +import gregtech.api.interfaces.IFluidAccess; +import gregtech.api.interfaces.IHasFluidDisplayItem; +import gregtech.api.interfaces.metatileentity.IFluidLockable; +import gregtech.api.util.GT_Utility; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Supplier; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemStack; +import net.minecraft.network.PacketBuffer; +import net.minecraftforge.fluids.Fluid; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.IFluidContainerItem; + +public class FluidDisplaySlotWidget extends SlotWidget { + + private IHasFluidDisplayItem iHasFluidDisplay; + private Supplier fluidAccessConstructor; + private Supplier canDrainGetter; + private Supplier canFillGetter; + private Action actionRealClick = Action.NONE; + private Action actionDragAndDrop = Action.NONE; + private BiFunction beforeRealClick; + private BiFunction beforeDragAndDrop; + private Runnable updateFluidDisplayItem = () -> { + if (iHasFluidDisplay != null) { + iHasFluidDisplay.updateFluidDisplayItem(); + } + }; + + public FluidDisplaySlotWidget(BaseSlot slot) { + super(slot); + setAccess(false, false); + disableShiftInsert(); + } + + public FluidDisplaySlotWidget(IItemHandlerModifiable handler, int index) { + this(new BaseSlot(handler, index, true)); + } + + // === client actions === + + @Override + public ClickResult onClick(int buttonId, boolean doubleClick) { + if (actionRealClick == Action.NONE) return ClickResult.REJECT; + if (interactionDisabled) return ClickResult.REJECT; + + /* + * While a logical client don't really need to process fluid cells upon click (it could have just wait + * for server side to send the result), doing so would result in every fluid interaction having a + * noticeable delay between clicking and changes happening even on single player. + * I'd imagine this lag to become only more severe when playing MP over ethernet, which would have much more latency + * than a memory connection + */ + ClickData clickData = ClickData.create(buttonId, doubleClick); + ItemStack verifyToken = executeRealClick(clickData); + syncToServer(2, buffer -> { + clickData.writeToPacket(buffer); + try { + buffer.writeItemStackToBuffer(verifyToken); + } catch (IOException e) { + e.printStackTrace(); + } + }); + return ClickResult.ACCEPT; + } + + @Override + public boolean handleDragAndDrop(ItemStack draggedStack, int button) { + if (actionDragAndDrop == Action.NONE || actionDragAndDrop == Action.TRANSFER) return false; + if (interactionDisabled) return false; + + ClickData clickData = ClickData.create(button, false); + executeDragAndDrop(clickData, draggedStack); + syncToServer(5, buffer -> { + try { + clickData.writeToPacket(buffer); + buffer.writeItemStackToBuffer(draggedStack); + } catch (IOException e) { + e.printStackTrace(); + } + }); + draggedStack.stackSize = 0; + return true; + } + + @Override + public List getExtraTooltip() { + return Collections.emptyList(); + } + + // === server actions === + + @Override + public void readOnServer(int id, PacketBuffer buf) throws IOException { + if (id == 1) { + getMcSlot().xDisplayPosition = buf.readVarIntFromBuffer(); + getMcSlot().yDisplayPosition = buf.readVarIntFromBuffer(); + } else if (id == 2) { + onClickServer(ClickData.readPacket(buf), buf.readItemStackFromBuffer()); + } else if (id == 3) { + phantomScroll(buf.readVarIntFromBuffer()); + } else if (id == 4) { + setEnabled(buf.readBoolean()); + } else if (id == 5) { + executeDragAndDrop(ClickData.readPacket(buf), buf.readItemStackFromBuffer()); + if (onDragAndDropComplete != null) { + onDragAndDropComplete.accept(this); + } + } + markForUpdate(); + } + + private void onClickServer(ClickData clickData, ItemStack clientVerifyToken) { + ItemStack serverVerifyToken = executeRealClick(clickData); + // similar to what NetHandlerPlayServer#processClickWindow does + if (!ItemStack.areItemStacksEqual(clientVerifyToken, serverVerifyToken)) { + ((EntityPlayerMP) getContext().getPlayer()) + .sendContainerToPlayer(getContext().getContainer()); + } + } + + // === client/server actions === + + private ItemStack executeRealClick(ClickData clickData) { + if (actionRealClick == Action.NONE) return null; + if (beforeRealClick != null && !beforeRealClick.apply(clickData, this)) return null; + + ItemStack ret = null; + if (actionRealClick == Action.TRANSFER) { + if (fluidAccessConstructor == null) { + GT_Mod.GT_FML_LOGGER.warn( + "FluidDisplaySlotWidget is asked to transfer fluid, but fluidAccessConstructor is null!"); + return null; + } + ret = transferFluid( + fluidAccessConstructor.get(), + getContext().getPlayer(), + clickData.mouseButton == 0, + canDrainGetter != null ? canDrainGetter.get() : true, + canFillGetter != null ? canFillGetter.get() : true); + } else if (actionRealClick == Action.LOCK) { + lockFluid(getContext().getPlayer().inventory.getItemStack()); + } + + updateFluidDisplayItem.run(); + return ret; + } + + protected ItemStack transferFluid( + IFluidAccess aFluidAccess, + EntityPlayer aPlayer, + boolean aProcessFullStack, + boolean aCanDrain, + boolean aCanFill) { + ItemStack tStackHeld = aPlayer.inventory.getItemStack(); + ItemStack tStackSizedOne = GT_Utility.copyAmount(1, tStackHeld); + if (tStackSizedOne == null || tStackHeld.stackSize == 0) return null; + FluidStack tInputFluid = aFluidAccess.get(); + FluidStack tFluidHeld = GT_Utility.getFluidForFilledItem(tStackSizedOne, true); + if (tFluidHeld != null && tFluidHeld.amount <= 0) tFluidHeld = null; + if (tInputFluid == null) { + // tank empty, consider fill only from now on + if (!aCanFill) + // cannot fill and nothing to take, bail out + return null; + if (tFluidHeld == null) + // no fluid to fill + return null; + return fillFluid(aFluidAccess, aPlayer, tFluidHeld, aProcessFullStack); + } + // tank not empty, both action possible + if (tFluidHeld != null && tInputFluid.amount < aFluidAccess.getCapacity()) { + // both nonnull and have space left for filling. + if (aCanFill) + // actually both pickup and fill is reasonable, but I'll go with fill here + return fillFluid(aFluidAccess, aPlayer, tFluidHeld, aProcessFullStack); + if (!aCanDrain) + // cannot take AND cannot fill, why make this call then? + return null; + // the slot does not allow filling, so try take some + return drainFluid(aFluidAccess, aPlayer, aProcessFullStack); + } else { + // cannot fill and there is something to take + if (!aCanDrain) + // but the slot does not allow taking, so bail out + return null; + return drainFluid(aFluidAccess, aPlayer, aProcessFullStack); + } + } + + protected static ItemStack drainFluid(IFluidAccess aFluidAccess, EntityPlayer aPlayer, boolean aProcessFullStack) { + FluidStack tTankStack = aFluidAccess.get(); + if (tTankStack == null) return null; + ItemStack tStackHeld = aPlayer.inventory.getItemStack(); + ItemStack tStackSizedOne = GT_Utility.copyAmount(1, tStackHeld); + if (tStackSizedOne == null || tStackHeld.stackSize == 0) return null; + int tOriginalFluidAmount = tTankStack.amount; + ItemStack tFilledContainer = GT_Utility.fillFluidContainer(tTankStack, tStackSizedOne, true, false); + if (tFilledContainer == null && tStackSizedOne.getItem() instanceof IFluidContainerItem) { + IFluidContainerItem tContainerItem = (IFluidContainerItem) tStackSizedOne.getItem(); + int tFilledAmount = tContainerItem.fill(tStackSizedOne, tTankStack, true); + if (tFilledAmount > 0) { + tFilledContainer = tStackSizedOne; + tTankStack.amount -= tFilledAmount; + } + } + if (tFilledContainer != null) { + if (aProcessFullStack) { + int tFilledAmount = tOriginalFluidAmount - tTankStack.amount; + /* + work out how many more items we can fill + one cell is already used, so account for that + the round down behavior will left over a fraction of a cell worth of fluid + the user then get to decide what to do with it + it will not be too fancy if it spills out partially filled cells + */ + int tAdditionalParallel = Math.min(tStackHeld.stackSize - 1, tTankStack.amount / tFilledAmount); + tTankStack.amount -= tFilledAmount * tAdditionalParallel; + tFilledContainer.stackSize += tAdditionalParallel; + } + replaceCursorItemStack(aPlayer, tFilledContainer); + } + aFluidAccess.verifyFluidStack(); + return tFilledContainer; + } + + protected static ItemStack fillFluid( + IFluidAccess aFluidAccess, EntityPlayer aPlayer, FluidStack aFluidHeld, boolean aProcessFullStack) { + // we are not using aMachine.fill() here any more, so we need to check for fluid type here ourselves + if (aFluidAccess.get() != null && !aFluidAccess.get().isFluidEqual(aFluidHeld)) return null; + ItemStack tStackHeld = aPlayer.inventory.getItemStack(); + ItemStack tStackSizedOne = GT_Utility.copyAmount(1, tStackHeld); + if (tStackSizedOne == null) return null; + + int tFreeSpace = aFluidAccess.getCapacity() - (aFluidAccess.get() != null ? aFluidAccess.get().amount : 0); + if (tFreeSpace <= 0) + // no space left + return null; + + // find out how much fluid can be taken + // some cells cannot be partially filled + ItemStack tStackEmptied = null; + int tAmountTaken = 0; + if (tFreeSpace >= aFluidHeld.amount) { + // fully accepted - try take it from item now + // IFluidContainerItem is intentionally not checked here. it will be checked later + tStackEmptied = GT_Utility.getContainerForFilledItem(tStackSizedOne, false); + tAmountTaken = aFluidHeld.amount; + } + if (tStackEmptied == null && tStackSizedOne.getItem() instanceof IFluidContainerItem) { + // either partially accepted, or is IFluidContainerItem + IFluidContainerItem container = (IFluidContainerItem) tStackSizedOne.getItem(); + FluidStack tDrained = container.drain(tStackSizedOne, tFreeSpace, true); + if (tDrained != null && tDrained.amount > 0) { + // something is actually drained - change the cell and drop it to player + tStackEmptied = tStackSizedOne; + tAmountTaken = tDrained.amount; + } + } + if (tStackEmptied == null) + // somehow the cell refuse to give out that amount of fluid, no op then + return null; + + // find out how many fill can we do + // same round down behavior as above + // however here the fluid stack is not changed at all, so the exact code will slightly differ + int tParallel = aProcessFullStack ? Math.min(tFreeSpace / tAmountTaken, tStackHeld.stackSize) : 1; + if (aFluidAccess.get() == null) { + FluidStack tNewFillableStack = aFluidHeld.copy(); + tNewFillableStack.amount = tAmountTaken * tParallel; + aFluidAccess.set(tNewFillableStack); + } else { + aFluidAccess.addAmount(tAmountTaken * tParallel); + } + tStackEmptied.stackSize = tParallel; + replaceCursorItemStack(aPlayer, tStackEmptied); + return tStackEmptied; + } + + protected static void replaceCursorItemStack(EntityPlayer aPlayer, ItemStack tStackResult) { + int tStackResultMaxStackSize = tStackResult.getMaxStackSize(); + while (tStackResult.stackSize > tStackResultMaxStackSize) { + aPlayer.inventory.getItemStack().stackSize -= tStackResultMaxStackSize; + GT_Utility.addItemToPlayerInventory(aPlayer, tStackResult.splitStack(tStackResultMaxStackSize)); + } + if (aPlayer.inventory.getItemStack().stackSize == tStackResult.stackSize) { + // every cell is mutated. it could just stay on the cursor. + aPlayer.inventory.setItemStack(tStackResult); + } else { + // some cells not mutated. The mutated cells must go into the inventory + // or drop into the world if there isn't enough space. + ItemStack tStackHeld = aPlayer.inventory.getItemStack(); + tStackHeld.stackSize -= tStackResult.stackSize; + GT_Utility.addItemToPlayerInventory(aPlayer, tStackResult); + } + } + + protected void executeDragAndDrop(ClickData clickData, ItemStack draggedStack) { + if (actionDragAndDrop == Action.NONE || actionDragAndDrop == Action.TRANSFER) return; + if (beforeDragAndDrop != null && !beforeDragAndDrop.apply(clickData, this)) return; + + if (actionDragAndDrop == Action.LOCK) { + lockFluid(draggedStack); + } + updateFluidDisplayItem.run(); + } + + protected void lockFluid(ItemStack cursorStack) { + if (!(iHasFluidDisplay instanceof IFluidLockable)) return; + IFluidLockable mteToLock = (IFluidLockable) iHasFluidDisplay; + + if (cursorStack == null) { + if (!mteToLock.allowChangingLockedFluid(null)) return; + + mteToLock.lockFluid(false); + mteToLock.setLockedFluidName(null); + GT_Utility.sendChatToPlayer(getContext().getPlayer(), GT_Utility.trans("300.1", "Fluid Lock Cleared.")); + + if (!isClient()) { + mteToLock.onFluidLockPacketReceived(null); + } + } else { + FluidStack fluidStack = GT_Utility.getFluidFromContainerOrFluidDisplay(cursorStack); + if (fluidStack == null) return; + Fluid tFluid = fluidStack.getFluid(); + if (tFluid == null) return; + + if (!mteToLock.allowChangingLockedFluid(tFluid.getName())) return; + + mteToLock.lockFluid(true); + mteToLock.setLockedFluidName(tFluid.getName()); + GT_Utility.sendChatToPlayer( + getContext().getPlayer(), + String.format( + GT_Utility.trans("151.4", "Successfully locked Fluid to %s"), + new FluidStack(tFluid, 1).getLocalizedName())); + + if (!isClient()) { + mteToLock.onFluidLockPacketReceived(tFluid.getName()); + } + } + } + + protected void updateFluidDisplayItem() { + if (iHasFluidDisplay != null) { + iHasFluidDisplay.updateFluidDisplayItem(); + } + } + + // === setters === + + public FluidDisplaySlotWidget setFluidAccessConstructor(Supplier fluidAccessConstructor) { + this.fluidAccessConstructor = fluidAccessConstructor; + return this; + } + + public FluidDisplaySlotWidget setIHasFluidDisplay(IHasFluidDisplayItem iHasFluidDisplay) { + this.iHasFluidDisplay = iHasFluidDisplay; + return this; + } + + public FluidDisplaySlotWidget setCanDrainGetter(Supplier canDrainGetter) { + this.canDrainGetter = canDrainGetter; + return this; + } + + public FluidDisplaySlotWidget setCanDrain(boolean canDrain) { + return setCanDrainGetter(() -> canDrain); + } + + public FluidDisplaySlotWidget setCanFillGetter(Supplier canFillGetter) { + this.canFillGetter = canFillGetter; + return this; + } + + public FluidDisplaySlotWidget setCanFill(boolean canFill) { + return setCanFillGetter(() -> canFill); + } + + /** + * Sets action called on click while holding the real item. + */ + public FluidDisplaySlotWidget setActionRealClick(Action actionRealClick) { + this.actionRealClick = actionRealClick; + return this; + } + + /** + * Sets action called on drag-and-drop from NEI. + * You can't use {@link Action#TRANSFER} here. + */ + public FluidDisplaySlotWidget setActionDragAndDrop(Action actionDragAndDrop) { + this.actionDragAndDrop = actionDragAndDrop; + return this; + } + + /** + * Sets function called before {@link #executeRealClick}. + * @param beforeRealClick (click data, this widget) -> if allow click + */ + public FluidDisplaySlotWidget setBeforeRealClick( + BiFunction beforeRealClick) { + this.beforeRealClick = beforeRealClick; + return this; + } + + /** + * Sets function called before {@link #executeDragAndDrop}. + * @param beforeDragAndDrop (click data, this widget) -> if allow click + */ + public FluidDisplaySlotWidget setBeforeDragAndDrop( + BiFunction beforeDragAndDrop) { + this.beforeDragAndDrop = beforeDragAndDrop; + return this; + } + + /** + * Sets function called before both of {@link #executeRealClick} and {@link #executeDragAndDrop}. + * @param beforeClick (click data, this widget) -> if allow click + */ + public FluidDisplaySlotWidget setBeforeClick(BiFunction beforeClick) { + setBeforeRealClick(beforeClick); + setBeforeDragAndDrop(beforeClick); + return this; + } + + /** + * By default, this widget runs {@link IHasFluidDisplayItem#updateFluidDisplayItem} after click. + * You can specify custom update action with this method. + */ + public FluidDisplaySlotWidget setUpdateFluidDisplayItem(Runnable updateFluidDisplayItem) { + this.updateFluidDisplayItem = updateFluidDisplayItem; + return this; + } + + /** + * Action triggered on mouse click or NEI drag-and-drop. + */ + public enum Action { + + /** + * Fill/drain fluid into/from the tank. + * Uses fluid amount, so drag-and-drop cannot use this mode. + */ + TRANSFER, + + /** + * Lock fluid for {@link IFluidLockable}. + * Does not use fluid amount. + */ + LOCK, + + /** + * Set filter for the tank. (not implemented yet) + * Does not use fluid amount. + */ + FILTER, + + /** + * Does nothing. + */ + NONE + } +} diff --git a/src/main/java/gregtech/common/gui/modularui/widget/ItemWatcherSlotWidget.java b/src/main/java/gregtech/common/gui/modularui/widget/ItemWatcherSlotWidget.java new file mode 100644 index 0000000000..7385208874 --- /dev/null +++ b/src/main/java/gregtech/common/gui/modularui/widget/ItemWatcherSlotWidget.java @@ -0,0 +1,47 @@ +package gregtech.common.gui.modularui.widget; + +import com.gtnewhorizons.modularui.api.forge.IItemHandlerModifiable; +import com.gtnewhorizons.modularui.api.forge.ItemStackHandler; +import com.gtnewhorizons.modularui.common.internal.wrapper.BaseSlot; +import com.gtnewhorizons.modularui.common.widget.SlotWidget; +import gregtech.api.util.GT_Utility; +import java.util.function.Supplier; +import net.minecraft.item.ItemStack; + +/** + * Watches specific ItemStack and pulls changes from it. + * Player cannot interact with slot, other than viewing NEI recipe or adding bookmark. + */ +public class ItemWatcherSlotWidget extends SlotWidget { + + private ItemStack lastItem; + private Supplier getter; + + public ItemWatcherSlotWidget() { + super(BaseSlot.phantom(new ItemStackHandler(), 0)); + disableInteraction(); + } + + public ItemWatcherSlotWidget setGetter(Supplier getter) { + this.getter = getter; + return this; + } + + @Override + public void detectAndSendChanges(boolean init) { + ItemStack target = getter.get(); + if (init || !GT_Utility.areStacksEqual(lastItem, target)) { + ItemStack toPut; + if (target != null) { + toPut = target.copy(); + toPut.stackSize = 1; + } else { + toPut = null; + } + ((IItemHandlerModifiable) getMcSlot().getItemHandler()).setStackInSlot(0, toPut); + lastItem = target; + getMcSlot().onSlotChanged(); + } + super.detectAndSendChanges(init); + } +} -- cgit