diff options
author | Aaron <51387595+AzureAaron@users.noreply.github.com> | 2023-10-27 23:53:11 -0400 |
---|---|---|
committer | Aaron <51387595+AzureAaron@users.noreply.github.com> | 2023-10-27 23:53:11 -0400 |
commit | 2903241fe145137f6c1c925ed508c5673228a8d7 (patch) | |
tree | fd36976443c9cb3116de0746423ee2e2596e28d0 | |
parent | 31cfd39b39915bea976f6179ee75edec437c9e2e (diff) | |
download | Skyblocker-2903241fe145137f6c1c925ed508c5673228a8d7.tar.gz Skyblocker-2903241fe145137f6c1c925ed508c5673228a8d7.tar.bz2 Skyblocker-2903241fe145137f6c1c925ed508c5673228a8d7.zip |
Museum Item Cache
4 files changed, 156 insertions, 2 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index dc8896cf..5e23e2e2 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -106,6 +106,7 @@ public class SkyblockerMod implements ClientModInitializer { ItemProtection.init(); CreeperBeams.init(); ItemRarityBackgrounds.init(); + MuseumItemCache.init(); containerSolverManager.init(); statusBarTracker.init(); Scheduler.INSTANCE.scheduleCyclic(Utils::update, 20); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java b/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java new file mode 100644 index 00000000..cb11d702 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java @@ -0,0 +1,146 @@ +package de.hysky.skyblocker.skyblock.item; + +import java.io.ByteArrayInputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.lang.reflect.Type; +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.concurrent.CompletableFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.reflect.TypeToken; +import com.mojang.util.UndashedUuid; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.Http; +import de.hysky.skyblocker.utils.Http.ApiResponse; +import de.hysky.skyblocker.utils.Utils; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtList; +import net.minecraft.util.Util; + +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 Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, ProfileMuseumData>> MUSEUM_ITEM_CACHE = new Object2ObjectOpenHashMap<>(); + private static final Type MAP_TYPE = new TypeToken<Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, ProfileMuseumData>>>() {}.getType(); + + private static CompletableFuture<Void> loaded; + + public static void init() { + ClientLifecycleEvents.CLIENT_STARTED.register(MuseumItemCache::load); + } + + private static void load(MinecraftClient client) { + loaded = CompletableFuture.runAsync(() -> { + try (BufferedReader reader = Files.newBufferedReader(CACHE_FILE)) { + Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, ProfileMuseumData>> cachedData = SkyblockerMod.GSON.fromJson(reader, MAP_TYPE); + + 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(MUSEUM_ITEM_CACHE, writer); + } catch (IOException e) { + LOGGER.error("[Skyblocker] Failed to save cached museum items!", e); + } + }); + } + + private static void updateData4ProfileMember(String uuid, String profileId) { + CompletableFuture.runAsync(() -> { + try { + ApiResponse response = Http.sendHypixelRequest("skyblock/museum", "?profile=" + profileId); + + //The request was successful + if (response.ok()) { + JsonObject profileData = JsonParser.parseString(response.content()).getAsJsonObject(); + JsonObject memberData = profileData.get("members").getAsJsonObject().get(uuid).getAsJsonObject(); + + //We call them sets because it could either be a singular item or an entire armour set + Map<String, JsonElement> donatedSets = memberData.get("items").getAsJsonObject().asMap(); + + //Set of all found item ids on profile + ObjectOpenHashSet<String> itemIds = new ObjectOpenHashSet<>(); + + for (Map.Entry<String, JsonElement> donatedSet : donatedSets.entrySet()) { + //Item is plural here because the nbt is a list + String itemsData = donatedSet.getValue().getAsJsonObject().get("items").getAsJsonObject().get("data").getAsString(); + NbtList items = NbtIo.readCompressed(new ByteArrayInputStream(Base64.getDecoder().decode(itemsData))).getList("i", NbtElement.COMPOUND_TYPE); + + for (int i = 0; i < items.size(); i++) { + NbtCompound tag = items.getCompound(i).getCompound("tag"); + + if (tag.contains("ExtraAttributes")) { + NbtCompound extraAttributes = tag.getCompound("ExtraAttributes"); + + if (extraAttributes.contains("id")) itemIds.add(extraAttributes.getString("id")); + } + } + } + + MUSEUM_ITEM_CACHE.get(uuid).put(profileId, new ProfileMuseumData(System.currentTimeMillis(), itemIds)); + save(); + + LOGGER.info("[Skyblocker] Successfully updated museum item cache for profile {}", profileId); + } + } catch (Exception e) { + LOGGER.error("[Skyblocker] Failed to refresh museum item data for profile {}", profileId, e); + } + }); + } + + /** + * The cache is ticked upon switching skyblock servers + */ + public static void tick(String profileId) { + if (loaded.isDone()) { + String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull()); + Object2ObjectOpenHashMap<String, ProfileMuseumData> playerData = MUSEUM_ITEM_CACHE.computeIfAbsent(uuid, uuid1 -> Util.make(new Object2ObjectOpenHashMap<>(), map -> { + map.put(profileId, ProfileMuseumData.EMPTY); + })); + + if (playerData.get(profileId).stale()) updateData4ProfileMember(uuid, profileId); + } + } + + public static boolean hasItemInMuseum(String id) { + String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull()); + ObjectOpenHashSet<String> collectedItemIds = MUSEUM_ITEM_CACHE.get(uuid).get(Utils.getProfileId()).collectedItemIds(); + + return collectedItemIds != null && collectedItemIds.contains(id); + } + + private record ProfileMuseumData(long lastUpdated, ObjectOpenHashSet<String> collectedItemIds) { + private static final ProfileMuseumData EMPTY = new ProfileMuseumData(0L, null); + private static final long MAX_AGE = 86_400_000; + + private boolean stale() { + return System.currentTimeMillis() > lastUpdated + MAX_AGE; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/Http.java b/src/main/java/de/hysky/skyblocker/utils/Http.java index 94506b05..7e3a7a27 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Http.java +++ b/src/main/java/de/hysky/skyblocker/utils/Http.java @@ -51,7 +51,7 @@ public class Http { InputStream decodedInputStream = getDecodedInputStream(response); String body = new String(decodedInputStream.readAllBytes()); - return new ApiResponse(body, getCacheStatus(response.headers())); + return new ApiResponse(body, response.statusCode(), getCacheStatus(response.headers())); } public static HttpHeaders sendHeadRequest(String url) throws IOException, InterruptedException { @@ -120,7 +120,11 @@ public class Http { } //TODO If ever needed, we could just replace cache status with the response headers and go from there - public record ApiResponse(String content, String cacheStatus) { + public record ApiResponse(String content, int statusCode, String cacheStatus) { + + public boolean ok() { + return statusCode == 200; + } public boolean cached() { return cacheStatus.equals("HIT"); diff --git a/src/main/java/de/hysky/skyblocker/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java index 1205c71b..ff406b61 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Utils.java +++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java @@ -3,6 +3,7 @@ package de.hysky.skyblocker.utils; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import de.hysky.skyblocker.events.SkyblockEvents; +import de.hysky.skyblocker.skyblock.item.MuseumItemCache; import de.hysky.skyblocker.skyblock.item.PriceInfoTooltip; import de.hysky.skyblocker.skyblock.rift.TheRift; import de.hysky.skyblocker.utils.scheduler.MessageScheduler; @@ -365,6 +366,8 @@ public class Utils { if (isOnSkyblock && message.startsWith("Profile ID: ")) { profileId = message.replace("Profile ID: ", ""); + + MuseumItemCache.tick(profileId); } return true; |