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 --- .../gregtech/common/gui/modularui/UIHelper.java | 222 ++++++++++ .../modularui/uifactory/SelectItemUIFactory.java | 221 ++++++++++ .../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 ++ 12 files changed, 1727 insertions(+) create mode 100644 src/main/java/gregtech/common/gui/modularui/UIHelper.java create mode 100644 src/main/java/gregtech/common/gui/modularui/uifactory/SelectItemUIFactory.java 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') diff --git a/src/main/java/gregtech/common/gui/modularui/UIHelper.java b/src/main/java/gregtech/common/gui/modularui/UIHelper.java new file mode 100644 index 0000000000..f500514258 --- /dev/null +++ b/src/main/java/gregtech/common/gui/modularui/UIHelper.java @@ -0,0 +1,222 @@ +package gregtech.common.gui.modularui; + +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.math.Pos2d; +import gregtech.api.enums.SteamVariant; +import gregtech.api.gui.modularui.SteamTexture; +import gregtech.api.util.GT_Recipe; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +public class UIHelper { + + /** + * Iterates over candidates for slot placement. + */ + public static void forEachSlots( + ForEachSlot forEachItemInputSlot, + ForEachSlot forEachItemOutputSlot, + ForEachSlot forEachSpecialSlot, + ForEachSlot forEachFluidInputSlot, + ForEachSlot forEachFluidOutputSlot, + IDrawable itemSlotBackground, + IDrawable fluidSlotBackground, + @Nullable GT_Recipe.GT_Recipe_Map recipeMap, + int itemInputCount, + int itemOutputCount, + int fluidInputCount, + int fluidOutputCount, + SteamVariant steamVariant, + Pos2d offset) { + List itemInputPositions = recipeMap != null + ? recipeMap.getItemInputPositions(itemInputCount) + : UIHelper.getItemInputPositions(itemInputCount); + itemInputPositions = itemInputPositions.stream().map(p -> p.add(offset)).collect(Collectors.toList()); + for (int i = 0; i < itemInputPositions.size(); i++) { + forEachItemInputSlot.accept( + i, + getBackgroundsForSlot(itemSlotBackground, recipeMap, false, false, i, false, steamVariant), + itemInputPositions.get(i)); + } + + List itemOutputPositions = recipeMap != null + ? recipeMap.getItemOutputPositions(itemOutputCount) + : UIHelper.getItemOutputPositions(itemOutputCount); + itemOutputPositions = + itemOutputPositions.stream().map(p -> p.add(offset)).collect(Collectors.toList()); + for (int i = 0; i < itemOutputPositions.size(); i++) { + forEachItemOutputSlot.accept( + i, + getBackgroundsForSlot(itemSlotBackground, recipeMap, false, true, i, false, steamVariant), + itemOutputPositions.get(i)); + } + + forEachSpecialSlot.accept( + 0, + getBackgroundsForSlot(itemSlotBackground, recipeMap, false, false, 0, true, steamVariant), + (recipeMap != null ? recipeMap.getSpecialItemPosition() : UIHelper.getSpecialItemPosition()) + .add(offset)); + + List fluidInputPositions = recipeMap != null + ? recipeMap.getFluidInputPositions(fluidInputCount) + : UIHelper.getFluidInputPositions(fluidInputCount); + fluidInputPositions = + fluidInputPositions.stream().map(p -> p.add(offset)).collect(Collectors.toList()); + for (int i = 0; i < fluidInputPositions.size(); i++) { + forEachFluidInputSlot.accept( + i, + getBackgroundsForSlot(fluidSlotBackground, recipeMap, true, false, i, false, steamVariant), + fluidInputPositions.get(i)); + } + + List fluidOutputPositions = recipeMap != null + ? recipeMap.getFluidOutputPositions(fluidOutputCount) + : UIHelper.getFluidOutputPositions(fluidOutputCount); + fluidOutputPositions = + fluidOutputPositions.stream().map(p -> p.add(offset)).collect(Collectors.toList()); + for (int i = 0; i < fluidOutputPositions.size(); i++) { + forEachFluidOutputSlot.accept( + i, + getBackgroundsForSlot(fluidSlotBackground, recipeMap, true, true, i, false, steamVariant), + fluidOutputPositions.get(i)); + } + } + + /** + * @return Display positions for GUI, including border (18x18 size) + */ + public static List getItemInputPositions(int itemInputCount) { + switch (itemInputCount) { + case 0: + return Collections.emptyList(); + case 1: + return getItemGridPositions(itemInputCount, 52, 24, 1, 1); + case 2: + return getItemGridPositions(itemInputCount, 34, 24, 2, 1); + case 3: + return getItemGridPositions(itemInputCount, 16, 24, 3, 1); + case 4: + case 5: + return getItemGridPositions(itemInputCount, 16, 24, 3, 2); + case 6: + return getItemGridPositions(itemInputCount, 16, 15, 3, 2); + default: + return getItemGridPositions(itemInputCount, 16, 6, 3, 3); + } + } + + /** + * @return Display positions for GUI, including border (18x18 size) + */ + public static List getItemOutputPositions(int itemOutputCount) { + switch (itemOutputCount) { + case 0: + return Collections.emptyList(); + case 1: + return getItemGridPositions(itemOutputCount, 106, 24, 1, 1); + case 2: + return getItemGridPositions(itemOutputCount, 106, 24, 2, 1); + case 3: + return getItemGridPositions(itemOutputCount, 106, 24, 3, 1); + case 4: + return getItemGridPositions(itemOutputCount, 106, 15, 2, 2); + case 5: + case 6: + return getItemGridPositions(itemOutputCount, 106, 15, 3, 2); + default: + return getItemGridPositions(itemOutputCount, 106, 6, 3, 3); + } + } + + /** + * @return Display position for GUI, including border (18x18 size) + */ + public static Pos2d getSpecialItemPosition() { + return new Pos2d(124, 62); + } + + /** + * @return Display positions for GUI, including border (18x18 size) + */ + public static List getFluidInputPositions(int fluidInputCount) { + List results = new ArrayList<>(); + int x = 52; + for (int i = 0; i < fluidInputCount; i++) { + results.add(new Pos2d(x, 62)); + x -= 18; + } + return results; + } + + /** + * @return Display positions for GUI, including border (18x18 size) + */ + public static List getFluidOutputPositions(int fluidOutputCount) { + List results = new ArrayList<>(); + int x = 106; + for (int i = 0; i < fluidOutputCount; i++) { + results.add(new Pos2d(x, 62)); + x += 18; + } + return results; + } + + public static List getItemGridPositions( + int itemCount, int xOrigin, int yOrigin, int xDirMaxCount, int yDirMaxCount) { + // 18 pixels to get to a new grid for placing an item tile since they are 16x16 and have 1 pixel buffers + // around them. + int distanceGrid = 18; + int xMax = xOrigin + xDirMaxCount * distanceGrid; + + List results = new ArrayList<>(); + // Temp variables to keep track of current coordinates to place item at. + int xCoord = xOrigin; + int yCoord = yOrigin; + + for (int i = 0; i < itemCount; i++) { + results.add(new Pos2d(xCoord, yCoord)); + xCoord += distanceGrid; + if (xCoord == xMax) { + xCoord = xOrigin; + yCoord += distanceGrid; + } + } + + return results; + } + + private static IDrawable[] getBackgroundsForSlot( + IDrawable base, + GT_Recipe.GT_Recipe_Map recipeMap, + boolean isFluid, + boolean isOutput, + int index, + boolean isSpecial, + SteamVariant steamVariant) { + if (recipeMap != null) { + IDrawable overlay; + if (steamVariant != SteamVariant.NONE) { + SteamTexture steamTexture = recipeMap.getOverlayForSlotSteam(isFluid, isOutput, index, isSpecial); + if (steamTexture != null) { + overlay = steamTexture.get(steamVariant); + } else { + overlay = null; + } + } else { + overlay = recipeMap.getOverlayForSlot(isFluid, isOutput, index, isSpecial); + } + if (overlay != null) { + return new IDrawable[] {base, overlay}; + } + } + return new IDrawable[] {base}; + } + + @FunctionalInterface + public interface ForEachSlot { + void accept(int index, IDrawable[] backgrounds, Pos2d pos); + } +} diff --git a/src/main/java/gregtech/common/gui/modularui/uifactory/SelectItemUIFactory.java b/src/main/java/gregtech/common/gui/modularui/uifactory/SelectItemUIFactory.java new file mode 100644 index 0000000000..c3da5cb1b4 --- /dev/null +++ b/src/main/java/gregtech/common/gui/modularui/uifactory/SelectItemUIFactory.java @@ -0,0 +1,221 @@ +package gregtech.common.gui.modularui.uifactory; + +import com.gtnewhorizons.modularui.api.ModularUITextures; +import com.gtnewhorizons.modularui.api.drawable.IDrawable; +import com.gtnewhorizons.modularui.api.drawable.ItemDrawable; +import com.gtnewhorizons.modularui.api.drawable.Text; +import com.gtnewhorizons.modularui.api.forge.ItemStackHandler; +import com.gtnewhorizons.modularui.api.screen.ModularWindow; +import com.gtnewhorizons.modularui.api.screen.UIBuildContext; +import com.gtnewhorizons.modularui.common.internal.wrapper.BaseSlot; +import com.gtnewhorizons.modularui.common.widget.ButtonWidget; +import com.gtnewhorizons.modularui.common.widget.SlotWidget; +import com.gtnewhorizons.modularui.common.widget.TextWidget; +import gregtech.api.enums.Dyes; +import gregtech.api.gui.GT_GUIColorOverride; +import gregtech.api.gui.modularui.GT_UITextures; +import gregtech.api.util.GT_Util; +import gregtech.api.util.GT_Utility; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Supplier; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.StatCollector; + +/** + * Creates UI for selecting item from given list. + * This is client-only UI to allow using client-preferred settings. + */ +public class SelectItemUIFactory { + + private final String header; + private final ItemStack headerItem; + public static final int UNSELECTED = -1; + private static final int cols = 9; + private final Consumer selectedCallback; + // passed in stack + private final List stacks; + private final boolean noDeselect; + private int selected; + private boolean anotherWindow = false; + private AtomicBoolean dialogOpened; + private int guiTint = GT_Util.getRGBInt(Dyes.MACHINE_METAL.getRGBA()); + private final ItemStackHandler currentDisplayItemHandler = new ItemStackHandler(); + private Supplier currentGetter; + + private final GT_GUIColorOverride colorOverride = new GT_GUIColorOverride("SelectItemUIFactory"); + + private int getTextColorOrDefault(String textType, int defaultColor) { + return colorOverride.getTextColorOrDefault(textType, defaultColor); + } + + private final Supplier COLOR_TITLE = () -> getTextColorOrDefault("title", 0x222222); + private final Supplier COLOR_TEXT_GRAY = () -> getTextColorOrDefault("text_gray", 0x555555); + + public SelectItemUIFactory( + String header, ItemStack headerItem, Consumer selectedCallback, List stacks) { + this(header, headerItem, selectedCallback, stacks, UNSELECTED); + } + + public SelectItemUIFactory( + String header, + ItemStack headerItem, + Consumer selectedCallback, + List stacks, + int selected) { + this(header, headerItem, selectedCallback, stacks, selected, false); + } + + /** + * Constructor for a dialog to select an item from given list. Given callback may be called zero or more times depending on user action. + * @param header Header text + * @param headerItem ItemStack to use as Dialog icon + * @param selectedCallback callback upon selected + * @param stacks list to choose from + * @param selected preselected item. Use {@link #UNSELECTED} for unselected. Invalid selected will be clamped to 0 or highest index + * @param noDeselect true if player cannot deselect, false otherwise. If this is set to true, selectedCallback is guaranteed to be called with a nonnull stack + */ + public SelectItemUIFactory( + String header, + ItemStack headerItem, + Consumer selectedCallback, + List stacks, + int selected, + boolean noDeselect) { + this.header = header; + this.headerItem = headerItem; + this.selectedCallback = selectedCallback; + this.stacks = stacks; + this.noDeselect = noDeselect; + this.selected = noDeselect ? Math.max(0, selected) : selected; + this.currentDisplayItemHandler.setStackInSlot(0, getCandidate(selected)); + } + + /** + * @param anotherWindow If UI is shown on top of another window + * @param dialogOpened Flag to store whether this UI is opened and hence it should block duplicated creation of this UI + */ + public SelectItemUIFactory setAnotherWindow(boolean anotherWindow, AtomicBoolean dialogOpened) { + this.anotherWindow = anotherWindow; + this.dialogOpened = dialogOpened; + return this; + } + + public SelectItemUIFactory setGuiTint(int guiTint) { + this.guiTint = guiTint; + return this; + } + + /** + * @param currentGetter Getter for "current" item displayed that may change from external reasons + */ + public SelectItemUIFactory setCurrentGetter(Supplier currentGetter) { + this.currentGetter = currentGetter; + return this; + } + + public ModularWindow createWindow(UIBuildContext buildContext) { + ModularWindow.Builder builder = + ModularWindow.builder(getGUIWidth(), 53 + 18 * ((stacks.size() - 1) / cols + 1)); + builder.setBackground(ModularUITextures.VANILLA_BACKGROUND); + builder.setGuiTint(guiTint); + + if (headerItem != null) { + builder.widget(new ItemDrawable(headerItem).asWidget().setPos(5, 5).setSize(16, 16)); + } + builder.widget(new TextWidget(header).setDefaultColor(COLOR_TITLE.get()).setPos(25, 9)); + + builder.widget( + new SlotWidget(BaseSlot.phantom(currentDisplayItemHandler, 0)) { + @Override + public void draw(float partialTicks) { + if (currentGetter != null) { + ItemStack current = currentGetter.get(); + currentDisplayItemHandler.setStackInSlot(0, current); + selected = GT_Utility.findMatchingStackInList(stacks, current); + } + super.draw(partialTicks); + } + }.disableInteraction() + .setBackground(GT_UITextures.SLOT_DARK_GRAY) + .setPos( + 9 + + getFontRenderer() + .getStringWidth(StatCollector.translateToLocal( + "GT5U.gui.select.current")), + 24)) + .widget(new TextWidget(StatCollector.translateToLocal("GT5U.gui.select.current")) + .setDefaultColor(COLOR_TEXT_GRAY.get()) + .setPos(8, 25 + (18 - getFontRenderer().FONT_HEIGHT) / 2)); + + for (int i = 0; i < stacks.size(); i++) { + final int index = i; + builder.widget( + new SlotWidget(new BaseSlot(new ItemStackHandler(new ItemStack[] {stacks.get(index)}), 0, true)) { + @Override + public ClickResult onClick(int buttonId, boolean doubleClick) { + if (buttonId == 0) { + setSelected(index); + } else if (buttonId == 1) { + setSelected(UNSELECTED); + } else { + return ClickResult.ACCEPT; + } + selectedCallback.accept(getCandidate(getSelected())); + return ClickResult.SUCCESS; + } + + @Override + public IDrawable[] getBackground() { + return new IDrawable[] { + index == selected ? GT_UITextures.SLOT_DARK_GRAY : ModularUITextures.ITEM_SLOT + }; + } + }.disableInteraction().setPos(7 + 18 * (index % cols), 43 + 18 * (index / cols))); + } + + if (anotherWindow) { + dialogOpened.set(true); + builder.widget( + new ButtonWidget() { + @Override + public void onDestroy() { + dialogOpened.set(false); + } + }.setOnClick((clickData, widget) -> widget.getWindow().tryClose()) + .setBackground(ModularUITextures.VANILLA_BACKGROUND, new Text("x")) + .setPos(getGUIWidth() - 15, 3) + .setSize(12, 12)); + } + + return builder.build(); + } + + public int getSelected() { + return selected; + } + + public void setSelected(int selected) { + if (selected == this.selected) return; + int newSelected = GT_Utility.clamp(selected, UNSELECTED, stacks.size() - 1); + if (noDeselect && newSelected == UNSELECTED) return; + + this.selected = newSelected; + currentDisplayItemHandler.setStackInSlot(0, getCandidate(this.selected)); + } + + private ItemStack getCandidate(int listIndex) { + return listIndex < 0 || listIndex >= stacks.size() ? null : stacks.get(listIndex); + } + + private FontRenderer getFontRenderer() { + return Minecraft.getMinecraft().fontRenderer; + } + + private int getGUIWidth() { + return 176; + } +} 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 (!aCanDrai