Skip to content

Commit

Permalink
Merge pull request #613 from AzureAaron/accessories-helper
Browse files Browse the repository at this point in the history
Accessories Helper
  • Loading branch information
kevinthegreat1 authored Apr 4, 2024
2 parents 97a5b8a + 69d9257 commit bbf544b
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 7 deletions.
2 changes: 2 additions & 0 deletions src/main/java/de/hysky/skyblocker/SkyblockerMod.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import de.hysky.skyblocker.skyblock.garden.LowerSensitivity;
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;
Expand Down Expand Up @@ -103,6 +104,7 @@ public void onInitializeClient() {
PlayerHeadHashCache.init();
HotbarSlotLock.init();
ItemTooltip.init();
AccessoriesHelper.init();
WikiLookup.init();
FairySouls.init();
Relics.init();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,9 @@ public static class ItemTooltip {

@SerialEntry
public boolean enableExoticTooltip = true;

@SerialEntry
public boolean enableAccessoriesHelper = true;
}

public static class ItemInfoDisplay {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,16 @@ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig
newValue -> config.general.itemTooltip.enableExoticTooltip = newValue)
.controller(ConfigUtils::createBooleanController)
.build())
.option(Option.<Boolean>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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
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.Matcher;
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.Pair;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
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 \\((?<page>\\d+)/\\d+\\)");
//UUID -> Profile Id & Data
private static final Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, ProfileAccessoryData>> COLLECTED_ACCESSORIES = new Object2ObjectOpenHashMap<>();
private static final Predicate<String> NON_EMPTY = s -> !s.isEmpty();
private static final Predicate<Accessory> HAS_FAMILY = Accessory::hasFamily;
private static final ToIntFunction<Accessory> ACCESSORY_TIER = Accessory::tier;

private static Map<String, Accessory> ACCESSORY_DATA = new Object2ObjectOpenHashMap<>();
//remove??
private static CompletableFuture<Void> 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) {
Matcher matcher = ACCESSORY_BAG_TITLE.matcher(genericContainerScreen.getTitle().getString());

if (matcher.matches()) {
ScreenEvents.afterTick(screen).register(_screen -> {
GenericContainerScreenHandler handler = genericContainerScreen.getScreenHandler();

collectAccessories(handler.slots.subList(0, handler.getRows() * 9), Integer.parseInt(matcher.group("page")));
});
}
}
});
}

//Note: JsonOps.COMPRESSED must be used if you're using maps with non-string keys
private static void load() {
loaded = CompletableFuture.runAsync(() -> {
try (BufferedReader reader = Files.newBufferedReader(FILE)) {
COLLECTED_ACCESSORIES.putAll(ProfileAccessoryData.SERIALIZATION_CODEC.parse(JsonOps.COMPRESSED, 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.COMPRESSED, COLLECTED_ACCESSORIES).result().orElseThrow(), writer);
} catch (Exception e) {
LOGGER.error("[Skyblocker Accessory Helper] Failed to save accessory file!", e);
}
}

private static void collectAccessories(List<Slot> slots, int page) {
//Is this even needed?
if (!loaded.isDone()) return;

List<String> accessoryIds = slots.stream()
.map(Slot::getStack)
.map(ItemUtils::getItemId)
.filter(NON_EMPTY)
.toList();

String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull());

COLLECTED_ACCESSORIES.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()).computeIfAbsent(Utils.getProfileId(), profileId -> ProfileAccessoryData.createDefault()).pages()
.put(page, new ObjectOpenHashSet<>(accessoryIds));
}

static Pair<AccessoryReport, String> calculateReport4Accessory(String accessoryId) {
if (!ACCESSORY_DATA.containsKey(accessoryId) || Utils.getProfileId().isEmpty()) return Pair.of(AccessoryReport.INELIGIBLE, null);

Accessory accessory = ACCESSORY_DATA.get(accessoryId);
String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull());
Set<Accessory> collectedAccessories = COLLECTED_ACCESSORIES.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()).computeIfAbsent(Utils.getProfileId(), profileId -> ProfileAccessoryData.createDefault()).pages().values().stream()
.flatMap(ObjectOpenHashSet::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 Pair.of(AccessoryReport.HAS_HIGHEST_TIER, null);

Predicate<Accessory> HAS_SAME_FAMILY = accessory::hasSameFamily;
Set<Accessory> 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 Pair.of(AccessoryReport.MISSING, null);

Set<Accessory> 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 accessories in the same family as {@code accessory}, then get the one with the highest tier
Optional<Accessory> highestTierOfFamily = accessoriesInTheSameFamily.stream()
.max(Comparator.comparingInt(ACCESSORY_TIER));
int maxTierInFamily = highestTierOfFamily.orElse(Accessory.EMPTY).tier();

if (collectedAccessoriesInTheSameFamily.stream().anyMatch(ca -> ca.tier() == maxTierInFamily)) return Pair.of(AccessoryReport.HAS_HIGHEST_TIER, null);

//If this accessory is a higher tier than all the other collected accessories in the same family
OptionalInt highestTierOfAllCollectedInFamily = collectedAccessoriesInTheSameFamily.stream()
.mapToInt(ACCESSORY_TIER)
.max();

if (accessory.tier() > highestTierOfAllCollectedInFamily.getAsInt()) return Pair.of(AccessoryReport.IS_GREATER_TIER, String.format("(%d→%d/%d)", highestTierOfAllCollectedInFamily.orElse(0), accessory.tier(), maxTierInFamily));

//If this accessory is a lower tier than one already obtained from same family
if (accessory.tier() < highestTierOfAllCollectedInFamily.getAsInt()) return Pair.of(AccessoryReport.OWNS_BETTER_TIER, String.format("(%d→%d/%d)", highestTierOfAllCollectedInFamily.orElse(0), accessory.tier(), maxTierInFamily));

//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 Pair.of(AccessoryReport.HAS_GREATER_TIER, String.format("(%d/%d)", highestTierOfAllCollectedInFamily.orElse(0), maxTierInFamily));

return Pair.of(AccessoryReport.MISSING, null);
}

static void refreshData(JsonObject data) {
try {
ACCESSORY_DATA = Accessory.MAP_CODEC.parse(JsonOps.INSTANCE, data).result().orElseThrow();
} catch (Exception e) {
LOGGER.error("[Skyblocker Accessory Helper] Failed to parse data!", e);
}
}

private record ProfileAccessoryData(Int2ObjectOpenHashMap<ObjectOpenHashSet<String>> pages) {
private static final Codec<ProfileAccessoryData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.unboundedMap(Codec.INT, Codec.STRING.listOf().xmap(ObjectOpenHashSet::new, ObjectArrayList::new))
.xmap(Int2ObjectOpenHashMap::new, Int2ObjectOpenHashMap::new).fieldOf("pages").forGetter(ProfileAccessoryData::pages))
.apply(instance, ProfileAccessoryData::new));
private static final Codec<Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, ProfileAccessoryData>>> SERIALIZATION_CODEC = Codec.unboundedMap(Codec.STRING, Codec.unboundedMap(Codec.STRING, CODEC)
.xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new))
.xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new);

private static ProfileAccessoryData createDefault() {
return new ProfileAccessoryData(new Int2ObjectOpenHashMap<>());
}
}

/**
* @author AzureAaron
* @implSpec <a href="https://github.com/AzureAaron/aaron-mod/blob/1.20/src/main/java/net/azureaaron/mod/commands/MagicalPowerCommand.java#L475">Aaron's Mod</a>
*/
private record Accessory(String id, Optional<String> family, int tier) {
private static final Codec<Accessory> 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<String, Accessory>> MAP_CODEC = Codec.unboundedMap(Codec.STRING, CODEC);
private static final Accessory EMPTY = new Accessory("", Optional.empty(), 0);

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 -- Shows you what tier this accessory is in its family
HAS_GREATER_TIER, //This accessory has a higher tier upgrade - Upgradable -- Shows you the highest tier accessory you've collected in that family
OWNS_BETTER_TIER, //You've collected an accessory in this family with a higher tier - Downgrade -- Shows you the highest tier accessory you've collected in that family
MISSING, //You don't have any accessories in this family - Missing
INELIGIBLE
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
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;
import de.hysky.skyblocker.utils.scheduler.Scheduler;
import it.unimi.dsi.fastutil.Pair;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.item.TooltipContext;
import net.minecraft.item.DyeableItem;
Expand Down Expand Up @@ -238,6 +240,27 @@ public static void getTooltip(ItemStack stack, TooltipContext context, List<Text
}
}
}

if (TooltipInfoType.ACCESSORIES.isTooltipEnabledAndHasOrNullWarning(internalID)) {
Pair<AccessoryReport, String> report = AccessoriesHelper.calculateReport4Accessory(internalID);

if (report.left() != AccessoryReport.INELIGIBLE) {
MutableText title = Text.literal(String.format("%-19s", "Accessory: ")).withColor(0xf57542);

Text stateText = switch (report.left()) {
case HAS_HIGHEST_TIER -> Text.literal("✔ Collected").formatted(Formatting.GREEN);
case IS_GREATER_TIER -> Text.literal("✦ Upgrade ").withColor(0x218bff).append(Text.literal(report.right()).withColor(0xf8f8ff));
case HAS_GREATER_TIER -> Text.literal("↑ Upgradable ").withColor(0xf8d048).append(Text.literal(report.right()).withColor(0xf8f8ff));
case OWNS_BETTER_TIER -> Text.literal("↓ Downgrade ").formatted(Formatting.GRAY).append(Text.literal(report.right()).withColor(0xf8f8ff));
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<Text> lines, String internalID, NbtCompound nbt, String colorHex, String expectedHex, String existingTooltip) {
Expand Down Expand Up @@ -390,6 +413,7 @@ public static void init() {
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);
Expand Down
Loading

0 comments on commit bbf544b

Please sign in to comment.