From cdfcdf9d5e9cbdad30c591d1b58d4259a1fa3a38 Mon Sep 17 00:00:00 2001 From: Aaron <51387595+AzureAaron@users.noreply.github.com> Date: Sun, 16 Jun 2024 16:08:01 -0400 Subject: Track Museum Item Donations --- .../skyblocker/mixins/HandledScreenMixin.java | 29 ++++-- .../skyblocker/skyblock/item/MuseumItemCache.java | 113 +++++++++++++++++++-- .../resources/assets/skyblocker/lang/en_us.json | 5 + 3 files changed, 127 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java index a7685ffc..7fdb5738 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java @@ -11,6 +11,7 @@ import de.hysky.skyblocker.skyblock.experiment.UltrasequencerSolver; import de.hysky.skyblocker.skyblock.garden.VisitorHelper; import de.hysky.skyblocker.skyblock.item.ItemProtection; import de.hysky.skyblocker.skyblock.item.ItemRarityBackgrounds; +import de.hysky.skyblocker.skyblock.item.MuseumItemCache; import de.hysky.skyblocker.skyblock.item.WikiLookup; import de.hysky.skyblocker.skyblock.item.slottext.SlotText; import de.hysky.skyblocker.skyblock.item.slottext.SlotTextManager; @@ -248,17 +249,27 @@ public abstract class HandledScreenMixin extends Screen ci.cancel(); return; } - if (this.handler instanceof GenericContainerScreenHandler genericContainerScreenHandler && genericContainerScreenHandler.getRows() == 6) { - VisitorHelper.onSlotClick(slot, slotId, title, genericContainerScreenHandler.getSlot(13).getStack()); - - // Prevent selling to NPC shops - ItemStack sellStack = this.handler.slots.get(49).getStack(); - if (sellStack.getName().getString().equals("Sell Item") || ItemUtils.getLoreLineIf(sellStack, text -> text.contains("buyback")) != null) { - if (slotId != 49 && ItemProtection.isItemProtected(stack)) { - ci.cancel(); - return; + + switch (this.handler) { + case GenericContainerScreenHandler genericContainerScreenHandler when genericContainerScreenHandler.getRows() == 6 -> { + VisitorHelper.onSlotClick(slot, slotId, title, genericContainerScreenHandler.getSlot(13).getStack()); + + // Prevent selling to NPC shops + ItemStack sellStack = this.handler.slots.get(49).getStack(); + if (sellStack.getName().getString().equals("Sell Item") || ItemUtils.getLoreLineIf(sellStack, text -> text.contains("buyback")) != null) { + if (slotId != 49 && ItemProtection.isItemProtected(stack)) { + ci.cancel(); + return; + } } } + + case GenericContainerScreenHandler genericContainerScreenHandler when title.equals(MuseumItemCache.DONATION_CONFIRMATION_SCREEN_TITLE) -> { + //Museum Item Cache donation tracking + MuseumItemCache.handleClick(slot, slotId, genericContainerScreenHandler.slots); + } + + case null, default -> {} } if (currentSolver != null) { 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 c78724ca..11e8ea9c 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java @@ -1,22 +1,36 @@ package de.hysky.skyblocker.skyblock.item; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; + import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +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.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 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.*; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.collection.DefaultedList; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,11 +50,29 @@ public class MuseumItemCache { 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 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; private static CompletableFuture loaded; public static void init() { ClientLifecycleEvents.CLIENT_STARTED.register(MuseumItemCache::load); + ClientCommandRegistrationCallback.EVENT.register(MuseumItemCache::registerCommands); + } + + private static void registerCommands(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess) { + dispatcher.register(literal(SkyblockerMod.NAMESPACE) + .then(literal("museum") + .then(literal("resync") + .executes(context -> { + if (tryResync(context.getSource())) { + context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.museum.attemptingResync"))); + } else { + context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.museum.cannotResync"))); + } + + return Command.SINGLE_SUCCESS; + })))); } private static void load(MinecraftClient client) { @@ -67,7 +99,35 @@ public class MuseumItemCache { }); } + 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 + for (int i = 0; i < 17; i++) { + ItemStack stack = slots.get(i).getStack(); + + if (!stack.isEmpty()) { + String itemId = ItemUtils.getItemId(stack); + String profileId = Utils.getProfileId(); + + if (!itemId.isEmpty() && !profileId.isEmpty()) { + String uuid = getUndashedUuid(MinecraftClient.getInstance()); + //Be safe about access to avoid NPEs + Map playerData = MUSEUM_ITEM_CACHE.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()); + playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY); + + playerData.get(profileId).collectedItemIds().add(itemId); + save(); + } + } + } + } + } + private static void updateData4ProfileMember(String uuid, String profileId) { + updateData4ProfileMember(uuid, profileId, null); + } + + private static void updateData4ProfileMember(String uuid, String profileId, FabricClientCommandSource source) { CompletableFuture.runAsync(() -> { try (ApiResponse response = Http.sendHypixelRequest("skyblock/museum", "?profile=" + profileId)) { //The request was successful @@ -103,58 +163,89 @@ public class MuseumItemCache { MUSEUM_ITEM_CACHE.get(uuid).put(profileId, new ProfileMuseumData(System.currentTimeMillis(), itemIds)); save(); + if (source != null) source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.museum.resyncSuccess"))); + LOGGER.info("[Skyblocker] Successfully updated museum item cache for profile {}", profileId); } else { //If the player's Museum API is disabled putEmpty(uuid, profileId); + if (source != null) source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.museum.resyncFailure"))); + LOGGER.warn(ERROR_LOG_TEMPLATE + " because the Museum API is disabled!", profileId); } } else { //If the request returns a non 200 status code putEmpty(uuid, profileId); + if (source != null) source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.museum.resyncFailure"))); + LOGGER.error(ERROR_LOG_TEMPLATE + " because a non 200 status code was encountered! Status Code: {}", profileId, response.statusCode()); } } catch (Exception e) { //If an exception was somehow thrown putEmpty(uuid, profileId); + if (source != null) source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.museum.resyncFailure"))); + LOGGER.error(ERROR_LOG_TEMPLATE, profileId, e); } }); } private static void putEmpty(String uuid, String profileId) { - MUSEUM_ITEM_CACHE.get(uuid).put(profileId, new ProfileMuseumData(System.currentTimeMillis(), ObjectOpenHashSet.of())); + //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(); } + private static boolean tryResync(FabricClientCommandSource source) { + String uuid = getUndashedUuid(source.getClient()); + 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()) { + updateData4ProfileMember(uuid, profileId, source); + + return true; + } + + return false; + } + /** - * The cache is ticked upon switching skyblock servers + * 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) { - if (loaded.isDone()) { - String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull()); + String uuid = getUndashedUuid(MinecraftClient.getInstance()); + + 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); - if (playerData.get(profileId).stale()) updateData4ProfileMember(uuid, profileId); + updateData4ProfileMember(uuid, profileId); } } public static boolean hasItemInMuseum(String id) { - String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull()); + String uuid = getUndashedUuid(MinecraftClient.getInstance()); 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); } - private record ProfileMuseumData(long lastUpdated, ObjectOpenHashSet collectedItemIds) { + private static String getUndashedUuid(MinecraftClient client) { + return UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull()); + } + + private record ProfileMuseumData(long lastResync, ObjectOpenHashSet collectedItemIds) { private static final ProfileMuseumData EMPTY = new ProfileMuseumData(0L, null); - private static final long MAX_AGE = 86_400_000; + private static final long TIME_BETWEEN_RESYNCING_ALLOWED = 172_800_000L; private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( - Codec.LONG.fieldOf("lastUpdated").forGetter(ProfileMuseumData::lastUpdated), + Codec.LONG.fieldOf("lastResync").forGetter(ProfileMuseumData::lastResync), Codec.STRING.listOf() .xmap(ObjectOpenHashSet::new, ObjectArrayList::new) .fieldOf("collectedItemIds") @@ -165,8 +256,8 @@ public class MuseumItemCache { .xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new) ).xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new); - private boolean stale() { - return System.currentTimeMillis() > lastUpdated + MAX_AGE; + private boolean canResync() { + return this.lastResync + TIME_BETWEEN_RESYNCING_ALLOWED < System.currentTimeMillis(); } } } \ No newline at end of file diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index d03e2a33..a73d523d 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -661,6 +661,11 @@ "skyblocker.updateRepository.failed": "§cUpdating the local repository failed. See logs for details.", "skyblocker.updateRepository.error": "§cEncountered an error while deleting and updating the local repository. Try again in a moment or remove the files manually and restart the game. See logs for details.", + "skyblocker.museum.attemptingResync": "Attempting to resync your museum item donations...", + "skyblocker.museum.cannotResync": "Cannot resync your museum item donations! Note that you can only do this once every two days.", + "skyblocker.museum.resyncSuccess": "Successfully resynced your museum item donations!", + "skyblocker.museum.resyncFailure": "Failed to resync your museum item donations!", + "skyblocker.dungeons.secrets.physicalEntranceNotFound": "§cDungeon Entrance Room coordinates not found. Please go back to the green Entrance Room.", "skyblocker.dungeons.secrets.markSecretFound": "§rMarked secret #%d as found.", "skyblocker.dungeons.secrets.markSecretMissing": "§rMarked secret #%d as missing.", -- cgit