diff options
Diffstat (limited to 'src/main/java/de')
22 files changed, 1683 insertions, 46 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index 36ef5a4f..2987f493 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -2,10 +2,14 @@ package de.hysky.skyblocker; import com.google.gson.Gson; import com.google.gson.GsonBuilder; + +import de.hysky.skyblocker.config.ImageRepoLoader; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.debug.Debug; import de.hysky.skyblocker.skyblock.*; +import de.hysky.skyblocker.skyblock.chat.ChatRuleAnnouncementScreen; import de.hysky.skyblocker.skyblock.crimson.kuudra.Kuudra; +import de.hysky.skyblocker.skyblock.chat.ChatRulesHandler; import de.hysky.skyblocker.skyblock.dungeon.*; import de.hysky.skyblocker.skyblock.dungeon.partyfinder.PartyFinderScreen; import de.hysky.skyblocker.skyblock.dungeon.puzzle.*; @@ -92,6 +96,7 @@ public class SkyblockerMod implements ClientModInitializer { SkyblockerConfigManager.init(); Tips.init(); NEURepoManager.init(); + ImageRepoLoader.init(); ItemRepository.init(); PlayerHeadHashCache.init(); HotbarSlotLock.init(); @@ -108,6 +113,8 @@ public class SkyblockerMod implements ClientModInitializer { CrystalsLocationsManager.init(); ChatMessageListener.init(); Shortcuts.init(); + ChatRulesHandler.init(); + ChatRuleAnnouncementScreen.init(); DiscordRPCManager.init(); LividColor.init(); FishingHelper.init(); diff --git a/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java b/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java index 8b0f27a7..781f7f15 100644 --- a/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java +++ b/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java @@ -1,16 +1,26 @@ package de.hysky.skyblocker.config; import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.OptionDescription; import dev.isxander.yacl3.api.controller.*; import net.minecraft.text.Text; import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.FileUtils; +import java.nio.file.Path; import java.util.function.Function; public class ConfigUtils { public static final ValueFormatter<Formatting> FORMATTING_FORMATTER = formatting -> Text.literal(StringUtils.capitalize(formatting.getName().replaceAll("_", " "))); public static final ValueFormatter<Float> FLOAT_TWO_FORMATTER = value -> Text.literal(String.format("%,.2f", value).replaceAll("[\u00a0\u202F]", " ")); + private static final Path IMAGE_DIRECTORY = ImageRepoLoader.REPO_DIRECTORY.resolve("Skyblocker-Assets-images"); public static BooleanControllerBuilder createBooleanController(Option<Boolean> opt) { return BooleanControllerBuilder.create(opt).yesNoFormatter().coloured(true); @@ -34,4 +44,15 @@ public class ConfigUtils { public static <E extends Enum<E>> Function<Option<E>, ControllerBuilder<E>> getEnumDropdownControllerFactory(ValueFormatter<E> formatter) { return opt -> EnumDropdownControllerBuilder.create(opt).formatValue(formatter); } + + /** + * Creates an {@link OptionDescription} with an image and text. + */ + @SafeVarargs + public static OptionDescription withImage(Path imagePath, @Nullable Text... texts) { + return OptionDescription.createBuilder() + .text(ArrayUtils.isNotEmpty(texts) ? texts : new Text[] {}) + .image(IMAGE_DIRECTORY.resolve(imagePath), new Identifier(SkyblockerMod.NAMESPACE, "config_image_" + FileUtils.normalizePath(imagePath))) + .build(); + } } diff --git a/src/main/java/de/hysky/skyblocker/config/ImageRepoLoader.java b/src/main/java/de/hysky/skyblocker/config/ImageRepoLoader.java new file mode 100644 index 00000000..0591cd96 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/config/ImageRepoLoader.java @@ -0,0 +1,144 @@ +package de.hysky.skyblocker.config; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.CompletableFuture; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.slf4j.Logger; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.FileUtils; +import de.hysky.skyblocker.utils.Http; + +public class ImageRepoLoader { + private static final Logger LOGGER = LogUtils.getLogger(); + static final Path REPO_DIRECTORY = SkyblockerMod.CONFIG_DIR.resolve("image-repo"); + private static final String BRANCH_INFO = "https://api.github.com/repos/SkyblockerMod/Skyblocker-Assets/branches/images"; + private static final String REPO_DOWNLOAD = "https://github.com/SkyblockerMod/Skyblocker-Assets/archive/refs/heads/images.zip"; + private static final String PLACEHOLDER_HASH = "None!"; + + public static void init() { + update(0); + } + + /** + * Attempts to update/load the image repository, if any errors are encountered it will try 3 times. + */ + private static void update(int retries) { + CompletableFuture.runAsync(() -> { + if (retries < 3) { + try { + long start = System.currentTimeMillis(); + //Retrieve the saved commit hash + String savedCommitHash = checkSavedCommitData(); + + //Fetch the latest commit data + JsonObject response = JsonParser.parseString(Http.sendGetRequest(BRANCH_INFO)).getAsJsonObject(); + String latestCommitHash = response.getAsJsonObject("commit").get("sha").getAsString(); + + //Download the repository if there was a new commit + if (!savedCommitHash.equals(latestCommitHash)) { + InputStream in = Http.downloadContent(REPO_DOWNLOAD); + + //Delete all directories to clear potentially now unused/old files + //TODO change this to only delete periodically? + deleteDirectories(); + + try (ZipInputStream zis = new ZipInputStream(in)) { + ZipEntry entry; + + while ((entry = zis.getNextEntry()) != null) { + Path outputFile = REPO_DIRECTORY.resolve(entry.getName()); + + if (entry.isDirectory()) { + Files.createDirectories(outputFile); + } else { + Files.createDirectories(outputFile.getParent()); + Files.copy(zis, outputFile, StandardCopyOption.REPLACE_EXISTING); + } + } + } + + writeCommitData(latestCommitHash); + + long end = System.currentTimeMillis(); + LOGGER.info("[Skyblocker] Successfully updated the Image Respository in {} ms! {} → {}", end - start, savedCommitHash, latestCommitHash); + } else { + LOGGER.info("[Skyblocker] The Image Respository is up to date!"); + } + } catch (Exception e) { + LOGGER.error("[Skyblocker] Error while downloading image repo on attempt {}!", retries, e); + update(retries + 1); + } + } + }); + } + + /** + * @return The stored hash or the {@link #PLACEHOLDER_HASH}. + */ + private static String checkSavedCommitData() throws IOException { + Path file = REPO_DIRECTORY.resolve("image_repo.json"); + + if (Files.exists(file)) { + try (BufferedReader reader = Files.newBufferedReader(file)) { + CommitData commitData = CommitData.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).result().orElseThrow(); + + return commitData.commit(); + } + } + + return PLACEHOLDER_HASH; + } + + /** + * Writes the {@code newHash} into a file to be used to check for repo updates. + * + * @implNote Checking whether the directory exists or not isn't needed as this is called after all files are written successfully. + */ + private static void writeCommitData(String newHash) throws IOException { + Path file = REPO_DIRECTORY.resolve("image_repo.json"); + CommitData commitData = new CommitData(newHash, System.currentTimeMillis()); + + try (BufferedWriter writer = Files.newBufferedWriter(file)) { + SkyblockerMod.GSON.toJson(CommitData.CODEC.encodeStart(JsonOps.INSTANCE, commitData).result().orElseThrow(), writer); + } + } + + /** + * Deletes all directories (not files) inside of the {@link #REPO_DIRECTORY} + * @throws IOException + */ + private static void deleteDirectories() throws IOException { + Files.list(REPO_DIRECTORY) + .filter(Files::isDirectory) + .forEach(dir -> { + try { + FileUtils.recursiveDelete(dir); + } catch (Exception e) { + LOGGER.error("[Skyblocker] Encountered an exception while deleting a directory! Path: {}", dir.toAbsolutePath(), e); + } + }); + } + + record CommitData(String commit, long lastUpdated) { + static final Codec<CommitData> CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.STRING.fieldOf("commit").forGetter(CommitData::commit), + Codec.LONG.fieldOf("lastUpdated").forGetter(CommitData::lastUpdated)) + .apply(instance, CommitData::new)); + } +} diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java index 78605c00..4626003d 100644 --- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java @@ -863,6 +863,9 @@ public class SkyblockerConfig { public boolean includeEssence = true; @SerialEntry + public boolean croesusProfit = true; + + @SerialEntry public int neutralThreshold = 1000; @SerialEntry @@ -1192,6 +1195,15 @@ public class SkyblockerConfig { @SerialEntry public ChatFilterResult hideDicer = ChatFilterResult.PASS; + + @SerialEntry + public ChatRuleConfig chatRuleConfig = new ChatRuleConfig(); + } + public static class ChatRuleConfig { + @SerialEntry + public int announcementLength = 60; + @SerialEntry + public int announcementScale = 3; } public enum Info { diff --git a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java index 3ebd5d76..9d6e1beb 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java @@ -279,6 +279,14 @@ public class DungeonsCategory { newValue -> config.locations.dungeons.dungeonChestProfit.includeEssence = newValue) .controller(ConfigUtils::createBooleanController) .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.croesusProfit")) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.croesusProfit.@Tooltip"))) + .binding(defaults.locations.dungeons.dungeonChestProfit.croesusProfit, + () -> config.locations.dungeons.dungeonChestProfit.croesusProfit, + newValue -> config.locations.dungeons.dungeonChestProfit.croesusProfit = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) .option(Option.<Integer>createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.neutralThreshold")) .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.neutralThreshold.@Tooltip"))) diff --git a/src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java index acdc8169..0f95bcaa 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java @@ -2,10 +2,12 @@ package de.hysky.skyblocker.config.categories; import de.hysky.skyblocker.config.ConfigUtils; import de.hysky.skyblocker.config.SkyblockerConfig; +import de.hysky.skyblocker.skyblock.chat.ChatRulesConfigScreen; +import de.hysky.skyblocker.skyblock.dwarven.CrystalsHudConfigScreen; import de.hysky.skyblocker.utils.chat.ChatFilterResult; -import dev.isxander.yacl3.api.ConfigCategory; -import dev.isxander.yacl3.api.Option; -import dev.isxander.yacl3.api.OptionDescription; +import dev.isxander.yacl3.api.*; +import dev.isxander.yacl3.api.controller.IntegerSliderControllerBuilder; +import net.minecraft.client.MinecraftClient; import net.minecraft.text.Text; public class MessageFilterCategory { @@ -126,6 +128,32 @@ public class MessageFilterCategory { newValue -> config.messages.hideDicer = newValue) .controller(ConfigUtils::createEnumCyclingListController) .build()) + //chat rules options + .group(OptionGroup.createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules")) + .collapsed(false) + .option(ButtonOption.createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen")) + .text(Text.translatable("text.skyblocker.open")) + .action((screen, opt) -> MinecraftClient.getInstance().setScreen(new ChatRulesConfigScreen(screen))) + .build()) + .option(Option.<Integer>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.announcementLength")) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.announcementLength.@Tooltip"))) + .binding(defaults.messages.chatRuleConfig.announcementLength, + () -> config.messages.chatRuleConfig.announcementLength, + newValue -> config.messages.chatRuleConfig.announcementLength = newValue) + .controller(opt -> IntegerSliderControllerBuilder.create(opt).range(5, 200).step(1)) + .build()) + .option(Option.<Integer>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.announcementScale")) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.announcementScale.@Tooltip"))) + .binding(defaults.messages.chatRuleConfig.announcementScale, + () -> config.messages.chatRuleConfig.announcementScale, + newValue -> config.messages.chatRuleConfig.announcementScale = newValue) + .controller(opt -> IntegerSliderControllerBuilder.create(opt).range(1, 8).step(1)) + .build()) + .build()) .build(); } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRule.java b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRule.java new file mode 100644 index 00000000..34cc6352 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRule.java @@ -0,0 +1,264 @@ +package de.hysky.skyblocker.skyblock.chat; + +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.sound.SoundEvent; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +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<ChatRule> 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<ChatRule>> 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<String> replaceMessage, Optional<SoundEvent> 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<String> getReplaceMessageOpt() { + return replaceMessage == null ? Optional.empty() : Optional.of(replaceMessage); + } + + protected void setReplaceMessage(String replaceMessage) { + this.replaceMessage = replaceMessage; + } + + protected SoundEvent getCustomSound() { + return customSound; + } + + private Optional<SoundEvent> 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 rawLocation = Utils.getLocationRaw(); + Boolean isLocationValid = null; + + for (String validLocation : validLocations.replace(" ", "").toLowerCase().split(",")) {//the locations are raw locations split by "," and start with ! if not locations + String rawValidLocation = ChatRulesHandler.locations.get(validLocation.replace("!","")); + if (rawValidLocation == null) continue; + if (validLocation.startsWith("!")) {//not location + if (Objects.equals(rawValidLocation, rawLocation.toLowerCase())) { + isLocationValid = false; + break; + } + } else { + if (Objects.equals(rawValidLocation, rawLocation.toLowerCase())) { //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; + } +} + + + diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleAnnouncementScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleAnnouncementScreen.java new file mode 100644 index 00000000..bafada27 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleAnnouncementScreen.java @@ -0,0 +1,48 @@ +package de.hysky.skyblocker.skyblock.chat; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; + +public class ChatRuleAnnouncementScreen { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + private static float timer; + private static Text text = null; + + public static void init() { + HudRenderCallback.EVENT.register((context, tickDelta) -> { + if (timer <= 0 || text == null) { + return; + } + render(context, tickDelta); + }); + } + + /** + * renders {@link ChatRuleAnnouncementScreen#text} to the middle of the top of the screen. + * @param context render context + * @param tickDelta difference from last render to remove from timer + */ + private static void render(DrawContext context, float tickDelta) { + int scale = SkyblockerConfigManager.get().messages.chatRuleConfig.announcementScale; + //decrement timer + timer -= tickDelta; + //scale text up and center + MatrixStack matrices = context.getMatrices(); + matrices.push(); + matrices.translate(context.getScaledWindowWidth() / 2f, context.getScaledWindowHeight() * 0.3, 0f); + matrices.scale(scale, scale, 0f); + //render text + context.drawCenteredTextWithShadow(CLIENT.textRenderer, text, 0, 0, 0xFFFFFFFF); + + matrices.pop(); + } + + protected static void setText(Text newText) { + text = newText; + timer = SkyblockerConfigManager.get().messages.chatRuleConfig.announcementLength; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleConfigScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleConfigScreen.java new file mode 100644 index 00000000..c99aeed8 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleConfigScreen.java @@ -0,0 +1,345 @@ +package de.hysky.skyblocker.skyblock.chat; + +import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.client.gui.DrawContext; +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.sound.SoundEvent; +import net.minecraft.sound.SoundEvents; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + + +import java.awt.*; +import java.util.List; +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<MutableText, SoundEvent> soundsLookup = Map.ofEntries( + entry(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.sounds.pling"), SoundEvents.BLOCK_NOTE_BLOCK_PLING.value()), + entry(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.sounds.cave"), SoundEvents.AMBIENT_CAVE.value()), + entry(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.sounds.zombie"), SoundEvents.ENTITY_ZOMBIE_AMBIENT), + entry(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.sounds.crit"), SoundEvents.ENTITY_PLAYER_ATTACK_CRIT), + entry(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.sounds.arrowHit"), SoundEvents.ENTITY_ARROW_HIT_PLAYER), + entry(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.sounds.amethyst"), SoundEvents.BLOCK_AMETHYST_BLOCK_HIT), + entry(Text.translatable("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.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<SoundEvent> soundOptions = soundsLookup.values().stream().toList(); + Identifier ruleSoundId = chatRule.getCustomSound().getId(); + + for (int i = 0; i < soundOptions.size(); i++) { + if (soundOptions.get(i).getId().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("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.partialMatch.@Tooltip"))) + .build(); + lineXOffset += buttonWidth + SPACER_X; + regexTextPos = IntIntPair.of(currentPos.leftInt() + lineXOffset,currentPos.rightInt()); + lineXOffset += client.textRenderer.getWidth(Text.translatable("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.regex.@Tooltip"))) + .build(); + lineXOffset += buttonWidth + SPACER_X; + ignoreCaseTextPos = IntIntPair.of(currentPos.leftInt() + lineXOffset,currentPos.rightInt()); + lineXOffset += client.textRenderer.getWidth(Text.translatable("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.ignoreCase.@Tooltip"))) + .build(); + currentPos = IntIntPair.of(currentPos.leftInt(), currentPos.rightInt() + SPACER_Y); + + locationLabelTextPos = currentPos; + lineXOffset = client.textRenderer.getWidth(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.locations")) + SPACER_X; + locationsInput = new TextFieldWidget(client.textRenderer, currentPos.leftInt() + lineXOffset, currentPos.rightInt(), 200, 20, Text.of("")); + locationsInput.setText(chatRule.getValidLocations()); + MutableText locationToolTip = Text.translatable("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.hideMessage.@Tooltip"))) + .build(); + lineXOffset += buttonWidth + SPACER_X; + actionBarTextPos = IntIntPair.of(currentPos.leftInt() + lineXOffset,currentPos.rightInt()); + lineXOffset += client.textRenderer.getWidth(Text.translatable("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.announcement.@Tooltip"))) + .build(); + lineXOffset += buttonWidth + SPACER_X; + customSoundLabelTextPos = IntIntPair.of(currentPos.leftInt() + lineXOffset,currentPos.rightInt()); + lineXOffset += client.textRenderer.getWidth(Text.translatable("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.sounds.@Tooltip"))) + .build(); + currentPos = IntIntPair.of(currentPos.leftInt(), currentPos.rightInt() + SPACER_Y); + + replaceMessageLabelTextPos = currentPos; + lineXOffset = client.textRenderer.getWidth(Text.translatable("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.replace.@Tooltip"))); + replaceMessageInput.setText(chatRule.getReplaceMessage()); + + ButtonWidget finishButton = ButtonWidget.builder(Text.translatable("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.partialMatch")); + total += client.textRenderer.getWidth(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.regex")); + total += client.textRenderer.getWidth(Text.translatable("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.true").withColor(Color.GREEN.getRGB()); + } else { + return Text.translatable("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.inputs"), inputsLabelTextPos.leftInt(), inputsLabelTextPos.rightInt() + yOffset, 0xFFFFFFFF); + context.drawTextWithShadow(this.textRenderer, Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.name"), nameLabelTextPos.leftInt(), nameLabelTextPos.rightInt() + yOffset, 0xFFFFFFFF); + context.drawTextWithShadow(this.textRenderer, Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.filter"), filterLabelTextPos.leftInt(), filterLabelTextPos.rightInt() + yOffset, 0xFFFFFFFF); + context.drawTextWithShadow(this.textRenderer, Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.partialMatch"), partialMatchTextPos.leftInt(), partialMatchTextPos.rightInt() + yOffset, 0xFFFFFFFF); + context.drawTextWithShadow(this.textRenderer, Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.regex"), regexTextPos.leftInt(), regexTextPos.rightInt() + yOffset, 0xFFFFFFFF); + context.drawTextWithShadow(this.textRenderer, Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.ignoreCase"), ignoreCaseTextPos.leftInt(), ignoreCaseTextPos.rightInt() + yOffset, 0xFFFFFFFF); + context.drawTextWithShadow(this.textRenderer, Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.locations"), locationLabelTextPos.leftInt(), locationLabelTextPos.rightInt() + yOffset, 0xFFFFFFFF); + context.drawTextWithShadow(this.textRenderer, Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.outputs"), outputsLabelTextPos.leftInt(), outputsLabelTextPos.rightInt() + yOffset, 0xFFFFFFFF); + context.drawTextWithShadow(this.textRenderer, Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.hideMessage"), hideMessageTextPos.leftInt(), hideMessageTextPos.rightInt() + yOffset, 0xFFFFFFFF); + context.drawTextWithShadow(this.textRenderer, Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.actionBar"), actionBarTextPos.leftInt(), actionBarTextPos.rightInt() + yOffset, 0xFFFFFFFF); + context.drawTextWithShadow(this.textRenderer, Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.announcement"), announcementTextPos.leftInt(), announcementTextPos.rightInt() + yOffset, 0xFFFFFFFF); + context.drawTextWithShadow(this.textRenderer, Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.sounds"), customSoundLabelTextPos.leftInt(), customSoundLabelTextPos.rightInt() + yOffset, 0xFFFFFFFF); + context.drawTextWithShadow(this.textRenderer, Text.translatable("text.autoconfig.skyblocker.option.messages.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("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.sounds.none"); + } + + return soundsLookup.keySet().stream().toList().get(currentSoundIndex); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesConfigListWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesConfigListWidget.java new file mode 100644 index 00000000..a1b9317a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesConfigListWidget.java @@ -0,0 +1,198 @@ +package de.hysky.skyblocker.skyblock.chat; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.screen.ConfirmScreen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.ElementListWidget; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.text.Text; + +import java.awt.*; +import java.util.List; + +public class ChatRulesConfigListWidget extends ElementListWidget<ChatRulesConfigListWidget.AbstractChatRuleEntry> { + private final ChatRulesConfigScreen screen; + private Boolean hasChanged; + + public ChatRulesConfigListWidget(MinecraftClient client, ChatRulesConfigScreen screen, int width, int height, int y, int itemHeight) { + super(client, width, height, y, itemHeight); + this.screen = screen; + this.hasChanged = false; + + //add labels + addEntry(new ChatRuleLabelsEntry()); + //add entry fall all existing rules + for (int i = 0; i < ChatRulesHandler.chatRuleList.size(); i++){ + addEntry(new ChatRuleConfigEntry(i)); + } + } + + @Override + public int getRowWidth() { + return super.getRowWidth() + 100; + } + + @Override + protected int getScrollbarPositionX() { + return super.getScrollbarPositionX() + 50; + } + + protected void addRuleAfterSelected() { + hasChanged = true; + int newIndex = children().indexOf(getSelectedOrNull()) + 1; + + ChatRulesHandler.chatRuleList.add(newIndex, new ChatRule()); + children().add(newIndex + 1, new ChatRuleConfigEntry(newIndex)); + } + + protected boolean removeEntry(AbstractChatRuleEntry entry) { + hasChanged = true; + return super.removeEntry(entry); + } + + protected void saveRules() { + hasChanged = false; + ChatRulesHandler.saveChatRules(); + } + + protected boolean hasChanges() { + return (hasChanged || children().stream().filter(ChatRuleConfigEntry.class::isInstance).map(ChatRuleConfigEntry.class::cast).anyMatch(ChatRuleConfigEntry::isChange)); + } + + protected static abstract class AbstractChatRuleEntry extends ElementListWidget.Entry<ChatRulesConfigListWidget.AbstractChatRuleEntry> { + } + + private class ChatRuleLabelsEntry extends AbstractChatRuleEntry { + + @Override + public List<? extends Selectable> selectableChildren() { + return List.of(); + } + + @Override + public List<? extends Element> children() { + return List.of(); + } + + @Override + public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + context.drawCenteredTextWithShadow(client.textRenderer, Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleName"), width / 2 - 125, y + 5, 0xFFFFFFFF); + context.drawCenteredTextWithShadow(client.textRenderer, Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleEnabled"), width / 2, y + 5, 0xFFFFFFFF); + context.drawCenteredTextWithShadow(client.textRenderer, Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.modify"), width / 2 + 100, y + 5, 0xFFFFFFFF); + } + } + + private class ChatRuleConfigEntry extends AbstractChatRuleEntry { + //data + private final int chatRuleIndex; + private final ChatRule chatRule; + + private final List<? extends Element> children; + + //widgets + private final ButtonWidget enabledButton; + private final ButtonWidget openConfigButton; + private final ButtonWidget deleteButton; + + //text location + private final int nameX = width / 2 - 125; + //saved data + private double oldScrollAmount = 0; + + + public ChatRuleConfigEntry(int chatRuleIndex) { + this.chatRuleIndex = chatRuleIndex; + this.chatRule = ChatRulesHandler.chatRuleList.get(chatRuleIndex); + + enabledButton = ButtonWidget.builder(enabledButtonText(), a -> toggleEnabled()) + .size(50, 20) + .position(width / 2 - 25, 5) + .build(); + + openConfigButton = ButtonWidget.builder(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.editRule"), a -> { + client.setScreen(new ChatRuleConfigScreen(screen, chatRuleIndex)); + }) + .size(50, 20) + .position(width / 2 + 45, 5) + .tooltip(Tooltip.of(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.editRule.@Tooltip"))) + .build(); + + deleteButton = ButtonWidget.builder(Text.translatable("selectServer.delete"), a -> { + oldScrollAmount = getScrollAmount(); + client.setScreen(new ConfirmScreen(this::deleteEntry, Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.deleteQuestion"), Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.deleteWarning", chatRule.getName()), Text.translatable("selectServer.deleteButton"), ScreenTexts.CANCEL)); + }) + .size(50, 20) + .position(width / 2 + 105, 5) + .build(); + + children = List.of(enabledButton, openConfigButton, deleteButton); + } + + private Text enabledButtonText() { + if (chatRule.getEnabled()) { + return Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.true").withColor(Color.GREEN.getRGB()); + } else { + return Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen.false").withColor(Color.RED.getRGB()); + } + } + private void toggleEnabled() { + hasChanged = true; + chatRule.setEnabled(!chatRule.getEnabled()); + enabledButton.setMessage(enabledButtonText()); + } + + private void deleteEntry(boolean confirmedAction) { + if (confirmedAction) { + //delete this + ChatRulesHandler.chatRuleList.remove(chatRuleIndex); + removeEntry(this); + } + + client.setScreen(screen); + setScrollAmount(oldScrollAmount); + } + + @Override + public List<? extends Selectable> selectableChildren() { + return List.of(new Selectable() { + @Override + public SelectionType getType() { + return SelectionType.HOVERED; + } + + @Override + public void appendNarrations(NarrationMessageBuilder builder) { + builder.put(NarrationPart.TITLE,chatRule.getName()); + } + }); + } + + @Override + public List<? extends Element> children() { + return children; + } + + @Override + public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { //todo get strings form en_us.json + //widgets + enabledButton.setY(y); + enabledButton.render(context, mouseX, mouseY, tickDelta); + openConfigButton.setY(y); + openConfigButton.render(context, mouseX, mouseY, tickDelta); + deleteButton.setY(y); + deleteButton.render(context, mouseX, mouseY, tickDelta); + //text + context.drawCenteredTextWithShadow(client.textRenderer, chatRule.getName(), nameX, y + 5, 0xFFFFFFFF); + } + + public boolean isChange() { + return (!chatRule.getEnabled().equals(ChatRulesHandler.chatRuleList.get(chatRuleIndex).getEnabled())); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesConfigScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesConfigScreen.java new file mode 100644 index 00000000..49ef735d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesConfigScreen.java @@ -0,0 +1,75 @@ +package de.hysky.skyblocker.skyblock.chat; + +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ConfirmScreen; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.GridWidget; +import net.minecraft.client.gui.widget.SimplePositioningWidget; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.text.Text; + +public class ChatRulesConfigScreen extends Screen { + + private ChatRulesConfigListWidget chatRulesConfigListWidget; + private final Screen parent; + + public ChatRulesConfigScreen(Screen parent) { + super(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.ruleScreen")); + this.parent = parent; + } + + @Override + public void setTooltip(Text tooltip) { + super.setTooltip(tooltip); + } + + @Override + protected void init() { + super.init(); + chatRulesConfigListWidget = new ChatRulesConfigListWidget(client, this, width, height - 96, 32, 25); + addDrawableChild(chatRulesConfigListWidget); + GridWidget gridWidget = new GridWidget(); + gridWidget.getMainPositioner().marginX(5).marginY(2); + GridWidget.Adder adder = gridWidget.createAdder(3); + adder.add(ButtonWidget.builder(ScreenTexts.CANCEL, button -> { + if (client != null) { + close(); + } + }).build()); + ButtonWidget buttonNew1 = ButtonWidget.builder(Text.translatable("text.autoconfig.skyblocker.option.messages.chatRules.screen.new"), buttonNew -> chatRulesConfigListWidget.addRuleAfterSelected()).build(); + adder.add(buttonNew1); + ButtonWidget buttonDone = ButtonWidget.builder(ScreenTexts.DONE, button -> { + chatRulesConfigListWidget.saveRules(); + if (client != null) { + close(); + } + }).build(); + adder.add(buttonDone); + gridWidget.refreshPositions(); + SimplePositioningWidget.setPos(gridWidget, 0, this.height - 64, this.width, 64); + gridWidget.forEachChild(this::addDrawableChild); + + } + + @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); + } + + @Override + public void close() { + if (client != null && chatRulesConfigListWidget.hasChanges()) { + client.setScreen(new ConfirmScreen(confirmedAction -> { + if (confirmedAction) { + this.client.setScreen(parent); + } else { + client.setScreen(this); + } + }, Text.translatable("text.skyblocker.quit_config"), Text.translatable("text.skyblocker.quit_config_sure"), Text.translatable("text.skyblocker.quit_discard"), ScreenTexts.CANCEL)); + } else { + this.client.setScreen(parent); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesHandler.java b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesHandler.java new file mode 100644 index 00000000..13115bee --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesHandler.java @@ -0,0 +1,182 @@ +package de.hysky.skyblocker.skyblock.chat; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.Http; +import de.hysky.skyblocker.utils.Utils; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.sound.SoundEvents; +import net.minecraft.text.MutableText; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +public class ChatRulesHandler { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + private static final Logger LOGGER = LoggerFactory.getLogger(ChatRule.class); + private static final Path CHAT_RULE_FILE = SkyblockerMod.CONFIG_DIR.resolve("chat_rules.json"); + private static final Codec<Map<String, List<ChatRule>>> MAP_CODEC = Codec.unboundedMap(Codec.STRING, ChatRule.LIST_CODEC); + /** + * look up table for the locations input by the users to raw locations + */ + protected static final HashMap<String, String> locations = new HashMap<>(); + /** + * list of possible locations still formatted for the tool tip + */ + protected static final List<String> locationsList = new ArrayList<>(); + + protected static final List<ChatRule> chatRuleList = new ArrayList<>(); + + public static void init() { + CompletableFuture.runAsync(ChatRulesHandler::loadChatRules); + CompletableFuture.runAsync(ChatRulesHandler::loadLocations); + ClientReceiveMessageEvents.ALLOW_GAME.register(ChatRulesHandler::checkMessage); + } + + private static void loadChatRules() { + try (BufferedReader reader = Files.newBufferedReader(CHAT_RULE_FILE)) { + Map<String, List<ChatRule>> chatRules = MAP_CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).result().orElseThrow(); + LOGGER.info("[Sky: " + chatRules.toString()); + + chatRuleList.addAll(chatRules.get("rules")); + + LOGGER.info("[Skyblocker] Loaded chat rules"); + } catch (NoSuchFileException e) { + registerDefaultChatRules(); + LOGGER.warn("[Skyblocker] chat rule file not found, using default rules. This is normal when using for the first time."); + } catch (Exception e) { + LOGGER.error("[Skyblocker] Failed to load shortcuts file", e); + } + } + + private static void registerDefaultChatRules() { + //clean hub chat + ChatRule cleanHubRule = new ChatRule("Clean Hub Chat", false, true, true, true, "(selling)|(buying)|(lowb)|(visit)|(/p)|(/ah)|(my ah)", "hub", true, false, false, "", null); + //mining Ability + ChatRule miningAbilityRule = new ChatRule("Mining Ability Alert", false, true, false, true, "is now available!", "Crystal Hollows, Dwarven Mines", false, false, true, "&1Ability", SoundEvents.ENTITY_ARROW_HIT_PLAYER); + + chatRuleList.add(cleanHubRule); + chatRuleList.add(miningAbilityRule); + } + + private static void loadLocations() { + try { + String response = Http.sendGetRequest("https://api.hypixel.net/v2/resources/games"); + JsonObject locationsJson = JsonParser.parseString(response).getAsJsonObject().get("games").getAsJsonObject().get("SKYBLOCK").getAsJsonObject().get("modeNames").getAsJsonObject(); + for (Map.Entry<String, JsonElement> entry : locationsJson.entrySet()) { + //fix old naming todo remove when hypixel fix + if (Objects.equals(entry.getKey(), "instanced")) { + locationsList.add(entry.getValue().getAsString()); + locations.put(entry.getValue().getAsString().replace(" ", "").toLowerCase(), "kuudra"); + continue; + } + locationsList.add(entry.getValue().getAsString()); + //add to list in a simplified for so more lenient for user input + locations.put(entry.getValue().getAsString().replace(" ", "").toLowerCase(), entry.getKey()); + } + } catch (Exception e) { + LOGGER.error("[Skyblocker] Failed to load locations!", e); + } + } + + protected static void saveChatRules() { + JsonObject chatRuleJson = new JsonObject(); + chatRuleJson.add("rules", ChatRule.LIST_CODEC.encodeStart(JsonOps.INSTANCE, chatRuleList).result().orElseThrow()); + try (BufferedWriter writer = Files.newBufferedWriter(CHAT_RULE_FILE)) { + SkyblockerMod.GSON.toJson(chatRuleJson, writer); + LOGGER.info("[Skyblocker] Saved chat rules file"); + } catch (Exception e) { + LOGGER.error("[Skyblocker] Failed to save chat rules file", e); + } + } + + /** + * Checks each rule in {@link ChatRulesHandler#chatRuleList} to see if they are a match for the message and if so change outputs based on the options set in the {@link ChatRule}. + * @param message the chat message + * @param overlay if its overlay + */ + private static boolean checkMessage(Text message, boolean overlay) { + if (!Utils.isOnSkyblock()) return true; //do not work not on skyblock + if (overlay) return true; //ignore messages in overlay + String plain = Formatting.strip(message.getString()); + + for (ChatRule rule : chatRuleList) { + if (rule.isMatch(plain)) { + //get a replacement message + Text newMessage; + if (!rule.getReplaceMessage().isBlank()) { + newMessage = formatText(rule.getReplaceMessage()); + } else { + newMessage = message; + } + + if (rule.getShowAnnouncement()) { + ChatRuleAnnouncementScreen.setText(newMessage); + } + + //show in action bar + if (rule.getShowActionBar() && CLIENT.player != null) { + CLIENT.player.sendMessage(newMessage, true); + } + + //hide message + if (!rule.getHideMessage() && CLIENT.player != null) { + CLIENT.player.sendMessage(newMessage, false); + } + + //play sound + if (rule.getCustomSound() != null && CLIENT.player != null) { + CLIENT.player.playSound(rule.getCustomSound(), 100f, 0.1f); + } + + //do not send original message + return false; + } + } + return true; + } + + /** + * Converts a string with color codes into a formatted Text object + * @param codedString the string with color codes in + * @return formatted text + */ + protected static MutableText formatText(String codedString) { + if (codedString.contains(String.valueOf(Formatting.FORMATTING_CODE_PREFIX)) || codedString.contains("&")) { + MutableText newText = Text.literal(""); + String[] parts = codedString.split("[" + Formatting.FORMATTING_CODE_PREFIX +"&]"); + Style style = Style.EMPTY; + + for (String part : parts) { + if (part.isEmpty()) continue; + Formatting formatting = Formatting.byCode(part.charAt(0)); + + if (formatting != null) { + style = style.withFormatting(formatting); + Text.literal(part.substring(1)).getWithStyle(style).forEach(newText::append); + } else { + newText.append(Text.of(part)); + } + } + return newText; + } + return Text.literal(codedString); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusHelper.java index e95b47c9..01422770 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusHelper.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusHelper.java @@ -25,8 +25,12 @@ public class CroesusHelper extends ContainerSolver { List<ColorHighlight> highlights = new ArrayList<>(); for (Map.Entry<Integer, ItemStack> entry : slots.entrySet()) { ItemStack stack = entry.getValue(); - if (stack != null && stack.getNbt() != null && (stack.getNbt().toString().contains("No more Chests to open!") || stack.getNbt().toString().contains("Opened Chest:"))) { - highlights.add(ColorHighlight.gray(entry.getKey())); + if (stack != null && stack.getNbt() != null) { + if (stack.getNbt().toString().contains("Opened Chest:")) { + highlights.add(ColorHighlight.gray(entry.getKey())); + } else if (stack.getNbt().toString().contains("No more Chests to open!")) { + highlights.add(ColorHighlight.red(entry.getKey())); + } } } return highlights; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java new file mode 100644 index 00000000..ca166915 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java @@ -0,0 +1,267 @@ +package de.hysky.skyblocker.skyblock.dungeon; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.render.gui.ColorHighlight; +import de.hysky.skyblocker.utils.render.gui.ContainerSolver; +import net.minecraft.item.ItemStack; +import net.minecraft.text.Text; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CroesusProfit extends ContainerSolver { + private static final Pattern ESSENCE_PATTERN = Pattern.compile("(?<type>[A-Za-z]+) Essence x(?<amount>[0-9]+)"); + public CroesusProfit() { + super(".*Catacombs - Floor.*"); + } + + @Override + protected boolean isEnabled() { + return SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.croesusProfit; + } + + @Override + protected List<ColorHighlight> getColors(String[] groups, Map<Integer, ItemStack> slots) { + List<ColorHighlight> highlights = new ArrayList<>(); + ItemStack bestChest = null, secondBestChest = null; + long bestValue = 0, secondBestValue = 0; // If negative value of chest - it is out of the question + long dungeonKeyPriceData = getItemPrice("DUNGEON_CHEST_KEY") * 2; // lesser ones don't worth the hassle + + for (Map.Entry<Integer, ItemStack> entry : slots.entrySet()) { + ItemStack stack = entry.getValue(); + if (stack != null && stack.getNbt() != null && stack.getName().toString().contains("Chest")) { + long value = valueChest(stack); + if (value > bestValue) { + secondBestChest = bestChest; + secondBestValue = bestValue; + bestChest = stack; + bestValue = value; + } else if (value > secondBestValue) { + secondBestChest = stack; + secondBestValue = value; + } + } + } + + for (Map.Entry<Integer, ItemStack> entry : slots.entrySet()) { + ItemStack stack = entry.getValue(); + if (stack != null && stack.getNbt() != null) { + if (stack.equals(bestChest)) { + highlights.add(ColorHighlight.green(entry.getKey())); + } else if (stack.equals(secondBestChest) && secondBestValue > dungeonKeyPriceData) { + highlights.add(ColorHighlight.yellow(entry.getKey())); + } + } + } + return highlights; + } + + + private long valueChest(@NotNull ItemStack chest) { + long chestValue = 0; + int chestPrice = 0; + List<String> chestItems = new ArrayList<>(); + + boolean processingContents = false; + for (Text line : ItemUtils.getNbtTooltips(chest)) { + String lineString = line.getString(); + if (lineString.contains("Contents")) { + processingContents = true; + continue; + } else if (lineString.isEmpty()) { + processingContents = false; + } else if (lineString.contains("Coins") && !processingContents) { + chestPrice = Integer.parseInt(lineString.replaceAll(",", "").replaceAll("\\D", "")); + } + + if (processingContents) { + if (lineString.contains("Essence")) { + Matcher matcher = ESSENCE_PATTERN.matcher(lineString); + if (matcher.matches()) { // add to chest value result of multiplying price of essence on it's amount + chestValue += getItemPrice(("ESSENCE_" + matcher.group("type")).toUpperCase()) * Integer.parseInt(matcher.group("amount")); + } + } else { + if (lineString.contains("Spirit")) { // TODO: make code like this to detect recombed gear (it can drop with 1% chance, according to wiki, tho I never saw any?) + chestValue += line.getStyle().toString().contains("color=dark_purple") ? getItemPrice("Spirit Epic") : getItemPrice(lineString); + } else { + chestItems.add(lineString); + } + } + } + } + for (String item : chestItems){ + chestValue += getItemPrice(item); + } + return chestValue-chestPrice; + } + + + private long getItemPrice(String itemDisplayName) { + JsonObject bazaarPrices = TooltipInfoType.BAZAAR.getData(); + JsonObject lbinPrices = TooltipInfoType.LOWEST_BINS.getData(); + long itemValue = 0; + String id = dungeonDropsNameToId.get(itemDisplayName); + + if (bazaarPrices == null || lbinPrices == null) return 0; + + if (bazaarPrices.has(id)) { + JsonObject item = bazaarPrices.get(id).getAsJsonObject(); + boolean isPriceNull = item.get("sellPrice").isJsonNull(); + return (isPriceNull ? 0L : item.get("sellPrice").getAsLong()); + } else if (lbinPrices.has(id)) { + return lbinPrices.get(id).getAsLong(); + } + return itemValue; + } + + + // I did a thing :( + final Map<String, String> dungeonDropsNameToId = new HashMap<>() {{ + put("Enchanted Book (Ultimate Jerry I)", "ENCHANTMENT_ULTIMATE_JERRY_1"); // ultimate books start + put("Enchanted Book (Ultimate Jerry II)", "ENCHANTMENT_ULTIMATE_JERRY_2"); + put("Enchanted Book (Ultimate Jerry III)", "ENCHANTMENT_ULTIMATE_JERRY_3"); + put("Enchanted Book (Bank I)", "ENCHANTMENT_ULTIMATE_BANK_1"); + put("Enchanted Book (Bank II)", "ENCHANTMENT_ULTIMATE_BANK_2"); + put("Enchanted Book (Bank III)", "ENCHANTMENT_ULTIMATE_BANK_3"); + put("Enchanted Book (Combo I)", "ENCHANTMENT_ULTIMATE_COMBO_1"); + put("Enchanted Book (Combo II)", "ENCHANTMENT_ULTIMATE_COMBO_2"); + put("Enchanted Book (No Pain No Gain I)", "ENCHANTMENT_ULTIMATE_NO_PAIN_NO_GAIN_1"); + put("Enchanted Book (No Pain No Gain II)", "ENCHANTMENT_ULTIMATE_NO_PAIN_NO_GAIN_2"); + put("Enchanted Book (Ultimate Wise I)", "ENCHANTMENT_ULTIMATE_WISE_1"); + put("Enchanted Book (Ultimate Wise II)", "ENCHANTMENT_ULTIMATE_WISE_2"); + put("Enchanted Book (Wisdom I)", "ENCHANTMENT_ULTIMATE_WISDOM_1"); + put("Enchanted Book (Wisdom II)", "ENCHANTMENT_ULTIMATE_WISDOM_2"); + put("Enchanted Book (Last Stand I)", "ENCHANTMENT_ULTIMATE_LAST_STAND_1"); + put("Enchanted Book (Last Stand II)", "ENCHANTMENT_ULTIMATE_LAST_STAND_2"); + put("Enchanted Book (Rend I)", "ENCHANTMENT_ULTIMATE_REND_1"); + put("Enchanted Book (Rend II)", "ENCHANTMENT_ULTIMATE_REND_2"); + put("Enchanted Book (Legion I)", "ENCHANTMENT_ULTIMATE_LEGION_1"); + put("Enchanted Book (Swarm I)", "ENCHANTMENT_ULTIMATE_SWARM_1"); + put("Enchanted Book (One For All I)", "ENCHANTMENT_ULTIMATE_ONE_FOR_ALL_1"); + put("Enchanted Book (Soul Eater I)", "ENCHANTMENT_ULTIMATE_SOUL_EATER_1"); // ultimate books end + put("Enchanted Book (Infinite Quiver VI)", "ENCHANTMENT_INFINITE_QUIVER_6"); // enchanted books start + put("Enchanted Book (Infinite Quiver VII)", "ENCHANTMENT_INFINITE_QUIVER_7"); + put("Enchanted Book (Feather Falling VI)", "ENCHANTMENT_FEATHER_FALLING_6"); + put("Enchanted Book (Feather Falling VII)", "ENCHANTMENT_FEATHER_FALLING_7"); + put("Enchanted Book (Rejuvenate I)", "ENCHANTMENT_REJUVENATE_1"); + put("Enchanted Book (Rejuvenate II)", "ENCHANTMENT_REJUVENATE_2"); + put("Enchanted Book (Rejuvenate III)", "ENCHANTMENT_REJUVENATE_3"); + put("Enchanted Book (Overload)", "ENCHANTMENT_OVERLOAD_1"); + put("Enchanted Book (Lethality VI)", "ENCHANTMENT_LETHALITY_6"); + put("Enchanted Book (Thunderlord VII)", "ENCHANTMENT_THUNDERLORD_7"); // enchanted books end + + put("Hot Potato Book", "HOT_POTATO_BOOK"); // HPB, FPB, Recomb (universal drops) + put("Fuming Potato Book", "FUMING_POTATO_BOOK"); + put("Recombobulator 3000", "RECOMBOBULATOR_3000"); + put("Necromancer's Brooch", "NECROMANCER_BROOCH"); + put("ESSENCE_WITHER","ESSENCE_WITHER"); // Essences. Really stupid way of doing this + put("ESSENCE_UNDEAD", "ESSENCE_UNDEAD"); + put("ESSENCE_DRAGON", "ESSENCE_DRAGON"); + put("ESSENCE_SPIDER", "ESSENCE_SPIDER"); + put("ESSENCE_ICE", "ESSENCE_ICE"); + put("ESSENCE_DIAMOND", "ESSENCE_DIAMOND"); + put("ESSENCE_GOLD", "ESSENCE_GOLD"); + put("ESSENCE_CRIMSON", "ESSENCE_CRIMSON"); + put("DUNGEON_CHEST_KEY", "DUNGEON_CHEST_KEY"); + + put("Bonzo's Staff", "BONZO_STAFF"); // F1 M1 + put("Master Skull - Tier 1", "MASTER_SKULL_TIER_1"); + put("Bonzo's Mask", "BONZO_MASK"); + put("Balloon Snake", "BALLOON_SNAKE"); + put("Red Nose", "RED_NOSE"); + + put("Red Scarf", "RED_SCARF"); // F2 M2 + put("Adaptive Blade", "STONE_BLADE"); + put("Master Skull - Tier 2", "MASTER_SKULL_TIER_2"); + put("Adaptive Belt", "ADAPTIVE_BELT"); + put("Scarf's Studies", "SCARF_STUDIES"); + + put("First Master Star", "FIRST_MASTER_STAR"); // F3 M3 + put("Adaptive Helmet", "ADAPTIVE_HELMET"); + put("Adaptive Chestplate", "ADAPTIVE_CHESTPLATE"); + put("Adaptive Leggings", "ADAPTIVE_LEGGINGS"); + put("Adaptive Boots", "ADAPTIVE_BOOTS"); + put("Master Skull - Tier 3", "MASTER_SKULL_TIER_3"); + put("Suspicious Vial", "SUSPICIOUS_VIAL"); + + put("Spirit Sword", "SPIRIT_SWORD"); // F4 M4 + put("Spirit Shortbow", "ITEM_SPIRIT_BOW"); + put("Spirit Boots", "THORNS_BOOTS"); + put("Spirit", "LVL_1_LEGENDARY_SPIRIT"); // Spirit pet (Legendary) + put("Spirit Epic", "LVL_1_EPIC_SPIRIT"); + + put("Second Master Star", "SECOND_MASTER_STAR"); + put("Spirit Wing", "SPIRIT_WING"); + put("Spirit Bone", "SPIRIT_BONE"); + put("Spirit Stone", "SPIRIT_DECOY"); + + put("Shadow Fury", "SHADOW_FURY"); // F5 M5 + put("Last Breath", "LAST_BREATH"); + put("Third Master Star", "THIRD_MASTER_STAR"); + put("Warped Stone", "AOTE_STONE"); + put("Livid Dagger", "LIVID_DAGGER"); + put("Shadow Assassin Helmet", "SHADOW_ASSASSIN_HELMET"); + put("Shadow Assassin Chestplate", "SHADOW_ASSASSIN_CHESTPLATE"); + put("Shadow Assassin Leggings", "SHADOW_ASSASSIN_LEGGINGS"); + put("Shadow Assassin Boots", "SHADOW_ASSASSIN_BOOTS"); + put("Shadow Assassin Cloak", "SHADOW_ASSASSIN_CLOAK"); + put("Master Skull - Tier 4", "MASTER_SKULL_TIER_4"); + put("Dark Orb", "DARK_ORB"); + + put("Precursor Eye", "PRECURSOR_EYE"); // F6 M6 + put("Giant's Sword", "GIANTS_SWORD"); + put("Necromancer Lord Helmet", "NECROMANCER_LORD_HELMET"); + put("Necromancer Lord Chestplate", "NECROMANCER_LORD_CHESTPLATE"); + put("Necromancer Lord Leggings", "NECROMANCER_LORD_LEGGINGS"); + put("Necromancer Lord Boots", "NECROMANCER_LORD_BOOTS"); + put("Fourth Master Star", "FOURTH_MASTER_STAR"); + put("Summoning Ring", "SUMMONING_RING"); + put("Fel Skull", "FEL_SKULL"); + put("Necromancer Sword", "NECROMANCER_SWORD"); + put("Soulweaver Gloves", "SOULWEAVER_GLOVES"); + put("Sadan's Brooch", "SADAN_BROOCH"); + put("Giant Tooth", "GIANT_TOOTH"); + + put("Precursor Gear", "PRECURSOR_GEAR"); // F7 M7 + put("Necron Dye", "DYE_NECRON"); + put("Storm the Fish", "STORM_THE_FISH"); + put("Maxor the Fish", "MAXOR_THE_FISH"); + put("Goldor the Fish", "GOLDOR_THE_FISH"); + put("Dark Claymore", "DARK_CLAYMORE"); + put("Necron's Handle", "NECRON_HANDLE"); + put("Master Skull - Tier 5", "MASTER_SKULL_TIER_5"); + put("Shadow Warp", "SHADOW_WARP_SCROLL"); + put("Wither Shield", "WITHER_SHIELD_SCROLL"); + put("Implosion", "IMPLOSION_SCROLL"); + put("Fifth Master Star", "FIFTH_MASTER_STAR"); + put("Auto Recombobulator", "AUTO_RECOMBOBULATOR"); + put("Wither Helmet", "WITHER_HELMET"); + put("Wither Chestplate", "WITHER_CHESTPLATE"); + put("Wither Leggings", "WITHER_LEGGINGS"); + put("Wither Boots", "WITHER_BOOTS"); + put("Wither Catalyst", "WITHER_CATALYST"); + put("Wither Cloak Sword", "WITHER_CLOAK"); + put("Wither Blood", "WITHER_BLOOD"); + + put("Shiny Wither Helmet", "SHINY_WITHER_HELMET"); // M7 shiny drops + put("Shiny Wither Chestplate", "SHINY_WITHER_CHESTPLATE"); + put("Shiny Wither Leggings", "SHINY_WITHER_LEGGINGS"); + put("Shiny Wither Boots", "SHINY_WITHER_BOOTS"); + put("Shiny Necron's Handle", "SHINY_NECRON_HANDLE"); // cool thing + + put("Dungeon Disc", "DUNGEON_DISC_1"); + put("Clown Disc", "DUNGEON_DISC_2"); + put("Watcher Disc", "DUNGEON_DISC_3"); + put("Old Disc", "DUNGEON_DISC_4"); + put("Necron Disc", "DUNGEON_DISC_5"); + }}; +} + diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/Fetchur.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/Fetchur.java index 6cc5f194..27cd62ad 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/Fetchur.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/Fetchur.java @@ -42,8 +42,6 @@ public class Fetchur extends ChatPatternListener { answers.put("yellow and see through", Text.translatable("block.minecraft.yellow_stained_glass").getString()); answers.put("circular and sometimes moves", Text.translatable("item.minecraft.compass").getString()); - // TODO remove when typo fixed by hypixel - answers.put("circlular and sometimes moves", Text.translatable("item.minecraft.compass").getString()); answers.put("expensive minerals", "Mithril"); answers.put("useful during celebrations", Text.translatable("item.minecraft.firework_rocket").getString()); answers.put("hot and gives energy", "Cheap / Decent / Black Coffee"); @@ -56,10 +54,5 @@ public class Fetchur extends ChatPatternListener { answers.put("shiny and makes sparks", Text.translatable("item.minecraft.flint_and_steel").getString()); answers.put("green and some dudes trade stuff for it", Text.translatable("item.minecraft.emerald").getString()); answers.put("red and soft", Text.translatable("block.minecraft.red_wool").getString()); - - // old riddles that should no longer be active - // TODO remove if not seen for a few months - answers.put("round and green, or purple", Text.translatable("item.minecraft.ender_pearl").getString()); // removed Aug 21, 2021 - answers.put("red and white and you can mine it", Text.translatable("block.minecraft.nether_quartz_ore").getString()); // removed Sep 26, 2023 } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java index fbef1bcb..1b3f402d 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java @@ -249,7 +249,7 @@ public class ItemTooltip { public static void nullWarning() { if (!sentNullWarning && client.player != null) { - client.player.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.itemTooltip.nullMessage")), false); + LOGGER.warn(Constants.PREFIX.get().append(Text.translatable("skyblocker.itemTooltip.nullMessage")).getString()); sentNullWarning = true; } } @@ -355,15 +355,19 @@ public class ItemTooltip { // If these options is true beforehand, the client will get first data of these options while loading. // After then, it will only fetch the data if it is on Skyblock. - public static int minute = -1; + public static int minute = 0; public static void init() { Scheduler.INSTANCE.scheduleCyclic(() -> { - if (!Utils.isOnSkyblock() && 0 < minute++) { + if (!Utils.isOnSkyblock() && 0 < minute) { sentNullWarning = false; return; } + if (++minute % 60 == 0) { + sentNullWarning = false; + } + List<CompletableFuture<Void>> futureList = new ArrayList<>(); TooltipInfoType.NPC.downloadIfEnabled(futureList); @@ -387,9 +391,10 @@ public class ItemTooltip { TooltipInfoType.MUSEUM.downloadIfEnabled(futureList); TooltipInfoType.COLOR.downloadIfEnabled(futureList); - minute++; - CompletableFuture.allOf(futureList.toArray(CompletableFuture[]::new)) - .whenComplete((unused, throwable) -> sentNullWarning = false); + CompletableFuture.allOf(futureList.toArray(CompletableFuture[]::new)).exceptionally(e -> { + LOGGER.error("Encountered unknown error while downloading tooltip data", e); + return null; + }); }, 1200, true); } }
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java index fc5c7087..d798451e 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java @@ -15,8 +15,8 @@ import java.util.function.Predicate; public enum TooltipInfoType implements Runnable { NPC("https://hysky.de/api/npcprice", itemTooltip -> itemTooltip.enableNPCPrice, true), - BAZAAR("https://hysky.de/api/bazaar", itemTooltip -> itemTooltip.enableBazaarPrice || SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.enableProfitCalculator || SkyblockerConfigManager.get().general.chestValue.enableChestValue, itemTooltip -> itemTooltip.enableBazaarPrice, false), - LOWEST_BINS("https://hysky.de/api/auctions/lowestbins", itemTooltip -> itemTooltip.enableLowestBIN || SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.enableProfitCalculator || SkyblockerConfigManager.get().general.chestValue.enableChestValue, itemTooltip -> itemTooltip.enableLowestBIN, false), + BAZAAR("https://hysky.de/api/bazaar", itemTooltip -> itemTooltip.enableBazaarPrice || SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.enableProfitCalculator || SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.croesusProfit || SkyblockerConfigManager.get().general.chestValue.enableChestValue, itemTooltip -> itemTooltip.enableBazaarPrice, false), + LOWEST_BINS("https://hysky.de/api/auctions/lowestbins", itemTooltip -> itemTooltip.enableLowestBIN || SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.enableProfitCalculator || SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.croesusProfit || SkyblockerConfigManager.get().general.chestValue.enableChestValue, itemTooltip -> itemTooltip.enableLowestBIN, false), ONE_DAY_AVERAGE("https://moulberry.codes/auction_averages_lbin/1day.json", itemTooltip -> itemTooltip.enableAvgBIN, false), THREE_DAY_AVERAGE("https://moulberry.codes/auction_averages_lbin/3day.json", itemTooltip -> itemTooltip.enableAvgBIN || SkyblockerConfigManager.get().general.searchOverlay.enableAuctionHouse, itemTooltip -> itemTooltip.enableAvgBIN, false), MOTES("https://hysky.de/api/motesprice", itemTooltip -> itemTooltip.enableMotesPrice, itemTooltip -> itemTooltip.enableMotesPrice && Utils.isInTheRift(), true), diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java index 6629c377..45a52388 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java @@ -92,8 +92,8 @@ public class MythologicalRitual { griffinBurrows.get(pos).init(); } } else if (ParticleTypes.DUST.equals(packet.getParameters().getType())) { - BlockPos pos = BlockPos.ofFloored(packet.getX(), packet.getY(), packet.getZ()).down(2); - GriffinBurrow burrow = griffinBurrows.get(pos); + BlockPos pos = BlockPos.ofFloored(packet.getX(), packet.getY(), packet.getZ()); + GriffinBurrow burrow = griffinBurrows.get(pos.down(2)); if (burrow == null) { return; } @@ -106,7 +106,7 @@ public class MythologicalRitual { if (burrow.nextBurrowLine == null) { burrow.nextBurrowLine = new Vec3d[1001]; } - fillLine(burrow.nextBurrowLine, Vec3d.of(pos), nextBurrowDirection); + fillLine(burrow.nextBurrowLine, Vec3d.ofCenter(pos.up()), nextBurrowDirection); } else if (ParticleTypes.DRIPPING_LAVA.equals(packet.getParameters().getType()) && packet.getCount() == 2) { if (System.currentTimeMillis() > lastEchoTime + 10_000) { return; diff --git a/src/main/java/de/hysky/skyblocker/utils/FileUtils.java b/src/main/java/de/hysky/skyblocker/utils/FileUtils.java new file mode 100644 index 00000000..22611441 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/FileUtils.java @@ -0,0 +1,36 @@ +package de.hysky.skyblocker.utils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.slf4j.Logger; + +import com.mojang.logging.LogUtils; + +public class FileUtils { + private static final Logger LOGGER = LogUtils.getLogger(); + + public static void recursiveDelete(Path dir) throws IOException { + if (Files.isDirectory(dir) && !Files.isSymbolicLink(dir)) { + Files.list(dir).forEach(child -> { + try { + recursiveDelete(child); + } catch (Exception e) { + LOGGER.error("[Skyblocker] Encountered an exception while deleting a file! Path: {}", child.toAbsolutePath(), e); + } + }); + } + + Files.delete(dir); + } + + /** + * Replaces any characters that do not match the regex: [^a-z0-9_.-] + * + * @implNote Designed to convert a file path to an {@link net.minecraft.util.Identifier} + */ + public static String normalizePath(Path path) { + return path.toString().toLowerCase().replaceAll("[^a-z0-9_.-]", ""); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/Http.java b/src/main/java/de/hysky/skyblocker/utils/Http.java index 58deced2..871eac78 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Http.java +++ b/src/main/java/de/hysky/skyblocker/utils/Http.java @@ -33,10 +33,6 @@ public class Http { .followRedirects(Redirect.NORMAL) .build(); - public static String sendGetRequest(String url) throws IOException, InterruptedException { - return sendCacheableGetRequest(url).content(); - } - private static ApiResponse sendCacheableGetRequest(String url) throws IOException, InterruptedException { HttpRequest request = HttpRequest.newBuilder() .GET() @@ -55,6 +51,26 @@ public class Http { return new ApiResponse(body, response.statusCode(), getCacheStatuses(headers), getAge(headers)); } + + public static InputStream downloadContent(String url) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate") + .header("User-Agent", USER_AGENT) + .version(Version.HTTP_2) + .uri(URI.create(url)) + .build(); + + HttpResponse<InputStream> response = HTTP_CLIENT.send(request, BodyHandlers.ofInputStream()); + InputStream decodedInputStream = getDecodedInputStream(response); + + return decodedInputStream; + } + + public static String sendGetRequest(String url) throws IOException, InterruptedException { + return sendCacheableGetRequest(url).content(); + } public static HttpHeaders sendHeadRequest(String url) throws IOException, InterruptedException { HttpRequest request = HttpRequest.newBuilder() diff --git a/src/main/java/de/hysky/skyblocker/utils/NEURepoManager.java b/src/main/java/de/hysky/skyblocker/utils/NEURepoManager.java index 870e94da..c779d666 100644 --- a/src/main/java/de/hysky/skyblocker/utils/NEURepoManager.java +++ b/src/main/java/de/hysky/skyblocker/utils/NEURepoManager.java @@ -13,13 +13,10 @@ import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.stream.Stream; /** * Initializes the NEU repo, which contains item metadata and fairy souls location data. Clones the repo if it does not exist and checks for updates. Use {@link #runAsyncAfterLoad(Runnable)} to run code after the repo is initialized. @@ -76,7 +73,7 @@ public class NEURepoManager { CompletableFuture.runAsync(() -> { try { ItemRepository.setFilesImported(false); - recursiveDelete(NEURepoManager.LOCAL_REPO_DIR); + FileUtils.recursiveDelete(NEURepoManager.LOCAL_REPO_DIR); } catch (Exception ex) { if (MinecraftClient.getInstance().player != null) MinecraftClient.getInstance().player.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.updaterepository.failed")), false); @@ -86,21 +83,6 @@ public class NEURepoManager { }); } - @SuppressWarnings("ResultOfMethodCallIgnored") - private static void recursiveDelete(Path dir) throws IOException { - if (Files.isDirectory(dir) && !Files.isSymbolicLink(dir)) { - Files.list(dir).forEach(child -> { - try { - recursiveDelete(child); - } catch (Exception e) { - LOGGER.error("[Skyblocker] Encountered an exception while deleting a file! Path: {}", child.toAbsolutePath(), e); - } - }); - } - - Files.delete(dir); - } - /** * Runs the given runnable after the NEU repo is initialized. * @param runnable the runnable to run diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java index f78e5184..460f34dd 100644 --- a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java @@ -3,6 +3,7 @@ package de.hysky.skyblocker.utils.render.gui; import com.mojang.blaze3d.systems.RenderSystem; import de.hysky.skyblocker.mixin.accessor.HandledScreenAccessor; import de.hysky.skyblocker.skyblock.dungeon.CroesusHelper; +import de.hysky.skyblocker.skyblock.dungeon.CroesusProfit; import de.hysky.skyblocker.skyblock.dungeon.terminal.ColorTerminal; import de.hysky.skyblocker.skyblock.dungeon.terminal.OrderTerminal; import de.hysky.skyblocker.skyblock.dungeon.terminal.StartsWithTerminal; @@ -40,6 +41,7 @@ public class ContainerSolverManager { new OrderTerminal(), new StartsWithTerminal(), new CroesusHelper(), + new CroesusProfit(), new ChronomatronSolver(), new SuperpairsSolver(), UltrasequencerSolver.INSTANCE |