From 0f2c29fe6cc57cae766fa0d862cda6c26f1b0741 Mon Sep 17 00:00:00 2001 From: nmccullagh Date: Mon, 13 May 2024 15:54:01 +0100 Subject: [PATCH 1/5] profile viewer skill page --- .../de/hysky/skyblocker/SkyblockerMod.java | 2 + .../skyblocker/mixins/ItemStackMixin.java | 6 +- .../hysky/skyblocker/skyblock/PetCache.java | 27 +- .../skyblock/dungeon/DungeonScore.java | 2 +- .../skyblock/itemlist/ItemStackBuilder.java | 6 +- .../profileviewer/ProfileViewerNavButton.java | 65 ++++ .../profileviewer/ProfileViewerPage.java | 19 ++ .../profileviewer/ProfileViewerScreen.java | 256 +++++++++++++++ .../ProfileViewerTextWidget.java | 55 ++++ .../collections/CollectionsPage.java | 100 ++++++ .../collections/GenericCategory.java | 136 ++++++++ .../dungeons/DungeonClassWidget.java | 62 ++++ .../dungeons/DungeonFloorRunsWidget.java | 55 ++++ .../dungeons/DungeonHeaderWidget.java | 46 +++ .../dungeons/DungeonMiscStatsWidgets.java | 61 ++++ .../profileviewer/dungeons/DungeonsPage.java | 39 +++ .../profileviewer/inventory/Inventory.java | 120 +++++++ .../inventory/InventoryPage.java | 113 +++++++ .../inventory/PaginationButton.java | 46 +++ .../skyblock/profileviewer/inventory/Pet.java | 186 +++++++++++ .../inventory/PlayerInventory.java | 73 +++++ .../itemLoaders/BackpackItemLoader.java | 34 ++ .../itemLoaders/InventoryItemLoader.java | 30 ++ .../inventory/itemLoaders/ItemLoader.java | 139 ++++++++ .../itemLoaders/PetsInventoryItemLoader.java | 42 +++ .../WardrobeInventoryItemLoader.java | 39 +++ .../profileviewer/skills/SkillWidget.java | 95 ++++++ .../profileviewer/skills/SkillsPage.java | 93 ++++++ .../profileviewer/slayers/SlayerWidget.java | 93 ++++++ .../profileviewer/slayers/SlayersPage.java | 41 +++ .../profileviewer/utils/LevelFinder.java | 307 ++++++++++++++++++ .../profileviewer/utils/SkullCreator.java | 27 ++ .../utils/SubPageSelectButton.java | 65 ++++ .../skyblock/shortcut/Shortcuts.java | 1 - .../skyblocker/skyblock/tabhud/util/Ico.java | 38 ++- .../tabhud/widget/DungeonDeathWidget.java | 2 +- .../tabhud/widget/ElectionWidget.java | 4 +- .../tabhud/widget/GardenSkillsWidget.java | 4 +- .../tabhud/widget/ReputationWidget.java | 2 +- .../skyblock/tabhud/widget/SkillsWidget.java | 8 +- .../de/hysky/skyblocker/utils/ApiUtils.java | 6 +- .../java/de/hysky/skyblocker/utils/Http.java | 11 +- .../hysky/skyblocker/utils/ProfileUtils.java | 37 ++- .../gui/profile_viewer/base_plate.png | Bin 0 -> 5665 bytes .../textures/gui/profile_viewer/blaze.png | Bin 0 -> 10784 bytes .../gui/profile_viewer/button_icon.png | Bin 0 -> 4301 bytes .../button_icon_highlighted.png | Bin 0 -> 4516 bytes .../profile_viewer/button_icon_toggled.png | Bin 0 -> 4515 bytes .../button_icon_toggled_highlighted.png | Bin 0 -> 4513 bytes .../gui/profile_viewer/dungeons_body.png | Bin 0 -> 5058 bytes .../gui/profile_viewer/dungeons_header.png | Bin 0 -> 4940 bytes .../textures/gui/profile_viewer/enderman.png | Bin 0 -> 7829 bytes .../gui/profile_viewer/icon_data_widget.png | Bin 0 -> 4979 bytes .../textures/gui/profile_viewer/run_icon.png | Bin 0 -> 5042 bytes .../textures/gui/profile_viewer/spider.png | Bin 0 -> 9659 bytes .../textures/gui/profile_viewer/vampire.png | Bin 0 -> 10351 bytes .../textures/gui/profile_viewer/wolf.png | Bin 0 -> 8813 bytes .../textures/gui/profile_viewer/zombie.png | Bin 0 -> 9167 bytes 58 files changed, 2522 insertions(+), 71 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerNavButton.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerPage.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerTextWidget.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/CollectionsPage.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/GenericCategory.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonClassWidget.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonFloorRunsWidget.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonHeaderWidget.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonMiscStatsWidgets.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonsPage.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Inventory.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/InventoryPage.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PaginationButton.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Pet.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PlayerInventory.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/BackpackItemLoader.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/ItemLoader.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/PetsInventoryItemLoader.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillWidget.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillsPage.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayerWidget.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayersPage.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/LevelFinder.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SkullCreator.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SubPageSelectButton.java create mode 100644 src/main/resources/assets/skyblocker/textures/gui/profile_viewer/base_plate.png create mode 100644 src/main/resources/assets/skyblocker/textures/gui/profile_viewer/blaze.png create mode 100644 src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon.png create mode 100644 src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_highlighted.png create mode 100644 src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled.png create mode 100644 src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled_highlighted.png create mode 100644 src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_body.png create mode 100644 src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_header.png create mode 100644 src/main/resources/assets/skyblocker/textures/gui/profile_viewer/enderman.png create mode 100644 src/main/resources/assets/skyblocker/textures/gui/profile_viewer/icon_data_widget.png create mode 100644 src/main/resources/assets/skyblocker/textures/gui/profile_viewer/run_icon.png create mode 100644 src/main/resources/assets/skyblocker/textures/gui/profile_viewer/spider.png create mode 100644 src/main/resources/assets/skyblocker/textures/gui/profile_viewer/vampire.png create mode 100644 src/main/resources/assets/skyblocker/textures/gui/profile_viewer/wolf.png create mode 100644 src/main/resources/assets/skyblocker/textures/gui/profile_viewer/zombie.png diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index d793e73d33..05c7fb1e11 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -36,6 +36,7 @@ import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; import de.hysky.skyblocker.skyblock.item.tooltip.TooltipManager; import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; import de.hysky.skyblocker.skyblock.rift.TheRift; import de.hysky.skyblocker.skyblock.searchoverlay.SearchOverManager; import de.hysky.skyblocker.skyblock.shortcut.Shortcuts; @@ -105,6 +106,7 @@ public void onInitializeClient() { Utils.init(); SkyblockerConfigManager.init(); SkyblockerScreen.initClass(); + ProfileViewerScreen.initClass(); Tips.init(); NEURepoManager.init(); //ImageRepoLoader.init(); diff --git a/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java index 3abbfbcd04..c0186fee56 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java @@ -8,9 +8,11 @@ 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.skyblock.profileviewer.ProfileViewerScreen; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.client.MinecraftClient; import net.minecraft.component.ComponentHolder; import net.minecraft.component.type.ItemEnchantmentsComponent; import net.minecraft.item.ItemStack; @@ -108,8 +110,8 @@ public abstract class ItemStackMixin implements ComponentHolder, SkyblockerStack } @Unique - private boolean skyblocker$shouldProcess() { - return Utils.isOnSkyblock() && SkyblockerConfigManager.get().mining.enableDrillFuel && ItemUtils.hasCustomDurability((ItemStack) (Object) this); + private boolean skyblocker$shouldProcess() { // Durability bar renders atop of tooltips in ProfileViewer so disable on this screen + return !(MinecraftClient.getInstance().currentScreen instanceof ProfileViewerScreen) && Utils.isOnSkyblock() && SkyblockerConfigManager.get().mining.enableDrillFuel && ItemUtils.hasCustomDurability((ItemStack) (Object) this); } @Unique diff --git a/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java b/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java index d8cd6e4897..8d0406cb7f 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java @@ -1,22 +1,10 @@ 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; @@ -26,6 +14,16 @@ import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtCompound; 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.Optional; +import java.util.concurrent.CompletableFuture; /** * Doesn't work with auto pet right now because thats complicated. @@ -135,12 +133,13 @@ public static PetInfo getCurrentPet() { 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 uuid) { + public record PetInfo(String type, double exp, String tier, Optional uuid, Optional item) { public static final Codec 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)) + Codec.STRING.optionalFieldOf("uuid").forGetter(PetInfo::uuid), + Codec.STRING.optionalFieldOf("heldItem").forGetter(PetInfo::item)) .apply(instance, PetInfo::new)); private static final Codec>> SERIALIZATION_CODEC = Codec.unboundedMap(Codec.STRING, Codec.unboundedMap(Codec.STRING, CODEC).xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java index 7a5abed1bb..d1fc08eccd 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java @@ -304,7 +304,7 @@ private static void checkMessageForDeaths(String message) { if (s.equals("You")) return MinecraftClient.getInstance().getSession().getUsername(); //This will be wrong if the dead player is called 'You' but that's unlikely else return s; }); - ProfileUtils.updateProfile(whoDied).thenAccept(player -> firstDeathHasSpiritPet = hasSpiritPet(player, whoDied)); + ProfileUtils.updateProfileByName(whoDied).thenAccept(player -> firstDeathHasSpiritPet = hasSpiritPet(player, whoDied)); } private static void checkMessageForWatcher(String message) { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java index 7eda7646c2..01ffc1446b 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java @@ -21,8 +21,8 @@ import java.util.regex.Pattern; public class ItemStackBuilder { - private static final Pattern SKULL_UUID_PATTERN = Pattern.compile("(?<=SkullOwner:\\{)Id:\"(.{36})\""); - private static final Pattern SKULL_TEXTURE_PATTERN = Pattern.compile("(?<=Properties:\\{textures:\\[0:\\{Value:)\"(.+?)\""); + public static final Pattern SKULL_UUID_PATTERN = Pattern.compile("(?<=SkullOwner:\\{)Id:\"(.{36})\""); + public static final Pattern SKULL_TEXTURE_PATTERN = Pattern.compile("(?<=Properties:\\{textures:\\[0:\\{Value:)\"(.+?)\""); private static final Pattern COLOR_PATTERN = Pattern.compile("color:(\\d+)"); private static final Pattern EXPLOSION_COLOR_PATTERN = Pattern.compile("\\{Explosion:\\{(?:Type:[0-9a-z]+,)?Colors:\\[(?[0-9]+)]\\}"); private static Map> petNums; @@ -138,7 +138,7 @@ private static List> petData(String internalName) { return list; } - private static String injectData(String string, List> injectors) { + public static String injectData(String string, List> injectors) { for (Pair injector : injectors) { string = string.replaceAll(injector.getLeft(), injector.getRight()); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerNavButton.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerNavButton.java new file mode 100644 index 0000000000..d867a0e622 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerNavButton.java @@ -0,0 +1,65 @@ +package de.hysky.skyblocker.skyblock.profileviewer; + +import com.mojang.blaze3d.systems.RenderSystem; +import de.hysky.skyblocker.skyblock.profileviewer.utils.SkullCreator; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.item.ItemStack; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.util.Map; + +public class ProfileViewerNavButton extends ClickableWidget { + private final static Identifier BUTTON_TEXTURES_TOGGLED = Identifier.of("container/creative_inventory/tab_top_selected_2"); + private final static Identifier BUTTON_TEXTURES = Identifier.of("container/creative_inventory/tab_top_unselected_2"); + private boolean toggled; + private final int index; + private final ProfileViewerScreen screen; + private final ItemStack icon; + + private static final Map HEAD_ICON = Map.ofEntries( + Map.entry("Skills", Ico.IRON_SWORD), + Map.entry("Slayers", SkullCreator.createSkull("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHBzOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzkzMzZkN2NjOTVjYmY2Njg5ZjVlOGM5NTQyOTRlYzhkMWVmYzQ5NGE0MDMxMzI1YmI0MjdiYzgxZDU2YTQ4NGQifX19")), + Map.entry("Pets", Ico.BONE), + Map.entry("Dungeons", SkullCreator.createSkull("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHBzOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzliNTY4OTViOTY1OTg5NmFkNjQ3ZjU4NTk5MjM4YWY1MzJkNDZkYjljMWIwMzg5YjhiYmViNzA5OTlkYWIzM2QifX19")), + Map.entry("Inventories", Ico.E_CHEST), + Map.entry("Collections", Ico.PAINTING) + ); + + public ProfileViewerNavButton(ProfileViewerScreen screen, String tabName, int index, boolean toggled) { + super(-100, -100, 28, 32, Text.empty()); + this.screen = screen; + this.toggled = toggled; + this.index = index; + this.icon = HEAD_ICON.getOrDefault(tabName, Ico.BARRIER); + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + RenderSystem.disableDepthTest(); + + context.drawGuiTexture(toggled ? BUTTON_TEXTURES_TOGGLED : BUTTON_TEXTURES, this.getX(), this.getY(), this.width, this.height - ((this.toggled) ? 0 : 4)); + context.drawItem(this.icon, this.getX() + 6, this.getY() + (this.toggled ? 7 : 9)); + + RenderSystem.enableDepthTest(); + } + + @Override + public void onClick(double mouseX, double mouseY) { + screen.onNavButtonClick(this); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} + + public void setToggled(boolean toggled) { + this.toggled = toggled; + } + + public int getIndex() { + return index; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerPage.java new file mode 100644 index 0000000000..f5a5ec40c0 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerPage.java @@ -0,0 +1,19 @@ +package de.hysky.skyblocker.skyblock.profileviewer; + +import de.hysky.skyblocker.skyblock.profileviewer.utils.SubPageSelectButton; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.ClickableWidget; + +import java.util.List; + +public interface ProfileViewerPage { + void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY); + default List getButtons() { + return null; + } + default void onNavButtonClick(SubPageSelectButton selectButton) {} + default void markWidgetsAsVisible() {} + default void markWidgetsAsInvisible() {} + default void nextPage() {} + default void previousPage() {} +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java new file mode 100644 index 0000000000..939d2bd166 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java @@ -0,0 +1,256 @@ +package de.hysky.skyblocker.skyblock.profileviewer; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.mixins.accessors.SkullBlockEntityAccessor; +import de.hysky.skyblocker.skyblock.profileviewer.collections.CollectionsPage; +import de.hysky.skyblocker.skyblock.profileviewer.dungeons.DungeonsPage; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.InventoryPage; +import de.hysky.skyblocker.skyblock.profileviewer.skills.SkillsPage; +import de.hysky.skyblocker.skyblock.profileviewer.slayers.SlayersPage; +import de.hysky.skyblocker.utils.ApiUtils; +import de.hysky.skyblocker.utils.Http; +import de.hysky.skyblocker.utils.ProfileUtils; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.network.OtherClientPlayerEntity; +import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.client.util.SkinTextures; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerModelPart; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.*; +import java.io.IOException; +import java.util.List; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import static net.minecraft.client.gui.screen.ingame.InventoryScreen.drawEntity; + +public class ProfileViewerScreen extends Screen { + public static final Logger LOGGER = LoggerFactory.getLogger(ProfileViewerScreen.class); + private static final Text TITLE = Text.of("Skyblocker Profile Viewer"); + private static final String HYPIXEL_COLLECTIONS = "https://api.hypixel.net/v2/resources/skyblock/collections"; + private static final Object2ObjectOpenHashMap> COLLECTIONS_CACHE = new Object2ObjectOpenHashMap<>(); + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/base_plate.png"); + private static final int GUI_WIDTH = 322; + private static final int GUI_HEIGHT = 180; + + private String playerName; + private JsonObject hypixelProfile; + private JsonObject playerProfile; + + private int activePage = 0; + private static final String[] PAGE_NAMES = {"Skills", "Slayers", "Dungeons", "Inventories", "Collections"}; + private final ProfileViewerPage[] profileViewerPages = new ProfileViewerPage[PAGE_NAMES.length]; + private final List profileViewerNavButtons = new ArrayList<>(); + private OtherClientPlayerEntity entity; + private ProfileViewerTextWidget textWidget; + + public ProfileViewerScreen(String username) { + super(TITLE); + fetchPlayerData(username).thenRun(this::initialisePagesAndWidgets); + + for (int i = 0; i < PAGE_NAMES.length; i++) { + profileViewerNavButtons.add(new ProfileViewerNavButton(this, PAGE_NAMES[i], i, i == 0)); + } + } + + private void initialisePagesAndWidgets() { + textWidget = new ProfileViewerTextWidget(hypixelProfile, playerProfile); + + CompletableFuture skillsFuture = CompletableFuture.runAsync(() -> profileViewerPages[0] = new SkillsPage(hypixelProfile, playerProfile)); + CompletableFuture slayersFuture = CompletableFuture.runAsync(() -> profileViewerPages[1] = new SlayersPage(playerProfile)); + CompletableFuture dungeonsFuture = CompletableFuture.runAsync(() -> profileViewerPages[2] = new DungeonsPage(playerProfile)); + CompletableFuture inventoriesFuture = CompletableFuture.runAsync(() -> profileViewerPages[3] = new InventoryPage(playerProfile)); + CompletableFuture collectionsFuture = CompletableFuture.runAsync(() -> profileViewerPages[4] = new CollectionsPage(hypixelProfile, playerProfile)); + + CompletableFuture.allOf(skillsFuture, slayersFuture, dungeonsFuture, inventoriesFuture, collectionsFuture) + .thenRun(() -> { + synchronized (this) { + clearAndInit(); + } + }); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + synchronized (this) { + super.render(context, mouseX, mouseY, delta); + } + + int rootX = width / 2 - GUI_WIDTH / 2; + int rootY = height / 2 - GUI_HEIGHT / 2 + 5; + + context.drawTexture(TEXTURE, rootX, rootY, 0, 0, GUI_WIDTH, GUI_HEIGHT, GUI_WIDTH, GUI_HEIGHT); + for (ProfileViewerNavButton button : profileViewerNavButtons) { + button.setX(rootX + button.getIndex() * 28 + 4); + button.setY(rootY - 28); + button.render(context, mouseX, mouseY, delta); + } + + if (textWidget != null) textWidget.render(context, textRenderer, rootX + 8, rootY + 120); + drawPlayerEntity(context, playerName != null ? playerName : "Loading...", rootX, rootY, mouseX, mouseY); + + if (profileViewerPages[activePage] != null) { + profileViewerPages[activePage].markWidgetsAsVisible(); + profileViewerPages[activePage].render(context, mouseX, mouseY, delta, rootX + 93, rootY + 7); + } else { + context.drawText(textRenderer, "Loading...", rootX + 180, rootY + 80, Color.WHITE.getRGB(), true); + } + } + + private void drawPlayerEntity(DrawContext context, String username, int rootX, int rootY, int mouseX, int mouseY) { + if (entity != null) + drawEntity(context, rootX + 9, rootY + 16, rootX + 89, rootY + 124, 42, 0.0625F, mouseX, mouseY, entity); + context.drawCenteredTextWithShadow(textRenderer, username.length() > 15 ? username.substring(0, 15) : username, rootX + 47, rootY + 14, Color.WHITE.getRGB()); + + } + + private CompletableFuture fetchPlayerData(String username) { + CompletableFuture profileFuture = ProfileUtils.fetchFullProfile(username).thenAccept(profiles -> { + this.hypixelProfile = profiles.getAsJsonArray("profiles").asList().stream() + .map(JsonElement::getAsJsonObject) + .filter(profile -> profile.getAsJsonPrimitive("selected").getAsBoolean()) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No selected profile found!")); + + this.playerProfile = hypixelProfile.getAsJsonObject("members").get(ApiUtils.name2Uuid(username)).getAsJsonObject(); + }); + + CompletableFuture minecraftProfileFuture = SkullBlockEntityAccessor.invokeFetchProfileByName(username).thenAccept(profile -> { + this.playerName = profile.get().getName(); + entity = new OtherClientPlayerEntity(MinecraftClient.getInstance().world, profile.get()) { + @Override + public SkinTextures getSkinTextures() { + PlayerListEntry playerListEntry = new PlayerListEntry(profile.get(), false); + return playerListEntry.getSkinTextures(); + } + + @Override + public boolean isPartVisible(PlayerModelPart modelPart) { + return !(modelPart.getName().equals(PlayerModelPart.CAPE.getName())); + } + + @Override + public boolean isInvisibleTo(PlayerEntity player) { + return true; + } + }; + entity.setCustomNameVisible(false); + }).exceptionally(ex -> { + this.playerName = "User not found"; + return null; + }); + + return CompletableFuture.allOf(profileFuture, minecraftProfileFuture); + } + + public void onNavButtonClick(ProfileViewerNavButton clickedButton) { + if (profileViewerPages[activePage] != null) profileViewerPages[activePage].markWidgetsAsInvisible(); + for (ProfileViewerNavButton button : profileViewerNavButtons) { + button.setToggled(false); + } + activePage = clickedButton.getIndex(); + clickedButton.setToggled(true); + } + + @Override + public void init() { + profileViewerNavButtons.forEach(this::addDrawableChild); + for (ProfileViewerPage profileViewerPage : profileViewerPages) { + if (profileViewerPage != null && profileViewerPage.getButtons() != null) { + for (ClickableWidget button : profileViewerPage.getButtons()) { + if (button != null) addDrawableChild(button); + } + } + } + } + + public static void initClass() { + fetchCollectionsData(); // caching on launch + + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { + dispatcher.register( + ClientCommandManager.literal(SkyblockerMod.NAMESPACE) + .then(ClientCommandManager.literal("pv") + .then(ClientCommandManager.argument("username", StringArgumentType.string()) + .executes(context -> { + String username = StringArgumentType.getString(context, "username"); + Command cmd = Scheduler.queueOpenScreenCommand(() -> new ProfileViewerScreen(username)); + return cmd.run(context); + }) + ) + .executes(context -> { + String username = MinecraftClient.getInstance().getSession().getUsername(); + Command cmd = Scheduler.queueOpenScreenCommand(() -> new ProfileViewerScreen(username)); + return cmd.run(context); + }) + ) + ); + + dispatcher.register( + ClientCommandManager.literal("pv") + .then(ClientCommandManager.argument("username", StringArgumentType.string()) + .executes(context -> { + String username = StringArgumentType.getString(context, "username"); + Command cmd = Scheduler.queueOpenScreenCommand(() -> new ProfileViewerScreen(username)); + return cmd.run(context); + }) + ) + .executes(context -> { + String username = MinecraftClient.getInstance().getSession().getUsername(); + Command cmd = Scheduler.queueOpenScreenCommand(() -> new ProfileViewerScreen(username)); + return cmd.run(context); + }) + ); + }); + } + + @NotNull + public static Map> fetchCollectionsData() { + if (!COLLECTIONS_CACHE.isEmpty()) return COLLECTIONS_CACHE; + try { + JsonObject jsonObject = JsonParser.parseString(Http.sendGetRequest(HYPIXEL_COLLECTIONS)).getAsJsonObject(); + if (jsonObject.get("success").getAsBoolean()) { + Map collectionsMap = new HashMap<>(); + Map> tierRequirementsMap = new HashMap<>(); + JsonObject collections = jsonObject.getAsJsonObject("collections"); + collections.entrySet().forEach(entry -> { + String category = entry.getKey(); + JsonObject itemsObject = entry.getValue().getAsJsonObject().getAsJsonObject("items"); + String[] items = itemsObject.keySet().toArray(new String[0]); + collectionsMap.put(category, items); + itemsObject.entrySet().forEach(itemEntry -> { + List tierReqs = new ArrayList<>(); + itemEntry.getValue().getAsJsonObject().getAsJsonArray("tiers").forEach(req -> + tierReqs.add(req.getAsJsonObject().get("amountRequired").getAsInt())); + tierRequirementsMap.put(itemEntry.getKey(), tierReqs); + }); + }); + COLLECTIONS_CACHE.put("COLLECTIONS", collectionsMap); + COLLECTIONS_CACHE.put("TIER_REQS", tierRequirementsMap); + return COLLECTIONS_CACHE; + } + } catch (IOException | InterruptedException e) { + LOGGER.error("[Skyblocker Profile Viewer] Failed to fetch collections data", e); + } + return Collections.emptyMap(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerTextWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerTextWidget.java new file mode 100644 index 0000000000..4ee2dbba90 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerTextWidget.java @@ -0,0 +1,55 @@ +package de.hysky.skyblocker.skyblock.profileviewer; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Colors; + +public class ProfileViewerTextWidget { + private static final int ROW_GAP = 9; + + private String PROFILE_NAME = "UNKNOWN"; + private int SKYBLOCK_LEVEL = 0; + private double PURSE = 0; + private double BANK = 0; + + public ProfileViewerTextWidget(JsonObject hypixelProfile, JsonObject playerProfile){ + try { + this.PROFILE_NAME = hypixelProfile.get("cute_name").getAsString(); + this.SKYBLOCK_LEVEL = playerProfile.getAsJsonObject("leveling").get("experience").getAsInt() / 100; + this.PURSE = playerProfile.getAsJsonObject("currencies").get("coin_purse").getAsDouble(); + this.BANK = hypixelProfile.getAsJsonObject("banking").get("balance").getAsDouble(); + } catch (Exception ignored) {} + } + + public void render(DrawContext context, TextRenderer textRenderer, int root_x, int root_y){ + // Profile Icon + MatrixStack matrices = context.getMatrices(); + matrices.push(); + matrices.scale(0.75f, 0.75f, 1); + int rootAdjustedX = (int) ((root_x) / 0.75f); + int rootAdjustedY = (int) ((root_y) / 0.75f); + context.drawItem(Ico.PAINTING, rootAdjustedX, rootAdjustedY); + matrices.pop(); + + context.drawText(textRenderer, "§n"+PROFILE_NAME, root_x + 14, root_y + 3, Colors.WHITE, true); + context.drawText(textRenderer, "§aLevel:§r " + SKYBLOCK_LEVEL, root_x + 2, root_y + 6 + ROW_GAP, Colors.WHITE, true); + context.drawText(textRenderer, "§6Purse:§r " + formatCoins(PURSE), root_x + 2, root_y + 6 + ROW_GAP * 2, Colors.WHITE, true); + context.drawText(textRenderer, "§6Bank:§r " + formatCoins(BANK), root_x + 2, root_y + 6 + ROW_GAP * 3, Colors.WHITE, true); + context.drawText(textRenderer, "§6NW:§r " + "Soon™", root_x + 2, root_y + 6 + ROW_GAP * 4, Colors.WHITE, true ); + } + + private String formatCoins(double amount) { + if (amount >= 1_000_000_000) { + return String.format("%.4gB", amount / 1_000_000_000); + } else if (amount >= 1_000_000) { + return String.format("%.4gM", amount / 1_000_000); + } else if (amount >= 1_000) { + return String.format("%.4gK", amount / 1_000); + } else { + return String.valueOf((int)(amount)); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/CollectionsPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/CollectionsPage.java new file mode 100644 index 0000000000..b77c3e7a55 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/CollectionsPage.java @@ -0,0 +1,100 @@ +package de.hysky.skyblocker.skyblock.profileviewer.collections; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import de.hysky.skyblocker.skyblock.profileviewer.utils.SubPageSelectButton; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.item.ItemStack; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class CollectionsPage implements ProfileViewerPage { + private static final String[] COLLECTION_CATEGORIES = {"MINING", "FARMING", "COMBAT", "FISHING", "FORAGING", "RIFT"}; + private static final int TOTAL_HEIGHT = 165; + private static final Map ICON_MAP = Map.ofEntries( + Map.entry("MINING", Ico.STONE_PICKAXE), + Map.entry("FARMING", Ico.GOLDEN_HOE), + Map.entry("COMBAT", Ico.STONE_SWORD), + Map.entry("FISHING", Ico.FISH_ROD), + Map.entry("FORAGING", Ico.JUNGLE_SAPLING), + // Map.entry("BOSS", Ico.WITHER), Not currently part of Collections API so skipping for now + Map.entry("RIFT", Ico.MYCELIUM) + ); + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + + private final GenericCategory[] collections = new GenericCategory[COLLECTION_CATEGORIES.length]; + private final List collectionSelectButtons = new ArrayList<>(); + private int activePage = 0; + + + public CollectionsPage(JsonObject hProfile, JsonObject pProfile) { + for (int i = 0; i < COLLECTION_CATEGORIES.length; i++) { + try { + collectionSelectButtons.add(new SubPageSelectButton(this, -100, 0, i, ICON_MAP.getOrDefault(COLLECTION_CATEGORIES[i], Ico.BARRIER))); + collections[i] = new GenericCategory(hProfile, pProfile, COLLECTION_CATEGORIES[i]); + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Error creating Collections Page", e); + } + } + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + int startingY = rootY + (TOTAL_HEIGHT - collectionSelectButtons.size() * 21) / 2; + for (int i = 0; i < collectionSelectButtons.size(); i++) { + collectionSelectButtons.get(i).setX(rootX); + collectionSelectButtons.get(i).setY(startingY + i * 21); + collectionSelectButtons.get(i).render(context, mouseX, mouseY, delta); + } + + if (collections[activePage] == null) { + context.drawText(textRenderer, "No data...", rootX + 92, rootY + 72, Color.DARK_GRAY.getRGB(), false); + return; + } + + collections[activePage].markWidgetsAsVisible(); + collections[activePage].render(context, mouseX, mouseY, delta, rootX + 35, rootY + 6); + } + + public void onNavButtonClick(SubPageSelectButton selectButton) { + if (collections[activePage] != null) collections[activePage].markWidgetsAsInvisible(); + for (SubPageSelectButton button : collectionSelectButtons) { + button.setToggled(false); + } + activePage = selectButton.getIndex(); + selectButton.setToggled(true); + } + + @Override + public List getButtons() { + List clickableWidgets = new ArrayList<>(collectionSelectButtons); + for (ProfileViewerPage page : collections) { + if (page != null && page.getButtons() != null) clickableWidgets.addAll(page.getButtons()); + } + return clickableWidgets; + } + + @Override + public void markWidgetsAsVisible() { + for (SubPageSelectButton button : collectionSelectButtons) { + button.visible = true; + button.active = true; + } + } + + @Override + public void markWidgetsAsInvisible() { + for (SubPageSelectButton button : collectionSelectButtons) { + button.visible = false; + button.active = false; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/GenericCategory.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/GenericCategory.java new file mode 100644 index 0000000000..ef26332e51 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/GenericCategory.java @@ -0,0 +1,136 @@ +package de.hysky.skyblocker.skyblock.profileviewer.collections; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.NEURepoManager; +import io.github.moulberry.repo.data.NEUItem; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.LoreComponent; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.tooltip.TooltipType; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.text.NumberFormat; +import java.util.List; +import java.util.*; + +import static de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen.fetchCollectionsData; + +public class GenericCategory implements ProfileViewerPage { + private final String category; + private final LinkedList collections = new LinkedList<>(); + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final NumberFormat FORMATTER = NumberFormat.getInstance(Locale.US); + private static final Identifier BUTTON_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/button_icon_toggled.png"); + private static final int COLUMN_GAP = 26; + private static final int ROW_GAP = 34; + private static final int COLUMNS = 7; + + private final Map collectionsMap; + private final Map> tierRequirementsMap; + private final Map ICON_TRANSLATION = Map.ofEntries( + Map.entry("MUSHROOM_COLLECTION", "RED_MUSHROOM")); + private final String[] ROMAN_NUMERALS = {"-", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII", "XIII", "XIV", "XV", "XVI", "XVII", "XVIII", "XIX", "XX"}; + + public GenericCategory(JsonObject hProfile, JsonObject pProfile, String collection) { + Map> fetchedData = fetchCollectionsData(); + //noinspection unchecked + collectionsMap = (Map) fetchedData.get("COLLECTIONS"); + //noinspection unchecked + tierRequirementsMap = (Map>) fetchedData.get("TIER_REQS"); + this.category = collection; + setupItemStacks(hProfile, pProfile); + } + + private int calculateTier(int achieved, List requirements) { + return (int) requirements.stream().filter(req -> achieved >= req).count(); + } + + private void setupItemStacks(JsonObject hProfile, JsonObject pProfile) { + JsonObject playerCollection = pProfile.getAsJsonObject("collection"); + + for (String collection : collectionsMap.get(this.category)) { + Map items = NEURepoManager.NEU_REPO.getItems().getItems(); + ItemStack itemStack = items.values().stream() + .filter(i -> Formatting.strip(i.getSkyblockItemId()).equals(ICON_TRANSLATION.getOrDefault(collection, collection).replace(':', '-'))) + .findFirst() + .map(NEUItem::getSkyblockItemId) + .map(ItemRepository::getItemStack) + .map(ItemStack::copy) + .orElse(Ico.BARRIER.copy()); + + if (itemStack.getItem().getName().getString().equals("Barrier")) itemStack.set(DataComponentTypes.ITEM_NAME, Text.of(collection)); + + int personalColl = playerCollection != null && playerCollection.has(collection) ? playerCollection.get(collection).getAsInt() : 0; + + int coopColl = 0; + for (String member : hProfile.get("members").getAsJsonObject().keySet()) { + if (!hProfile.getAsJsonObject("members").getAsJsonObject(member).has("collection")) continue; + JsonObject memberColl = hProfile.getAsJsonObject("members").getAsJsonObject(member).getAsJsonObject("collection"); + coopColl += memberColl.has(collection) ? memberColl.get(collection).getAsInt() : 0; + } + + int collectionTier = calculateTier(coopColl, tierRequirementsMap.get(collection)); + List tierRequirements = tierRequirementsMap.get(collection); + + List lore = new ArrayList<>(); + Style style = Style.EMPTY.withItalic(false); + lore.add(Text.literal("Collection: " + FORMATTER.format(personalColl)).setStyle(style).formatted(Formatting.YELLOW)); + if (hProfile.get("members").getAsJsonObject().keySet().size() > 1) { + lore.add(Text.literal("Co-op Collection: " + FORMATTER.format(coopColl)).setStyle(style).formatted(Formatting.AQUA)); + } + lore.add(Text.literal("Collection Tier: " + collectionTier).setStyle(style).formatted(Formatting.LIGHT_PURPLE)); + itemStack.set(DataComponentTypes.LORE, new LoreComponent(lore)); + + if (collectionTier == tierRequirements.size()) itemStack.set(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, true); + + collections.add(itemStack); + } + } + + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + Text categoryTitle = Text.literal(category.charAt(0) + category.substring(1).toLowerCase() + " Collections").formatted(Formatting.BOLD); + context.drawText(textRenderer, categoryTitle, rootX + 88 - (textRenderer.getWidth(categoryTitle) / 2), rootY, Color.DARK_GRAY.getRGB(), false); + + for (int i = 0; i < collections.size(); i++) { + int x = rootX + 2 + (i % COLUMNS) * COLUMN_GAP; + int y = rootY + 19 + (i / COLUMNS) * ROW_GAP; + + context.fill(x - 3, y - 3, x + 19, y + 19, Color.BLACK.getRGB()); + context.drawTexture(BUTTON_TEXTURE, x - 2, y - 2, 0, 0, 20, 20, 20, 20); + context.drawItem(collections.get(i), x, y); + + ItemStack itemStack = collections.get(i); + List lore = itemStack.getOrDefault(DataComponentTypes.LORE, LoreComponent.DEFAULT).lines(); + for (Text text : lore) { + if (!text.getString().startsWith("Collection Tier: ")) continue; + int cTier = Integer.parseInt(text.getString().substring("Collection Tier: ".length())); + Color colour = Boolean.TRUE.equals(itemStack.get(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE)) ? Color.MAGENTA : Color.darkGray; + context.drawText(textRenderer, Text.literal(toRomanNumerals(cTier)), x + 9 - (textRenderer.getWidth(toRomanNumerals(cTier)) / 2), y + 21, colour.getRGB(), false); + break; + } + + if (mouseX > x && mouseX < x + 16 && mouseY > y && mouseY < y + 16) { + List tooltip = collections.get(i).getTooltip(Item.TooltipContext.DEFAULT, MinecraftClient.getInstance().player, TooltipType.BASIC); + context.drawTooltip(textRenderer, tooltip, mouseX, mouseY); + } + } + } + + private String toRomanNumerals(int number) { + return number <= ROMAN_NUMERALS.length ? ROMAN_NUMERALS[number] : "Err"; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonClassWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonClassWidget.java new file mode 100644 index 0000000000..3b847b1b5c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonClassWidget.java @@ -0,0 +1,62 @@ +package de.hysky.skyblocker.skyblock.profileviewer.dungeons; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.Map; + +public class DungeonClassWidget { + private final String className; + private LevelFinder.LevelInfo classLevel; + private static final int CLASS_CAP = 50; + private JsonObject classData; + private final ItemStack stack; + private boolean active = false; + + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/icon_data_widget.png"); + private static final Identifier ACTIVE_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/item_protection.png"); + private static final Identifier BAR_FILL = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_fill"); + private static final Identifier BAR_BACK = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_back"); + + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final Map CLASS_ICON = Map.ofEntries( + Map.entry("Healer", Ico.S_POTION), + Map.entry("Mage", Ico.B_ROD), + Map.entry("Berserk", Ico.IRON_SWORD), + Map.entry("Archer", Ico.BOW), + Map.entry("Tank", Ico.CHESTPLATE) + ); + + public DungeonClassWidget(String className, JsonObject playerProfile) { + this.className = className; + stack = CLASS_ICON.getOrDefault(className, Ico.BARRIER); + try { + classData = playerProfile.getAsJsonObject("dungeons").getAsJsonObject("player_classes").getAsJsonObject(this.className.toLowerCase()); + classLevel = LevelFinder.getLevelInfo("Catacombs", classData.get("experience").getAsLong()); + active = playerProfile.getAsJsonObject("dungeons").get("selected_dungeon_class").getAsString().equals(className.toLowerCase()); + } catch (Exception ignored) { + classLevel = LevelFinder.getLevelInfo("", 0); + } + } + + public void render(DrawContext context, int x, int y) { + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 26, 109, 26); + context.drawItem(stack, x + 3, y + 5); + if (active) context.drawTexture(ACTIVE_TEXTURE, x + 3, y + 5, 0, 0, 16, 16, 16, 16); + + context.drawText(textRenderer, className + " " + classLevel.level, x + 31, y + 5, Color.WHITE.getRGB(), false); + Color fillColor = classLevel.level >= CLASS_CAP ? Color.MAGENTA : Color.GREEN; + context.drawGuiTexture(BAR_BACK, x + 30, y + 15, 75, 6); + RenderHelper.renderNineSliceColored(context, BAR_FILL, x + 30, y + 15, (int) (75 * classLevel.fill), 6, fillColor); + } + +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonFloorRunsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonFloorRunsWidget.java new file mode 100644 index 0000000000..7c9206c0b7 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonFloorRunsWidget.java @@ -0,0 +1,55 @@ +package de.hysky.skyblocker.skyblock.profileviewer.dungeons; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.Map; + +public class DungeonFloorRunsWidget { + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/dungeons_body.png"); + + private static final String[] DUNGEONS = {"catacombs", "master_catacombs"}; + private JsonObject dungeonsStats; + + public DungeonFloorRunsWidget(JsonObject pProfile) { + try { + dungeonsStats = pProfile.getAsJsonObject("dungeons").getAsJsonObject("dungeon_types"); + } catch (Exception ignored) {} + } + + // TODO: Hovering on each floor should probably showcase best run times in a tooltip + public void render(DrawContext context, int x, int y) { + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 110, 109, 110); + context.drawText(textRenderer, Text.literal("Floor Runs").formatted(Formatting.BOLD), x + 6, y + 4, Color.WHITE.getRGB(), true); + + int columnX = x + 4; + int elementY = y + 15; + for (String dungeon : DUNGEONS) { + JsonObject dungeonData; + try { + dungeonData = dungeonsStats.getAsJsonObject(dungeon).getAsJsonObject(dungeon.equals("catacombs") ? "times_played" : "tier_completions"); + for (Map.Entry entry : dungeonData.entrySet()) { + if (entry.getKey().equals("total")) continue; + + String textToRender = String.format((dungeon.equals("catacombs") ? "§aF" : "§cM") + "%s§r %s", entry.getKey(), entry.getValue().getAsInt()); + context.drawText(textRenderer, textToRender, columnX + 2, elementY + 2, Color.WHITE.getRGB(), true); + + elementY += 11; + } + columnX += 52; + elementY = y + 26; + } catch (Exception e) { + return; + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonHeaderWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonHeaderWidget.java new file mode 100644 index 0000000000..1a62a47be4 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonHeaderWidget.java @@ -0,0 +1,46 @@ +package de.hysky.skyblocker.skyblock.profileviewer.dungeons; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.text.DecimalFormat; + +public class DungeonHeaderWidget { + private LevelFinder.LevelInfo classLevel; + private float classAvg; + + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final DecimalFormat DF = new DecimalFormat("#.##"); + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/dungeons_header.png"); + + public DungeonHeaderWidget(JsonObject playerProfile, String[] classes) { + try { + JsonObject DUNGEONS_PROFILE = playerProfile.getAsJsonObject("dungeons").getAsJsonObject("dungeon_types").getAsJsonObject("catacombs"); + this.classLevel = LevelFinder.getLevelInfo("Catacombs", DUNGEONS_PROFILE.get("experience").getAsLong()); + + float avg = 0; + JsonObject CLASS_DATA = playerProfile.getAsJsonObject("dungeons").getAsJsonObject("player_classes"); + for (String element : classes) { + avg += LevelFinder.getLevelInfo("Catacombs", CLASS_DATA.getAsJsonObject(element.toLowerCase()).get("experience").getAsLong()).level; + } + classAvg = avg/classes.length; + } catch (Exception ignored) { + this.classLevel = LevelFinder.getLevelInfo("", 0); + classAvg = 0; + } + } + + public void render(DrawContext context, int x, int y) { + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 26, 109, 26); + + context.drawText(textRenderer, "§i§6§lCatacombs §r" + this.classLevel.level, x + 3, y + 4, Color.WHITE.getRGB(), true); + + context.drawText(textRenderer, "§eClass Average §r" + DF.format(this.classAvg), x + 3, y + 14, Color.WHITE.getRGB(), true); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonMiscStatsWidgets.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonMiscStatsWidgets.java new file mode 100644 index 0000000000..679cc5759f --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonMiscStatsWidgets.java @@ -0,0 +1,61 @@ +package de.hysky.skyblocker.skyblock.profileviewer.dungeons; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.text.DecimalFormat; +import java.util.HashMap; +import java.util.Map; + +public class DungeonMiscStatsWidgets { + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/icon_data_widget.png"); + private static final Identifier RUN_ICON = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/run_icon.png"); + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final DecimalFormat DF = new DecimalFormat("#.##"); + private static final String[] DUNGEONS = {"catacombs", "master_catacombs"}; + + private final Map dungeonRuns = new HashMap<>(); + private int secrets = 0; + private int totalRuns = 0; + + public DungeonMiscStatsWidgets(JsonObject pProfile) { + JsonObject DUNGEONS_DATA = pProfile.getAsJsonObject("dungeons"); + try { + secrets = DUNGEONS_DATA.get("secrets").getAsInt(); + + for (String dungeon : DUNGEONS) { + JsonObject dungeonData = DUNGEONS_DATA.getAsJsonObject("dungeon_types").getAsJsonObject(dungeon).getAsJsonObject(dungeon.equals("catacombs") ? "times_played" : "tier_completions"); + int runs = 0; + for (Map.Entry entry : dungeonData.entrySet()) { + String key = entry.getKey(); + if (key.equals("total")) continue; + runs += entry.getValue().getAsInt(); + } + dungeonRuns.put(dungeon, runs); + totalRuns += runs; + } + + } catch (Exception ignored) {} + } + + public void render(DrawContext context, int x, int y) { + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 26, 109, 26); + context.drawItem(Ico.FEATHER, x + 2, y + 4); + + context.drawText(textRenderer, "Secrets " + secrets, x + 30, y + 4, Color.WHITE.getRGB(), true); + context.drawText(textRenderer, "Avg " + (totalRuns > 0 ? DF.format(secrets / (float) totalRuns) : 0) + "/Run", x + 30, y + 14, Color.WHITE.getRGB(), true); + + context.drawTexture(TEXTURE, x, y + 28, 0, 0, 109, 26, 109, 26); + context.drawTexture(RUN_ICON, x + 4, y + 33, 0, 0, 14, 16, 14, 16); + + context.drawText(textRenderer, "§aNormal §r" + dungeonRuns.getOrDefault("catacombs", 0), x + 30, y + 32, Color.WHITE.getRGB(), true); + context.drawText(textRenderer, "§cMaster §r" + dungeonRuns.getOrDefault("master_catacombs", 0), x + 30, y + 42, Color.WHITE.getRGB(), true); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonsPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonsPage.java new file mode 100644 index 0000000000..b13986616d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonsPage.java @@ -0,0 +1,39 @@ +package de.hysky.skyblocker.skyblock.profileviewer.dungeons; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.utils.ProfileUtils; +import net.minecraft.client.gui.DrawContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class DungeonsPage implements ProfileViewerPage { + public static final Logger LOGGER = LoggerFactory.getLogger(ProfileUtils.class); + private static final String[] CLASSES = {"Healer", "Mage", "Berserk", "Archer", "Tank"}; + + private final DungeonHeaderWidget dungeonHeaderWidget; + private final List dungeonClassWidgetsList = new ArrayList<>(); + private final DungeonFloorRunsWidget dungeonFloorRunsWidget; + private final DungeonMiscStatsWidgets dungeonMiscStatsWidgets; + + public DungeonsPage(JsonObject pProfile) { + dungeonHeaderWidget = new DungeonHeaderWidget(pProfile, CLASSES); + dungeonFloorRunsWidget = new DungeonFloorRunsWidget(pProfile); + dungeonMiscStatsWidgets = new DungeonMiscStatsWidgets(pProfile); + for (String element : CLASSES) { + dungeonClassWidgetsList.add(new DungeonClassWidget(element, pProfile)); + } + } + + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + dungeonHeaderWidget.render(context, rootX, rootY); + dungeonFloorRunsWidget.render(context, rootX + 113, rootY + 56); + dungeonMiscStatsWidgets.render(context, rootX + 113, rootY); + for (int i = 0; i < dungeonClassWidgetsList.size(); i++) { + dungeonClassWidgetsList.get(i).render(context, rootX, rootY + 28 + i * 28); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Inventory.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Inventory.java new file mode 100644 index 0000000000..a2f7d9d6fa --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Inventory.java @@ -0,0 +1,120 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders.ItemLoader; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.tooltip.TooltipType; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +public class Inventory implements ProfileViewerPage { + private static final Identifier TEXTURE = Identifier.of("textures/gui/container/generic_54.png"); + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private final IntIntPair dimensions; + private final int itemsPerPage; + private final List containerList; + private final String containerName; + private int activePage = 0; + private int totalPages = 1; + private final PaginationButton previousPage = new PaginationButton(this, -1000, 0, false); + private final PaginationButton nextPage = new PaginationButton(this, -1000, 0, true); + + public Inventory(String name, IntIntPair dimensions, JsonObject inventory) { + this(name, dimensions, inventory, new ItemLoader()); + } + + public Inventory(String name, IntIntPair dimensions, JsonObject inventory, ItemLoader itemLoader) { + containerName = name; + this.dimensions = dimensions; + itemsPerPage = dimensions.rightInt() * dimensions.leftInt(); + this.containerList = itemLoader.loadItems(inventory); + this.totalPages = (int) Math.ceil((double) containerList.size() / itemsPerPage); + } + + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + int rootYAdjusted = rootY + (26 - dimensions.leftInt() * 3); + context.drawTexture(TEXTURE, rootX, rootYAdjusted, 0, 0, dimensions.rightInt() * 18 + 7, dimensions.leftInt() * 18 + 17); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootYAdjusted, 169, 0, 7, dimensions.leftInt() * 18 + 17); + context.drawTexture(TEXTURE, rootX, rootYAdjusted + dimensions.leftInt() * 18 + 17, 0, 215, dimensions.rightInt() * 18 + 7, 7); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootYAdjusted + dimensions.leftInt() * 18 + 17, 169, 215, 7, 7); + + context.drawText(textRenderer, containerName, rootX + 7, rootYAdjusted + 7, Color.DARK_GRAY.getRGB(), false); + + if (containerList.size() > itemsPerPage) { + previousPage.setX(rootX + 44); + previousPage.setY(rootY + 136); + previousPage.render(context, mouseX, mouseY, delta); + + context.drawCenteredTextWithShadow(textRenderer, "Page: " + (activePage + 1) + "/" + totalPages, rootX + 88, rootY + 140, Color.WHITE.getRGB()); + + nextPage.setX(rootX + 121); + nextPage.setY(rootY + 136); + nextPage.render(context, mouseX, mouseY, delta); + } + + int startIndex = activePage * itemsPerPage; + int endIndex = Math.min(startIndex + itemsPerPage, containerList.size()); + for (int i = 0; i < endIndex - startIndex; i++) { + if (containerList.get(startIndex + i) == ItemStack.EMPTY) continue; + int column = i % dimensions.rightInt(); + int row = i / dimensions.rightInt(); + + int x = rootX + 8 + column * 18; + int y = rootYAdjusted + 18 + row * 18; + context.drawItem(containerList.get(startIndex + i), x, y); + context.drawItemInSlot(textRenderer, containerList.get(startIndex + i), x, y); + + if (mouseX > x && mouseX < x + 16 && mouseY > y && mouseY < y + 16) { + List tooltip = containerList.get(startIndex + i).getTooltip(Item.TooltipContext.DEFAULT, MinecraftClient.getInstance().player, TooltipType.BASIC); + context.drawTooltip(textRenderer, tooltip, mouseX, mouseY); + } + } + } + + public void nextPage() { + if (activePage < totalPages - 1) { + activePage++; + } + } + + public void previousPage() { + if (activePage > 0) { + activePage--; + } + } + + @Override + public void markWidgetsAsVisible() { + nextPage.visible = true; + previousPage.visible = true; + nextPage.active = true; + previousPage.active = true; + } + + @Override + public void markWidgetsAsInvisible() { + nextPage.visible = false; + previousPage.visible = false; + nextPage.active = false; + previousPage.active = false; + } + + @Override + public List getButtons() { + List buttons = new ArrayList<>(); + buttons.add(nextPage); + buttons.add(previousPage); + return buttons; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/InventoryPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/InventoryPage.java new file mode 100644 index 0000000000..8b0cbefc29 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/InventoryPage.java @@ -0,0 +1,113 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders.BackpackItemLoader; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders.PetsInventoryItemLoader; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders.WardrobeInventoryItemLoader; +import de.hysky.skyblocker.skyblock.profileviewer.utils.SkullCreator; +import de.hysky.skyblocker.skyblock.profileviewer.utils.SubPageSelectButton; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.item.ItemStack; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class InventoryPage implements ProfileViewerPage { + private static final String[] INVENTORY_PAGES = {"Inventory", "Enderchest", "Backpack", "Wardrobe", "Pets", "Accessory Bag"}; + private static final int TOTAL_HEIGHT = 165; + private static final Map ICON_MAP = Map.ofEntries( + Map.entry("Wardrobe", Ico.L_CHESTPLATE), + Map.entry("Inventory", Ico.CHEST), + Map.entry("Backpack", SkullCreator.createSkull("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHBzOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzYyZjNiM2EwNTQ4MWNkZTc3MjQwMDA1YzBkZGNlZTFjMDY5ZTU1MDRhNjJjZTA5Nzc4NzlmNTVhMzkzOTYxNDYifX19")), + Map.entry("Pets", Ico.BONE), + Map.entry("Enderchest", Ico.E_CHEST), + Map.entry("Accessory Bag", SkullCreator.createSkull("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTYxYTkxOGMwYzQ5YmE4ZDA1M2U1MjJjYjkxYWJjNzQ2ODkzNjdiNGQ4YWEwNmJmYzFiYTkxNTQ3MzA5ODVmZiJ9fX0=")) + ); + + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private final ProfileViewerPage[] inventorySubPages = new ProfileViewerPage[6]; + private final List inventorySelectButtons = new ArrayList<>(); + private int activePage = 0; + + public InventoryPage(JsonObject pProfile) { + for (int i = 0; i < INVENTORY_PAGES.length; i++) { + inventorySelectButtons.add(new SubPageSelectButton(this, -100, 0, i, ICON_MAP.getOrDefault(INVENTORY_PAGES[i], Ico.BARRIER))); + } + + try { + JsonObject inventoryData = pProfile.getAsJsonObject("inventory"); + if (inventoryData == null) return; + inventorySubPages[0] = new PlayerInventory(inventoryData); + inventorySubPages[1] = new Inventory(INVENTORY_PAGES[1], IntIntPair.of(5, 9), inventoryData.getAsJsonObject("ender_chest_contents")); + inventorySubPages[2] = new Inventory(INVENTORY_PAGES[2], IntIntPair.of(5, 9), inventoryData.getAsJsonObject("backpack_contents"), new BackpackItemLoader()); + inventorySubPages[3] = new Inventory(INVENTORY_PAGES[3], IntIntPair.of(4, 9), inventoryData.getAsJsonObject("wardrobe_contents"), new WardrobeInventoryItemLoader(inventoryData)); + inventorySubPages[4] = new Inventory(INVENTORY_PAGES[4], IntIntPair.of(4, 9), pProfile, new PetsInventoryItemLoader()); + inventorySubPages[5] = new Inventory(INVENTORY_PAGES[5], IntIntPair.of(5, 9), inventoryData.getAsJsonObject("bag_contents").getAsJsonObject("talisman_bag")); + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Error while loading inventory data: ", e); + } + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + int startingY = rootY + (TOTAL_HEIGHT - inventorySelectButtons.size() * 21) / 2; + for (int i = 0; i < inventorySelectButtons.size(); i++) { + inventorySelectButtons.get(i).setX(rootX); + inventorySelectButtons.get(i).setY(startingY + i * 21); + inventorySelectButtons.get(i).render(context, mouseX, mouseY, delta); + } + + if (inventorySubPages[activePage] == null) { + context.drawText(textRenderer, "No data...", rootX + 92, rootY + 72, Color.DARK_GRAY.getRGB(), false); + return; + } + + inventorySubPages[activePage].markWidgetsAsVisible(); + inventorySubPages[activePage].render(context, mouseX, mouseY, delta, rootX + 35, rootY + 6); + } + + public void onNavButtonClick(SubPageSelectButton clickedButton) { + if (inventorySubPages[activePage] != null) inventorySubPages[activePage].markWidgetsAsInvisible(); + for (SubPageSelectButton button : inventorySelectButtons) { + button.setToggled(false); + } + activePage = clickedButton.getIndex(); + clickedButton.setToggled(true); + } + + @Override + public List getButtons() { + List clickableWidgets = new ArrayList<>(inventorySelectButtons); + for (ProfileViewerPage page : inventorySubPages) { + if (page != null && page.getButtons() != null) clickableWidgets.addAll(page.getButtons()); + } + return clickableWidgets; + } + + @Override + public void markWidgetsAsVisible() { + if (inventorySubPages[activePage] != null) inventorySubPages[activePage].markWidgetsAsVisible(); + for (SubPageSelectButton button : inventorySelectButtons) { + button.visible = true; + button.active = true; + } + } + + @Override + public void markWidgetsAsInvisible() { + if (inventorySubPages[activePage] != null) inventorySubPages[activePage].markWidgetsAsInvisible(); + for (SubPageSelectButton button : inventorySelectButtons) { + button.visible = false; + button.active = false; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PaginationButton.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PaginationButton.java new file mode 100644 index 0000000000..1a725e1aaa --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PaginationButton.java @@ -0,0 +1,46 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory; + +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +public class PaginationButton extends ClickableWidget { + private final ProfileViewerPage screen; + private final boolean isNextButton; + private final Identifier TEXTURE; + private final Identifier HIGHLIGHT; + + public PaginationButton(ProfileViewerPage screen, int x, int y, boolean isNextButton) { + super(x, y, 12, 17, Text.empty()); + this.screen = screen; + this.isNextButton = isNextButton; + if (isNextButton) { + TEXTURE = Identifier.of("minecraft", "textures/gui/sprites/recipe_book/page_forward.png"); + HIGHLIGHT = Identifier.of("minecraft", "textures/gui/sprites/recipe_book/page_forward_highlighted.png"); + } else { + TEXTURE = Identifier.of("minecraft", "textures/gui/sprites/recipe_book/page_backward.png"); + HIGHLIGHT = Identifier.of("minecraft", "textures/gui/sprites/recipe_book/page_backward_highlighted.png"); + } + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawTexture(TEXTURE, this.getX(), this.getY(), 0, 0, 12, 17, 12, 17); + if (isMouseOver(mouseX, mouseY)) context.drawTexture(HIGHLIGHT, this.getX(), this.getY(), 0, 0, 12, 17, 12, 17); + } + + @Override + public void onClick(double mouseX, double mouseY) { + if (isNextButton) { + screen.nextPage(); + } else { + screen.previousPage(); + } + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Pet.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Pet.java new file mode 100644 index 0000000000..b3389d3920 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Pet.java @@ -0,0 +1,186 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory; + +import de.hysky.skyblocker.skyblock.PetCache; +import de.hysky.skyblocker.skyblock.itemlist.ItemFixerUpper; +import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.NEURepoManager; +import io.github.moulberry.repo.constants.PetNumbers; +import io.github.moulberry.repo.data.NEUItem; +import io.github.moulberry.repo.data.Rarity; +import io.github.moulberry.repo.util.PetId; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.LoreComponent; +import net.minecraft.component.type.ProfileComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtString; +import net.minecraft.registry.Registries; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import net.minecraft.util.Pair; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.regex.Matcher; +import java.util.stream.Collectors; + +import static de.hysky.skyblocker.skyblock.itemlist.ItemStackBuilder.SKULL_TEXTURE_PATTERN; +import static de.hysky.skyblocker.skyblock.itemlist.ItemStackBuilder.SKULL_UUID_PATTERN; + +public class Pet { + private final String name; + private final double xp; + private final String tier; + private final Optional heldItem; + private final int level; + private final ItemStack icon; + + private static final Map TIER_MAP = Map.of( + "COMMON", 0, "UNCOMMON", 1, "RARE", 2, "EPIC", 3, "LEGENDARY", 4, "MYTHIC", 5 + ); + + public Pet(PetCache.PetInfo petData) { + this.name = petData.type(); + this.xp = petData.exp(); + this.heldItem = petData.item(); + if ((heldItem.isPresent() && heldItem.get().equals("PET_ITEM_TIER_BOOST"))) { + this.tier = switch (petData.tier()) { + case "COMMON" -> "UNCOMMON"; + case "UNCOMMON" -> "RARE"; + case "RARE" -> "EPIC"; + case "EPIC" -> "LEGENDARY"; + case "LEGENDARY" -> "MYTHIC"; + default -> petData.tier(); + }; + } else { + this.tier = petData.tier(); + } + this.level = LevelFinder.getLevelInfo(this.name.equals("GOLDEN_DRAGON") ? "PET_GREG" : "PET_" + this.tier, (long) xp).level; + this.icon = createIcon(); + } + + public String getName() { return name; } + public long getXP() { return (long) xp; } + public int getTier() { return TIER_MAP.getOrDefault(tier, 0); } + public String getTierAsString() { return tier; } + public String getSkin() { return null; } + public int getLevel() { return level; } + public ItemStack getIcon() { return icon; } + + + private ItemStack createIcon() { + if (NEURepoManager.isLoading() || !ItemRepository.filesImported()) return Ico.BARRIER; + Map items = NEURepoManager.NEU_REPO.getItems().getItems(); + if (items == null) return Ico.BARRIER; + + String targetItemId = this.getName() + ";" + this.getTier(); + NEUItem item = items.values().stream() + .filter(i -> Formatting.strip(i.getSkyblockItemId()).equals(targetItemId)) + .findFirst().orElse(null); + + NEUItem petItem = null; + if (this.heldItem.isPresent()) { + petItem = items.values().stream() + .filter(i -> Formatting.strip(i.getSkyblockItemId()).equals(this.heldItem.get())) + .findFirst().orElse(null); + } + + return fromNEUItem(item, petItem); + } + + /** + * Converts NEU item data into an ItemStack. + *

This method converts NEU item data into a Pet by using the placeholder + * information from NEU-REPO and injecting the player's calculated pet stats into the lore and transforming + * the NBT Data into modern DataComponentTypes before returning the final ItemStack

> injectors = new ArrayList<>(createLoreReplacers(item.getSkyblockItemId(), helditem)); + Identifier itemId = Identifier.of(ItemFixerUpper.convertItemId(item.getMinecraftItemId(), item.getDamage())); + ItemStack stack = new ItemStack(Registries.ITEM.get(itemId)); + + NbtCompound customData = new NbtCompound(); + customData.put(ItemUtils.ID, NbtString.of(item.getSkyblockItemId())); + stack.set(DataComponentTypes.CUSTOM_NAME, Text.of(injectData(item.getDisplayName(), injectors))); + + stack.set(DataComponentTypes.LORE, new LoreComponent( + item.getLore().stream().map(line -> injectData(line, injectors)) + .filter(line -> !line.contains("SKIP")).map(Text::of) + .collect(Collectors.toList()))); + + Matcher skullUuid = SKULL_UUID_PATTERN.matcher(item.getNbttag()); + Matcher skullTexture = SKULL_TEXTURE_PATTERN.matcher(item.getNbttag()); + if (skullUuid.find() && skullTexture.find()) { + UUID uuid = UUID.fromString(skullUuid.group(1)); + String textureValue = this.getSkin() == null ? skullTexture.group(1) : this.getSkin(); + stack.set(DataComponentTypes.PROFILE, new ProfileComponent( + Optional.of(item.getSkyblockItemId()), Optional.of(uuid), + ItemUtils.propertyMapWithTexture(textureValue))); + } + return stack; + } + + /** + * Generates a list of placeholder-replacement pairs for the itemName of a pet item. + *

This method uses the pet's data from the NEU repository and uses PetInfo to generate replacers, and optionally + * includes data about a held item.

+ * + * @param itemSkyblockID The initial itemName string containing the pet's name and tier separated by a semicolon. + * @param helditem The NEUItem representing the held item, if any. + * @return A list of placeholder-replacement pairs to be used for injecting data into the pet item's itemName. + */ + private List> createLoreReplacers(String itemSkyblockID, NEUItem helditem) { + List> list = new ArrayList<>(); + Map<@PetId String, Map> petNums = NEURepoManager.NEU_REPO.getConstants().getPetNumbers(); + String petName = itemSkyblockID.split(";")[0]; + if (!itemSkyblockID.contains(";") || !petNums.containsKey(petName)) return list; + + Rarity rarity = Rarity.values()[Integer.parseInt(itemSkyblockID.split(";")[1])]; + try { + PetNumbers data = petNums.get(petName).get(rarity); + list.add(new Pair<>("\\{LVL\\}", String.valueOf(this.level))); + data.interpolatedStatsAtLevel(this.level).getStatNumbers().forEach((key, value) -> + list.add(new Pair<>("\\{" + key + "\\}", fixDecimals(value, true)))); + + List otherNumsMin = data.interpolatedStatsAtLevel(this.level).getOtherNumbers(); + for (int i = 0; i < otherNumsMin.size(); ++i) { + list.add(new Pair<>("\\{" + i + "\\}", fixDecimals(otherNumsMin.get(i), false))); + } + + list.add(new Pair<>("Right-click to add this pet to", + helditem != null ? "§r§6Held Item: " + helditem.getDisplayName() : "SKIP")); + list.add(new Pair<>("pet menu!", "SKIP")); + } catch (Exception e) { + if (petName.equals("GOLDEN_DRAGON")) { + list.add(new Pair<>("Golden Dragon", + "§r§7[Lvl " + this.level + "] " + "§6Golden Dragon Egg §c[Not Supported by NEU-Repo]")); + } + } + return list; + } + + private String injectData(String string, List> injectors) { + for (Pair injector : injectors) { + if (string.contains(injector.getLeft())) return injector.getRight(); + string = string.replaceAll(injector.getLeft(), injector.getRight()); + } + return string; + } + + private String fixDecimals(double num, boolean truncate) { + if (num % 1 == 0) return String.valueOf((int) num); + BigDecimal roundedNum = new BigDecimal(num).setScale(3, RoundingMode.HALF_UP); + return truncate && num > 1 ? String.valueOf(roundedNum.intValue()) + : roundedNum.stripTrailingZeros().toPlainString(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PlayerInventory.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PlayerInventory.java new file mode 100644 index 0000000000..2667369316 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PlayerInventory.java @@ -0,0 +1,73 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders.InventoryItemLoader; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.tooltip.TooltipType; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.List; + +public class PlayerInventory implements ProfileViewerPage { + private static final Identifier TEXTURE = Identifier.of("textures/gui/container/generic_54.png"); + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private final List containerList; + + public PlayerInventory(JsonObject inventory) { + this.containerList = new InventoryItemLoader().loadItems(inventory); + } + + // Z-STACKING forces this nonsense of separating the Background texture and Item Drawing :( + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + drawContainerTextures(context, "Armour", rootX, rootY + 108, IntIntPair.of(1, 4)); + drawContainerTextures(context, "Inventory", rootX, rootY + 2, IntIntPair.of(4, 9)); + drawContainerTextures(context, "Equipment", rootX + 90, rootY + 108, IntIntPair.of(1, 4)); + + drawContainerItems(context, rootX, rootY + 108, IntIntPair.of(1, 4), 36, 40, mouseX, mouseY); + drawContainerItems(context, rootX, rootY + 2, IntIntPair.of(4, 9), 0, 36, mouseX, mouseY); + drawContainerItems(context, rootX + 90, rootY + 108, IntIntPair.of(1, 4), 40, containerList.size(), mouseX, mouseY); + } + + private void drawContainerTextures(DrawContext context, String containerName, int rootX, int rootY, IntIntPair dimensions) { + if (containerName.equals("Inventory")) { + context.drawTexture(TEXTURE, rootX, rootY + dimensions.leftInt() + 10, 0, 136, dimensions.rightInt() * 18 + 7, dimensions.leftInt() * 18 + 17); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootY, 169, 0, 7, dimensions.leftInt() * 18 + 21); + context.drawTexture(TEXTURE, rootX, rootY, 0, 0, dimensions.rightInt() * 18 + 7, 14); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootY + dimensions.leftInt() * 18 + 21, 169, 215, 7, 7); + } else { + context.drawTexture(TEXTURE, rootX, rootY, 0, 0, dimensions.rightInt() * 18 + 7, dimensions.leftInt() * 18 + 17); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootY, 169, 0, 7, dimensions.leftInt() * 18 + 17); + context.drawTexture(TEXTURE, rootX, rootY + dimensions.leftInt() * 18 + 17, 0, 215, dimensions.rightInt() * 18 + 7, 7); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootY + dimensions.leftInt() * 18 + 17, 169, 215, 7, 7); + } + + context.drawText(textRenderer, containerName, rootX + 7, rootY + 7, Color.DARK_GRAY.getRGB(), false); + } + + private void drawContainerItems(DrawContext context, int rootX, int rootY, IntIntPair dimensions, int startIndex, int endIndex, int mouseX, int mouseY) { + for (int i = 0; i < endIndex - startIndex; i++) { + if (containerList.get(startIndex + i) == ItemStack.EMPTY) continue; + int column = i % dimensions.rightInt(); + int row = i / dimensions.rightInt(); + + int x = rootX + 8 + column * 18; + int y = (rootY + 18 + row * 18) + (dimensions.leftInt() > 1 && row + 1 == dimensions.leftInt() ? 4 : 0); + + context.drawItem(containerList.get(startIndex + i), x, y); + context.drawItemInSlot(textRenderer, containerList.get(startIndex + i), x, y); + + if (mouseX > x && mouseX < x + 16 && mouseY > y && mouseY < y + 16) { + List tooltip = containerList.get(startIndex + i).getTooltip(Item.TooltipContext.DEFAULT, MinecraftClient.getInstance().player, TooltipType.BASIC); + context.drawTooltip(textRenderer, tooltip, mouseX, mouseY); + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/BackpackItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/BackpackItemLoader.java new file mode 100644 index 0000000000..99e728beb6 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/BackpackItemLoader.java @@ -0,0 +1,34 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import net.minecraft.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class BackpackItemLoader extends ItemLoader { + @Override + public List loadItems(JsonObject data) { + List backpackItems = new ArrayList<>(); + + // Sort the data by keys numerically + List> sortedEntries = data.entrySet().stream() + .sorted((e1, e2) -> { + int key1 = Integer.parseInt(e1.getKey()); + int key2 = Integer.parseInt(e2.getKey()); + return Integer.compare(key1, key2); + }).toList(); + + for (int i = 0; i < sortedEntries.size(); i++) { + backpackItems.addAll(super.loadItems(sortedEntries.get(i).getValue().getAsJsonObject())); + int padding = (i + 1) * 45 % (backpackItems.isEmpty() ? 1 : backpackItems.size()); + for (int j = 0; j < padding; j++) { + backpackItems.add(ItemStack.EMPTY); + } + } + + return backpackItems; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java new file mode 100644 index 0000000000..900fcc00ee --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java @@ -0,0 +1,30 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders; + +import com.google.gson.JsonObject; +import net.minecraft.item.ItemStack; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class InventoryItemLoader extends ItemLoader { + + private static final String[] INVENTORIES = {"inv_contents", "inv_armor", "equipment_contents"}; + + @Override + public List loadItems(JsonObject data) { + List inventoryItems = new ArrayList<>(); + for (String inventory : INVENTORIES) { + List inv = super.loadItems(data.getAsJsonObject(inventory)); + switch (inventory) { + case "inv_armor" -> inventoryItems.addAll(inv.reversed()); + case "inv_contents" -> { + inventoryItems.addAll(inv.subList(9,inv.size())); + inventoryItems.addAll(inv.subList(0, 9)); + } + default -> inventoryItems.addAll(inv); + } + } + return inventoryItems; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/ItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/ItemLoader.java new file mode 100644 index 0000000000..9d9b1b0770 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/ItemLoader.java @@ -0,0 +1,139 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; +import de.hysky.skyblocker.skyblock.PetCache; +import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.Pet; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.NEURepoManager; +import io.github.moulberry.repo.data.NEUItem; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.AttributeModifiersComponent; +import net.minecraft.component.type.DyedColorComponent; +import net.minecraft.component.type.LoreComponent; +import net.minecraft.component.type.ProfileComponent; +import net.minecraft.datafixer.fix.ItemIdFix; +import net.minecraft.datafixer.fix.ItemInstanceTheFlatteningFix; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.*; +import net.minecraft.registry.Registries; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import java.io.ByteArrayInputStream; +import java.util.*; +import java.util.stream.Collectors; + +import static de.hysky.skyblocker.skyblock.itemlist.ItemRepository.getItemStack; + +public class ItemLoader { + + public List loadItems(JsonObject data) { + NbtList containerContent = decompress(data); + List itemList = new ArrayList<>(); + + for (int i = 0; i < containerContent.size(); i++) { + if (containerContent.getCompound(i).getInt("id") == 0) { + itemList.add(ItemStack.EMPTY); + continue; + } + + NbtCompound nbttag = containerContent.getCompound(i).getCompound("tag"); + String internalName = nbttag.getCompound("ExtraAttributes").getString("id"); + if (internalName.equals("PET")) { + NbtCompound extraAttributes = nbttag .getCompound("ExtraAttributes"); + PetCache.PetInfo petInfo = PetCache.PetInfo.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(extraAttributes.getString("petInfo"))).getOrThrow(); + Pet pet = new Pet(petInfo); + itemList.add(pet.getIcon()); + continue; + } + + Identifier itemId = identifierFromOldId(containerContent.getCompound(i).getInt("id"), containerContent.getCompound(i).getInt("Damage")); + ItemStack stack = itemId.toString().equals("minecraft:air") ? getItemStack(internalName) : new ItemStack(Registries.ITEM.get(itemId)); + + if (stack == null || stack.isEmpty() || stack.getItem().equals(Ico.BARRIER.getItem())) { + // Last ditch effort to find item in NEU REPO + Map items = NEURepoManager.NEU_REPO.getItems().getItems(); + stack = items.values().stream() + .filter(j -> Formatting.strip(j.getSkyblockItemId()).equals(Formatting.strip(internalName).replace(":", "-"))) + .findFirst() + .map(NEUItem::getSkyblockItemId) + .map(ItemRepository::getItemStack) + .orElse(Ico.BARRIER.copy()); + + + if (stack.getName().getString().contains("barrier")) { + stack.set(DataComponentTypes.CUSTOM_NAME, Text.literal("Err: " + internalName)); + itemList.add(stack); + continue; + } + } + + // Custom Data + NbtCompound customData = new NbtCompound(); + + // Add Skyblock Item Id + customData.put(ItemUtils.ID, NbtString.of(internalName)); + + + // Item Name + stack.set(DataComponentTypes.CUSTOM_NAME, Text.of(nbttag.getCompound("display").getString("Name"))); + + // Lore + NbtList loreList = nbttag.getCompound("display").getList("Lore", 8); + stack.set(DataComponentTypes.LORE, new LoreComponent(loreList.stream() + .map(NbtElement::asString) + .map(Text::literal) + .collect(Collectors.toList()))); + + // add skull texture + NbtList texture = nbttag.getCompound("SkullOwner").getCompound("Properties").getList("textures", 10); + if (!texture.isEmpty()) { + stack.set(DataComponentTypes.PROFILE, new ProfileComponent(Optional.of(internalName), Optional.of(UUID.fromString(nbttag.getCompound("SkullOwner").get("Id").asString())), ItemUtils.propertyMapWithTexture(texture.getCompound(0).getString("Value")))); + } + + // Colour + if (nbttag.getCompound("display").contains("color")) { + int color = nbttag.getCompound("display").getInt("color"); + stack.set(DataComponentTypes.DYED_COLOR, new DyedColorComponent(color, false)); + } + + // add enchantment glint + if (nbttag.getKeys().contains("ench")) { + stack.set(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, true); + } + + // Hide weapon damage and other useless info + stack.set(DataComponentTypes.ATTRIBUTE_MODIFIERS, new AttributeModifiersComponent(List.of(), false)); + + // Set Count + stack.setCount(containerContent.getCompound(i).getInt("Count")); + + itemList.add(stack); + } + + return itemList; + } + + private static Identifier identifierFromOldId(int id, int damage) { + try { + return damage != 0 ? Identifier.of(ItemInstanceTheFlatteningFix.getItem(ItemIdFix.fromId(id), damage)) : Identifier.of(ItemIdFix.fromId(id)); + } catch (Exception e) { + return Identifier.of("air"); + } + } + + private static NbtList decompress(JsonObject data) { + try { + return NbtIo.readCompressed(new ByteArrayInputStream(Base64.getDecoder().decode(data.get("data").getAsString())), NbtSizeTracker.ofUnlimitedBytes()).getList("i", NbtElement.COMPOUND_TYPE); + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Failed to decompress item data", e); + } + return null; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/PetsInventoryItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/PetsInventoryItemLoader.java new file mode 100644 index 0000000000..cd3b7a2613 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/PetsInventoryItemLoader.java @@ -0,0 +1,42 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; +import de.hysky.skyblocker.skyblock.PetCache; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.Pet; +import net.minecraft.item.ItemStack; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class PetsInventoryItemLoader extends ItemLoader { + private static final List TIER_ORDER = List.of("MYTHIC", "LEGENDARY", "EPIC", "RARE", "UNCOMMON", "COMMON"); + + @Override + public List loadItems(JsonObject data) { + List petList = new ArrayList<>(); + try { + JsonObject petsData = data.getAsJsonObject("pets_data"); + if (petsData != null && petsData.has("pets")) { + for (var petElement : petsData.get("pets").getAsJsonArray()) { + PetCache.PetInfo petInfo = PetCache.PetInfo.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(petElement.toString())).getOrThrow(); + petList.add(new Pet(petInfo)); + } + } + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Failed to load pets", e); + } + + // Sort pets by tier (in reverse order) and level (in reverse order) + petList.sort(Comparator.comparingInt((Pet pet) -> TIER_ORDER.indexOf(pet.getTierAsString())).reversed().thenComparingInt(Pet::getLevel).reversed()); + + List itemList = new ArrayList<>(); + for (Pet pet : petList) { + itemList.add(pet.getIcon()); + } + return itemList; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java new file mode 100644 index 0000000000..cb3a866a33 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java @@ -0,0 +1,39 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import net.minecraft.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +public class WardrobeInventoryItemLoader extends ItemLoader { + private final int activeSlot; + private final JsonObject activeArmorSet; + + public WardrobeInventoryItemLoader(JsonObject inventory) { + this.activeSlot = inventory.get("wardrobe_equipped_slot").getAsInt(); + this.activeArmorSet = inventory.get("inv_armor").getAsJsonObject(); + } + + @Override + public List loadItems(JsonObject data) { + List itemList = new ArrayList<>(); + + try { + itemList.addAll(super.loadItems(data)); + if (activeSlot != -1) { + for (int i = 0; i < 4; i++) { + int baseIndex = activeSlot % 9; + int page = activeSlot / 9; + int slotIndex = (page * 36) + (i * 9) + baseIndex - 1; + itemList.set(slotIndex, super.loadItems(activeArmorSet).reversed().get(i)); + } + } + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Failed to load wardrobe items", e); + } + + return itemList; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillWidget.java new file mode 100644 index 0000000000..3a3870f3e3 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillWidget.java @@ -0,0 +1,95 @@ +package de.hysky.skyblocker.skyblock.profileviewer.skills; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; +import de.hysky.skyblocker.skyblock.profileviewer.utils.SkullCreator; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.Map; + +public class SkillWidget { + private final String SKILL_NAME; + private final LevelFinder.LevelInfo SKILL_LEVEL; + + private static final Identifier BAR_FILL = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_fill"); + private static final Identifier BAR_BACK = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_back"); + + private final ItemStack stack; + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final Map SKILL_LOGO = Map.ofEntries( + Map.entry("Combat", Ico.STONE_SWORD), + Map.entry("Farming", Ico.GOLDEN_HOE), + Map.entry("Mining", Ico.STONE_PICKAXE), + Map.entry("Foraging", Ico.JUNGLE_SAPLING), + Map.entry("Fishing", Ico.FISH_ROD), + Map.entry("Enchanting", Ico.ENCHANTING_TABLE), + Map.entry("Alchemy", Ico.BREWING_STAND), + Map.entry("Taming", Ico.SPAWN_EGG), + Map.entry("Carpentry", Ico.CRAFTING_TABLE), + Map.entry("Catacombs", SkullCreator.createSkull("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHBzOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzliNTY4OTViOTY1OTg5NmFkNjQ3ZjU4NTk5MjM4YWY1MzJkNDZkYjljMWIwMzg5YjhiYmViNzA5OTlkYWIzM2QifX19")), + Map.entry("Runecraft", Ico.MAGMA_CREAM), + Map.entry("Social", Ico.EMERALD) + ); + private static final Map SKILL_CAP = Map.ofEntries( + Map.entry("Combat", 60), + Map.entry("Farming", 60), + Map.entry("Mining", 60), + Map.entry("Foraging", 50), + Map.entry("Fishing", 50), + Map.entry("Enchanting", 60), + Map.entry("Alchemy", 50), + Map.entry("Taming", 60), + Map.entry("Carpentry", 50), + Map.entry("Catacombs", 50), + Map.entry("Runecraft", 25), + Map.entry("Social", 25) + ); + private static final Map SOFT_SKILL_CAP = Map.of( + "Taming", 50, + "Farming", 50 + ); + + private static final Map INFINITE = Map.of( + "Catacombs", 0 + ); + + public SkillWidget(String skill, long xp, int playerCap) { + this.SKILL_NAME = skill; + this.SKILL_LEVEL = LevelFinder.getLevelInfo(skill, xp); + if (SKILL_LEVEL.level >= SKILL_CAP.get(skill) && !INFINITE.containsKey(skill)) { + SKILL_LEVEL.fill = 1; + SKILL_LEVEL.level = SKILL_CAP.get(skill); + } + + this.stack = SKILL_LOGO.getOrDefault(skill, Ico.BARRIER); + if (playerCap != -1) { + this.SKILL_LEVEL.level = Math.min(SKILL_LEVEL.level, (SOFT_SKILL_CAP.get(this.SKILL_NAME) + playerCap)); + } + + } + + public void render(DrawContext context, int x, int y) { + context.drawItem(this.stack, x + 3, y + 2); + context.drawText(textRenderer, SKILL_NAME + " " + SKILL_LEVEL.level, x + 31, y + 2, Color.white.hashCode(), false); + + Color fillColor = Color.green; + if (SKILL_LEVEL.level >= SKILL_CAP.get(SKILL_NAME)) { + fillColor = Color.MAGENTA; + } + + if ((SOFT_SKILL_CAP.containsKey(SKILL_NAME) && SKILL_LEVEL.level > SOFT_SKILL_CAP.get(SKILL_NAME)) && SKILL_LEVEL.level < SKILL_CAP.get(SKILL_NAME) && SKILL_LEVEL.fill == 1 || + (SKILL_NAME.equals("Taming") && SKILL_LEVEL.level >= SOFT_SKILL_CAP.get(SKILL_NAME))) { + fillColor = Color.YELLOW; + } + + context.drawGuiTexture(BAR_BACK, x + 30, y + 12, 75, 6); + RenderHelper.renderNineSliceColored(context, BAR_FILL, x + 30, y + 12, (int) (75 * SKILL_LEVEL.fill), 6, fillColor); + } +} \ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillsPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillsPage.java new file mode 100644 index 0000000000..c331bbdd06 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillsPage.java @@ -0,0 +1,93 @@ +package de.hysky.skyblocker.skyblock.profileviewer.skills; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.util.Identifier; + +import java.util.ArrayList; +import java.util.List; + +public class SkillsPage implements ProfileViewerPage { + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/icon_data_widget.png"); + private static final String[] SKILLS = {"Combat", "Mining", "Farming", "Foraging", "Fishing", "Enchanting", "Alchemy", "Taming", "Carpentry", "Catacombs", "Runecraft", "Social"}; + private static final int ROW_GAP = 28; + + private final JsonObject HYPIXEL_PROFILE; + private final JsonObject PLAYER_PROFILE; + + private final List skillWidgets = new ArrayList<>(); + private JsonObject skills; + + public SkillsPage(JsonObject hProfile, JsonObject pProfile) { + this.HYPIXEL_PROFILE = hProfile; + this.PLAYER_PROFILE = pProfile; + + try { + this.skills = this.PLAYER_PROFILE.getAsJsonObject("player_data").getAsJsonObject("experience"); + for (String skill : SKILLS) { + skillWidgets.add(new SkillWidget(skill, getSkillXP("SKILL_" + skill.toUpperCase()), getSkillCap(skill))); + } + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Error creating widgets.", e); + } + } + + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + int column2 = rootX + 113; + for (int i = 0; i < skillWidgets.size(); i++) { + int x = (i < 6) ? rootX : column2; + int y = rootY + (i % 6) * ROW_GAP; + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 26, 109, 26); + skillWidgets.get(i).render(context, x, y + 3); + } + } + + private int getSkillCap(String skill) { + try { + return switch (skill) { + case "Farming" -> this.PLAYER_PROFILE.getAsJsonObject("jacobs_contest").getAsJsonObject("perks").get("farming_level_cap").getAsInt(); + default -> -1; + }; + } catch (Exception e) { + return 0; + } + } + + private long getSkillXP(String skill) { + try { + return switch (skill) { + case "SKILL_CATACOMBS" -> getCatacombsXP(); + case "SKILL_SOCIAL" -> getCoopSocialXP(); + case "SKILL_RUNECRAFT" -> this.skills.get("SKILL_RUNECRAFTING").getAsLong(); + default -> this.skills.get(skill).getAsLong(); + }; + } catch (Exception e) { + return 0; + } + } + + private long getCatacombsXP() { + try { + JsonObject dungeonSkills = this.PLAYER_PROFILE.getAsJsonObject("dungeons").getAsJsonObject("dungeon_types"); + return dungeonSkills.getAsJsonObject("catacombs").get("experience").getAsLong(); + } catch (Exception e) { + return 0; + } + } + + private long getCoopSocialXP() { + long socialXP = 0; + JsonObject members = HYPIXEL_PROFILE.getAsJsonObject("members"); + for (String memberId : members.keySet()) { + try { + socialXP += members.getAsJsonObject(memberId).getAsJsonObject("player_data").getAsJsonObject("experience").get("SKILL_SOCIAL").getAsLong(); + } catch (Exception e) { + ProfileViewerScreen.LOGGER.warn("[Skyblocker Profile Viewer] Error calculating co-op social xp", e); + } + } + return socialXP; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayerWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayerWidget.java new file mode 100644 index 0000000000..a9c05c1145 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayerWidget.java @@ -0,0 +1,93 @@ +package de.hysky.skyblocker.skyblock.profileviewer.slayers; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.Map; + +public class SlayerWidget { + private final String slayerName; + private final LevelFinder.LevelInfo slayerLevel; + private JsonObject slayerData = null; + + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/icon_data_widget.png"); + private static final Identifier BAR_FILL = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_fill"); + private static final Identifier BAR_BACK = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_back"); + private final Identifier item; + private final ItemStack drop; + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final Map HEAD_ICON = Map.ofEntries( + Map.entry("Zombie", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/zombie.png")), + Map.entry("Spider", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/spider.png")), + Map.entry("Wolf", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/wolf.png")), + Map.entry("Enderman", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/enderman.png")), + Map.entry("Vampire", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/vampire.png")), + Map.entry("Blaze", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/blaze.png")) + ); + + private static final Map DROP_ICON = Map.ofEntries( + Map.entry("Zombie", Ico.FLESH), + Map.entry("Spider", Ico.STRING), + Map.entry("Wolf", Ico.MUTTON), + Map.entry("Enderman", Ico.E_PEARL), + Map.entry("Vampire", Ico.REDSTONE), + Map.entry("Blaze", Ico.B_POWDER) + ); + + public SlayerWidget(String slayer, long xp, JsonObject playerProfile) { + this.slayerName = slayer; + this.slayerLevel = LevelFinder.getLevelInfo(slayer, xp); + this.item = HEAD_ICON.get(slayer); + this.drop = DROP_ICON.getOrDefault(slayer, Ico.BARRIER); + try { + this.slayerData = playerProfile.getAsJsonObject("slayer").getAsJsonObject("slayer_bosses").getAsJsonObject(this.slayerName.toLowerCase()); + } catch (Exception ignored) {} + } + + public void render(DrawContext context, int x, int y) { + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 26, 109, 26); + context.drawTexture(this.item, x + 1, y + 3, 0, 0, 20, 20, 20, 20); + context.drawText(textRenderer, slayerName + " " + slayerLevel.level, x + 31, y + 5, Color.white.hashCode(), false); + + int col2 = x + 113; + context.drawTexture(TEXTURE, col2, y, 0, 0, 109, 26, 109, 26); + context.drawItem(this.drop, col2 + 3, y + 5); + context.drawText(textRenderer, "§aKills: §r" + findTotalKills(), col2 + 30, y + 4, Color.white.hashCode(), true); + context.drawText(textRenderer, findTopTierKills(), findTopTierKills().equals("No Data") ? col2 + 30 : col2 + 29, y + 15, Color.white.hashCode(), true); + + context.drawGuiTexture(BAR_BACK, x + 30, y + 15, 75, 6); + Color fillColor = slayerLevel.fill == 1 ? Color.MAGENTA : Color.green; + RenderHelper.renderNineSliceColored(context, BAR_FILL, x + 30, y + 15, (int) (75 * slayerLevel.fill), 6, fillColor); + } + + private int findTotalKills() { + try { + int totalKills = 0; + for (String key : this.slayerData.keySet()) { + if (key.startsWith("boss_kills_tier_")) totalKills += this.slayerData.get(key).getAsInt(); + } + return totalKills; + } catch (Exception e) { + return 0; + } + } + + private String findTopTierKills() { + try { + for (int tier = 4; tier >= 0; tier--) { + String key = "boss_kills_tier_" + tier; + if (this.slayerData.has(key)) return "§cT" + (tier + 1) + " Kills: §r" + this.slayerData.get(key).getAsInt(); + } + } catch (Exception ignored) {} + return "No Data"; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayersPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayersPage.java new file mode 100644 index 0000000000..08e2ca0647 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayersPage.java @@ -0,0 +1,41 @@ +package de.hysky.skyblocker.skyblock.profileviewer.slayers; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import net.minecraft.client.gui.DrawContext; + +import java.util.ArrayList; +import java.util.List; + +public class SlayersPage implements ProfileViewerPage { + private static final String[] SLAYERS = {"Zombie", "Spider", "Wolf", "Enderman", "Vampire", "Blaze"}; + private static final int ROW_GAP = 28; + + private final List slayerWidgets = new ArrayList<>(); + + public SlayersPage(JsonObject pProfile) { + try { + for (String slayer : SLAYERS) { + slayerWidgets.add(new SlayerWidget(slayer, getSlayerXP(slayer.toLowerCase(), pProfile), pProfile)); + } + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Error creating slayer widgets", e); + } + } + + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + for (int i = 0; i < slayerWidgets.size(); i++) { + slayerWidgets.get(i).render(context, rootX, rootY + i * ROW_GAP); + } + } + + private long getSlayerXP(String slayer, JsonObject pProfile) { + try { + return pProfile.getAsJsonObject("slayer").getAsJsonObject("slayer_bosses") + .getAsJsonObject(slayer).get("xp").getAsLong(); + } catch (Exception e) { + return 0; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/LevelFinder.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/LevelFinder.java new file mode 100644 index 0000000000..b52fd5796f --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/LevelFinder.java @@ -0,0 +1,307 @@ +package de.hysky.skyblocker.skyblock.profileviewer.utils; + +import java.util.ArrayList; +import java.util.List; + +public class LevelFinder { + public static class LevelInfo { + public long xp; + public int level; + public double fill; + + public LevelInfo(long xp, int level) { + this.xp = xp; + this.level = level; + } + + public LevelInfo(int level, double fill) { + this.level = level; + this.fill = fill; + } + } + + private static final long CATA_XP_PER_LEVEL = 200_000_000; + private static final List GENERIC_SKILL_BOUNDARIES = createGenericSkillBoundaries(); + private static final List CATACOMBS_SKILL_BOUNDARIES = createCatacombsSkillBoundaries(); + private static final List RUNECRAFT_SKILL_BOUNDARIES = createRunecraftSkillBoundaries(); + private static final List SOCIAL_SKILL_BOUNDARIES = createSocialSkillBoundaries(); + + private static final List COMMON_PET_BOUNDARIES = createCommonPetBoundaries(); + private static final List UNCOMMON_PET_BOUNDARIES = createUncommonPetBoundaries(); + private static final List RARE_PET_BOUNDARIES = createRarePetBoundaries(); + private static final List EPIC_PET_BOUNDARIES = createEpicPetBoundaries(); + private static final List LEGENDARY_PET_BOUNDARIES = createLegendaryPetBoundaries(); + + + private static final List GENERIC_SLAYER_BOUNDARIES = createGenericSlayerBoundaries(); + private static final List VAMPIRE_SLAYER_BOUNDARIES = createVampireSlayerBoundaries(); + + private static List createGenericSkillBoundaries() { + List boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 50L, 175L, 375L, 675L, 1175L, 1925L, 2925L, 4425L, 6425L, + 9925L, 14925L, 22425L, 32425L, 47425L, 67425L, 97425L, 147425L, + 222425L, 322425L, 522425L, 822425L, 1222425L, 1722425L, 2322425L, + 3022425L, 3822425L, 4722425L, 5722425L, 6822425L, 8022425L, + 9322425L, 10722425L, 12222425L, 13822425L, 15522425L, 17322425L, + 19222425L, 21222425L, 23322425L, 25522425L, 27822425L, 30222425L, + 32722425L, 35322425L, 38072425L, 40972425L, 44072425L, 47472425L, + 51172425L, 55172425L, 59472425L, 64072425L, 68972425L, 74172425L, + 79672425L, 85472425L, 91572425L, 97972425L, 104672425L, 111672425L + }; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + return boundaries; + } + + private static List createCatacombsSkillBoundaries() { + List boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 50L, 125L, 235L, 395L, 625L, 955L, 1425L, 2095L, 3045L, + 4385L, 6275L, 8940L, 12700L, 17960L, 25340L, 35640L, 50040L, + 70040L, 97640L, 135640L, 188140L, 259640L, 356640L, 488640L, + 668640L, 911640L, 1239640L, 1684640L, 2284640L, 3084640L, + 4149640L, 5559640L, 7459640L, 9959640L, 13259640L, 17559640L, + 23159640L, 30359640L, 39359640L, 51359640L, 66359640L, 85359640L, + 109559640L, 139559640L, 177559640L, 225559640L, 295559640L, + 360559640L, 453559640L, 569809640L + }; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + return boundaries; + } + + private static List createRunecraftSkillBoundaries() { + List boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 50L, 150L, 275L, 435L, 635L, 885L, 1200L, 1600L, 2100L, + 2725L, 3150L, 4510L, 5760L, 7325L, 9325L, 11825L, 14950L, + 18950L, 23950L, 30200L, 38050L, 47850L, 60100L, 75400L, 94500L + }; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + return boundaries; + } + + private static List createSocialSkillBoundaries() { + List boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 50L, 150L, 300L, 550L, 1050L, 1800L, 2800L, 4050L, 5550L, + 7550L, 10050L, 13050L, 16800L, 21300L, 27300L, 35300L, 45300L, + 57800L, 72800L, 92800L, 117800L, 147800L, 182800L, 222800L, + 272800L + }; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + return boundaries; + } + + private static List createGenericSlayerBoundaries() { + List boundaries = new ArrayList<>(); + long[] cumulativeXp = {0L, 5L, 15L, 200L, 1000L, 5000L,20000L,100000L,400000L,1000000L}; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + return boundaries; + } + + private static List createVampireSlayerBoundaries() { + List boundaries = new ArrayList<>(); + long[] cumulativeXp = {0L, 20L, 75L, 240L, 840L, 2400L}; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + private static List createCommonPetBoundaries() { + List boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 0L, 100L, 210L, 330L, 460L, 605L, 765L, 940L, 1130L, 1340L, 1570L, 1820L, 2095L, + 2395L, 2725L, 3085L, 3485L, 3925L, 4415L, 4955L, 5555L, 6215L, 6945L, 7745L, + 8625L, 9585L, 10635L, 11785L, 13045L, 14425L, 15935L, 17585L, 19385L, 21345L, + 23475L, 25785L, 28285L, 30985L, 33905L, 37065L, 40485L, 44185L, 48185L, 52535L, + 57285L, 62485L, 68185L, 74485L, 81485L, 89285L, 97985L, 107685L, 118485L, 130485L, + 143785L, 158485L, 174685L, 192485L, 211985L, 233285L, 256485L, 281685L, 309085L, + 338885L, 371285L, 406485L, 444685L, 486085L, 530885L, 579285L, 631485L, 687685L, + 748085L, 812885L, 882285L, 956485L, 1035685L, 1120385L, 1211085L, 1308285L, + 1412485L, 1524185L, 1643885L, 1772085L, 1909285L, 2055985L, 2212685L, 2380385L, + 2560085L, 2752785L, 2959485L, 3181185L, 3418885L, 3673585L, 3946285L, 4237985L, + 4549685L, 4883385L, 5241085L, 5624785L + }; + + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + private static List createUncommonPetBoundaries() { + List boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 0L, 175L, 365L, 575L, 805L, 1055L, 1330L, 1630L, 1960L, 2320L, 2720L, 3160L, + 3650L, 4190L, 4790L, 5450L, 6180L, 6980L, 7860L, 8820L, 9870L, 11020L, 12280L, + 13660L, 15170L, 16820L, 18620L, 20580L, 22710L, 25020L, 27520L, 30220L, 33140L, + 36300L, 39720L, 43420L, 47420L, 51770L, 56520L, 61720L, 67420L, 73720L, 80720L, + 88520L, 97220L, 106920L, 117720L, 129720L, 143020L, 157720L, 173920L, 191720L, + 211220L, 232520L, 255720L, 280920L, 308320L, 338120L, 370520L, 405720L, 443920L, + 485320L, 530120L, 578520L, 630720L, 686920L, 747320L, 812120L, 881520L, 955720L, + 1034920L, 1119620L, 1210320L, 1307520L, 1411720L, 1523420L, 1643120L, 1771320L, + 1908520L, 2055220L, 2211920L, 2379620L, 2559320L, 2752020L, 2958720L, 3180420L, + 3418120L, 3672820L, 3945520L, 4237220L, 4548920L, 4882620L, 5240320L, 5624020L, + 6035720L, 6477420L, 6954120L, 7470820L, 8032520L, 8644220L + }; + + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + private static List createRarePetBoundaries() { + List boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 0L, 275L, 575L, 905L, 1265L, 1665L, 2105L, 2595L, 3135L, 3735L, 4395L, 5125L, + 5925L, 6805L, 7765L, 8815L, 9965L, 11225L, 12605L, 14115L, 15765L, 17565L, 19525L, + 21655L, 23965L, 26465L, 29165L, 32085L, 35245L, 38665L, 42365L, 46365L, 50715L, + 55465L, 60665L, 66365L, 72665L, 79665L, 87465L, 96165L, 105865L, 116665L, 128665L, + 141965L, 156665L, 172865L, 190665L, 210165L, 231465L, 254665L, 279865L, 307265L, + 337065L, 369465L, 404665L, 442865L, 484265L, 529065L, 577465L, 629665L, 685865L, + 746265L, 811065L, 880465L, 954665L, 1033865L, 1118565L, 1209265L, 1306465L, + 1410665L, 1522365L, 1642065L, 1770265L, 1907465L, 2054165L, 2210865L, 2378565L, + 2558265L, 2750965L, 2957665L, 3179365L, 3417065L, 3671765L, 3944465L, 4236165L, + 4547865L, 4881565L, 5239265L, 5622965L, 6034665L, 6476365L, 6953065L, 7469765L, + 8031465L, 8643165L, 9309865L, 10036565L, 10828265L, 11689965L, 12626665L + }; + + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + private static List createEpicPetBoundaries() { + List boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 0L, 440L, 930L, 1470L, 2070L, 2730L, 3460L, 4260L, 5140L, 6100L, 7150L, 8300L, + 9560L, 10940L, 12450L, 14100L, 15900L, 17860L, 19990L, 22300L, 24800L, 27500L, 30420L, + 33580L, 37000L, 40700L, 44700L, 49050L, 53800L, 59000L, 64700L, 71000L, 78000L, 85800L, + 94500L, 104200L, 115000L, 127000L, 140300L, 155000L, 171200L, 189000L, 208500L, 229800L, + 253000L, 278200L, 305600L, 335400L, 367800L, 403000L, 441200L, 482600L, 527400L, 575800L, + 628000L, 684200L, 744600L, 809400L, 878800L, 953000L, 1032200L, 1116900L, 1207600L, 1304800L, + 1409000L, 1520700L, 1640400L, 1768600L, 1905800L, 2052500L, 2209200L, 2376900L, 2556600L, + 2749300L, 2956000L, 3177700L, 3415400L, 3670100L, 3942800L, 4234500L, 4546200L, 4879900L, + 5237600L, 5621300L, 6033000L, 6474700L, 6951400L, 7468100L, 8029800L, 8641500L, 9308200L, + 10034900L, 10826600L, 11688300L, 12625000L, 13641700L, 14743400L, 15935100L, 17221800L, + 18608500L + }; + + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + private static List createLegendaryPetBoundaries() { + List boundaries = new ArrayList<>(); + Long[] cumulativeXp = { + 0L, 0L, 660L, 1390L, 2190L, 3070L, 4030L, 5080L, 6230L, 7490L, + 8870L, 10380L, 12030L, 13830L, 15790L, 17920L, 20230L, 22730L, + 25430L, 28350L, 31510L, 34930L, 38630L, 42630L, 46980L, 51730L, + 56930L, 62630L, 68930L, 75930L, 83730L, 92430L, 102130L, 112930L, + 124930L, 138230L, 152930L, 169130L, 186930L, 206430L, 227730L, + 250930L, 276130L, 303530L, 333330L, 365730L, 400930L, 439130L, + 480530L, 525330L, 573730L, 625930L, 682130L, 742530L, 807330L, + 876730L, 950930L, 1030130L, 1114830L, 1205530L, 1302730L, 1406930L, + 1518630L, 1638330L, 1766530L, 1903730L, 2050430L, 2207130L, 2374830L, + 2554530L, 2747230L, 2953930L, 3175630L, 3413330L, 3668030L, 3940730L, + 4232430L, 4544130L, 4877830L, 5235530L, 5619230L, 6030930L, 6472630L, + 6949330L, 7466030L, 8027730L, 8639430L, 9306130L, 10032830L, 10824530L, + 11686230L, 12622930L, 13639630L, 14741330L, 15933030L, 17219730L, 18606430L, + 20103130L, 21719830L, 23466530L, 25353230L, 25353230L, 25358785L, 27245485L, + 29132185L, 31018885L, 32905585L, 34792285L, 36678985L, 38565685L, 40452385L, + 42339085L, 44225785L, 46112485L, 47999185L, 49885885L, 51772585L, 53659285L, + 55545985L, 57432685L, 59319385L, 61206085L, 63092785L, 64979485L, 66866185L, + 68752885L, 70639585L, 72526285L, 74412985L, 76299685L, 78186385L, 80073085L, + 81959785L, 83846485L, 85733185L, 87619885L, 89506585L, 91393285L, 93279985L, + 95166685L, 97053385L, 98940085L, 100826785L, 102713485L, 104600185L, 106486885L, + 108373585L, 110260285L, 112146985L, 114033685L, 115920385L, 117807085L, 119693785L, + 121580485L, 123467185L, 125353885L, 127240585L, 129127285L, 131013985L, 132900685L, + 134787385L, 136674085L, 138560785L, 140447485L, 142334185L, 144220885L, 146107585L, + 147994285L, 149880985L, 151767685L, 153654385L, 155541085L, 157427785L, 159314485L, + 161201185L, 163087885L, 164974585L, 166861285L, 168747985L, 170634685L, 172521385L, + 174408085L, 176294785L, 178181485L, 180068185L, 181954885L, 183841585L, 185728285L, + 187614985L, 189501685L, 191388385L, 193275085L, 195161785L, 197048485L, 198935185L, + 200821885L, 202708585L, 204595285L, 206481985L, 208368685L, 210255385L + }; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + public static LevelInfo getLevelInfo(String name, long xp) { + List boundaries = getLevelBoundaries(name, xp); + for (int i = boundaries.size() - 1; i >= 0 ; i--) { + if (xp >= boundaries.get(i).xp) { + double fill; + if (i < boundaries.getLast().level) { + double currentLevelXP = boundaries.get(i).xp; + double nextLevelXP = boundaries.get(i + 1).xp; + double levelXPRange = nextLevelXP - currentLevelXP; + double xpInCurrentLevel = xp - currentLevelXP; + fill = xpInCurrentLevel / levelXPRange; + } else { + fill = 1.0; + } + return new LevelInfo(boundaries.get(i).level, fill); + } + } + return new LevelInfo(0L, 0); + } + + + private static List getLevelBoundaries(String levelName, long xp) { + return switch (levelName) { + case "Vampire" -> VAMPIRE_SLAYER_BOUNDARIES; + case "Zombie", "Spider", "Wolf", "Enderman", "Blaze" -> GENERIC_SLAYER_BOUNDARIES; + case "PET_COMMON" -> COMMON_PET_BOUNDARIES; + case "PET_UNCOMMON" -> UNCOMMON_PET_BOUNDARIES; + case "PET_RARE" -> RARE_PET_BOUNDARIES; + case "PET_EPIC" -> EPIC_PET_BOUNDARIES; + case "PET_LEGENDARY", "PET_MYTHIC" -> LEGENDARY_PET_BOUNDARIES.subList(0,101); + case "PET_GREG" -> LEGENDARY_PET_BOUNDARIES; + case "Social" -> SOCIAL_SKILL_BOUNDARIES; + case "Runecraft" -> RUNECRAFT_SKILL_BOUNDARIES; + case "Catacombs" -> calculateCatacombsSkillBoundaries(xp); + default -> GENERIC_SKILL_BOUNDARIES; + }; + } + + private static List calculateCatacombsSkillBoundaries(long xp) { + if (xp >= CATACOMBS_SKILL_BOUNDARIES.getLast().xp) { + int additionalLevels = (int) ((xp - CATACOMBS_SKILL_BOUNDARIES.getLast().xp) / CATA_XP_PER_LEVEL) ; + + List updatedBoundaries = new ArrayList<>(CATACOMBS_SKILL_BOUNDARIES); + for (int i = 0; i <= additionalLevels; i++) { + int level = CATACOMBS_SKILL_BOUNDARIES.getLast().level + i + 1; + long nextLevelXP = updatedBoundaries.getLast().xp + CATA_XP_PER_LEVEL; + updatedBoundaries.add(new LevelInfo(nextLevelXP, level)); + } + + return updatedBoundaries; + } + + return CATACOMBS_SKILL_BOUNDARIES; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SkullCreator.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SkullCreator.java new file mode 100644 index 0000000000..b074952cbb --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SkullCreator.java @@ -0,0 +1,27 @@ +package de.hysky.skyblocker.skyblock.profileviewer.utils; + +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.ProfileComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; + +import java.util.Optional; +import java.util.UUID; + +public class SkullCreator { + public static ItemStack createSkull(String textureB64) { + ItemStack skull = new ItemStack(Items.PLAYER_HEAD); + try { + PropertyMap map = new PropertyMap(); + map.put("textures", new Property("textures", textureB64)); + ProfileComponent profile = new ProfileComponent(Optional.of("skull"), Optional.of(UUID.randomUUID()), map); + skull.set(DataComponentTypes.PROFILE, profile); + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Failed to create skull", e); + } + return skull; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SubPageSelectButton.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SubPageSelectButton.java new file mode 100644 index 0000000000..4c9dcda461 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SubPageSelectButton.java @@ -0,0 +1,65 @@ +package de.hysky.skyblocker.skyblock.profileviewer.utils; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ButtonTextures; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.LoreComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; + +import java.awt.*; + +public class SubPageSelectButton extends ClickableWidget { + private final ProfileViewerPage page; + private final int index; + private boolean toggled; + + private static final ButtonTextures TEXTURES = new ButtonTextures(Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/button_icon_toggled.png"), Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/button_icon.png"), Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/button_icon_toggled_highlighted.png"), Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/button_icon_highlighted.png")); + private final ItemStack ICON; + + public SubPageSelectButton(ProfileViewerPage page, int x, int y, int index, ItemStack item) { + super(x, y, 22, 22, item.getName()); + this.ICON = item; + this.toggled = index == 0; + this.index = index; + this.page = page; + visible = false; + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + context.fill(this.getX(), this.getY(), this.getX() + 20, this.getY() + 20, Color.BLACK.getRGB()); + context.drawTexture(TEXTURES.get(toggled, isHovered()), this.getX() + 1, this.getY() + 1,0, 0, 18, 18, 18, 18); + context.drawItem(ICON, this.getX() + 2, this.getY() + 2); + if ((mouseX > getX() + 1 && mouseX < getX() + 19 && mouseY > getY() + 1 && mouseY < getY() + 19)) { + LoreComponent lore = ICON.get(DataComponentTypes.LORE); + if (lore != null) context.drawTooltip(MinecraftClient.getInstance().textRenderer, lore.lines(), mouseX, mouseY + 10); + } + } + + @Override + protected boolean clicked(double mouseX, double mouseY) { + return this.active && this.visible &&(mouseX > getX() + 1 && mouseX < getX() + 19 && mouseY > getY() + 1 && mouseY < getY() + 19); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} + + public void setToggled(boolean toggled) { + this.toggled = toggled; + } + + @Override + public void onClick(double mouseX, double mouseY) { + page.onNavButtonClick(this); + } + + public int getIndex() { + return index; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java index c2c952cf59..21d6680575 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java @@ -97,7 +97,6 @@ private static void registerDefaultShortcuts() { // Party commandArgs.put("/pa", "/p accept"); - commands.put("/pv", "/p leave"); commands.put("/pd", "/p disband"); commands.put("/rp", "/reparty"); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/Ico.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/Ico.java index 818056f0dd..b37a3883c2 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/Ico.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/Ico.java @@ -1,8 +1,11 @@ package de.hysky.skyblocker.skyblock.tabhud.util; +import net.minecraft.enchantment.Enchantment; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; +import static net.minecraft.enchantment.Enchantments.PROTECTION; + /** * Stores convenient shorthands for common ItemStack definitions */ @@ -10,23 +13,29 @@ public class Ico { public static final ItemStack MAP = new ItemStack(Items.FILLED_MAP); public static final ItemStack NTAG = new ItemStack(Items.NAME_TAG); public static final ItemStack EMERALD = new ItemStack(Items.EMERALD); + public static final ItemStack MAGMA_CREAM = new ItemStack(Items.MAGMA_CREAM); public static final ItemStack AMETHYST_SHARD = new ItemStack(Items.AMETHYST_SHARD); public static final ItemStack CLOCK = new ItemStack(Items.CLOCK); public static final ItemStack DIASWORD = new ItemStack(Items.DIAMOND_SWORD); public static final ItemStack DBUSH = new ItemStack(Items.DEAD_BUSH); public static final ItemStack VILLAGER = new ItemStack(Items.VILLAGER_SPAWN_EGG); + public static final ItemStack SPAWN_EGG = new ItemStack(Items.GHAST_SPAWN_EGG); public static final ItemStack MOREGOLD = new ItemStack(Items.GOLDEN_APPLE); public static final ItemStack COMPASS = new ItemStack(Items.COMPASS); public static final ItemStack SUGAR = new ItemStack(Items.SUGAR); - public static final ItemStack HOE = new ItemStack(Items.IRON_HOE); + public static final ItemStack IRON_HOE = new ItemStack(Items.IRON_HOE); + public static final ItemStack GOLDEN_HOE = new ItemStack(Items.GOLDEN_HOE); public static final ItemStack GOLD = new ItemStack(Items.GOLD_INGOT); + public static final ItemStack IRON = new ItemStack(Items.IRON_INGOT); public static final ItemStack BONE = new ItemStack(Items.BONE); public static final ItemStack SIGN = new ItemStack(Items.OAK_SIGN); public static final ItemStack FISH_ROD = new ItemStack(Items.FISHING_ROD); - public static final ItemStack SWORD = new ItemStack(Items.IRON_SWORD); + public static final ItemStack STONE_SWORD = new ItemStack(Items.STONE_SWORD); + public static final ItemStack IRON_SWORD = new ItemStack(Items.IRON_SWORD); public static final ItemStack LANTERN = new ItemStack(Items.LANTERN); public static final ItemStack COOKIE = new ItemStack(Items.COOKIE); public static final ItemStack POTION = new ItemStack(Items.POTION); + public static final ItemStack S_POTION = new ItemStack(Items.SPLASH_POTION); public static final ItemStack BARRIER = new ItemStack(Items.BARRIER); public static final ItemStack PLAYER = new ItemStack(Items.PLAYER_HEAD); public static final ItemStack WATER = new ItemStack(Items.WATER_BUCKET); @@ -37,6 +46,7 @@ public class Ico { public static final ItemStack STRING = new ItemStack(Items.STRING); public static final ItemStack WITHER = new ItemStack(Items.WITHER_SKELETON_SKULL); public static final ItemStack FLESH = new ItemStack(Items.ROTTEN_FLESH); + public static final ItemStack MUTTON = new ItemStack(Items.MUTTON); public static final ItemStack DRAGON = new ItemStack(Items.DRAGON_HEAD); public static final ItemStack DIAMOND = new ItemStack(Items.DIAMOND); public static final ItemStack ICE = new ItemStack(Items.ICE); @@ -46,25 +56,18 @@ public class Ico { public static final ItemStack BOOK = new ItemStack(Items.WRITABLE_BOOK); public static final ItemStack FURNACE = new ItemStack(Items.FURNACE); public static final ItemStack CHESTPLATE = new ItemStack(Items.IRON_CHESTPLATE); + public static final ItemStack L_CHESTPLATE = new ItemStack(Items.LEATHER_CHESTPLATE); public static final ItemStack B_ROD = new ItemStack(Items.BLAZE_ROD); + public static final ItemStack B_POWDER = new ItemStack(Items.BLAZE_POWDER); public static final ItemStack BOW = new ItemStack(Items.BOW); public static final ItemStack COPPER = new ItemStack(Items.COPPER_INGOT); public static final ItemStack NETHERITE_UPGRADE_ST = new ItemStack(Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE); public static final ItemStack COMPOSTER = new ItemStack(Items.COMPOSTER); public static final ItemStack SAPLING = new ItemStack(Items.OAK_SAPLING); public static final ItemStack SEEDS = new ItemStack(Items.WHEAT_SEEDS); - public static final ItemStack WHEAT = new ItemStack(Items.WHEAT); - public static final ItemStack CARROT = new ItemStack(Items.CARROT); - public static final ItemStack POTATO = new ItemStack(Items.POTATO); - public static final ItemStack SUGAR_CANE = new ItemStack(Items.SUGAR_CANE); - public static final ItemStack NETHER_WART = new ItemStack(Items.NETHER_WART); - public static final ItemStack MUSHROOM = new ItemStack(Items.RED_MUSHROOM); - public static final ItemStack CACTUS = new ItemStack(Items.CACTUS); - public static final ItemStack MELON = new ItemStack(Items.MELON); - public static final ItemStack PUMPKIN = new ItemStack(Items.PUMPKIN); - public static final ItemStack COCOA_BEANS = new ItemStack(Items.COCOA_BEANS); public static final ItemStack MILESTONE = new ItemStack(Items.LODESTONE); - public static final ItemStack PICKAXE = new ItemStack(Items.IRON_PICKAXE); + public static final ItemStack STONE_PICKAXE = new ItemStack(Items.STONE_PICKAXE); + public static final ItemStack IRON_PICKAXE = new ItemStack(Items.IRON_PICKAXE); public static final ItemStack NETHER_STAR = new ItemStack(Items.NETHER_STAR); public static final ItemStack HEART_OF_THE_SEA = new ItemStack(Items.HEART_OF_THE_SEA); public static final ItemStack EXPERIENCE_BOTTLE = new ItemStack(Items.EXPERIENCE_BOTTLE); @@ -73,4 +76,13 @@ public class Ico { public static final ItemStack ENCHANTED_BOOK = new ItemStack(Items.ENCHANTED_BOOK); public static final ItemStack SPIDER_EYE = new ItemStack(Items.SPIDER_EYE); public static final ItemStack BLUE_ICE = new ItemStack(Items.BLUE_ICE); + public static final ItemStack JUNGLE_SAPLING = new ItemStack(Items.JUNGLE_SAPLING); + public static final ItemStack ENCHANTING_TABLE = new ItemStack(Items.ENCHANTING_TABLE); + public static final ItemStack BREWING_STAND = new ItemStack(Items.BREWING_STAND); + public static final ItemStack CRAFTING_TABLE = new ItemStack(Items.CRAFTING_TABLE); + public static final ItemStack PAINTING = new ItemStack(Items.PAINTING); + public static final ItemStack E_PEARL = new ItemStack(Items.ENDER_PEARL); + public static final ItemStack FEATHER = new ItemStack(Items.FEATHER); + public static final ItemStack E_CHEST = new ItemStack(Items.ENDER_CHEST); + public static final ItemStack MYCELIUM = new ItemStack(Items.MYCELIUM); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDeathWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDeathWidget.java index 9c29921087..79193b5125 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDeathWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDeathWidget.java @@ -38,7 +38,7 @@ public void updateContent() { this.addComponent(deaths); } - this.addSimpleIcoText(Ico.SWORD, "Damage Dealt:", Formatting.RED, 26); + this.addSimpleIcoText(Ico.IRON_SWORD, "Damage Dealt:", Formatting.RED, 26); this.addSimpleIcoText(Ico.POTION, "Healing Done:", Formatting.RED, 27); this.addSimpleIcoText(Ico.NTAG, "Milestone:", Formatting.YELLOW, 28); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ElectionWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ElectionWidget.java index ec935faf22..8f50f9ff5b 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ElectionWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ElectionWidget.java @@ -32,10 +32,10 @@ public class ElectionWidget extends Widget { static { MAYOR_DATA.put("Aatrox", Ico.DIASWORD); - MAYOR_DATA.put("Cole", Ico.PICKAXE); + MAYOR_DATA.put("Cole", Ico.IRON_PICKAXE); MAYOR_DATA.put("Diana", Ico.BONE); MAYOR_DATA.put("Diaz", Ico.GOLD); - MAYOR_DATA.put("Finnegan", Ico.HOE); + MAYOR_DATA.put("Finnegan", Ico.IRON_HOE); MAYOR_DATA.put("Foxy", Ico.SUGAR); MAYOR_DATA.put("Paul", Ico.COMPASS); MAYOR_DATA.put("Scorpius", Ico.MOREGOLD); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenSkillsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenSkillsWidget.java index 75652b331e..9e1f398966 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenSkillsWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenSkillsWidget.java @@ -82,14 +82,14 @@ public void updateContent() { Text speed = Widget.simpleEntryText(67, "SPD", Formatting.WHITE); IcoTextComponent spd = new IcoTextComponent(Ico.SUGAR, speed); Text farmfort = Widget.simpleEntryText(68, "FFO", Formatting.GOLD); - IcoTextComponent ffo = new IcoTextComponent(Ico.HOE, farmfort); + IcoTextComponent ffo = new IcoTextComponent(Ico.IRON_HOE, farmfort); TableComponent tc = new TableComponent(2, 1, Formatting.YELLOW.getColorValue()); tc.addToCell(0, 0, spd); tc.addToCell(1, 0, ffo); this.addComponent(tc); - this.addComponent(new IcoTextComponent(Ico.HOE, PlayerListMgr.textAt(70))); + this.addComponent(new IcoTextComponent(Ico.IRON_HOE, PlayerListMgr.textAt(70))); ProgressComponent pc2; Matcher milestoneMatcher = PlayerListMgr.regexAt(69, MS_PATTERN); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ReputationWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ReputationWidget.java index 3c218fb18e..34d15a280b 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ReputationWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ReputationWidget.java @@ -44,7 +44,7 @@ public void updateContent() { if (fname.equals("Mage")) { faction = new IcoTextComponent(Ico.POTION, Text.literal(fname).formatted(Formatting.DARK_AQUA)); } else { - faction = new IcoTextComponent(Ico.SWORD, Text.literal(fname).formatted(Formatting.RED)); + faction = new IcoTextComponent(Ico.IRON_SWORD, Text.literal(fname).formatted(Formatting.RED)); } } this.addComponent(faction); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/SkillsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/SkillsWidget.java index 379fbb62cc..c9cf61aaaa 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/SkillsWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/SkillsWidget.java @@ -58,13 +58,13 @@ public void updateContent() { Text speed = Widget.simpleEntryText(67, "SPD", Formatting.WHITE); IcoTextComponent spd = new IcoTextComponent(Ico.SUGAR, speed); Text strength = Widget.simpleEntryText(68, "STR", Formatting.RED); - IcoTextComponent str = new IcoTextComponent(Ico.SWORD, strength); + IcoTextComponent str = new IcoTextComponent(Ico.IRON_SWORD, strength); Text critDmg = Widget.simpleEntryText(69, "CCH", Formatting.BLUE); - IcoTextComponent cdg = new IcoTextComponent(Ico.SWORD, critDmg); + IcoTextComponent cdg = new IcoTextComponent(Ico.IRON_SWORD, critDmg); Text critCh = Widget.simpleEntryText(70, "CDG", Formatting.BLUE); - IcoTextComponent cch = new IcoTextComponent(Ico.SWORD, critCh); + IcoTextComponent cch = new IcoTextComponent(Ico.IRON_SWORD, critCh); Text aSpeed = Widget.simpleEntryText(71, "ASP", Formatting.YELLOW); - IcoTextComponent asp = new IcoTextComponent(Ico.HOE, aSpeed); + IcoTextComponent asp = new IcoTextComponent(Ico.IRON_HOE, aSpeed); TableComponent tc = new TableComponent(2, 3, Formatting.YELLOW.getColorValue()); tc.addToCell(0, 0, spd); diff --git a/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java b/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java index c63af3ba28..93e314a7e8 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java @@ -1,16 +1,14 @@ package de.hysky.skyblocker.utils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.google.gson.JsonParser; import com.mojang.util.UndashedUuid; - import de.hysky.skyblocker.utils.Http.ApiResponse; import de.hysky.skyblocker.utils.scheduler.Scheduler; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.minecraft.client.MinecraftClient; import net.minecraft.client.session.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /* * Contains only basic helpers for using Http APIs diff --git a/src/main/java/de/hysky/skyblocker/utils/Http.java b/src/main/java/de/hysky/skyblocker/utils/Http.java index 99db03163e..e5fd18a0ad 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Http.java +++ b/src/main/java/de/hysky/skyblocker/utils/Http.java @@ -1,5 +1,10 @@ package de.hysky.skyblocker.utils; +import de.hysky.skyblocker.SkyblockerMod; +import net.minecraft.SharedConstants; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -16,12 +21,6 @@ import java.util.zip.GZIPInputStream; import java.util.zip.InflaterInputStream; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import de.hysky.skyblocker.SkyblockerMod; -import net.minecraft.SharedConstants; - /** * @implNote All http requests are sent using HTTP 2 */ diff --git a/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java b/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java index a786e79f3d..aa7a04924d 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java @@ -4,7 +4,6 @@ import com.google.gson.JsonObject; import de.hysky.skyblocker.SkyblockerMod; import it.unimi.dsi.fastutil.objects.ObjectLongPair; -import net.minecraft.client.MinecraftClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,13 +16,24 @@ public class ProfileUtils { private static final long HYPIXEL_API_COOLDOWN = 300000; // 5min = 300000 public static Map> players = new HashMap<>(); - - public static CompletableFuture updateProfile() { - return updateProfile(MinecraftClient.getInstance().getSession().getUsername()); + public static Map> profiles = new HashMap<>(); + + public static CompletableFuture updateProfileByName(String name) { + return fetchFullProfile(name).thenApply(profile -> { + JsonObject player = profile.getAsJsonArray("profiles").asList().stream() + .map(JsonElement::getAsJsonObject) + .filter(profileObj -> profileObj.getAsJsonPrimitive("selected").getAsBoolean()) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No selected profile found!?")) + .getAsJsonObject("members").get(name).getAsJsonObject(); + + players.put(name, ObjectLongPair.of(player, System.currentTimeMillis())); + return player; + }); } - public static CompletableFuture updateProfile(String name) { - ObjectLongPair playerCache = players.get(name); + public static CompletableFuture fetchFullProfile(String name) { + ObjectLongPair playerCache = profiles.get(name); if (playerCache != null && playerCache.rightLong() + HYPIXEL_API_COOLDOWN > System.currentTimeMillis()) { return CompletableFuture.completedFuture(playerCache.left()); } @@ -32,19 +42,12 @@ public static CompletableFuture updateProfile(String name) { String uuid = ApiUtils.name2Uuid(name); try (Http.ApiResponse response = Http.sendHypixelRequest("skyblock/profiles", "?uuid=" + uuid)) { if (!response.ok()) { - throw new IllegalStateException("Failed to get profile uuid for players " + name + "! Response: " + response.content()); + throw new IllegalStateException("Failed to get profile uuid for player: " + name + "! Response: " + response.content()); } - JsonObject responseJson = SkyblockerMod.GSON.fromJson(response.content(), JsonObject.class); - - JsonObject player = responseJson.getAsJsonArray("profiles").asList().stream() - .map(JsonElement::getAsJsonObject) - .filter(profile -> profile.getAsJsonPrimitive("selected").getAsBoolean()) - .findFirst() - .orElseThrow(() -> new IllegalStateException("No selected profile found!?")) - .getAsJsonObject("members").get(uuid).getAsJsonObject(); + JsonObject profile = SkyblockerMod.GSON.fromJson(response.content(), JsonObject.class); + profiles.put(name, ObjectLongPair.of(profile, System.currentTimeMillis())); - players.put(name, ObjectLongPair.of(player, System.currentTimeMillis())); - return player; + return profile; } catch (Exception e) { LOGGER.error("[Skyblocker Profile Utils] Failed to get Player Profile Data for players {}, is the API Down/Limited?", name, e); } diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/base_plate.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/base_plate.png new file mode 100644 index 0000000000000000000000000000000000000000..0edf57051a911cba0a29ca6559578e84b8fa761e GIT binary patch literal 5665 zcmeHLYfuwe7H$wEQi^0zVOd;59MC~J>F#t$x+MZ3JmQKXpg2;DcKQKP@-S&2APnf> z19d=39k*PkK*y<}6d#L&x;}A0l+_(vcFiF23@ZOoKEo*i5k12BL zKKGpOp6|Qo^rb0|pB*#C-Paw0pfTE5O#%ctsloMEf{|dnAq)rxzs}|+E#MQVY?0Yw z;`AA;h|e*zBG#toAjo#b5O46P6A4~)y`Sas^kUmGzNXQ5;O1EOK*PbU-Qg!)nxB7E z@)#4D+p4vO8qQT={CmsB&Ws@7ukc}d(Y|wm*z(e7*jJbf?TqNDu^6gTlYM00`AZtp{m9aLlg>0LOt~jcbc7Wt zM@^mPRXjF4aC_yRxtq(j1uRidnP2|&^32?g1=$N@!#aHzHf6Rh3IZI9^pTPA+Q`U$ zN`T1r{4!N+UAW(wMTzUh9+Gd4l1JQEwz{o7`0=Kl>o-3+wd-8zqF&2(B#=uS*cIXZ-bddb?R@x~_Ubz4rqRFt(}mDwYZnCf9QbElm9Da0RxgZdnY!rQ zu`2WRwyI0MRn|E#zg|2yrP1;k=c5hwUxjPVrB|%;4X#gbiABqHyeL{zaN)t1jRBkX zxJQ?`e!D|$dEQ#keJtf5uDTWI>J{F4=1^F}ikGPs*S@;fMPD_RLod@>XWo5w7hZgS z(_=1qy`NLv)!H+e$(wo#&re+ZSi5y}W7%_jRl+D+*ypAoX!7CO)@@f)(k6X5HoWBM z!jn!O9xIA5cK`C@%PS5aE@3c^ndFj%3sIvAGsDF?V(nox7AK7;)GP z_fQ)N01PZoiEM@pqm{I„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

jAo^VO}W(VDIM}q zCGd-(NWu$=Jq)RczWZVgi%lI#q>~y5av$Oyh91m40Y)f~hjmGOwHlt=W!J0ovx3Bn z0-Ja=`AEnyW`Oe~$KfX0D&RKWXu<(SK??^>Hi5RJP`Sgh0>V60sSvqAL_8cY+knpq zabTn%ZUPo7ZUZI;XUu|yr%4hDoHd2wiKvLG5(HDDQmF()1%c7xvsgKtu|bO(4T*DR z8;=9d3pK_&2PkrWL9w47oH_#qJq`x67I;Ovr8w_Q&glF)>uGzb(QazJ}P z3=2|Bz7O__Ay}pWRZq%FF=mpYO_a@QW+>)9s2oORREsJrMH-B@#E7~uEJ_DS3)D(Q z1PMFJhIK|EP^73=l7e=<8Um(z4h?(Ih7y1RTtI<{l%!28X<=!rm$I-FdZxZlQY@K* zmjuyQ`~T4D_QCR#i(V|sX#U!SDY>W0;M(L{@--+XHWP*=wgn6LWC}8?pcOFktAqHu{H z>4>yK1)9(bTQIOx3y0%l0jRD6k_;y;I7NABq?DPZta{?Xr)Oe}J{xEC;UvkJaT9vQ zISTbKD?@U?1g%Drr-z68&C*M$h{5#o3AG^(E@nRQKg*p2q;yauOVWlqGivn2P^TZz zOYNx-@HbFq4{!!VJvzyV^c|IJRIU*zFe311b&bk3A_YbS9<8o#O)lO2vmy+m54;#U z3Htt>vlX2pCq{~YC~Gu^9Q8_z|H@H=j9SI*Db)6*&weN?t;e~{i%c_>!V1MHh13Iv zCh^q^1uP~i0ZjesUDc2^9f3@4*Anl7E`Fbhw~l@0(yi0e^D;9td+TrRYW?ivPb-&Y t3JuFc2RfHHo6r+BOX~W~_CL>AQD)9BpPg zOq>42&FtRp^YMG1=l49%JNs^|sw|&voNqK3471%8u4?$7rC;f(@c#Q(=E1w+uWjCX zt(uP_5k>Y(L4atj5rBZ0zdcfLBR$=FiMet4>#NpBZ_~9a`(FsY zVd=SBn@c`8GP3Lz*VVP%boP6@mJrR&VP@{^#}A(BegE0^++9D6JwIz^-h1HbHJ8#N zZ_T=pf2r?qr~k*#oKY5hJ7e`z!;N2i{kI0rz4e1{ zw{@2NT=Um{I@ptO?CP<+{5=1kU%%orhTi#|!@hbj<8QCr_~V_N!EX(T?d5ry=J#Lg zq&ovN_JixiP3_rv8*aWByU?AHRidnD`A3)f0pAgrx!LP`iCI+mhsW!~d>!E(o;Tdx zvvLRCyP}Sfo9Z6P9vWD%c6Z6%pX42=RKN0A;ls}`u6O&oJF^O(Za7;(ZtMQ6;(^b% zUVe7_Bl*kj*_@NH(7EBt&7+@RjQ0D(Z6`dRl((O}|NNb~Ggcp5G`RBohwr?(q9ob+ zq!tP-HX?RK^mEm&vQY^5=0F=XEyCe=G#WKU%_dn1;3UhkI6>hQg+U8UZ4GOD3=6Bd zIz$4)1yn(iBAO(J5gn8F$<3MrMWG%Ui!T)Mc*fzw>L?444?M<4aMDEJp%9+zp=zZq z5M(r#OpvujP07_dxSj$a@5QTC7NV5`*ry~kD2!aqaRpF}Sq$SJUo~m&V zoq~WAip0Gj*^?|a$v;8Xq}X&#Je}k~p!+!PB>K9;ijEFyyuqD{aoqLso}i^yU;Yi3ES#Z1tom;~hxs~R5`fDQ%0O%lZ6S(3Fe zK7iRopBb~5X&U2!nZ!tojkWoFHr~&WNf3`J66{Jom>iW3B|<1a!BVu%M`9GYE%?};m z3`k*7j;a$XuM`3`8n5$7(j;r8EG)rTNt&_I6GnAFQDHCYs3c)BGjWYx7!IZbspa)f zg#d9m%!YF+fY)TjE6YI#st1MWp5tl{94L|3co(k$2ue^Ej<9hQ?WHKr!f+JxO@icz zBzswu{H_1VS|1<8o+x^Sq{8)EejU(gI9un81wD9qxc8O%Zp0ApC-!&qQ5TWx06Li+y&U6uV>lvlvw z0OS#J1sgQZ6|!)&R0VO?#-oWg0KE>7GK{ccB6)f%N(@ZNc)Gi$>6#J)Q!<|JuKyWb#)-2c2*VG& zC_D+iW^6hSPm!}C70XqF!IZ0CDX|}wv_N5+=Ju4PU7h(rHjy(zK3E4u^R&`>%_)cU z1BM~~)eA%24-><4HU#wfD(`Yn{Jr(Y1^q;L>nG4YUog=yEuKBp# zSFcoqC>D-Lez6H+N?RCWFedsnn%KL&o6apBrZWG1WBFZa3y*>PkG|frbNBt(@wa`? z`3iaF&E_KULC4_r-!D$T^I%bJW4f`f?()jagtzC+P|uA;&m6cCTa|8#UoP7C_g6O$ zU&X6(A36VUcCY9C{>X~y8IXditLm4Q<@wPyFON zJwF44*=jkH`9keL{j$V6B?q^TTb}uv`?Val)9BptThkvlJU(RKo7nK~(we~&KQ7Gb;aeyyvrPN3LfSjI zl|_%C+u>)*Bi=IKMf!N+!9eCXtf8;J+BnFHB1zurytZ9lL#zW!Ck zUwi#jf7bB$aA8rQ|Kk@gak}6ezjN3-PGwy^@{f;h<)7LxCUlk;E-}9U^SCu0uo6#R zD{1arQP_R+iP(F6S=r9W*4BUa%I85>AmdRV_Y_lF{HHDTAy7y8KFs~FuYdb~^3c{g zMry9RZ^c+*^}|m)pMRn7c%{5(Q}I3DW87~Z?u%y^Khkivg5KNrSw-IGdoKQ9-+e`! z?|3vnYmKY>(#=;szYrbthuTNHe=F~NE9cy;ucmjLC_T0P+y`$Q+v-d^d1mN`W7iKh z4!yVf&u5=qXWsnw6e4Wx2;&HRYj75Dz4QyH9U{ofvfYgg2amgn|w6+ zDahH(fZ!>PBTNRXKrp<~L;#wG77m&i!C;v|g z3?dMOeFsGMRD1Rxm>1(E}*;Ox38j{`U8Q_!TPstG85zRZrSV+sqVgWvmQMF;>Ggs18PC)Qc)BP3etHazrf*i_$^T0<}{SLDG)0 zVOuMlN4e_PU&T2nQ!IrWx$Z4#4<3B!`hf(3jk1sSwLAsHv)N)7Q1AQXUT z|CnglNxS$3&0v7QfGUnE$kV8wD8Lbn1$CCuX5ei$6KC@a=GpABN;H4~-!#%z4F0jhOCk`a`JplP20DP?A9D^A}2^wgHNni&h48kz_bV>S>R#Q?&{ zm>6i{%rxLqMVk`ue@kz=Bc{@`lj?>zSxh_mU*%2#X6&FymXr%o1bJ(`p(NWFV~zDm=k!uyXNJZlLB)B&v)1VO)lN^SrLZN2VNAN1iLgt zqv#a*m2kzkWsOE(pk8UQpE_HSQLA{oW!mwKyrntXqgZJhGUX~|^@>Xhss{{B@~amL zSX5LRF!ifwIg@@V~nE@sUz|SMm#nE literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_body.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_body.png new file mode 100644 index 0000000000000000000000000000000000000000..379557e49e5f85c41399ed798746508153d025b0 GIT binary patch literal 5058 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/run_icon.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/run_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d83fad7036804cdda15570e6e8377e834bcd5afe GIT binary patch literal 5042 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 VEnnS=zX>Z*SkmU3_NC;O{};-x_W}R_ literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/spider.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/spider.png new file mode 100644 index 0000000000000000000000000000000000000000..a5daa3d651cd86e9c71953901cbfd9fa2a877c5e GIT binary patch literal 9659 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 K#kca7!T$%YL%tCJ literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/vampire.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/vampire.png new file mode 100644 index 0000000000000000000000000000000000000000..efbe985d444994cebf66bbb2b936d10393d15f78 GIT binary patch literal 10351 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/zombie.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/zombie.png new file mode 100644 index 0000000000000000000000000000000000000000..37ea069f3780e101810d3eb7e7ce47cbb131c414 GIT binary patch literal 9167 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 literal 0 HcmV?d00001 From 42439425b43537145a5dc326d8a7f4eb932c6547 Mon Sep 17 00:00:00 2001 From: nmccullagh Date: Sat, 29 Jun 2024 05:40:12 +0100 Subject: [PATCH 2/5] command changes and some logic cleanup --- .../profileviewer/ProfileViewerScreen.java | 27 ++++--------------- .../WardrobeInventoryItemLoader.java | 3 ++- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java index 939d2bd166..4829a712fb 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java @@ -5,6 +5,7 @@ import com.google.gson.JsonParser; import com.mojang.brigadier.Command; import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.mixins.accessors.SkullBlockEntityAccessor; import de.hysky.skyblocker.skyblock.profileviewer.collections.CollectionsPage; @@ -187,26 +188,7 @@ public static void initClass() { fetchCollectionsData(); // caching on launch ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { - dispatcher.register( - ClientCommandManager.literal(SkyblockerMod.NAMESPACE) - .then(ClientCommandManager.literal("pv") - .then(ClientCommandManager.argument("username", StringArgumentType.string()) - .executes(context -> { - String username = StringArgumentType.getString(context, "username"); - Command cmd = Scheduler.queueOpenScreenCommand(() -> new ProfileViewerScreen(username)); - return cmd.run(context); - }) - ) - .executes(context -> { - String username = MinecraftClient.getInstance().getSession().getUsername(); - Command cmd = Scheduler.queueOpenScreenCommand(() -> new ProfileViewerScreen(username)); - return cmd.run(context); - }) - ) - ); - - dispatcher.register( - ClientCommandManager.literal("pv") + LiteralArgumentBuilder literalArgumentBuilder = ClientCommandManager.literal("pv") .then(ClientCommandManager.argument("username", StringArgumentType.string()) .executes(context -> { String username = StringArgumentType.getString(context, "username"); @@ -218,8 +200,9 @@ public static void initClass() { String username = MinecraftClient.getInstance().getSession().getUsername(); Command cmd = Scheduler.queueOpenScreenCommand(() -> new ProfileViewerScreen(username)); return cmd.run(context); - }) - ); + }); + dispatcher.register(literalArgumentBuilder); + dispatcher.register(ClientCommandManager.literal(SkyblockerMod.NAMESPACE).then(literalArgumentBuilder)); }); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java index cb3a866a33..4aebb26f11 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java @@ -23,11 +23,12 @@ public List loadItems(JsonObject data) { try { itemList.addAll(super.loadItems(data)); if (activeSlot != -1) { + List activeArmour = super.loadItems(activeArmorSet); for (int i = 0; i < 4; i++) { int baseIndex = activeSlot % 9; int page = activeSlot / 9; int slotIndex = (page * 36) + (i * 9) + baseIndex - 1; - itemList.set(slotIndex, super.loadItems(activeArmorSet).reversed().get(i)); + itemList.set(slotIndex, activeArmour.get(i)); } } } catch (Exception e) { From ccc04297a8375cc8a684c77dcb15bfe7cb7b5844 Mon Sep 17 00:00:00 2001 From: nmccullagh Date: Sat, 29 Jun 2024 05:42:22 +0100 Subject: [PATCH 3/5] single import removal lol --- .../profileviewer/inventory/itemLoaders/InventoryItemLoader.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java index 900fcc00ee..f73661a13c 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java @@ -4,7 +4,6 @@ import net.minecraft.item.ItemStack; import java.util.ArrayList; -import java.util.Collections; import java.util.List; public class InventoryItemLoader extends ItemLoader { From 033819242330fecd1e74d7c75f3ebb4c92748461 Mon Sep 17 00:00:00 2001 From: nmccullagh Date: Sat, 29 Jun 2024 05:55:52 +0100 Subject: [PATCH 4/5] lol revert thre removal of the reversion --- .../inventory/itemLoaders/WardrobeInventoryItemLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java index 4aebb26f11..9d4347264c 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java @@ -23,7 +23,7 @@ public List loadItems(JsonObject data) { try { itemList.addAll(super.loadItems(data)); if (activeSlot != -1) { - List activeArmour = super.loadItems(activeArmorSet); + List activeArmour = super.loadItems(activeArmorSet).reversed(); for (int i = 0; i < 4; i++) { int baseIndex = activeSlot % 9; int page = activeSlot / 9; From d824171262521557c73688068804c5d9119fbc7c Mon Sep 17 00:00:00 2001 From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> Date: Sat, 29 Jun 2024 14:01:33 +0800 Subject: [PATCH 5/5] Refactor profile viewer command and update open screen commands --- .../profileviewer/ProfileViewerScreen.java | 13 +----- .../skyblocker/utils/scheduler/Scheduler.java | 46 +++++++++++++++++-- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java index 4829a712fb..1d0b21cacd 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java @@ -3,7 +3,6 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import com.mojang.brigadier.Command; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import de.hysky.skyblocker.SkyblockerMod; @@ -190,17 +189,9 @@ public static void initClass() { ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { LiteralArgumentBuilder literalArgumentBuilder = ClientCommandManager.literal("pv") .then(ClientCommandManager.argument("username", StringArgumentType.string()) - .executes(context -> { - String username = StringArgumentType.getString(context, "username"); - Command cmd = Scheduler.queueOpenScreenCommand(() -> new ProfileViewerScreen(username)); - return cmd.run(context); - }) + .executes(Scheduler.queueOpenScreenFactoryCommand(context -> new ProfileViewerScreen(StringArgumentType.getString(context, "username")))) ) - .executes(context -> { - String username = MinecraftClient.getInstance().getSession().getUsername(); - Command cmd = Scheduler.queueOpenScreenCommand(() -> new ProfileViewerScreen(username)); - return cmd.run(context); - }); + .executes(Scheduler.queueOpenScreenCommand(() -> new ProfileViewerScreen(MinecraftClient.getInstance().getSession().getUsername()))); dispatcher.register(literalArgumentBuilder); dispatcher.register(ClientCommandManager.literal(SkyblockerMod.NAMESPACE).then(literalArgumentBuilder)); }); diff --git a/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java b/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java index 2f5375fe24..547adc5ccf 100644 --- a/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java +++ b/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java @@ -2,6 +2,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; import it.unimi.dsi.fastutil.ints.AbstractInt2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; @@ -14,6 +15,7 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Function; import java.util.function.Supplier; /** @@ -73,18 +75,56 @@ public void scheduleCyclic(Runnable task, int period, boolean multithreaded) { } } + /** + * Creates a command that queues a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. + * + * @param screenFactory the factory of the screen to open + * @return the command + */ + public static Command queueOpenScreenFactoryCommand(Function, Screen> screenFactory) { + return context -> queueOpenScreen(screenFactory.apply(context)); + } + + /** + * Creates a command that queues a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. + * + * @param screenSupplier the supplier of the screen to open + * @return the command + */ public static Command queueOpenScreenCommand(Supplier screenSupplier) { - return context -> INSTANCE.queueOpenScreen(screenSupplier); + return context -> queueOpenScreen(screenSupplier.get()); + } + + /** + * Creates a command that queues a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. + * + * @param screen the screen to open + * @return the command + */ + public static Command queueOpenScreenCommand(Screen screen) { + return context -> queueOpenScreen(screen); } /** * Schedules a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. * + * @deprecated Use {@link #queueOpenScreen(Screen)} instead * @param screenSupplier the supplier of the screen to open * @see #queueOpenScreenCommand(Supplier) */ - public int queueOpenScreen(Supplier screenSupplier) { - MinecraftClient.getInstance().send(() -> MinecraftClient.getInstance().setScreen(screenSupplier.get())); + @Deprecated(forRemoval = true) + public static int queueOpenScreen(Supplier screenSupplier) { + return queueOpenScreen(screenSupplier.get()); + } + + /** + * Schedules a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. + * + * @param screen the screen to open + * @see #queueOpenScreenFactoryCommand(Function) + */ + public static int queueOpenScreen(Screen screen) { + MinecraftClient.getInstance().send(() -> MinecraftClient.getInstance().setScreen(screen)); return Command.SINGLE_SUCCESS; }