From e4ea0c5fffa16868d638c2d6cc23f610669bd5fb Mon Sep 17 00:00:00 2001 From: Kevin <92656833+kevinthegreat1@users.noreply.github.com> Date: Sat, 4 Jan 2025 21:06:01 -0500 Subject: [PATCH] Profiled data api (#1102) * Add ProfiledData * Refactor end stats * Use UUID * Refactor MuseumItemCache * null safety + item cooldown bug fix * Migrate SlayerTimer and fix compressed * Fix accessories crash * Migrate PowderMiningTracker * Create directories * Add async options * Migrate async saves --- .../categories/OtherLocationsCategory.java | 6 +- .../hysky/skyblocker/skyblock/PetCache.java | 56 +----- .../skyblock/dwarven/PowderMiningTracker.java | 58 +----- .../skyblocker/skyblock/end/EndHudWidget.java | 7 +- .../hysky/skyblocker/skyblock/end/TheEnd.java | 134 +++----------- .../skyblock/item/ItemCooldowns.java | 4 +- .../skyblock/item/MuseumItemCache.java | 98 +++------- .../item/tooltip/AccessoriesHelper.java | 50 +----- .../skyblock/slayers/SlayerTimer.java | 53 +----- .../tabhud/widget/ComponentBasedWidget.java | 2 +- .../java/de/hysky/skyblocker/utils/Utils.java | 9 +- .../utils/profile/ProfiledData.java | 169 ++++++++++++++++++ 12 files changed, 265 insertions(+), 381 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/utils/profile/ProfiledData.java diff --git a/src/main/java/de/hysky/skyblocker/config/categories/OtherLocationsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/OtherLocationsCategory.java index 259e91d9bf..c7fa6cb272 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/OtherLocationsCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/OtherLocationsCategory.java @@ -134,11 +134,7 @@ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig .option(ButtonOption.createBuilder() .name(Text.translatable("skyblocker.config.otherLocations.end.resetName")) .text(Text.translatable("skyblocker.config.otherLocations.end.resetText")) - .action((screen, opt) -> { - TheEnd.zealotsKilled = 0; - TheEnd.zealotsSinceLastEye = 0; - TheEnd.eyes = 0; - }) + .action((screen, opt) -> TheEnd.PROFILES_STATS.put(TheEnd.EndStats.EMPTY)) .build()) .option(Option.createBuilder() .name(Text.translatable("skyblocker.config.otherLocations.end.muteEndermanSounds")) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java b/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java index 35fea3fac6..603f10176b 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java @@ -1,41 +1,27 @@ package de.hysky.skyblocker.skyblock; -import com.google.gson.JsonParser; -import com.mojang.logging.LogUtils; -import com.mojang.serialization.Codec; -import com.mojang.serialization.JsonOps; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.annotations.Init; import de.hysky.skyblocker.skyblock.item.PetInfo; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import de.hysky.skyblocker.utils.profile.ProfiledData; import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; import net.minecraft.item.ItemStack; import net.minecraft.screen.slot.Slot; import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -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.concurrent.CompletableFuture; /** - * Doesn't work with auto pet right now because thats complicated. + * Doesn't work with auto pet right now because that's complicated. *

* Want support? Ask the Admins for a Mod API event or open your pets menu. */ public class PetCache { - private static final Logger LOGGER = LogUtils.getLogger(); private static final Path FILE = SkyblockerMod.CONFIG_DIR.resolve("pet_cache.json"); - private static final Object2ObjectOpenHashMap> CACHED_PETS = new Object2ObjectOpenHashMap<>(); - public static final Codec>> SERIALIZATION_CODEC = Codec.unboundedMap(Codec.STRING, - Codec.unboundedMap(Codec.STRING, PetInfo.CODEC).xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new) - ).xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new); + private static final ProfiledData CACHED_PETS = new ProfiledData<>(FILE, PetInfo.CODEC, true, true); /** * Used in case the server lags to prevent the screen tick check from overwriting the clicked pet logic @@ -44,7 +30,7 @@ public class PetCache { @Init public static void init() { - load(); + CACHED_PETS.load(); ScreenEvents.BEFORE_INIT.register((_client, screen, _scaledWidth, _scaledHeight) -> { if (Utils.isOnSkyblock() && screen instanceof GenericContainerScreen genericContainerScreen) { @@ -70,27 +56,6 @@ public static void init() { }); } - private static void load() { - CompletableFuture.runAsync(() -> { - try (BufferedReader reader = Files.newBufferedReader(FILE)) { - CACHED_PETS.putAll(SERIALIZATION_CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).getOrThrow()); - } catch (NoSuchFileException ignored) { - } catch (Exception e) { - LOGGER.error("[Skyblocker Pet Cache] Failed to load saved pet!", e); - } - }); - } - - private static void save() { - CompletableFuture.runAsync(() -> { - try (BufferedWriter writer = Files.newBufferedWriter(FILE)) { - SkyblockerMod.GSON.toJson(SERIALIZATION_CODEC.encodeStart(JsonOps.INSTANCE, CACHED_PETS).getOrThrow(), writer); - } catch (Exception e) { - LOGGER.error("[Skyblocker Pet Cache] Failed to save pet data to the cache!", e); - } - }); - } - public static void handlePetEquip(Slot slot, int slotId) { //Ignore inventory clicks if (slotId >= 0 && slotId <= 53) { @@ -112,24 +77,19 @@ private static void parsePet(ItemStack stack, boolean clicked) { shouldLook4Pets = false; - Object2ObjectOpenHashMap playerData = CACHED_PETS.computeIfAbsent(Utils.getUndashedUuid(), _uuid -> new Object2ObjectOpenHashMap<>()); - //Handle deselecting pets if (clicked && getCurrentPet() != null && getCurrentPet().uuid().orElse("").equals(petInfo.uuid().orElse(""))) { - playerData.remove(profileId); + CACHED_PETS.remove(); } else { - playerData.put(profileId, petInfo); + CACHED_PETS.put(petInfo); } - save(); + CACHED_PETS.save(); } } @Nullable public static PetInfo getCurrentPet() { - String uuid = Utils.getUndashedUuid(); - String profileId = Utils.getProfileId(); - - return CACHED_PETS.containsKey(uuid) && CACHED_PETS.get(uuid).containsKey(profileId) ? CACHED_PETS.get(uuid).get(profileId) : null; + return CACHED_PETS.get(); } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/PowderMiningTracker.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/PowderMiningTracker.java index 5459d9551c..121422d529 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/PowderMiningTracker.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/PowderMiningTracker.java @@ -1,9 +1,6 @@ package de.hysky.skyblocker.skyblock.dwarven; -import com.google.gson.JsonElement; import com.mojang.serialization.Codec; -import com.mojang.serialization.JsonOps; -import com.mojang.serialization.codecs.UnboundedMapCodec; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.annotations.Init; import de.hysky.skyblocker.config.SkyblockerConfigManager; @@ -16,10 +13,10 @@ import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Location; import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.profile.ProfiledData; import it.unimi.dsi.fastutil.doubles.DoubleBooleanPair; import it.unimi.dsi.fastutil.objects.*; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; -import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.hud.ChatHud; @@ -33,9 +30,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.nio.file.Files; import java.nio.file.Path; import java.text.NumberFormat; +import java.util.Comparator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -47,27 +44,16 @@ public class PowderMiningTracker { private static final Pattern GEMSTONE_SYMBOLS = Pattern.compile("[α☘☠✎✧❁❂❈❤⸕] "); private static final Pattern REWARD_PATTERN = Pattern.compile(" {4}(.*?) ?x?([\\d,]*)"); private static final Codec> REWARDS_CODEC = CodecUtils.object2IntMapCodec(Codec.STRING); - // Doesn't matter if the codec outputs a java map instead of a fastutils map, it's only used in #putAll anyway so the contents are copied over - private static final UnboundedMapCodec> ALL_REWARDS_CODEC = Codec.unboundedMap(Codec.STRING, REWARDS_CODEC); private static final Object2ObjectArrayMap NAME2ID_MAP = new Object2ObjectArrayMap<>(50); // This constructor takes in a comparator that is triggered to decide where to add the element in the tree map // This causes it to be sorted at all times. This is for rendering them in a sort of easy-to-read manner. - private static final Object2IntAVLTreeMap SHOWN_REWARDS = new Object2IntAVLTreeMap<>((o1, o2) -> { - String o1String = o1.getString(); - String o2String = o2.getString(); - int priority1 = comparePriority(o1String); - int priority2 = comparePriority(o2String); - if (priority1 != priority2) return Integer.compare(priority1, priority2); - return o1String.compareTo(o2String); - }); + private static final Object2IntAVLTreeMap SHOWN_REWARDS = new Object2IntAVLTreeMap<>(Comparator.comparingInt(text -> comparePriority(text.getString())).thenComparing(Text::getString)); /** * Holds the total reward maps for all accounts and profiles. {@link #currentProfileRewards} is a subset of this map, updated on profile change. - * - * @implNote This is a map from (account uuid + "+" + profile uuid) to itemId/amount map. */ - private static final Object2ObjectArrayMap> ALL_REWARDS = new Object2ObjectArrayMap<>(); + private static final ProfiledData> ALL_REWARDS = new ProfiledData<>(getRewardFilePath(), REWARDS_CODEC); /** *

@@ -98,8 +84,7 @@ public static void init() { if (isEnabled()) recalculatePrices(); }); - ClientLifecycleEvents.CLIENT_STARTED.register(PowderMiningTracker::loadRewards); - ClientLifecycleEvents.CLIENT_STOPPING.register(PowderMiningTracker::saveRewards); + ALL_REWARDS.init(); SkyblockEvents.PROFILE_CHANGE.register(PowderMiningTracker::onProfileChange); SkyblockEvents.PROFILE_INIT.register(PowderMiningTracker::onProfileInit); @@ -163,7 +148,7 @@ private static void onProfileChange(String prevProfileId, String newProfileId) { private static void onProfileInit(String profileId) { if (!isEnabled()) return; - currentProfileRewards = ALL_REWARDS.computeIfAbsent(getCombinedId(profileId), k -> new Object2IntArrayMap<>()); + currentProfileRewards = ALL_REWARDS.computeIfAbsent(Object2IntArrayMap::new); recalculateAll(); } @@ -247,33 +232,6 @@ public static Object2ObjectMap getName2IdMap() { return Object2ObjectMaps.unmodifiable(NAME2ID_MAP); } - private static void loadRewards(MinecraftClient client) { - if (Files.notExists(getRewardFilePath())) return; - try { - String jsonString = Files.readString(getRewardFilePath()); - JsonElement json = SkyblockerMod.GSON.fromJson(jsonString, JsonElement.class); - ALL_REWARDS.clear(); - ALL_REWARDS.putAll(ALL_REWARDS_CODEC.decode(JsonOps.INSTANCE, json).getOrThrow().getFirst()); - LOGGER.info("Loaded powder mining rewards from file."); - } catch (Exception e) { - LOGGER.error("Failed to load powder mining rewards from file!", e); - } - } - - private static void saveRewards(MinecraftClient client) { - try { - String jsonString = ALL_REWARDS_CODEC.encodeStart(JsonOps.INSTANCE, ALL_REWARDS).getOrThrow().toString(); - if (Files.notExists(getRewardFilePath())) { - Files.createDirectories(getRewardFilePath().getParent()); // Create all parent directories if they don't exist - Files.createFile(getRewardFilePath()); - } - Files.writeString(getRewardFilePath(), jsonString); - LOGGER.info("Saved powder mining rewards to file."); - } catch (Exception e) { - LOGGER.error("Failed to save powder mining rewards to file!", e); - } - } - static { NAME2ID_MAP.put("Gemstone Powder", "GEMSTONE_POWDER"); // Not an actual item, but since we're using IDs for mapping to colored text we need to have this here @@ -347,10 +305,6 @@ private static Path getRewardFilePath() { return SkyblockerMod.CONFIG_DIR.resolve("reward-trackers/powder-mining.json"); } - private static String getCombinedId(String profileUuid) { - return Utils.getUndashedUuid() + "+" + profileUuid; - } - private static void render(DrawContext context, RenderTickCounter tickCounter) { if (Utils.getLocation() != Location.CRYSTAL_HOLLOWS || !isEnabled()) return; int y = MinecraftClient.getInstance().getWindow().getScaledHeight() / 2 - 100; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/end/EndHudWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/end/EndHudWidget.java index a4b183f7ab..ddfc892f05 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/end/EndHudWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/end/EndHudWidget.java @@ -68,10 +68,11 @@ public Set availableLocations() { public void updateContent() { // Zealots if (SkyblockerConfigManager.get().otherLocations.end.zealotKillsEnabled) { + TheEnd.EndStats endStats = TheEnd.PROFILES_STATS.putIfAbsent(TheEnd.EndStats.EMPTY); addComponent(new IcoTextComponent(ENDERMAN_HEAD, Text.literal("Zealots").formatted(Formatting.BOLD))); - addComponent(new PlainTextComponent(Text.translatable("skyblocker.end.hud.zealotsSinceLastEye", TheEnd.zealotsSinceLastEye))); - addComponent(new PlainTextComponent(Text.translatable("skyblocker.end.hud.zealotsTotalKills", TheEnd.zealotsKilled))); - String avg = TheEnd.eyes == 0 ? "???" : DECIMAL_FORMAT.format((float) TheEnd.zealotsKilled / TheEnd.eyes); + addComponent(new PlainTextComponent(Text.translatable("skyblocker.end.hud.zealotsSinceLastEye", endStats.zealotsSinceLastEye()))); + addComponent(new PlainTextComponent(Text.translatable("skyblocker.end.hud.zealotsTotalKills", endStats.totalZealotKills()))); + String avg = endStats.eyes() == 0 ? "???" : DECIMAL_FORMAT.format((float) endStats.totalZealotKills() / endStats.eyes()); addComponent(new PlainTextComponent(Text.translatable("skyblocker.end.hud.avgKillsPerEye", avg))); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/end/TheEnd.java b/src/main/java/de/hysky/skyblocker/skyblock/end/TheEnd.java index bbbc29e47b..a87ac6d91a 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/end/TheEnd.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/end/TheEnd.java @@ -1,16 +1,16 @@ package de.hysky.skyblocker.skyblock.end; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.annotations.Init; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.events.SkyblockEvents; import de.hysky.skyblocker.utils.ColorUtils; import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.profile.ProfiledData; import de.hysky.skyblocker.utils.waypoint.Waypoint; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientChunkEvents; -import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; @@ -30,29 +30,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -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.*; -import java.util.concurrent.CompletableFuture; public class TheEnd { protected static final Logger LOGGER = LoggerFactory.getLogger(TheEnd.class); + private static final Path FILE = SkyblockerMod.CONFIG_DIR.resolve("end.json"); public static Set hitZealots = new HashSet<>(); - public static int zealotsSinceLastEye = 0; - public static int zealotsKilled = 0; - public static int eyes = 0; - /** - * needs to be saved? - */ - private static boolean dirty = false; - private static String currentProfile = ""; - private static JsonObject PROFILES_STATS; + public static ProfiledData PROFILES_STATS = new ProfiledData<>(FILE, EndStats.CODEC); - private static final Path FILE = SkyblockerMod.CONFIG_DIR.resolve("end.json"); public static List protectorLocations = List.of( new ProtectorLocation(-649, -219, Text.translatable("skyblocker.end.hud.protectorLocations.left")), new ProtectorLocation(-644, -269, Text.translatable("skyblocker.end.hud.protectorLocations.front")), @@ -86,20 +73,12 @@ public static void init() { if (isProtectorHere(world, protectorLocation)) break; } } - if (currentProfile.isEmpty()) load(); // Wacky fix for when you join skyblock, and you are directly in the end (profile id isn't parsed yet most of the time) } - - }); + // Fix for when you join skyblock, and you are directly in the end + SkyblockEvents.PROFILE_CHANGE.register((prev, profile) -> EndHudWidget.getInstance().update()); // Reset when changing island - SkyblockEvents.LOCATION_CHANGE.register(location -> { - resetLocation(); - save(); - load(); - EndHudWidget.getInstance().update(); - }); - // Save when leaving as well - ClientLifecycleEvents.CLIENT_STOPPING.register((client) -> save()); + SkyblockEvents.LOCATION_CHANGE.register(location -> resetLocation()); ClientReceiveMessageEvents.GAME.register((message, overlay) -> { if (!Utils.isInTheEnd() || overlay) return; @@ -115,7 +94,7 @@ public static void init() { }); WorldRenderEvents.AFTER_TRANSLUCENT.register(TheEnd::renderWaypoint); - ClientLifecycleEvents.CLIENT_STARTED.register((client -> loadFile())); + PROFILES_STATS.init(); } private static void checkAllProtectorLocations() { @@ -153,15 +132,13 @@ private static void resetLocation() { public static void onEntityDeath(Entity entity) { if (!(entity instanceof EndermanEntity enderman) || !isZealot(enderman)) return; if (hitZealots.contains(enderman.getUuid())) { - //MinecraftClient.getInstance().player.sendMessage(Text.literal("You killed a zealot!!!")); + EndStats stats = PROFILES_STATS.putIfAbsent(EndStats.EMPTY); if (isSpecialZealot(enderman)) { - zealotsSinceLastEye = 0; - eyes++; - } - else zealotsSinceLastEye++; - zealotsKilled++; - dirty = true; - hitZealots.remove(enderman.getUuid()); + PROFILES_STATS.put(new EndStats(stats.totalZealotKills() + 1, 0, stats.eyes() + 1)); + } else { + PROFILES_STATS.put(new EndStats(stats.totalZealotKills() + 1, stats.zealotsSinceLastEye() + 1, stats.eyes())); + } + hitZealots.remove(enderman.getUuid()); EndHudWidget.getInstance().update(); } } @@ -183,82 +160,21 @@ public static boolean isSpecialZealot(EndermanEntity enderman) { return isZealot(enderman) && enderman.getCarriedBlock() != null && enderman.getCarriedBlock().isOf(Blocks.END_PORTAL_FRAME); } - /** - * Loads if needed - */ - public static void load() { - if (!Utils.isOnSkyblock() || Utils.getProfileId().isEmpty()) return; - String id = MinecraftClient.getInstance().getSession().getUuidOrNull().toString().replaceAll("-", ""); - String profile = Utils.getProfileId(); - if (!profile.equals(currentProfile) && PROFILES_STATS != null) { - currentProfile = profile; - JsonElement jsonElement = PROFILES_STATS.get(id); - if (jsonElement == null) return; - JsonElement jsonElement1 = jsonElement.getAsJsonObject().get(profile); - if (jsonElement1 == null) return; - zealotsKilled = jsonElement1.getAsJsonObject().get("totalZealotKills").getAsInt(); - zealotsSinceLastEye = jsonElement1.getAsJsonObject().get("zealotsSinceLastEye").getAsInt(); - eyes = jsonElement1.getAsJsonObject().get("eyes").getAsInt(); - EndHudWidget.getInstance().update(); - } - } - - private static void loadFile() { - CompletableFuture.runAsync(() -> { - try (BufferedReader reader = Files.newBufferedReader(FILE)) { - PROFILES_STATS = SkyblockerMod.GSON.fromJson(reader, JsonObject.class); - LOGGER.debug("[Skyblocker End] Loaded end stats"); - } catch (NoSuchFileException ignored) { - PROFILES_STATS = new JsonObject(); - } catch (Exception e) { - LOGGER.error("[Skyblocker End] Failed to load end stats", e); - } - }); - } - - /** - * Saves if dirty - */ - public static void save() { - if (dirty && PROFILES_STATS != null) { - String uuid = MinecraftClient.getInstance().getSession().getUuidOrNull().toString().replaceAll("-", ""); - JsonObject jsonObject = PROFILES_STATS.getAsJsonObject(uuid); - if (jsonObject == null) { - PROFILES_STATS.add(uuid, new JsonObject()); - jsonObject = PROFILES_STATS.getAsJsonObject(uuid); - } - - jsonObject.add(currentProfile, new JsonObject()); - JsonElement jsonElement1 = jsonObject.get(currentProfile); - - jsonElement1.getAsJsonObject().addProperty("totalZealotKills", zealotsKilled); - jsonElement1.getAsJsonObject().addProperty("zealotsSinceLastEye", zealotsSinceLastEye); - jsonElement1.getAsJsonObject().addProperty("eyes", eyes); - - if (Utils.isOnSkyblock()) { - CompletableFuture.runAsync(TheEnd::performSave); - } else { - performSave(); - } - } - } - - private static void performSave() { - try (BufferedWriter writer = Files.newBufferedWriter(FILE)) { - SkyblockerMod.GSON.toJson(PROFILES_STATS, writer); - LOGGER.info("[Skyblocker End] Saved end stats"); - dirty = false; - } catch (Exception e) { - LOGGER.error("[Skyblocker End] Failed to save end stats", e); - } - } - - private static void renderWaypoint(WorldRenderContext context) { + private static void renderWaypoint(WorldRenderContext context) { if (!SkyblockerConfigManager.get().otherLocations.end.waypoint) return; if (currentProtectorLocation == null || stage != 5) return; currentProtectorLocation.waypoint().render(context); } + public record EndStats(int totalZealotKills, int zealotsSinceLastEye, int eyes) { + private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.INT.fieldOf("totalZealotKills").forGetter(EndStats::totalZealotKills), + Codec.INT.fieldOf("zealotsSinceLastEye").forGetter(EndStats::zealotsSinceLastEye), + Codec.INT.fieldOf("eyes").forGetter(EndStats::eyes) + ).apply(instance, EndStats::new)); + public static final EndStats EMPTY = new EndStats(0, 0, 0); + } + public record ProtectorLocation(int x, int z, Text name, Waypoint waypoint) { public ProtectorLocation(int x, int z, Text name) { this(x, z, name, new Waypoint(new BlockPos(x, 0, z), Waypoint.Type.WAYPOINT, ColorUtils.getFloatComponents(DyeColor.MAGENTA))); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java index cf0356bcf0..f9dd1705f3 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java @@ -58,7 +58,7 @@ public static void init() { public static void updateCooldown() { PetInfo pet = PetCache.getCurrentPet(); - if (pet != null && pet.tier().equals("LEGENDARY")) { + if (pet != null && pet.tier().equals(SkyblockItemRarity.LEGENDARY)) { monkeyExp = pet.exp(); monkeyLevel = 0; @@ -172,4 +172,4 @@ public float getRemainingCooldownPercent() { return this.isOnCooldown() ? (float) this.getRemainingCooldown() / cooldown : 0.0f; } } -} \ No newline at end of file +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java b/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java index 7adda9577d..84ceb82c11 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java @@ -6,22 +6,22 @@ import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; 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.annotations.Init; +import de.hysky.skyblocker.events.SkyblockEvents; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.Http; import de.hysky.skyblocker.utils.Http.ApiResponse; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import de.hysky.skyblocker.utils.profile.ProfiledData; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; -import net.minecraft.client.MinecraftClient; import net.minecraft.command.CommandRegistryAccess; import net.minecraft.item.ItemStack; import net.minecraft.nbt.*; @@ -31,15 +31,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.Base64; import java.util.Map; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; @@ -48,7 +44,7 @@ public class MuseumItemCache { private static final Logger LOGGER = LoggerFactory.getLogger(MuseumItemCache.class); private static final Path CACHE_FILE = SkyblockerMod.CONFIG_DIR.resolve("museum_item_cache.json"); - private static final Map> MUSEUM_ITEM_CACHE = new Object2ObjectOpenHashMap<>(); + private static final ProfiledData MUSEUM_ITEM_CACHE = new ProfiledData<>(CACHE_FILE, ProfileMuseumData.CODEC, true, true); private static final String ERROR_LOG_TEMPLATE = "[Skyblocker] Failed to refresh museum item data for profile {}"; public static final String DONATION_CONFIRMATION_SCREEN_TITLE = "Confirm Donation"; private static final int CONFIRM_DONATION_BUTTON_SLOT = 20; @@ -57,8 +53,9 @@ public class MuseumItemCache { @Init public static void init() { - ClientLifecycleEvents.CLIENT_STARTED.register(MuseumItemCache::load); + ClientLifecycleEvents.CLIENT_STARTED.register(client -> loaded = MUSEUM_ITEM_CACHE.load()); ClientCommandRegistrationCallback.EVENT.register(MuseumItemCache::registerCommands); + SkyblockEvents.PROFILE_CHANGE.register((prev, profile) -> tick()); } private static void registerCommands(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess) { @@ -76,30 +73,6 @@ private static void registerCommands(CommandDispatcher { - try (BufferedReader reader = Files.newBufferedReader(CACHE_FILE)) { - Map> cachedData = ProfileMuseumData.SERIALIZATION_CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).getOrThrow(); - - MUSEUM_ITEM_CACHE.putAll(cachedData); - LOGGER.info("[Skyblocker] Loaded museum items cache"); - } catch (NoSuchFileException ignored) { - } catch (IOException e) { - LOGGER.error("[Skyblocker] Failed to load cached museum items", e); - } - }); - } - - private static void save() { - CompletableFuture.runAsync(() -> { - try (BufferedWriter writer = Files.newBufferedWriter(CACHE_FILE)) { - SkyblockerMod.GSON.toJson(ProfileMuseumData.SERIALIZATION_CODEC.encodeStart(JsonOps.INSTANCE, MUSEUM_ITEM_CACHE).getOrThrow(), writer); - } catch (IOException e) { - LOGGER.error("[Skyblocker] Failed to save cached museum items!", e); - } - }); - } - public static void handleClick(Slot slot, int slotId, DefaultedList slots) { if (slotId == CONFIRM_DONATION_BUTTON_SLOT) { //Slots 0 to 17 can have items, well not all but thats the general range @@ -111,24 +84,19 @@ public static void handleClick(Slot slot, int slotId, DefaultedList slots) String profileId = Utils.getProfileId(); if (!itemId.isEmpty() && !profileId.isEmpty()) { - String uuid = Utils.getUndashedUuid(); - //Be safe about access to avoid NPEs - Map playerData = MUSEUM_ITEM_CACHE.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()); - playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY.get()); - - playerData.get(profileId).collectedItemIds().add(itemId); - save(); + MUSEUM_ITEM_CACHE.putIfAbsent(ProfileMuseumData.EMPTY.get()).collectedItemIds().add(itemId); + MUSEUM_ITEM_CACHE.save(); } } } } } - private static void updateData4ProfileMember(String uuid, String profileId) { + private static void updateData4ProfileMember(UUID uuid, String profileId) { updateData4ProfileMember(uuid, profileId, null); } - private static void updateData4ProfileMember(String uuid, String profileId, FabricClientCommandSource source) { + private static void updateData4ProfileMember(UUID uuid, String profileId, FabricClientCommandSource source) { CompletableFuture.runAsync(() -> { try (ApiResponse response = Http.sendHypixelRequest("skyblock/museum", "?profile=" + profileId)) { //The request was successful @@ -136,8 +104,9 @@ private static void updateData4ProfileMember(String uuid, String profileId, Fabr JsonObject profileData = JsonParser.parseString(response.content()).getAsJsonObject(); JsonObject members = profileData.getAsJsonObject("members"); - if (members.has(uuid)) { - JsonObject memberData = members.get(uuid).getAsJsonObject(); + String uuidString = UndashedUuid.toString(uuid); + if (members.has(uuidString)) { + JsonObject memberData = members.get(uuidString).getAsJsonObject(); //We call them sets because it could either be a singular item or an entire armour set Map donatedSets = memberData.get("items").getAsJsonObject().asMap(); @@ -161,8 +130,8 @@ private static void updateData4ProfileMember(String uuid, String profileId, Fabr } } - MUSEUM_ITEM_CACHE.get(uuid).put(profileId, new ProfileMuseumData(System.currentTimeMillis(), itemIds)); - save(); + MUSEUM_ITEM_CACHE.put(uuid, profileId, new ProfileMuseumData(System.currentTimeMillis(), itemIds)); + MUSEUM_ITEM_CACHE.save(); if (source != null) source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.museum.resyncSuccess"))); @@ -194,21 +163,18 @@ private static void updateData4ProfileMember(String uuid, String profileId, Fabr }); } - private static void putEmpty(String uuid, String profileId) { + private static void putEmpty(UUID uuid, String profileId) { //Only put new data if they didn't have any before - if (!MUSEUM_ITEM_CACHE.get(uuid).containsKey(profileId)) { - MUSEUM_ITEM_CACHE.get(uuid).put(profileId, new ProfileMuseumData(System.currentTimeMillis(), ObjectOpenHashSet.of())); - } - - save(); + MUSEUM_ITEM_CACHE.computeIfAbsent(uuid, profileId, () -> new ProfileMuseumData(System.currentTimeMillis(), ObjectOpenHashSet.of())); + MUSEUM_ITEM_CACHE.save(); } private static boolean tryResync(FabricClientCommandSource source) { - String uuid = Utils.getUndashedUuid(); + UUID uuid = Utils.getUuid(); String profileId = Utils.getProfileId(); //Only allow resyncing if the data is actually present yet, otherwise the player needs to swap servers for the tick method to be called - if (loaded.isDone() && !profileId.isEmpty() && MUSEUM_ITEM_CACHE.containsKey(uuid) && MUSEUM_ITEM_CACHE.get(uuid).containsKey(profileId) && MUSEUM_ITEM_CACHE.get(uuid).get(profileId).canResync()) { + if (loaded.isDone() && !profileId.isEmpty() && MUSEUM_ITEM_CACHE.containsKey() && MUSEUM_ITEM_CACHE.get().canResync()) { updateData4ProfileMember(uuid, profileId, source); return true; @@ -220,22 +186,18 @@ private static boolean tryResync(FabricClientCommandSource source) { /** * The cache is ticked upon switching Skyblock servers. Only loads from the API if the profile wasn't cached yet. */ - public static void tick(String profileId) { - String uuid = Utils.getUndashedUuid(); + public static void tick() { + UUID uuid = Utils.getUuid(); - if (loaded.isDone() && (!MUSEUM_ITEM_CACHE.containsKey(uuid) || !MUSEUM_ITEM_CACHE.getOrDefault(uuid, new Object2ObjectOpenHashMap<>()).containsKey(profileId))) { - Map playerData = MUSEUM_ITEM_CACHE.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()); - playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY.get()); + if (loaded.isDone() && !MUSEUM_ITEM_CACHE.containsKey()) { + MUSEUM_ITEM_CACHE.putIfAbsent(ProfileMuseumData.EMPTY.get()); - updateData4ProfileMember(uuid, profileId); + updateData4ProfileMember(uuid, Utils.getProfileId()); } } public static boolean hasItemInMuseum(String id) { - String uuid = Utils.getUndashedUuid(); - ObjectOpenHashSet collectedItemIds = (!MUSEUM_ITEM_CACHE.containsKey(uuid) || Utils.getProfileId().isBlank() || !MUSEUM_ITEM_CACHE.get(uuid).containsKey(Utils.getProfileId())) ? null : MUSEUM_ITEM_CACHE.get(uuid).get(Utils.getProfileId()).collectedItemIds(); - - return collectedItemIds != null && collectedItemIds.contains(id); + return MUSEUM_ITEM_CACHE.containsKey() && MUSEUM_ITEM_CACHE.get().collectedItemIds().contains(id); } private record ProfileMuseumData(long lastResync, ObjectOpenHashSet collectedItemIds) { @@ -246,12 +208,8 @@ private record ProfileMuseumData(long lastResync, ObjectOpenHashSet coll Codec.STRING.listOf() .xmap(ObjectOpenHashSet::new, ObjectArrayList::new) .fieldOf("collectedItemIds") - .forGetter(i -> new ObjectOpenHashSet<>(i.collectedItemIds())) + .forGetter(ProfileMuseumData::collectedItemIds) ).apply(instance, ProfileMuseumData::new)); - //Mojang's internal Codec implementation uses ImmutableMaps so we'll just xmap those away and type safety while we're at it :') - private static final Codec>> SERIALIZATION_CODEC = Codec.unboundedMap(Codec.STRING, Codec.unboundedMap(Codec.STRING, CODEC) - .xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new) - ).xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new); private boolean canResync() { return this.lastResync + TIME_BETWEEN_RESYNCING_ALLOWED < System.currentTimeMillis(); 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 index f10a0b2f46..d32b315169 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java @@ -1,33 +1,23 @@ package de.hysky.skyblocker.skyblock.item.tooltip; -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.skyblock.item.tooltip.info.TooltipInfoType; import de.hysky.skyblocker.annotations.Init; +import de.hysky.skyblocker.skyblock.item.tooltip.info.TooltipInfoType; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.profile.ProfiledData; 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; -import org.slf4j.Logger; -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.List; import java.util.Map; @@ -41,11 +31,10 @@ import java.util.stream.Collectors; 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 Object2ObjectOpenHashMap> COLLECTED_ACCESSORIES = new Object2ObjectOpenHashMap<>(); + private static final ProfiledData COLLECTED_ACCESSORIES = new ProfiledData<>(FILE, ProfileAccessoryData.CODEC, true); 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; @@ -56,8 +45,7 @@ public class AccessoriesHelper { @Init public static void init() { - ClientLifecycleEvents.CLIENT_STARTED.register((_client) -> load()); - ClientLifecycleEvents.CLIENT_STOPPING.register((_client) -> save()); + loaded = COLLECTED_ACCESSORIES.init(); 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()); @@ -74,26 +62,6 @@ public static void init() { }); } - //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)).getOrThrow()); - } 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).getOrThrow(), writer); - } catch (Exception e) { - LOGGER.error("[Skyblocker Accessory Helper] Failed to save accessory file!", e); - } - } - private static void collectAccessories(List slots, int page) { //Is this even needed? if (!loaded.isDone()) return; @@ -104,9 +72,7 @@ private static void collectAccessories(List slots, int page) { .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() + COLLECTED_ACCESSORIES.computeIfAbsent(ProfileAccessoryData::createDefault).pages() .put(page, new ObjectOpenHashSet<>(accessoryIds)); } @@ -114,8 +80,7 @@ public static Pair calculateReport4Accessory(String acc 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 collectedAccessories = COLLECTED_ACCESSORIES.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()).computeIfAbsent(Utils.getProfileId(), profileId -> ProfileAccessoryData.createDefault()).pages().values().stream() + Set collectedAccessories = COLLECTED_ACCESSORIES.computeIfAbsent(ProfileAccessoryData::createDefault).pages().values().stream() .flatMap(ObjectOpenHashSet::stream) .filter(ACCESSORY_DATA::containsKey) .map(ACCESSORY_DATA::get) @@ -176,9 +141,6 @@ private record ProfileAccessoryData(Int2ObjectOpenHashMap>> 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<>()); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/slayers/SlayerTimer.java b/src/main/java/de/hysky/skyblocker/skyblock/slayers/SlayerTimer.java index 2f90ec22e3..20701edd65 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/slayers/SlayerTimer.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/slayers/SlayerTimer.java @@ -1,60 +1,29 @@ package de.hysky.skyblocker.skyblock.slayers; -import com.google.gson.JsonParser; 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.annotations.Init; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.utils.Constants; -import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.profile.ProfiledData; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.minecraft.client.MinecraftClient; 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.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.time.Duration; import java.time.Instant; -import java.util.concurrent.CompletableFuture; import java.util.function.Function; public class SlayerTimer { - private static final Logger LOGGER = LoggerFactory.getLogger(SlayerTimer.class); - private static final Path FILE = SkyblockerMod.CONFIG_DIR.resolve("SlayerPb.json"); - private static final Object2ObjectOpenHashMap>> CACHED_SLAYER_STATS = new Object2ObjectOpenHashMap<>(); + private static final Path FILE = SkyblockerMod.CONFIG_DIR.resolve("slayer_personal_best.json"); + private static final ProfiledData>> CACHED_SLAYER_STATS = new ProfiledData<>(FILE, SlayerInfo.SERIALIZATION_CODEC, true, true); @Init public static void init() { - load(); - } - - private static void load() { - CompletableFuture.runAsync(() -> { - try (BufferedReader reader = Files.newBufferedReader(FILE)) { - CACHED_SLAYER_STATS.putAll(SlayerInfo.SERIALIZATION_CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).getOrThrow()); - } catch (NoSuchFileException ignored) { - } catch (Exception e) { - LOGGER.error("[Skyblocker Slayer Cache] Failed to load saved slayer data!", e); - } - }); - } - - private static void save() { - CompletableFuture.runAsync(() -> { - try (BufferedWriter writer = Files.newBufferedWriter(FILE)) { - SkyblockerMod.GSON.toJson(SlayerInfo.SERIALIZATION_CODEC.encodeStart(JsonOps.INSTANCE, CACHED_SLAYER_STATS).getOrThrow(), writer); - } catch (Exception e) { - LOGGER.error("[Skyblocker Slayer Cache] Failed to save slayer data to cache!", e); - } - }); + CACHED_SLAYER_STATS.load(); } public static void onBossDeath(Instant startTime) { @@ -78,8 +47,7 @@ public static void onBossDeath(Instant startTime) { } private static long getPersonalBest(SlayerType slayerType, SlayerTier slayerTier) { - String profileId = Utils.getProfileId(); - Object2ObjectOpenHashMap> profileData = CACHED_SLAYER_STATS.computeIfAbsent(profileId, _uuid -> new Object2ObjectOpenHashMap<>()); + Object2ObjectOpenHashMap> profileData = CACHED_SLAYER_STATS.computeIfAbsent(Object2ObjectOpenHashMap::new); Object2ObjectOpenHashMap typeData = profileData.computeIfAbsent(slayerType, _type -> new Object2ObjectOpenHashMap<>()); SlayerInfo currentBest = typeData.get(slayerTier); @@ -87,15 +55,14 @@ private static long getPersonalBest(SlayerType slayerType, SlayerTier slayerTier } private static void updateBestTime(SlayerType slayerType, SlayerTier slayerTier, long timeElapsed) { - String profileId = Utils.getProfileId(); long nowMillis = System.currentTimeMillis(); - Object2ObjectOpenHashMap> profileData = CACHED_SLAYER_STATS.computeIfAbsent(profileId, _uuid -> new Object2ObjectOpenHashMap<>()); + Object2ObjectOpenHashMap> profileData = CACHED_SLAYER_STATS.computeIfAbsent(Object2ObjectOpenHashMap::new); Object2ObjectOpenHashMap typeData = profileData.computeIfAbsent(slayerType, _type -> new Object2ObjectOpenHashMap<>()); SlayerInfo newInfo = new SlayerInfo(timeElapsed, nowMillis); typeData.put(slayerTier, newInfo); - save(); + CACHED_SLAYER_STATS.save(); } private static String formatTime(long millis) { @@ -108,10 +75,8 @@ public record SlayerInfo(long bestTimeMillis, long dateMillis) { Codec.LONG.fieldOf("dateMillis").forGetter(SlayerInfo::dateMillis) ).apply(instance, SlayerInfo::new)); - private static final Codec>>> SERIALIZATION_CODEC = Codec.unboundedMap(Codec.STRING, - Codec.unboundedMap(SlayerType.CODEC, - Codec.unboundedMap(SlayerTier.CODEC, CODEC).xmap(Object2ObjectOpenHashMap::new, Function.identity()) - ).xmap(Object2ObjectOpenHashMap::new, Function.identity()) + private static final Codec>> SERIALIZATION_CODEC = Codec.unboundedMap(SlayerType.CODEC, + Codec.unboundedMap(SlayerTier.CODEC, CODEC).xmap(Object2ObjectOpenHashMap::new, Function.identity()) ).xmap(Object2ObjectOpenHashMap::new, Function.identity()); } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ComponentBasedWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ComponentBasedWidget.java index d5eb512bb0..bfa6356791 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ComponentBasedWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ComponentBasedWidget.java @@ -71,7 +71,7 @@ public final void update() { try { this.updateContent(); } catch (Exception e) { - if (!e.getMessage().equals(lastError)) { + if (e.getMessage() == null || !e.getMessage().equals(lastError)) { lastError = e.getMessage(); LOGGER.error("Failed to update contents of {}", this, e); } diff --git a/src/main/java/de/hysky/skyblocker/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java index ac557e80a5..1d87fe1170 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Utils.java +++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java @@ -39,6 +39,7 @@ import java.time.Instant; import java.util.Collections; +import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -519,8 +520,6 @@ public static boolean onChatMessage(Text text, boolean overlay) { SkyblockEvents.PROFILE_INIT.invoker().onSkyblockProfileInit(profileId); firstProfileUpdate = false; } - - MuseumItemCache.tick(profileId); } } @@ -540,7 +539,11 @@ public static void sendMessageToBypassEvents(Text message) { client.getNarratorManager().narrateSystemMessage(message); } + public static UUID getUuid() { + return MinecraftClient.getInstance().getSession().getUuidOrNull(); + } + public static String getUndashedUuid() { - return UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull()); + return UndashedUuid.toString(getUuid()); } } diff --git a/src/main/java/de/hysky/skyblocker/utils/profile/ProfiledData.java b/src/main/java/de/hysky/skyblocker/utils/profile/ProfiledData.java new file mode 100644 index 0000000000..f7afc2f122 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/profile/ProfiledData.java @@ -0,0 +1,169 @@ +package de.hysky.skyblocker.utils.profile; + +import com.google.gson.JsonObject; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.Utils; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.minecraft.util.StringIdentifiable; +import net.minecraft.util.Uuids; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.function.Supplier; + +public class ProfiledData { + private static final Logger LOGGER = LoggerFactory.getLogger(ProfiledData.class); + private final Path file; + private final Codec>> codec; + private final boolean compressed; + private final boolean loadAsync; + private final boolean saveAsync; + private Object2ObjectOpenHashMap> data = new Object2ObjectOpenHashMap<>(); + + public ProfiledData(Path file, Codec codec) { + this(file, codec, false); + } + + public ProfiledData(Path file, Codec codec, boolean compressed) { + this(file, codec, compressed, true, false); + } + + public ProfiledData(Path file, Codec codec, boolean loadAsync, boolean saveAsync) { + this(file, codec, false, loadAsync, saveAsync); + } + + /** + * @param compressed Whether the {@link JsonOps#COMPRESSED} should be used. + * When compressed, {@link net.minecraft.util.StringIdentifiable#createCodec(Supplier)} will use the ordinals instead of {@link StringIdentifiable#asString()}. + * When compressed, codecs built with {@link com.mojang.serialization.codecs.RecordCodecBuilder} will be serialized as a list instead of a map. + * {@link JsonOps#COMPRESSED} is required for maps with non-string keys. + * @param loadAsync Whether the data should be loaded asynchronously. Default true. + * @param saveAsync Whether the data should be saved asynchronously. Default false. + * Do not save async if saving is done with {@link ClientLifecycleEvents#CLIENT_STOPPING}. + */ + public ProfiledData(Path file, Codec codec, boolean compressed, boolean loadAsync, boolean saveAsync) { + this.file = file; + // Mojang's internal Codec implementation uses ImmutableMaps so we'll just xmap those away and type safety while we're at it :') + this.codec = Codec.unboundedMap(Uuids.CODEC, Codec.unboundedMap(Codec.STRING, codec) + .xmap(Object2ObjectOpenHashMap::new, Function.identity()) + ).xmap(Object2ObjectOpenHashMap::new, Function.identity()); + this.compressed = compressed; + this.loadAsync = loadAsync; + this.saveAsync = saveAsync; + } + + public CompletableFuture init() { + ClientLifecycleEvents.CLIENT_STOPPING.register(client -> save()); + return load(); + } + + public CompletableFuture load() { + if (loadAsync) { + return CompletableFuture.runAsync(this::loadInternal); + } else { + loadInternal(); + return CompletableFuture.completedFuture(null); + } + } + + // Note: JsonOps.COMPRESSED must be used if you're using maps with non-string keys + private void loadInternal() { + try (BufferedReader reader = Files.newBufferedReader(file)) { + // Atomic operation to prevent concurrent modification + data = codec.parse(compressed ? JsonOps.COMPRESSED : JsonOps.INSTANCE, SkyblockerMod.GSON.fromJson(reader, JsonObject.class)).getOrThrow(); + } catch (NoSuchFileException ignored) { + } catch (Exception e) { + LOGGER.error("[Skyblocker Profiled Data] Failed to load data from file: {}", file, e); + } + } + + public CompletableFuture save() { + if (saveAsync) { + return CompletableFuture.runAsync(this::saveInternal); + } else { + saveInternal(); + return CompletableFuture.completedFuture(null); + } + } + + private void saveInternal() { + try { + Files.createDirectories(file.getParent()); + } catch (Exception e) { + LOGGER.error("[Skyblocker Profiled Data] Failed to create directories for file: {}", file, e); + } + + try (BufferedWriter writer = Files.newBufferedWriter(file)) { + SkyblockerMod.GSON.toJson(codec.encodeStart(compressed ? JsonOps.COMPRESSED : JsonOps.INSTANCE, data).getOrThrow(), writer); + } catch (Exception e) { + LOGGER.error("[Skyblocker Profiled Data] Failed to save data to file: {}", file, e); + } + } + + public boolean containsKey() { + return containsKey(Utils.getUuid(), Utils.getProfileId()); + } + + public boolean containsKey(UUID uuid, String profileId) { + return getPlayerData(uuid).containsKey(profileId); + } + + @Nullable + public T get() { + return get(Utils.getUuid(), Utils.getProfileId()); + } + + @Nullable + public T get(UUID uuid, String profileId) { + return getPlayerData(uuid).get(profileId); + } + + public T put(T value) { + return put(Utils.getUuid(), Utils.getProfileId(), value); + } + + public T put(UUID uuid, String profileId, T value) { + return getPlayerData(uuid).put(profileId, value); + } + + public T putIfAbsent(T value) { + return putIfAbsent(Utils.getUuid(), Utils.getProfileId(), value); + } + + public T putIfAbsent(UUID uuid, String profileId, T value) { + return getPlayerData(uuid).putIfAbsent(profileId, value); + } + + public T computeIfAbsent(Supplier valueSupplier) { + return computeIfAbsent(Utils.getUuid(), Utils.getProfileId(), valueSupplier); + } + + public T computeIfAbsent(UUID uuid, String profileId, Supplier valueSupplier) { + return getPlayerData(uuid).computeIfAbsent(profileId, _profileId -> valueSupplier.get()); + } + + public T remove() { + return remove(Utils.getUuid(), Utils.getProfileId()); + } + + public T remove(UUID uuid, String profileId) { + return getPlayerData(uuid).remove(profileId); + } + + private Map getPlayerData(UUID uuid) { + return data.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()); + } +}