GregTechCEu Modern

GregTechCEu Modern

6M Downloads

Dynamic Cover help

xKostmand opened this issue · 5 comments

commented

Cross-mod Integration

No response

Minecraft Version

1.20.1 Forge

Feature Description

hello, i would like some help with coding.

i am trying to build a mod that adds a dynamic cover for the gt machines

basically, the dynamic cover is a container that can store multiple other covers inside it and all of them work concurrently.

i am just building the mod for fun and for training, my inspiration was this mod for 1.12 Nomifactory: https://github.com/Zorbatron/ZBGT adding dual covers in the game and i thought, it could be extended

here is my problem:

I can put multiple covers inside the Dynamic Cover GUI but they dont work concurrently, only the first one i put inside works and the others remain idle. The core problem is that the GregTech CEu API for CoverBehavior does not seem to directly support the kind of concurrent ticking, NBT handling, and redstone interaction that would be required for the dynamic cover to function as intended. The ICoverable interface delegates some lifecycle methods, but not the crucial ones for continuous operation.

Any help is much loved, if there is a better place to ask this, please tell me. Also, i can share my code if it helps. I know this is a feature request but i thought there is no better place but to ask the GregTech devs

commented

check out the gregtech discord https://discord.gg/ZV5r5jcz,
we mostly talk in the #modern-dev-talks channel

commented

btw i tried to already do that but got stuck on trying to make the syncdata system work with it, if you want you can check out my code on the tar/multicover branch

commented

thanks for replying. i got stuck in the following. i could either get the inner cover gui to load or they would work concurrently. for some reason when i fixed one of them the other would break unfortunately. I just wanted dual covers on 1.20 and i thought maybe i could extend it but ive lost hope.

now i am trying to just simply implement the dual covers.

btw, if you have any easy way to create a multiblock let me know because i cant figure it out, tyvm!

commented

there isn't really an easy way to make multis (if you need examples though you can look at the code for any existing multiblock in GTCEuM 1.20.1 branch)
there's a multiblock rework planned for the GTCEu 8.0, though it's only partially completed
As for the covers, could you please share your code?

commented

Here is my cover logic, this is with concurrency working. i also have another version with gui working

any help is much appreciated

package com.jmoiron.ulvcovm.data.covers;

import com.gregtechceu.gtceu.api.capability.ICoverable;
import com.gregtechceu.gtceu.api.cover.CoverBehavior;
import com.gregtechceu.gtceu.api.cover.CoverDefinition;
import com.gregtechceu.gtceu.api.cover.IUICover;
import com.gregtechceu.gtceu.api.gui.GuiTextures;
import com.gregtechceu.gtceu.api.gui.widget.SlotWidget;
import com.gregtechceu.gtceu.api.machine.MachineCoverContainer;
import com.gregtechceu.gtceu.api.machine.MetaMachine;
import com.gregtechceu.gtceu.api.machine.TickableSubscription;
import com.gregtechceu.gtceu.api.registry.GTRegistries;
import com.gregtechceu.gtceu.api.transfer.item.CustomItemStackHandler;
import com.gregtechceu.gtceu.common.cover.ItemFilterCover;
import com.gregtechceu.gtceu.common.cover.PumpCover;
import com.gregtechceu.gtceu.common.cover.RobotArmCover;
import com.lowdragmc.lowdraglib.gui.texture.ColorBorderTexture;
import com.lowdragmc.lowdraglib.gui.texture.GuiTextureGroup;
import com.lowdragmc.lowdraglib.gui.widget.LabelWidget;
import com.lowdragmc.lowdraglib.gui.widget.Widget;
import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup;
import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced;
import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted;
import com.lowdragmc.lowdraglib.syncdata.annotation.RequireRerender;
import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder;
import com.lowdragmc.lowdraglib.utils.LocalizationUtils;
import net.minecraft.core.Direction;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.core.BlockPos;
import net.minecraftforge.registries.ForgeRegistries;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**

  • Dynamic Cover - Fixed Version with Proper Subscription Management
    */
    public class DynamicCoverBehavior extends CoverBehavior implements IUICover {

    public static final ManagedFieldHolder MANAGED_FIELD_HOLDER =
    new ManagedFieldHolder(DynamicCoverBehavior.class, CoverBehavior.MANAGED_FIELD_HOLDER);

    private static final int SLOT_COUNT = 9;

    @persisted
    @DescSynced
    public final CustomItemStackHandler coverSlots;

    @persisted
    @DescSynced
    @RequireRerender
    private int selectedIndex = -1;

    // Thread-safe collections for concurrent access
    private final Map<Integer, CoverBehavior> internalCovers = new ConcurrentHashMap<>();
    private final Map<Integer, TickableSubscription> tickSubscriptions = new ConcurrentHashMap<>();
    private final List slotWidgets = new ArrayList<>();
    private WidgetGroup innerCoverUIArea;

    // Lifecycle management
    private volatile boolean isInitialized = false;
    private volatile boolean isDisposed = false;

    // Prevent double ticking with a proper gate
    private final Map<Integer, Long> lastTickTime = new ConcurrentHashMap<>();
    private final Map<Integer, Boolean> isCurrentlyTicking = new ConcurrentHashMap<>();

    public DynamicCoverBehavior(@NotNull CoverDefinition definition,
    @NotNull ICoverable coverableView,
    @NotNull Direction attachedSide,
    int tier) {
    super(definition, coverableView, attachedSide);

     this.coverSlots = new CustomItemStackHandler(SLOT_COUNT) {
    
         @Override
         public int getSlotLimit(int slot) {
             return 1;
         }
    
         @Override
         public boolean isItemValid(int slot, @NotNull ItemStack stack) {
             return findDefinitionFor(stack) != null;
         }
    
         @Override
         public void onContentsChanged(int slot) {
             super.onContentsChanged(slot);
             if (isInitialized && !isDisposed) {
                 handleSlotChange(slot);
             }
         }
     };
    

    }

    public DynamicCoverBehavior(@NotNull CoverDefinition definition,
    @NotNull ICoverable coverableView,
    @NotNull Direction attachedSide) {
    this(definition, coverableView, attachedSide, 0);
    }

    @OverRide
    public @NotNull ManagedFieldHolder getFieldHolder() {
    return MANAGED_FIELD_HOLDER;
    }

    @OverRide
    public boolean canAttach() {
    return coverHolder instanceof MachineCoverContainer && super.canAttach();
    }

    /* =======================================
    FIXED: Proper Subscription Management
    ======================================= */

    private void subscribeCoverTicking(int slot, CoverBehavior cover) {
    if (isDisposed) return;

     // Prevent double subscription with proper synchronization
     synchronized (this) {
         if (tickSubscriptions.containsKey(slot)) {
             System.out.println("DEBUG: Slot " + slot + " already has subscription, skipping");
             return;
         }
    
         System.out.println("DEBUG: Subscribing cover in slot " + slot + " of type " + cover.getClass().getSimpleName());
    
         MetaMachine machine = getMachine();
         if (machine == null) {
             System.out.println("DEBUG: No machine found for subscription");
             return;
         }
    
         try {
             // Create subscription with proper synchronization
             TickableSubscription subscription = machine.subscribeServerTick(() -> {
                 if (isDisposed || !internalCovers.containsKey(slot)) return;
    
                 // Prevent concurrent ticking of the same cover
                 if (isCurrentlyTicking.putIfAbsent(slot, true) != null) {
                     return; // Already ticking
                 }
    
                 try {
                     long currentTime = System.currentTimeMillis();
                     Long lastTick = lastTickTime.get(slot);
    
                     // Ensure minimum 50ms between ticks (20 TPS max)
                     if (lastTick != null && (currentTime - lastTick) < 50) {
                         return;
                     }
    
                     tickInternalCover(cover);
                     lastTickTime.put(slot, currentTime);
    
                 } catch (Throwable t) {
                     System.err.println("Error ticking internal cover in slot " + slot + ": " + t.getMessage());
                 } finally {
                     isCurrentlyTicking.remove(slot);
                 }
             });
    
             if (subscription != null) {
                 tickSubscriptions.put(slot, subscription);
                 System.out.println("DEBUG: Successfully subscribed cover in slot " + slot);
             } else {
                 System.out.println("DEBUG: Failed to create subscription for slot " + slot);
             }
    
         } catch (Exception e) {
             System.err.println("DEBUG: Exception during subscription for slot " + slot + ": " + e.getMessage());
             e.printStackTrace();
         }
     }
    

    }

    private void unsubscribeCoverTicking(int slot) {
    synchronized (this) {
    TickableSubscription subscription = tickSubscriptions.remove(slot);
    if (subscription != null) {
    MetaMachine machine = getMachine();
    if (machine != null) {
    try {
    machine.unsubscribe(subscription);
    System.out.println("DEBUG: Unsubscribed cover in slot " + slot);
    } catch (Exception e) {
    System.err.println("DEBUG: Error unsubscribing slot " + slot + ": " + e.getMessage());
    }
    }
    }

         // Clean up timing data
         lastTickTime.remove(slot);
         isCurrentlyTicking.remove(slot);
     }
    

    }

    @nullable
    private MetaMachine getMachine() {
    if (coverHolder instanceof MachineCoverContainer machineContainer) {
    return machineContainer.getMachine();
    }
    return null;
    }

    private void tickInternalCover(CoverBehavior cover) {
    try {
    // Use reflection to find and call the appropriate update method
    Method updateMethod = findUpdateMethod(cover.getClass());
    if (updateMethod != null) {
    updateMethod.setAccessible(true);
    updateMethod.invoke(cover);
    }
    } catch (Exception e) {
    // Silent - this is normal for many tick cycles where covers don't need to do anything
    }
    }

    private Method findUpdateMethod(Class<?> clazz) {
    // Try common update method names
    String[] methodNames = {"update", "serverTick", "tick", "onUpdate"};

     for (String methodName : methodNames) {
         try {
             Method method = clazz.getDeclaredMethod(methodName);
             if (method.getParameterCount() == 0) {
                 return method;
             }
         } catch (NoSuchMethodException ignored) {
         }
    
         try {
             Method method = clazz.getMethod(methodName);
             if (method.getParameterCount() == 0) {
                 return method;
             }
         } catch (NoSuchMethodException ignored) {
         }
     }
    
     // Try parent class
     Class<?> superClass = clazz.getSuperclass();
     if (superClass != null && superClass != Object.class && superClass != CoverBehavior.class) {
         return findUpdateMethod(superClass);
     }
    
     return null;
    

    }

    /* ============================
    Lifecycle Management
    ============================ */

    @OverRide
    public void onLoad() {
    super.onLoad();
    isDisposed = false;
    isInitialized = false;

     // Clear any existing state
     synchronized (this) {
         internalCovers.clear();
         for (int slot : new ArrayList<>(tickSubscriptions.keySet())) {
             unsubscribeCoverTicking(slot);
         }
     }
    
     rebuildInternalCoversFromSlots();
     validateSelection();
     isInitialized = true;
    
     System.out.println("DEBUG: Dynamic cover loaded with " + internalCovers.size() + " internal covers");
    

    }

    @OverRide
    public void onUnload() {
    super.onUnload();
    isInitialized = false;
    isDisposed = true;

     synchronized (this) {
         // Unsubscribe all ticking
         for (int slot : new ArrayList<>(tickSubscriptions.keySet())) {
             unsubscribeCoverTicking(slot);
         }
    
         // Unload covers
         for (CoverBehavior cover : internalCovers.values()) {
             try {
                 cover.onUnload();
             } catch (Throwable t) {
                 System.err.println("Error unloading internal cover: " + t.getMessage());
             }
         }
    
         // Clear state
         internalCovers.clear();
     }
    

    }

    @OverRide
    public void onNeighborChanged(Block block, BlockPos fromPos, boolean isMoving) {
    super.onNeighborChanged(block, fromPos, isMoving);
    for (CoverBehavior cover : internalCovers.values()) {
    try {
    cover.onNeighborChanged(block, fromPos, isMoving);
    } catch (Throwable ignored) {}
    }
    }

    @OverRide
    public void onRemoved() {
    super.onRemoved();
    isInitialized = false;
    isDisposed = true;

     synchronized (this) {
         // Clean up tick subscriptions first
         for (int slot : new ArrayList<>(tickSubscriptions.keySet())) {
             unsubscribeCoverTicking(slot);
         }
    
         // Clean up child covers
         for (CoverBehavior cover : internalCovers.values()) {
             try {
                 cover.onRemoved();
             } catch (Throwable t) {
                 System.err.println("Error removing internal cover: " + t.getMessage());
             }
         }
    
         internalCovers.clear();
     }
    

    }

    @OverRide
    @NotNull
    public List getAdditionalDrops() {
    var list = super.getAdditionalDrops();
    for (int i = 0; i < SLOT_COUNT; i++) {
    ItemStack stack = coverSlots.getStackInSlot(i);
    if (!stack.isEmpty()) list.add(stack.copy());
    }
    return list;
    }

    /* =========================
    Slot & Behavior Management
    ========================= */

    private void handleSlotChange(int slot) {
    if (!isInitialized || isDisposed) return;

     ItemStack stack = coverSlots.getStackInSlot(slot);
    
     if (stack.isEmpty()) {
         removeInternalCover(slot);
     } else {
         createInternalCover(slot, stack);
     }
    
     validateSelection();
     updateInnerCoverInfo(); // Changed from updateInnerCoverUI
    
     // Proper synchronization
     coverHolder.markDirty();
     coverHolder.notifyBlockUpdate();
     coverHolder.scheduleRenderUpdate();
    

    }

    private void rebuildInternalCoversFromSlots() {
    System.out.println("DEBUG: Rebuilding internal covers from slots");

     for (int i = 0; i < SLOT_COUNT; i++) {
         ItemStack stack = coverSlots.getStackInSlot(i);
         if (!stack.isEmpty()) {
             createInternalCover(i, stack);
         }
     }
    
     System.out.println("DEBUG: Rebuilt " + internalCovers.size() + " internal covers");
    

    }

    private void createInternalCover(int slot, ItemStack stack) {
    if (isDisposed) return;

     // Remove any existing cover first
     removeInternalCover(slot);
    
     CoverDefinition def = findDefinitionFor(stack);
     if (def == null) {
         System.out.println("DEBUG: No cover definition found for " + stack.getItem());
         return;
     }
    
     try {
         System.out.println("DEBUG: Creating internal cover for slot " + slot + " with definition " + def.getId());
    
         // Create the behavior
         CoverBehavior behavior = def.createCoverBehavior(coverHolder, attachedSide);
    
         // Properly initialize the cover
         behavior.onLoad();
    
         // Store the cover
         internalCovers.put(slot, behavior);
    
         // Subscribe to ticking
         subscribeCoverTicking(slot, behavior);
    
         // Update selection if needed
         if (selectedIndex == -1) {
             selectedIndex = slot;
             // Force sync of selection
             coverHolder.markDirty();
         }
    
         System.out.println("DEBUG: Successfully created internal cover of type " + behavior.getClass().getSimpleName() + " in slot " + slot);
    
     } catch (Throwable t) {
         System.err.println("Error creating internal cover for slot " + slot + ": " + t.getMessage());
         t.printStackTrace();
    
         // Clean up on failure
         internalCovers.remove(slot);
         unsubscribeCoverTicking(slot);
     }
    

    }

    private void removeInternalCover(int slot) {
    if (isDisposed) return;

     // Unsubscribe first
     unsubscribeCoverTicking(slot);
    
     CoverBehavior existing = internalCovers.remove(slot);
     if (existing != null) {
         try {
             existing.onRemoved();
             System.out.println("DEBUG: Removed internal cover from slot " + slot);
         } catch (Throwable t) {
             System.err.println("Error removing internal cover from slot " + slot + ": " + t.getMessage());
         }
     }
    
     // Update selection if the removed cover was selected
     if (selectedIndex == slot) {
         selectedIndex = findNextOccupiedSlot();
         // Force sync of selection change
         coverHolder.markDirty();
     }
    

    }

    private void validateSelection() {
    int oldSelection = selectedIndex;

     if (selectedIndex >= 0 && !internalCovers.containsKey(selectedIndex)) {
         selectedIndex = findNextOccupiedSlot();
     }
    
     // Force sync if selection changed
     if (oldSelection != selectedIndex) {
         coverHolder.markDirty();
         coverHolder.scheduleRenderUpdate();
     }
    

    }

    private int findNextOccupiedSlot() {
    if (internalCovers.isEmpty()) return -1;
    return internalCovers.keySet().stream().min(Integer::compareTo).orElse(-1);
    }

    @nullable
    private CoverDefinition findDefinitionFor(ItemStack stack) {
    if (stack.isEmpty()) return null;
    var item = stack.getItem();

     // Check if it's a ComponentItem with CoverPlaceBehavior
     if (item instanceof com.gregtechceu.gtceu.api.item.ComponentItem) {
         var components = ((com.gregtechceu.gtceu.api.item.ComponentItem) item).getComponents();
         for (var component : components) {
             if (component instanceof com.gregtechceu.gtceu.common.item.CoverPlaceBehavior) {
                 return ((com.gregtechceu.gtceu.common.item.CoverPlaceBehavior) component).coverDefinition();
             }
         }
     }
    
     // Try registry lookup
     var itemId = ForgeRegistries.ITEMS.getKey(item);
     if (itemId != null) {
         var def = GTRegistries.COVERS.get(itemId);
         if (def != null) {
             return def;
         }
    
         // Fallback: search all cover definitions
         for (CoverDefinition coverDef : GTRegistries.COVERS.values()) {
             var coverItem = ForgeRegistries.ITEMS.getValue(coverDef.getId());
             if (coverItem == item) {
                 return coverDef;
             }
         }
     }
    
     return null;
    

    }

    /* ======================
    FIXED UI Implementation
    ====================== */

    @OverRide
    public Widget createUIWidget() {
    // CRITICAL: Create a completely isolated UI that doesn't interact with container slots
    WidgetGroup root = new WidgetGroup(0, 0, 176, 200);

     // Title
     root.addWidget(new LabelWidget(8, 5, LocalizationUtils.format("cover.dynamic.title")));
    
     // Create slot row - these are the ONLY container slots we should have
     WidgetGroup slotRow = new WidgetGroup(8, 20, 160, 20);
     slotWidgets.clear();
    
     for (int i = 0; i < SLOT_COUNT; i++) {
         final int slotIndex = i;
         int x = (i % 9) * 18;
    
         SlotWidget slot = new SlotWidget(coverSlots, i, x, 0) {
             @Override
             public boolean mouseClicked(double mouseX, double mouseY, int button) {
                 if (isMouseOverElement(mouseX, mouseY) && button == 1) { // Right click
                     if (!coverSlots.getStackInSlot(slotIndex).isEmpty() && internalCovers.containsKey(slotIndex)) {
                         // Update selection
                         selectedIndex = slotIndex;
    
                         // Force UI update and synchronization
                         updateSlotHighlights();
                         updateInnerCoverInfo(); // Changed from updateInnerCoverUI
    
                         // Critical: ensure proper synchronization
                         coverHolder.markDirty();
                         coverHolder.notifyBlockUpdate();
                         coverHolder.scheduleRenderUpdate();
    
                         System.out.println("DEBUG: Selected slot " + slotIndex);
                         return true;
                     }
                 }
                 return super.mouseClicked(mouseX, mouseY, button);
             }
         };
    
         // Set change listener for slot updates
         slot.setChangeListener(() -> {
             updateSlotHighlights();
             updateInnerCoverInfo(); // Changed from updateInnerCoverUI
             updateTooltips();
         });
    
         slotWidgets.add(slot);
         slotRow.addWidget(slot);
     }
    
     root.addWidget(slotRow);
    
     // Instructions
     root.addWidget(new LabelWidget(8, 44, Component.literal("Right-click to select cover").getString()));
    
     // CRITICAL: Instead of trying to embed full cover UIs, just show info and basic controls
     innerCoverUIArea = new WidgetGroup(0, 58, 176, 142);
     root.addWidget(innerCoverUIArea);
    
     // Initialize UI state - but NEVER try to create inner cover UIs that have container slots
     updateSlotHighlights();
     updateInnerCoverInfo(); // Changed from updateInnerCoverUI
     updateTooltips();
    
     return root;
    

    }

    /**

    • CRITICAL: Show cover info without creating problematic nested UIs

    • This prevents container slot conflicts entirely by not creating inner cover UIs
      */
      private void updateInnerCoverInfo() {
      if (innerCoverUIArea == null) return;

      innerCoverUIArea.clearAllWidgets();

      if (selectedIndex >= 0 && internalCovers.containsKey(selectedIndex)) {
      CoverBehavior selected = internalCovers.get(selectedIndex);
      System.out.println("DEBUG: Showing info for selected cover: " + selected.getClass().getSimpleName());

       // Create informational display instead of trying to embed the actual UI
       Widget infoWidget = createCoverInfoDisplay(selected);
       innerCoverUIArea.addWidget(infoWidget);
      

      } else if (internalCovers.isEmpty()) {
      innerCoverUIArea.addWidget(new LabelWidget(8, 0, "No covers installed. Place cover items in slots above."));
      } else {
      innerCoverUIArea.addWidget(new LabelWidget(8, 0, "Right-click a cover slot to select it"));
      }
      }

    private void updateSlotHighlights() {
    for (int i = 0; i < slotWidgets.size(); i++) {
    SlotWidget slot = slotWidgets.get(i);
    if (selectedIndex == i && !coverSlots.getStackInSlot(i).isEmpty()) {
    // Highlight selected slot
    slot.setBackgroundTexture(new GuiTextureGroup(
    GuiTextures.SLOT,
    new ColorBorderTexture(2, 0xFF00FF00) // Green border
    ));
    } else {
    // Normal slot
    slot.setBackgroundTexture(GuiTextures.SLOT);
    }
    }
    }

    private void updateTooltips() {
    for (int i = 0; i < SLOT_COUNT && i < slotWidgets.size(); i++) {
    slotWidgets.get(i).setHoverTooltips(createTooltipForSlot(i));
    }
    }

    private List createTooltipForSlot(int index) {
    List tooltip = new ArrayList<>();
    ItemStack stack = coverSlots.getStackInSlot(index);

     if (stack.isEmpty()) {
         tooltip.add(Component.literal("Empty Cover Slot " + (index + 1)));
         tooltip.add(Component.literal("Place a cover item here"));
     } else {
         if (selectedIndex == index) {
             tooltip.add(Component.literal("§a[SELECTED] " + stack.getDisplayName().getString()));
         } else {
             tooltip.add(Component.literal(stack.getDisplayName().getString()));
         }
    
         if (internalCovers.containsKey(index)) {
             if (tickSubscriptions.containsKey(index)) {
                 tooltip.add(Component.literal("§2Status: Active & Ticking"));
             } else {
                 tooltip.add(Component.literal("§6Status: Loaded but not ticking"));
             }
         } else {
             tooltip.add(Component.literal("§cStatus: Failed to load"));
         }
    
         tooltip.add(Component.literal("§7Right-click to select"));
     }
    
     return tooltip;
    

    }

    /* ==========
    Utilities
    ========== */

    public boolean addCover(CoverDefinition def, int slotIndex) {
    if (slotIndex < 0 || slotIndex >= SLOT_COUNT) return false;
    if (!coverSlots.getStackInSlot(slotIndex).isEmpty()) return false;

     var item = ForgeRegistries.ITEMS.getValue(def.getId());
     if (item == null) return false;
    
     ItemStack stack = new ItemStack(item);
     if (stack.isEmpty()) return false;
    
     coverSlots.setStackInSlot(slotIndex, stack.copy());
     return true;
    

    }

    public int getSelectedIndex() {
    return selectedIndex;
    }

    @nullable
    public CoverBehavior getSelectedCover() {
    return internalCovers.get(selectedIndex);
    }

    public Map<Integer, CoverBehavior> getInternalCovers() {
    return Collections.unmodifiableMap(internalCovers);
    }

    /**

    • Creates a safe informational display about a cover without creating container slots

    • This completely avoids the IndexOutOfBoundsException by not embedding actual cover UIs
      */
      private Widget createCoverInfoDisplay(CoverBehavior cover) {
      WidgetGroup group = new WidgetGroup(0, 0, 176, 120);
      int yOffset = 0;

      // Cover type and status
      String coverType = cover.getClass().getSimpleName().replace("Cover", "");
      group.addWidget(new LabelWidget(8, yOffset, "§6" + coverType + " Cover"));
      yOffset += 15;

      // Status indicator
      if (tickSubscriptions.containsKey(selectedIndex)) {
      group.addWidget(new LabelWidget(8, yOffset, "§aStatus: Active & Running"));
      } else {
      group.addWidget(new LabelWidget(8, yOffset, "§eStatus: Loaded but not ticking"));
      }
      yOffset += 15;

      // Cover-specific information
      try {
      if (cover instanceof RobotArmCover) {
      group.addWidget(new LabelWidget(8, yOffset, "§7Type: Item Transfer System"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§7Function: Automated item import/export"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§bNote: This cover is working concurrently"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§cConfiguration: Use cover directly on machine"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§cfor full settings (import/export modes, etc.)"));
      } else if (cover instanceof PumpCover) {
      group.addWidget(new LabelWidget(8, yOffset, "§7Type: Fluid Transfer System"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§7Function: Automated fluid import/export"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§bNote: This cover is working concurrently"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§cConfiguration: Use cover directly on machine"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§cfor full settings (pump speed, modes, etc.)"));
      } else if (cover.getClass().getSimpleName().contains("Controller")) {
      group.addWidget(new LabelWidget(8, yOffset, "§7Type: Machine Control System"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§7Function: Redstone-based machine control"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§bNote: This cover is working concurrently"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§cConfiguration: Use cover directly on machine"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§cfor redstone settings and control modes"));
      } else if (cover instanceof ItemFilterCover) {
      group.addWidget(new LabelWidget(8, yOffset, "§7Type: Item Filter System"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§7Function: Selective item filtering"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§bNote: This cover is working concurrently"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§cConfiguration: Use cover directly on machine"));
      } else {
      group.addWidget(new LabelWidget(8, yOffset, "§7Type: " + coverType + " System"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§bNote: This cover is working concurrently"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§cConfiguration: Limited in dynamic covers"));
      }
      } catch (Throwable t) {
      group.addWidget(new LabelWidget(8, yOffset, "§7Cover is active and functioning"));
      yOffset += 12;
      }

      yOffset += 10;
      group.addWidget(new LabelWidget(8, yOffset, "§8" + "─".repeat(18)));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§eWhy limited configuration?"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§7Dynamic covers can't embed full UIs"));
      yOffset += 12;
      group.addWidget(new LabelWidget(8, yOffset, "§7due to container slot conflicts."));

      // Adjust group size
      group.setSize(176, yOffset + 20);
      return group;
      }

    // Add method to force UI refresh (useful for debugging)
    public void forceUIRefresh() {
    if (isInitialized) {
    updateSlotHighlights();
    updateInnerCoverInfo(); // Changed from updateInnerCoverUI
    updateTooltips();
    coverHolder.scheduleRenderUpdate();
    }
    }
    }