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 --- .../modularui/widget/FluidDisplaySlotWidget.java | 474 +++++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100644 src/main/java/gregtech/common/gui/modularui/widget/FluidDisplaySlotWidget.java (limited to 'src/main/java/gregtech/common/gui/modularui/widget/FluidDisplaySlotWidget.java') diff --git a/src/main/java/gregtech/common/gui/modularui/widget/FluidDisplaySlotWidget.java b/src/main/java/gregtech/common/gui/modularui/widget/FluidDisplaySlotWidget.java new file mode 100644 index 0000000000..99200e5e8d --- /dev/null +++ b/src/main/java/gregtech/common/gui/modularui/widget/FluidDisplaySlotWidget.java @@ -0,0 +1,474 @@ +package gregtech.common.gui.modularui.widget; + +import com.gtnewhorizons.modularui.api.forge.IItemHandlerModifiable; +import com.gtnewhorizons.modularui.common.internal.wrapper.BaseSlot; +import com.gtnewhorizons.modularui.common.widget.SlotWidget; +import gregtech.GT_Mod; +import gregtech.api.interfaces.IFluidAccess; +import gregtech.api.interfaces.IHasFluidDisplayItem; +import gregtech.api.interfaces.metatileentity.IFluidLockable; +import gregtech.api.util.GT_Utility; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Supplier; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemStack; +import net.minecraft.network.PacketBuffer; +import net.minecraftforge.fluids.Fluid; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.IFluidContainerItem; + +public class FluidDisplaySlotWidget extends SlotWidget { + + private IHasFluidDisplayItem iHasFluidDisplay; + private Supplier fluidAccessConstructor; + private Supplier canDrainGetter; + private Supplier canFillGetter; + private Action actionRealClick = Action.NONE; + private Action actionDragAndDrop = Action.NONE; + private BiFunction beforeRealClick; + private BiFunction beforeDragAndDrop; + private Runnable updateFluidDisplayItem = () -> { + if (iHasFluidDisplay != null) { + iHasFluidDisplay.updateFluidDisplayItem(); + } + }; + + public FluidDisplaySlotWidget(BaseSlot slot) { + super(slot); + setAccess(false, false); + disableShiftInsert(); + } + + public FluidDisplaySlotWidget(IItemHandlerModifiable handler, int index) { + this(new BaseSlot(handler, index, true)); + } + + // === client actions === + + @Override + public ClickResult onClick(int buttonId, boolean doubleClick) { + if (actionRealClick == Action.NONE) return ClickResult.REJECT; + if (interactionDisabled) return ClickResult.REJECT; + + /* + * While a logical client don't really need to process fluid cells upon click (it could have just wait + * for server side to send the result), doing so would result in every fluid interaction having a + * noticeable delay between clicking and changes happening even on single player. + * I'd imagine this lag to become only more severe when playing MP over ethernet, which would have much more latency + * than a memory connection + */ + ClickData clickData = ClickData.create(buttonId, doubleClick); + ItemStack verifyToken = executeRealClick(clickData); + syncToServer(2, buffer -> { + clickData.writeToPacket(buffer); + try { + buffer.writeItemStackToBuffer(verifyToken); + } catch (IOException e) { + e.printStackTrace(); + } + }); + return ClickResult.ACCEPT; + } + + @Override + public boolean handleDragAndDrop(ItemStack draggedStack, int button) { + if (actionDragAndDrop == Action.NONE || actionDragAndDrop == Action.TRANSFER) return false; + if (interactionDisabled) return false; + + ClickData clickData = ClickData.create(button, false); + executeDragAndDrop(clickData, draggedStack); + syncToServer(5, buffer -> { + try { + clickData.writeToPacket(buffer); + buffer.writeItemStackToBuffer(draggedStack); + } catch (IOException e) { + e.printStackTrace(); + } + }); + draggedStack.stackSize = 0; + return true; + } + + @Override + public List getExtraTooltip() { + return Collections.emptyList(); + } + + // === server actions === + + @Override + public void readOnServer(int id, PacketBuffer buf) throws IOException { + if (id == 1) { + getMcSlot().xDisplayPosition = buf.readVarIntFromBuffer(); + getMcSlot().yDisplayPosition = buf.readVarIntFromBuffer(); + } else if (id == 2) { + onClickServer(ClickData.readPacket(buf), buf.readItemStackFromBuffer()); + } else if (id == 3) { + phantomScroll(buf.readVarIntFromBuffer()); + } else if (id == 4) { + setEnabled(buf.readBoolean()); + } else if (id == 5) { + executeDragAndDrop(ClickData.readPacket(buf), buf.readItemStackFromBuffer()); + if (onDragAndDropComplete != null) { + onDragAndDropComplete.accept(this); + } + } + markForUpdate(); + } + + private void onClickServer(ClickData clickData, ItemStack clientVerifyToken) { + ItemStack serverVerifyToken = executeRealClick(clickData); + // similar to what NetHandlerPlayServer#processClickWindow does + if (!ItemStack.areItemStacksEqual(clientVerifyToken, serverVerifyToken)) { + ((EntityPlayerMP) getContext().getPlayer()) + .sendContainerToPlayer(getContext().getContainer()); + } + } + + // === client/server actions === + + private ItemStack executeRealClick(ClickData clickData) { + if (actionRealClick == Action.NONE) return null; + if (beforeRealClick != null && !beforeRealClick.apply(clickData, this)) return null; + + ItemStack ret = null; + if (actionRealClick == Action.TRANSFER) { + if (fluidAccessConstructor == null) { + GT_Mod.GT_FML_LOGGER.warn( + "FluidDisplaySlotWidget is asked to transfer fluid, but fluidAccessConstructor is null!"); + return null; + } + ret = transferFluid( + fluidAccessConstructor.get(), + getContext().getPlayer(), + clickData.mouseButton == 0, + canDrainGetter != null ? canDrainGetter.get() : true, + canFillGetter != null ? canFillGetter.get() : true); + } else if (actionRealClick == Action.LOCK) { + lockFluid(getContext().getPlayer().inventory.getItemStack()); + } + + updateFluidDisplayItem.run(); + return ret; + } + + protected ItemStack transferFluid( + IFluidAccess aFluidAccess, + EntityPlayer aPlayer, + boolean aProcessFullStack, + boolean aCanDrain, + boolean aCanFill) { + ItemStack tStackHeld = aPlayer.inventory.getItemStack(); + ItemStack tStackSizedOne = GT_Utility.copyAmount(1, tStackHeld); + if (tStackSizedOne == null || tStackHeld.stackSize == 0) return null; + FluidStack tInputFluid = aFluidAccess.get(); + FluidStack tFluidHeld = GT_Utility.getFluidForFilledItem(tStackSizedOne, true); + if (tFluidHeld != null && tFluidHeld.amount <= 0) tFluidHeld = null; + if (tInputFluid == null) { + // tank empty, consider fill only from now on + if (!aCanFill) + // cannot fill and nothing to take, bail out + return null; + if (tFluidHeld == null) + // no fluid to fill + return null; + return fillFluid(aFluidAccess, aPlayer, tFluidHeld, aProcessFullStack); + } + // tank not empty, both action possible + if (tFluidHeld != null && tInputFluid.amount < aFluidAccess.getCapacity()) { + // both nonnull and have space left for filling. + if (aCanFill) + // actually both pickup and fill is reasonable, but I'll go with fill here + return fillFluid(aFluidAccess, aPlayer, tFluidHeld, aProcessFullStack); + if (!aCanDrain) + // cannot take AND cannot fill, why make this call then? + return null; + // the slot does not allow filling, so try take some + return drainFluid(aFluidAccess, aPlayer, aProcessFullStack); + } else { + // cannot fill and there is something to take + if (!aCanDrain) + // but the slot does not allow taking, so bail out + return null; + return drainFluid(aFluidAccess, aPlayer, aProcessFullStack); + } + } + + protected static ItemStack drainFluid(IFluidAccess aFluidAccess, EntityPlayer aPlayer, boolean aProcessFullStack) { + FluidStack tTankStack = aFluidAccess.get(); + if (tTankStack == null) return null; + ItemStack tStackHeld = aPlayer.inventory.getItemStack(); + ItemStack tStackSizedOne = GT_Utility.copyAmount(1, tStackHeld); + if (tStackSizedOne == null || tStackHeld.stackSize == 0) return null; + int tOriginalFluidAmount = tTankStack.amount; + ItemStack tFilledContainer = GT_Utility.fillFluidContainer(tTankStack, tStackSizedOne, true, false); + if (tFilledContainer == null && tStackSizedOne.getItem() instanceof IFluidContainerItem) { + IFluidContainerItem tContainerItem = (IFluidContainerItem) tStackSizedOne.getItem(); + int tFilledAmount = tContainerItem.fill(tStackSizedOne, tTankStack, true); + if (tFilledAmount > 0) { + tFilledContainer = tStackSizedOne; + tTankStack.amount -= tFilledAmount; + } + } + if (tFilledContainer != null) { + if (aProcessFullStack) { + int tFilledAmount = tOriginalFluidAmount - tTankStack.amount; + /* + work out how many more items we can fill + one cell is already used, so account for that + the round down behavior will left over a fraction of a cell worth of fluid + the user then get to decide what to do with it + it will not be too fancy if it spills out partially filled cells + */ + int tAdditionalParallel = Math.min(tStackHeld.stackSize - 1, tTankStack.amount / tFilledAmount); + tTankStack.amount -= tFilledAmount * tAdditionalParallel; + tFilledContainer.stackSize += tAdditionalParallel; + } + replaceCursorItemStack(aPlayer, tFilledContainer); + } + aFluidAccess.verifyFluidStack(); + return tFilledContainer; + } + + protected static ItemStack fillFluid( + IFluidAccess aFluidAccess, EntityPlayer aPlayer, FluidStack aFluidHeld, boolean aProcessFullStack) { + // we are not using aMachine.fill() here any more, so we need to check for fluid type here ourselves + if (aFluidAccess.get() != null && !aFluidAccess.get().isFluidEqual(aFluidHeld)) return null; + ItemStack tStackHeld = aPlayer.inventory.getItemStack(); + ItemStack tStackSizedOne = GT_Utility.copyAmount(1, tStackHeld); + if (tStackSizedOne == null) return null; + + int tFreeSpace = aFluidAccess.getCapacity() - (aFluidAccess.get() != null ? aFluidAccess.get().amount : 0); + if (tFreeSpace <= 0) + // no space left + return null; + + // find out how much fluid can be taken + // some cells cannot be partially filled + ItemStack tStackEmptied = null; + int tAmountTaken = 0; + if (tFreeSpace >= aFluidHeld.amount) { + // fully accepted - try take it from item now + // IFluidContainerItem is intentionally not checked here. it will be checked later + tStackEmptied = GT_Utility.getContainerForFilledItem(tStackSizedOne, false); + tAmountTaken = aFluidHeld.amount; + } + if (tStackEmptied == null && tStackSizedOne.getItem() instanceof IFluidContainerItem) { + // either partially accepted, or is IFluidContainerItem + IFluidContainerItem container = (IFluidContainerItem) tStackSizedOne.getItem(); + FluidStack tDrained = container.drain(tStackSizedOne, tFreeSpace, true); + if (tDrained != null && tDrained.amount > 0) { + // something is actually drained - change the cell and drop it to player + tStackEmptied = tStackSizedOne; + tAmountTaken = tDrained.amount; + } + } + if (tStackEmptied == null) + // somehow the cell refuse to give out that amount of fluid, no op then + return null; + + // find out how many fill can we do + // same round down behavior as above + // however here the fluid stack is not changed at all, so the exact code will slightly differ + int tParallel = aProcessFullStack ? Math.min(tFreeSpace / tAmountTaken, tStackHeld.stackSize) : 1; + if (aFluidAccess.get() == null) { + FluidStack tNewFillableStack = aFluidHeld.copy(); + tNewFillableStack.amount = tAmountTaken * tParallel; + aFluidAccess.set(tNewFillableStack); + } else { + aFluidAccess.addAmount(tAmountTaken * tParallel); + } + tStackEmptied.stackSize = tParallel; + replaceCursorItemStack(aPlayer, tStackEmptied); + return tStackEmptied; + } + + protected static void replaceCursorItemStack(EntityPlayer aPlayer, ItemStack tStackResult) { + int tStackResultMaxStackSize = tStackResult.getMaxStackSize(); + while (tStackResult.stackSize > tStackResultMaxStackSize) { + aPlayer.inventory.getItemStack().stackSize -= tStackResultMaxStackSize; + GT_Utility.addItemToPlayerInventory(aPlayer, tStackResult.splitStack(tStackResultMaxStackSize)); + } + if (aPlayer.inventory.getItemStack().stackSize == tStackResult.stackSize) { + // every cell is mutated. it could just stay on the cursor. + aPlayer.inventory.setItemStack(tStackResult); + } else { + // some cells not mutated. The mutated cells must go into the inventory + // or drop into the world if there isn't enough space. + ItemStack tStackHeld = aPlayer.inventory.getItemStack(); + tStackHeld.stackSize -= tStackResult.stackSize; + GT_Utility.addItemToPlayerInventory(aPlayer, tStackResult); + } + } + + protected void executeDragAndDrop(ClickData clickData, ItemStack draggedStack) { + if (actionDragAndDrop == Action.NONE || actionDragAndDrop == Action.TRANSFER) return; + if (beforeDragAndDrop != null && !beforeDragAndDrop.apply(clickData, this)) return; + + if (actionDragAndDrop == Action.LOCK) { + lockFluid(draggedStack); + } + updateFluidDisplayItem.run(); + } + + protected void lockFluid(ItemStack cursorStack) { + if (!(iHasFluidDisplay instanceof IFluidLockable)) return; + IFluidLockable mteToLock = (IFluidLockable) iHasFluidDisplay; + + if (cursorStack == null) { + if (!mteToLock.allowChangingLockedFluid(null)) return; + + mteToLock.lockFluid(false); + mteToLock.setLockedFluidName(null); + GT_Utility.sendChatToPlayer(getContext().getPlayer(), GT_Utility.trans("300.1", "Fluid Lock Cleared.")); + + if (!isClient()) { + mteToLock.onFluidLockPacketReceived(null); + } + } else { + FluidStack fluidStack = GT_Utility.getFluidFromContainerOrFluidDisplay(cursorStack); + if (fluidStack == null) return; + Fluid tFluid = fluidStack.getFluid(); + if (tFluid == null) return; + + if (!mteToLock.allowChangingLockedFluid(tFluid.getName())) return; + + mteToLock.lockFluid(true); + mteToLock.setLockedFluidName(tFluid.getName()); + GT_Utility.sendChatToPlayer( + getContext().getPlayer(), + String.format( + GT_Utility.trans("151.4", "Successfully locked Fluid to %s"), + new FluidStack(tFluid, 1).getLocalizedName())); + + if (!isClient()) { + mteToLock.onFluidLockPacketReceived(tFluid.getName()); + } + } + } + + protected void updateFluidDisplayItem() { + if (iHasFluidDisplay != null) { + iHasFluidDisplay.updateFluidDisplayItem(); + } + } + + // === setters === + + public FluidDisplaySlotWidget setFluidAccessConstructor(Supplier fluidAccessConstructor) { + this.fluidAccessConstructor = fluidAccessConstructor; + return this; + } + + public FluidDisplaySlotWidget setIHasFluidDisplay(IHasFluidDisplayItem iHasFluidDisplay) { + this.iHasFluidDisplay = iHasFluidDisplay; + return this; + } + + public FluidDisplaySlotWidget setCanDrainGetter(Supplier canDrainGetter) { + this.canDrainGetter = canDrainGetter; + return this; + } + + public FluidDisplaySlotWidget setCanDrain(boolean canDrain) { + return setCanDrainGetter(() -> canDrain); + } + + public FluidDisplaySlotWidget setCanFillGetter(Supplier canFillGetter) { + this.canFillGetter = canFillGetter; + return this; + } + + public FluidDisplaySlotWidget setCanFill(boolean canFill) { + return setCanFillGetter(() -> canFill); + } + + /** + * Sets action called on click while holding the real item. + */ + public FluidDisplaySlotWidget setActionRealClick(Action actionRealClick) { + this.actionRealClick = actionRealClick; + return this; + } + + /** + * Sets action called on drag-and-drop from NEI. + * You can't use {@link Action#TRANSFER} here. + */ + public FluidDisplaySlotWidget setActionDragAndDrop(Action actionDragAndDrop) { + this.actionDragAndDrop = actionDragAndDrop; + return this; + } + + /** + * Sets function called before {@link #executeRealClick}. + * @param beforeRealClick (click data, this widget) -> if allow click + */ + public FluidDisplaySlotWidget setBeforeRealClick( + BiFunction beforeRealClick) { + this.beforeRealClick = beforeRealClick; + return this; + } + + /** + * Sets function called before {@link #executeDragAndDrop}. + * @param beforeDragAndDrop (click data, this widget) -> if allow click + */ + public FluidDisplaySlotWidget setBeforeDragAndDrop( + BiFunction beforeDragAndDrop) { + this.beforeDragAndDrop = beforeDragAndDrop; + return this; + } + + /** + * Sets function called before both of {@link #executeRealClick} and {@link #executeDragAndDrop}. + * @param beforeClick (click data, this widget) -> if allow click + */ + public FluidDisplaySlotWidget setBeforeClick(BiFunction beforeClick) { + setBeforeRealClick(beforeClick); + setBeforeDragAndDrop(beforeClick); + return this; + } + + /** + * By default, this widget runs {@link IHasFluidDisplayItem#updateFluidDisplayItem} after click. + * You can specify custom update action with this method. + */ + public FluidDisplaySlotWidget setUpdateFluidDisplayItem(Runnable updateFluidDisplayItem) { + this.updateFluidDisplayItem = updateFluidDisplayItem; + return this; + } + + /** + * Action triggered on mouse click or NEI drag-and-drop. + */ + public enum Action { + + /** + * Fill/drain fluid into/from the tank. + * Uses fluid amount, so drag-and-drop cannot use this mode. + */ + TRANSFER, + + /** + * Lock fluid for {@link IFluidLockable}. + * Does not use fluid amount. + */ + LOCK, + + /** + * Set filter for the tank. (not implemented yet) + * Does not use fluid amount. + */ + FILTER, + + /** + * Does nothing. + */ + NONE + } +} -- cgit