package ggfab.mte; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; import net.minecraft.entity.item.EntityItem; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ChatComponentTranslation; import net.minecraft.util.StatCollector; import net.minecraft.world.WorldSavedData; import net.minecraftforge.common.util.Constants; import net.minecraftforge.common.util.ForgeDirection; import com.gtnewhorizons.modularui.api.drawable.Text; import com.gtnewhorizons.modularui.api.forge.ItemStackHandler; import com.gtnewhorizons.modularui.api.math.Alignment; import com.gtnewhorizons.modularui.api.math.Color; 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.CycleButtonWidget; import com.gtnewhorizons.modularui.common.widget.SlotGroup; import com.gtnewhorizons.modularui.common.widget.TextWidget; import com.gtnewhorizons.modularui.common.widget.textfield.TextFieldWidget; import ggfab.GGConstants; import gregtech.api.enums.ItemList; import gregtech.api.gui.modularui.GTUITextures; import gregtech.api.interfaces.IDataCopyable; import gregtech.api.interfaces.ITexture; import gregtech.api.interfaces.tileentity.IGregTechTileEntity; import gregtech.api.metatileentity.MetaTileEntity; import gregtech.api.metatileentity.implementations.MTEHatchInputBus; import gregtech.api.metatileentity.implementations.MTEMultiBlockBase; import gregtech.api.recipe.check.CheckRecipeResult; import gregtech.api.recipe.check.CheckRecipeResultRegistry; import gregtech.api.util.GTOreDictUnificator; import gregtech.api.util.GTUtility; import gregtech.common.tileentities.machines.IRecipeProcessingAwareHatch; public class MTELinkedInputBus extends MTEHatchInputBus implements IRecipeProcessingAwareHatch, IDataCopyable { public static final int SIZE_INVENTORY = 18; public static final String COPIED_DATA_IDENTIFIER = "linkedinputbus"; private SharedInventory mRealInventory; private final ItemStackHandlerProxy handler = new ItemStackHandlerProxy(); private String mChannel; private boolean mPrivate; private State mState; private WorldSave save; public MTELinkedInputBus(int id, String name, String nameRegional, int tier) { super( id, name, nameRegional, tier, 1, new String[] { SIZE_INVENTORY + " slot input bus linked together wirelessly", "Link does not cross world boundary", "Left/right click with data stick to copy/paste configuration", GGConstants.GGMARK_TOOLTIP, }); } public MTELinkedInputBus(String aName, int aTier, String[] aDescription, ITexture[][][] aTextures) { super(aName, aTier, 1, aDescription, aTextures); } @Override public MetaTileEntity newMetaEntity(IGregTechTileEntity aTileEntity) { return new MTELinkedInputBus(mName, mTier, mDescriptionArray, mTextures); } @Override public int getCircuitSlot() { return 0; } @Override public void addUIWidgets(ModularWindow.Builder builder, UIBuildContext buildContext) { builder.widget( new TextFieldWidget().setSynced(true, true) .setGetter(() -> mChannel == null ? "" : mChannel) .setSetter(this::setChannel) .setTextColor(Color.WHITE.dark(1)) .setTextAlignment(Alignment.CenterLeft) .setBackground(GTUITextures.BACKGROUND_TEXT_FIELD) .setGTTooltip(() -> mTooltipCache.getData("ggfab.tooltip.linked_input_bus.change_freq_warn")) .setSize(60, 18) .setPos(48, 3)) .widget( new CycleButtonWidget().setToggle(this::isPrivate, this::setPrivate) .setTextureGetter( i -> i == 1 ? GTUITextures.OVERLAY_BUTTON_CHECKMARK : GTUITextures.OVERLAY_BUTTON_CROSS) .setVariableBackground(GTUITextures.BUTTON_STANDARD_TOGGLE) .setSynced(true, true) .setGTTooltip(() -> mTooltipCache.getData("ggfab.tooltip.linked_input_bus.private")) .setSize(18, 18) .setPos(150, 3)) .widget( SlotGroup.ofItemHandler(handler, 9) .startFromSlot(0) .endAtSlot(SIZE_INVENTORY - 1) .background(getGUITextureSet().getItemSlot()) .slotCreator(i -> new BaseSlot(handler, i, false) { @Override public ItemStack getStack() { return isEnabled() ? super.getStack() : null; } @Override public boolean isEnabled() { return mChannel != null; } }) .build() .setPos(7, 24)) .widget( new TextWidget(new Text("Private")).setPos(110, 3) .setSize(43, 20)) .widget( new TextWidget(new Text("Channel")).setPos(5, 3) .setSize(43, 20)); } @Override public int getCircuitSlotX() { return 152; } @Override public ItemStack getStackInSlot(int aIndex) { if (aIndex == getCircuitSlot()) return super.getStackInSlot(aIndex); if (mState != State.Blocked && mChannel != null && mRealInventory != null) { if (aIndex > 0 && aIndex <= SIZE_INVENTORY) return mRealInventory.stacks[aIndex - 1]; } return null; } @Override public void setInventorySlotContents(int aIndex, ItemStack aStack) { if (aIndex == getCircuitSlot()) { mInventory[0] = GTUtility.copyAmount(0, aStack); markDirty(); } else if (mState != State.Blocked && mChannel != null && mRealInventory != null) { if (aIndex > 0 && aIndex <= SIZE_INVENTORY) { mRealInventory.stacks[aIndex - 1] = aStack; getWorldSave().markDirty(); } } } @Override public ITexture[] getTexturesActive(ITexture aBaseTexture) { return super.getTexturesActive(aBaseTexture); } @Override public ITexture[] getTexturesInactive(ITexture aBaseTexture) { return super.getTexturesInactive(aBaseTexture); } @Override public boolean canInsertItem(int aIndex, ItemStack aStack, int ordinalSide) { return isValidSlot(aIndex) && aStack != null && mChannel != null && mRealInventory != null && aIndex > getCircuitSlot() && aIndex < SIZE_INVENTORY + 1 && (mRealInventory.stacks[aIndex - 1] == null || GTUtility.areStacksEqual(aStack, mRealInventory.stacks[aIndex - 1])) && allowPutStack(getBaseMetaTileEntity(), aIndex, ForgeDirection.getOrientation(ordinalSide), aStack); } @Override public boolean allowPutStack(IGregTechTileEntity aBaseMetaTileEntity, int aIndex, ForgeDirection side, ItemStack aStack) { return side == getBaseMetaTileEntity().getFrontFacing() && aIndex != getCircuitSlot() && (mRecipeMap == null || disableFilter || mRecipeMap.containsInput(aStack)) && (mRealInventory.disableLimited || limitedAllowPutStack(aIndex, aStack)); } @Override protected boolean limitedAllowPutStack(int aIndex, ItemStack aStack) { for (int i = 0; i < SIZE_INVENTORY; i++) if (GTUtility.areStacksEqual(GTOreDictUnificator.get_nocopy(aStack), mRealInventory.stacks[i])) return i == aIndex - 1; return mRealInventory.stacks[aIndex - 1] == null; } @Override public boolean canExtractItem(int aIndex, ItemStack aStack, int aSide) { return false; } @Override public int getSizeInventory() { if (mState != State.Blocked && mChannel != null && mRealInventory != null) return SIZE_INVENTORY + 1; return 1; } @Override public void startRecipeProcessing() { if (mRealInventory == null) return; if (mRealInventory.used) { mState = State.Blocked; } else { mRealInventory.used = true; mState = State.Activated; } } @Override public CheckRecipeResult endRecipeProcessing(MTEMultiBlockBase controller) { if (mState == State.Activated) { assert mRealInventory != null; mRealInventory.used = false; } mState = State.Default; return CheckRecipeResultRegistry.SUCCESSFUL; } @Override public void updateSlots() { if (mChannel == null || mRealInventory == null) return; for (int i = 0; i < mRealInventory.stacks.length; i++) { if (mRealInventory.stacks[i] != null && (mRealInventory.stacks[i].getItem() == null || mRealInventory.stacks[i].stackSize <= 0)) mRealInventory.stacks[i] = null; } if (!mRealInventory.disableSort) fillStacksIntoFirstSlots(); markDirty(); getWorldSave().markDirty(); } @Override protected void fillStacksIntoFirstSlots() { // sanity check if (mRealInventory == null) return; final int L = SIZE_INVENTORY; HashMap slots = new HashMap<>(L); HashMap stacks = new HashMap<>(L); List order = new ArrayList<>(L); List validSlots = new ArrayList<>(L); for (int i = 0; i < L; i++) { validSlots.add(i); ItemStack s = mRealInventory.stacks[i]; if (s == null) continue; GTUtility.ItemId sID = GTUtility.ItemId.createNoCopy(s); slots.merge(sID, s.stackSize, Integer::sum); if (!stacks.containsKey(sID)) stacks.put(sID, s); order.add(sID); mRealInventory.stacks[i] = null; } int slotindex = 0; for (GTUtility.ItemId sID : order) { int toSet = slots.get(sID); if (toSet == 0) continue; int slot = validSlots.get(slotindex); slotindex++; mRealInventory.stacks[slot] = stacks.get(sID) .copy(); toSet = Math.min(toSet, mRealInventory.stacks[slot].getMaxStackSize()); mRealInventory.stacks[slot].stackSize = toSet; slots.merge(sID, toSet, (a, b) -> a - b); } } private void dropItems(ItemStack[] aStacks) { for (ItemStack stack : aStacks) { if (!GTUtility.isStackValid(stack)) continue; EntityItem ei = new EntityItem( getBaseMetaTileEntity().getWorld(), getBaseMetaTileEntity().getOffsetX(getBaseMetaTileEntity().getFrontFacing(), 1) + 0.5, getBaseMetaTileEntity().getOffsetY(getBaseMetaTileEntity().getFrontFacing(), 1) + 0.5, getBaseMetaTileEntity().getOffsetZ(getBaseMetaTileEntity().getFrontFacing(), 1) + 0.5, stack); ei.motionX = ei.motionY = ei.motionZ = 0; getBaseMetaTileEntity().getWorld() .spawnEntityInWorld(ei); } } @Override public boolean shouldDropItemAt(int index) { // NOTE by this time onBlockDestroyed has already been called, i.e. so ref has already been decremented. // so we really should check for ref <= 0 instead of ref <= 1 return mRealInventory != null && mRealInventory.ref <= 0; } @Override public void onBlockDestroyed() { super.onBlockDestroyed(); if (mRealInventory != null) { if (--mRealInventory.ref <= 0) getWorldSave().remove(mChannel); } } @Override public void saveNBTData(NBTTagCompound aNBT) { super.saveNBTData(aNBT); if (mChannel != null) aNBT.setString("channel", mChannel); aNBT.setBoolean("private", mPrivate); } @Override public void loadNBTData(NBTTagCompound aNBT) { super.loadNBTData(aNBT); String channel = aNBT.getString("channel"); if ("".equals(channel)) channel = null; this.mChannel = channel; mPrivate = aNBT.getBoolean("private"); } public String getChannel() { return mChannel; } @Override public void onFirstTick(IGregTechTileEntity aBaseMetaTileEntity) { super.onFirstTick(aBaseMetaTileEntity); if (mChannel != null) { mRealInventory = getWorldSave().get(getRealChannel()); handler.set(mRealInventory.stacks); } } @Override public void onScrewdriverRightClick(ForgeDirection side, EntityPlayer aPlayer, float aX, float aY, float aZ) { if (!getBaseMetaTileEntity().getCoverBehaviorAtSideNew(side) .isGUIClickable( side, getBaseMetaTileEntity().getCoverIDAtSide(side), getBaseMetaTileEntity().getComplexCoverDataAtSide(side), getBaseMetaTileEntity())) return; if (aPlayer.isSneaking()) { if (this.mRealInventory == null) { aPlayer.addChatMessage(new ChatComponentTranslation("ggfab.info.linked_input_bus.no_channel")); return; } if (mRealInventory.disableSort) { mRealInventory.disableSort = false; } else { if (mRealInventory.disableLimited) { mRealInventory.disableLimited = false; } else { mRealInventory.disableSort = true; mRealInventory.disableLimited = true; } } GTUtility.sendChatToPlayer( aPlayer, StatCollector.translateToLocal("GT5U.hatch.disableSort." + mRealInventory.disableSort) + " " + StatCollector.translateToLocal("GT5U.hatch.disableLimited." + mRealInventory.disableLimited)); } else { this.disableFilter = !this.disableFilter; GTUtility.sendChatToPlayer( aPlayer, StatCollector.translateToLocal("GT5U.hatch.disableFilter." + this.disableFilter)); } } @Override public NBTTagCompound getCopiedData(EntityPlayer player) { if (getChannel() == null) { return null; } NBTTagCompound tag = new NBTTagCompound(); tag.setString("type", COPIED_DATA_IDENTIFIER); tag.setString("channel", getChannel()); tag.setTag("circuit", GTUtility.saveItem(getStackInSlot(getCircuitSlot()))); if (isPrivate()) { tag.setLong( "owner1", getBaseMetaTileEntity().getOwnerUuid() .getMostSignificantBits()); tag.setLong( "owner2", getBaseMetaTileEntity().getOwnerUuid() .getLeastSignificantBits()); } return tag; } @Override public boolean pasteCopiedData(EntityPlayer player, NBTTagCompound nbt) { // backwards compat if (nbt == null || (!COPIED_DATA_IDENTIFIER.equals(nbt.getString("ggfab.type")) && !COPIED_DATA_IDENTIFIER.equals(nbt.getString("type")))) { return false; } ItemStack circuit = GTUtility.loadItem(nbt, "circuit"); String channel = nbt.getString("channel"); if (GTUtility.isStackInvalid(circuit)) circuit = null; if ("".equals(channel)) { return false; } else if (circuit != null && getConfigurationCircuits().stream() .noneMatch(circuit::isItemEqual)) { return false; } UUID owner = nbt.hasKey("owner1") ? new UUID(nbt.getLong("owner1"), nbt.getLong("owner2")) : null; if (owner != null && !owner.equals(getBaseMetaTileEntity().getOwnerUuid())) { return false; } setPrivate(owner != null); setChannel(channel); setInventorySlotContents(getCircuitSlot(), circuit); return true; } @Override public String getCopiedDataIdentifier(EntityPlayer player) { return COPIED_DATA_IDENTIFIER; } @Override public boolean onRightclick(IGregTechTileEntity aBaseMetaTileEntity, EntityPlayer aPlayer, ForgeDirection side, float aX, float aY, float aZ) { if (!(aPlayer instanceof EntityPlayerMP)) return super.onRightclick(aBaseMetaTileEntity, aPlayer, side, aX, aY, aZ); ItemStack stick = aPlayer.inventory.getCurrentItem(); if (!ItemList.Tool_DataStick.isStackEqual(stick, false, true)) return super.onRightclick(aBaseMetaTileEntity, aPlayer, side, aX, aY, aZ); if (!stick.hasTagCompound() || !COPIED_DATA_IDENTIFIER.equals(stick.stackTagCompound.getString("ggfab.type"))) { aPlayer.addChatMessage(new ChatComponentTranslation("ggfab.info.linked_input_bus.no_data")); return true; } ItemStack circuit = GTUtility.loadItem(stick.stackTagCompound, "circuit"); String channel = stick.stackTagCompound.getString("channel"); if (GTUtility.isStackInvalid(circuit)) circuit = null; if ("".equals(channel)) { aPlayer.addChatMessage(new ChatComponentTranslation("ggfab.info.linked_input_bus.no_data")); return true; } else if (circuit != null && getConfigurationCircuits().stream() .noneMatch(circuit::isItemEqual)) { aPlayer.addChatMessage(new ChatComponentTranslation("ggfab.info.linked_input_bus.invalid_circuit")); return true; } UUID owner = stick.stackTagCompound.hasKey("owner1") ? new UUID(stick.stackTagCompound.getLong("owner1"), stick.stackTagCompound.getLong("owner2")) : null; if (owner != null && !owner.equals(getBaseMetaTileEntity().getOwnerUuid())) { aPlayer.addChatMessage(new ChatComponentTranslation("ggfab.info.linked_input_bus.not_owned")); return true; } setPrivate(owner != null); setChannel(channel); setInventorySlotContents(getCircuitSlot(), circuit); aPlayer.addChatMessage(new ChatComponentTranslation("ggfab.info.linked_input_bus.data_pasted", channel)); return true; } @Override public void onLeftclick(IGregTechTileEntity aBaseMetaTileEntity, EntityPlayer aPlayer) { if (!(aPlayer instanceof EntityPlayerMP)) return; ItemStack stick = aPlayer.inventory.getCurrentItem(); if (!ItemList.Tool_DataStick.isStackEqual(stick, false, true)) return; if (getChannel() == null) { aPlayer.addChatMessage(new ChatComponentTranslation("ggfab.info.linked_input_bus.no_channel")); return; } aPlayer.addChatMessage(new ChatComponentTranslation("ggfab.info.linked_input_bus.data_copied", getChannel())); stick.stackTagCompound = getCopiedData(aPlayer); stick.setStackDisplayName("Linked Input Bus configuration"); // abuse the title mechanism here. I assure you it will be fine (tm). GTUtility.ItemNBT.setBookTitle(stick, "Channel: " + getChannel()); if (getBaseMetaTileEntity().getOwnerName() != null) GTUtility.ItemNBT.setBookAuthor(stick, getBaseMetaTileEntity().getOwnerName()); } private String getRealChannel() { if (mChannel == null) return null; if (mPrivate) return getBaseMetaTileEntity().getOwnerUuid() + mChannel; return new UUID(0, 0) + mChannel; } public boolean isPrivate() { return mPrivate; } public void setPrivate(boolean aPrivate) { if (aPrivate == mPrivate) return; if (getBaseMetaTileEntity().isClientSide()) { mPrivate = aPrivate; return; } if (this.mChannel == null) { mPrivate = aPrivate; return; } getWorldSave().markDirty(); if (--this.mRealInventory.ref <= 0) { // last referrer, drop inventory dropItems(mRealInventory.stacks); getWorldSave().remove(getRealChannel()); } mPrivate = aPrivate; mRealInventory = getWorldSave().get(getRealChannel()); this.handler.set(mRealInventory.stacks); mRealInventory.ref++; getWorldSave().markDirty(); } public void setChannel(String aChannel) { if ("".equals(aChannel)) aChannel = null; if (getBaseMetaTileEntity().isClientSide()) { mChannel = aChannel; return; } if (Objects.equals(this.mChannel, aChannel)) return; // noop if (this.mChannel != null) { if (--this.mRealInventory.ref <= 0) { // last referrer, drop inventory dropItems(mRealInventory.stacks); getWorldSave().remove(getRealChannel()); } } if (aChannel == null) { this.mChannel = null; this.mRealInventory = null; this.handler.setFake(); } else { this.mChannel = aChannel; this.mRealInventory = getWorldSave().get(getRealChannel()); this.handler.set(mRealInventory.stacks); mRealInventory.ref++; } getWorldSave().markDirty(); } private WorldSave getWorldSave() { if (save == null) { WorldSave save = (WorldSave) getBaseMetaTileEntity().getWorld() .loadItemData(WorldSave.class, "LinkedInputBusses"); if (save == null) { save = new WorldSave("LinkedInputBusses"); getBaseMetaTileEntity().getWorld() .setItemData(save.mapName, save); } this.save = save; } return save; } private enum State { Activated, Blocked, Default, } private static class SharedInventory { private final ItemStack[] stacks; /** * Inventory wrapper for ModularUI */ private final ItemStackHandler inventoryHandler; public boolean disableLimited = true; public boolean disableSort; private boolean used; private int ref; public SharedInventory() { this.stacks = new ItemStack[SIZE_INVENTORY]; inventoryHandler = new ItemStackHandler(stacks); } public SharedInventory(NBTTagCompound tag) { this.stacks = new ItemStack[SIZE_INVENTORY]; inventoryHandler = new ItemStackHandler(stacks); for (int i = 0; i < SIZE_INVENTORY; i++) { String key = "" + i; if (!tag.hasKey(key, Constants.NBT.TAG_COMPOUND)) continue; stacks[i] = ItemStack.loadItemStackFromNBT(tag.getCompoundTag(key)); } ref = tag.getInteger("ref"); disableLimited = tag.getBoolean("dl"); disableSort = tag.getBoolean("ds"); } public NBTTagCompound save() { NBTTagCompound tag = new NBTTagCompound(); for (int i = 0; i < SIZE_INVENTORY; i++) { ItemStack stack = stacks[i]; if (stack == null) continue; tag.setTag("" + i, stack.writeToNBT(new NBTTagCompound())); } tag.setInteger("ref", ref); tag.setBoolean("ds", disableSort); tag.setBoolean("dl", disableLimited); return tag; } } public static class WorldSave extends WorldSavedData { private final Map data = new HashMap<>(); public WorldSave(String p_i2141_1_) { super(p_i2141_1_); } @Override public void readFromNBT(NBTTagCompound tag) { data.clear(); @SuppressWarnings("unchecked") Set> set = tag.tagMap.entrySet(); for (Map.Entry e : set) { data.put(e.getKey(), new SharedInventory((NBTTagCompound) e.getValue())); } } @Override public void writeToNBT(NBTTagCompound tag) { for (Map.Entry e : data.entrySet()) { if (e.getValue().ref > 0) tag.setTag( e.getKey(), e.getValue() .save()); } } public SharedInventory get(Object channel) { return data.computeIfAbsent(channel.toString(), k -> new SharedInventory()); } public void remove(Object channel) { data.remove(channel.toString()); markDirty(); } } private static class ItemStackHandlerProxy extends ItemStackHandler { private static final ItemStack[] EMPTY = new ItemStack[SIZE_INVENTORY]; private boolean fake; public ItemStackHandlerProxy() { super(EMPTY); fake = true; } public void setFake() { set(EMPTY); fake = true; } public boolean isFake() { return fake; } public void set(ItemStack[] stacks) { this.stacks = Arrays.asList(stacks); fake = false; } @Override public NBTTagCompound serializeNBT() { NBTTagCompound tag = super.serializeNBT(); tag.setBoolean("fake", fake); return tag; } @Override public void deserializeNBT(NBTTagCompound nbt) { super.deserializeNBT(nbt); fake = nbt.getBoolean("fake"); } } }