From fe254525f2b08dd3144043222c6e46298cdb22d0 Mon Sep 17 00:00:00 2001 From: Good627 <193092356+Good627@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:02:18 +0300 Subject: [PATCH 1/5] feat: FINALV1GUILDRAIDSAFETY - Disallow readying up if not a guild raid --- src/tel/eden/mod/config/BridgeConfig.java | 3 + src/tel/eden/mod/gui/BridgeConfigScreen.java | 1 + .../mixin/AbstractContainerScreenMixin.java | 34 ++++++++++-- src/tel/eden/mod/raid/RaidGambitSafety.java | 55 +++++++++++++++++-- .../eden/mod/raid/RaidGambitSafetyTest.java | 34 ++++++++++++ 5 files changed, 116 insertions(+), 11 deletions(-) diff --git a/src/tel/eden/mod/config/BridgeConfig.java b/src/tel/eden/mod/config/BridgeConfig.java index 8ab280f..05f9fb9 100644 --- a/src/tel/eden/mod/config/BridgeConfig.java +++ b/src/tel/eden/mod/config/BridgeConfig.java @@ -271,6 +271,9 @@ public void sanitize() { /** Minimum number of gambits required to ready up normally (range 0-3, default 1). */ public int raidGambitMinimum = 1; + /** Require shift-clicking to ready up in the raid menu if the raid is not a guild raid. */ + public boolean requireGuildRaid = false; + /** Custom palette for the 10 party slots. Defaults to PartyHighlightManager.DEFAULT_PALETTE. */ public List partyColors = new ArrayList<>(tel.eden.mod.party.PartyHighlightManager.DEFAULT_PALETTE); diff --git a/src/tel/eden/mod/gui/BridgeConfigScreen.java b/src/tel/eden/mod/gui/BridgeConfigScreen.java index 24db504..dd8c606 100644 --- a/src/tel/eden/mod/gui/BridgeConfigScreen.java +++ b/src/tel/eden/mod/gui/BridgeConfigScreen.java @@ -148,6 +148,7 @@ protected void init() { addToggleRow(Category.PARTY_RAIDS, "Raid gambit ready safety", () -> config.raidGambitSafety, v -> config.raidGambitSafety = v, "On", "Off", true); IntConfigSlider gambitSlider = new IntConfigSlider(CONTROL_W, 20, 0, 3, 1, () -> config.raidGambitMinimum, v -> config.raidGambitMinimum = v, v -> Component.literal(v == 0 ? "Off (0)" : String.valueOf(v))); addSliderRow(Category.PARTY_RAIDS, "Minimum raid gambits", gambitSlider, gambitSlider::syncFromConfig, () -> config.raidGambitMinimum = 1); + addToggleRow(Category.PARTY_RAIDS, "Require guild raid to ready", () -> config.requireGuildRaid, v -> config.requireGuildRaid = v, "On", "Off", false); // War & Territory addToggleRow(Category.WAR, "Attack timers HUD", () -> config.warAttackTimers, v -> config.warAttackTimers = v, "On", "Off", true); diff --git a/src/tel/eden/mod/mixin/AbstractContainerScreenMixin.java b/src/tel/eden/mod/mixin/AbstractContainerScreenMixin.java index 0eb6e4e..f8b31c3 100644 --- a/src/tel/eden/mod/mixin/AbstractContainerScreenMixin.java +++ b/src/tel/eden/mod/mixin/AbstractContainerScreenMixin.java @@ -46,15 +46,39 @@ public abstract class AbstractContainerScreenMixin { try { if (this.hoveredSlot != null && RaidGambitSafety.isReadyClickSlot(this.hoveredSlot)) { BridgeConfig config = EdenModClient.instance().config(); - if (config != null && config.raidGambitSafety && config.raidGambitMinimum > 0) { + if (config != null) { AbstractContainerMenu menu = getMenu(); if (RaidGambitSafety.isRaidReadyMenu(menu) && !RaidGambitSafety.isPlayerAlreadyReadied(menu)) { - int enabled = RaidGambitSafety.countEnabledGambits(menu); - if (enabled < config.raidGambitMinimum) { + List warnings = new ArrayList<>(); + if (config.requireGuildRaid && !RaidGambitSafety.isGuildRaidAvailable(menu)) { + warnings.add(RaidGambitSafety.getGuildRaidTooltipWarning()); + } + if (config.raidGambitSafety && config.raidGambitMinimum > 0) { + int enabled = RaidGambitSafety.countEnabledGambits(menu); + if (enabled < config.raidGambitMinimum) { + warnings.add(RaidGambitSafety.getTooltipWarning(config.raidGambitMinimum)); + } + } + if (!warnings.isEmpty()) { List base = cir.getReturnValue(); List tooltip = new ArrayList<>(base != null ? base : List.of()); - tooltip.add(Component.empty()); - tooltip.add(RaidGambitSafety.getTooltipWarning(config.raidGambitMinimum)); + int insertIdx = -1; + for (int i = 0; i < tooltip.size(); i++) { + String text = net.minecraft.ChatFormatting.stripFormatting(tooltip.get(i).getString()).trim(); + if (text.contains("Click to Ready Up") || text.equalsIgnoreCase("Click to Ready Up")) { + insertIdx = i; + break; + } + } + if (insertIdx != -1) { + for (Component w : warnings) { + tooltip.add(insertIdx++, w); + } + tooltip.add(insertIdx, Component.empty()); + } else { + tooltip.add(Component.empty()); + tooltip.addAll(warnings); + } cir.setReturnValue(tooltip); } } diff --git a/src/tel/eden/mod/raid/RaidGambitSafety.java b/src/tel/eden/mod/raid/RaidGambitSafety.java index 9c925ba..14ce564 100644 --- a/src/tel/eden/mod/raid/RaidGambitSafety.java +++ b/src/tel/eden/mod/raid/RaidGambitSafety.java @@ -165,9 +165,31 @@ public static boolean isShiftKeyDown() { return false; } + /** Checks if the raid preparation menu has a guild raid available. */ + public static boolean isGuildRaidAvailable(AbstractContainerMenu menu) { + if (menu == null) { + return false; + } + for (int i = 0; i < Math.min(CONTAINER_SLOTS, menu.slots.size()); i++) { + ItemStack stack = menu.getSlot(i).getItem(); + if (!stack.isEmpty()) { + String name = ChatFormatting.stripFormatting(stack.getHoverName().getString()).trim(); + if (name.equalsIgnoreCase("Guild Raid Available") || name.contains("Guild Raid Available")) { + return true; + } + } + } + return false; + } + /** Evaluates whether a ready click should be blocked based on active gambits and settings. */ public static boolean evaluateSafetyBlock(int slotIndex, boolean isRaidMenu, boolean isAlreadyReadied, int enabledGambits, ClickType clickType, boolean shiftHeld, BridgeConfig config) { - if (config == null || !config.raidGambitSafety || config.raidGambitMinimum <= 0) { + return evaluateSafetyBlock(slotIndex, isRaidMenu, isAlreadyReadied, enabledGambits, true, clickType, shiftHeld, config); + } + + /** Evaluates whether a ready click should be blocked based on active gambits, guild raid status, and settings. */ + public static boolean evaluateSafetyBlock(int slotIndex, boolean isRaidMenu, boolean isAlreadyReadied, int enabledGambits, boolean isGuildRaid, ClickType clickType, boolean shiftHeld, BridgeConfig config) { + if (config == null) { return false; } if (slotIndex < READY_SLOT_MIN || slotIndex > READY_SLOT_MAX) { @@ -179,16 +201,26 @@ public static boolean evaluateSafetyBlock(int slotIndex, boolean isRaidMenu, boo if (isAlreadyReadied) { return false; } - if (enabledGambits >= config.raidGambitMinimum) { + boolean isShift = (clickType == ClickType.QUICK_MOVE || shiftHeld); + if (isShift) { return false; } - boolean isShift = (clickType == ClickType.QUICK_MOVE || shiftHeld); - return !isShift; + if (config.requireGuildRaid && !isGuildRaid) { + return true; + } + if (config.raidGambitSafety && config.raidGambitMinimum > 0 && enabledGambits < config.raidGambitMinimum) { + return true; + } + return false; } /** Determines whether the clicked slot should be blocked from readying up. */ public static boolean shouldBlockReadyClick(AbstractContainerMenu menu, Slot slot, ClickType clickType, BridgeConfig config) { - if (config == null || !config.raidGambitSafety || config.raidGambitMinimum <= 0) { + if (config == null) { + return false; + } + boolean gambitActive = config.raidGambitSafety && config.raidGambitMinimum > 0; + if (!gambitActive && !config.requireGuildRaid) { return false; } if (!isReadyClickSlot(slot)) { @@ -201,7 +233,8 @@ public static boolean shouldBlockReadyClick(AbstractContainerMenu menu, Slot slo return false; } int enabled = countEnabledGambits(menu); - return evaluateSafetyBlock(slot.index, true, false, enabled, clickType, isShiftKeyDown(), config); + boolean isGuildRaid = isGuildRaidAvailable(menu); + return evaluateSafetyBlock(slot.index, true, false, enabled, isGuildRaid, clickType, isShiftKeyDown(), config); } /** Generates the user-facing warning message with correct singular/plural phrasing. */ @@ -215,6 +248,16 @@ public static Component getTooltipWarning(int minRequired) { return Component.literal("§c" + getWarningMessage(minRequired)); } + /** Generates the user-facing warning message for non-guild raid readying. */ + public static String getGuildRaidWarningMessage() { + return "This raid is not a Guild Raid, Shift-Click to ready up anyways."; + } + + /** Generates the colored tooltip warning component for non-guild raid readying. */ + public static Component getGuildRaidTooltipWarning() { + return Component.literal("§c" + getGuildRaidWarningMessage()); + } + /** Plays rejection sound when a ready click is blocked. */ public static void notifyBlocked(Minecraft mc) { if (mc == null) { diff --git a/test/tel/eden/mod/raid/RaidGambitSafetyTest.java b/test/tel/eden/mod/raid/RaidGambitSafetyTest.java index c9eccae..3433c33 100644 --- a/test/tel/eden/mod/raid/RaidGambitSafetyTest.java +++ b/test/tel/eden/mod/raid/RaidGambitSafetyTest.java @@ -139,4 +139,38 @@ void generatesAccurateWarningMessagesWithSingularAndPlural() { // 3 gambits (plural) assertEquals("You have less than 3 gambits selected, Shift-Click to ready up anyways.", RaidGambitSafety.getWarningMessage(3)); } + + @Test + void guildRaidSafetyBlocksWhenNotGuildRaid() { + config.requireGuildRaid = true; + config.raidGambitSafety = false; + + // Not a guild raid -> blocked on normal click + boolean blocked = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, false, ClickType.PICKUP, false, config); + assertTrue(blocked, "Normal click must be blocked when requireGuildRaid is true and raid is not a guild raid"); + + // Shift click -> allowed + boolean shiftAllowed = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, false, ClickType.QUICK_MOVE, false, config); + assertFalse(shiftAllowed, "Shift-click must be allowed even when raid is not a guild raid"); + + // Is a guild raid -> allowed + boolean graidAllowed = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, true, ClickType.PICKUP, false, config); + assertFalse(graidAllowed, "Normal click must be allowed when raid is a guild raid"); + } + + @Test + void guildRaidSafetyDisabledByDefault() { + assertFalse(config.requireGuildRaid, "requireGuildRaid must be false by default"); + + // Even when not a guild raid, if requireGuildRaid is false, it does not block + config.raidGambitSafety = false; + boolean blocked = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, false, ClickType.PICKUP, false, config); + assertFalse(blocked, "Must not block when requireGuildRaid is disabled"); + } + + @Test + void generatesGuildRaidWarningMessage() { + assertEquals("This raid is not a Guild Raid, Shift-Click to ready up anyways.", RaidGambitSafety.getGuildRaidWarningMessage()); + assertEquals("§cThis raid is not a Guild Raid, Shift-Click to ready up anyways.", RaidGambitSafety.getGuildRaidTooltipWarning().getString()); + } } From ce3604f684faa2cdada5b12e83abb86d98978dfd Mon Sep 17 00:00:00 2001 From: Good627 <193092356+Good627@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:21:53 +0300 Subject: [PATCH 2/5] =?UTF-8?q?fix(raid):=20check=20lore=20=E2=9C=96=20opt?= =?UTF-8?q?-out=20and=20remove=20shift=20bypass=20for=20guild=20raid=20saf?= =?UTF-8?q?ety?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tel/eden/mod/raid/RaidGambitSafety.java | 29 +++++++++++++++---- .../eden/mod/raid/RaidGambitSafetyTest.java | 27 +++++++++++++---- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/tel/eden/mod/raid/RaidGambitSafety.java b/src/tel/eden/mod/raid/RaidGambitSafety.java index 14ce564..31cc430 100644 --- a/src/tel/eden/mod/raid/RaidGambitSafety.java +++ b/src/tel/eden/mod/raid/RaidGambitSafety.java @@ -165,7 +165,7 @@ public static boolean isShiftKeyDown() { return false; } - /** Checks if the raid preparation menu has a guild raid available. */ + /** Checks if the raid preparation menu has a guild raid available and all players are opted in. */ public static boolean isGuildRaidAvailable(AbstractContainerMenu menu) { if (menu == null) { return false; @@ -175,13 +175,30 @@ public static boolean isGuildRaidAvailable(AbstractContainerMenu menu) { if (!stack.isEmpty()) { String name = ChatFormatting.stripFormatting(stack.getHoverName().getString()).trim(); if (name.equalsIgnoreCase("Guild Raid Available") || name.contains("Guild Raid Available")) { - return true; + return isAllGuildMembersOptedIn(getLoreLines(stack)); } } } return false; } + /** Checks whether all players have opted in to the guild raid (no ✖ / opted out indicators). */ + public static boolean isAllGuildMembersOptedIn(List loreLines) { + if (loreLines == null || loreLines.isEmpty()) { + return false; + } + for (String line : loreLines) { + if (line == null) { + continue; + } + String stripped = ChatFormatting.stripFormatting(line).trim(); + if (stripped.contains("✖") || stripped.contains("\u2716") || stripped.toLowerCase(Locale.ROOT).contains("opted out")) { + return false; + } + } + return true; + } + /** Evaluates whether a ready click should be blocked based on active gambits and settings. */ public static boolean evaluateSafetyBlock(int slotIndex, boolean isRaidMenu, boolean isAlreadyReadied, int enabledGambits, ClickType clickType, boolean shiftHeld, BridgeConfig config) { return evaluateSafetyBlock(slotIndex, isRaidMenu, isAlreadyReadied, enabledGambits, true, clickType, shiftHeld, config); @@ -201,13 +218,13 @@ public static boolean evaluateSafetyBlock(int slotIndex, boolean isRaidMenu, boo if (isAlreadyReadied) { return false; } + if (config.requireGuildRaid && !isGuildRaid) { + return true; + } boolean isShift = (clickType == ClickType.QUICK_MOVE || shiftHeld); if (isShift) { return false; } - if (config.requireGuildRaid && !isGuildRaid) { - return true; - } if (config.raidGambitSafety && config.raidGambitMinimum > 0 && enabledGambits < config.raidGambitMinimum) { return true; } @@ -250,7 +267,7 @@ public static Component getTooltipWarning(int minRequired) { /** Generates the user-facing warning message for non-guild raid readying. */ public static String getGuildRaidWarningMessage() { - return "This raid is not a Guild Raid, Shift-Click to ready up anyways."; + return "This raid is not a Guild Raid, disable Require guild raid to ready in settings to ready up anyways."; } /** Generates the colored tooltip warning component for non-guild raid readying. */ diff --git a/test/tel/eden/mod/raid/RaidGambitSafetyTest.java b/test/tel/eden/mod/raid/RaidGambitSafetyTest.java index 3433c33..819c7e9 100644 --- a/test/tel/eden/mod/raid/RaidGambitSafetyTest.java +++ b/test/tel/eden/mod/raid/RaidGambitSafetyTest.java @@ -141,7 +141,7 @@ void generatesAccurateWarningMessagesWithSingularAndPlural() { } @Test - void guildRaidSafetyBlocksWhenNotGuildRaid() { + void guildRaidSafetyBlocksWhenNotGuildRaidWithoutShiftBypass() { config.requireGuildRaid = true; config.raidGambitSafety = false; @@ -149,9 +149,12 @@ void guildRaidSafetyBlocksWhenNotGuildRaid() { boolean blocked = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, false, ClickType.PICKUP, false, config); assertTrue(blocked, "Normal click must be blocked when requireGuildRaid is true and raid is not a guild raid"); - // Shift click -> allowed - boolean shiftAllowed = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, false, ClickType.QUICK_MOVE, false, config); - assertFalse(shiftAllowed, "Shift-click must be allowed even when raid is not a guild raid"); + // Shift click -> STILL BLOCKED (no shift bypass for guild raid safety) + boolean quickMoveBlocked = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, false, ClickType.QUICK_MOVE, false, config); + assertTrue(quickMoveBlocked, "QUICK_MOVE must be blocked when requireGuildRaid is true and raid is not a guild raid"); + + boolean shiftHeldBlocked = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, false, ClickType.PICKUP, true, config); + assertTrue(shiftHeldBlocked, "Click with shift held must be blocked when requireGuildRaid is true and raid is not a guild raid"); // Is a guild raid -> allowed boolean graidAllowed = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, true, ClickType.PICKUP, false, config); @@ -168,9 +171,21 @@ void guildRaidSafetyDisabledByDefault() { assertFalse(blocked, "Must not block when requireGuildRaid is disabled"); } + @Test + void detectsAllGuildMembersOptedInFromLore() { + List allOptedIn = List.of("§7All members of this party", "§7✔ altaqska has opted in", "§7✔ agency agency has opted in", "§7✔ blob has opted in", "§8All players must opt in"); + assertTrue(RaidGambitSafety.isAllGuildMembersOptedIn(allOptedIn), "Must be true when all players opted in"); + + List hasOptedOut = List.of("§7All members of this party", "§7✔ altaqska has opted in", "§7✖ Liu Ruyan has opted out", "§7✔ agency agency has opted in", "§7✔ blob has opted in", "§8All players must opt in"); + assertFalse(RaidGambitSafety.isAllGuildMembersOptedIn(hasOptedOut), "Must be false when a player has ✖ / opted out"); + + assertFalse(RaidGambitSafety.isAllGuildMembersOptedIn(List.of())); + assertFalse(RaidGambitSafety.isAllGuildMembersOptedIn(null)); + } + @Test void generatesGuildRaidWarningMessage() { - assertEquals("This raid is not a Guild Raid, Shift-Click to ready up anyways.", RaidGambitSafety.getGuildRaidWarningMessage()); - assertEquals("§cThis raid is not a Guild Raid, Shift-Click to ready up anyways.", RaidGambitSafety.getGuildRaidTooltipWarning().getString()); + assertEquals("This raid is not a Guild Raid, disable Require guild raid to ready in settings to ready up anyways.", RaidGambitSafety.getGuildRaidWarningMessage()); + assertEquals("§cThis raid is not a Guild Raid, disable Require guild raid to ready in settings to ready up anyways.", RaidGambitSafety.getGuildRaidTooltipWarning().getString()); } } From 834d13c299cbd035ec3921d480dfa760646cf1d7 Mon Sep 17 00:00:00 2001 From: Good627 <193092356+Good627@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:45:24 +0300 Subject: [PATCH 3/5] fix(raid): display graid warning below gambit warning in ready tooltip --- src/tel/eden/mod/mixin/AbstractContainerScreenMixin.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tel/eden/mod/mixin/AbstractContainerScreenMixin.java b/src/tel/eden/mod/mixin/AbstractContainerScreenMixin.java index f8b31c3..d8c9f42 100644 --- a/src/tel/eden/mod/mixin/AbstractContainerScreenMixin.java +++ b/src/tel/eden/mod/mixin/AbstractContainerScreenMixin.java @@ -50,15 +50,15 @@ public abstract class AbstractContainerScreenMixin { AbstractContainerMenu menu = getMenu(); if (RaidGambitSafety.isRaidReadyMenu(menu) && !RaidGambitSafety.isPlayerAlreadyReadied(menu)) { List warnings = new ArrayList<>(); - if (config.requireGuildRaid && !RaidGambitSafety.isGuildRaidAvailable(menu)) { - warnings.add(RaidGambitSafety.getGuildRaidTooltipWarning()); - } if (config.raidGambitSafety && config.raidGambitMinimum > 0) { int enabled = RaidGambitSafety.countEnabledGambits(menu); if (enabled < config.raidGambitMinimum) { warnings.add(RaidGambitSafety.getTooltipWarning(config.raidGambitMinimum)); } } + if (config.requireGuildRaid && !RaidGambitSafety.isGuildRaidAvailable(menu)) { + warnings.add(RaidGambitSafety.getGuildRaidTooltipWarning()); + } if (!warnings.isEmpty()) { List base = cir.getReturnValue(); List tooltip = new ArrayList<>(base != null ? base : List.of()); From c4db692ec0e393072b88c896eeaf2585b53e382b Mon Sep 17 00:00:00 2001 From: Good627 <193092356+Good627@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:29:24 +0300 Subject: [PATCH 4/5] fix(raid): case-insensitive graid detection, top-of-screen popup with Eden logo, and tests --- .../mixin/AbstractContainerScreenMixin.java | 21 +- src/tel/eden/mod/raid/RaidGambitSafety.java | 225 +++++++++++++----- .../eden/mod/raid/RaidGambitSafetyTest.java | 166 +++++++++++-- 3 files changed, 339 insertions(+), 73 deletions(-) diff --git a/src/tel/eden/mod/mixin/AbstractContainerScreenMixin.java b/src/tel/eden/mod/mixin/AbstractContainerScreenMixin.java index d8c9f42..81f084c 100644 --- a/src/tel/eden/mod/mixin/AbstractContainerScreenMixin.java +++ b/src/tel/eden/mod/mixin/AbstractContainerScreenMixin.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.network.chat.Component; import net.minecraft.world.inventory.AbstractContainerMenu; @@ -33,14 +34,32 @@ public abstract class AbstractContainerScreenMixin { private void edenmod$onSlotClicked(Slot slot, int slotId, int mouseButton, ClickType clickType, CallbackInfo ci) { try { BridgeConfig config = EdenModClient.instance().config(); - if (RaidGambitSafety.shouldBlockReadyClick(getMenu(), slot, clickType, config)) { + List reasons = RaidGambitSafety.getActiveBlockReasons(getMenu(), slot, clickType, config); + if (!reasons.isEmpty()) { ci.cancel(); RaidGambitSafety.notifyBlocked(Minecraft.getInstance(), config.raidGambitMinimum); + RaidGambitSafety.showPopupWarning(reasons); } } catch (Throwable ignored) { } } + @Inject(method = "render", at = @At("RETURN")) + private void edenmod$renderSafetyPopup(GuiGraphics graphics, int mouseX, int mouseY, float delta, CallbackInfo ci) { + try { + Minecraft mc = Minecraft.getInstance(); + if (mc != null && mc.font != null && mc.getWindow() != null) { + RaidGambitSafety.renderPopupWarning(graphics, mc.font, mc.getWindow().getGuiScaledWidth()); + } + } catch (Throwable ignored) { + } + } + + @Inject(method = "removed", at = @At("TAIL")) + private void edenmod$onContainerScreenRemoved(CallbackInfo ci) { + RaidGambitSafety.clearPopupWarning(); + } + @Inject(method = "getTooltipFromContainerItem(Lnet/minecraft/world/item/ItemStack;)Ljava/util/List;", at = @At("RETURN"), cancellable = true) private void edenmod$appendRaidReadyTooltip(ItemStack stack, CallbackInfoReturnable> cir) { try { diff --git a/src/tel/eden/mod/raid/RaidGambitSafety.java b/src/tel/eden/mod/raid/RaidGambitSafety.java index 31cc430..77c6275 100644 --- a/src/tel/eden/mod/raid/RaidGambitSafety.java +++ b/src/tel/eden/mod/raid/RaidGambitSafety.java @@ -1,14 +1,20 @@ package tel.eden.mod.raid; import com.mojang.blaze3d.platform.InputConstants; +import java.util.ArrayList; import java.util.List; import java.util.Locale; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.client.resources.sounds.SimpleSoundInstance; import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; import net.minecraft.sounds.SoundEvents; +import net.minecraft.util.FormattedCharSequence; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.ClickType; import net.minecraft.world.inventory.Slot; @@ -18,12 +24,11 @@ import tel.eden.mod.config.BridgeConfig; /** - * Enforces raid gambit ready safety in Wynncraft raid lobbies. + * Enforces raid gambit and guild raid ready safety in Wynncraft raid lobbies. * *

Prevents players from accidentally readying up without their desired minimum number - * of gambits enabled. Chest slots 33, 34, and 35 are the clickable ready buttons in the - * raid lobby menu. If fewer than {@link BridgeConfig#raidGambitMinimum} gambits are enabled, - * clicking to ready up requires holding Shift to confirm. + * of gambits enabled, or into a non-guild raid when guild raid safety is enabled. + * Chest slots 33, 34, and 35 are the clickable ready buttons in the raid lobby menu. */ public final class RaidGambitSafety { public static final int READY_SLOT_MIN = 33; @@ -32,6 +37,11 @@ public final class RaidGambitSafety { public static final int CONTAINER_SLOTS = 36; public static final int TOTAL_SLOTS = 72; + private static final Identifier LOGO_TEXTURE = Identifier.parse("edenmod:icon.png"); + private static final int LOGO_SRC_SIZE = 256; + private static List activePopupMessages = null; + private static long popupExpiresAtMs = 0L; + private RaidGambitSafety() { } @@ -165,18 +175,45 @@ public static boolean isShiftKeyDown() { return false; } + /** Checks whether item lore lines indicate guild raid availability. */ + public static boolean isGuildRaidLore(List loreLines) { + if (loreLines == null) { + return false; + } + for (String line : loreLines) { + if (line != null && ChatFormatting.stripFormatting(line).toLowerCase(Locale.ROOT).contains("guild raid available")) { + return true; + } + } + return false; + } + + /** Checks whether an item stack represents the guild raid status indicator. */ + public static boolean isGuildRaidItem(ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return false; + } + String name = ChatFormatting.stripFormatting(stack.getHoverName().getString()).toLowerCase(Locale.ROOT); + if (name.contains("guild raid available")) { + return true; + } + return isGuildRaidLore(getLoreLines(stack)); + } + /** Checks if the raid preparation menu has a guild raid available and all players are opted in. */ public static boolean isGuildRaidAvailable(AbstractContainerMenu menu) { if (menu == null) { return false; } for (int i = 0; i < Math.min(CONTAINER_SLOTS, menu.slots.size()); i++) { - ItemStack stack = menu.getSlot(i).getItem(); - if (!stack.isEmpty()) { - String name = ChatFormatting.stripFormatting(stack.getHoverName().getString()).trim(); - if (name.equalsIgnoreCase("Guild Raid Available") || name.contains("Guild Raid Available")) { - return isAllGuildMembersOptedIn(getLoreLines(stack)); - } + Slot slot = menu.getSlot(i); + if (slot == null) { + continue; + } + ItemStack stack = slot.getItem(); + if (!stack.isEmpty() && isGuildRaidItem(stack)) { + List lore = getLoreLines(stack); + return lore.isEmpty() || isAllGuildMembersOptedIn(lore); } } return false; @@ -199,6 +236,38 @@ public static boolean isAllGuildMembersOptedIn(List loreLines) { return true; } + /** + * Returns active block reason messages in priority order (minimum gambit error first, + * guild raid safety error second). + */ + public static List getActiveBlockReasons(int slotIndex, boolean isRaidMenu, boolean isAlreadyReadied, int enabledGambits, boolean isGuildRaid, ClickType clickType, boolean shiftHeld, BridgeConfig config) { + if (config == null || slotIndex < READY_SLOT_MIN || slotIndex > READY_SLOT_MAX || !isRaidMenu || isAlreadyReadied) { + return List.of(); + } + boolean isShift = (clickType == ClickType.QUICK_MOVE || shiftHeld); + List reasons = new ArrayList<>(2); + + if (!isShift && config.raidGambitSafety && config.raidGambitMinimum > 0 && enabledGambits < config.raidGambitMinimum) { + reasons.add(getWarningMessage(config.raidGambitMinimum)); + } + + if (config.requireGuildRaid && !isGuildRaid) { + reasons.add(getGuildRaidWarningMessage()); + } + + return reasons; + } + + /** Returns active block reason messages for a clicked container slot. */ + public static List getActiveBlockReasons(AbstractContainerMenu menu, Slot slot, ClickType clickType, BridgeConfig config) { + if (config == null || !isReadyClickSlot(slot) || !isRaidReadyMenu(menu) || isPlayerAlreadyReadied(menu)) { + return List.of(); + } + int enabled = countEnabledGambits(menu); + boolean isGuildRaid = isGuildRaidAvailable(menu); + return getActiveBlockReasons(slot.index, true, false, enabled, isGuildRaid, clickType, isShiftKeyDown(), config); + } + /** Evaluates whether a ready click should be blocked based on active gambits and settings. */ public static boolean evaluateSafetyBlock(int slotIndex, boolean isRaidMenu, boolean isAlreadyReadied, int enabledGambits, ClickType clickType, boolean shiftHeld, BridgeConfig config) { return evaluateSafetyBlock(slotIndex, isRaidMenu, isAlreadyReadied, enabledGambits, true, clickType, shiftHeld, config); @@ -206,52 +275,12 @@ public static boolean evaluateSafetyBlock(int slotIndex, boolean isRaidMenu, boo /** Evaluates whether a ready click should be blocked based on active gambits, guild raid status, and settings. */ public static boolean evaluateSafetyBlock(int slotIndex, boolean isRaidMenu, boolean isAlreadyReadied, int enabledGambits, boolean isGuildRaid, ClickType clickType, boolean shiftHeld, BridgeConfig config) { - if (config == null) { - return false; - } - if (slotIndex < READY_SLOT_MIN || slotIndex > READY_SLOT_MAX) { - return false; - } - if (!isRaidMenu) { - return false; - } - if (isAlreadyReadied) { - return false; - } - if (config.requireGuildRaid && !isGuildRaid) { - return true; - } - boolean isShift = (clickType == ClickType.QUICK_MOVE || shiftHeld); - if (isShift) { - return false; - } - if (config.raidGambitSafety && config.raidGambitMinimum > 0 && enabledGambits < config.raidGambitMinimum) { - return true; - } - return false; + return !getActiveBlockReasons(slotIndex, isRaidMenu, isAlreadyReadied, enabledGambits, isGuildRaid, clickType, shiftHeld, config).isEmpty(); } /** Determines whether the clicked slot should be blocked from readying up. */ public static boolean shouldBlockReadyClick(AbstractContainerMenu menu, Slot slot, ClickType clickType, BridgeConfig config) { - if (config == null) { - return false; - } - boolean gambitActive = config.raidGambitSafety && config.raidGambitMinimum > 0; - if (!gambitActive && !config.requireGuildRaid) { - return false; - } - if (!isReadyClickSlot(slot)) { - return false; - } - if (!isRaidReadyMenu(menu)) { - return false; - } - if (isPlayerAlreadyReadied(menu)) { - return false; - } - int enabled = countEnabledGambits(menu); - boolean isGuildRaid = isGuildRaidAvailable(menu); - return evaluateSafetyBlock(slot.index, true, false, enabled, isGuildRaid, clickType, isShiftKeyDown(), config); + return !getActiveBlockReasons(menu, slot, clickType, config).isEmpty(); } /** Generates the user-facing warning message with correct singular/plural phrasing. */ @@ -275,6 +304,93 @@ public static Component getGuildRaidTooltipWarning() { return Component.literal("§c" + getGuildRaidWarningMessage()); } + /** Sets active popup warning message(s) to display at the top of the screen. */ + public static void showPopupWarning(List messages) { + if (messages != null && !messages.isEmpty()) { + activePopupMessages = List.copyOf(messages); + popupExpiresAtMs = System.currentTimeMillis() + 3500L; + } + } + + /** Returns current active popup warning messages if active and not expired. */ + public static List getActivePopupMessages() { + if (activePopupMessages != null && System.currentTimeMillis() < popupExpiresAtMs) { + return activePopupMessages; + } + return null; + } + + /** Clears active popup warning state. */ + public static void clearPopupWarning() { + activePopupMessages = null; + popupExpiresAtMs = 0L; + } + + /** Renders top-of-screen popup box warning with the Eden logo and gold/wood frame. */ + public static void renderPopupWarning(GuiGraphics graphics, Font font, int screenWidth) { + List messages = getActivePopupMessages(); + if (messages == null || messages.isEmpty() || graphics == null || font == null) { + return; + } + + int logoSize = 24; + int maxTextWidth = Math.max(120, screenWidth - (logoSize + 50)); + List wrappedLines = new ArrayList<>(); + for (String msg : messages) { + wrappedLines.addAll(font.split(Component.literal("§c" + msg), maxTextWidth)); + } + + int maxLineWidth = 0; + for (FormattedCharSequence line : wrappedLines) { + maxLineWidth = Math.max(maxLineWidth, font.width(line)); + } + + int boxWidth = Math.min(screenWidth - 20, maxLineWidth + logoSize + 28); + int lineHeight = font.lineHeight; + int textTotalHeight = wrappedLines.size() * (lineHeight + 2) - 2; + int boxHeight = Math.max(logoSize + 12, textTotalHeight + 14); + int boxX = (screenWidth - boxWidth) / 2; + int boxY = 6; + + // Outer dark wood outline with clipped 1px corners + graphics.fill(boxX + 1, boxY, boxX + boxWidth - 1, boxY + boxHeight, 0xFF3C2812); + graphics.fill(boxX, boxY + 1, boxX + boxWidth, boxY + boxHeight - 1, 0xFF3C2812); + + // Outer wood base + graphics.fill(boxX + 1, boxY + 1, boxX + boxWidth - 1, boxY + boxHeight - 1, 0xFF5A3E1E); + + // Gold border + graphics.fill(boxX + 2, boxY + 2, boxX + boxWidth - 2, boxY + boxHeight - 2, 0xFFC6973A); + + // Gold highlights (top & left) + graphics.fill(boxX + 2, boxY + 2, boxX + boxWidth - 2, boxY + 3, 0xFFF8DC82); + graphics.fill(boxX + 2, boxY + 2, boxX + 3, boxY + boxHeight - 2, 0xFFF8DC82); + + // Gold shadows (bottom & right) + graphics.fill(boxX + 2, boxY + boxHeight - 3, boxX + boxWidth - 2, boxY + boxHeight - 2, 0xFF8A621E); + graphics.fill(boxX + boxWidth - 3, boxY + 2, boxX + boxWidth - 2, boxY + boxHeight - 2, 0xFF8A621E); + + // Inner dark wood rim + graphics.fill(boxX + 4, boxY + 4, boxX + boxWidth - 4, boxY + boxHeight - 4, 0xFF281A0B); + + // Interior dark background + graphics.fill(boxX + 5, boxY + 5, boxX + boxWidth - 5, boxY + boxHeight - 5, 0xF214110E); + + // Eden logo on left + int logoX = boxX + 8; + int logoY = boxY + (boxHeight - logoSize) / 2; + graphics.blit(RenderPipelines.GUI_TEXTURED, LOGO_TEXTURE, logoX, logoY, 0.0f, 0.0f, logoSize, logoSize, LOGO_SRC_SIZE, LOGO_SRC_SIZE, LOGO_SRC_SIZE, LOGO_SRC_SIZE); + + // Text lines to the right of logo + int textStartX = logoX + logoSize + 7; + int textStartY = boxY + (boxHeight - textTotalHeight) / 2; + for (int i = 0; i < wrappedLines.size(); i++) { + FormattedCharSequence line = wrappedLines.get(i); + int lineY = textStartY + i * (lineHeight + 2); + graphics.drawString(font, line, textStartX, lineY, 0xFFFFFFFF, true); + } + } + /** Plays rejection sound when a ready click is blocked. */ public static void notifyBlocked(Minecraft mc) { if (mc == null) { @@ -293,7 +409,10 @@ public static void notifyBlocked(Minecraft mc, int minRequired) { notifyBlocked(mc); } - private static List getLoreLines(ItemStack stack) { + static List getLoreLines(ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return List.of(); + } ItemLore lore = stack.get(DataComponents.LORE); return lore == null ? List.of() : lore.lines().stream().map(Component::getString).toList(); } diff --git a/test/tel/eden/mod/raid/RaidGambitSafetyTest.java b/test/tel/eden/mod/raid/RaidGambitSafetyTest.java index 819c7e9..aa93bed 100644 --- a/test/tel/eden/mod/raid/RaidGambitSafetyTest.java +++ b/test/tel/eden/mod/raid/RaidGambitSafetyTest.java @@ -2,10 +2,25 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.ArrayList; import java.util.List; +import net.minecraft.SharedConstants; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.ItemLore; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import tel.eden.mod.config.BridgeConfig; @@ -13,11 +28,54 @@ class RaidGambitSafetyTest { private BridgeConfig config; + @BeforeAll + static void setupMinecraft() { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + } + @BeforeEach void setUp() { config = new BridgeConfig(); config.raidGambitSafety = true; config.raidGambitMinimum = 1; + RaidGambitSafety.clearPopupWarning(); + } + + private static ItemStack createItem(String name, String... loreLines) { + ItemStack stack = new ItemStack(Items.SNOWBALL); + if (name != null) { + stack.set(DataComponents.CUSTOM_NAME, Component.literal(name)); + } + if (loreLines != null && loreLines.length > 0) { + List lore = new ArrayList<>(); + for (String line : loreLines) { + lore.add(Component.literal(line)); + } + stack.set(DataComponents.LORE, new ItemLore(lore)); + } + return stack; + } + + private static AbstractContainerMenu createMenu(ItemStack... items) { + AbstractContainerMenu menu = new AbstractContainerMenu(null, 1) { + @Override + public ItemStack quickMoveStack(Player player, int index) { + return ItemStack.EMPTY; + } + + @Override + public boolean stillValid(Player player) { + return true; + } + }; + SimpleContainer container = new SimpleContainer(Math.max(RaidGambitSafety.TOTAL_SLOTS, items.length)); + for (int i = 0; i < Math.max(RaidGambitSafety.TOTAL_SLOTS, items.length); i++) { + ItemStack stack = i < items.length && items[i] != null ? items[i] : ItemStack.EMPTY; + container.setItem(i, stack); + menu.slots.add(new Slot(container, i, 0, 0)); + } + return menu; } @Test @@ -40,7 +98,6 @@ void recognizesGambitLore() { @Test void blocksUnreadyClickOnReadySlotsWhenBelowMinimum() { - // Chest slots 33, 34, and 35 are the ready buttons for (int slot = RaidGambitSafety.READY_SLOT_MIN; slot <= RaidGambitSafety.READY_SLOT_MAX; slot++) { boolean blocked = RaidGambitSafety.evaluateSafetyBlock(slot, true, false, 0, ClickType.PICKUP, false, config); assertTrue(blocked, "Normal click on slot " + slot + " should be blocked when 0 gambits enabled and min is 1"); @@ -50,11 +107,9 @@ void blocksUnreadyClickOnReadySlotsWhenBelowMinimum() { @Test void allowsShiftClickOnReadySlotsWhenBelowMinimum() { for (int slot = RaidGambitSafety.READY_SLOT_MIN; slot <= RaidGambitSafety.READY_SLOT_MAX; slot++) { - // Quick move (shift click) boolean quickMoveBlocked = RaidGambitSafety.evaluateSafetyBlock(slot, true, false, 0, ClickType.QUICK_MOVE, false, config); assertFalse(quickMoveBlocked, "Shift click (QUICK_MOVE) on slot " + slot + " should pass through"); - // Physical shift key down boolean shiftKeyDownBlocked = RaidGambitSafety.evaluateSafetyBlock(slot, true, false, 0, ClickType.PICKUP, true, config); assertFalse(shiftKeyDownBlocked, "Click with shift held on slot " + slot + " should pass through"); } @@ -62,13 +117,11 @@ void allowsShiftClickOnReadySlotsWhenBelowMinimum() { @Test void allowsNormalClickWhenGambitThresholdMet() { - // 1 enabled, min is 1 -> allowed on slots 33, 34, 35 for (int slot = RaidGambitSafety.READY_SLOT_MIN; slot <= RaidGambitSafety.READY_SLOT_MAX; slot++) { boolean blocked = RaidGambitSafety.evaluateSafetyBlock(slot, true, false, 1, ClickType.PICKUP, false, config); assertFalse(blocked, "Normal click should be allowed when threshold met on slot " + slot); } - // 3 enabled, min is 2 -> allowed config.raidGambitMinimum = 2; boolean blockedAbove = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 3, ClickType.PICKUP, false, config); assertFalse(blockedAbove, "Normal click should be allowed when exceeding threshold"); @@ -77,18 +130,15 @@ void allowsNormalClickWhenGambitThresholdMet() { @Test void blocksWhenMinimumIsHigherThanEnabledCount() { config.raidGambitMinimum = 2; - // 1 enabled, min is 2 -> blocked boolean blocked = RaidGambitSafety.evaluateSafetyBlock(34, true, false, 1, ClickType.PICKUP, false, config); assertTrue(blocked, "Normal click should be blocked when 1 enabled and min is 2"); - // Shift click -> allowed boolean shiftAllowed = RaidGambitSafety.evaluateSafetyBlock(34, true, false, 1, ClickType.QUICK_MOVE, false, config); assertFalse(shiftAllowed, "Shift click should be allowed even if below min=2"); } @Test void neverBlocksUnreadying() { - // When player is already readied, clicking to unready must always be allowed for (int slot = RaidGambitSafety.READY_SLOT_MIN; slot <= RaidGambitSafety.READY_SLOT_MAX; slot++) { boolean blocked = RaidGambitSafety.evaluateSafetyBlock(slot, true, true, 0, ClickType.PICKUP, false, config); assertFalse(blocked, "Clicking to unready on slot " + slot + " must never be blocked"); @@ -97,11 +147,9 @@ void neverBlocksUnreadying() { @Test void neverBlocksIndicatorOrOtherSlots() { - // Slot 27 is an indicator for other players, not the ready click button boolean slot27Blocked = RaidGambitSafety.evaluateSafetyBlock(RaidGambitSafety.STATUS_INDICATOR_SLOT, true, false, 0, ClickType.PICKUP, false, config); assertFalse(slot27Blocked, "Slot 27 is an indicator and must not be intercepted"); - // Clicking gambit slot 1 or 3 should never be blocked boolean slot1Blocked = RaidGambitSafety.evaluateSafetyBlock(1, true, false, 0, ClickType.PICKUP, false, config); assertFalse(slot1Blocked, "Clicking a gambit slot must not be blocked"); } @@ -114,12 +162,10 @@ void neverBlocksInUnrelatedMenus() { @Test void disabledWhenMasterToggleOffOrMinimumZero() { - // Master toggle off config.raidGambitSafety = false; boolean blockedToggleOff = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, ClickType.PICKUP, false, config); assertFalse(blockedToggleOff, "Must not block when master toggle is off"); - // Minimum 0 config.raidGambitSafety = true; config.raidGambitMinimum = 0; boolean blockedMinZero = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, ClickType.PICKUP, false, config); @@ -128,15 +174,12 @@ void disabledWhenMasterToggleOffOrMinimumZero() { @Test void generatesAccurateWarningMessagesWithSingularAndPlural() { - // 1 gambit (singular) assertEquals("You have less than 1 gambit selected, Shift-Click to ready up anyways.", RaidGambitSafety.getWarningMessage(1)); assertEquals("§cYou have less than 1 gambit selected, Shift-Click to ready up anyways.", RaidGambitSafety.getTooltipWarning(1).getString()); - // 2 gambits (plural) assertEquals("You have less than 2 gambits selected, Shift-Click to ready up anyways.", RaidGambitSafety.getWarningMessage(2)); assertEquals("§cYou have less than 2 gambits selected, Shift-Click to ready up anyways.", RaidGambitSafety.getTooltipWarning(2).getString()); - // 3 gambits (plural) assertEquals("You have less than 3 gambits selected, Shift-Click to ready up anyways.", RaidGambitSafety.getWarningMessage(3)); } @@ -145,18 +188,15 @@ void guildRaidSafetyBlocksWhenNotGuildRaidWithoutShiftBypass() { config.requireGuildRaid = true; config.raidGambitSafety = false; - // Not a guild raid -> blocked on normal click boolean blocked = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, false, ClickType.PICKUP, false, config); assertTrue(blocked, "Normal click must be blocked when requireGuildRaid is true and raid is not a guild raid"); - // Shift click -> STILL BLOCKED (no shift bypass for guild raid safety) boolean quickMoveBlocked = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, false, ClickType.QUICK_MOVE, false, config); assertTrue(quickMoveBlocked, "QUICK_MOVE must be blocked when requireGuildRaid is true and raid is not a guild raid"); boolean shiftHeldBlocked = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, false, ClickType.PICKUP, true, config); assertTrue(shiftHeldBlocked, "Click with shift held must be blocked when requireGuildRaid is true and raid is not a guild raid"); - // Is a guild raid -> allowed boolean graidAllowed = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, true, ClickType.PICKUP, false, config); assertFalse(graidAllowed, "Normal click must be allowed when raid is a guild raid"); } @@ -165,7 +205,6 @@ void guildRaidSafetyBlocksWhenNotGuildRaidWithoutShiftBypass() { void guildRaidSafetyDisabledByDefault() { assertFalse(config.requireGuildRaid, "requireGuildRaid must be false by default"); - // Even when not a guild raid, if requireGuildRaid is false, it does not block config.raidGambitSafety = false; boolean blocked = RaidGambitSafety.evaluateSafetyBlock(33, true, false, 0, false, ClickType.PICKUP, false, config); assertFalse(blocked, "Must not block when requireGuildRaid is disabled"); @@ -179,6 +218,9 @@ void detectsAllGuildMembersOptedInFromLore() { List hasOptedOut = List.of("§7All members of this party", "§7✔ altaqska has opted in", "§7✖ Liu Ruyan has opted out", "§7✔ agency agency has opted in", "§7✔ blob has opted in", "§8All players must opt in"); assertFalse(RaidGambitSafety.isAllGuildMembersOptedIn(hasOptedOut), "Must be false when a player has ✖ / opted out"); + List hasOptedOutUnicode = List.of("§7All members of this party", "§7\u2716 someone has opted out"); + assertFalse(RaidGambitSafety.isAllGuildMembersOptedIn(hasOptedOutUnicode), "Must be false when a player has unicode \u2716"); + assertFalse(RaidGambitSafety.isAllGuildMembersOptedIn(List.of())); assertFalse(RaidGambitSafety.isAllGuildMembersOptedIn(null)); } @@ -188,4 +230,90 @@ void generatesGuildRaidWarningMessage() { assertEquals("This raid is not a Guild Raid, disable Require guild raid to ready in settings to ready up anyways.", RaidGambitSafety.getGuildRaidWarningMessage()); assertEquals("§cThis raid is not a Guild Raid, disable Require guild raid to ready in settings to ready up anyways.", RaidGambitSafety.getGuildRaidTooltipWarning().getString()); } + + @Test + void detectsGuildRaidItemCaseInsensitiveFromHoverNameAndLore() { + ItemStack nameStack = createItem("§a[Guild raid available]"); + assertTrue(RaidGambitSafety.isGuildRaidItem(nameStack), "Should match formatted case-insensitive hover name"); + + ItemStack exactName = createItem("Guild Raid Available"); + assertTrue(RaidGambitSafety.isGuildRaidItem(exactName), "Should match exact hover name"); + + ItemStack upperName = createItem("GUILD RAID AVAILABLE"); + assertTrue(RaidGambitSafety.isGuildRaidItem(upperName), "Should match uppercase hover name"); + + ItemStack loreStack = createItem("Snowball", "§7Difficulty Modifiers:", "§aGuild Raid Available", "§7✔ Player has opted in"); + assertTrue(RaidGambitSafety.isGuildRaidItem(loreStack), "Should match 'guild raid available' in lore lines"); + + ItemStack upperLoreStack = createItem("Snowball", "§bGUILD RAID AVAILABLE"); + assertTrue(RaidGambitSafety.isGuildRaidItem(upperLoreStack), "Should match uppercase 'GUILD RAID AVAILABLE' in lore"); + + ItemStack nonGraid = createItem("Anemic's Gambit", "§8Refreshes in 4 hours", "§aClick to Enable"); + assertFalse(RaidGambitSafety.isGuildRaidItem(nonGraid), "Gambit item must not be detected as guild raid item"); + + assertFalse(RaidGambitSafety.isGuildRaidItem(ItemStack.EMPTY), "Empty item must not be guild raid item"); + assertFalse(RaidGambitSafety.isGuildRaidItem(null), "Null item must not be guild raid item"); + } + + @Test + void isGuildRaidAvailableWithMenu() { + ItemStack graidItem = createItem("Guild Raid Available", "§7✔ player1 has opted in", "§7✔ player2 has opted in"); + AbstractContainerMenu validMenu = createMenu(graidItem); + assertTrue(RaidGambitSafety.isGuildRaidAvailable(validMenu), "Menu with graid item and all opted in must return true"); + + ItemStack graidOptOut = createItem("Guild Raid Available", "§7✔ player1 has opted in", "§7✖ player2 has opted out"); + AbstractContainerMenu optOutMenu = createMenu(graidOptOut); + assertFalse(RaidGambitSafety.isGuildRaidAvailable(optOutMenu), "Menu with opted out player must return false"); + + AbstractContainerMenu emptyMenu = createMenu(createItem("Regular Item")); + assertFalse(RaidGambitSafety.isGuildRaidAvailable(emptyMenu), "Menu without graid item must return false"); + + assertFalse(RaidGambitSafety.isGuildRaidAvailable(null), "Null menu must return false"); + } + + @Test + void evaluatesActiveBlockReasonsAndOrdering() { + config.raidGambitSafety = true; + config.raidGambitMinimum = 2; + config.requireGuildRaid = true; + + // Normal click when both fail: gambit warning must be strictly above guild raid warning + List bothFail = RaidGambitSafety.getActiveBlockReasons(33, true, false, 0, false, ClickType.PICKUP, false, config); + assertEquals(2, bothFail.size()); + assertEquals(RaidGambitSafety.getWarningMessage(2), bothFail.get(0), "Gambit error must be above guild raid error"); + assertEquals(RaidGambitSafety.getGuildRaidWarningMessage(), bothFail.get(1)); + + // Shift click when both fail: gambit check is bypassed, but guild raid safety still blocks + List shiftBothFail = RaidGambitSafety.getActiveBlockReasons(33, true, false, 0, false, ClickType.QUICK_MOVE, false, config); + assertEquals(1, shiftBothFail.size()); + assertEquals(RaidGambitSafety.getGuildRaidWarningMessage(), shiftBothFail.get(0), "Shift click must still report guild raid error"); + + // Only gambit fails, normal click: returns gambit error + List gambitOnly = RaidGambitSafety.getActiveBlockReasons(33, true, false, 1, true, ClickType.PICKUP, false, config); + assertEquals(List.of(RaidGambitSafety.getWarningMessage(2)), gambitOnly); + + // Only gambit fails, shift click: returns empty (bypassed, no popup) + List shiftGambitOnly = RaidGambitSafety.getActiveBlockReasons(33, true, false, 1, true, ClickType.QUICK_MOVE, false, config); + assertTrue(shiftGambitOnly.isEmpty(), "Shift click must bypass when only gambit safety is failing"); + + // Only guild raid fails, shift click: returns guild raid error (cannot bypass, popup triggers) + List shiftGraidOnly = RaidGambitSafety.getActiveBlockReasons(33, true, false, 2, false, ClickType.QUICK_MOVE, false, config); + assertEquals(List.of(RaidGambitSafety.getGuildRaidWarningMessage()), shiftGraidOnly); + + // Everything satisfied: returns empty + List allGood = RaidGambitSafety.getActiveBlockReasons(33, true, false, 2, true, ClickType.PICKUP, false, config); + assertTrue(allGood.isEmpty()); + } + + @Test + void popupWarningLifecycle() { + assertNull(RaidGambitSafety.getActivePopupMessages()); + + List messages = List.of("Warning 1", "Warning 2"); + RaidGambitSafety.showPopupWarning(messages); + assertEquals(messages, RaidGambitSafety.getActivePopupMessages()); + + RaidGambitSafety.clearPopupWarning(); + assertNull(RaidGambitSafety.getActivePopupMessages()); + } } From 0933d02f0ecc0019455af87da6b3a62d819e3a95 Mon Sep 17 00:00:00 2001 From: Good627 <193092356+Good627@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:42:53 +0300 Subject: [PATCH 5/5] docs(config): update requireGuildRaid docstring to reflect disallowed readying --- src/tel/eden/mod/config/BridgeConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tel/eden/mod/config/BridgeConfig.java b/src/tel/eden/mod/config/BridgeConfig.java index 05f9fb9..9091feb 100644 --- a/src/tel/eden/mod/config/BridgeConfig.java +++ b/src/tel/eden/mod/config/BridgeConfig.java @@ -271,7 +271,7 @@ public void sanitize() { /** Minimum number of gambits required to ready up normally (range 0-3, default 1). */ public int raidGambitMinimum = 1; - /** Require shift-clicking to ready up in the raid menu if the raid is not a guild raid. */ + /** Disallow readying up in the raid menu if the raid is not a guild raid. */ public boolean requireGuildRaid = false; /** Custom palette for the 10 party slots. Defaults to PartyHighlightManager.DEFAULT_PALETTE. */