From 1d3303178635566b14a6f24b5741bbf331113f84 Mon Sep 17 00:00:00 2001 From: Aaron <51387595+AzureAaron@users.noreply.github.com> Date: Sun, 24 Mar 2024 02:27:13 -0400 Subject: Accessories Helper --- .../java/de/hysky/skyblocker/SkyblockerMod.java | 2 + .../hysky/skyblocker/config/SkyblockerConfig.java | 3 + .../config/categories/GeneralCategory.java | 10 + .../skyblock/item/tooltip/AccessoriesHelper.java | 230 +++++++++++++++++++++ .../skyblock/item/tooltip/ItemTooltip.java | 23 +++ .../skyblock/item/tooltip/TooltipInfoType.java | 31 ++- .../resources/assets/skyblocker/lang/en_us.json | 7 + 7 files changed, 299 insertions(+), 7 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index e334ef86..ee82209d 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -25,6 +25,7 @@ import de.hysky.skyblocker.skyblock.end.TheEnd; import de.hysky.skyblocker.skyblock.garden.FarmingHud; import de.hysky.skyblocker.skyblock.garden.VisitorHelper; import de.hysky.skyblocker.skyblock.item.*; +import de.hysky.skyblocker.skyblock.item.tooltip.AccessoriesHelper; import de.hysky.skyblocker.skyblock.item.tooltip.BackpackPreview; import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; @@ -102,6 +103,7 @@ public class SkyblockerMod implements ClientModInitializer { PlayerHeadHashCache.init(); HotbarSlotLock.init(); ItemTooltip.init(); + AccessoriesHelper.init(); WikiLookup.init(); FairySouls.init(); Relics.init(); diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java index a3e710c1..8d65806f 100644 --- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java @@ -555,6 +555,9 @@ public class SkyblockerConfig { @SerialEntry public boolean enableExoticTooltip = true; + + @SerialEntry + public boolean enableAccessoriesHelper = true; } public static class ItemInfoDisplay { diff --git a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java index 23ce7bb6..abe20e18 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java @@ -434,6 +434,16 @@ public class GeneralCategory { newValue -> config.general.itemTooltip.enableExoticTooltip = newValue) .controller(ConfigUtils::createBooleanController) .build()) + .option(Option.createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper")) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[0]"), Text.literal("\n\n✔ Collected").formatted(Formatting.GREEN), Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[1]"), + Text.literal("\n✦ Upgrade").withColor(0x218bff), Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[2]"), Text.literal("\n↑ Upgradable").withColor(0xf8d048), Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[3]"), + Text.literal("\n↓ Downgrade").formatted(Formatting.GRAY), Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[4]"), Text.literal("\n✖ Missing").formatted(Formatting.RED), Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[5]"))) + .binding(defaults.general.itemTooltip.enableAccessoriesHelper, + () -> config.general.itemTooltip.enableAccessoriesHelper, + newValue -> config.general.itemTooltip.enableAccessoriesHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) .build()) //Item Info Display diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java new file mode 100644 index 00000000..b5291af6 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java @@ -0,0 +1,230 @@ +package de.hysky.skyblocker.skyblock.item.tooltip; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import java.util.function.ToIntFunction; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +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 com.mojang.util.UndashedUuid; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.Utils; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; +import net.minecraft.screen.GenericContainerScreenHandler; +import net.minecraft.screen.slot.Slot; + +public class AccessoriesHelper { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final Path FILE = SkyblockerMod.CONFIG_DIR.resolve("collected_accessories.json"); + private static final Pattern ACCESSORY_BAG_TITLE = Pattern.compile("Accessory Bag \\(\\d+\\/\\d+\\)"); + //UUID -> Profile Id & Data + private static final Map> COLLECTED_ACCESSORIES = new Object2ObjectOpenHashMap<>(); + private static final Predicate NON_EMPTY = s -> !s.isEmpty(); + private static final Predicate HAS_FAMILY = Accessory::hasFamily; + private static final ToIntFunction ACCESSORY_TIER = Accessory::tier; + + private static Map ACCESSORY_DATA = new Object2ObjectOpenHashMap<>(); + //remove?? + private static CompletableFuture loaded; + + public static void init() { + ClientLifecycleEvents.CLIENT_STARTED.register((_client) -> load()); + ClientLifecycleEvents.CLIENT_STOPPING.register((_client) -> save()); + ScreenEvents.BEFORE_INIT.register((_client, screen, _scaledWidth, _scaledHeight) -> { + if (Utils.isOnSkyblock() && TooltipInfoType.ACCESSORIES.isTooltipEnabled() && !Utils.getProfileId().isEmpty() && screen instanceof GenericContainerScreen genericContainerScreen) { + if (ACCESSORY_BAG_TITLE.matcher(genericContainerScreen.getTitle().getString()).matches()) { + ScreenEvents.afterRender(screen).register((_screen, _context, _mouseX, _mouseY, _delta) -> { + GenericContainerScreenHandler handler = genericContainerScreen.getScreenHandler(); + + collectAccessories(handler.slots.subList(0, handler.getRows() * 9)); + }); + } + } + }); + } + + private static void load() { + loaded = CompletableFuture.runAsync(() -> { + try (BufferedReader reader = Files.newBufferedReader(FILE)) { + COLLECTED_ACCESSORIES.putAll(ProfileAccessoryData.SERIALIZATION_CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).result().orElseThrow()); + } catch (NoSuchFileException ignored) { + } catch (Exception e) { + LOGGER.error("[Skyblocker Accessory Helper] Failed to load accessory file!", e); + } + }); + } + + private static void save() { + try (BufferedWriter writer = Files.newBufferedWriter(FILE)) { + SkyblockerMod.GSON.toJson(ProfileAccessoryData.SERIALIZATION_CODEC.encodeStart(JsonOps.INSTANCE, COLLECTED_ACCESSORIES).result().orElseThrow(), writer); + } catch (Exception e) { + LOGGER.error("[Skyblocker Accessory Helper] Failed to save accessory file!", e); + } + } + + private static void collectAccessories(List slots) { + //Is this even needed? + if (!loaded.isDone()) return; + + List accessoryIds = slots.stream() + .map(Slot::getStack) + .map(ItemUtils::getItemId) + .filter(NON_EMPTY) + .collect(Collectors.toUnmodifiableList()); + + String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull()); + + Map playerData = COLLECTED_ACCESSORIES.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()); + playerData.putIfAbsent(Utils.getProfileId(), ProfileAccessoryData.createDefault()); + + ProfileAccessoryData profileData = playerData.get(Utils.getProfileId()); + + profileData.accessoryIds().addAll(accessoryIds); + } + + static AccessoryReport calculateReport4Accessory(String accessoryId) { + if (!ACCESSORY_DATA.containsKey(accessoryId) || Utils.getProfileId().isEmpty()) return AccessoryReport.INELIGIBLE; + + Accessory accessory = ACCESSORY_DATA.get(accessoryId); + String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull()); + Set collectedAccessories = COLLECTED_ACCESSORIES.get(uuid).get(Utils.getProfileId()).accessoryIds().stream() + .filter(ACCESSORY_DATA::containsKey) + .map(ACCESSORY_DATA::get) + .collect(Collectors.toSet()); + + //If the player has this accessory and it doesn't belong to a family + if (collectedAccessories.contains(accessory) && accessory.family().isEmpty()) return AccessoryReport.HAS_HIGHEST_TIER; + + Predicate HAS_SAME_FAMILY = accessory::hasSameFamily; + Set collectedAccessoriesInTheSameFamily = collectedAccessories.stream() + .filter(HAS_FAMILY) + .filter(HAS_SAME_FAMILY) + .collect(Collectors.toSet()); + + //If the player doesn't have any collected accessories with same family + if (collectedAccessoriesInTheSameFamily.isEmpty()) return AccessoryReport.MISSING; + + Set accessoriesInTheSameFamily = ACCESSORY_DATA.values().stream() + .filter(HAS_FAMILY) + .filter(HAS_SAME_FAMILY) + .collect(Collectors.toSet()); + + ///If the player has the highest tier accessory in this family + //Take the the accessories in the same family as {@code accessory}, then get the one with the highest tier + Optional highestTierOfFamily = accessoriesInTheSameFamily.stream() + .max(Comparator.comparingInt(ACCESSORY_TIER)); + + if (highestTierOfFamily.isPresent()) { + Accessory highestTier = highestTierOfFamily.orElseThrow(); + + if (collectedAccessoriesInTheSameFamily.contains(highestTier)) return AccessoryReport.HAS_HIGHEST_TIER; + + //For when the highest tier is tied + if (highestTier.hasSameFamily(accessory) && collectedAccessoriesInTheSameFamily.stream().allMatch(ca -> ca.tier() == highestTier.tier())) return AccessoryReport.HAS_HIGHEST_TIER; + } + + //If this accessory is a higher tier than all of other collected accessories in the same family + OptionalInt highestTierOfAllCollectedInFamily = collectedAccessoriesInTheSameFamily.stream() + .mapToInt(ACCESSORY_TIER) + .max(); + + if (highestTierOfAllCollectedInFamily.isPresent() && accessory.tier() > highestTierOfAllCollectedInFamily.orElseThrow()) return AccessoryReport.IS_GREATER_TIER; + + //If this accessory is a lower tier than one already obtained from same family + if (highestTierOfAllCollectedInFamily.isPresent() && accessory.tier() < highestTierOfAllCollectedInFamily.orElseThrow()) return AccessoryReport.OWNS_BETTER_TIER; + + //If there is an accessory in the same family that has a higher tier + //Take the accessories in the same family, then check if there is an accessory whose tier is greater than {@code accessory} + boolean hasGreaterTierInFamily = accessoriesInTheSameFamily.stream() + .anyMatch(ca -> ca.tier() > accessory.tier()); + + if (hasGreaterTierInFamily) return AccessoryReport.HAS_GREATER_TIER; + + return AccessoryReport.MISSING; + } + + static void refreshData(JsonObject data) { + try { + Map accessoryData = Accessory.MAP_CODEC.parse(JsonOps.INSTANCE, data).result().orElseThrow(); + + ACCESSORY_DATA = accessoryData; + } catch (Exception e) { + LOGGER.error("[Skyblocker Accessory Helper] Failed to parse data!", e); + } + } + + private record ProfileAccessoryData(Set accessoryIds) { + private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.STRING.listOf() + .xmap(ObjectOpenHashSet::new, ObjectArrayList::new) + .fieldOf("accessoryIds") + .forGetter(i -> new ObjectOpenHashSet(i.accessoryIds()))) + .apply(instance, ProfileAccessoryData::new)); + //Mojang's internal Codec implementation uses ImmutableMaps so we'll just xmap those away and type safety while we're at it :') + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static final Codec>> SERIALIZATION_CODEC = Codec.unboundedMap(Codec.STRING, Codec.unboundedMap(Codec.STRING, CODEC) + .xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new)) + .xmap(Object2ObjectOpenHashMap::new, m -> (Map) new Object2ObjectOpenHashMap(m)); + + private static ProfileAccessoryData createDefault() { + return new ProfileAccessoryData(new ObjectOpenHashSet<>()); + } + } + + /** + * @author AzureAaron + * @implSpec Aaron's Mod + */ + private record Accessory(String id, Optional family, int tier) { + private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.STRING.fieldOf("id").forGetter(Accessory::id), + Codec.STRING.optionalFieldOf("family").forGetter(Accessory::family), + Codec.INT.optionalFieldOf("tier", 0).forGetter(Accessory::tier)) + .apply(instance, Accessory::new)); + private static final Codec> MAP_CODEC = Codec.unboundedMap(Codec.STRING, CODEC); + + private boolean hasFamily() { + return family.isPresent(); + } + + private boolean hasSameFamily(Accessory other) { + return other.family().equals(this.family); + } + } + + enum AccessoryReport { + HAS_HIGHEST_TIER, //You've collected the highest tier - Collected + IS_GREATER_TIER, //This accessory is an upgrade from the one in the same family that you already have - Upgrade + HAS_GREATER_TIER, //This accessory has a higher tier upgrade - Upgradable + OWNS_BETTER_TIER, //You've collected an accessory in this family with a higher tier - Downgrade + MISSING, //You don't have any accessories in this family - Missing + INELIGIBLE; + } +} 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 1b3f402d..8a11b9bd 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 @@ -5,6 +5,7 @@ import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.config.SkyblockerConfig; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.skyblock.item.MuseumItemCache; +import de.hysky.skyblocker.skyblock.item.tooltip.AccessoriesHelper.AccessoryReport; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; @@ -238,6 +239,27 @@ public class ItemTooltip { } } } + + if (TooltipInfoType.ACCESSORIES.isTooltipEnabledAndHasOrNullWarning(internalID)) { + AccessoryReport report = AccessoriesHelper.calculateReport4Accessory(internalID); + + if (report != AccessoryReport.INELIGIBLE) { + MutableText title = Text.literal(String.format("%-19s", "Accessory: ")).withColor(0xf57542); + + Text stateText = switch (report) { + case HAS_HIGHEST_TIER -> Text.literal("✔ Collected").formatted(Formatting.GREEN); + case IS_GREATER_TIER -> Text.literal("✦ Upgrade").withColor(0x218bff); + case HAS_GREATER_TIER -> Text.literal("↑ Upgradable").withColor(0xf8d048); + case OWNS_BETTER_TIER -> Text.literal("↓ Downgrade").formatted(Formatting.GRAY); + case MISSING -> Text.literal("✖ Missing").formatted(Formatting.RED); + + //Should never be the case + default -> Text.literal("? Unknown").formatted(Formatting.GRAY); + }; + + lines.add(title.append(stateText)); + } + } } private static void addExoticTooltip(List lines, String internalID, NbtCompound nbt, String colorHex, String expectedHex, String existingTooltip) { @@ -390,6 +412,7 @@ public class ItemTooltip { TooltipInfoType.MOTES.downloadIfEnabled(futureList); TooltipInfoType.MUSEUM.downloadIfEnabled(futureList); TooltipInfoType.COLOR.downloadIfEnabled(futureList); + TooltipInfoType.ACCESSORIES.downloadIfEnabled(futureList); CompletableFuture.allOf(futureList.toArray(CompletableFuture[]::new)).exceptionally(e -> { LOGGER.error("Encountered unknown error while downloading tooltip data", e); 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 4aba040d..d0cf91b8 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 @@ -6,13 +6,15 @@ import de.hysky.skyblocker.config.SkyblockerConfig; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.utils.Http; import de.hysky.skyblocker.utils.Utils; -import org.jetbrains.annotations.Nullable; import java.net.http.HttpHeaders; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; import java.util.function.Predicate; +import org.jetbrains.annotations.Nullable; + 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().locations.dungeons.dungeonChestProfit.croesusProfit || SkyblockerConfigManager.get().general.chestValue.enableChestValue, itemTooltip -> itemTooltip.enableBazaarPrice, false), @@ -22,7 +24,8 @@ public enum TooltipInfoType implements Runnable { MOTES("https://hysky.de/api/motesprice", itemTooltip -> itemTooltip.enableMotesPrice, itemTooltip -> itemTooltip.enableMotesPrice && Utils.isInTheRift(), true), OBTAINED(itemTooltip -> itemTooltip.enableObtainedDate), MUSEUM("https://hysky.de/api/museum", itemTooltip -> itemTooltip.enableMuseumInfo, true), - COLOR("https://hysky.de/api/color", itemTooltip -> itemTooltip.enableExoticTooltip, true); + COLOR("https://hysky.de/api/color", itemTooltip -> itemTooltip.enableExoticTooltip, true), + ACCESSORIES("https://api.azureaaron.net/skyblock/accessories", itemTooltip -> itemTooltip.enableAccessoriesHelper, true, AccessoriesHelper::refreshData); private final String address; private final Predicate dataEnabled; @@ -30,12 +33,23 @@ public enum TooltipInfoType implements Runnable { private JsonObject data; private final boolean cacheable; private long hash; + private final Consumer callback; /** * Use this for when you're adding tooltip info that has no data associated with it */ TooltipInfoType(Predicate enabled) { - this(null, itemTooltip -> false, enabled, null, false); + this(null, itemTooltip -> false, enabled, false, null); + } + + /** + * @param address the address to download the data from + * @param enabled the predicate to check if the data should be downloaded and the tooltip should be shown + * @param cacheable whether the data should be cached + * @param callback called when the {@code data} is refreshed + */ + TooltipInfoType(String address, Predicate enabled, boolean cacheable, Consumer callback) { + this(address, enabled, enabled, cacheable, callback); } /** @@ -44,7 +58,7 @@ public enum TooltipInfoType implements Runnable { * @param cacheable whether the data should be cached */ TooltipInfoType(String address, Predicate enabled, boolean cacheable) { - this(address, enabled, enabled, null, cacheable); + this(address, enabled, enabled, cacheable, null); } /** @@ -54,7 +68,7 @@ public enum TooltipInfoType implements Runnable { * @param cacheable whether the data should be cached */ TooltipInfoType(String address, Predicate dataEnabled, Predicate tooltipEnabled, boolean cacheable) { - this(address, dataEnabled, tooltipEnabled, null, cacheable); + this(address, dataEnabled, tooltipEnabled, cacheable, null); } /** @@ -64,12 +78,13 @@ public enum TooltipInfoType implements Runnable { * @param data the data * @param cacheable whether the data should be cached */ - TooltipInfoType(String address, Predicate dataEnabled, Predicate tooltipEnabled, @Nullable JsonObject data, boolean cacheable) { + TooltipInfoType(String address, Predicate dataEnabled, Predicate tooltipEnabled, boolean cacheable, @Nullable Consumer callback) { this.address = address; this.dataEnabled = dataEnabled; this.tooltipEnabled = tooltipEnabled; - this.data = data; + this.data = null; this.cacheable = cacheable; + this.callback = callback; } /** @@ -146,6 +161,8 @@ public enum TooltipInfoType implements Runnable { else this.hash = hash; } data = SkyblockerMod.GSON.fromJson(Http.sendGetRequest(address), JsonObject.class); + + if (callback != null) callback.accept(data); } catch (Exception e) { ItemTooltip.LOGGER.warn("[Skyblocker] Failed to download " + this + " prices!", e); } diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index ff0ffa11..380bcdc1 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -90,6 +90,13 @@ "text.autoconfig.skyblocker.option.general.itemTooltip.enableMuseumInfo.@Tooltip": "If this item is donatable to the museum, then the item's category in the musuem is displayed. It also displays a marker indicating whether you've donated that item to your musuem or not (freebies not yet supported).\n\nMake sure to enable your Museum API for accurate information!", "text.autoconfig.skyblocker.option.general.itemTooltip.enableExoticTooltip": "Enable Exotic Tooltip", "text.autoconfig.skyblocker.option.general.itemTooltip.enableExoticTooltip.@Tooltip": "Displays the type of exotic below the item's name if an armor piece is exotic.", + "text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper": "Enable Accessories Helper", + "text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[0]": "When hovering over an accessory you are informed about whether you already have it or not, and whether it's worse than what you have already collected or better. List of Statuses:", + "text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[1]": "You have the highest tier accessory from that family.", + "text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[2]": "This accessory is an upgrade from the one in the same family that you already have.", + "text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[3]": "This accessory can be upgraded.", + "text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[4]": "You already own an accessory in the same family that is better than this one.", + "text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[5]": "You don't own any accessory from this family.", "text.autoconfig.skyblocker.option.general.dungeonQuality": "Dungeon Quality", "text.autoconfig.skyblocker.option.general.dungeonQuality.@Tooltip": "Displays quality and tier of dungeon drops from mobs.\n\n\nReminder:\nTier 1-3 dropped from F1-F3\nTier 4-7 dropped from F4-F7 or M1-M4\nTier 8-10 are dropped only from M5-M7", "text.autoconfig.skyblocker.option.general.itemInfoDisplay": "Item Info Display", -- cgit