aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerMod.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java36
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java51
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/PetCache.java149
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java44
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java109
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java5
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java169
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Http.java36
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Utils.java6
12 files changed, 553 insertions, 62 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
index eff88783..d793e73d 100644
--- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
@@ -173,9 +173,10 @@ public class SkyblockerMod implements ClientModInitializer {
VisitorHelper.init();
ItemRarityBackgrounds.init();
MuseumItemCache.init();
+ PetCache.init();
SecretsTracker.init();
+ ApiAuthentication.init();
ApiUtils.init();
- ProfileUtils.init();
Debug.init();
Kuudra.init();
RenderHelper.init();
diff --git a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java
index a7685ffc..f2e3e907 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java
@@ -4,6 +4,7 @@ import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.blaze3d.systems.RenderSystem;
import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.PetCache;
import de.hysky.skyblocker.skyblock.experiment.ChronomatronSolver;
import de.hysky.skyblocker.skyblock.experiment.ExperimentSolver;
import de.hysky.skyblocker.skyblock.experiment.SuperpairsSolver;
@@ -11,6 +12,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;
@@ -36,6 +38,7 @@ import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import org.lwjgl.glfw.GLFW;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@@ -248,17 +251,32 @@ public abstract class HandledScreenMixin<T extends ScreenHandler> 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 -> {}
+ }
+
+ //Pet Caching
+ if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT && title.startsWith("Pets")) {
+ PetCache.handlePetEquip(slot, slotId);
}
if (currentSolver != null) {
diff --git a/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java
index 2154c4b5..3abbfbcd 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java
@@ -1,10 +1,12 @@
package de.hysky.skyblocker.mixins;
-import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
-import de.hysky.skyblocker.SkyblockerMod;
+import com.mojang.serialization.JsonOps;
+
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.injected.SkyblockerStack;
+import de.hysky.skyblocker.skyblock.PetCache.PetInfo;
import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
@@ -165,6 +167,7 @@ public abstract class ItemStackMixin implements ComponentHolder, SkyblockerStack
}
// Transformation to API format.
+ //TODO future - remove this and just handle it directly for the NEU id conversion because this whole system is confusing and hard to follow
if (customData.contains("is_shiny")) {
return "ISSHINY_" + customDataString;
}
@@ -178,12 +181,14 @@ public abstract class ItemStackMixin implements ComponentHolder, SkyblockerStack
return "ENCHANTMENT_" + enchant.toUpperCase(Locale.ENGLISH) + "_" + enchants.getInt(enchant);
}
}
+
case "PET" -> {
if (customData.contains("petInfo")) {
- JsonObject petInfo = SkyblockerMod.GSON.fromJson(customData.getString("petInfo"), JsonObject.class);
- return "LVL_1_" + petInfo.get("tier").getAsString() + "_" + petInfo.get("type").getAsString();
+ PetInfo petInfo = PetInfo.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(customData.getString("petInfo"))).getOrThrow();
+ return "LVL_1_" + petInfo.tier() + "_" + petInfo.type();
}
}
+
case "POTION" -> {
String enhanced = customData.contains("enhanced") ? "_ENHANCED" : "";
String extended = customData.contains("extended") ? "_EXTENDED" : "";
@@ -193,6 +198,7 @@ public abstract class ItemStackMixin implements ComponentHolder, SkyblockerStack
+ enhanced + extended + splash).toUpperCase(Locale.ENGLISH);
}
}
+
case "RUNE" -> {
if (customData.contains("runes")) {
NbtCompound runes = customData.getCompound("runes");
@@ -201,6 +207,7 @@ public abstract class ItemStackMixin implements ComponentHolder, SkyblockerStack
return rune.toUpperCase(Locale.ENGLISH) + "_RUNE_" + runes.getInt(rune);
}
}
+
case "ATTRIBUTE_SHARD" -> {
if (customData.contains("attributes")) {
NbtCompound shards = customData.getCompound("attributes");
@@ -209,6 +216,42 @@ public abstract class ItemStackMixin implements ComponentHolder, SkyblockerStack
return customDataString + "-" + shard.toUpperCase(Locale.ENGLISH) + "_" + shards.getInt(shard);
}
}
+
+ case "NEW_YEAR_CAKE" -> {
+ return customDataString + "_" + customData.getInt("new_years_cake");
+ }
+
+ case "PARTY_HAT_CRAB", "PARTY_HAT_CRAB_ANIMATED", "BALLOON_HAT_2024" -> {
+ return customDataString + "_" + customData.getString("party_hat_color").toUpperCase(Locale.ENGLISH);
+ }
+
+ case "PARTY_HAT_SLOTH" -> {
+ return customDataString + "_" + customData.getString("party_hat_emoji").toUpperCase(Locale.ENGLISH);
+ }
+
+ case "CRIMSON_HELMET", "CRIMSON_CHESTPLATE", "CRIMSON_LEGGINGS", "CRIMSON_BOOTS" -> {
+ NbtCompound attributes = customData.getCompound("attributes");
+
+ if (attributes.contains("magic_find") && attributes.contains("veteran")) {
+ return customDataString + "-MAGIC_FIND-VETERAN";
+ }
+ }
+
+ case "AURORA_HELMET", "AURORA_CHESTPLATE", "AURORA_LEGGINGS", "AURORA_BOOTS" -> {
+ NbtCompound attributes = customData.getCompound("attributes");
+
+ if (attributes.contains("mana_pool") && attributes.contains("mana_regeneration")) {
+ return customDataString + "-MANA_POOL-MANA_REGENERATION";
+ }
+ }
+
+ case "TERROR_HELMET", "TERROR_CHESTPLATE", "TERROR_LEGGINGS", "TERROR_BOOTS" -> {
+ NbtCompound attributes = customData.getCompound("attributes");
+
+ if (attributes.contains("lifeline") && attributes.contains("mana_pool")) {
+ return customDataString + "-LIFELINE-MANA_POOL";
+ }
+ }
}
return customDataString;
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java b/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java
new file mode 100644
index 00000000..d8cd6e48
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java
@@ -0,0 +1,149 @@
+package de.hysky.skyblocker.skyblock;
+
+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;
+import java.util.Optional;
+
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+
+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 de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.Utils;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+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.nbt.NbtCompound;
+import net.minecraft.screen.slot.Slot;
+
+/**
+ * Doesn't work with auto pet right now because thats 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<String, Object2ObjectOpenHashMap<String, PetInfo>> CACHED_PETS = new Object2ObjectOpenHashMap<>();
+
+ /**
+ * Used in case the server lags to prevent the screen tick check from overwriting the clicked pet logic
+ */
+ private static boolean shouldLook4Pets;
+
+ public static void init() {
+ load();
+
+ ScreenEvents.BEFORE_INIT.register((_client, screen, _scaledWidth, _scaledHeight) -> {
+ if (Utils.isOnSkyblock() && screen instanceof GenericContainerScreen genericContainerScreen) {
+ if (genericContainerScreen.getTitle().getString().startsWith("Pets")) {
+ shouldLook4Pets = true;
+
+ ScreenEvents.afterTick(screen).register(screen1 -> {
+ if (shouldLook4Pets) {
+ for (Slot slot : genericContainerScreen.getScreenHandler().slots) {
+ ItemStack stack = slot.getStack();
+
+ if (!stack.isEmpty() && ItemUtils.getLoreLineIf(stack, line -> line.equals("Click to despawn!")) != null) {
+ shouldLook4Pets = false;
+ parsePet(stack, false);
+
+ break;
+ }
+ }
+ }
+ });
+ }
+ }
+ });
+ }
+
+ private static void load() {
+ CompletableFuture.runAsync(() -> {
+ try (BufferedReader reader = Files.newBufferedReader(FILE)) {
+ CACHED_PETS.putAll(PetInfo.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(PetInfo.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) {
+ ItemStack stack = slot.getStack();
+
+ if (!stack.isEmpty()) parsePet(stack, true);
+ }
+ }
+
+ private static void parsePet(ItemStack stack, boolean clicked) {
+ String id = ItemUtils.getItemId(stack);
+ String profileId = Utils.getProfileId();
+
+ if (id.equals("PET") && !profileId.isEmpty()) {
+ NbtCompound customData = ItemUtils.getCustomData(stack);
+
+ //Should never fail, all pets must have this but you never know with Hypixel
+ try {
+ PetInfo petInfo = PetInfo.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(customData.getString("petInfo"))).getOrThrow();
+ shouldLook4Pets = false;
+
+ Object2ObjectOpenHashMap<String, PetInfo> 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);
+ } else {
+ playerData.put(profileId, petInfo);
+ }
+
+ save();
+ } catch (Exception e) {
+ LOGGER.error(LogUtils.FATAL_MARKER, "[Skyblocker Pet Cache] Failed to parse pet's pet info!", e);
+ }
+ }
+ }
+
+ @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;
+ }
+
+ public record PetInfo(String type, double exp, String tier, Optional<String> uuid) {
+ public static final Codec<PetInfo> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codec.STRING.fieldOf("type").forGetter(PetInfo::type),
+ Codec.DOUBLE.fieldOf("exp").forGetter(PetInfo::exp),
+ Codec.STRING.fieldOf("tier").forGetter(PetInfo::tier),
+ Codec.STRING.optionalFieldOf("uuid").forGetter(PetInfo::uuid))
+ .apply(instance, PetInfo::new));
+ private static final Codec<Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, PetInfo>>> SERIALIZATION_CODEC = Codec.unboundedMap(Codec.STRING,
+ Codec.unboundedMap(Codec.STRING, CODEC).xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new)
+ ).xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new);
+ }
+}
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 96c21d22..93d29714 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java
@@ -1,9 +1,9 @@
package de.hysky.skyblocker.skyblock.item;
-import com.google.gson.JsonElement;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.PetCache;
+import de.hysky.skyblocker.skyblock.PetCache.PetInfo;
import de.hysky.skyblocker.utils.ItemUtils;
-import de.hysky.skyblocker.utils.ProfileUtils;
import net.fabricmc.fabric.api.event.client.player.ClientPlayerBlockBreakEvents;
import net.fabricmc.fabric.api.event.player.UseItemCallback;
import net.minecraft.block.BlockState;
@@ -36,8 +36,8 @@ public class ItemCooldowns {
561700, 611700, 666700, 726700, 791700, 861700, 936700, 1016700, 1101700, 1191700,
1286700, 1386700, 1496700, 1616700, 1746700, 1886700
};
- public static int monkeyLevel = 1;
- public static double monkeyExp = 0;
+ private static int monkeyLevel = 1;
+ private static double monkeyExp = 0;
public static void init() {
ClientPlayerBlockBreakEvents.AFTER.register(ItemCooldowns::afterBlockBreak);
@@ -45,30 +45,24 @@ public class ItemCooldowns {
}
public static void updateCooldown() {
- ProfileUtils.updateProfile().thenAccept(player -> {
- for (JsonElement pet : player.getAsJsonObject("pets_data").getAsJsonArray("pets")) {
- if (!pet.getAsJsonObject().get("type").getAsString().equals("MONKEY")) continue;
- if (!pet.getAsJsonObject().get("active").getAsString().equals("true")) continue;
- if (pet.getAsJsonObject().get("tier").getAsString().equals("LEGENDARY")) {
- monkeyExp = Double.parseDouble(pet.getAsJsonObject().get("exp").getAsString());
- monkeyLevel = 0;
- for (int xpLevel : EXPERIENCE_LEVELS) {
- if (monkeyExp < xpLevel) {
- break;
- } else {
- monkeyExp -= xpLevel;
- monkeyLevel++;
- }
- }
+ PetInfo pet = PetCache.getCurrentPet();
+
+ if (pet != null && pet.tier().equals("LEGENDARY")) {
+ monkeyExp = pet.exp();
+
+ monkeyLevel = 0;
+ for (int xpLevel : EXPERIENCE_LEVELS) {
+ if (monkeyExp < xpLevel) {
+ break;
+ } else {
+ monkeyExp -= xpLevel;
+ monkeyLevel++;
}
}
- }).exceptionally(e -> {
- ProfileUtils.LOGGER.error("[Skyblocker Item Cooldown] Failed to get Player Pet Data, is the API Down/Limited?", e);
- return null;
- });
+ }
}
- private static int getCooldown() {
+ private static int getCooldown4Foraging() {
int baseCooldown = 2000;
int monkeyPetCooldownReduction = baseCooldown * monkeyLevel / 200;
return baseCooldown - monkeyPetCooldownReduction;
@@ -82,7 +76,7 @@ public class ItemCooldowns {
if (usedItemId.equals(JUNGLE_AXE_ID) || usedItemId.equals(TREECAPITATOR_ID)) {
updateCooldown();
if (!isOnCooldown(JUNGLE_AXE_ID) || !isOnCooldown(TREECAPITATOR_ID)) {
- ITEM_COOLDOWNS.put(usedItemId, new CooldownEntry(getCooldown()));
+ ITEM_COOLDOWNS.put(usedItemId, new CooldownEntry(getCooldown4Foraging()));
}
}
}
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..50982d29 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<String, Object2ObjectOpenHashMap<String, ProfileMuseumData>> 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<Void> loaded;
public static void init() {
ClientLifecycleEvents.CLIENT_STARTED.register(MuseumItemCache::load);
+ ClientCommandRegistrationCallback.EVENT.register(MuseumItemCache::registerCommands);
+ }
+
+ private static void registerCommands(CommandDispatcher<FabricClientCommandSource> 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<Slot> 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 = Utils.getUndashedUuid();
+ //Be safe about access to avoid NPEs
+ Map<String, ProfileMuseumData> 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,85 @@ 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 = Utils.getUndashedUuid();
+ 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 = Utils.getUndashedUuid();
+
+ if (loaded.isDone() && (!MUSEUM_ITEM_CACHE.containsKey(uuid) || !MUSEUM_ITEM_CACHE.getOrDefault(uuid, new Object2ObjectOpenHashMap<>()).containsKey(profileId))) {
Map<String, ProfileMuseumData> 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 = Utils.getUndashedUuid();
ObjectOpenHashSet<String> 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<String> collectedItemIds) {
+ private record ProfileMuseumData(long lastResync, ObjectOpenHashSet<String> 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 = 600_000L;
private static final Codec<ProfileMuseumData> 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 +252,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/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java
index 2f5408a1..cc3d2099 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java
@@ -52,6 +52,11 @@ public class ItemTooltip {
case "POTION" -> apiId = "";
case "ATTRIBUTE_SHARD" ->
apiId = id + "+" + apiId.replace("SHARD-", "").replaceAll("_(?!.*_)", ";");
+ case "NEW_YEAR_CAKE" -> apiId = id + "+" + apiId.replace("NEW_YEAR_CAKE_", "");
+ case "PARTY_HAT_CRAB_ANIMATED" -> apiId = "PARTY_HAT_CRAB_" + apiId.replace("PARTY_HAT_CRAB_ANIMATED_", "") + "_ANIMATED";
+ case "CRIMSON_HELMET", "CRIMSON_CHESTPLATE", "CRIMSON_LEGGINGS", "CRIMSON_BOOTS",
+ "AURORA_HELMET", "AURORA_CHESTPLATE", "AURORA_LEGGINGS", "AURORA_BOOTS",
+ "TERROR_HELMET", "TERROR_CHESTPLATE", "TERROR_LEGGINGS", "TERROR_BOOTS" -> apiId = id;
default -> apiId = apiId.replace(":", "-");
}
return apiId;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java
index 89d41290..10e12ace 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java
@@ -78,7 +78,8 @@ public class ItemListTab extends ItemListWidget.TabContainerWidget {
return true;
} else if (results != null) {
this.searchField.setFocused(false);
- this.results.mouseClicked(mouseX, mouseY, button);
+
+ return this.results.mouseClicked(mouseX, mouseY, button);
}
return false;
diff --git a/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java b/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java
new file mode 100644
index 00000000..fbf814ee
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java
@@ -0,0 +1,169 @@
+package de.hysky.skyblocker.utils;
+
+import java.nio.ByteBuffer;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.util.Base64;
+import java.util.Objects;
+import java.util.UUID;
+
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+
+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 de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.minecraft.SharedConstants;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.network.encryption.PlayerKeyPair;
+import net.minecraft.text.Text;
+import net.minecraft.util.Uuids;
+import net.minecraft.util.dynamic.Codecs;
+
+/**
+ * This class is responsible for communicating with the API to retrieve a fully custom token used to gain access to more privileged APIs
+ * such as the Hypixel API Proxy. The main point of this is to verify that a person is most likely playing Minecraft, and thus is likely to be
+ * using the mod, and not somebody who is attempting to freeload off of our services.
+ */
+public class ApiAuthentication {
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+ private static final String MINECRAFT_VERSION = SharedConstants.getGameVersion().getName();
+ private static final String AUTH_URL = "https://hysky.de/api/aaron/authenticate";
+ private static final String CONTENT_TYPE = "application/json";
+ private static final String ALGORITHM = "SHA256withRSA";
+
+ private static TokenInfo tokenInfo = null;
+
+ public static void init() {
+ //Update token after the profileKeys instance is initialized
+ ClientLifecycleEvents.CLIENT_STARTED.register(_client -> updateToken());
+ }
+
+ /**
+ * Refreshes the token by fetching the player's key pair from the Minecraft Services API.
+ *
+ * We use the player's uuid, public key, public key signature, public key expiry date, and randomly signed data by the private key to verify the identity/legitimacy
+ * of the player without the server needing to handle any privileged information.
+ *
+ * Mojang provides a signature for each key pair which is comprised of the player's uuid, public key, and expiry time so we can easily validate if the key pair
+ * was generated by Mojang and is tied to said player. For information about what the randomly signed data is used for and why see {@link #getRandomSignedData(PrivateKey)}
+ */
+ private static void updateToken() {
+ //The fetching runs async in ProfileKeysImpl#getKeyPair
+ CLIENT.getProfileKeys().fetchKeyPair().thenAcceptAsync(playerKeypairOpt -> {
+ if (playerKeypairOpt.isPresent()) {
+ PlayerKeyPair playerKeyPair = playerKeypairOpt.get();
+
+ //The key header and footer can be sent but that doesn't matter to the server
+ String publicKey = Base64.getMimeEncoder().encodeToString(playerKeyPair.publicKey().data().key().getEncoded());
+ byte[] publicKeySignature = playerKeyPair.publicKey().data().keySignature();
+ long expiresAt = playerKeyPair.publicKey().data().expiresAt().toEpochMilli();
+
+ TokenRequest.KeyPairInfo keyPairInfo = new TokenRequest.KeyPairInfo(Objects.requireNonNull(CLIENT.getSession().getUuidOrNull()), publicKey, publicKeySignature, expiresAt);
+ TokenRequest.SignedData signedData = Objects.requireNonNull(getRandomSignedData(playerKeyPair.privateKey()));
+ TokenRequest tokenRequest = new TokenRequest(keyPairInfo, signedData, SkyblockerMod.SKYBLOCKER_MOD.getMetadata().getId(), MINECRAFT_VERSION, SkyblockerMod.VERSION);
+
+ String request = SkyblockerMod.GSON.toJson(TokenRequest.CODEC.encodeStart(JsonOps.INSTANCE, tokenRequest).getOrThrow());
+
+ try {
+ tokenInfo = TokenInfo.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(Http.sendPostRequest(AUTH_URL, request, CONTENT_TYPE))).getOrThrow();
+ int refreshAtTicks = (int) (((tokenInfo.expiresAt() - tokenInfo.issuedAt()) / 1000L) - 300L) * 20; //Refresh 5 minutes before expiry date
+
+ Scheduler.INSTANCE.schedule(ApiAuthentication::updateToken, refreshAtTicks, true);
+ } catch (Exception e) {
+ //Try again in 1 minute
+ logErrorAndScheduleRetry(Text.translatable("skyblocker.api.token.authFailure"), 60 * 20, "[Skyblocker Api Auth] Failed to refresh the api token! Some features might not work :(", e);
+ }
+ } else {
+ //The Minecraft Services API is probably down so we will retry in 5 minutes, either that or your access token has expired (game open for 24h) and you need to restart.
+ logErrorAndScheduleRetry(Text.translatable("skyblocker.api.token.noProfileKeys"), 300 * 20, "[Skyblocker Api Auth] Failed to fetch profile keys! Some features may not work temporarily :( (Has your game been open for more than 24 hours? If so restart.)");
+ }