diff options
author | miozune <miozune@gmail.com> | 2022-11-26 01:45:28 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-25 17:45:28 +0100 |
commit | 9a2741128a78bb52eba50a631126e090a5a2abd8 (patch) | |
tree | a90f47aa94951acb4050e45dc3ed60698e79cf32 /src/main/java/gregtech/common/gui/modularui/widget | |
parent | 51537482fefc4f9c6d3fbd93d119c333a63dcd7b (diff) | |
download | GT5-Unofficial-9a2741128a78bb52eba50a631126e090a5a2abd8.tar.gz GT5-Unofficial-9a2741128a78bb52eba50a631126e090a5a2abd8.tar.bz2 GT5-Unofficial-9a2741128a78bb52eba50a631126e090a5a2abd8.zip |
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 <miozune@gmail.com>
* 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 <mitchej@gmail.com>
Diffstat (limited to 'src/main/java/gregtech/common/gui/modularui/widget')
10 files changed, 1284 insertions, 0 deletions
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<T extends ISerializableObject> extends DataControllerWidget<T> { + + protected final GT_CoverBehaviorBase<T> 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<T> dataGetter, Function<T, Boolean> dataSetter, GT_CoverBehaviorBase<T> coverBehavior) { + super(dataGetter, dataSetter); + this.coverBehavior = coverBehavior; + } + + @Override + public <U, W extends Widget & IDataFollowerWidget<T, U>> CoverDataControllerWidget<T> addFollower( + W widget, Function<T, U> dataToStateGetter, BiFunction<T, U, T> dataUpdater, Consumer<W> 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<T extends ISerializableObject> + extends CoverDataControllerWidget<T> { + + private final BiFunction<Integer, T, Boolean> dataToStateGetter; + private final BiFunction<Integer, T, T> 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<T> coverDataGetter, + Function<T, Boolean> coverDataSetter, + GT_CoverBehaviorBase<T> coverBehavior, + BiFunction<Integer, T, Boolean> dataToStateGetter, + BiFunction<Integer, T, T> 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 <W extends CoverDataFollower_ToggleButtonWidget<T>> + CoverDataIndexedControllerWidget_ToggleButtons<T> addToggleButton( + int index, W widget, Consumer<CoverDataFollower_ToggleButtonWidget<T>> 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<T extends ISerializableObject> + extends CoverDataControllerWidget<T> { + + private final BiFunction<Integer, T, Integer> dataToStateGetter; + private final BiFunction<Integer, T, T> 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<T> coverDataGetter, + Function<T, Boolean> coverDataSetter, + GT_CoverBehaviorBase<T> coverBehavior, + BiFunction<Integer, T, Integer> dataToStateGetter, + BiFunction<Integer, T, T> 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 <W extends CoverDataFollower_CycleButtonWidget<T>> + CoverDataIndexedControllerWidget_CycleButtons<T> addCycleButton( + int index, W widget, Consumer<CoverDataFollower_CycleButtonWidget<T>> 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<T extends ISerializableObject> extends CoverCycleButtonWidget + implements IDataFollowerWidget<T, Integer> { + + private Function<T, Integer> dataToStateGetter; + + public CoverDataFollower_CycleButtonWidget() { + super(); + setGetter(() -> 0); // fake getter; used only for init + setSynced(false, false); + } + + @Override + public CoverDataFollower_CycleButtonWidget<T> setDataToStateGetter(Function<T, Integer> dataToStateGetter) { + this.dataToStateGetter = dataToStateGetter; + return this; + } + + @Override + public CoverDataFollower_CycleButtonWidget<T> setStateSetter(Consumer<Integer> 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<T extends ISerializableObject> extends SlotWidget + implements IDataFollowerWidget<T, ItemStack> { + + private Function<T, ItemStack> dataToStateGetter; + private Consumer<ItemStack> 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<T> setDataToStateGetter(Function<T, ItemStack> dataToStateGetter) { + this.dataToStateGetter = dataToStateGetter; + return this; + } + + @Override + public CoverDataFollower_SlotWidget<T> setStateSetter(Consumer<ItemStack> 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<T extends ISerializableObject> extends TextFieldWidget + implements IDataFollowerWidget<T, String> { + + private Function<T, String> 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<T> setDataToStateGetter(Function<T, String> dataToStateGetter) { + this.dataToStateGetter = dataToStateGetter; + return this; + } + + @Override + public CoverDataFollower_TextFieldWidget<T> setStateSetter(Consumer<String> setter) { + super.setSetter(setter); + return this; + } + + @Override + public void updateState(T data) { + setText(dataToStateGetter.apply(data)); + } + + public CoverDataFollower_TextFieldWidget<T> 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<T> setOnScrollNumbers() { + return setOnScrollNumbers(1, 50, 1000); + } + + public CoverDataFollower_TextFieldWidget<T> 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<T> setOnScrollText() { + return setOnScrollText(1, 5, 50); + } + + public CoverDataFollower_TextFieldWidget<T> 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<T extends ISerializableObject> extends CoverCycleButtonWidget + implements IDataFollowerWidget<T, Boolean> { + + private Function<T, Boolean> dataToStateGetter; + + public CoverDataFollower_ToggleButtonWidget() { + super(); + setGetter(() -> 0); // fake getter; used only for init + setSynced(false, false); + setLength(2); + } + + @Override + public CoverDataFollower_ToggleButtonWidget<T> setDataToStateGetter(Function<T, Boolean> dataToStateGetter) { + this.dataToStateGetter = dataToStateGetter; + return this; + } + + @Override + public CoverDataFollower_ToggleButtonWidget<T> setStateSetter(Consumer<Boolean> 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<T> setToggleTexture(IDrawable active, IDrawable inactive) { + setTextureGetter(state -> state == 1 ? active : inactive); + return this; + } + + public static <T extends ISerializableObject> CoverDataFollower_ToggleButtonWidget<T> ofCheckAndCross() { + return new CoverDataFollower_ToggleButtonWidget<T>() + .setToggleTexture(GT_UITextures.OVERLAY_BUTTON_CHECKMARK, GT_UITextures.OVERLAY_BUTTON_CROSS); + } + + public static <T extends ISerializableObject> CoverDataFollower_ToggleButtonWidget<T> ofCheck() { + return new CoverDataFollower_ToggleButtonWidget<T>() + .setToggleTexture(GT_UITextures.OVERLAY_BUTTON_CHECKMARK, GT_UITextures.TRANSPARENT); + } + + public static <T extends ISerializableObject> CoverDataFollower_ToggleButtonWidget<T> ofRedstone() { + return new CoverDataFollower_ToggleButtonWidget<T>() + .setToggleTexture(GT_UITextures.OVERLAY_BUTTON_REDSTONE_ON, GT_UITextures.OVERLAY_BUTTON_REDSTONE_OFF); + } + + public static <T extends ISerializableObject> CoverDataFollower_ToggleButtonWidget<T> ofDisableable() { + return new CoverDataFollower_DisableableToggleButtonWidget<>(); + } + + /** + * Disables clicking if button is already pressed. + */ + public static class CoverDataFollower_DisableableToggleButtonWidget<T extends ISerializableObject> + extends CoverDataFollower_ToggleButtonWidget<T> { + + 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. + * <br> This widget wraps data and handles validation, e.g. tell client to close GUI when tile is broken or cover is removed. + * <br> Data can be anything, e.g. {@link ISerializableObject} or machine recipe mode. + * @param <T> Data type stored in this widget + * @see IDataFollowerWidget + */ +public abstract class DataControllerWidget<T> extends MultiChildWidget implements ISyncedWidget { + + private final Supplier<T> dataGetter; + private final Function<T, Boolean> 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<T> dataGetter, Function<T, Boolean> 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<T, ?>) child).updateState(getLastData()); + if (postInit) { + ((IDataFollowerWidget<T, ?>) 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 <U> state type stored in the widget to add + * @param <W> widget type to add + */ + public <U, W extends Widget & IDataFollowerWidget<T, U>> DataControllerWidget<T> addFollower( + W widget, Function<T, U> dataToStateGetter, BiFunction<T, U, T> dataUpdater, Consumer<W> 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<IFluidAccess> fluidAccessConstructor; + private Supplier<Boolean> canDrainGetter; + private Supplier<Boolean> canFillGetter; + private Action actionRealClick = Action.NONE; + private Action actionDragAndDrop = Action.NONE; + private BiFunction<ClickData, FluidDisplaySlotWidget, Boolean> beforeRealClick; + private BiFunction<ClickData, FluidDisplaySlotWidget, Boolean> 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<String> 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<IFluidAccess> fluidAccessConstructor) { + this.fluidAccessConstructor = fluidAccessConstructor; + return this; + } + + public FluidDisplaySlotWidget setIHasFluidDisplay(IHasFluidDisplayItem iHasFluidDisplay) { + this.iHasFluidDisplay = iHasFluidDisplay; + return this; + } + + public FluidDisplaySlotWidget setCanDrainGetter(Supplier<Boolean> canDrainGetter) { + this.canDrainGetter = canDrainGetter; + return this; + } + + public FluidDisplaySlotWidget setCanDrain(boolean canDrain) { + return setCanDrainGetter(() -> canDrain); + } + + public FluidDisplaySlotWidget setCanFillGetter(Supplier<Boolean> 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<ClickData, FluidDisplaySlotWidget, Boolean> 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<ClickData, FluidDisplaySlotWidget, Boolean> 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<ClickData, FluidDisplaySlotWidget, Boolean> 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<ItemStack> getter; + + public ItemWatcherSlotWidget() { + super(BaseSlot.phantom(new ItemStackHandler(), 0)); + disableInteraction(); + } + + public ItemWatcherSlotWidget setGetter(Supplier<ItemStack> 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); + } +} |