From fc478774143a73aa1470e6348d75896231fa21ca Mon Sep 17 00:00:00 2001 From: Rime <81419447+Emirlol@users.noreply.github.com> Date: Thu, 27 Feb 2025 23:48:55 +0300 Subject: Chat rule location config overhaul (#1138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Chat rule location config overhaul * Fix incorrect logic in ItemTickList * Boolean → boolean * Update location name * Remove locations list * Revamp widgets in `ChatRuleConfigScreen` * Complete codec to decode both string and enumset * Take negated locations into account * Fix exclusion parsing and add tests * Clean up codec with Codec::either * Dynamic width calculation Also moves `Ignore Case` button to the next row, with the location config button. * Remove stale javadoc * Small code cleanup * Remove `UNKNOWN` and `MODERN_FORAGING_ISLAND` from the location selector * Future-proofing * Consider valid locations set of only `Location.UNKNOWN` as empty --------- Co-authored-by: Kevin <92656833+kevinthegreat1@users.noreply.github.com> --- .../config/screens/powdertracker/ItemTickList.java | 58 +- .../powdertracker/PowderFilterConfigScreen.java | 2 +- .../hysky/skyblocker/skyblock/chat/ChatRule.java | 508 ++++++------ .../skyblock/chat/ChatRuleConfigScreen.java | 857 +++++++++++++-------- .../chat/ChatRuleLocationConfigScreen.java | 72 ++ .../skyblock/chat/ChatRulesConfigListWidget.java | 12 +- .../skyblocker/skyblock/chat/ChatRulesHandler.java | 84 +- .../java/de/hysky/skyblocker/utils/CodecUtils.java | 33 +- .../de/hysky/skyblocker/utils/CollectionUtils.java | 13 + .../java/de/hysky/skyblocker/utils/Location.java | 219 +++--- .../de/hysky/skyblocker/utils/WidgetUtils.java | 27 + .../resources/assets/skyblocker/lang/en_us.json | 6 +- .../skyblocker/skyblock/chat/ChatRuleTest.java | 94 ++- 13 files changed, 1181 insertions(+), 804 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleLocationConfigScreen.java create mode 100644 src/main/java/de/hysky/skyblocker/utils/CollectionUtils.java create mode 100644 src/main/java/de/hysky/skyblocker/utils/WidgetUtils.java diff --git a/src/main/java/de/hysky/skyblocker/config/screens/powdertracker/ItemTickList.java b/src/main/java/de/hysky/skyblocker/config/screens/powdertracker/ItemTickList.java index ed67b456..db3dc933 100644 --- a/src/main/java/de/hysky/skyblocker/config/screens/powdertracker/ItemTickList.java +++ b/src/main/java/de/hysky/skyblocker/config/screens/powdertracker/ItemTickList.java @@ -9,16 +9,49 @@ import net.minecraft.client.gui.widget.CheckboxWidget; import net.minecraft.client.gui.widget.ElementListWidget; import net.minecraft.text.Text; +import java.util.Collection; import java.util.List; -public class ItemTickList extends ElementListWidget { - private final List filters; - private final List allItems; +/** + * A checkbox list for filter configuring purposes. + */ +public class ItemTickList extends ElementListWidget { + private final Collection filters; + private final Collection allItems; + private final boolean whitelist; - public ItemTickList(MinecraftClient minecraftClient, int width, int height, int y, int entryHeight, List filters, List allItems) { + /** + * + * @param minecraftClient Minecraft client. + * @param width The width of the list. + * @param height The height of the list. + * @param y The y value at which the list should render. + * @param entryHeight Height of a single item + * @param filters The items that will be marked. This should be a subset of allItems. + * @param allItems All possible values + */ + public ItemTickList(MinecraftClient minecraftClient, int width, int height, int y, int entryHeight, Collection filters, Collection allItems) { super(minecraftClient, width, height, y, entryHeight); this.filters = filters; this.allItems = allItems; + this.whitelist = false; + } + + /** + * @param minecraftClient Minecraft client. + * @param width The width of the list. + * @param height The height of the list. + * @param y The y value at which the list should render. + * @param entryHeight Height of a single item + * @param filters The items that will be marked. This should be a subset of allItems. + * @param allItems All possible values + * @param whitelist Whether the filter logic works as a whitelist or blacklist, to change whether the boxes for items in the filters collection should be checked. As an example: PowderFilter keeps which items to remove inside the filter (blacklist), while ChatRuleLocation keeps which locations the feature should work in (whitelist). + */ + public ItemTickList(MinecraftClient minecraftClient, int width, int height, int y, int entryHeight, Collection filters, Collection allItems, boolean whitelist) { + super(minecraftClient, width, height, y, entryHeight); + this.filters = filters; + this.allItems = allItems; + this.whitelist = whitelist; } public void clearAndInit() { @@ -26,14 +59,19 @@ public class ItemTickList extends ElementListWidget init(); } - public ItemTickList init() { - for (String item : allItems) { + public ItemTickList init() { + for (T item : allItems) { ItemTickEntry entry = new ItemTickEntry( - CheckboxWidget.builder(Text.of(item), client.textRenderer) - .checked(!filters.contains(item)) + CheckboxWidget.builder(Text.of(item.toString()), client.textRenderer) + .checked(whitelist == filters.contains(item)) .callback((checkbox1, checked) -> { - if (checked) filters.remove(item); - else filters.add(item); + if (whitelist) { + if (checked) filters.add(item); + else filters.remove(item); + } else { + if (checked) filters.remove(item); + else filters.add(item); + } }) .build() ); diff --git a/src/main/java/de/hysky/skyblocker/config/screens/powdertracker/PowderFilterConfigScreen.java b/src/main/java/de/hysky/skyblocker/config/screens/powdertracker/PowderFilterConfigScreen.java index 84337d7b..fbd2668a 100644 --- a/src/main/java/de/hysky/skyblocker/config/screens/powdertracker/PowderFilterConfigScreen.java +++ b/src/main/java/de/hysky/skyblocker/config/screens/powdertracker/PowderFilterConfigScreen.java @@ -34,7 +34,7 @@ public class PowderFilterConfigScreen extends Screen { assert client != null; context.drawCenteredTextWithShadow(client.textRenderer, Text.translatable("skyblocker.config.mining.crystalHollows.powderTrackerFilter.screenTitle").formatted(Formatting.BOLD), width / 2, (32 - client.textRenderer.fontHeight) / 2, 0xFFFFFF); }); - ItemTickList itemTickList = addDrawableChild(new ItemTickList(MinecraftClient.getInstance(), width, height - 96, 32, 24, filters, allItems).init()); + ItemTickList itemTickList = addDrawableChild(new ItemTickList<>(MinecraftClient.getInstance(), width, height - 96, 32, 24, filters, allItems).init()); //Grid code gratuitously stolen from WaypointsScreen. Same goes for the y and heights above. GridWidget gridWidget = new GridWidget(); gridWidget.getMainPositioner().marginX(5).marginY(2); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRule.java b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRule.java index 7fd6844d..d60b18e5 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRule.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRule.java @@ -1,263 +1,277 @@ package de.hysky.skyblocker.skyblock.chat; +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import de.hysky.skyblocker.utils.CollectionUtils; +import de.hysky.skyblocker.utils.Location; import de.hysky.skyblocker.utils.Utils; import net.minecraft.sound.SoundEvent; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; +import java.util.Arrays; +import java.util.EnumSet; import java.util.List; -import java.util.Objects; import java.util.Optional; +import java.util.function.Function; import java.util.regex.Pattern; -import com.mojang.serialization.Codec; -import com.mojang.serialization.codecs.RecordCodecBuilder; - /** * Data class to contain all the settings for a chat rule */ public class ChatRule { - private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( - Codec.STRING.fieldOf("name").forGetter(ChatRule::getName), - Codec.BOOL.fieldOf("enabled").forGetter(ChatRule::getEnabled), - Codec.BOOL.fieldOf("isPartialMatch").forGetter(ChatRule::getPartialMatch), - Codec.BOOL.fieldOf("isRegex").forGetter(ChatRule::getRegex), - Codec.BOOL.fieldOf("isIgnoreCase").forGetter(ChatRule::getIgnoreCase), - Codec.STRING.fieldOf("filter").forGetter(ChatRule::getFilter), - Codec.STRING.fieldOf("validLocations").forGetter(ChatRule::getValidLocations), - Codec.BOOL.fieldOf("hideMessage").forGetter(ChatRule::getHideMessage), - Codec.BOOL.fieldOf("showActionBar").forGetter(ChatRule::getShowActionBar), - Codec.BOOL.fieldOf("showAnnouncement").forGetter(ChatRule::getShowAnnouncement), - Codec.STRING.optionalFieldOf("replaceMessage").forGetter(ChatRule::getReplaceMessageOpt), - SoundEvent.CODEC.optionalFieldOf("customSound").forGetter(ChatRule::getCustomSoundOpt)) - .apply(instance, ChatRule::new)); - public static final Codec> LIST_CODEC = CODEC.listOf(); - - private String name; - - //inputs - private Boolean enabled; - private Boolean isPartialMatch; - private Boolean isRegex; - private Boolean isIgnoreCase; - private String filter; - private String validLocations; - - //output - private Boolean hideMessage; - private Boolean showActionBar; - private Boolean showAnnouncement; - private String replaceMessage; - private SoundEvent customSound; - /** - * Creates a chat rule with default options. - */ - protected ChatRule() { - this.name = "New Rule"; - - this.enabled = true; - this.isPartialMatch = false; - this.isRegex = false; - this.isIgnoreCase = true; - this.filter = ""; - this.validLocations = ""; - - this.hideMessage = true; - this.showActionBar = false; - this.showAnnouncement = false; - this.replaceMessage = null; - this.customSound = null; - } - - public ChatRule(String name, Boolean enabled, Boolean isPartialMatch, Boolean isRegex, Boolean isIgnoreCase, String filter, String validLocations, Boolean hideMessage, Boolean showActionBar, Boolean showAnnouncement, String replaceMessage, SoundEvent customSound) { - this.name = name; - this.enabled = enabled; - this.isPartialMatch = isPartialMatch; - this.isRegex = isRegex; - this.isIgnoreCase = isIgnoreCase; - this.filter = filter; - this.validLocations = validLocations; - this.hideMessage = hideMessage; - this.showActionBar = showActionBar; - this.showAnnouncement = showAnnouncement; - this.replaceMessage = replaceMessage; - this.customSound = customSound; - } - - private ChatRule(String name, Boolean enabled, Boolean isPartialMatch, Boolean isRegex, Boolean isIgnoreCase, String filter, String validLocations, Boolean hideMessage, Boolean showActionBar, Boolean showAnnouncement, Optional replaceMessage, Optional customSound) { - this(name, enabled, isPartialMatch, isRegex, isIgnoreCase, filter, validLocations, hideMessage, showActionBar, showAnnouncement, replaceMessage.orElse(null), customSound.orElse(null)); - } - - protected String getName() { - return name; - } - - protected void setName(String name) { - this.name = name; - } - - protected Boolean getEnabled() { - return enabled; - } - - protected void setEnabled(Boolean enabled) { - this.enabled = enabled; - } - - protected Boolean getPartialMatch() { - return isPartialMatch; - } - - protected void setPartialMatch(Boolean partialMatch) { - isPartialMatch = partialMatch; - } - - protected Boolean getRegex() { - return isRegex; - } - - protected void setRegex(Boolean regex) { - isRegex = regex; - } - - protected Boolean getIgnoreCase() { - return isIgnoreCase; - } - - protected void setIgnoreCase(Boolean ignoreCase) { - isIgnoreCase = ignoreCase; - } - - protected String getFilter() { - return filter; - } - - protected void setFilter(String filter) { - this.filter = filter; - } - - protected Boolean getHideMessage() { - return hideMessage; - } - - protected void setHideMessage(Boolean hideMessage) { - this.hideMessage = hideMessage; - } - - protected Boolean getShowActionBar() { - return showActionBar; - } - - protected void setShowActionBar(Boolean showActionBar) { - this.showActionBar = showActionBar; - } - - protected Boolean getShowAnnouncement() { - return showAnnouncement; - } - - protected void setShowAnnouncement(Boolean showAnnouncement) { - this.showAnnouncement = showAnnouncement; - } - - protected String getReplaceMessage() { - return replaceMessage; - } - - private Optional getReplaceMessageOpt() { - return replaceMessage == null ? Optional.empty() : Optional.of(replaceMessage); - } - - protected void setReplaceMessage(String replaceMessage) { - this.replaceMessage = replaceMessage; - } - - protected SoundEvent getCustomSound() { - return customSound; - } - - private Optional getCustomSoundOpt() { - return customSound == null ? Optional.empty() : Optional.of(customSound); - } - - protected void setCustomSound(SoundEvent customSound) { - this.customSound = customSound; - } - - protected String getValidLocations() { - return validLocations; - } - - protected void setValidLocations(String validLocations) { - this.validLocations = validLocations; - } - - /** - * checks every input option and if the games state and the inputted str matches them returns true. - * @param inputString the chat message to check if fits - * @return if the inputs are all true and the outputs should be performed - */ - protected Boolean isMatch(String inputString) { - //enabled - if (!enabled) return false; - - //ignore case - String testString; - String testFilter; - - if (isIgnoreCase) { - testString = inputString.toLowerCase(); - testFilter = filter.toLowerCase(); - } else { - testString = inputString; - testFilter = filter; - } - - //filter - if (testFilter.isBlank()) return false; - if (isRegex) { - if (isPartialMatch) { - if (!Pattern.compile(testFilter).matcher(testString).find()) return false; - } else { - if (!testString.matches(testFilter)) return false; - } - } else { - if (isPartialMatch) { - if (!testString.contains(testFilter)) return false; - } else { - if (!testFilter.equals(testString)) return false; - } - } - - //location - if (validLocations.isBlank()) { //if no locations do not check - return true; - } - - String cleanedMapLocation = Utils.getMap().toLowerCase().replace(" ", ""); - Boolean isLocationValid = null; - for (String validLocation : validLocations.replace(" ", "").toLowerCase().split(",")) {//the locations are split by "," and start with ! if not locations - if (validLocation == null) continue; - if (validLocation.startsWith("!")) {//not location - if (Objects.equals(validLocation.substring(1), cleanedMapLocation)) { - isLocationValid = false; - break; - } else { - isLocationValid = true; - } - } else { - if (Objects.equals(validLocation, cleanedMapLocation)) { //normal location - isLocationValid = true; - break; - } - } - } - - //if location is not in the list at all and is a not a "!" location or and is a normal location - if (isLocationValid != null && isLocationValid) { - return true; - } - - return false; - } + /** + * Codec that can decode both {@link String} and {@link EnumSet} of locations, while encoding only {@link EnumSet} of locations. + *
+ * This is necessary due to a change in how the locations are stored in the config. + */ + @VisibleForTesting + static final Codec> LOCATION_FIXING_CODEC = Codec.either(Location.SET_CODEC, Codec.STRING).xmap( + either -> either.map(Function.identity(), ChatRule::encodeString), + Either::left + ); + + private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.STRING.fieldOf("name").forGetter(ChatRule::getName), + Codec.BOOL.fieldOf("enabled").forGetter(ChatRule::getEnabled), + Codec.BOOL.fieldOf("isPartialMatch").forGetter(ChatRule::getPartialMatch), + Codec.BOOL.fieldOf("isRegex").forGetter(ChatRule::getRegex), + Codec.BOOL.fieldOf("isIgnoreCase").forGetter(ChatRule::getIgnoreCase), + Codec.STRING.fieldOf("filter").forGetter(ChatRule::getFilter), + LOCATION_FIXING_CODEC.fieldOf("validLocations").forGetter(ChatRule::getValidLocations), + Codec.BOOL.fieldOf("hideMessage").forGetter(ChatRule::getHideMessage), + Codec.BOOL.fieldOf("showActionBar").forGetter(ChatRule::getShowActionBar), + Codec.BOOL.fieldOf("showAnnouncement").forGetter(ChatRule::getShowAnnouncement), + Codec.STRING.optionalFieldOf("replaceMessage").forGetter(ChatRule::getReplaceMessageOpt), + SoundEvent.CODEC.optionalFieldOf("customSound").forGetter(ChatRule::getCustomSoundOpt) + ).apply(instance, ChatRule::new)); + public static final Codec> LIST_CODEC = CODEC.listOf(); + + private String name; + + // Inputs + private boolean enabled; + private boolean isPartialMatch; + private boolean isRegex; + private boolean isIgnoreCase; + private String filter; + private EnumSet validLocations; + + // Outputs + private boolean hideMessage; + private boolean showActionBar; + private boolean showAnnouncement; + private String replaceMessage; + private SoundEvent customSound; + + /** + * Creates a chat rule with default options. + */ + protected ChatRule() { + this.name = "New Rule"; + + this.enabled = true; + this.isPartialMatch = false; + this.isRegex = false; + this.isIgnoreCase = true; + this.filter = ""; + this.validLocations = EnumSet.noneOf(Location.class); + + this.hideMessage = true; + this.showActionBar = false; + this.showAnnouncement = false; + this.replaceMessage = null; + this.customSound = null; + } + + public ChatRule(String name, boolean enabled, boolean isPartialMatch, boolean isRegex, boolean isIgnoreCase, String filter, EnumSet validLocations, boolean hideMessage, boolean showActionBar, boolean showAnnouncement, @Nullable String replaceMessage, @Nullable SoundEvent customSound) { + this.name = name; + this.enabled = enabled; + this.isPartialMatch = isPartialMatch; + this.isRegex = isRegex; + this.isIgnoreCase = isIgnoreCase; + this.filter = filter; + this.validLocations = validLocations; + this.hideMessage = hideMessage; + this.showActionBar = showActionBar; + this.showAnnouncement = showAnnouncement; + this.replaceMessage = replaceMessage; + this.customSound = customSound; + } + + private ChatRule(String name, boolean enabled, boolean isPartialMatch, boolean isRegex, boolean isIgnoreCase, String filter, EnumSet validLocations, boolean hideMessage, boolean showActionBar, boolean showAnnouncement, Optional replaceMessage, Optional customSound) { + this(name, enabled, isPartialMatch, isRegex, isIgnoreCase, filter, validLocations, hideMessage, showActionBar, showAnnouncement, replaceMessage.orElse(null), customSound.orElse(null)); + } + + protected String getName() { + return name; + } + + protected void setName(String name) { + this.name = name; + } + + protected boolean getEnabled() { + return enabled; + } + + protected void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + protected boolean getPartialMatch() { + return isPartialMatch; + } + + protected void setPartialMatch(boolean partialMatch) { + isPartialMatch = partialMatch; + } + + protected boolean getRegex() { + return isRegex; + } + + protected void setRegex(boolean regex) { + isRegex = regex; + } + + protected boolean getIgnoreCase() { + return isIgnoreCase; + } + + protected void setIgnoreCase(boolean ignoreCase) { + isIgnoreCase = ignoreCase; + } + + protected String getFilter() { + return filter; + } + + protected void setFilter(String filter) { + this.filter = filter; + } + + protected boolean getHideMessage() { + return hideMessage; + } + + protected void setHideMessage(boolean hideMessage) { + this.hideMessage = hideMessage; + } + + protected boolean getShowActionBar() { + return showActionBar; + } + + protected void setShowActionBar(boolean showActionBar) { + this.showActionBar = showActionBar; + } + + protected boolean getShowAnnouncement() { + return showAnnouncement; + } + + protected void setShowAnnouncement(boolean showAnnouncement) { + this.showAnnouncement = showAnnouncement; + } + + protected String getReplaceMessage() { + return replaceMessage; + } + + private Optional getReplaceMessageOpt() { + return Optional.ofNullable(replaceMessage); + } + + protected void setReplaceMessage(String replaceMessage) { + this.replaceMessage = replaceMessage; + } + + protected SoundEvent getCustomSound() { + return customSound; + } + + private Optional getCustomSoundOpt() { + return Optional.ofNullable(customSound); + } + + protected void setCustomSound(SoundEvent customSound) { + this.customSound = customSound; + } + + protected EnumSet getValidLocations() { + return validLocations; + } + + protected void setValidLocations(EnumSet validLocations) { + this.validLocations = validLocations; + } + + /** + * checks every input option and if the games state and the inputted str matches them returns true. + * + * @param inputString the chat message to check if fits + * @return if the inputs are all true and the outputs should be performed + */ + protected boolean isMatch(String inputString) { + //enabled + if (!enabled) return false; + + //ignore case + String testString; + String testFilter; + + if (isIgnoreCase) { + testString = inputString.toLowerCase(); + testFilter = filter.toLowerCase(); + } else { + testString = inputString; + testFilter = filter; + } + + //filter + if (testFilter.isBlank()) return false; + if (isRegex) { + if (isPartialMatch) { + if (!Pattern.compile(testFilter).matcher(testString).find()) return false; + } else { + if (!testString.matches(testFilter)) return false; + } + } else { + if (isPartialMatch) { + if (!testString.contains(testFilter)) return false; + } else { + if (!testFilter.equals(testString)) return false; + } + } + + // As a special case, if there are no valid locations all locations are valid. + // This exists because it doesn't make sense to remove all valid locations, you should disable the chat rule if you want to do that. + // This way, we can also default to an empty set for validLocations. + if (validLocations.isEmpty()) return true; + // UNKNOWN isn't a valid location, so we act the same as the list being empty. + if (validLocations.size() == 1 && validLocations.contains(Location.UNKNOWN)) return true; + return validLocations.contains(Utils.getLocation()); + } + + // This maps invalid entries to `Location.UNKNOWN`, which is better than failing outright. + private static EnumSet encodeString(String string) { + // Necessary for empty strings, which would've been decoded as UNKNOWN otherwise. + if (string.isEmpty()) return EnumSet.noneOf(Location.class); + + // If a location's name contains a ! prefix, it's negated, meaning every location except that one is valid. + if (string.contains("!")) return EnumSet.complementOf( + Arrays.stream(string.split(", ?")) + .filter(s1 -> s1.startsWith("!")) // Filter out the non-negated locations because the negation of any element in the list already implies those non-negated locations being valid. + .map(s -> s.substring(1)) // Skip the `!` + .map(Location::fromFriendlyName) + .collect(CollectionUtils.enumSetCollector(Location.class)) + ); + return Arrays.stream(string.split(", ?")) + .map(Location::fromFriendlyName) + .collect(CollectionUtils.enumSetCollector(Location.class)); + } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleConfigScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleConfigScreen.java index 11d5b72b..54236d07 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleConfigScreen.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleConfigScreen.java @@ -1,18 +1,23 @@ package de.hysky.skyblocker.skyblock.chat; +import de.hysky.skyblocker.utils.WidgetUtils; +import it.unimi.dsi.fastutil.ints.IntIntImmutablePair; +import it.unimi.dsi.fastutil.ints.IntIntMutablePair; import it.unimi.dsi.fastutil.ints.IntIntPair; -import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.tooltip.Tooltip; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.client.gui.widget.TextWidget; +import net.minecraft.client.gui.widget.Widget; import net.minecraft.sound.SoundEvent; import net.minecraft.sound.SoundEvents; import net.minecraft.text.MutableText; import net.minecraft.text.Text; +import net.minecraft.util.Formatting; import net.minecraft.util.Identifier; - import java.awt.*; import java.util.List; import java.util.Map; @@ -20,327 +25,529 @@ import java.util.Map; import static java.util.Map.entry; public class ChatRuleConfigScreen extends Screen { - private static final int SPACER_X = 5; - private static final int SPACER_Y = 25; - - private final Map soundsLookup = Map.ofEntries( - entry(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.pling"), SoundEvents.BLOCK_NOTE_BLOCK_PLING.value()), - entry(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.cave"), SoundEvents.AMBIENT_CAVE.value()), - entry(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.zombie"), SoundEvents.ENTITY_ZOMBIE_AMBIENT), - entry(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.crit"), SoundEvents.ENTITY_PLAYER_ATTACK_CRIT), - entry(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.arrowHit"), SoundEvents.ENTITY_ARROW_HIT_PLAYER), - entry(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.amethyst"), SoundEvents.BLOCK_AMETHYST_BLOCK_HIT), - entry(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.anvil"), SoundEvents.BLOCK_ANVIL_LAND) - ); - - private int buttonWidth = 75; - - private final int chatRuleIndex; - private final ChatRule chatRule; - private TextFieldWidget nameInput; - private TextFieldWidget filterInput; - private ButtonWidget partialMatchToggle; - private ButtonWidget regexToggle; - private ButtonWidget ignoreCaseToggle; - private TextFieldWidget locationsInput; - private ButtonWidget hideMessageToggle; - private ButtonWidget actionBarToggle; - private ButtonWidget announcementToggle; - private ButtonWidget soundsToggle; - private TextFieldWidget replaceMessageInput; - - //textLocations - private IntIntPair nameLabelTextPos; - private IntIntPair inputsLabelTextPos; - private IntIntPair filterLabelTextPos; - private IntIntPair partialMatchTextPos; - private IntIntPair regexTextPos; - private IntIntPair ignoreCaseTextPos; - private IntIntPair locationLabelTextPos; - private IntIntPair outputsLabelTextPos; - private IntIntPair hideMessageTextPos; - private IntIntPair actionBarTextPos; - private IntIntPair announcementTextPos; - private IntIntPair customSoundLabelTextPos; - private IntIntPair replaceMessageLabelTextPos; - - private int currentSoundIndex; - - private final Screen parent; - - public ChatRuleConfigScreen(Screen parent, int chatRuleIndex) { - super(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen")); - this.chatRuleIndex = chatRuleIndex; - this.chatRule = ChatRulesHandler.chatRuleList.get(chatRuleIndex); - this.parent = parent; - this.currentSoundIndex = getCurrentSoundIndex(); - } - - private int getCurrentSoundIndex() { - if (chatRule.getCustomSound() == null) return -1; //if no sound just return -1 - - List soundOptions = soundsLookup.values().stream().toList(); - Identifier ruleSoundId = chatRule.getCustomSound().id(); - - for (int i = 0; i < soundOptions.size(); i++) { - if (soundOptions.get(i).id().compareTo(ruleSoundId) == 0) { - return i; - } - } - //not found - return -1; - } - - @Override - protected void init() { - super.init(); - if (client == null) return; - //start centered on the X and 1/3 down on the Y - calculateMaxButtonWidth(); - IntIntPair currentPos = IntIntPair.of((this.width - getMaxUsedWidth()) / 2,(int)((this.height - getMaxUsedHeight()) * 0.33)); - int lineXOffset; - - nameLabelTextPos = currentPos; - lineXOffset = client.textRenderer.getWidth(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.name")) + SPACER_X; - nameInput = new TextFieldWidget(client.textRenderer, currentPos.leftInt() + lineXOffset, currentPos.rightInt(), 100, 20, Text.of("")); - nameInput.setText(chatRule.getName()); - nameInput.setTooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.name.@Tooltip"))); - currentPos = IntIntPair.of(currentPos.leftInt(), currentPos.rightInt() + SPACER_Y); - - inputsLabelTextPos = currentPos; - currentPos = IntIntPair.of(currentPos.leftInt() + 10, currentPos.rightInt() + SPACER_Y); - - filterLabelTextPos = currentPos; - lineXOffset = client.textRenderer.getWidth(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.filter")) + SPACER_X; - filterInput = new TextFieldWidget(client.textRenderer, currentPos.leftInt() + lineXOffset, currentPos.rightInt(), 200, 20, Text.of("")); - filterInput.setMaxLength(96); - filterInput.setText(chatRule.getFilter()); - filterInput.setTooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.filter.@Tooltip"))); - currentPos = IntIntPair.of(currentPos.leftInt(),currentPos.rightInt() + SPACER_Y); - lineXOffset = 0; - - partialMatchTextPos = IntIntPair.of(currentPos.leftInt() + lineXOffset, currentPos.rightInt()); - lineXOffset += client.textRenderer.getWidth(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.partialMatch")) + SPACER_X; - partialMatchToggle = ButtonWidget.builder(enabledButtonText(chatRule.getPartialMatch()), a -> { - chatRule.setPartialMatch(!chatRule.getPartialMatch()); - partialMatchToggle.setMessage(enabledButtonText(chatRule.getPartialMatch())); - }) - .position(currentPos.leftInt() + lineXOffset, currentPos.rightInt()) - .size(buttonWidth,20) - .tooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.partialMatch.@Tooltip"))) - .build(); - lineXOffset += buttonWidth + SPACER_X; - regexTextPos = IntIntPair.of(currentPos.leftInt() + lineXOffset,currentPos.rightInt()); - lineXOffset += client.textRenderer.getWidth(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.regex")) + SPACER_X; - regexToggle = ButtonWidget.builder(enabledButtonText(chatRule.getRegex()), a -> { - chatRule.setRegex(!chatRule.getRegex()); - regexToggle.setMessage(enabledButtonText(chatRule.getRegex())); - }) - .position(currentPos.leftInt() + lineXOffset, currentPos.rightInt()) - .size(buttonWidth,20) - .tooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.regex.@Tooltip"))) - .build(); - lineXOffset += buttonWidth + SPACER_X; - ignoreCaseTextPos = IntIntPair.of(currentPos.leftInt() + lineXOffset,currentPos.rightInt()); - lineXOffset += client.textRenderer.getWidth(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.ignoreCase")) + SPACER_X; - ignoreCaseToggle = ButtonWidget.builder(enabledButtonText(chatRule.getIgnoreCase()), a -> { - chatRule.setIgnoreCase(!chatRule.getIgnoreCase()); - ignoreCaseToggle.setMessage(enabledButtonText(chatRule.getIgnoreCase())); - }) - .position(currentPos.leftInt() + lineXOffset, currentPos.rightInt()) - .size(buttonWidth,20) - .tooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.ignoreCase.@Tooltip"))) - .build(); - currentPos = IntIntPair.of(currentPos.leftInt(), currentPos.rightInt() + SPACER_Y); - - locationLabelTextPos = currentPos; - lineXOffset = client.textRenderer.getWidth(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.locations")) + SPACER_X; - locationsInput = new TextFieldWidget(client.textRenderer, currentPos.leftInt() + lineXOffset, currentPos.rightInt(), 200, 20, Text.of("")); - locationsInput.setMaxLength(96); - locationsInput.setText(chatRule.getValidLocations()); - MutableText locationToolTip = Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.locations.@Tooltip"); - locationToolTip.append("\n"); - ChatRulesHandler.locationsList.forEach(location -> locationToolTip.append(" " + location + ",\n")); - locationsInput.setTooltip(Tooltip.of(locationToolTip)); - currentPos = IntIntPair.of(currentPos.leftInt(), currentPos.rightInt() + SPACER_Y); - - outputsLabelTextPos = IntIntPair.of(currentPos.leftInt() - 10,currentPos.rightInt()); - currentPos = IntIntPair.of(currentPos.leftInt(), currentPos.rightInt() + SPACER_Y); - - hideMessageTextPos = currentPos; - lineXOffset = client.textRenderer.getWidth(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.hideMessage")) + SPACER_X; - hideMessageToggle = ButtonWidget.builder(enabledButtonText(chatRule.getHideMessage()), a -> { - chatRule.setHideMessage(!chatRule.getHideMessage()); - hideMessageToggle.setMessage(enabledButtonText(chatRule.getHideMessage())); - }) - .position(currentPos.leftInt() + lineXOffset, currentPos.rightInt()) - .size(buttonWidth,20) - .tooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.hideMessage.@Tooltip"))) - .build(); - lineXOffset += buttonWidth + SPACER_X; - actionBarTextPos = IntIntPair.of(currentPos.leftInt() + lineXOffset,currentPos.rightInt()); - lineXOffset += client.textRenderer.getWidth(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.actionBar")) + SPACER_X; - actionBarToggle = ButtonWidget.builder(enabledButtonText(chatRule.getShowActionBar()), a -> { - chatRule.setShowActionBar(!chatRule.getShowActionBar()); - actionBarToggle.setMessage(enabledButtonText(chatRule.getShowActionBar())); - }) - .position(currentPos.leftInt() + lineXOffset, currentPos.rightInt()) - .size(buttonWidth,20) - .tooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.actionBar.@Tooltip"))) - .build(); - lineXOffset = 0; - currentPos = IntIntPair.of(currentPos.leftInt(), currentPos.rightInt() + SPACER_Y); - - announcementTextPos = IntIntPair.of(currentPos.leftInt() + lineXOffset, currentPos.rightInt()); - lineXOffset += client.textRenderer.getWidth(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.announcement")) + SPACER_X; - announcementToggle = ButtonWidget.builder(enabledButtonText(chatRule.getShowAnnouncement()), a -> { - chatRule.setShowAnnouncement(!chatRule.getShowAnnouncement()); - announcementToggle.setMessage(enabledButtonText(chatRule.getShowAnnouncement())); - }) - .position(currentPos.leftInt() + lineXOffset, currentPos.rightInt()) - .size(buttonWidth,20) - .tooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.announcement.@Tooltip"))) - .build(); - lineXOffset += buttonWidth + SPACER_X; - customSoundLabelTextPos = IntIntPair.of(currentPos.leftInt() + lineXOffset,currentPos.rightInt()); - lineXOffset += client.textRenderer.getWidth(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds")) + SPACER_X; - soundsToggle = ButtonWidget.builder(getSoundName(), a -> { - currentSoundIndex += 1; - if (currentSoundIndex == soundsLookup.size()) { - currentSoundIndex = -1; - } - MutableText newText = getSoundName(); - soundsToggle.setMessage(newText); - SoundEvent sound = soundsLookup.get(newText); - chatRule.setCustomSound(sound); - if (client.player != null && sound != null) { - client.player.playSound(sound, 100f, 0.1f); - }}) - .position(currentPos.leftInt() + lineXOffset, currentPos.rightInt()) - .size(buttonWidth,20) - .tooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.@Tooltip"))) - .build(); - currentPos = IntIntPair.of(currentPos.leftInt(), currentPos.rightInt() + SPACER_Y); - - replaceMessageLabelTextPos = currentPos; - lineXOffset = client.textRenderer.getWidth(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.replace")) + SPACER_X; - replaceMessageInput = new TextFieldWidget(client.textRenderer, currentPos.leftInt() + lineXOffset, currentPos.rightInt(), 200, 20, Text.of("")); - replaceMessageInput.setMaxLength(96); - replaceMessageInput.setTooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.replace.@Tooltip"))); - replaceMessageInput.setText(chatRule.getReplaceMessage()); - - ButtonWidget finishButton = ButtonWidget.builder(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.finish"), a -> close()) - .position(this.width - buttonWidth - SPACER_Y, this.height - SPACER_Y) - .size(buttonWidth, 20) - .build(); - - addDrawableChild(nameInput); - addDrawableChild(filterInput); - addDrawableChild(partialMatchToggle); - addDrawableChild(regexToggle); - addDrawableChild(ignoreCaseToggle); - addDrawableChild(locationsInput); - addDrawableChild(hideMessageToggle); - addDrawableChild(actionBarToggle); - addDrawableChild(announcementToggle); - addDrawableChild(soundsToggle); - addDrawableChild(replaceMessageInput); - addDrawableChild(finishButton); - } - - /** - * if the maxUsedWidth is above the available width decrease the button width to fix this - */ - private void calculateMaxButtonWidth() { - if (client == null || client.currentScreen == null) return; - buttonWidth = 75; - int available = client.currentScreen.width - getMaxUsedWidth() - SPACER_X * 2; - if (available >= 0) return; //keep the largest size if room - buttonWidth += available / 3; //remove the needed width from the width of the total 3 buttons - buttonWidth = Math.max(10,buttonWidth); //do not let the width go below 10 - } - - /** - * Works out the width of the maximum line - * @return the max used width - */ - private int getMaxUsedWidth() { - if (client == null) return 0; - //text - int total = client.textRenderer.getWidth(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.partialMatch")); - total += client.textRenderer.getWidth(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.regex")); - total += client.textRenderer.getWidth(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.ignoreCase")); - //space - total += SPACER_X * 6; - //button width - total += buttonWidth * 3; - return total; - } - - /** - * Works out the height used - * @return height used by the gui - */ - private int getMaxUsedHeight() { - //there are 8 rows so just times the spacer by 8 - return SPACER_Y * 8; - } - - private Text enabledButtonText(boolean enabled) { - if (enabled) { - return Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.true").withColor(Color.GREEN.getRGB()); - } else { - return Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.false").withColor(Color.RED.getRGB()); - } - } - - @Override - public void render(DrawContext context, int mouseX, int mouseY, float delta) { - super.render(context, mouseX, mouseY, delta); - context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 16, 0xFFFFFFFF); - - //draw labels ands text - int yOffset = (SPACER_Y - this.textRenderer.fontHeight) / 2; - context.drawTextWithShadow(this.textRenderer, Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.inputs"), inputsLabelTextPos.leftInt(), inputsLabelTextPos.rightInt() + yOffset, 0xFFFFFFFF); - context.drawTextWithShadow(this.textRenderer, Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.name"), nameLabelTextPos.leftInt(), nameLabelTextPos.rightInt() + yOffset, 0xFFFFFFFF); - context.drawTextWithShadow(this.textRenderer, Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.filter"), filterLabelTextPos.leftInt(), filterLabelTextPos.rightInt() + yOffset, 0xFFFFFFFF); - context.drawTextWithShadow(this.textRenderer, Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.partialMatch"), partialMatchTextPos.leftInt(), partialMatchTextPos.rightInt() + yOffset, 0xFFFFFFFF); - context.drawTextWithShadow(this.textRenderer, Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.regex"), regexTextPos.leftInt(), regexTextPos.rightInt() + yOffset, 0xFFFFFFFF); - context.drawTextWithShadow(this.textRenderer, Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.ignoreCase"), ignoreCaseTextPos.leftInt(), ignoreCaseTextPos.rightInt() + yOffset, 0xFFFFFFFF); - context.drawTextWithShadow(this.textRenderer, Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.locations"), locationLabelTextPos.leftInt(), locationLabelTextPos.rightInt() + yOffset, 0xFFFFFFFF); - context.drawTextWithShadow(this.textRenderer, Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.outputs"), outputsLabelTextPos.leftInt(), outputsLabelTextPos.rightInt() + yOffset, 0xFFFFFFFF); - context.drawTextWithShadow(this.textRenderer, Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.hideMessage"), hideMessageTextPos.leftInt(), hideMessageTextPos.rightInt() + yOffset, 0xFFFFFFFF); - context.drawTextWithShadow(this.textRenderer, Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.actionBar"), actionBarTextPos.leftInt(), actionBarTextPos.rightInt() + yOffset, 0xFFFFFFFF); - context.drawTextWithShadow(this.textRenderer, Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.announcement"), announcementTextPos.leftInt(), announcementTextPos.rightInt() + yOffset, 0xFFFFFFFF); - context.drawTextWithShadow(this.textRenderer, Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds"), customSoundLabelTextPos.leftInt(), customSoundLabelTextPos.rightInt() + yOffset, 0xFFFFFFFF); - context.drawTextWithShadow(this.textRenderer, Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.replace"), replaceMessageLabelTextPos.leftInt(), replaceMessageLabelTextPos.rightInt() + yOffset, 0xFFFFFFFF); - } - - /** - * Saves and returns to parent screen - */ - @Override - public void close() { - if (client != null) { - save(); - client.setScreen(parent); - } - } - - private void save() { - chatRule.setName(nameInput.getText()); - chatRule.setFilter(filterInput.getText()); - chatRule.setReplaceMessage(replaceMessageInput.getText()); - chatRule.setValidLocations(locationsInput.getText()); - - ChatRulesHandler.chatRuleList.set(chatRuleIndex, chatRule); - } - - private MutableText getSoundName() { - if (currentSoundIndex == -1) { - return Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.none"); - } - - return soundsLookup.keySet().stream().toList().get(currentSoundIndex); - } + private static final int SPACER_X = 5; + private static final int SPACER_Y = 5; + + private final Map soundsLookup = Map.ofEntries( + entry(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.pling"), SoundEvents.BLOCK_NOTE_BLOCK_PLING.value()), + entry(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.cave"), SoundEvents.AMBIENT_CAVE.value()), + entry(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.zombie"), SoundEvents.ENTITY_ZOMBIE_AMBIENT), + entry(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.crit"), SoundEvents.ENTITY_PLAYER_ATTACK_CRIT), + entry(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.arrowHit"), SoundEvents.ENTITY_ARROW_HIT_PLAYER), + entry(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.amethyst"), SoundEvents.BLOCK_AMETHYST_BLOCK_HIT), + entry(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.anvil"), SoundEvents.BLOCK_ANVIL_LAND) + ); + + private static final int MAX_WIDTH = 360; + private static final int BUTTON_WIDTH = 75; // Placeholder for all buttons except the finish button, the others are calculated dynamically but still need an initial value + private static final int ROW_HEIGHT = ButtonWidget.DEFAULT_HEIGHT; + private static final int Y_OFFSET = (ROW_HEIGHT - MinecraftClient.getInstance().textRenderer.fontHeight) / 2; + + private final int chatRuleIndex; + private final ChatRule chatRule; + private final TextFieldWidget nameInput; + private final TextFieldWidget filterInput; + private ButtonWidget partialMatchToggle; + private ButtonWidget regexToggle; + private ButtonWidget ignoreCaseToggle; + private final ButtonWidget locationsConfigButton; + private ButtonWidget hideMessageToggle; + private ButtonWidget actionBarToggle; + private ButtonWidget announcementToggle; + private ButtonWidget soundsToggle; + private final ButtonWidget finishButton; + private final TextFieldWidget replaceMessageInput; + + // Text widgets + private final TextWidget nameLabel; + private final TextWidget inputsLabel; + private final TextWidget filterLabel; + private final TextWidget partialMatchLabel; + private final TextWidget regexLabel; + private final TextWidget ignoreCaseLabel; + private final TextWidget locationLabel; + private final TextWidget outputsLabel; + private final TextWidget hideMessageLabel; + private final TextWidget actionBarLabel; + private final TextWidget announcementLabel; + private final TextWidget soundsLabel; + private final TextWidget replaceMessageLabel; + private final TextWidget titleWidget; + + private int currentSoundIndex; + + private final Screen parent; + + public ChatRuleConfigScreen(Screen parent, int chatRuleIndex) { + super(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen")); + this.chatRuleIndex = chatRuleIndex; + this.chatRule = ChatRulesHandler.chatRuleList.get(chatRuleIndex); + this.parent = parent; + this.currentSoundIndex = getCurrentSoundIndex(); + + // Early initialization of values from the static instance because we want to initialize this stuff in the constructor + this.client = MinecraftClient.getInstance(); + this.width = client.getWindow().getScaledWidth(); + this.height = client.getWindow().getScaledHeight(); + + // Title + titleWidget = new TextWidget(0, 16, this.width, client.textRenderer.fontHeight, getTitle(), client.textRenderer).alignCenter(); + + // Start centered + IntIntPair rootPos = getRootPos(); + IntIntMutablePair currentPos = IntIntMutablePair.of(0, 0); // Offset from root pos, add them up and we get the actual position + int yOffset = (ROW_HEIGHT - client.textRenderer.fontHeight) / 2; + + // Row 1, name + Text nameText = Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.name"); + nameLabel = textWidget(rootPos, currentPos, yOffset, nameText); + nextColumn(currentPos, client.textRenderer.getWidth(nameText)); + + int textFieldWidth = 200; // Placeholder value, their size is calculated dynamically afterward + nameInput = new TextFieldWidget(client.textRenderer, rootPos.leftInt() + currentPos.leftInt(), rootPos.rightInt() + currentPos.rightInt(), textFieldWidth, ROW_HEIGHT, Text.of("")); + nameInput.setText(chatRule.getName()); + nameInput.setTooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.name.@Tooltip"))); + nextRow(currentPos); + + // Row 2, inputs header + + inputsLabel = textWidget(rootPos, currentPos, yOffset, Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.inputs").formatted(Formatting.BOLD)); + nextRow(currentPos); + + // Row 3, filter + + Text filterText = Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.filter"); + filterLabel = textWidget(currentPos, rootPos, yOffset, filterText); + nextColumn(currentPos, client.textRenderer.getWidth(filterText)); + filterInput = new TextFieldWidget(client.textRenderer, rootPos.leftInt() + currentPos.leftInt(), rootPos.rightInt() + currentPos.rightInt(), textFieldWidth, ROW_HEIGHT, Text.of("")); + filterInput.setMaxLength(96); + filterInput.setText(chatRule.getFilter()); + filterInput.setTooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.filter.@Tooltip"))); + nextRow(currentPos); + + // Row 4, partial match and regex + + Text partialMatchText = Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.partialMatch"); + partialMatchLabel = textWidget(rootPos, currentPos, yOffset, partialMatchText); + nextColumn(currentPos, client.textRenderer.getWidth(partialMatchText)); + partialMatchToggle = ButtonWidget.builder(enabledButtonText(chatRule.getPartialMatch()), a -> { + chatRule.setPartialMatch(!chatRule.getPartialMatch()); + partialMatchToggle.setMessage(enabledButtonText(chatRule.getPartialMatch())); + }) + .position(rootPos.leftInt() + currentPos.leftInt(), rootPos.rightInt() + currentPos.rightInt()) + .size(BUTTON_WIDTH, ROW_HEIGHT) + .tooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.partialMatch.@Tooltip"))) + .build(); + nextColumn(currentPos, BUTTON_WIDTH); + + Text regexLabelText = Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.regex"); + regexLabel = textWidget(rootPos, currentPos, yOffset, regexLabelText); + nextColumn(currentPos, client.textRenderer.getWidth(regexLabelText)); + regexToggle = ButtonWidget.builder(enabledButtonText(chatRule.getRegex()), a -> { + chatRule.setRegex(!chatRule.getRegex()); + regexToggle.setMessage(enabledButtonText(chatRule.getRegex())); + }) + .position(rootPos.leftInt() + currentPos.leftInt(), rootPos.rightInt() + currentPos.rightInt()) + .size(BUTTON_WIDTH, ROW_HEIGHT) + .tooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.regex.@Tooltip"))) + .build(); + nextRow(currentPos); + + // Row 5, ignore case and location selection + Text ignoreCaseText = Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.ignoreCase"); + ignoreCaseLabel = textWidget(rootPos, currentPos, yOffset, ignoreCaseText); + nextColumn(currentPos, client.textRenderer.getWidth(ignoreCaseText)); + ignoreCaseToggle = ButtonWidget.builder(enabledButtonText(chatRule.getIgnoreCase()), a -> { + chatRule.setIgnoreCase(!chatRule.getIgnoreCase()); + ignoreCaseToggle.setMessage(enabledButtonText(chatRule.getIgnoreCase())); + }) + .position(rootPos.leftInt() + currentPos.leftInt(), rootPos.rightInt() + currentPos.rightInt()) + .size(BUTTON_WIDTH, ROW_HEIGHT) + .tooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.ignoreCase.@Tooltip"))) + .build(); + nextColumn(currentPos, BUTTON_WIDTH); + + Text locationsText = Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.locations"); + locationLabel = textWidget(rootPos, currentPos, yOffset, locationsText); + nextColumn(currentPos, client.textRenderer.getWidth(locationsText)); + + locationsConfigButton = ButtonWidget.builder(Text.translatable("text.skyblocker.open"), + widget -> client.setScreen(new ChatRuleLocationConfigScreen(this, chatRule))) + .tooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.locations.@Tooltip"))) + .dimensions(rootPos.leftInt() + currentPos.leftInt(), rootPos.rightInt() + currentPos.rightInt(), BUTTON_WIDTH, ROW_HEIGHT) + .build(); + + nextRow(currentPos); + + // Row 6, outputs header + + outputsLabel = textWidget(rootPos, currentPos, yOffset, Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.outputs").formatted(Formatting.BOLD)); + nextRow(currentPos); + + // Row 7, hide message, action bar, announcement checkboxes and sound selection + + Text hideMessageText = Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.hideMessage"); + hideMessageLabel = textWidget(rootPos, currentPos, yOffset, hideMessageText); + nextColumn(currentPos, client.textRenderer.getWidth(hideMessageText)); + hideMessageToggle = ButtonWidget.builder(enabledButtonText(chatRule.getHideMessage()), a -> { + chatRule.setHideMessage(!chatRule.getHideMessage()); + hideMessageToggle.setMessage(enabledButtonText(chatRule.getHideMessage())); + }) + .position(rootPos.leftInt() + currentPos.leftInt(), rootPos.rightInt() + currentPos.rightInt()) + .size(BUTTON_WIDTH, ROW_HEIGHT) + .tooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.hideMessage.@Tooltip"))) + .build(); + nextColumn(currentPos, BUTTON_WIDTH); + + Text actionBarText = Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.actionBar"); + actionBarLabel = textWidget(rootPos, currentPos, yOffset, actionBarText); + nextColumn(currentPos, client.textRenderer.getWidth(actionBarText)); + actionBarToggle = ButtonWidget.builder(enabledButtonText(chatRule.getShowActionBar()), a -> { + chatRule.setShowActionBar(!chatRule.getShowActionBar()); + actionBarToggle.setMessage(enabledButtonText(chatRule.getShowActionBar())); + }) + .position(rootPos.leftInt() + currentPos.leftInt(), rootPos.rightInt() + currentPos.rightInt()) + .size(BUTTON_WIDTH, ROW_HEIGHT) + .tooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.actionBar.@Tooltip"))) + .build(); + nextRow(currentPos); + + // Row 8, announcement, sounds + + Text announcementText = Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.announcement"); + announcementLabel = textWidget(rootPos, currentPos, yOffset, announcementText); + nextColumn(currentPos, client.textRenderer.getWidth(announcementText)); + announcementToggle = ButtonWidget.builder(enabledButtonText(chatRule.getShowAnnouncement()), a -> { + chatRule.setShowAnnouncement(!chatRule.getShowAnnouncement()); + announcementToggle.setMessage(enabledButtonText(chatRule.getShowAnnouncement())); + }) + .position(rootPos.leftInt() + currentPos.leftInt(), rootPos.rightInt() + currentPos.rightInt()) + .size(BUTTON_WIDTH, ROW_HEIGHT) + .tooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.announcement.@Tooltip"))) + .build(); + nextColumn(currentPos, BUTTON_WIDTH); + + Text soundsText = Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds"); + soundsLabel = textWidget(rootPos, currentPos, yOffset, soundsText); + nextColumn(currentPos, client.textRenderer.getWidth(soundsText)); + + soundsToggle = ButtonWidget.builder(getSoundName(), a -> { + currentSoundIndex += 1; + if (currentSoundIndex == soundsLookup.size()) { + currentSoundIndex = -1; + } + MutableText newText = getSoundName(); + soundsToggle.setMessage(newText); + SoundEvent sound = soundsLookup.get(newText); + chatRule.setCustomSound(sound); + if (client.player != null && sound != null) { + client.player.playSound(sound, 100f, 0.1f); + } + }) + .position(rootPos.leftInt() + currentPos.leftInt(), rootPos.rightInt() + currentPos.rightInt()) + .size(BUTTON_WIDTH, ROW_HEIGHT) + .tooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.sounds.@Tooltip"))) + .build(); + nextRow(currentPos); + + // Row 9, replacement message + + Text replaceMessageText = Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.replace"); + replaceMessageLabel = textWidget(rootPos, currentPos, yOffset, replaceMessageText); + nextColumn(currentPos, client.textRenderer.getWidth(replaceMessageText)); + replaceMessageInput = new TextFieldWidget(client.textRenderer, rootPos.leftInt() + currentPos.leftInt(), rootPos.rightInt() + currentPos.rightInt(), textFieldWidth, ROW_HEIGHT, Text.of("")); + replaceMessageInput.setMaxLength(96); + replaceMessageInput.setTooltip(Tooltip.of(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.replace.@Tooltip"))); + replaceMessageInput.setText(chatRule.getReplaceMessage()); + + // Finish button at bottom right corner + + finishButton = ButtonWidget.builder(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.finish"), a -> close()) + .position(this.width - BUTTON_WIDTH - SPACER_Y, this.height - SPACER_Y) + .size(BUTTON_WIDTH, ROW_HEIGHT) + .build(); + calculateTextFieldWidths(); + calculateButtonWidths(); + } + + private int getCurrentSoundIndex() { + if (chatRule.getCustomSound() == null) return -1; //if no sound just return -1 + + List soundOptions = soundsLookup.values().stream().toList(); + Identifier ruleSoundId = chatRule.getCustomSound().id(); + + for (int i = 0; i < soundOptions.size(); i++) { + if (soundOptions.get(i).id().compareTo(ruleSoundId) == 0) { + return i; + } + } + //not found + return -1; + } + + @Override + protected void init() { + recalculateWidgetPositions(); + // Title + addDrawableChild(titleWidget); + // Row 1 + addDrawableChild(nameInput); + addDrawableChild(nameLabel); + // Row 2 + addDrawableChild(inputsLabel); + // Row 3 + addDrawableChild(filterInput); + addDrawableChild(filterLabel); + // Row 4 + addDrawableChild(partialMatchToggle); + addDrawableChild(partialMatchLabel); + addDrawableChild(regexToggle); + addDrawableChild(regexLabel); + // Row 5 + addDrawableChild(ignoreCaseToggle); + addDrawableChild(ignoreCaseLabel); + addDrawableChild(locationsConfigButton); + addDrawableChild(locationLabel); + // Row 6 + addDrawableChild(outputsLabel); + // Row 7 + addDrawableChild(hideMessageToggle); + addDrawableChild(hideMessageLabel); + addDrawableChild(actionBarToggle); + addDrawableChild(actionBarLabel); + // Row 8 + addDrawableChild(announcementToggle); + addDrawableChild(announcementLabel); + addDrawableChild(soundsToggle); + addDrawableChild(soundsLabel); + // Row 9 + addDrawableChild(replaceMessageInput); + addDrawableChild(replaceMessageLabel); + // Finish button + addDrawableChild(finishButton); + calculateTextFieldWidths(); + calculateButtonWidths(); + } + + private void recalculateWidgetPositions() { + IntIntPair rootPos = getRootPos(); + IntIntMutablePair currentPos = IntIntMutablePair.of(0, 0); // Offset from root pos, add them up and we get the actual position + assert client != null; + // Title + titleWidget.setWidth(this.width); + // Row 1 + setWidgetPosition(nameLabel, rootPos, currentPos, Y_OFFSET); + nextColumn(currentPos, client.textRenderer.getWidth(nameLabel.getMessage())); + setWidgetPosition(nameInput, rootPos, currentPos); + nextRow(currentPos); + // Row 2 + setWidgetPosition(inputsLabel, rootPos, currentPos, Y_OFFSET); + nextRow(currentPos); + // Row 3 + setWidgetPosition(filterLabel, rootPos, currentPos, Y_OFFSET); + nextColumn(currentPos, client.textRenderer.getWidth(filterLabel.getMessage())); + setWidgetPosition(filterInput, rootPos, currentPos); + nextRow(currentPos); + // Row 4 + setWidgetPosition(partialMatchLabel, rootPos, currentPos, Y_OFFSET); + nextColumn(currentPos, client.textRenderer.getWidth(partialMatchLabel.getMessage())); + setWidgetPosition(partialMatchToggle, rootPos, currentPos); + partialMatchToggle.setWidth(BUTTON_WIDTH); + nextColumn(currentPos, BUTTON_WIDTH); + setWidgetPosition(regexLabel, rootPos, currentPos, Y_OFFSET); + nextColumn(currentPos, client.textRenderer.getWidth(regexLabel.getMessage())); + setWidgetPosition(regexToggle, rootPos, currentPos); + regexToggle.setWidth(BUTTON_WIDTH); + nextRow(currentPos); + // Row 5 + setWidgetPosition(ignoreCaseLabel, rootPos, currentPos, Y_OFFSET); + nextColumn(currentPos, client.textRenderer.getWidth(ignoreCaseLabel.getMessage())); + setWidgetPosition(ignoreCaseToggle, rootPos, currentPos); + ignoreCaseToggle.setWidth(BUTTON_WIDTH); + nextColumn(currentPos, BUTTON_WIDTH); + setWidgetPosition(locationLabel, rootPos, currentPos, Y_OFFSET); + nextColumn(currentPos, client.textRenderer.getWidth(locationLabel.getMessage())); + setWidgetPosition(locationsConfigButton, rootPos, currentPos); + locationsConfigButton.setWidth(BUTTON_WIDTH); + nextRow(currentPos); + // Row 6 + setWidgetPosition(outputsLabel, rootPos, currentPos, Y_OFFSET); + nextRow(currentPos); + // Row 7 + setWidgetPosition(hideMessageLabe