diff --git a/src/main/java/de/hysky/skyblocker/config/categories/QuickNavigationCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/QuickNavigationCategory.java index 8af96f61a95..e15e91da533 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/QuickNavigationCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/QuickNavigationCategory.java @@ -4,6 +4,7 @@ import de.hysky.skyblocker.config.ConfigUtils; import de.hysky.skyblocker.config.SkyblockerConfig; import de.hysky.skyblocker.config.configs.QuickNavigationConfig; +import de.hysky.skyblocker.config.screens.quicknav.QuickNavConfigScreen; import de.hysky.skyblocker.utils.render.gui.ItemSelectionPopup; import de.hysky.skyblocker.utils.datafixer.ItemStackComponentizationFixer; import net.azureaaron.dandelion.api.ButtonOption; @@ -32,6 +33,11 @@ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig newValue -> config.quickNav.enableQuickNav = newValue) .controller(ConfigUtils.createBooleanController()) .build()) + .optionIf(Minecraft.getInstance().player != null, ButtonOption.createBuilder() + .name(Component.translatable("skyblocker.config.quickNav.quickNavConfigScreen")) + .prompt(Component.translatable("text.skyblocker.open")) + .action(screen -> Minecraft.getInstance().gui.setScreen(new QuickNavConfigScreen(screen))) + .build()) //Buttons .group(quickNavButton(defaults.quickNav.button1, config.quickNav.button1, 1)) diff --git a/src/main/java/de/hysky/skyblocker/config/configs/QuickNavigationConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/QuickNavigationConfig.java index deaae568734..d77354c6cae 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/QuickNavigationConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/QuickNavigationConfig.java @@ -78,17 +78,26 @@ public QuickNavItem(boolean doubleClick, ItemData itemData, @Language("RegExp") this.tooltip = tooltip; } + public QuickNavItem(QuickNavItem quickNavItem) { + this.doubleClick = quickNavItem.doubleClick; + this.itemData = quickNavItem.itemData; + this.uiTitle = quickNavItem.uiTitle; + this.clickEvent = quickNavItem.clickEvent; + this.tooltip = quickNavItem.tooltip; + this.render = quickNavItem.render; + } + public boolean render = true; public boolean doubleClick = false; - public ItemData itemData; + public ItemData itemData = new ItemData(Items.POISONOUS_POTATO); - public String uiTitle; + public @Language("RegExp") String uiTitle = "none"; public String tooltip = ""; - public String clickEvent; + public String clickEvent = "/help"; } public static class ItemData { diff --git a/src/main/java/de/hysky/skyblocker/config/screens/quicknav/ItemEditPopup.java b/src/main/java/de/hysky/skyblocker/config/screens/quicknav/ItemEditPopup.java new file mode 100644 index 00000000000..c34c8a7c52d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/config/screens/quicknav/ItemEditPopup.java @@ -0,0 +1,257 @@ +package de.hysky.skyblocker.config.screens.quicknav; + +import com.demonwav.mcdev.annotations.Translatable; +import com.google.gson.JsonElement; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.JsonOps; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.config.configs.QuickNavigationConfig; +import de.hysky.skyblocker.skyblock.profileviewer2.widgets.ButtonWidget; +import de.hysky.skyblocker.utils.command.CommandUtils; +import de.hysky.skyblocker.utils.command.argumenttypes.RegexArgumentType; +import de.hysky.skyblocker.utils.datafixer.ItemStackComponentizationFixer; +import de.hysky.skyblocker.utils.render.gui.AbstractPopupScreen; +import de.hysky.skyblocker.utils.render.gui.ComponentEditWidget; +import de.hysky.skyblocker.utils.render.gui.ItemSelectionPopup; +import de.hysky.skyblocker.utils.render.gui.SuggestionsEditBox; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.Checkbox; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.ScrollableLayout; +import net.minecraft.client.gui.components.StringWidget; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.layouts.GridLayout; +import net.minecraft.client.gui.layouts.LayoutElement; +import net.minecraft.client.gui.layouts.LayoutSettings; +import net.minecraft.client.gui.layouts.LinearLayout; +import net.minecraft.client.gui.layouts.SpacerElement; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.arguments.item.ItemArgument; +import net.minecraft.commands.arguments.item.ItemInput; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.util.ARGB; +import net.minecraft.world.item.ItemStack; +import org.slf4j.Logger; + +import java.util.regex.Pattern; + +class ItemEditPopup extends AbstractPopupScreen { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int SCROLLABLE_CONTENT_HEIGHT_DIFF = BACKGROUND_MARGIN * 2 + 20 + 50; // 20: height of buttons, 50: some hardcoded constant + + private final Runnable onClose; + private final QuickNavigationConfig.QuickNavItem item; + private final QuickNavConfigScreen.ConfigItemSetter setter; + private final LinearLayout layout = LinearLayout.vertical().spacing(2); + private ScrollableLayout scrollableContent = new ScrollableLayout(minecraft, LinearLayout.vertical(), 0); // placeholder + + private Component currentTooltip; + + ItemEditPopup(Screen backgroundScreen, Runnable onClose, QuickNavigationConfig.QuickNavItem item, QuickNavConfigScreen.ConfigItemSetter setter, int index) { + super(Component.translatable("skyblocker.config.quickNav.screen.title", index + 1).withStyle(ChatFormatting.BOLD, ChatFormatting.UNDERLINE), backgroundScreen); + this.onClose = onClose; + this.item = new QuickNavigationConfig.QuickNavItem(item); + this.setter = setter; + try { + currentTooltip = ComponentSerialization.CODEC.decode(JsonOps.INSTANCE, SkyblockerMod.GSON.fromJson(item.tooltip, JsonElement.class)).getOrThrow().getFirst(); + } catch (Exception _) { + currentTooltip = Component.literal(item.tooltip); + } + } + + @Override + protected void init() { + layout.addChild(new StringWidget(getTitle(), font), l -> l.alignHorizontallyCenter().paddingTop(2).paddingBottom(6)); + LinearLayout content = LinearLayout.vertical().spacing(10); + content.defaultCellSetting().padding(3); + CommandBuildContext context = CommandUtils.newContext(); + LinearLayout commandLayout = content.addChild(createSectionLayout()); + // click event + addTitle(commandLayout, "skyblocker.config.quickNav.button.clickEvent"); + EditBox commandBox = SuggestionsEditBox.builder().width(250).buildVanillaDispatcher( + minecraft, font, this, Component.empty(), + false); + commandBox.setValue(item.clickEvent); + commandBox.setResponder(s -> item.clickEvent = s); + commandLayout.addChild(commandBox); + + // tooltip + LinearLayout tooltipLayout = content.addChild(createSectionLayout()); + addTitle(tooltipLayout, "skyblocker.config.quickNav.button.tooltip"); + ComponentEditWidget editWidget = new ComponentEditWidget(this, Component.literal("Customize Tooltip"), component -> currentTooltip = component.copy()); + tooltipLayout.addChild(editWidget); + editWidget.setText(currentTooltip.copy(), false); + + // menu regex + LinearLayout regexLayout = content.addChild(createSectionLayout()); + addTitle(regexLayout, "skyblocker.config.quickNav.button.uiTitle"); + SuggestionsEditBox.Argument patternBox = SuggestionsEditBox.builder().width(250).onlyShowIfCursorPastError(false).buildArg( + minecraft, font, this, Component.empty(), + new RegexArgumentType() + ); + patternBox.setTooltip(Tooltip.create(Component.literal("The button will appear pressed in the menu matching this title.\nThis supports Regex!\nCan be left empty if button doesn't open a menu."))); + patternBox.setMaxLength(2048); + patternBox.setValue(item.uiTitle); + patternBox.setValueResponder(p -> item.uiTitle = p.pattern().isBlank() ? "lorem ipsum" : p.pattern()); + regexLayout.addChild(patternBox); + + // item selection + LinearLayout iconLayout = content.addChild(createSectionLayout()); + addTitle(iconLayout, "skyblocker.config.quickNav.button.icon"); + GridLayout itemLayout = iconLayout.addChild(new GridLayout()).columnSpacing(4).rowSpacing(2); + + ItemStack stack = ItemStackComponentizationFixer.fromComponentsString(item.itemData.item.toString(), item.itemData.count, item.itemData.components); + ItemWidget itemWidget = itemLayout.addChild(new ItemWidget(stack), 1, 0, l -> l.alignVerticallyMiddle().alignHorizontallyCenter()); + int itemWidth = 250; + itemLayout.addChild(new StringWidget(Component.translatable("skyblocker.config.quickNav.button.item.itemName"), font), 0, 1).setMaxWidth(itemWidth, StringWidget.TextOverflow.SCROLLING); + SuggestionsEditBox.Argument itemBox = SuggestionsEditBox.builder().width(250).buildArg( + minecraft, font, this, Component.empty(), + new ItemArgument(context) + ); + itemLayout.addChild(itemBox, 1, 1); + itemBox.setMaxLength(4096); + itemBox.setValue(item.itemData.item + item.itemData.components); + itemBox.setValueResponder(itemInput -> { + ItemStack itemStack = new ItemStack(itemInput.item(), item.itemData.count, itemInput.components()); + itemWidget.stack = itemStack; + item.itemData.item = itemStack.getItem(); + item.itemData.components = ItemStackComponentizationFixer.componentsAsString(itemStack); + }); + + int countWidth = 30; + itemLayout.addChild(new StringWidget(Component.translatable("skyblocker.config.quickNav.screen.count"), font), 0, 2).setMaxWidth(countWidth, StringWidget.TextOverflow.SCROLLING); + SuggestionsEditBox.Argument countBox = SuggestionsEditBox.builder().width(20).buildArg( + minecraft, font, this, Component.empty(), + IntegerArgumentType.integer(1) + ); + itemLayout.addChild(countBox, 1, 2); + countBox.setMaxLength(2); + countBox.setValue(String.valueOf(item.itemData.count)); + countBox.setValueResponder(count -> { + item.itemData.count = Math.max(count, 1); + itemWidget.stack = itemWidget.stack.copyWithCount(item.itemData.count); + }); + + iconLayout.addChild(ButtonWidget.builder(Component.translatable("skyblocker.config.quickNav.button.chooseSkyblockItem"), _ -> minecraft.gui.setScreen( + new ItemSelectionPopup(this, itemStack -> { + if (itemStack != null) { + itemWidget.stack = itemStack; + item.itemData.item = itemStack.getItem(); + String components = ItemStackComponentizationFixer.componentsAsString(itemStack); + item.itemData.components = components; + itemBox.setValue(itemStack.getItem() + components); + } + }) + )).tooltip(Tooltip.create(Component.translatable("skyblocker.config.quickNav.button.chooseSkyblockItem.@Tooltip"))).build()); + + // require double click + LinearLayout doubleClickLayout = content.addChild(createSectionLayout()); + doubleClickLayout.addChild(Checkbox.builder(Component.translatable("skyblocker.config.quickNav.button.doubleClick"), font) + .onValueChange((_, value) -> item.doubleClick = value) + .selected(item.doubleClick) + .build() + ).setTooltip(Tooltip.create(Component.translatable("skyblocker.config.quickNav.button.doubleClick.@Tooltip"))); + + content.addChild(SpacerElement.height(0)); + scrollableContent = layout.addChild(new ScrollableLayout(minecraft, content, height - SCROLLABLE_CONTENT_HEIGHT_DIFF)); + + // the buttons at the bottom + LinearLayout buttonsLayout = layout.addChild(LinearLayout.horizontal().spacing(4), LayoutSettings::alignHorizontallyCenter); + buttonsLayout.addChild(ButtonWidget.builder(CommonComponents.GUI_CANCEL, _ -> onClose()).build()); + buttonsLayout.addChild(ButtonWidget.builder(CommonComponents.GUI_DONE, _ -> { + save(); + onClose(); + }).build()); + + layout.visitWidgets(this::addRenderableWidget); + super.init(); + } + + private void addTitle(LinearLayout layout, @Translatable String title) { + layout.addChild(new StringWidget(Component.translatable(title).withStyle(ChatFormatting.BOLD), font), l -> l.paddingBottom(4)); + } + + private LinearLayout createSectionLayout() { + LinearLayout linearLayout = LinearLayout.vertical().spacing(2); + linearLayout.addChild(new BackgroundRender(linearLayout, layout)); + return linearLayout; + } + + @Override + public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractBackground(graphics, mouseX, mouseY, a); + extractPopupBackground(graphics, layout.getX(), layout.getY(), layout.getWidth(), layout.getHeight(), true); + } + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractRenderState(graphics, mouseX, mouseY, a); + } + + @Override + protected void repositionElements() { + super.repositionElements(); + scrollableContent.arrangeElements(); + scrollableContent.setMaxHeight(height - SCROLLABLE_CONTENT_HEIGHT_DIFF); + layout.arrangeElements(); + layout.setPosition((width - layout.getWidth()) / 2, (height - layout.getHeight()) / 2); + } + + @Override + public void onClose() { + super.onClose(); + onClose.run(); + } + + private void save() { + item.tooltip = ComponentSerialization.CODEC.encodeStart(JsonOps.INSTANCE, currentTooltip) + .ifError(error -> LOGGER.error("Failed to serialize component! {}", error.message())).result() + .map(SkyblockerMod.GSON_COMPACT::toJson).orElse(currentTooltip.getString()); + SkyblockerConfigManager.updateOnly(config -> setter.accept(config.quickNav, item)); + } + + private static class ItemWidget extends AbstractWidget { + private ItemStack stack; + + private ItemWidget(ItemStack stack) { + super(0, 0, 16, 16, stack.getItemName()); + this.stack = stack; + } + + @Override + protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + graphics.item(stack, getX(), getY()); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput output) {} + } + + private static class BackgroundRender extends AbstractWidget { + private final LayoutElement heightLayout; + private final LayoutElement widthLayout; + + private BackgroundRender(LayoutElement heightLayout, LayoutElement widthLayout) { + super(0, 0, 0, 0, Component.empty()); + active = false; + this.heightLayout = heightLayout; + this.widthLayout = widthLayout; + } + + @Override + protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + graphics.fill(widthLayout.getX(), heightLayout.getY() - 3, widthLayout.getX() + widthLayout.getWidth(), heightLayout.getY() + heightLayout.getHeight() + 3, ARGB.black(0.15f)); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput output) {} + } +} diff --git a/src/main/java/de/hysky/skyblocker/config/screens/quicknav/QuickNavConfigScreen.java b/src/main/java/de/hysky/skyblocker/config/screens/quicknav/QuickNavConfigScreen.java new file mode 100644 index 00000000000..bb39704095d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/config/screens/quicknav/QuickNavConfigScreen.java @@ -0,0 +1,267 @@ +package de.hysky.skyblocker.config.screens.quicknav; + +import com.mojang.blaze3d.platform.InputConstants; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.config.configs.QuickNavigationConfig; +import de.hysky.skyblocker.skyblock.quicknav.QuickNavButton; +import de.hysky.skyblocker.utils.datafixer.ItemStackComponentizationFixer; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.InventoryScreen; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.input.MouseButtonInfo; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.util.ARGB; +import net.minecraft.util.CommonColors; +import net.minecraft.world.entity.EntityTypes; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.animal.feline.Cat; +import net.minecraft.world.item.ItemStack; +import org.jspecify.annotations.Nullable; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +public class QuickNavConfigScreen extends Screen { + private static final int INVENTORY_WIDTH = 176; + private static final int INVENTORY_HEIGHT = 166; + private static final ConfigItemSupplier[] ITEM_SUPPLIERS = new ConfigItemSupplier[]{ + config -> config.button1, + config -> config.button2, + config -> config.button3, + config -> config.button4, + config -> config.button5, + config -> config.button6, + config -> config.button7, + config -> config.button8, + config -> config.button9, + config -> config.button10, + config -> config.button11, + config -> config.button12, + config -> config.button13, + config -> config.button14 + }; + private static final ConfigItemSetter[] SETTERS = new ConfigItemSetter[]{ + (config, item) -> config.button1 = item, + (config, item) -> config.button2 = item, + (config, item) -> config.button3 = item, + (config, item) -> config.button4 = item, + (config, item) -> config.button5 = item, + (config, item) -> config.button6 = item, + (config, item) -> config.button7 = item, + (config, item) -> config.button8 = item, + (config, item) -> config.button9 = item, + (config, item) -> config.button10 = item, + (config, item) -> config.button11 = item, + (config, item) -> config.button12 = item, + (config, item) -> config.button13 = item, + (config, item) -> config.button14 = item + }; + + private final QuickNavConfigButton[] buttons = new QuickNavConfigButton[ITEM_SUPPLIERS.length]; + private final @Nullable Screen parent; + private @Nullable QuickNavConfigButton highlightedButton; + private final @Nullable LivingEntity entityToRender; + + public QuickNavConfigScreen() { + this(null); + } + + public QuickNavConfigScreen(@Nullable Screen parent) { + super(Component.literal("Quick Navigation Config")); + if (Math.random() < 0.001 && minecraft.level != null) { + entityToRender = new Cat(EntityTypes.CAT, minecraft.level); + entityToRender.setId("meow".hashCode()); + } else { + entityToRender = minecraft.player; + } + this.parent = parent; + } + + @Override + protected void init() { + super.init(); + for (int i = 0; i < buttons.length; i++) { + QuickNavigationConfig.QuickNavItem item = ITEM_SUPPLIERS[i].apply(SkyblockerConfigManager.get().quickNav); + buttons[i] = addWidget(new QuickNavConfigButton( + item.render, + i, + ItemStackComponentizationFixer.fromComponentsString(item.itemData.item.toString(), Math.clamp(item.itemData.count, 1, 99), item.itemData.components), + item.tooltip + )); + } + repositionElements(); + } + + @Override + protected void repositionElements() { + for (QuickNavConfigButton button : buttons) { + button.setPositionFrom((width - INVENTORY_WIDTH) / 2, (height - INVENTORY_HEIGHT) / 2, INVENTORY_WIDTH, INVENTORY_HEIGHT); + } + } + + private void refreshButton(int index) { + QuickNavigationConfig.QuickNavItem item = ITEM_SUPPLIERS[index].apply(SkyblockerConfigManager.get().quickNav); + removeWidget(buttons[index]); + buttons[index] = addWidget(new QuickNavConfigButton( + item.render, + index, + ItemStackComponentizationFixer.fromComponentsString(item.itemData.item.toString(), Math.clamp(item.itemData.count, 1, 99), item.itemData.components), + item.tooltip + )); + repositionElements(); + } + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractRenderState(graphics, mouseX, mouseY, a); + graphics.centeredText(font, "Right click to disable/enable buttons.", width / 2, 4, CommonColors.GRAY); + graphics.centeredText(font, "Left click to edit button options.", width / 2, 4 + font.lineHeight, CommonColors.GRAY); + QuickNavConfigButton dragged = null; + for (QuickNavConfigButton button : buttons) { + if (button.dragging && dragged == null) { + dragged = button; + continue; + } + button.extractRenderState(graphics, mouseX, mouseY, a); + } + if (highlightedButton != null) { + graphics.fill(highlightedButton.getX(), highlightedButton.getY(), highlightedButton.getRight(), highlightedButton.getBottom(), ARGB.color(0.3f, CommonColors.YELLOW)); + } + // render dragged in front of everything + if (dragged != null) dragged.extractRenderState(graphics, mouseX, mouseY, a); + if (entityToRender != null) { + int xo = (width - INVENTORY_WIDTH) / 2; + int yo = (height - INVENTORY_HEIGHT) / 2; + InventoryScreen.extractEntityInInventoryFollowsMouse(graphics, xo + 26, yo + 8, xo + 75, yo + 78, 30, 0.0625f, mouseX, mouseY, entityToRender); + } + } + + @Override + public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractBackground(graphics, mouseX, mouseY, a); + int backgroundX = (width - INVENTORY_WIDTH) / 2; + int backgroundY = (height - INVENTORY_HEIGHT) / 2; + graphics.blit(RenderPipelines.GUI_TEXTURED, InventoryScreen.INVENTORY_LOCATION, backgroundX, backgroundY, 0.0f, 0.0f, INVENTORY_WIDTH, INVENTORY_HEIGHT, 256, 256); + } + + @Override + public void removed() { + super.removed(); + SkyblockerConfigManager.update(_ -> {}); + } + + @Override + public void onClose() { + minecraft.gui.setScreen(parent); + } + + private class QuickNavConfigButton extends QuickNavButton { + private boolean enabled; + private double dragX, dragY; + private boolean dragging; + + /** + * Constructs a new QuickNavButton with the given parameters. + * + * @param index the index of the button. + * @param icon the icon to display on the button. + * @param tooltip the tooltip to show when hovered + */ + private QuickNavConfigButton(boolean enabled, int index, ItemStack icon, String tooltip) { + super(index, true, "", icon, tooltip); + this.enabled = enabled; + setRenderInFront(true); + } + + @Override + protected void updateCoordinates() {} + + @Override + public void onClick(MouseButtonEvent click, boolean doubled) { + if (click.button() == InputConstants.MOUSE_BUTTON_RIGHT) { + enabled = !enabled; + SkyblockerConfigManager.updateOnly(config -> ITEM_SUPPLIERS[index].apply(config.quickNav).render = enabled); + } + dragX = click.x(); + dragY = click.y(); + } + + @Override + protected void setPositionFrom(int backgroundX, int backgroundY, int imageWidth, int imageHeight) { + super.setPositionFrom(backgroundX, backgroundY, imageWidth, imageHeight); + } + + @Override + protected void onDrag(MouseButtonEvent event, double dx, double dy) { + super.onDrag(event, dx, dy); + if (!dragging && enabled) { + double diffX = event.x() - dragX; + double diffY = event.y() - dragY; + if (diffX * diffX + diffY * diffY > 10 * 10) { + dragging = true; + setTooltip(null); + } + } + if (!dragging) return; + setPosition((int) (event.x() - getWidth() / 2d), (int) (event.y() - getHeight() / 2d)); + highlightedButton = null; + for (QuickNavConfigButton button : buttons) { + if (button == this) continue; + if (button.isMouseOver(event.x(), event.y())) { + highlightedButton = button; + break; + } + } + if (highlightedButton != null) setTooltip(Tooltip.create(Component.literal("Release to swap"))); + else setTooltip(null); + } + + @Override + public void onRelease(MouseButtonEvent event) { + super.onRelease(event); + if (dragging) { + if (highlightedButton != null) { + int i1 = highlightedButton.index; + int i2 = this.index; + SkyblockerConfigManager.updateOnly(config -> { + QuickNavigationConfig.QuickNavItem item1 = ITEM_SUPPLIERS[i1].apply(config.quickNav); + QuickNavigationConfig.QuickNavItem item2 = ITEM_SUPPLIERS[i2].apply(config.quickNav); + SETTERS[i1].accept(config.quickNav, item2); + SETTERS[i2].accept(config.quickNav, item1); + }); + refreshButton(i1); + refreshButton(i2); + highlightedButton = null; + } else { + repositionElements(); + } + dragging = false; + } else { + minecraft.gui.setScreen(new ItemEditPopup(QuickNavConfigScreen.this, () -> refreshButton(index), ITEM_SUPPLIERS[index].apply(SkyblockerConfigManager.get().quickNav), SETTERS[index], index)); + } + setTooltip(tooltip); + } + + @Override + public void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) { + if (enabled) { + super.extractWidgetRenderState(graphics, mouseX, mouseY, delta); + } + } + + @Override + protected boolean isValidClickButton(MouseButtonInfo buttonInfo) { + return super.isValidClickButton(buttonInfo) || buttonInfo.button() == InputConstants.MOUSE_BUTTON_RIGHT; + } + } + + // Little interface because type inference is wacky on arrays + @FunctionalInterface + interface ConfigItemSupplier extends Function {} + + @FunctionalInterface + interface ConfigItemSetter extends BiConsumer {} +} diff --git a/src/main/java/de/hysky/skyblocker/injected/SkyblockerGraphicsExtractor.java b/src/main/java/de/hysky/skyblocker/injected/SkyblockerGraphicsExtractor.java new file mode 100644 index 00000000000..1e4612f6d96 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/injected/SkyblockerGraphicsExtractor.java @@ -0,0 +1,9 @@ +package de.hysky.skyblocker.injected; + +import net.minecraft.client.gui.components.Renderable; + +public interface SkyblockerGraphicsExtractor { + default void skb$addDeferredElement(Renderable renderable) { + throw new AssertionError("Implemented in Mixin"); + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixins/AbstractContainerWidgetMixin.java b/src/main/java/de/hysky/skyblocker/mixins/AbstractContainerWidgetMixin.java new file mode 100644 index 00000000000..b48212e6edf --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/AbstractContainerWidgetMixin.java @@ -0,0 +1,21 @@ +package de.hysky.skyblocker.mixins; + +import net.minecraft.client.gui.components.AbstractContainerWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(AbstractContainerWidget.class) +public class AbstractContainerWidgetMixin { + @Shadow + private @Nullable GuiEventListener focused; + + @Inject(method = "setFocused(Lnet/minecraft/client/gui/components/events/GuiEventListener;)V", at = @At("HEAD"), cancellable = true) + private void onSetFocused(GuiEventListener focused, CallbackInfo ci) { + if (focused == this.focused) ci.cancel(); // AbstractContainerEventHandler does this check but not this. + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixins/GuiGraphicsExtractorMixin.java b/src/main/java/de/hysky/skyblocker/mixins/GuiGraphicsExtractorMixin.java index f530ac2a879..4d57391a638 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/GuiGraphicsExtractorMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/GuiGraphicsExtractorMixin.java @@ -2,26 +2,33 @@ import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.sugar.Local; +import de.hysky.skyblocker.injected.SkyblockerGraphicsExtractor; import de.hysky.skyblocker.skyblock.item.ItemCooldowns; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.render.text.GridComponentManager; import de.hysky.skyblocker.utils.render.text.GridTooltipComponent; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Renderable; import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipPositioner; import net.minecraft.resources.Identifier; import net.minecraft.world.item.ItemStack; import org.jspecify.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.ArrayList; import java.util.List; @Mixin(GuiGraphicsExtractor.class) -public abstract class GuiGraphicsExtractorMixin { +public abstract class GuiGraphicsExtractorMixin implements SkyblockerGraphicsExtractor { + @Unique + private final List deferredElements = new ArrayList<>(); + @ModifyExpressionValue(method = "itemCooldown", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/ItemCooldowns;getCooldownPercent(Lnet/minecraft/world/item/ItemStack;F)F")) private float skyblocker$modifyItemCooldown(float cooldownProgress, @Local(name = "itemStack") ItemStack stack) { return Utils.isOnSkyblock() && ItemCooldowns.isOnCooldown(stack) ? ItemCooldowns.getItemCooldownEntry(stack).getRemainingCooldownPercent() : cooldownProgress; @@ -38,4 +45,18 @@ private void initializeGrids(Font font, List lines, int } } } + + @Inject(method = "extractDeferredElements", at = @At("TAIL")) + private void extractDeferredElements(int mouseX, int mouseY, float a, CallbackInfo ci) { + if (deferredElements.isEmpty()) return; + for (Renderable renderable : deferredElements) { + renderable.extractRenderState((GuiGraphicsExtractor) (Object) this, mouseX, mouseY, a); + } + deferredElements.clear(); + } + + @Override + public void skb$addDeferredElement(Renderable renderable) { + deferredElements.add(renderable); + } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/ItemTab.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/ItemTab.java index 3ef71c35b8c..22494cafd4c 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/ItemTab.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/ItemTab.java @@ -3,8 +3,8 @@ import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.skyblock.item.SkyblockInventoryScreen; -import de.hysky.skyblocker.skyblock.item.custom.screen.name.CustomizeNameWidget; import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.gui.ComponentEditWidget; import it.unimi.dsi.fastutil.objects.Object2BooleanMap; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; @@ -16,6 +16,7 @@ import net.minecraft.client.gui.components.StringWidget; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.components.tabs.GridLayoutTab; +import net.minecraft.client.gui.layouts.Layout; import net.minecraft.client.gui.layouts.LinearLayout; import net.minecraft.client.gui.layouts.SpacerElement; import net.minecraft.client.gui.narration.NarrationElementOutput; @@ -23,6 +24,7 @@ import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; import net.minecraft.resources.Identifier; import net.minecraft.util.TriState; import net.minecraft.world.item.ItemStack; @@ -36,7 +38,7 @@ public class ItemTab extends GridLayoutTab { private static final Identifier INNER_SPACE_TEXTURE = SkyblockerMod.id("menu_inner_space"); private final CustomizeScreen parentScreen; - private final CustomizeNameWidget nameWidget; + private final ComponentEditWidget nameWidget; private final Button glintButton; private final IdentifierTextField modelField; @@ -70,12 +72,19 @@ public ItemTab(CustomizeScreen parentScreen) { }); }); modelField.setHint(Component.translatable("skyblocker.customization.item.modelOverride").withStyle(ChatFormatting.ITALIC).withStyle(ChatFormatting.GRAY)); - nameWidget = new CustomizeNameWidget(parentScreen); + nameWidget = new ComponentEditWidget(parentScreen, Component.literal("Customize Item Name"), component -> { + String uuid = currentItem.getUuid(); + if (uuid.isEmpty()) return; + SkyblockerConfigManager.updateOnly(config -> { + if (component.getString().isBlank()) config.general.customItemNames.remove(currentItem.getUuid()); + else config.general.customItemNames.put(currentItem.getUuid(), component.copy().setStyle(Style.EMPTY.withItalic(false).withColor(ChatFormatting.WHITE))); + }); + }); layout.addChild(new ItemSelector(), 0, 0, 1, 1); - layout.addChild(new BackgroundRenderer(), 0, 1); - LinearLayout linearLayout = layout.addChild(LinearLayout.vertical(), 0, 1, p -> p.alignHorizontallyRight().paddingRight(3).paddingVertical(3)); + + LinearLayout linearLayout = LinearLayout.vertical(); linearLayout.addChild(glintButton); linearLayout.addChild(Button.builder(Component.translatable("skyblocker.customization.item.selectModel"), _ -> { Minecraft minecraft = Minecraft.getInstance(); @@ -87,25 +96,34 @@ public ItemTab(CustomizeScreen parentScreen) { minecraft.gui.setScreen(new ModelSelectionPopup(parentScreen, applyItemModel)); }).width(120).build(), p -> p.paddingTop(4)); linearLayout.addChild(modelField); + layout.addChild(new BackgroundRenderer(linearLayout, 3), 0, 1); + layout.addChild(linearLayout, 0, 1, p -> p.alignHorizontallyRight().paddingRight(3).paddingVertical(3)); + + LinearLayout nameLayout = LinearLayout.vertical().spacing(2); + nameLayout.addChild(nameWidget); + nameLayout.addChild(new StringWidget(20 * 16, parentScreen.getFont().lineHeight, Component.translatable("skyblocker.customItemNames.screen.howToRemove").withStyle(ChatFormatting.ITALIC, ChatFormatting.GRAY), parentScreen.getFont())); + layout.addChild(new BackgroundRenderer(nameLayout, 3), 1, 0); + layout.addChild(nameLayout, 1, 0, 1, 2, p -> p.padding(3)); - layout.addChild(nameWidget, 1, 0, 1, 2); LocalPlayer player = Minecraft.getInstance().player; - ItemStack handStack = player.getMainHandItem(); - if (!handStack.getUuid().isEmpty()) { - setCurrentItem(handStack); - return; - } - for (ItemStack stack : (Utils.isInTheRift() ? SkyblockInventoryScreen.equipment_rift : SkyblockInventoryScreen.equipment)) { - if (!stack.getUuid().isEmpty()) { - setCurrentItem(stack); + if (player != null) { + ItemStack handStack = player.getMainHandItem(); + if (!handStack.getUuid().isEmpty()) { + setCurrentItem(handStack); return; } - } - for (ItemStack stack : player.getInventory()) { - if (!stack.getUuid().isEmpty()) { - setCurrentItem(stack); - return; + for (ItemStack stack : (Utils.isInTheRift() ? SkyblockInventoryScreen.equipment_rift : SkyblockInventoryScreen.equipment)) { + if (!stack.getUuid().isEmpty()) { + setCurrentItem(stack); + return; + } + } + for (ItemStack stack : player.getInventory()) { + if (!stack.getUuid().isEmpty()) { + setCurrentItem(stack); + return; + } } } visitChildren(clickableWidget -> clickableWidget.visible = false); @@ -120,7 +138,7 @@ private void setCurrentItem(ItemStack itemStack) { visitChildren(clickableWidget -> clickableWidget.visible = !empty); if (empty) return; parentScreen.backupConfigs(itemStack); - nameWidget.setItem(itemStack); + nameWidget.setText(itemStack.getHoverName().copy(), false); if (SkyblockerConfigManager.get().general.customItemModel.containsKey(uuid)) { Identifier identifier = SkyblockerConfigManager.get().general.customItemModel.get(uuid); String string = identifier.toString(); @@ -217,23 +235,25 @@ protected void updateWidgetNarration(NarrationElementOutput builder) { } } - private class BackgroundRenderer extends AbstractWidget { + private static class BackgroundRenderer extends AbstractWidget { + private final Layout layout; + private final int padding; - BackgroundRenderer() { + BackgroundRenderer(Layout layout, int padding) { super(0, 0, 0, 0, Component.empty()); active = false; + this.layout = layout; + this.padding = padding; } @Override protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { - int x = glintButton.getX() - 3; - int y = glintButton.getY() - 3; graphics.blitSprite(RenderPipelines.GUI_TEXTURED, INNER_SPACE_TEXTURE, - x, - y, - modelField.getRight() + 3 - x, - modelField.getBottom() + 3 - y + layout.getX() - padding, + layout.getY() - padding, + layout.getWidth() + padding * 2, + layout.getHeight() + padding * 2 ); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNav.java b/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNav.java index 2145b3141c8..fdcd4e8bd3f 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNav.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNav.java @@ -1,26 +1,38 @@ package de.hysky.skyblocker.skyblock.quicknav; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.annotations.Init; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.config.configs.QuickNavigationConfig; +import de.hysky.skyblocker.config.screens.quicknav.QuickNavConfigScreen; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.datafixer.ItemStackComponentizationFixer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; -import java.util.regex.PatternSyntaxException; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.command.v2.ClientCommands; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.player.LocalPlayer; import net.minecraft.network.chat.Component; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.PatternSyntaxException; public class QuickNav { static final Logger LOGGER = LoggerFactory.getLogger(QuickNav.class); + @Init + public static void commandInit() { + ClientCommandRegistrationCallback.EVENT.register((dispatcher, _) -> dispatcher.register(ClientCommands.literal(SkyblockerMod.NAMESPACE).then( + ClientCommands.literal("quickNav").executes(Scheduler.queueOpenScreenCommand(QuickNavConfigScreen::new))))); + } + public static List init(String screenTitle) { List buttons = new ArrayList<>(); QuickNavigationConfig data = SkyblockerConfigManager.get().quickNav; @@ -47,6 +59,7 @@ public static List init(String screenTitle) { private static QuickNavButton parseButton(QuickNavigationConfig.QuickNavItem buttonInfo, String screenTitle, int id) throws CommandSyntaxException { QuickNavigationConfig.ItemData itemData = buttonInfo.itemData; + @SuppressWarnings("ConstantValue") ItemStack stack = itemData != null && itemData.item != null && itemData.components != null ? ItemStackComponentizationFixer.fromComponentsString(itemData.item.toString(), Math.clamp(itemData.count, 1, 99), itemData.components) : new ItemStack(Items.BARRIER); boolean uiTitleMatches = false; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNavButton.java b/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNavButton.java index 615ebf1d9ee..d4538e89182 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNavButton.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNavButton.java @@ -42,11 +42,11 @@ public class QuickNavButton extends AbstractWidget { @SuppressWarnings("unchecked") private static final @Nullable FallbackedTexture[] TAB_TEXTURES_SELECTED = new FallbackedTexture[14]; - private final int index; + protected final int index; private final boolean toggled; private final String command; private final ItemStack icon; - protected final Tooltip tooltip; + protected final @Nullable Tooltip tooltip; private boolean temporaryToggled = false; private long toggleTime; @@ -92,7 +92,7 @@ public QuickNavButton(int index, boolean toggled, String command, ItemStack icon this.command = command; this.icon = icon; this.toggleTime = 0; - if (tooltip == null || tooltip.isEmpty()) { + if (tooltip.isEmpty()) { this.tooltip = null; return; } @@ -106,7 +106,7 @@ public QuickNavButton(int index, boolean toggled, String command, ItemStack icon setTooltipDelay(Duration.ofMillis(100)); } - private void updateCoordinates() { + protected void updateCoordinates() { Screen screen = Minecraft.getInstance().gui.screen(); while (screen instanceof PopupScreen || screen instanceof AbstractPopupScreen) { if (screen instanceof PopupScreen) { @@ -127,11 +127,15 @@ private void updateCoordinates() { int h = accessibleScreen.getImageHeight(); if (handledScreen instanceof ContainerScreen) h--; // they messed up the height on these. int w = accessibleScreen.getImageWidth(); - this.setX(x + this.index % 7 * 25 + w / 2 - 176 / 2); - this.setY(this.index < 7 ? y - 28 : y + h - 4); + setPositionFrom(x, y, w, h); } } + protected void setPositionFrom(int backgroundX, int backgroundY, int imageWidth, int imageHeight) { + this.setX(backgroundX + this.index % 7 * 25 + imageWidth / 2 - 176 / 2); + this.setY(this.index < 7 ? backgroundY - 28 : backgroundY + imageHeight - 4); + } + /** * Handles click events. If the button is not currently toggled, * it sets the toggled state to true and sends a message with the command after cooldown. @@ -141,7 +145,7 @@ public void onClick(MouseButtonEvent click, boolean doubled) { if (!this.temporaryToggled) { this.temporaryToggled = true; this.toggleTime = System.currentTimeMillis(); - if (command == null || command.isEmpty()) { + if (command.isEmpty() && Minecraft.getInstance().player != null) { Minecraft.getInstance().player.sendSystemMessage(Constants.PREFIX.get().append(Component.literal("Quick Nav button index " + (index + 1) + " has no command!").withStyle(ChatFormatting.RED))); } else { MessageScheduler.INSTANCE.sendMessageAfterCooldown(command, true); diff --git a/src/main/java/de/hysky/skyblocker/utils/command/CommandUtils.java b/src/main/java/de/hysky/skyblocker/utils/command/CommandUtils.java index d0c8f502b4d..606b0f195ca 100644 --- a/src/main/java/de/hysky/skyblocker/utils/command/CommandUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/command/CommandUtils.java @@ -1,8 +1,114 @@ package de.hysky.skyblocker.utils.command; import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.ParseResults; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.tree.RootCommandNode; +import de.hysky.skyblocker.utils.RegistryUtils; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientSuggestionProvider; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.Commands; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.permissions.PermissionSet; +import net.minecraft.world.flag.FeatureFlagSet; +import net.minecraft.world.level.Level; +import org.jspecify.annotations.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; public final class CommandUtils { + private static @Nullable CommandDispatcher offlineCommandDispatcher; + private static @Nullable ClientSuggestionProvider offlineSuggestionProvider; public static final Command noOp = _ -> -1; + + public static CommandBuildContext newContext() { + LocalPlayer player = Minecraft.getInstance().player; + if (player == null) return CommandBuildContext.simple(RegistryUtils.getRegistryWrapperLookup(), FeatureFlagSet.of()); + return CommandBuildContext.simple(player.connection.registryAccess(), player.connection.enabledFeatures()); + } + + public static CommandDispatcher getOfflineCommandDispatcher() { + if (offlineCommandDispatcher == null) { + CommandBuildContext context = newContext(); + RootCommandNode root = unsafeCast(new Commands(Commands.CommandSelection.ALL, context).getDispatcher().getRoot()); + OfflineCommandDispatcher dispatcher = new OfflineCommandDispatcher<>(root); + ClientCommandRegistrationCallback.EVENT.invoker().register(unsafeCast(dispatcher), context); + offlineCommandDispatcher = dispatcher; + } + return offlineCommandDispatcher; + } + + public static ClientSuggestionProvider getOfflineSuggestionProvider() { + if (offlineSuggestionProvider == null) { + offlineSuggestionProvider = new FakeClientSuggestionProvider(); + } + return offlineSuggestionProvider; + } + + @SuppressWarnings("unchecked") + private static T unsafeCast(Object o) { + return (T) o; + } + + + private static class FakeClientSuggestionProvider extends ClientSuggestionProvider { + private final RegistryAccess registryAccess; + + private FakeClientSuggestionProvider() { + // maybe dangerous? either that or 20 new imports to create a fake connection + super(null, Minecraft.getInstance(), PermissionSet.NO_PERMISSIONS); + this.registryAccess = RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); + } + + @Override + public Set> levels() { + return Set.of(); + } + + @Override + public Collection getOnlinePlayerNames() { + return List.of(); + } + + @Override + public RegistryAccess registryAccess() { + return registryAccess; + } + + @Override + public FeatureFlagSet enabledFeatures() { + return FeatureFlagSet.of(); + } + + @Override + public Collection getAllTeams() { + return List.of(); + } + + @Override + public CompletableFuture customSuggestion(CommandContext context) { + return Suggestions.empty(); + } + } + + private static class OfflineCommandDispatcher extends CommandDispatcher { + private OfflineCommandDispatcher(RootCommandNode root) { + super(root); + } + @Override + public int execute(ParseResults parse) { + throw new UnsupportedOperationException(); + } + } } diff --git a/src/main/java/de/hysky/skyblocker/utils/command/argumenttypes/RegexArgumentType.java b/src/main/java/de/hysky/skyblocker/utils/command/argumenttypes/RegexArgumentType.java new file mode 100644 index 00000000000..b0d8d5dfc74 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/command/argumenttypes/RegexArgumentType.java @@ -0,0 +1,28 @@ +package de.hysky.skyblocker.utils.command.argumenttypes; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.exceptions.CommandExceptionType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * Greedy. + */ +public class RegexArgumentType implements ArgumentType { + public static final CommandExceptionType EXCEPTION_TYPE = new CommandExceptionType() {}; + @Override + public Pattern parse(StringReader reader) throws CommandSyntaxException { + int cursor = reader.getCursor(); + try { + Pattern compile = Pattern.compile(reader.getRemaining()); + reader.setCursor(reader.getTotalLength()); + return compile; + } catch (PatternSyntaxException e) { + reader.setCursor(cursor); + throw new CommandSyntaxException(EXCEPTION_TYPE, e::getDescription, reader.getRemaining(), e.getIndex()); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/command/suggestions/TextFieldSuggestions.java b/src/main/java/de/hysky/skyblocker/utils/command/suggestions/TextFieldSuggestions.java new file mode 100644 index 00000000000..9c2b140ee00 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/command/suggestions/TextFieldSuggestions.java @@ -0,0 +1,156 @@ +package de.hysky.skyblocker.utils.command.suggestions; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.context.SuggestionContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.tree.CommandNode; +import de.hysky.skyblocker.utils.command.CommandUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.CommandSuggestions; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.multiplayer.ClientSuggestionProvider; +import net.minecraft.client.renderer.Rect2i; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Style; +import net.minecraft.util.ARGB; +import net.minecraft.util.FormattedCharSequence; +import org.jspecify.annotations.Nullable; + +import java.util.List; +import java.util.Optional; + +/** + * A modified version of {@link CommandSuggestions} that doesn't have hardcoded positions + * and that allows to use a specific command node instead of the vanilla dispatcher. + */ +public class TextFieldSuggestions extends CommandSuggestions { + + private final CommandDispatcher dispatcher; + private final Mode mode; + private @Nullable CommandContext context; + + protected TextFieldSuggestions(Minecraft minecraft, Screen screen, EditBox input, Font font, boolean onlyShowIfCursorPastError, int suggestionLineLimit, @Nullable CommandNode node, boolean commandOnly) { + super(minecraft, screen, input, font, true, onlyShowIfCursorPastError, 0, suggestionLineLimit, false, ARGB.black(0.5f)); + if (node != null) { + this.dispatcher = new CommandDispatcher<>(); + this.dispatcher.getRoot().addChild(node); + this.mode = Mode.CUSTOM_NODE; + } else { + this.dispatcher = minecraft.player == null ? CommandUtils.getOfflineCommandDispatcher() : minecraft.player.connection.getCommands(); + this.mode = commandOnly ? Mode.VANILLA_COMMANDS_ONLY : Mode.VANILLA; + } + } + + /** + * Creates a text field suggestions with a specific node and children as the parser + */ + public static TextFieldSuggestions ofSpecificNode(Minecraft minecraft, Screen screen, EditBox input, Font font, boolean onlyShowIfCursorPastError, int suggestionLineLimit, CommandNode node) { + return new TextFieldSuggestions(minecraft, screen, input, font, onlyShowIfCursorPastError, suggestionLineLimit, node, true); + } + + /** + * Creates a text field suggestions that suggests like chat or a command block. Basically the same as {@link CommandSuggestions} but without hard coded positions and chat restrictions. + */ + public static TextFieldSuggestions ofVanillaDispatcher(Minecraft minecraft, Screen screen, EditBox input, Font font, boolean onlyShowIfCursorPastError, int suggestionLineLimit, boolean commandOnly) { + return new TextFieldSuggestions(minecraft, screen, input, font, onlyShowIfCursorPastError, suggestionLineLimit, null, commandOnly); + } + + @Override + public void showSuggestions(boolean immediateNarration) { + super.showSuggestions(immediateNarration); + updatePosition(); + } + + public final void updatePosition() { + if (suggestions != null) { + suggestions.rect = new Rect2i( + suggestions.rect.getX(), + input.getY() - suggestions.rect.getHeight(), + suggestions.rect.getWidth(), + suggestions.rect.getHeight() + ); + } + } + + /** + * Overridden to change the position. + */ + @Override + public void extractUsage(GuiGraphicsExtractor graphics) { + int y = 0; + + for (FormattedCharSequence line : this.commandUsage) { + int lineY = this.input.getBottom() + 12 * y; + graphics.fill(commandUsagePosition - 1, lineY, this.commandUsagePosition + this.commandUsageWidth + 1, lineY + 12, this.fillColor); + graphics.text(this.font, line, this.commandUsagePosition, lineY + 2, -1); + y++; + } + } + + public Optional getArgument(String argument, Class type) { + return Optional.ofNullable(context).map(c -> c.getArgument(argument, type)); + } + + @Override + public void updateCommandInfo() { + String command = this.input.getValue(); + if (this.currentParse != null && !this.currentParse.getReader().getString().equals(command)) { + this.currentParse = null; + this.context = null; + } + + if (!this.keepSuggestions) { + this.input.setSuggestion(null); + this.suggestions = null; + } + + commandUsage.clear(); + StringReader reader = new StringReader(command); + boolean startsWithSlash = reader.canRead() && reader.peek() == '/' && mode != Mode.CUSTOM_NODE; // do not allow slashes at all with a custom node + if (startsWithSlash) { + reader.skip(); + } + int cursorPosition = this.input.getCursorPosition(); + if (startsWithSlash || mode != Mode.VANILLA) { + CommandDispatcher commands = dispatcher; + if (this.currentParse == null) { + this.currentParse = commands.parse(reader, minecraft.player != null ? minecraft.player.connection.getSuggestionsProvider() : CommandUtils.getOfflineSuggestionProvider()); + CommandSyntaxException parseException = Commands.getParseException(currentParse); + if (mode == Mode.CUSTOM_NODE && parseException != null && parseException.getType() == CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument() && command.endsWith(" ")) { + input.setValue(command.trim()); + return; + } + if (currentParse.getExceptions().isEmpty()) this.context = currentParse.getContext().build(command); + } + + int parseStart = this.onlyShowIfCursorPastError ? reader.getCursor() : 1; + if (cursorPosition >= parseStart && (this.suggestions == null || !this.keepSuggestions)) { + this.pendingSuggestions = commands.getCompletionSuggestions(this.currentParse, cursorPosition); + this.pendingSuggestions.thenAccept(suggestionResult -> { + if (this.pendingSuggestions.isDone()) { + this.updateUsageInfo(this.currentParse, suggestionResult); + } + }); + } + } + } + + /** + * Overridden to hide the {@code } usage hint that shows up if your cursor is at the end + */ + @Override + protected List fillNodeUsage(SuggestionContext suggestionContext, Style usageFormat) { + return mode != Mode.CUSTOM_NODE ? super.fillNodeUsage(suggestionContext, usageFormat) : List.of(); + } + + private enum Mode { + VANILLA, + VANILLA_COMMANDS_ONLY, + CUSTOM_NODE + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/command/suggestions/package-info.java b/src/main/java/de/hysky/skyblocker/utils/command/suggestions/package-info.java new file mode 100644 index 00000000000..e4f64e725a3 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/command/suggestions/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package de.hysky.skyblocker.utils.command.suggestions; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractPopupScreen.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractPopupScreen.java index e244d6fdf8e..10ed43dea43 100644 --- a/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractPopupScreen.java +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractPopupScreen.java @@ -1,5 +1,6 @@ package de.hysky.skyblocker.utils.render.gui; +import de.hysky.skyblocker.SkyblockerMod; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.components.EditBox; @@ -14,7 +15,9 @@ * A more bare-bones version of Vanilla's Popup Screen. Meant to be extended. */ public class AbstractPopupScreen extends Screen { + protected static final int BACKGROUND_MARGIN = 18; private static final Identifier BACKGROUND_TEXTURE = Identifier.withDefaultNamespace("popup/background"); + private static final Identifier LIGHTER_BACKGROUND_TEXTURE = SkyblockerMod.id("background"); public final Screen backgroundScreen; protected AbstractPopupScreen(Component title, Screen backgroundScreen) { @@ -40,7 +43,11 @@ public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mou * These are the inner positions and size of the popup, not outer */ public static void extractPopupBackground(GuiGraphicsExtractor graphics, int x, int y, int width, int height) { - graphics.blitSprite(RenderPipelines.GUI_TEXTURED, BACKGROUND_TEXTURE, x - 18, y - 18, width + 36, height + 36); + extractPopupBackground(graphics, x, y, width, height, false); + } + + public static void extractPopupBackground(GuiGraphicsExtractor graphics, int x, int y, int width, int height, boolean light) { + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, light ? LIGHTER_BACKGROUND_TEXTURE : BACKGROUND_TEXTURE, x - BACKGROUND_MARGIN, y - BACKGROUND_MARGIN, width + BACKGROUND_MARGIN * 2, height + BACKGROUND_MARGIN * 2); } @Override diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/CustomizeNameWidget.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ComponentEditWidget.java similarity index 89% rename from src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/CustomizeNameWidget.java rename to src/main/java/de/hysky/skyblocker/utils/render/gui/ComponentEditWidget.java index bfdf871b310..a3f35203ee5 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/CustomizeNameWidget.java +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ComponentEditWidget.java @@ -1,12 +1,11 @@ -package de.hysky.skyblocker.skyblock.item.custom.screen.name; +package de.hysky.skyblocker.utils.render.gui; import com.google.common.collect.ImmutableList; import com.mojang.blaze3d.platform.InputConstants; import com.mojang.blaze3d.platform.cursor.CursorTypes; -import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.config.ConfigUtils; -import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.debug.Debug; +import de.hysky.skyblocker.skyblock.item.custom.screen.name.ColorPopup; import de.hysky.skyblocker.skyblock.item.custom.screen.name.visitor.GetClickedPositionVisitor; import de.hysky.skyblocker.skyblock.item.custom.screen.name.visitor.GetRenderWidthVisitor; import de.hysky.skyblocker.skyblock.item.custom.screen.name.visitor.GetStyleVisitor; @@ -37,12 +36,10 @@ import net.minecraft.client.input.KeyEvent; import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.client.input.PreeditEvent; -import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.client.sounds.SoundManager; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Style; import net.minecraft.network.chat.TextColor; -import net.minecraft.resources.Identifier; import net.minecraft.util.ARGB; import net.minecraft.util.CommonColors; import net.minecraft.util.StringUtil; @@ -51,15 +48,15 @@ import org.jspecify.annotations.Nullable; import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; import java.util.function.Predicate; -public class CustomizeNameWidget extends AbstractContainerWidget { - private static final Identifier INNER_SPACE_TEXTURE = SkyblockerMod.id("menu_inner_space"); - private static final int PADDING = 3; +public class ComponentEditWidget extends AbstractContainerWidget { private final Minecraft client = Minecraft.getInstance(); private final Font textRenderer = client.font; - private String uuid = ""; + private final Consumer responder; private Component text = Component.empty(); private String textString = ""; @@ -76,9 +73,10 @@ public class CustomizeNameWidget extends AbstractContainerWidget { private @Nullable Style insertAs; - public CustomizeNameWidget(Screen parent) { - super(0, 0, 0, 0, Component.literal("Customize Item Name"), AbstractScrollArea.defaultSettings(4)); + public ComponentEditWidget(Screen parent, Component message, Consumer responder) { + super(0, 0, 0, 0, message, AbstractScrollArea.defaultSettings(4)); ImmutableList.Builder builder = ImmutableList.builder(); + this.responder = responder; // the gui is a grid of 20 columns, should be 16 px each textField = grid.addChild(new TextField(), 1, 0, 1, 20); builder.add(textField); @@ -98,12 +96,11 @@ public CustomizeNameWidget(Screen parent) { builder.add(grid.addChild(Button.builder(Component.translatable("skyblocker.customItemNames.screen.gradientColor"), _ -> client.gui.setScreen(ColorPopup.createGradient(parent, this::createGradient)) ).size(48, 16).build(), 3, 17, 1, 3)); - builder.add(grid.addChild(new StringWidget(20 * 16, textRenderer.lineHeight, Component.translatable("skyblocker.customItemNames.screen.howToRemove").withStyle(ChatFormatting.ITALIC, ChatFormatting.GRAY), textRenderer)/*.alignLeft()*/, 4, 0, 1, 20, LayoutSettings.defaults().paddingTop(2))); - builder.add(previewWidget = grid.addChild(new StringWidget(20 * 16, textRenderer.lineHeight, Component.empty(), textRenderer).setMaxWidth(20 * 16, StringWidget.TextOverflow.SCROLLING), 5, 0, 1, 20, LayoutSettings.defaults().paddingVertical(2).alignHorizontallyCenter())); + builder.add(previewWidget = grid.addChild(new StringWidget(20 * 16, textRenderer.lineHeight, Component.empty(), textRenderer).setMaxWidth(20 * 16, StringWidget.TextOverflow.SCROLLING), 4, 0, 1, 20, LayoutSettings.defaults().paddingVertical(2).alignHorizontallyCenter())); widgets = builder.build(); grid.arrangeElements(); - grid.setPosition(getX() + PADDING, getY() + PADDING); - setSize(grid.getWidth() + PADDING * 2, grid.getHeight() + PADDING * 2); + grid.setPosition(getX(), getY()); + setSize(grid.getWidth(), grid.getHeight()); selectionStart = selectionEnd = textString.length(); } @@ -125,32 +122,24 @@ private void addFormattingButtons(ImmutableList.Builder builder) @Override public void setX(int x) { super.setX(x); - grid.setX(getX() + PADDING); + grid.setX(getX()); } @Override public void setY(int y) { super.setY(y); - grid.setY(getY() + PADDING); + grid.setY(getY()); } public void setItem(ItemStack stack) { - uuid = stack.getUuid(); setText(stack.getHoverName().copy(), false); } @Override protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float deltaTicks) { - graphics.blitSprite( - RenderPipelines.GUI_TEXTURED, - INNER_SPACE_TEXTURE, - getX(), - getY(), - getWidth(), - getHeight()); if (Debug.debugEnabled()) { - graphics.text(textRenderer, Component.literal("Selection Start: " + selectionStart + ", Selection End: " + selectionEnd), getX(), getBottom(), -1); - graphics.text(textRenderer, Component.literal("Insert Style: " + (insertAs == null ? "null" : insertAs.toString())), getX(), getBottom() + 10, -1); + graphics.text(textRenderer, Component.literal("Selection Start: " + selectionStart + ", Selection End: " + selectionEnd), getX() + getWidth() / 3, getY(), -1); + graphics.text(textRenderer, Component.literal("Insert Style: " + (insertAs == null ? "null" : insertAs.toString())), getX(), getY() + 56, -1); } for (AbstractWidget widget : widgets) { widget.extractRenderState(graphics, mouseX, mouseY, deltaTicks); @@ -158,7 +147,7 @@ protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouse } /** - * Creates a gradient that goes from {@link CustomizeNameWidget#selectionStart} to {@link CustomizeNameWidget#selectionEnd} + * Creates a gradient that goes from {@link ComponentEditWidget#selectionStart} to {@link ComponentEditWidget#selectionEnd} * * @param startColor the color at the start of the gradient * @param endColor the color at the end of the gradient @@ -213,20 +202,14 @@ private void updateStyleButtons() { * * @param text the text to set */ - public void setText(Component text, boolean updateConfig) { + public void setText(Component text, boolean callCallback) { this.text = text; textString = text.getString(); - if (updateConfig && !uuid.isEmpty()) { - SkyblockerConfigManager.updateOnly(config -> { - if (textString.isBlank()) config.general.customItemNames.remove(uuid); - else config.general.customItemNames.put(uuid, text.copy().setStyle(Style.EMPTY.withItalic(false).withColor(ChatFormatting.WHITE))); - }); - } + if (callCallback) responder.accept(text); previewWidget.setMessage(text); grid.arrangeElements(); - // called before init - if (textField != null) textField.updateMePrettyPlease = true; + textField.updateMePrettyPlease = true; } public void setText(Component text) { @@ -337,11 +320,6 @@ private int getWordSkipPosition(boolean left) { return i; } - @Override - public void setFocused(boolean focused) { - if (!focused) setFocused(null); - } - private class FormattingButton extends AbstractButton { private boolean enabled; private final ChatFormatting format; @@ -394,11 +372,14 @@ private class ColorButton extends AbstractButton { private final ChatFormatting color; private final int intColor; + /** + * @param format Callers should ensure that this is a color. + */ private ColorButton(ChatFormatting format) { super(0, 0, 16, 16, ConfigUtils.FORMATTING_FORMATTER.apply(format)); setTooltip(Tooltip.create(getMessage())); this.color = format; - this.intColor = ARGB.opaque(SkyBlockColors.fromVanilla(TextColor.fromLegacyFormat(color).getValue())); + this.intColor = ARGB.opaque(SkyBlockColors.fromVanilla(Objects.requireNonNull(TextColor.fromLegacyFormat(color)).getValue())); } @Override @@ -484,7 +465,6 @@ public boolean keyPressed(KeyEvent input) { default -> captured = false; } if (captured) return true; - assert client != null; if (input.isSelectAll()) { selectionStart = 0; selectionEnd = textString.length(); @@ -522,7 +502,7 @@ public boolean charTyped(CharacterEvent input) { @Override public boolean preeditUpdated(@Nullable final PreeditEvent event) { if (this.isActive()) { - this.preeditOverlay = event != null ? new IMEPreeditOverlay(event, CustomizeNameWidget.this.textRenderer, 9 + 1) : null; + this.preeditOverlay = event != null ? new IMEPreeditOverlay(event, ComponentEditWidget.this.textRenderer, 9 + 1) : null; return true; } diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/SuggestionsEditBox.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/SuggestionsEditBox.java new file mode 100644 index 00000000000..2e2803cc74a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/SuggestionsEditBox.java @@ -0,0 +1,282 @@ +package de.hysky.skyblocker.utils.render.gui; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.CommandNode; +import de.hysky.skyblocker.utils.command.suggestions.TextFieldSuggestions; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.multiplayer.ClientSuggestionProvider; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; +import java.util.function.Consumer; + +/// An edit box that can display suggestions using brigadier. +/// +/// It can be used to suggest an entire command (like in chat or a command block) +/// or to suggest something more specific like an item stack or an identifier using an argument +public class SuggestionsEditBox extends EditBox { + + private final TextFieldSuggestions suggestions; + private final @Nullable String argument; + private @Nullable Consumer responder; + + protected SuggestionsEditBox(Minecraft minecraft, Screen screen, Font font, int width, int height, Component narration, boolean onlyShowIfCursorPastError, int suggestionLineLimit, @Nullable CommandNode node, boolean commandsOnly) { + super(font, width, height, narration); + if (node != null) { + suggestions = TextFieldSuggestions.ofSpecificNode(minecraft, screen, this, font, onlyShowIfCursorPastError, suggestionLineLimit, node); + } else { + suggestions = TextFieldSuggestions.ofVanillaDispatcher(minecraft, screen, this, font, onlyShowIfCursorPastError, suggestionLineLimit, commandsOnly); + } + suggestions.setAllowSuggestions(true); + super.setResponder(this::onUpdate); + if (node instanceof ArgumentCommandNode argumentCommandNode) argument = argumentCommandNode.getName(); + else argument = null; + Screens.getWidgets(screen).addFirst(new SuggestionsWrapper()); + } + + @Override + public void setResponder(Consumer responder) { + this.responder = responder; + } + + public boolean isValid() { + return suggestions.currentParse != null && suggestions.currentParse.getExceptions().isEmpty(); + } + + private void onUpdate(String string) { + suggestions.updateCommandInfo(); + if (responder != null) responder.accept(string); + } + + public Optional getParsedValue(Class type) { + return argument == null ? Optional.empty() : getParsedValue(argument, type); + } + + public Optional getParsedValue(String argument, Class type) { + return suggestions.getArgument(argument, type); + } + + + @Override + public void setFocused(boolean focused) { + super.setFocused(focused); + if (!focused) suggestions.hide(); + else suggestions.updateCommandInfo(); + } + + @Override + public void onClick(MouseButtonEvent event, boolean doubleClick) { + super.onClick(event, doubleClick); + suggestions.updateCommandInfo(); + } + + @Override + public void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + super.extractWidgetRenderState(graphics, mouseX, mouseY, a); + if (visible) { + ScreenRectangle peeked = graphics.scissorStack.peek(); + if (peeked == null || peeked.intersects(getRectangle())) + graphics.skb$addDeferredElement((graphics1, mouseX1, mouseY1, _) -> suggestions.extractRenderState(graphics1, mouseX1, mouseY1)); + } + } + + @Override + public void setX(int x) { + super.setX(x); + suggestions.updatePosition(); + } + + @Override + public void setY(int y) { + super.setY(y); + suggestions.updatePosition(); + } + + @Override + public void setWidth(int width) { + super.setWidth(width); + suggestions.updatePosition(); + } + + @Override + public void setHeight(int height) { + super.setHeight(height); + suggestions.updatePosition(); + } + + @Override + public boolean keyPressed(KeyEvent event) { + if (suggestions.keyPressed(event)) return true; + if (this.isActive() && this.isFocused() && event.key() == InputConstants.KEY_ESCAPE) { + setFocused(false); + return true; + } + return super.keyPressed(event); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private int width = 150; + private int height = 20; + private int suggestionLineLimit = 7; + private boolean onlyShowIfCursorPastError = true; + + public Builder width(int width) { + this.width = width; + return this; + } + + public Builder height(int height) { + this.height = height; + return this; + } + + public Builder suggestionLineLimit(int suggestionLineLimit) { + this.suggestionLineLimit = suggestionLineLimit; + return this; + } + + public Builder onlyShowIfCursorPastError(boolean onlyShowIfCursorPastError) { + this.onlyShowIfCursorPastError = onlyShowIfCursorPastError; + return this; + } + + /** + * @see Builder#build(Minecraft, Font, Screen, Component, CommandNode) + */ + public SuggestionsEditBox build(Screen screen, Component narration, CommandNode node) { + return build(Minecraft.getInstance(), Minecraft.getInstance().font, screen, narration, node); + } + + /** + * @see Builder#build(Minecraft, Font, Screen, Component, CommandNode) + */ + public SuggestionsEditBox build(Minecraft minecraft, Font font, Screen screen, Component narration, ArgumentBuilder builder) { + return build(minecraft, font, screen, narration, builder.build()); + } + + /** + * Builds a suggestion edit box with the specified node.
+ * Note: if it is an argument node with no children it is recommended to use {@link Builder#buildArg(Minecraft, Font, Screen, Component, ArgumentType)} + * that has methods to get the parsed value directly. + */ + public SuggestionsEditBox build(Minecraft minecraft, Font font, Screen screen, Component narration, CommandNode node) { + return new SuggestionsEditBox(minecraft, screen, font, width, height, narration, onlyShowIfCursorPastError, suggestionLineLimit, node, true); + } + + /** + * Builds a suggestion edit box with a specific argument type as the parser and suggestions provider.
+ * Has methods to get the parsed value directly thanks to the generic type. + */ + public Argument buildArg(Minecraft minecraft, Font font, Screen screen, Component narration, ArgumentType argumentType) { + return new Argument<>(minecraft, screen, font, width, height, narration, onlyShowIfCursorPastError, suggestionLineLimit, argumentType); + } + + /** + * Builds a suggestion edit box that pretty much acts as if it was chat or a command block and suggests all commands. + */ + public SuggestionsEditBox buildVanillaDispatcher(Minecraft minecraft, Font font, Screen screen, Component narration, boolean commandsOnly) { + return new SuggestionsEditBox(minecraft, screen, font, width, height, narration, onlyShowIfCursorPastError, suggestionLineLimit, null, commandsOnly); + } + } + + /** + * A suggestion edit box based on an argument type. Has methods to get the parsed value directly. + * @param The edit box's parsed type + * @see Builder#buildArg(Minecraft, Font, Screen, Component, ArgumentType) + */ + public static class Argument extends SuggestionsEditBox { + + protected Argument(Minecraft minecraft, Screen screen, Font font, int width, int height, Component narration, boolean onlyShowIfCursorPastError, int suggestionLineLimit, ArgumentType argumentType) { + super(minecraft, screen, font, width, height, narration, onlyShowIfCursorPastError, suggestionLineLimit, RequiredArgumentBuilder.argument("argument", argumentType).build(), true); + + } + + public Optional getParsedValue() { + return (Optional) getParsedValue(Object.class); + } + + /** + * This responder will only get called if the value is valid and parsed correctly. + */ + public void setValueResponder(Consumer valueResponder) { + setResponder(_ -> getParsedValue().ifPresent(valueResponder)); + } + + /** + * This responder will get called even if the value isn't valid and couldn't be parsed. In those cases it will receive {@code null} + */ + public void setOptionalValueResponder(Consumer<@Nullable T> optionalValueResponder) { + setResponder(_ -> optionalValueResponder.accept(getParsedValue().orElse(null))); + } + } + + /** + * Small wrapper so suggestions box interactions take priority + */ + private class SuggestionsWrapper extends AbstractWidget { + + private SuggestionsWrapper() { + super(0, 0, 0, 0, Component.empty()); + } + + @Override + protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + + } + + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) { + return suggestions.mouseClicked(event); + } + + @Override + public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { + return suggestions.mouseScrolled(scrollY); + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return (suggestions.suggestions != null && suggestions.suggestions.rect.contains((int) mouseX, (int) mouseY)); + } + + @Override + public void setFocused(boolean focused) { + + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput output) { + + } + + @Override + public boolean isFocused() { + return false; + } + + @Override + public boolean shouldTakeFocusAfterInteraction() { + return false; + } + + } +} diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index e62fcc2d08c..3b8fd99df35 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -1076,6 +1076,8 @@ "skyblocker.config.quickNav.button.chooseSkyblockItem.@Tooltip": "Select a SkyBlock icon or item to fill this button with.", "skyblocker.config.quickNav.button.clickEvent": "Click Event", "skyblocker.config.quickNav.button.doubleClick": "Require Double Click", + "skyblocker.config.quickNav.button.doubleClick.@Tooltip": "Useful to avoid miss-clicks with warp buttons.", + "skyblocker.config.quickNav.button.icon": "Icon", "skyblocker.config.quickNav.button.item.components": "Item Components", "skyblocker.config.quickNav.button.item.components.@Tooltip": "A string of item components enclosed in square brackets, which is the same format as the /give command.\n\nExample: [minecraft:enchantment_glint_override=true]", "skyblocker.config.quickNav.button.item.count": "Item Count", @@ -1086,6 +1088,9 @@ "skyblocker.config.quickNav.button.uiTitle": "UI Title", "skyblocker.config.quickNav.enableQuickNav": "Enable Quick Navigation", "skyblocker.config.quickNav.enableQuickNav.@Tooltip": "Adds 14 customizable inventory buttons that can be styled and run a command when clicked.", + "skyblocker.config.quickNav.quickNavConfigScreen": "Open Configuration Screen", + "skyblocker.config.quickNav.screen.count": "Count", + "skyblocker.config.quickNav.screen.title": "Editing Button %s", "skyblocker.config.shortcutToKeybindsSettings": "Edit Keybind", "skyblocker.config.shortcutToKeybindsSettings.@Text": "Keybinds Menu", diff --git a/src/main/resources/skyblocker.classtweaker b/src/main/resources/skyblocker.classtweaker index d6f80c3bce3..08301f74c15 100644 --- a/src/main/resources/skyblocker.classtweaker +++ b/src/main/resources/skyblocker.classtweaker @@ -76,3 +76,22 @@ inject-interface net/minecraft/client/gui/screens/inventory/InventoryScreen de/h inject-interface net/minecraft/client/renderer/LevelRenderer de/hysky/skyblocker/injected/EntityRenderMarker inject-interface net/minecraft/client/renderer/feature/ItemFeatureRenderer$Submit de/hysky/skyblocker/injected/CustomGlowState inject-interface net/minecraft/client/renderer/rendertype/RenderType de/hysky/skyblocker/injected/GlowRenderTypeHolder +inject-interface net/minecraft/client/gui/GuiGraphicsExtractor de/hysky/skyblocker/injected/SkyblockerGraphicsExtractor + +# Suggestions +accessible field net/minecraft/client/gui/components/CommandSuggestions input Lnet/minecraft/client/gui/components/EditBox; +accessible field net/minecraft/client/gui/components/CommandSuggestions currentParse Lcom/mojang/brigadier/ParseResults; +accessible field net/minecraft/client/gui/components/CommandSuggestions keepSuggestions Z +accessible field net/minecraft/client/gui/components/CommandSuggestions minecraft Lnet/minecraft/client/Minecraft; +accessible field net/minecraft/client/gui/components/CommandSuggestions suggestions Lnet/minecraft/client/gui/components/CommandSuggestions$SuggestionsList; +accessible field net/minecraft/client/gui/components/CommandSuggestions onlyShowIfCursorPastError Z +accessible field net/minecraft/client/gui/components/CommandSuggestions pendingSuggestions Ljava/util/concurrent/CompletableFuture; +accessible field net/minecraft/client/gui/components/CommandSuggestions commandUsage Ljava/util/List; +accessible field net/minecraft/client/gui/components/CommandSuggestions font Lnet/minecraft/client/gui/Font; +accessible field net/minecraft/client/gui/components/CommandSuggestions commandUsagePosition I +accessible field net/minecraft/client/gui/components/CommandSuggestions commandUsageWidth I +accessible field net/minecraft/client/gui/components/CommandSuggestions fillColor I +mutable field net/minecraft/client/gui/components/CommandSuggestions$SuggestionsList rect Lnet/minecraft/client/renderer/Rect2i; +accessible field net/minecraft/client/gui/components/CommandSuggestions$SuggestionsList rect Lnet/minecraft/client/renderer/Rect2i; +extendable method net/minecraft/client/gui/components/CommandSuggestions updateUsageInfo (Lcom/mojang/brigadier/ParseResults;Lcom/mojang/brigadier/suggestion/Suggestions;)V +extendable method net/minecraft/client/gui/components/CommandSuggestions fillNodeUsage (Lcom/mojang/brigadier/context/SuggestionContext;Lnet/minecraft/network/chat/Style;)Ljava/util/List; diff --git a/src/main/resources/skyblocker.mixins.json b/src/main/resources/skyblocker.mixins.json index e924fd2a860..17b7cbee8b0 100644 --- a/src/main/resources/skyblocker.mixins.json +++ b/src/main/resources/skyblocker.mixins.json @@ -6,6 +6,7 @@ "client": [ "AbstractContainerMenuMixin", "AbstractContainerScreenMixin", + "AbstractContainerWidgetMixin", "AbstractSignEditScreenMixin", "BatMixin", "BossHealthOverlayMixin",