Skip to content

Resolve grouping of unresolved types that likely should result in none > unsupported type#121

Open
R00tB33rMan wants to merge 1 commit into
NichtStudioCode:mainfrom
MagnoliaLLC:main
Open

Resolve grouping of unresolved types that likely should result in none > unsupported type#121
R00tB33rMan wants to merge 1 commit into
NichtStudioCode:mainfrom
MagnoliaLLC:main

Conversation

@R00tB33rMan

@R00tB33rMan R00tB33rMan commented Jun 30, 2026

Copy link
Copy Markdown

I ran into an obscure issue that I "best resolved" by setting these types to "none" when upgrading from Minecraft 26.1 -> 26.2 (or from InvUI 2.1.1 -> 2.2.0).

Example warning: https://mclo.gs/ebZ1Oke (I confirmed this patch DOES minimize the trip).

The action processed correctly (changing an item stack with a customary ShopGUIPlus-like module via adding or removing items). I can't see specific use cases that should be natively defined; thus, I went ahead and proceeded down this route. Let me know if this is wrong or if you'd like this done differently!

@NichtStudioCode

Copy link
Copy Markdown
Owner

Hi, can you share a small example GUI that can be used to reproduce this issue? This exception would mean that the client sends the ContainerClickPacket with a different containerId. Depending on why this is it might make more sense to handle the click normally.

@R00tB33rMan

Copy link
Copy Markdown
Author

Hi, can you share a small example GUI that can be used to reproduce this issue? This exception would mean that the client sends the ContainerClickPacket with a different containerId. Depending on why this is it might make more sense to handle the click normally.

Sure! Here's an example:

/*
 * RootBeer
 *
 * Copyright [2016] - [2026] RootBeer
 * All Rights Reserved.
 *
 * NOTICE: All information contained herein is, and remains the property of RootBeer.
 * The intellectual and technical concepts contained herein are proprietary to RootBeer
 * and may be covered by applicable patents, patents in process, and are protected by
 * trade secret or copyright law. Dissemination of this information or reproduction of
 * this material is strictly forbidden unless prior written permission is obtained from
 * RootBeer.
 */

package com.rootbeer.magnolia.module.shop.menu;

import com.rootbeer.magnolia.module.shop.config.ShopConfig;
import com.rootbeer.magnolia.module.shop.config.ShopMessages;
import com.rootbeer.magnolia.module.shop.loader.ShopLoader;
import com.rootbeer.magnolia.module.shop.manager.TransactionManager;
import com.rootbeer.magnolia.module.shop.object.ShopCategory;
import com.rootbeer.magnolia.module.shop.object.ShopItem;
import com.rootbeer.magnolia.util.menu.MenuUtil;
import com.xcodiq.localization.component.Components;
import com.xcodiq.paper.util.itemstack.ItemStackBuilder;
import com.xcodiq.paper.menu.item.ConfigurableItem;
import com.xcodiq.paper.menu.parameter.MenuParameter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import com.rootbeer.magnolia.menu.MagnoliaConfigurableMenu;

import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;

public final class ShopAmountMenu extends MagnoliaConfigurableMenu {

    private static final char ITEM_MARKER = 'i';

    private final ShopCategory category;
    private final ShopLoader shopLoader;
    private final TransactionManager transactionManager;
    private final ShopItem shopItem;
    private final int stackSize;
    private final int maxAmount;
    private final int amount;
    private final boolean selling;

    public ShopAmountMenu(@NotNull ShopCategory category,
                          @NotNull ShopLoader shopLoader,
                          @NotNull TransactionManager transactionManager,
                          @NotNull ShopItem shopItem,
                          int amount,
                          boolean selling) {
        super(selling
            ? "module/shop/menu/sell_amount_menu.yml"
            : "module/shop/menu/amount_menu.yml", false);

        this.category = category;
        this.shopLoader = shopLoader;
        this.transactionManager = transactionManager;
        this.shopItem = shopItem;
        this.stackSize = this.shopItem.item().getMaxStackSize();
        this.selling = selling;

        final int maxStacks = Math.min(ShopConfig.AMOUNT_MENU_MAX_STACKS.toInt(),
            ShopConfig.AMOUNT_MENU_BUY_LIMIT_STACKS.toInt());
        this.maxAmount = this.stackSize * maxStacks;
        this.amount = Math.clamp(amount, 1, this.maxAmount);

        this.structure.addIngredient(ITEM_MARKER, ConfigurableItem.builder()
            .key("item_preview")
            .withItem(_ -> {
                final ItemStack preview = this.shopItem.displayItem().clone();
                preview.setAmount(Math.min(this.amount, this.stackSize));
                appendPreviewLore(preview);
                MenuUtil.tag(preview);

                return () -> preview;
            })
            .async()
            .build());

        registerStackButtons();
        this.initialize();

        this.feedbackButton("confirm",
            player -> this.selling || this.transactionManager.canAfford(player, this.shopItem, this.amount),
            this::confirm,
            _ -> ShopMessages.BUY_FEEDBACK_REASON.toStr().replace("%price%", this.formatPriceForMode(this.amount)));
    }

    private void registerStackButtons() {
        final int maxStacks = Math.min(ShopConfig.AMOUNT_MENU_MAX_STACKS.toInt(),
            ShopConfig.AMOUNT_MENU_BUY_LIMIT_STACKS.toInt());
        final String markers = ShopConfig.AMOUNT_MENU_STACKS_MARKERS.toStr();
        final String nameTemplate = ShopConfig.AMOUNT_MENU_STACKS_NAME.toStr();
        final List<String> loreTemplate = ShopConfig.AMOUNT_MENU_STACKS_LORE.toStringList();
        final String materialOverride = ShopConfig.AMOUNT_MENU_STACKS_MATERIAL.toStr();
        final Material overrideMaterial = materialOverride.equalsIgnoreCase("%item%")
            ? null
            : Material.matchMaterial(materialOverride);
        final boolean glint = ShopConfig.AMOUNT_MENU_STACKS_GLINT.toBoolean();

        for (int s = 0; s < markers.length() && s < maxStacks; s++) {
            final int stackCount = s + 1;
            final int totalAmount = this.stackSize * stackCount;
            final String formattedPrice = this.formatPriceForMode(totalAmount);

            this.structure.addIngredient(markers.charAt(s), ConfigurableItem.builder()
                .key("stacks_" + stackCount)
                .withItem(_ -> {
                    final ItemStack display;

                    if (overrideMaterial != null) {
                        display = new ItemStack(overrideMaterial, stackCount);
                    } else {
                        display = this.shopItem.displayItem().clone();
                        display.setAmount(stackCount);
                    }

                    final List<String> loreLines = new ArrayList<>();

                    for (final String line : loreTemplate) {
                        loreLines.add(Components.prepareLine(line,
                            "%stacks%", String.valueOf(stackCount),
                            "%amount%", String.valueOf(totalAmount),
                            "%price%", formattedPrice));
                    }

                    final ItemStack result = ItemStackBuilder.of(display)
                        .apply(builder -> {
                            if (glint) {
                                builder.transformMeta(m -> m.setEnchantmentGlintOverride(true));
                            }
                        })
                        .name(Components.prepareLine(nameTemplate,
                            "%stacks%", String.valueOf(stackCount)))
                        .clearLore()
                        .lore(loreLines)
                        .build();

                    MenuUtil.tag(result);

                    return () -> result;
                })
                .onClick((player, _) -> {
                    if (MenuUtil.unacquireClick(player.getUniqueId())) {
                        return;
                    }

                    final int newAmount = this.stackSize * stackCount;
                    final int clamped = clamp(newAmount);

                    if (clamped == this.amount) {
                        MenuUtil.releaseClick(player.getUniqueId());

                        return;
                    }

                    openWithAmount(player, clamped);
                })
                .async()
                .build());
        }
    }

    @Override
    public @NotNull MenuParameter[] setupMenuParameters() {
        final int stacks = (int) Math.ceil((double) this.amount / this.stackSize);

        return new MenuParameter[] {
            MenuParameter.of("item", formatItemName(this.shopItem.item())),
            MenuParameter.of("amount", String.valueOf(this.amount)),
            MenuParameter.of("stacks", String.valueOf(stacks)),
            MenuParameter.of("max_stack", String.valueOf(this.stackSize)),
            MenuParameter.of("price", this.formatPriceForMode(this.amount))
        };
    }

    /**
     * Handles quantity selection and buy/sell confirmation.
     * Supports preset amounts (1, 16, 32, 64) and custom input.
     */
    @Override
    public BiConsumer<Player, String> setupClick() {
        return (player, key) -> {
            if (MenuUtil.unacquireClick(player.getUniqueId())) {
                return;
            }

            if (key.equals("cancel")) {
                this.plugin.scheduler().now(player, () -> {
                    new ShopCategoryMenu(this.category, this.shopLoader, this.transactionManager)
                        .open(player);
                    MenuUtil.releaseClick(player.getUniqueId());
                });

                return;
            }

            final int newAmount = resolveAmount(key);

            if (newAmount < 0 || clamp(newAmount) == this.amount) {
                MenuUtil.releaseClick(player.getUniqueId());

                return;
            }

            openWithAmount(player, newAmount);
        };
    }

    @NotNull
    private String formatPriceForMode(int amount) {
        if (this.selling) {
            return this.transactionManager.formatPrice(this.shopItem,
                this.transactionManager.effectiveSellPrice(this.shopItem, amount));
        }

        return this.transactionManager.formatPrice(this.shopItem,
            this.transactionManager.effectiveBuyPrice(this.shopItem, amount));
    }

    private int resolveAmount(@NotNull String key) {
        if (key.equals("set_max")) {
            return this.stackSize;
        }

        if (key.startsWith("set_")) {
            final int value = parseTrailingInt(key, "set_");

            if (value > 0) {
                return value;
            }
        }

        if (key.startsWith("add_")) {
            final int value = parseTrailingInt(key, "add_");

            if (value > 0) {
                return this.amount + value;
            }
        }

        if (key.startsWith("remove_")) {
            final int value = parseTrailingInt(key, "remove_");

            if (value > 0) {
                return this.amount - value;
            }
        }

        if (key.startsWith("stacks_")) {
            final int value = parseTrailingInt(key, "stacks_");

            if (value > 0) {
                return this.stackSize * value;
            }
        }

        return -1;
    }

    private int clamp(int value) {
        return Math.clamp(value, 1, this.maxAmount);
    }

    private static int parseTrailingInt(@NotNull String key, @NotNull String prefix) {
        try {
            return Integer.parseInt(key.substring(prefix.length()));
        } catch (NumberFormatException e) {
            return -1;
        }
    }

    private void appendPreviewLore(@NotNull ItemStack item) {
        final int stacks = (int) Math.ceil((double) this.amount / this.stackSize);

        final List<String> lines = new ArrayList<>();
        lines.add("");
        lines.add(Components.prepareLine(ShopConfig.AMOUNT_MENU_LORE_AMOUNT.toStr(),
            "%amount%", String.valueOf(this.amount)));

        if (stacks > 1) {
            lines.add(Components.prepareLine(ShopConfig.AMOUNT_MENU_LORE_STACKS.toStr(),
                "%stacks%", String.valueOf(stacks)));
        }

        lines.add(Components.prepareLine(ShopConfig.AMOUNT_MENU_LORE_PRICE.toStr(),
            "%price%", this.formatPriceForMode(this.amount)));

        final ItemStack result = ItemStackBuilder.of(item).lore(lines).build();
        item.setItemMeta(result.getItemMeta());
    }

    private void confirm(@NotNull Player player) {
        if (this.selling) {
            this.plugin.scheduler().now(player, () ->
                this.transactionManager.handleSell(player, this.shopItem, this.amount));
        } else {
            this.plugin.scheduler().now(player, () ->
                this.transactionManager.handleBuy(player, this.shopItem, this.amount));
        }

        openWithAmount(player, this.amount);
    }

    private void openWithAmount(@NotNull Player player, int newAmount) {
        this.plugin.scheduler().now(player, () -> {
            new ShopAmountMenu(this.category, this.shopLoader,
                this.transactionManager, this.shopItem, newAmount, this.selling)
                .open(player);
            MenuUtil.releaseClick(player.getUniqueId());
        });
    }

    @NotNull
    private static String formatItemName(@NotNull ItemStack item) {
        if (item.hasItemMeta() && item.getItemMeta().hasDisplayName()) {
            final Component displayName = item.getItemMeta().displayName();

            if (displayName != null) {
                return PlainTextComponentSerializer.plainText().serialize(displayName);
            }
        }

        final String[] parts = item.getType().name().toLowerCase().split("_");
        final StringBuilder name = new StringBuilder();

        for (int i = 0; i < parts.length; i++) {
            if (i > 0) {
                name.append(" ");
            }

            name.append(Character.toUpperCase(parts[i].charAt(0)));
            name.append(parts[i].substring(1));
        }

        return name.toString();
    }
}

@NichtStudioCode

Copy link
Copy Markdown
Owner

Please send a small, standalone, code snippet that I can run alongside steps to reproduce the exception. I can't run the code above as it depends on other classes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants