aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerMod.java7
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerScreen.java33
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java64
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java14
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java18
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java7
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java27
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java6
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java16
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/ClientWorldMixin.java23
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java21
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java17
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java18
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java12
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java22
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/accessors/MinecraftClientAccessor.java12
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/PetCache.java27
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/Tips.java102
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java12
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ControlTestHelper.java77
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DisciplineTestHelper.java66
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DojoManager.java256
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ForceTestHelper.java81
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/MasteryTestHelper.java68
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/StaminaTestHelper.java279
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/SwiftnessTestHelper.java34
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/TenacityTestHelper.java100
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dwarven/CommissionHighlight.java38
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java10
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dwarven/NucleusWaypoints.java61
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java23
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java32
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java10
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java194
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java1
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java6
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java37
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerNavButton.java65
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerPage.java19
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java230
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerTextWidget.java55
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/CollectionsPage.java100
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/GenericCategory.java136
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonClassWidget.java62
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonFloorRunsWidget.java55
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonHeaderWidget.java46
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonMiscStatsWidgets.java61
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonsPage.java39
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Inventory.java120
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/InventoryPage.java113
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PaginationButton.java46
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Pet.java186
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PlayerInventory.java73
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/BackpackItemLoader.java34
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java29
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/ItemLoader.java139
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/PetsInventoryItemLoader.java42
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java40
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillWidget.java95
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillsPage.java93
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayerWidget.java93
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayersPage.java41
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/LevelFinder.java307
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SkullCreator.java27
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SubPageSelectButton.java65
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java1
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/Ico.java38
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDeathWidget.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ElectionWidget.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenSkillsWidget.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ReputationWidget.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/SkillsWidget.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java5
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ApiUtils.java6
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Http.java33
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java37
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Utils.java74
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java46
-rw-r--r--src/main/resources/assets/skyblocker/lang/en_us.json53
-rw-r--r--src/main/resources/assets/skyblocker/lang/lol_us.json3
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/profile_viewer/base_plate.pngbin0 -> 5665 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/profile_viewer/blaze.pngbin0 -> 10784 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon.pngbin0 -> 4301 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_highlighted.pngbin0 -> 4516 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled.pngbin0 -> 4515 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled_highlighted.pngbin0 -> 4513 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_body.pngbin0 -> 5058 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_header.pngbin0 -> 4940 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/profile_viewer/enderman.pngbin0 -> 7829 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/profile_viewer/icon_data_widget.pngbin0 -> 4979 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/profile_viewer/run_icon.pngbin0 -> 5042 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/profile_viewer/spider.pngbin0 -> 9659 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/profile_viewer/vampire.pngbin0 -> 10351 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/profile_viewer/wolf.pngbin0 -> 8813 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/profile_viewer/zombie.pngbin0 -> 9167 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/sprites/equipment/empty_icon.pngbin0 -> 173 bytes
-rw-r--r--src/main/resources/skyblocker.accesswidener21
-rw-r--r--src/main/resources/skyblocker.mixins.json3
106 files changed, 4425 insertions, 188 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
index d793e73d..f1eb4321 100644
--- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
@@ -11,6 +11,7 @@ import de.hysky.skyblocker.skyblock.chat.ChatRuleAnnouncementScreen;
import de.hysky.skyblocker.skyblock.chat.ChatRulesHandler;
import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder;
import de.hysky.skyblocker.skyblock.chocolatefactory.TimeTowerReminder;
+import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager;
import de.hysky.skyblocker.skyblock.crimson.kuudra.Kuudra;
import de.hysky.skyblocker.skyblock.dungeon.*;
import de.hysky.skyblocker.skyblock.dungeon.partyfinder.PartyFinderScreen;
@@ -36,6 +37,8 @@ import de.hysky.skyblocker.skyblock.item.tooltip.BackpackPreview;
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.mayors.JerryTimer;
+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 +108,7 @@ public class SkyblockerMod implements ClientModInitializer {
Utils.init();
SkyblockerConfigManager.init();
SkyblockerScreen.initClass();
+ ProfileViewerScreen.initClass();
Tips.init();
NEURepoManager.init();
//ImageRepoLoader.init();
@@ -179,8 +183,10 @@ public class SkyblockerMod implements ClientModInitializer {
ApiUtils.init();
Debug.init();
Kuudra.init();
+ DojoManager.init();
RenderHelper.init();
FancyStatusBars.init();
+ SkyblockInventoryScreen.initEquipment();
EventNotifications.init();
containerSolverManager.init();
statusBarTracker.init();
@@ -190,6 +196,7 @@ public class SkyblockerMod implements ClientModInitializer {
EggFinder.init();
TimeTowerReminder.init();
SkyblockTime.init();
+ JerryTimer.init();
TooltipManager.init();
SlotTextManager.init();
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java b/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java
index 0196a37a..9686c6d8 100644
--- a/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java
@@ -1,7 +1,5 @@
package de.hysky.skyblocker;
-import java.time.LocalDate;
-
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.Tips;
import de.hysky.skyblocker.utils.scheduler.Scheduler;
@@ -11,11 +9,7 @@ import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.ConfirmLinkScreen;
import net.minecraft.client.gui.screen.Screen;
-import net.minecraft.client.gui.widget.ButtonWidget;
-import net.minecraft.client.gui.widget.GridWidget;
-import net.minecraft.client.gui.widget.MultilineTextWidget;
-import net.minecraft.client.gui.widget.TextWidget;
-import net.minecraft.client.gui.widget.ThreePartsLayoutWidget;
+import net.minecraft.client.gui.widget.*;
import net.minecraft.screen.ScreenTexts;
import net.minecraft.text.OrderedText;
import net.minecraft.text.StringVisitable;
@@ -23,6 +17,8 @@ import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.Language;
+import java.time.LocalDate;
+
public class SkyblockerScreen extends Screen {
private static final int SPACING = 8;
private static final int BUTTON_WIDTH = 210;
@@ -36,7 +32,8 @@ public class SkyblockerScreen extends Screen {
private static final Text TRANSLATE_TEXT = Text.translatable("text.skyblocker.translate");
private static final Text MODRINTH_TEXT = Text.translatable("text.skyblocker.modrinth");
private static final Text DISCORD_TEXT = Text.translatable("text.skyblocker.discord");
- private final ThreePartsLayoutWidget layout = new ThreePartsLayoutWidget(this);
+ private ThreePartsLayoutWidget layout;
+ private MultilineTextWidget tip;
static {
LocalDate date = LocalDate.now();
@@ -44,7 +41,7 @@ public class SkyblockerScreen extends Screen {
ICON = date.getMonthValue() == 4 && date.getDayOfMonth() == 1 ? Identifier.of(SkyblockerMod.NAMESPACE, "icons.png") : Identifier.of(SkyblockerMod.NAMESPACE, "icon.png");
}
- private SkyblockerScreen() {
+ public SkyblockerScreen() {
super(TITLE);
}
@@ -57,6 +54,7 @@ public class SkyblockerScreen extends Screen {
@Override
protected void init() {
+ this.layout = new ThreePartsLayoutWidget(this, 50, 100);
this.layout.addHeader(new IconTextWidget(this.getTitle(), this.textRenderer, ICON));
GridWidget gridWidget = this.layout.addBody(new GridWidget()).setSpacing(SPACING);
@@ -72,17 +70,26 @@ public class SkyblockerScreen extends Screen {
adder.add(ButtonWidget.builder(DISCORD_TEXT, ConfirmLinkScreen.opening(this, "https://discord.gg/aNNJHQykck")).width(HALF_BUTTON_WIDTH).build());
adder.add(ButtonWidget.builder(ScreenTexts.DONE, button -> this.close()).width(BUTTON_WIDTH).build(), 2);
- MultilineTextWidget tip = new MultilineTextWidget(Text.translatable("skyblocker.tips.tip", Tips.nextTipInternal()), this.textRenderer)
- .setCentered(true)
- .setMaxWidth((int) (this.width * 0.7));
+ GridWidget footerGridWidget = this.layout.addFooter(new GridWidget()).setSpacing(SPACING).setRowSpacing(0);
+ footerGridWidget.getMainPositioner().alignHorizontalCenter();
+ GridWidget.Adder footerAdder = footerGridWidget.createAdder(2);
+ footerAdder.add(tip = new MultilineTextWidget(Tips.nextTip(), this.textRenderer).setCentered(true).setMaxWidth((int) (this.width * 0.7)), 2);
+ footerAdder.add(ButtonWidget.builder(Text.translatable("skyblocker.tips.previous"), button -> {
+ tip.setMessage(Tips.previousTip());
+ layout.refreshPositions();
+ }).build());
+ footerAdder.add(ButtonWidget.builder(Text.translatable("skyblocker.tips.next"), button -> {
+ tip.setMessage(Tips.nextTip());
+ layout.refreshPositions();
+ }).build());
- this.layout.addFooter(tip);
this.layout.refreshPositions();
this.layout.forEachChild(this::addDrawableChild);
}
@Override
protected void initTabNavigation() {
+ super.initTabNavigation();
this.layout.refreshPositions();
}
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java
index fed8fa96..e8127a0e 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java
@@ -81,6 +81,68 @@ public class CrimsonIsleCategory {
.controller(IntegerFieldControllerBuilder::create)
.build())
.build())
- .build();
+ //dojo
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("skyblocker.crimson.dojo"))
+ .collapsed(false)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.crimson.dojo.forceHelper"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.forceHelper.@Tooltip")))
+ .binding(config.crimsonIsle.dojo.enableForceHelper,
+ () -> config.crimsonIsle.dojo.enableForceHelper,
+ newValue -> config.crimsonIsle.dojo.enableForceHelper = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.crimson.dojo.staminaHelper"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.staminaHelper.@Tooltip")))
+ .binding(config.crimsonIsle.dojo.enableStaminaHelper,
+ () -> config.crimsonIsle.dojo.enableStaminaHelper,
+ newValue -> config.crimsonIsle.dojo.enableStaminaHelper = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.crimson.dojo.masteryHelper"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.masteryHelper.@Tooltip")))
+ .binding(config.crimsonIsle.dojo.enableMasteryHelper,
+ () -> config.crimsonIsle.dojo.enableMasteryHelper,
+ newValue -> config.crimsonIsle.dojo.enableMasteryHelper = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.crimson.dojo.disciplineHelper"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.disciplineHelper.@Tooltip")))
+ .binding(config.crimsonIsle.dojo.enableDisciplineHelper,
+ () -> config.crimsonIsle.dojo.enableDisciplineHelper,
+ newValue -> config.crimsonIsle.dojo.enableDisciplineHelper = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.crimson.dojo.swiftnessHelper"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.swiftnessHelper.@Tooltip")))
+ .binding(config.crimsonIsle.dojo.enableSwiftnessHelper,
+ () -> config.crimsonIsle.dojo.enableSwiftnessHelper,
+ newValue -> config.crimsonIsle.dojo.enableSwiftnessHelper = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.crimson.dojo.controlHelper"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.controlHelper.@Tooltip")))
+ .binding(config.crimsonIsle.dojo.enableControlHelper,
+ () -> config.crimsonIsle.dojo.enableControlHelper,
+ newValue -> config.crimsonIsle.dojo.enableControlHelper = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.crimson.dojo.tenacityHelper"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.tenacityHelper.@Tooltip")))
+ .binding(config.crimsonIsle.dojo.enableTenacityHelper,
+ () -> config.crimsonIsle.dojo.enableTenacityHelper,
+ newValue -> config.crimsonIsle.dojo.enableTenacityHelper = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
+ .build();
}
}
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
index 279b7f6a..96bb226d 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
@@ -1,5 +1,6 @@
package de.hysky.skyblocker.config.categories;
+import de.hysky.skyblocker.SkyblockerScreen;
import de.hysky.skyblocker.config.ConfigUtils;
import de.hysky.skyblocker.config.SkyblockerConfig;
import de.hysky.skyblocker.config.configs.GeneralConfig;
@@ -17,6 +18,13 @@ public class GeneralCategory {
return ConfigCategory.createBuilder()
.name(Text.translatable("skyblocker.config.general"))
+ //Skyblocker Screen
+ .option(ButtonOption.createBuilder()
+ .name(Text.translatable("skyblocker.skyblockerScreen"))
+ .text(Text.translatable("text.skyblocker.open"))
+ .action((screen, opt) -> MinecraftClient.getInstance().setScreen(new SkyblockerScreen()))
+ .build())
+
//Ungrouped Options
.option(Option.<Boolean>createBuilder()
.name(Text.translatable("skyblocker.config.general.enableTips"))
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java
index 9e4935cb..1518dfe7 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java
@@ -38,6 +38,20 @@ public class HelperCategory {
.build())
.build())
+ //Jerry Timer
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.jerry"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.jerry.enableJerryTimer"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.jerry.enableJerryTimer.@Tooltip")))
+ .binding(defaults.helpers.jerry.enableJerryTimer,
+ () -> config.helpers.jerry.enableJerryTimer,
+ newValue -> config.helpers.jerry.enableJerryTimer = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
//Experiments Solver
.group(OptionGroup.createBuilder()
.name(Text.translatable("skyblocker.config.helpers.experiments"))
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java
index e77e9f4b..4e11d869 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java
@@ -30,6 +30,14 @@ public class MiningCategory {
.controller(ConfigUtils::createBooleanController)
.build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.mining.commissionHighlight"))
+ .binding(defaults.mining.commissionHighlight,
+ () -> config.mining.commissionHighlight,
+ newValue -> config.mining.commissionHighlight = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+
//Dwarven Mines
.group(OptionGroup.createBuilder()
.name(Text.translatable("skyblocker.config.mining.dwarvenMines"))
@@ -95,7 +103,15 @@ public class MiningCategory {
newValue -> config.mining.crystalHollows.metalDetectorHelper = newValue)
.controller(ConfigUtils::createBooleanController)
.build())
- .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.mining.crystalHollows.nucleusWaypoints"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.mining.crystalHollows.nucleusWaypoints.@Tooltip")))
+ .binding(defaults.mining.crystalHollows.nucleusWaypoints,
+ () -> config.mining.crystalHollows.nucleusWaypoints,
+ newValue -> config.mining.crystalHollows.nucleusWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
//Crystal Hollows Map
.group(OptionGroup.createBuilder()
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java
index a2a0f815..889b253a 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java
@@ -68,6 +68,13 @@ public class UIAndVisualsCategory {
newValue -> config.uiAndVisuals.hideStatusEffectOverlay = newValue)
.controller(ConfigUtils::createBooleanController)
.build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.uiAndVisuals.showEquipmentInInventory"))
+ .binding(defaults.uiAndVisuals.showEquipmentInInventory,
+ () -> config.uiAndVisuals.showEquipmentInInventory,
+ newValue -> config.uiAndVisuals.showEquipmentInInventory = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
//Chest Value FIXME change dropdown to color controller
.group(OptionGroup.createBuilder()
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java
index 8dd93aee..451d1983 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java
@@ -7,6 +7,10 @@ public class CrimsonIsleConfig {
@SerialEntry
public Kuudra kuudra = new Kuudra();
+ @SerialEntry
+ public Dojo dojo = new Dojo();
+
+
public static class Kuudra {
@SerialEntry
public boolean supplyWaypoints = true;
@@ -32,4 +36,27 @@ public class CrimsonIsleConfig {
@SerialEntry
public int arrowPoisonThreshold = 32;
}
+
+ public static class Dojo {
+ @SerialEntry
+ public boolean enableForceHelper = true;
+
+ @SerialEntry
+ public boolean enableStaminaHelper = true;
+
+ @SerialEntry
+ public boolean enableMasteryHelper = true;
+
+ @SerialEntry
+ public boolean enableDisciplineHelper = true;
+
+ @SerialEntry
+ public boolean enableSwiftnessHelper = true;
+
+ @SerialEntry
+ public boolean enableControlHelper = true;
+
+ @SerialEntry
+ public boolean enableTenacityHelper = true;
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java
index c0314924..33047f1c 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java
@@ -12,6 +12,9 @@ public class HelperConfig {
public MythologicalRitual mythologicalRitual = new MythologicalRitual();
@SerialEntry
+ public Jerry jerry = new Jerry();
+
+ @SerialEntry
public Experiments experiments = new Experiments();
@SerialEntry
@@ -28,6 +31,11 @@ public class HelperConfig {
public boolean enableMythologicalRitualHelper = true;
}
+ public static class Jerry {
+ @SerialEntry
+ public boolean enableJerryTimer = false;
+ }
+
public static class Experiments {
@SerialEntry
public boolean enableChronomatronSolver = true;
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java
index a2a9bcf7..d71f57b6 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java
@@ -27,6 +27,9 @@ public class MiningConfig {
@SerialEntry
public Glacite glacite = new Glacite();
+ @SerialEntry
+ public boolean commissionHighlight = true;
+
public static class DwarvenMines {
@SerialEntry
public boolean solveFetchur = true;
@@ -61,6 +64,9 @@ public class MiningConfig {
public static class CrystalHollows {
@SerialEntry
public boolean metalDetectorHelper = true;
+
+ @SerialEntry
+ public boolean nucleusWaypoints = false;
}
public static class CrystalsHud {
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java
index e016988b..80bdb1c9 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java
@@ -28,6 +28,9 @@ public class UIAndVisualsConfig {
public boolean hideStatusEffectOverlay = false;
@SerialEntry
+ public boolean showEquipmentInInventory = true;
+
+ @SerialEntry
public ChestValue chestValue = new ChestValue();
@SerialEntry
diff --git a/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java b/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java
index c268103d..93426143 100644
--- a/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java
+++ b/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java
@@ -26,6 +26,16 @@ public final class SkyblockEvents {
}
});
+ /**
+ * Called when the player's Skyblock profile changes.
+ * @implNote This is called upon receiving the chat message for the profile change rather than the exact moment of profile change, so it may be delayed by a few seconds.
+ */
+ public static final Event<ProfileChange> PROFILE_CHANGE = EventFactory.createArrayBacked(ProfileChange.class, callbacks -> (prev, profile) -> {
+ for (ProfileChange callback : callbacks) {
+ callback.onSkyblockProfileChange(prev, profile);
+ }
+ });
+
@Environment(EnvType.CLIENT)
@FunctionalInterface
public interface SkyblockJoin {
@@ -43,4 +53,10 @@ public final class SkyblockEvents {
public interface SkyblockLocationChange {
void onSkyblockLocationChange(Location location);
}
+
+ @Environment(EnvType.CLIENT)
+ @FunctionalInterface
+ public interface ProfileChange {
+ void onSkyblockProfileChange(String prevProfileId, String profileId);
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
index 48389d40..7bbbac81 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
@@ -6,6 +6,7 @@ import com.llamalad7.mixinextras.sugar.Local;
import de.hysky.skyblocker.skyblock.CompactDamage;
import de.hysky.skyblocker.skyblock.FishingHelper;
import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder;
+import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager;
import de.hysky.skyblocker.skyblock.dungeon.DungeonScore;
import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager;
import de.hysky.skyblocker.skyblock.end.BeaconHighlighter;
@@ -97,6 +98,7 @@ public abstract class ClientPlayNetworkHandlerMixin {
@Inject(method = "onParticle", at = @At("RETURN"))
private void skyblocker$onParticle(ParticleS2CPacket packet, CallbackInfo ci) {
MythologicalRitual.onParticle(packet);
+ DojoManager.onParticle(packet);
EnderNodes.onParticle(packet);
}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/ClientWorldMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ClientWorldMixin.java
new file mode 100644
index 00000000..6c10e5d2
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixins/ClientWorldMixin.java
@@ -0,0 +1,23 @@
+package de.hysky.skyblocker.mixins;
+
+
+import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager;
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.util.math.BlockPos;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(ClientWorld.class)
+public class ClientWorldMixin {
+
+ @Inject(method = "handleBlockUpdate", at = @At("RETURN"))
+ private void skyblocker$handleBlockUpdate(BlockPos pos, BlockState state, int flags, CallbackInfo ci) {
+ if (Utils.isInCrimson()) {
+ DojoManager.onBlockUpdate(pos.toImmutable(), state);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java
index f75af09a..3c3dbd52 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java
@@ -2,7 +2,11 @@ package de.hysky.skyblocker.mixins;
import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.skyblock.dungeon.partyfinder.PartyFinderScreen;
+import de.hysky.skyblocker.skyblock.item.SkyblockInventoryScreen;
+import de.hysky.skyblocker.utils.Utils;
import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.GenericContainerScreenHandler;
import net.minecraft.screen.ScreenHandler;
@@ -22,8 +26,21 @@ public abstract class GenericContainerScreenHandlerMixin extends ScreenHandler {
public void setStackInSlot(int slot, int revision, ItemStack stack) {
super.setStackInSlot(slot, revision, stack);
SkyblockerMod.getInstance().containerSolverManager.markDirty();
- if (MinecraftClient.getInstance().currentScreen instanceof PartyFinderScreen screen) {
- screen.markDirty();
+
+ Screen currentScreen = MinecraftClient.getInstance().currentScreen;
+ switch (currentScreen) {
+ case PartyFinderScreen screen -> screen.markDirty();
+ case GenericContainerScreen screen when screen.getTitle().getString().toLowerCase().contains("equipment") -> {
+ int line = slot/9;
+ if (line > 0 && line < 5 && slot % 9 == 1) {
+ boolean empty = stack.getName().getString().trim().toLowerCase().startsWith("empty");
+ if (Utils.isInTheRift())
+ SkyblockInventoryScreen.equipment_rift[line - 1] = empty ? ItemStack.EMPTY : stack;
+ else
+ SkyblockInventoryScreen.equipment[line - 1] = empty ? ItemStack.EMPTY : stack;
+ }
+ }
+ case null, default -> {}
}
}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java
index 0d833c22..2194e7a8 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java
@@ -1,11 +1,16 @@
package de.hysky.skyblocker.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.itemlist.ItemListWidget;
import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.client.gui.screen.ButtonTextures;
import net.minecraft.client.gui.screen.ingame.InventoryScreen;
import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.gui.widget.TexturedButtonWidget;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@@ -15,4 +20,16 @@ public abstract class InventoryScreenMixin {
private RecipeBookWidget skyblocker$replaceRecipeBook(RecipeBookWidget original) {
return SkyblockerConfigManager.get().general.itemList.enableItemList && Utils.isOnSkyblock() ? new ItemListWidget() : original;
}
+
+ @WrapOperation(method = "init", at = @At(value = "NEW", target = "(IIIILnet/minecraft/client/gui/screen/ButtonTextures;Lnet/minecraft/client/gui/widget/ButtonWidget$PressAction;)Lnet/minecraft/client/gui/widget/TexturedButtonWidget;"))
+ private TexturedButtonWidget skyblocker$moveButton(int x, int y, int width, int height, ButtonTextures textures, ButtonWidget.PressAction pressAction, Operation<TexturedButtonWidget> original) {
+ if (!Utils.isOnSkyblock() || !SkyblockerConfigManager.get().uiAndVisuals.showEquipmentInInventory) return original.call(x, y, width, height, textures, pressAction);
+ return new TexturedButtonWidget(x + 21, y, width, height, textures, pressAction);
+ }
+
+ @WrapOperation(method = "method_19891", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/widget/ButtonWidget;setPosition(II)V"))
+ private void skyblocker$moveButtonWhenPressed(ButtonWidget instance, int i, int j, Operation<Void> original) {
+ if (!Utils.isOnSkyblock() || !SkyblockerConfigManager.get().uiAndVisuals.showEquipmentInInventory) original.call(instance, i, j);
+ else instance.setPosition(i + 21, j);
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java
index 3abbfbcd..1493cf26 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.config.SkyblockerConfigManager;
import de.hysky.skyblocker.injected.SkyblockerStack;
import de.hysky.skyblocker.skyblock.PetCache.PetInfo;
import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.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
@@ -252,6 +254,18 @@ public abstract class ItemStackMixin implements ComponentHolder, SkyblockerStack
return customDataString + "-LIFELINE-MANA_POOL";
}
}
+
+ case "MIDAS_SWORD" -> {
+ if (customData.getInt("winning_bid") >= 50000000) {
+ return customDataString + "_50M";
+ }
+ }
+
+ case "MIDAS_STAFF" -> {
+ if (customData.getInt("winning_bid") >= 100000000) {
+ return customDataString + "_100M";
+ }
+ }
}
return customDataString;
}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java b/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java
index b04f958f..f91ddc86 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java
@@ -1,7 +1,11 @@
package de.hysky.skyblocker.mixins;
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.item.HotbarSlotLock;
import de.hysky.skyblocker.skyblock.item.ItemProtection;
+import de.hysky.skyblocker.skyblock.item.SkyblockInventoryScreen;
import de.hysky.skyblocker.utils.JoinWorldPlaceholderScreen;
import de.hysky.skyblocker.utils.ReconfiguringPlaceholderScreen;
import de.hysky.skyblocker.utils.Utils;
@@ -9,8 +13,10 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.DownloadingTerrainScreen;
import net.minecraft.client.gui.screen.ReconfiguringScreen;
import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.ingame.InventoryScreen;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.entity.player.PlayerEntity;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@@ -54,4 +60,10 @@ public abstract class MinecraftClientMixin {
private Screen modifyJoinWorld(Screen screen) {
return Utils.isOnSkyblock() ? new JoinWorldPlaceholderScreen() : screen;
}
+
+ @WrapOperation(method = "handleInputEvents", at = @At(value = "NEW", target = "(Lnet/minecraft/entity/player/PlayerEntity;)Lnet/minecraft/client/gui/screen/ingame/InventoryScreen;"))
+ private InventoryScreen skyblocker$skyblockInventoryScreen(PlayerEntity player, Operation<InventoryScreen> original) {
+ if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().uiAndVisuals.showEquipmentInInventory) return new SkyblockInventoryScreen(player);
+ else return original.call(player);
+ }
} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java
new file mode 100644
index 00000000..a9fae752
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java
@@ -0,0 +1,22 @@
+package de.hysky.skyblocker.mixins;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager;
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.client.network.PingMeasurer;
+import net.minecraft.util.profiler.MultiValueDebugSampleLogImpl;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(PingMeasurer.class)
+public class PingMeasurerMixin {
+
+ @WrapOperation(method = "onPingResult", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/profiler/MultiValueDebugSampleLogImpl;push(J)V"))
+ private void skyblocker$onPingResult(MultiValueDebugSampleLogImpl log, long ping, Operation<Void> operation) {
+ if (Utils.isInCrimson()) {
+ DojoManager.onPingResult(ping);
+ }
+ operation.call(log, ping);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/accessors/MinecraftClientAccessor.java b/src/main/java/de/hysky/skyblocker/mixins/accessors/MinecraftClientAccessor.java
new file mode 100644
index 00000000..a750ded2
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixins/accessors/MinecraftClientAccessor.java
@@ -0,0 +1,12 @@
+package de.hysky.skyblocker.mixins.accessors;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.session.ProfileKeys;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(MinecraftClient.class)
+public interface MinecraftClientAccessor {
+ @Accessor
+ ProfileKeys getProfileKeys();
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java b/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java
index d8cd6e48..8d0406cb 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.client.gui.screen.ingame.GenericContainerScreen;
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 class PetCache {
return CACHED_PETS.containsKey(uuid) && CACHED_PETS.get(uuid).containsKey(profileId) ? CACHED_PETS.get(uuid).get(profileId) : null;
}
- public record PetInfo(String type, double exp, String tier, Optional<String> uuid) {
+ public record PetInfo(String type, double exp, String tier, Optional<String> uuid, Optional<String> item) {
public static final Codec<PetInfo> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.STRING.fieldOf("type").forGetter(PetInfo::type),
Codec.DOUBLE.fieldOf("exp").forGetter(PetInfo::exp),
Codec.STRING.fieldOf("tier").forGetter(PetInfo::tier),
- Codec.STRING.optionalFieldOf("uuid").forGetter(PetInfo::uuid))
+ Codec.STRING.optionalFieldOf("uuid").forGetter(PetInfo::uuid),
+ Codec.STRING.optionalFieldOf("heldItem").forGetter(PetInfo::item))
.apply(instance, PetInfo::new));
private static final Codec<Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, PetInfo>>> SERIALIZATION_CODEC = Codec.unboundedMap(Codec.STRING,
Codec.unboundedMap(Codec.STRING, CODEC).xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new)
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/Tips.java b/src/main/java/de/hysky/skyblocker/skyblock/Tips.java
index 513dc4b7..5d983e20 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/Tips.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/Tips.java
@@ -1,5 +1,6 @@
package de.hysky.skyblocker.skyblock;
+import com.demonwav.mcdev.annotations.Translatable;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.context.CommandContext;
@@ -14,16 +15,16 @@ import net.minecraft.command.CommandRegistryAccess;
import net.minecraft.text.ClickEvent;
import net.minecraft.text.Text;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
-import java.util.Random;
import java.util.function.Supplier;
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
public class Tips {
- private static final Random RANDOM = new Random();
- private static int previousTipIndex = -1;
- private static final List<Supplier<Text>> TIPS = List.of(
+ private static int currentTipIndex = 0;
+ private static final List<Supplier<Text>> TIPS = new ArrayList<>(List.of(
getTipFactory("skyblocker.tips.customItemNames", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker custom renameItem"),
getTipFactory("skyblocker.tips.customArmorDyeColors", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker custom dyeColor"),
getTipFactory("skyblocker.tips.customArmorTrims", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker custom armorTrim"),
@@ -36,47 +37,60 @@ public class Tips {
getTipFactory("skyblocker.tips.gallery", ClickEvent.Action.OPEN_URL, "https://hysky.de/skyblocker/gallery"),
getTipFactory("skyblocker.tips.itemRarityBackground", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"),
getTipFactory("skyblocker.tips.modMenuUpdate"),
- getTipFactory("skyblocker.tips.issues", ClickEvent.Action.OPEN_URL, "https://github.com/SkyblockerMod/Skyblocker"),
+ getTipFactory("skyblocker.tips.issues", ClickEvent.Action.OPEN_URL, "https://github.com/SkyblockerMod/Skyblocker/issues"),
getTipFactory("skyblocker.tips.beta", ClickEvent.Action.OPEN_URL, "https://github.com/SkyblockerMod/Skyblocker/actions"),
+ getTipFactory("skyblocker.tips.contribute", ClickEvent.Action.OPEN_URL, "https://github.com/SkyblockerMod/Skyblocker/wiki/contribute"),
getTipFactory("skyblocker.tips.discord", ClickEvent.Action.OPEN_URL, "https://discord.gg/aNNJHQykck"),
getTipFactory("skyblocker.tips.flameOverlay", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"),
getTipFactory("skyblocker.tips.wikiLookup", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"),
getTipFactory("skyblocker.tips.protectItem", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker protectItem"),
getTipFactory("skyblocker.tips.fairySoulsEnigmaSoulsRelics", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker fairySouls"),
- getTipFactory("skyblocker.tips.quickNav", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config")
- );
+ getTipFactory("skyblocker.tips.quickNav", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"),
+ getTipFactory("skyblocker.tips.waypoints", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker waypoint"),
+ getTipFactory("skyblocker.tips.orderedWaypoints", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker waypoint ordered"),
+ getTipFactory("skyblocker.tips.visitorHelper"),
+ getTipFactory("skyblocker.tips.slotText"),
+ getTipFactory("skyblocker.tips.profileViewer", ClickEvent.Action.SUGGEST_COMMAND, "/pv"),
+ getTipFactory("skyblocker.tips.configSearch", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"),
+ getTipFactory("skyblocker.tips.compactDamage", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"),
+ getTipFactory("skyblocker.tips.skyblockerScreen", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker"),
+ getTipFactory("skyblocker.tips.tipsClick", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker tips next"),
+ getTipFactory("skyblocker.tips.eventNotifications", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"),
+ getTipFactory("skyblocker.tips.signCalculator"),
+ getTipFactory("skyblocker.tips.calculateCommand", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker calculate"),
+ getTipFactory("skyblocker.tips.fancierBars", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker bars"),
+ getTipFactory("skyblocker.tips.crystalWaypointsShare", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker crystalWaypoints share"),
+ getTipFactory("skyblocker.tips.gardenMouseLock", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"),
+ getTipFactory("skyblocker.tips.newYearCakesHelper"),
+ getTipFactory("skyblocker.tips.accessoryHelper"),
+ getTipFactory("skyblocker.tips.fancyAuctionHouseCheapHighlight")
+ ));
private static boolean sentTip = false;
- private static Supplier<Text> getTipFactory(String key) {
+ private static Supplier<Text> getTipFactory(@Translatable String key) {
return () -> Text.translatable(key);
}
- private static Supplier<Text> getTipFactory(String key, ClickEvent.Action clickAction, String value) {
+ private static Supplier<Text> getTipFactory(@Translatable String key, ClickEvent.Action clickAction, String value) {
return () -> Text.translatable(key).styled(style -> style.withClickEvent(new ClickEvent(clickAction, value)));
}
public static void init() {
ClientCommandRegistrationCallback.EVENT.register(Tips::registerTipsCommand);
SkyblockEvents.JOIN.register(Tips::sendNextTip);
+ Collections.shuffle(TIPS);
}
private static void registerTipsCommand(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("tips")
.then(literal("enable").executes(Tips::enableTips))
.then(literal("disable").executes(Tips::disableTips))
- .then(literal("next").executes(Tips::nextTip))
+ .then(literal("previous").executes(Tips::sendPreviousTipCommand))
+ .then(literal("next").executes(Tips::sendNextTipCommand))
));
}
- private static void sendNextTip() {
- MinecraftClient client = MinecraftClient.getInstance();
- if (client.player != null && SkyblockerConfigManager.get().general.enableTips && !sentTip) {
- client.player.sendMessage(nextTip(), false);
- sentTip = true;
- }
- }
-
private static int enableTips(CommandContext<FabricClientCommandSource> context) {
SkyblockerConfigManager.get().general.enableTips = true;
SkyblockerConfigManager.save();
@@ -91,22 +105,52 @@ public class Tips {
return Command.SINGLE_SUCCESS;
}
- private static int nextTip(CommandContext<FabricClientCommandSource> context) {
- context.getSource().sendFeedback(nextTip());
+ private static void sendNextTip() {
+ MinecraftClient client = MinecraftClient.getInstance();
+ if (client.player != null && SkyblockerConfigManager.get().general.enableTips && !sentTip) {
+ client.player.sendMessage(tipMessage(nextTip()), false);
+ sentTip = true;
+ }
+ }
+
+ private static int sendNextTipCommand(CommandContext<FabricClientCommandSource> context) {
+ context.getSource().sendFeedback(tipMessage(nextTip()));
return Command.SINGLE_SUCCESS;
}
- private static Text nextTip() {
- return Constants.PREFIX.get().append(Text.translatable("skyblocker.tips.tip", nextTipInternal()))
+ public static Text nextTip() {
+ return Text.translatable("skyblocker.tips.tip", nextTipInternal());
+ }
+
+ private static Text nextTipInternal() {
+ currentTipIndex++;
+ currentTipIndex %= TIPS.size();
+ return TIPS.get(currentTipIndex).get();
+ }
+
+ private static int sendPreviousTipCommand(CommandContext<FabricClientCommandSource> context) {
+ context.getSource().sendFeedback(tipMessage(previousTip()));
+ return Command.SINGLE_SUCCESS;
+ }
+
+ public static Text previousTip() {
+ return Text.translatable("skyblocker.tips.tip", previousTipInternal());
+ }
+
+ private static Text previousTipInternal() {
+ currentTipIndex--;
+ currentTipIndex += TIPS.size();
+ currentTipIndex %= TIPS.size();
+ return TIPS.get(currentTipIndex).get();
+ }
+
+ private static Text tipMessage(Text tip) {
+ return Constants.PREFIX.get().append(tip)
+ .append(" ")
+ .append(Text.translatable("skyblocker.tips.clickPreviousTip").styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker tips previous"))))
+ .append(" ")
.append(Text.translatable("skyblocker.tips.clickNextTip").styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker tips next"))))
.append(" ")
.append(Text.translatable("skyblocker.tips.clickDisable").styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker tips disable"))));
}
-
- public static Text nextTipInternal() {
- int randomInt = RANDOM.nextInt(TIPS.size());
- while (randomInt == previousTipIndex) randomInt = RANDOM.nextInt(TIPS.size());
- previousTipIndex = randomInt;
- return TIPS.get(randomInt).get();
- }
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java
index e4f904b5..f984d751 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java
@@ -209,10 +209,10 @@ public class ChocolateFactorySolver extends ContainerSolver {
}
Matcher costMatcher = COST_PATTERN.matcher(coachLore);
- OptionalInt cost = RegexUtils.getIntFromMatcher(costMatcher, multiplierIncreaseMatcher.hasMatch() ? multiplierIncreaseMatcher.end() : 0); //Cost comes after the multiplier line
+ OptionalLong cost = RegexUtils.getLongFromMatcher(costMatcher, multiplierIncreaseMatcher.hasMatch() ? multiplierIncreaseMatcher.end() : 0); //Cost comes after the multiplier line
if (cost.isEmpty()) return Optional.empty();
- return Optional.of(new Rabbit(totalCps / totalCpsMultiplier * (nextCpsMultiplier.getAsDouble() - currentCpsMultiplier.getAsDouble()), cost.getAsInt(), COACH_SLOT));
+ return Optional.of(new Rabbit(totalCps / totalCpsMultiplier * (nextCpsMultiplier.getAsDouble() - currentCpsMultiplier.getAsDouble()), cost.getAsLong(), COACH_SLOT));
}
private static Optional<Rabbit> getRabbit(ItemStack item, int slot) {
@@ -227,9 +227,9 @@ public class ChocolateFactorySolver extends ContainerSolver {
}
Matcher costMatcher = COST_PATTERN.matcher(lore);
- OptionalInt cost = RegexUtils.getIntFromMatcher(costMatcher, cpsMatcher.hasMatch() ? cpsMatcher.end() : 0); //Cost comes after the cps line
+ OptionalLong cost = RegexUtils.getLongFromMatcher(costMatcher, cpsMatcher.hasMatch() ? cpsMatcher.end() : 0); //Cost comes after the cps line
if (cost.isEmpty()) return Optional.empty();
- return Optional.of(new Rabbit((nextCps.getAsInt() - currentCps.getAsInt())*(totalCpsMultiplier < 0 ? 1 : totalCpsMultiplier), cost.getAsInt(), slot));
+ return Optional.of(new Rabbit((nextCps.getAsInt() - currentCps.getAsInt())*(totalCpsMultiplier < 0 ? 1 : totalCpsMultiplier), cost.getAsLong(), slot));
}
private static Optional<ColorHighlight> getPrestigeHighlight() {
@@ -244,14 +244,14 @@ public class ChocolateFactorySolver extends ContainerSolver {
ItemStack item = slots.get(i);
if (!item.isOf(Items.PLAYER_HEAD)) continue;
String name = item.getName().getString();
- if (name.equals("CLICK ME!") || name.startsWith("GOLDEN RABBIT")) {
+ if (name.equals("CLICK ME!") || name.startsWith("Golden Rabbit - ")) {
highlights.add(ColorHighlight.green(i));
}
}
return highlights;
}
- private record Rabbit(double cpsIncrease, int cost, int slot) { }
+ private record Rabbit(double cpsIncrease, long cost, int slot) { }
public static final class Tooltip extends TooltipAdder {
public Tooltip() {
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ControlTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ControlTestHelper.java
new file mode 100644
index 00000000..2f616a1e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ControlTestHelper.java
@@ -0,0 +1,77 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.mob.WitherSkeletonEntity;
+import net.minecraft.util.math.Box;
+import net.minecraft.util.math.Vec3d;
+
+import java.awt.*;
+
+public class ControlTestHelper {
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+
+ private static WitherSkeletonEntity correctWitherSkeleton;
+ private static Vec3d lastPos;
+ private static long lastUpdate;
+ private static Vec3d pingOffset;
+ private static Vec3d lastPingOffset;
+
+ protected static void reset() {
+ correctWitherSkeleton = null;
+ lastPos = null;
+ lastUpdate = -1;
+ pingOffset = null;
+ lastPingOffset = null;
+ }
+
+ /**
+ * Find the correct WitherSkeleton entity when it spawns to start tracking it
+ *
+ * @param entity spawned entity
+ */
+ protected static void onEntitySpawn(Entity entity) {
+ if (entity instanceof WitherSkeletonEntity witherSkeleton && correctWitherSkeleton == null) {
+ correctWitherSkeleton = witherSkeleton;
+ }
+ }
+
+ /**
+ * Finds where to look in 3 ticks effected by ping
+ */
+ protected static void update() {
+ if (correctWitherSkeleton != null) {
+ //smoothly adjust the ping throughout the test
+ if (lastPos != null) {
+ lastPingOffset = pingOffset;
+ double ping = DojoManager.ping / 1000d;
+ //find distance between last position and current position of skeleton
+ Vec3d movementVector = correctWitherSkeleton.getPos().subtract(lastPos).multiply(1, 0.1, 1);
+ //adjust the vector to current ping (multiply by 1 + time in second until the next update offset by the players ping)
+ pingOffset = movementVector.multiply(1 + 3 / 20d + ping);
+ }
+ lastPos = correctWitherSkeleton.getPos();
+ lastUpdate = System.currentTimeMillis();
+ }
+ }
+
+ /**
+ * Renders an outline around where the player should aim (assumes values are updated every 3 ticks)
+ *
+ * @param context render context
+ */
+ protected static void render(WorldRenderContext context) {
+ if (CLIENT.player != null && correctWitherSkeleton != null && pingOffset != null && lastPingOffset != null) {
+ float tickDelta = context.tickCounter().getTickDelta(false);
+ //how long until net update
+ double updatePercent = (double) (System.currentTimeMillis() - lastUpdate) / 150;
+ Vec3d aimPos = correctWitherSkeleton.getEyePos().add(pingOffset.multiply(updatePercent)).add(lastPingOffset.multiply(1 - updatePercent));
+ Box targetBox = new Box(aimPos.add(-0.5, -0.5, -0.5), aimPos.add(0.5, 0.5, 0.5));
+ boolean playerLookingAtBox = targetBox.raycast(CLIENT.player.getCameraPosVec(tickDelta), CLIENT.player.getCameraPosVec(tickDelta).add(CLIENT.player.getRotationVec(tickDelta).multiply(30))).isPresent();
+ float[] boxColor = playerLookingAtBox ? Color.GREEN.getColorComponents(new float[]{0, 0, 0}) : Color.LIGHT_GRAY.getColorComponents(new float[]{0, 0, 0});
+ RenderHelper.renderOutline(context, targetBox, boxColor, 3, true);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DisciplineTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DisciplineTestHelper.java
new file mode 100644
index 00000000..ab0a0781
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DisciplineTestHelper.java
@@ -0,0 +1,66 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntMaps;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import net.minecraft.client.MinecraftClient;
+
+import java.util.Map;
+import java.util.Objects;
+
+public class DisciplineTestHelper {
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+
+ /**
+ * Stores what sword is needed for the name of a zombie
+ */
+ private static final Map<String, String> SWORD_TO_NAME_LOOKUP = Map.of(
+ "WOOD_SWORD", "Wood",
+ "IRON_SWORD", "Iron",
+ "GOLD_SWORD", "Gold",
+ "DIAMOND_SWORD", "Diamond"
+ );
+
+ /**
+ * Stores a color related to the color of the sword: wood = brown, iron = silver, gold = gold, diamond = cyan
+ */
+ private static final Object2IntMap<String> SWORD_TO_COLOR_LOOKUP = Object2IntMaps.unmodifiable(new Object2IntOpenHashMap<>(Map.of(
+ "WOOD_SWORD", 0xa52a2a,
+ "IRON_SWORD", 0xc0c0c0,
+ "GOLD_SWORD", 0xffd700,
+ "DIAMOND_SWORD", 0x00ffff
+ )));
+
+ /**
+ * Works out if a zombie should glow based on its name and the currently held item by the player
+ *
+ * @param name name of the zombie to see if it should glow
+ * @return if the zombie should glow
+ */
+ protected static boolean shouldGlow(String name) {
+ if (CLIENT == null || CLIENT.player == null) {
+ return false;
+ }
+ String heldId = CLIENT.player.getMainHandStack().getSkyblockId();
+ if (heldId == null) {
+ return false;
+ }
+ return Objects.equals(SWORD_TO_NAME_LOOKUP.get(heldId), name);
+ }
+
+ /**
+ * gets the color linked to the currently held sword for zombies to glow
+ *
+ * @return color linked to sword
+ */
+ protected static int getColor() {
+ if (DojoManager.currentChallenge != DojoManager.DojoChallenges.DISCIPLINE || CLIENT == null || CLIENT.player == null) {
+ return 0;
+ }
+ String heldId = CLIENT.player.getMainHandStack().getSkyblockId();
+ if (heldId == null) {
+ return 0;
+ }
+ return SWORD_TO_COLOR_LOOKUP.getOrDefault(heldId, 0);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DojoManager.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DojoManager.java
new file mode 100644
index 00000000..323c985c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DojoManager.java
@@ -0,0 +1,256 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientEntityEvents;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.fabricmc.fabric.api.event.player.AttackEntityCallback;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.network.packet.c2s.query.QueryPingC2SPacket;
+import net.minecraft.network.packet.s2c.play.ParticleS2CPacket;
+import net.minecraft.text.Text;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.Hand;
+import net.minecraft.util.Util;
+import net.minecraft.util.hit.EntityHitResult;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DojoManager {
+
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+ private static final String START_MESSAGE = "[NPC] Master Tao: Ahhh, here we go! Let's get you into the Arena.";
+ private static final Pattern TEST_OF_PATTERN = Pattern.compile("\\s+Test of (\\w+) OBJECTIVES");
+ private static final String CHALLENGE_FINISHED_REGEX = "\\s+CHALLENGE ((COMPLETED)|(FAILED))";
+
+
+ protected enum DojoChallenges {
+ NONE("none", enabled -> false),
+ FORCE("Force", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableForceHelper),
+ STAMINA("Stamina", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableStaminaHelper),
+ MASTERY("Mastery", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableMasteryHelper),
+ DISCIPLINE("Discipline", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableDisciplineHelper),
+ SWIFTNESS("Swiftness", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableSwiftnessHelper),
+ CONTROL("Control", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableControlHelper),
+ TENACITY("Tenacity", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableTenacityHelper);
+
+ private final String name;
+ private final Predicate<Boolean> enabled;
+
+ DojoChallenges(String name, Predicate<Boolean> enabled) {
+ this.name = name;
+ this.enabled = enabled;
+ }
+
+ public static DojoChallenges from(String name) {
+ return Arrays.stream(DojoChallenges.values()).filter(n -> name.equals(n.name)).findFirst().orElse(NONE);
+ }
+ }
+
+ protected static DojoChallenges currentChallenge = DojoChallenges.NONE;
+ public static boolean inArena = false;
+ protected static long ping = -1;
+
+ public static void init() {
+ ClientReceiveMessageEvents.GAME.register(DojoManager::onMessage);
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(DojoManager::render);
+ ClientPlayConnectionEvents.JOIN.register((_handler, _sender, _client) -> reset());
+ ClientEntityEvents.ENTITY_LOAD.register(DojoManager::onEntitySpawn);
+ ClientEntityEvents.ENTITY_UNLOAD.register(DojoManager::onEntityDespawn);
+ AttackEntityCallback.EVENT.register(DojoManager::onEntityAttacked);
+ Scheduler.INSTANCE.scheduleCyclic(DojoManager::update, 3);
+ }
+
+ private static void reset() {
+ inArena = false;
+ currentChallenge = DojoChallenges.NONE;
+ ForceTestHelper.reset();
+ StaminaTestHelper.reset();
+ MasteryTestHelper.reset();
+ SwiftnessTestHelper.reset();
+ ControlTestHelper.reset();
+ TenacityTestHelper.reset();
+ }
+
+ /**
+ * works out if the player is in dojo and if so what challenge based on chat messages
+ *
+ * @param text message
+ * @param overlay is overlay
+ */
+ private static void onMessage(Text text, Boolean overlay) {
+ if (!Utils.isInCrimson() || overlay) {
+ return;
+ }
+ if (Objects.equals(Formatting.strip(text.getString()), START_MESSAGE)) {
+ inArena = true;
+ //update the players ping
+ getPing();
+ return;
+ }
+ if (!inArena) {
+ return;
+ }
+ if (text.getString().matches(CHALLENGE_FINISHED_REGEX)) {
+ reset();
+ return;
+ }
+
+ //look for a message saying what challenge is starting if one has not already been found
+ if (currentChallenge != DojoChallenges.NONE) {
+ return;
+ }
+ Matcher nextChallenge = TEST_OF_PATTERN.matcher(text.getString());
+ if (nextChallenge.matches()) {
+ currentChallenge = DojoChallenges.from(nextChallenge.group(1));
+ if (!currentChallenge.enabled.test(true)) {
+ currentChallenge = DojoChallenges.NONE;
+ }
+ }
+ }
+
+ private static void getPing() {
+ ClientPlayNetworkHandler networkHandler = CLIENT.getNetworkHandler();
+ if (networkHandler != null) {
+ networkHandler.sendPacket(new QueryPingC2SPacket(Util.getMeasuringTimeMs()));
+ }
+ }
+
+ public static void onPingResult(long ping) {
+ DojoManager.ping = ping;
+ }
+
+ private static void update() {
+ if (!Utils.isInCrimson() || !inArena) {
+ return;
+ }
+ switch (currentChallenge) {
+ case STAMINA -> StaminaTestHelper.update();
+ case CONTROL -> ControlTestHelper.update();
+ }
+ }
+
+ /**
+ * called from the {@link de.hysky.skyblocker.skyblock.entity.MobGlow} class and checks the current challenge to see if zombies should be glowing
+ *
+ * @param name name of the zombie
+ * @return if the zombie should glow
+ */
+ public static boolean shouldGlow(String name) {
+ if (!Utils.isInCrimson() || !inArena) {
+ return false;
+ }
+ return switch (currentChallenge) {
+ case FORCE -> ForceTestHelper.shouldGlow(name);
+ case DISCIPLINE -> DisciplineTestHelper.shouldGlow(name);
+ default -> false;
+ };
+ }
+
+ /**
+ * called from the {@link de.hysky.skyblocker.skyblock.entity.MobGlow} class and checks the current challenge to see zombie outline color
+ *
+ * @return if the zombie should glow
+ */
+ public static int getColor() {
+ if (!Utils.isInCrimson() || !inArena) {
+ return 0xf57738;
+ }
+ return switch (currentChallenge) {
+ case FORCE -> ForceTestHelper.getColor();
+ case DISCIPLINE -> DisciplineTestHelper.getColor();
+ default -> 0xf57738;
+ };
+ }
+
+ /**
+ * when a block is updated check the current challenge and send the packet to correct helper
+ *
+ * @param pos the location of the updated block
+ * @param state the state of the new block
+ */
+ public static void onBlockUpdate(BlockPos pos, BlockState state) {
+ if (!Utils.isInCrimson() || !inArena) {
+ return;
+ }
+ switch (currentChallenge) {
+ case MASTERY -> MasteryTestHelper.onBlockUpdate(pos, state);
+ case SWIFTNESS -> SwiftnessTestHelper.onBlockUpdate(pos, state);
+ }
+ }
+
+ private static void onEntitySpawn(Entity entity, ClientWorld clientWorld) {
+ if (!Utils.isInCrimson() || !inArena || CLIENT == null || CLIENT.player == null) {
+ return;
+ }
+ // Check if within 50 blocks and 5 blocks vertically
+ if (entity.squaredDistanceTo(CLIENT.player) > 2500 || Math.abs(entity.getBlockY() - CLIENT.player.getBlockY()) > 5) {
+ return;
+ }
+ switch (currentChallenge) {
+ case FORCE -> ForceTestHelper.onEntitySpawn(entity);
+ case CONTROL -> ControlTestHelper.onEntitySpawn(entity);
+ case TENACITY -> TenacityTestHelper.onEntitySpawn(entity);
+ }
+ }
+
+ private static void onEntityDespawn(Entity entity, ClientWorld clientWorld) {
+ if (!Utils.isInCrimson() || !inArena) {
+ return;
+ }
+ switch (currentChallenge) {
+ case FORCE -> ForceTestHelper.onEntityDespawn(entity);
+ case TENACITY -> TenacityTestHelper.onEntityDespawn(entity);
+ }
+ }
+
+ private static ActionResult onEntityAttacked(PlayerEntity playerEntity, World world, Hand hand, Entity entity, EntityHitResult entityHitResult) {
+ if (!Utils.isInCrimson() || !inArena) {
+ return ActionResult.PASS;
+ }
+ if (currentChallenge == DojoChallenges.FORCE) {
+ ForceTestHelper.onEntityAttacked(entity);
+ }
+ return ActionResult.PASS;
+ }
+
+ public static void onParticle(ParticleS2CPacket packet) {
+ if (!Utils.isInCrimson() || !inArena) {
+ return;
+ }
+ if (currentChallenge == DojoChallenges.TENACITY) {
+ TenacityTestHelper.onParticle(packet);
+ }
+ }
+
+ private static void render(WorldRenderContext context) {
+ if (!Utils.isInCrimson() || !inArena) {
+ return;
+ }
+ switch (currentChallenge) {
+ case FORCE -> ForceTestHelper.render(context);
+ case STAMINA -> StaminaTestHelper.render(context);
+ case MASTERY -> MasteryTestHelper.render(context);
+ case SWIFTNESS -> SwiftnessTestHelper.render(context);
+ case CONTROL -> ControlTestHelper.render(context);
+ case TENACITY -> TenacityTestHelper.render(context);
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ForceTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ForceTestHelper.java
new file mode 100644
index 00000000..70d6a401
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ForceTestHelper.java
@@ -0,0 +1,81 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.mob.ZombieEntity;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.math.Vec3d;
+
+import java.awt.*;
+import java.text.DecimalFormat;
+import java.util.Map;
+
+public class ForceTestHelper {
+
+ private static final DecimalFormat FORMATTER = new DecimalFormat("0.0");
+ private static final int ZOMBIE_LIFE_TIME = 10100;
+
+ private static final Object2LongOpenHashMap<ZombieEntity> zombies = new Object2LongOpenHashMap<>();
+
+ protected static void reset() {
+ zombies.clear();
+ }
+
+ /**
+ * If a zombie value is negative make it glow
+ *
+ * @param name zombies value
+ * @return if the zombie should glow
+ */
+ protected static boolean shouldGlow(String name) {
+ return name.contains("-");
+ }
+
+ protected static int getColor() {
+ return Color.RED.getRGB();
+ }
+
+ protected static void onEntitySpawn(Entity entity) {
+ if (entity instanceof ZombieEntity zombie) {
+ zombies.put(zombie, System.currentTimeMillis() + ZOMBIE_LIFE_TIME);
+ }
+ }
+
+ protected static void onEntityAttacked(Entity entity) {
+ if (entity instanceof ZombieEntity zombie) {
+ if (zombies.containsKey(zombie)) {
+ zombies.put(zombie, System.currentTimeMillis() + ZOMBIE_LIFE_TIME); //timer is reset when they are hit
+ }
+ }
+ }
+
+ protected static void onEntityDespawn(Entity entity) {
+ if (entity instanceof ZombieEntity zombie) {
+ zombies.removeLong(zombie);
+ }
+ }
+
+ protected static void render(WorldRenderContext context) {
+ //render times
+ long currentTime = System.currentTimeMillis();
+ for (Map.Entry<ZombieEntity, Long> zombie : zombies.object2LongEntrySet()) {
+ float secondsTime = Math.max((zombie.getValue() - currentTime) / 1000f, 0);
+
+ MutableText text = Text.literal(FORMATTER.format(secondsTime));
+ if (secondsTime > 1) {
+ text = text.formatted(Formatting.GREEN);
+ } else if (secondsTime > 0) {
+ text = text.formatted(Formatting.YELLOW);
+ } else {
+ text = text.formatted(Formatting.RED);
+ }
+
+ Vec3d labelPos = zombie.getKey().getCameraPosVec(context.tickCounter().getTickDelta(false));
+ RenderHelper.renderText(context, text, labelPos, 1.5f, true);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/MasteryTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/MasteryTestHelper.java
new file mode 100644
index 00000000..625b91eb
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/MasteryTestHelper.java
@@ -0,0 +1,68 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.Blocks;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.text.Text;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Vec3d;
+
+import java.awt.*;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MasteryTestHelper {
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+ private static final DecimalFormat FORMATTER = new DecimalFormat("0.00");
+ /**
+ * How long it takes for a block to turn red
+ */
+ private static final int BLOCK_LIFE_TIME = 6550;
+
+ private static final List<BlockPos> blockOrder = new ArrayList<>();
+ private static final Object2LongOpenHashMap<BlockPos> endTimes = new Object2LongOpenHashMap<>();
+
+ protected static void reset() {
+ blockOrder.clear();
+ endTimes.clear();
+ }
+
+ protected static void onBlockUpdate(BlockPos pos, BlockState state) {
+ if (CLIENT == null || CLIENT.player == null) {
+ return;
+ }
+ if (state.isOf(Blocks.LIME_WOOL)) {
+ blockOrder.add(pos);
+ //add lifetime of a block to the time to get time when block expires
+ // work out how long it will take between the player firing and arrow hitting the block and to subtract from time
+ long travelTime = (long) (CLIENT.player.getPos().distanceTo(pos.toCenterPos()) * 1000 / 60); //an arrow speed is about 60 blocks a second from a full draw
+ endTimes.put(pos, System.currentTimeMillis() + BLOCK_LIFE_TIME - DojoManager.ping - travelTime);
+ }
+ if (state.isAir()) {
+ blockOrder.remove(pos);
+ endTimes.removeLong(pos);
+ }
+ }
+
+ protected static void render(WorldRenderContext context) {
+ //render connecting lines
+ if (!blockOrder.isEmpty()) {
+ RenderHelper.renderLineFromCursor(context, blockOrder.getFirst().toCenterPos(), Color.LIGHT_GRAY.getColorComponents(new float[]{0, 0, 0}), 1f, 2);
+ }
+ if (blockOrder.size() >= 2) {
+ RenderHelper.renderLinesFromPoints(context, new Vec3d[]{blockOrder.get(0).toCenterPos(), blockOrder.get(1).toCenterPos()}, Color.LIGHT_GRAY.getColorComponents(new float[]{0, 0, 0}), 1, 2, false);
+ }
+
+ //render times
+ long currentTime = System.currentTimeMillis();
+ for (BlockPos pos : blockOrder) {
+ long blockEndTime = endTimes.getLong(pos);
+ float secondsTime = Math.max((blockEndTime - currentTime) / 1000f, 0);
+ RenderHelper.renderText(context, Text.literal(FORMATTER.format(secondsTime)), pos.add(0, 1, 0).toCenterPos(), 3, true);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/StaminaTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/StaminaTestHelper.java
new file mode 100644
index 00000000..3f7dfe56
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/StaminaTestHelper.java
@@ -0,0 +1,279 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.Blocks;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Box;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.util.math.Vec3i;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class StaminaTestHelper {
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+ private static final int WALL_THRESHOLD_VALUE = 13;
+ private static final int WALL_HEIGHT = 5;
+ private static final float[] INCOMING_COLOR = new float[]{0f, 1f, 0f, 0f};
+ private static final float[] OUTGOING_COLOR = new float[]{1f, 0.64f, 0f, 0f};
+
+ private static final List<Box> wallHoles = new ArrayList<>();
+ private static final List<Box> lastHoles = new ArrayList<>();
+ private static final Map<Box, HoleDirection> holeDirections = new HashMap<>();
+ private static BlockPos middleBase;
+
+ private enum HoleDirection {
+ POSITIVE_X,
+ POSITIVE_Z,
+ NEGATIVE_X,
+ NEGATIVE_Z,
+ NEW,
+ UNCHANGED
+ }
+
+ protected static void reset() {
+ wallHoles.clear();
+ lastHoles.clear();
+ holeDirections.clear();
+ middleBase = null;
+ }
+
+ protected static void update() {
+
+ //search the world around the player for walls 30 x 10 x 30 area centered on player
+
+ List<BlockPos> currentBottomWallLocations = findWallBlocks();
+ if (currentBottomWallLocations == null) { //stop here if the center pos has not been found
+ return;
+ }
+ //find walls
+ List<Box> walls = findWalls(currentBottomWallLocations);
+
+ //find air then holes and add whole to list
+ lastHoles.clear();
+ lastHoles.addAll(wallHoles);
+ wallHoles.clear();
+ for (Box wall : walls) {
+ wallHoles.addAll(findHolesInBox(wall));
+ }
+ // get direction for the holes
+ Map<Box, HoleDirection> lastHoleDirections = new HashMap<>(holeDirections);
+ holeDirections.clear();
+ for (Box hole : wallHoles) {
+ HoleDirection holeDirection = getWholeDirection(hole);
+ if (holeDirection == HoleDirection.UNCHANGED) {
+ holeDirections.put(hole, lastHoleDirections.get(hole));
+ continue;
+ }
+ holeDirections.put(hole, holeDirection);
+ }
+ }
+
+ /**
+ * Locates the center of the game and once this is found scans the bottom of room for blocks that make up the walls
+ *
+ * @return list of blocks that make up the bottom of the walls
+ */
+ private static List<BlockPos> findWallBlocks() {
+ if (CLIENT == null || CLIENT.player == null || CLIENT.world == null) {
+ return null;
+ }
+ BlockPos playerPos = CLIENT.player.getBlockPos();
+ //find the center first before starting to look for walls
+ if (middleBase == null) {
+ for (int x = playerPos.getX() - 10; x < playerPos.getX() + 10; x++) {
+ for (int y = playerPos.getY() - 5; y < playerPos.getY(); y++) {
+ for (int z = playerPos.getZ() - 10; z < playerPos.getZ() + 10; z++) {
+ BlockPos pos = new BlockPos(x, y, z);
+ BlockState state = CLIENT.world.getBlockState(pos);
+ if (state.isOf(Blocks.CHISELED_STONE_BRICKS)) {
+ middleBase = pos;
+ return null;
+ }
+ }
+ }
+ }
+ return null;
+ }
+ List<BlockPos> currentBottomWallLocations = new ArrayList<>();
+ for (int x = middleBase.getX() - 15; x < middleBase.getX() + 15; x++) {
+ for (int z = middleBase.getZ() - 15; z < middleBase.getZ() + 15; z++) {
+ BlockPos pos = new BlockPos(x, middleBase.getY() + 1, z);
+ BlockState state = CLIENT.world.getBlockState(pos);
+ //find the bottom of walls
+ if (!state.isAir()) {
+ currentBottomWallLocations.add(pos);
+ }
+ }
+ }
+ return currentBottomWallLocations;
+ }
+
+ private static List<Box> findWalls(List<BlockPos> currentBottomWallLocations) {
+ Int2ObjectOpenHashMap<List<BlockPos>> possibleWallsX = new Int2ObjectOpenHashMap<>();
+ Int2ObjectOpenHashMap<List<BlockPos>> possibleWallsZ = new Int2ObjectOpenHashMap<>();
+ for (BlockPos block : currentBottomWallLocations) {
+ //add to the x walls
+ int x = block.getX();
+ if (!possibleWallsX.containsKey(x)) {
+ possibleWallsX.put(x, new ArrayList<>());
+
+ }
+ possibleWallsX.get(x).add(block);
+ //add to the z walls
+ int z = block.getZ();
+ if (!possibleWallsZ.containsKey(z)) {
+ possibleWallsZ.put(z, new ArrayList<>());
+ }
+ possibleWallsZ.get(z).add(block);
+ }
+
+ //extract only the lines that are long enough to be a wall and not from walls overlapping
+ List<List<BlockPos>> walls = new ArrayList<>();
+ for (List<BlockPos> line : possibleWallsX.values()) {
+ if (line.size() >= WALL_THRESHOLD_VALUE) {
+ walls.add(line);
+ }
+ }
+ for (List<BlockPos> line : possibleWallsZ.values()) {
+ if (line.size() >= WALL_THRESHOLD_VALUE) {
+ walls.add(line);
+ }
+ }
+
+ //final find the maximum values for each wall to output a box for them
+ List<Box> wallBoxes = new ArrayList<>();
+ for (List<BlockPos> wall : walls) {
+ BlockPos minPos = wall.getFirst();
+ BlockPos maxPos = wall.getFirst();
+ for (BlockPos pos : wall) {
+ if (pos.getX() < minPos.getX()) {
+ minPos = new BlockPos(pos.getX(), minPos.getY(), minPos.getZ());
+ }
+ if (pos.getZ() < minPos.getZ()) {
+ minPos = new BlockPos(minPos.getX(), minPos.getY(), pos.getZ());
+ }
+
+ if (pos.getX() > maxPos.getX()) {
+ maxPos = new BlockPos(pos.getX(), maxPos.getY(), maxPos.getZ());
+ }
+ if (pos.getZ() > maxPos.getZ()) {
+ maxPos = new BlockPos(maxPos.getX(), maxPos.getY(), pos.getZ());
+ }
+ }
+ //expand wall to top
+ maxPos = new BlockPos(maxPos.getX(), maxPos.getY() + WALL_HEIGHT, maxPos.getZ());
+
+ wallBoxes.add(Box.enclosing(minPos, maxPos));
+ }
+
+ return wallBoxes;
+ }
+
+ private static List<Box> findHolesInBox(Box box) {
+ List<Box> holes = new ArrayList<>();
+ if (CLIENT == null || CLIENT.player == null || CLIENT.world == null) {
+ return holes;
+ }
+ //get the direction vector
+ Vec3i wallDirection = box.getLengthX() == 1 ? new Vec3i(0, 0, 1) : new Vec3i(1, 0, 0);
+ //find the corners of boxes (only need 3)
+ List<BlockPos> topLeft = new ArrayList<>();
+ List<BlockPos> topRight = new ArrayList<>();
+ List<BlockPos> bottomLeft = new ArrayList<>();
+ for (int z = (int) box.minZ; z < box.maxZ; z++) {
+ for (int x = (int) box.minX; x < box.maxX; x++) {
+ for (int y = (int) box.minY; y < box.maxY; y++) {
+ BlockPos pos = new BlockPos(x, y, z);
+ BlockState state = CLIENT.world.getBlockState(pos);
+ if (!state.isAir()) {
+ //do not check non-air
+ continue;
+ }
+ boolean top = y == box.maxY - 1 || !CLIENT.world.getBlockState(pos.add(0, 1, 0)).isAir();
+ boolean bottom = !CLIENT.world.getBlockState(pos.add(0, -1, 0)).isAir();
+ boolean left = !CLIENT.world.getBlockState(pos.add(wallDirection)).isAir();
+ boolean right = !CLIENT.world.getBlockState(pos.subtract(wallDirection)).isAir();
+ if (top) {
+ if (left) {
+ topLeft.add(pos);
+ }
+ if (right) {
+ topRight.add(pos);
+ }
+ }
+ if (bottom && left) {
+ bottomLeft.add(pos);
+ }
+
+ }
+ }
+ }
+ // gets box around top of hole then expands to the bottom of hole
+ for (int i = 0; i < topLeft.size(); i++) {
+ if (topRight.size() <= i || bottomLeft.size() <= i) {
+ //if corners can not be found end looking
+ break;
+ }
+ Box hole = Box.enclosing(topLeft.get(i), topRight.get(i));
+ hole = hole.stretch(0, bottomLeft.get(i).getY() - topLeft.get(i).getY(), 0);
+ holes.add(hole);
+ }
+ return holes;
+ }
+
+ private static HoleDirection getWholeDirection(Box hole) {
+ //the value has not changed since last time
+ if (lastHoles.contains(hole)) {
+ return HoleDirection.UNCHANGED;
+ }
+ //check each direction to work out which way the whole is going
+ Box posX = hole.offset(1, 0, 0);
+ if (lastHoles.contains(posX)) {
+ return HoleDirection.POSITIVE_X;
+ }
+ Box negX = hole.offset(-1, 0, 0);
+ if (lastHoles.contains(negX)) {
+ return HoleDirection.NEGATIVE_X;
+ }
+ Box posZ = hole.offset(0, 0, 1);
+ if (lastHoles.contains(posZ)) {
+ return HoleDirection.POSITIVE_Z;
+ }
+ Box negZ = hole.offset(0, 0, -1);
+ if (lastHoles.contains(negZ)) {
+ return HoleDirection.NEGATIVE_Z;
+ }
+ // if pos can not be found mark as new
+ return HoleDirection.NEW;
+
+ }
+
+ protected static void render(WorldRenderContext context) {
+ if (wallHoles.isEmpty() || CLIENT == null || CLIENT.player == null) {
+ return;
+ }
+ BlockPos playerPos = CLIENT.player.getBlockPos();
+ for (Box hole : wallHoles) {
+ float[] color = isHoleIncoming(hole, holeDirections.get(hole), playerPos) ? INCOMING_COLOR : OUTGOING_COLOR;
+ RenderHelper.renderFilled(context, new BlockPos((int) hole.minX, (int) hole.minY, (int) hole.minZ), new Vec3d(hole.getLengthX(), hole.getLengthY(), hole.getLengthZ()), color, 0.3f, false);
+ }
+ }
+
+ private static boolean isHoleIncoming(Box holePos, HoleDirection holeDirection, BlockPos playerPos) {
+ return switch (holeDirection) {
+ case POSITIVE_X -> playerPos.getX() < holePos.minX;
+ case POSITIVE_Z -> playerPos.getZ() < holePos.minZ;
+ case NEGATIVE_X -> playerPos.getX() > holePos.maxX;
+ case NEGATIVE_Z -> playerPos.getZ() > holePos.maxZ;
+
+ default -> true;
+ };
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/SwiftnessTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/SwiftnessTestHelper.java
new file mode 100644
index 00000000..678005c4
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/SwiftnessTestHelper.java
@@ -0,0 +1,34 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.Blocks;
+import net.minecraft.util.math.BlockPos;
+
+public class SwiftnessTestHelper {
+
+ private static BlockPos lastBlock;
+
+ protected static void reset() {
+ lastBlock = null;
+ }
+
+ protected static void onBlockUpdate(BlockPos pos, BlockState state) {
+ if (state.isOf(Blocks.LIME_WOOL)) {
+ lastBlock = pos.toImmutable();
+ }
+ }
+
+ /**
+ * Renders a green block around the newest block
+ *
+ * @param context render context
+ */
+ protected static void render(WorldRenderContext context) {
+ if (lastBlock == null) {
+ return;
+ }
+ RenderHelper.renderFilled(context, lastBlock, new float[]{0f, 1f, 0f}, 0.5f, true);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/TenacityTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/TenacityTestHelper.java
new file mode 100644
index 00000000..51e99fbd
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/TenacityTestHelper.java
@@ -0,0 +1,100 @@
+package de.hysky.skyblocker.skyblock.crimson.dojo;
+
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.decoration.ArmorStandEntity;
+import net.minecraft.network.packet.s2c.play.ParticleS2CPacket;
+import net.minecraft.particle.ParticleTypes;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.hit.HitResult;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.world.RaycastContext;
+
+public class TenacityTestHelper {
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+
+ private static final Object2ObjectOpenHashMap<ArmorStandEntity, Vec3d> fireBallsWithStartPos = new Object2ObjectOpenHashMap<>();
+ private static final Object2ObjectOpenHashMap<ArmorStandEntity, Vec3d> particleOffsets = new Object2ObjectOpenHashMap<>();
+
+ protected static void reset() {
+ fireBallsWithStartPos.clear();
+ particleOffsets.clear();
+ }
+
+ protected static void render(WorldRenderContext context) {
+ for (ArmorStandEntity fireball : fireBallsWithStartPos.keySet()) {
+ Vec3d lineStart = fireBallsWithStartPos.get(fireball).add(particleOffsets.getOrDefault(fireball, Vec3d.ZERO));
+ Vec3d fireballPos = fireball.getPos().add(particleOffsets.getOrDefault(fireball, Vec3d.ZERO));
+
+ Vec3d distance = fireballPos.subtract(lineStart);
+ if (distance.length() > 0.02) { //if big enough gap try from start calculate and show trajectory
+ distance = distance.multiply(100);
+ Vec3d lineEnd = lineStart.add(distance);
+
+ RenderHelper.renderLinesFromPoints(context, new Vec3d[]{lineStart, lineEnd}, new float[]{1f, 0f, 0f}, 1, 3, false);
+
+ //get highlighted block
+ HitResult hitResult = raycast(lineStart, lineEnd, fireball);
+ if (hitResult != null && hitResult.getType() == HitResult.Type.BLOCK && hitResult instanceof BlockHitResult blockHitResult) {
+ RenderHelper.renderFilled(context, blockHitResult.getBlockPos(), new float[]{1f, 0f, 0f}, 0.5f, false);
+ }
+ }
+ }
+ }
+
+ protected static HitResult raycast(Vec3d start, Vec3d end, ArmorStandEntity fireball) {
+ if (CLIENT == null || CLIENT.world == null) {
+ return null;
+ }
+ return CLIENT.world.raycast(new RaycastContext(start, end, RaycastContext.ShapeType.OUTLINE, RaycastContext.FluidHandling.ANY, fireball));
+ }
+
+ /**
+ * If a spawned entity is an armour stand add it to the fireballs map (assuming all armour stands are fireballs)
+ *
+ * @param entity spawned entity
+ */
+ protected static void onEntitySpawn(Entity entity) {
+ if (entity instanceof ArmorStandEntity armorStand) {
+ fireBallsWithStartPos.put(armorStand, armorStand.getPos());
+ }
+ }
+
+ protected static void onEntityDespawn(Entity entity) {
+ if (entity instanceof ArmorStandEntity armorStand) {
+ fireBallsWithStartPos.remove(armorStand);
+ }
+ }
+
+ /**
+ * Uses the particles spawned with the fireballs to offset from the armour stand position to get a more accurate guess of where it's going
+ *
+ * @param packet particle packet
+ */
+ protected static void onParticle(ParticleS2CPacket packet) {
+ if (!ParticleTypes.FLAME.equals(packet.getParameters().getType())) {
+ return;
+ }
+ //get nearest fireball to particle
+ Vec3d particlePos = new Vec3d(packet.getX(), packet.getY(), packet.getZ());
+ ArmorStandEntity neareastFireball = null;
+ double clostestDistance = 50;
+ for (ArmorStandEntity fireball : fireBallsWithStartPos.keySet()) {
+ double distance = fireball.getPos().distanceTo(particlePos);
+ if (distance < clostestDistance) {
+ neareastFireball = fireball;
+ clostestDistance = distance;
+ }
+ }
+ if (neareastFireball == null) { //can not find fireball near particle
+ return;
+ }
+ //adjust fireball offset with particle pos
+ Vec3d delta = particlePos.subtract(neareastFireball.getPos());
+ //update values
+ particleOffsets.put(neareastFireball, delta);
+ }
+}
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 7a5abed1..d1fc08ec 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 @@ public class DungeonScore {
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/dwarven/CommissionHighlight.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CommissionHighlight.java
new file mode 100644
index 00000000..de26809c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CommissionHighlight.java
@@ -0,0 +1,38 @@
+package de.hysky.skyblocker.skyblock.dwarven;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.render.gui.ColorHighlight;
+import de.hysky.skyblocker.utils.render.gui.ContainerSolver;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import net.minecraft.component.DataComponentTypes;
+import net.minecraft.item.ItemStack;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CommissionHighlight extends ContainerSolver {
+
+ public CommissionHighlight() {
+ super("^Commissions$");
+ }
+
+ @Override
+ protected boolean isEnabled() {
+ return SkyblockerConfigManager.get().mining.commissionHighlight;
+ }
+
+ @Override
+ protected List<ColorHighlight> getColors(String[] groups, Int2ObjectMap<ItemStack> slots) {
+ List<ColorHighlight> highlights = new ArrayList<>();
+ for (Int2ObjectMap.Entry<ItemStack> entry : slots.int2ObjectEntrySet()) {
+ ItemStack stack = entry.getValue();
+ if (stack != null && stack.contains(DataComponentTypes.LORE)) {
+ if (ItemUtils.getLoreLineIf(stack, s -> s.contains("COMPLETED")) != null) {
+ highlights.add(ColorHighlight.green(entry.getIntKey()));
+ }
+ }
+ }
+ return highlights;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java
index 6f4c86a7..d709181f 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java
@@ -42,6 +42,12 @@ import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.arg
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
import static net.minecraft.command.CommandSource.suggestMatching;
+/**
+ * Manager for Crystal Hollows waypoints that handles {@link #update() location detection},
+ * {@link #extractLocationFromMessage(Text, Boolean) waypoints receiving}, {@link #shareWaypoint(String) sharing},
+ * {@link #registerWaypointLocationCommands(CommandDispatcher, CommandRegistryAccess) commands}, and
+ * {@link #render(WorldRenderContext) rendering}.
+ */
public class CrystalsLocationsManager {
private static final Logger LOGGER = LogUtils.getLogger();
private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
@@ -55,11 +61,15 @@ public class CrystalsLocationsManager {
protected static Map<String, CrystalsWaypoint> activeWaypoints = new HashMap<>();
public static void init() {
+ // Crystal Hollows Waypoints
Scheduler.INSTANCE.scheduleCyclic(CrystalsLocationsManager::update, 40);
WorldRenderEvents.AFTER_TRANSLUCENT.register(CrystalsLocationsManager::render);
ClientReceiveMessageEvents.GAME.register(CrystalsLocationsManager::extractLocationFromMessage);
ClientCommandRegistrationCallback.EVENT.register(CrystalsLocationsManager::registerWaypointLocationCommands);
ClientPlayConnectionEvents.JOIN.register((_handler, _sender, _client) -> reset());
+
+ // Nucleus Waypoints
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(NucleusWaypoints::render);
}
private static void extractLocationFromMessage(Text message, Boolean overlay) {
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/NucleusWaypoints.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/NucleusWaypoints.java
new file mode 100644
index 00000000..8046ed19
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/NucleusWaypoints.java
@@ -0,0 +1,61 @@
+package de.hysky.skyblocker.skyblock.dwarven;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.text.TextColor;
+import net.minecraft.text.Style;
+import net.minecraft.util.DyeColor;
+import net.minecraft.util.math.BlockPos;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+public class NucleusWaypoints {
+ private static final Logger LOGGER = LoggerFactory.getLogger(NucleusWaypoints.class);
+
+ private static class Waypoint {
+ BlockPos position;
+ String name;
+ DyeColor color;
+
+ Waypoint(BlockPos position, String name, DyeColor color) {
+ this.position = position;
+ this.name = name;
+ this.color = color;
+ }
+ }
+
+ private static final List<Waypoint> WAYPOINTS = List.of(
+ new Waypoint(new BlockPos(551, 116, 551), "Precursor Remnants", DyeColor.LIGHT_BLUE),
+ new Waypoint(new BlockPos(551, 116, 475), "Mithril Deposits", DyeColor.LIME),
+ new Waypoint(new BlockPos(475, 116, 551), "Goblin Holdout", DyeColor.ORANGE),
+ new Waypoint(new BlockPos(475, 116, 475), "Jungle", DyeColor.PURPLE),
+ new Waypoint(new BlockPos(513, 106, 524), "Nucleus", DyeColor.RED)
+ );
+
+ public static void render(WorldRenderContext context) {
+ try {
+ boolean enabled = SkyblockerConfigManager.get().mining.crystalHollows.nucleusWaypoints;
+ boolean inCrystalHollows = Utils.isInCrystalHollows();
+
+ if (enabled && inCrystalHollows) {
+ for (Waypoint waypoint : WAYPOINTS) {
+
+ int rgb = waypoint.color.getFireworkColor();
+ TextColor textColor = TextColor.fromRgb(rgb);
+
+ MutableText text = Text.literal(waypoint.name).setStyle(Style.EMPTY.withColor(textColor));
+
+ RenderHelper.renderText(context, text, waypoint.position.toCenterPos().add(0, 5, 0), 8, true);
+ }
+ }
+ } catch (Exception e) {
+ LOGGER.error("[{}] Error occurred while rendering Nucleus waypoints. {}", LOGGER.getName(), e);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java
index d6f9410b..81e328ca 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java
@@ -1,6 +1,7 @@
package de.hysky.skyblocker.skyblock.entity;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager;
import de.hysky.skyblocker.skyblock.dungeon.LividColor;
import de.hysky.skyblocker.skyblock.end.TheEnd;
import de.hysky.skyblocker.utils.ItemUtils;
@@ -10,6 +11,7 @@ import de.hysky.skyblocker.utils.render.culling.OcclusionCulling;
import net.minecraft.entity.Entity;
import net.minecraft.entity.decoration.ArmorStandEntity;
import net.minecraft.entity.mob.EndermanEntity;
+import net.minecraft.entity.mob.ZombieEntity;
import net.minecraft.entity.passive.BatEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
@@ -28,6 +30,7 @@ public class MobGlow {
if (OcclusionCulling.getReducedCuller().isVisible(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) {
String name = entity.getName().getString();
+
// Dungeons
if (Utils.isInDungeons() && !entity.isInvisible()) {
return switch (entity) {
@@ -46,6 +49,7 @@ public class MobGlow {
};
}
+
return switch (entity) {
// Rift
case PlayerEntity p when Utils.isInTheRift() && !entity.isInvisible() && name.equals("Blobbercyst ") -> SkyblockerConfigManager.get().otherLocations.rift.blobbercystGlow;
@@ -57,6 +61,9 @@ public class MobGlow {
// Special Zelot
case EndermanEntity enderman when Utils.isInTheEnd() && !entity.isInvisible() -> TheEnd.isSpecialZealot(enderman);
+ //dojo
+ case ZombieEntity zombie when Utils.isInCrimson() && DojoManager.inArena -> DojoManager.shouldGlow(getArmorStandName(zombie));
+
default -> false;
};
}
@@ -66,6 +73,7 @@ public class MobGlow {
/**
* Checks if an entity is starred by checking if its armor stand contains a star in its name.
+ *
* @param entity the entity to check.
* @return true if the entity is starred, false otherwise
*/
@@ -74,6 +82,20 @@ public class MobGlow {
return !armorStands.isEmpty() && armorStands.getFirst().getName().getString().contains("✯");
}
+ /**
+ * Returns name of entity by finding closed armor stand and getting name of that
+ *
+ * @param entity the entity to check
+ * @return the name string of the entities label
+ */
+ public static String getArmorStandName(Entity entity) {
+ List<ArmorStandEntity> armorStands = getArmorStands(entity);
+ if (armorStands.isEmpty()) {
+ return "";
+ }
+ return armorStands.getFirst().getName().getString();
+ }
+
public static List<ArmorStandEntity> getArmorStands(Entity entity) {
return getArmorStands(entity.getWorld(), entity.getBoundingBox());
}
@@ -94,6 +116,7 @@ public class MobGlow {
case EndermanEntity enderman when TheEnd.isSpecialZealot(enderman) -> Formatting.RED.getColorValue();
case ArmorStandEntity armorStand when isNukekubiHead(armorStand) -> 0x990099;
+ case ZombieEntity zombie when Utils.isInCrimson() && DojoManager.inArena -> DojoManager.getColor();
default -> 0xf57738;
};
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java b/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java
index da2a0c2f..e846a88a 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java
@@ -13,7 +13,6 @@ import de.hysky.skyblocker.utils.Http;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.scheduler.Scheduler;
import it.unimi.dsi.fastutil.ints.IntList;
-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.minecraft.client.MinecraftClient;
@@ -29,6 +28,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
public class EventNotifications {
private static final Logger LOGGER = LogUtils.getLogger();
@@ -39,21 +39,19 @@ public class EventNotifications {
public static final IntList DEFAULT_REMINDERS = IntList.of(60, 60 * 5);
- public static final Map<String, ItemStack> eventIcons = new Object2ObjectOpenHashMap<>();
-
- static {
- eventIcons.put("Dark Auction", new ItemStack(Items.NETHER_BRICK));
- eventIcons.put("Bonus Fishing Festival", new ItemStack(Items.FISHING_ROD));
- eventIcons.put("Bonus Mining Fiesta", new ItemStack(Items.IRON_PICKAXE));
- eventIcons.put(JACOBS, new ItemStack(Items.IRON_HOE));
- eventIcons.put("New Year Celebration", new ItemStack(Items.CAKE));
- eventIcons.put("Election Over!", new ItemStack(Items.JUKEBOX));
- eventIcons.put("Election Booth Opens", new ItemStack(Items.JUKEBOX));
- eventIcons.put("Spooky Festival", new ItemStack(Items.JACK_O_LANTERN));
- eventIcons.put("Season of Jerry", new ItemStack(Items.SNOWBALL));
- eventIcons.put("Jerry's Workshop Opens", new ItemStack(Items.SNOW_BLOCK));
- eventIcons.put("Traveling Zoo", new ItemStack(Items.HAY_BLOCK)); // change to the custom head one day
- }
+ public static final Map<String, ItemStack> eventIcons = Map.ofEntries(
+ Map.entry("Dark Auction", new ItemStack(Items.NETHER_BRICK)),
+ Map.entry("Bonus Fishing Festival", new ItemStack(Items.FISHING_ROD)),
+ Map.entry("Bonus Mining Fiesta", new ItemStack(Items.IRON_PICKAXE)),
+ Map.entry(JACOBS, new ItemStack(Items.IRON_HOE)),
+ Map.entry("New Year Celebration", new ItemStack(Items.CAKE)),
+ Map.entry("Election Over!", new ItemStack(Items.JUKEBOX)),
+ Map.entry("Election Booth Opens", new ItemStack(Items.JUKEBOX)),
+ Map.entry("Spooky Festival", new ItemStack(Items.JACK_O_LANTERN)),
+ Map.entry("Season of Jerry", new ItemStack(Items.SNOWBALL)),
+ Map.entry("Jerry's Workshop Opens", new ItemStack(Items.SNOW_BLOCK)),
+ Map.entry("Traveling Zoo", new ItemStack(Items.HAY_BLOCK)) // change to the custom head one day
+ );
public static void init() {
Scheduler.INSTANCE.scheduleCyclic(EventNotifications::timeUpdate, 20);
@@ -85,7 +83,7 @@ public class EventNotifications {
));
}
- private static final Map<String, LinkedList<SkyblockEvent>> events = new Object2ObjectOpenHashMap<>();
+ private static final Map<String, LinkedList<SkyblockEvent>> events = new ConcurrentHashMap<>();
public static Map<String, LinkedList<SkyblockEvent>> getEvents() {
return events;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java b/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java
index 50982d29..214ecc84 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java
@@ -10,7 +10,6 @@ import com.mojang.brigadier.CommandDispatcher;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.codecs.RecordCodecBuilder;
-import com.mojang.util.UndashedUuid;
import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.Http;
@@ -44,6 +43,7 @@ import java.nio.file.Path;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
public class MuseumItemCache {
private static final Logger LOGGER = LoggerFactory.getLogger(MuseumItemCache.class);
@@ -113,7 +113,7 @@ public class MuseumItemCache {
String uuid = Utils.getUndashedUuid();
//Be safe about access to avoid NPEs
Map<String, ProfileMuseumData> playerData = MUSEUM_ITEM_CACHE.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>());
- playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY);
+ playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY.get());
playerData.get(profileId).collectedItemIds().add(itemId);
save();
@@ -224,7 +224,7 @@ public class MuseumItemCache {
if (loaded.isDone() && (!MUSEUM_ITEM_CACHE.containsKey(uuid) || !MUSEUM_ITEM_CACHE.getOrDefault(uuid, new Object2ObjectOpenHashMap<>()).containsKey(profileId))) {
Map<String, ProfileMuseumData> playerData = MUSEUM_ITEM_CACHE.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>());
- playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY);
+ playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY.get());
updateData4ProfileMember(uuid, profileId);
}
@@ -238,7 +238,7 @@ public class MuseumItemCache {
}
private record ProfileMuseumData(long lastResync, ObjectOpenHashSet<String> collectedItemIds) {
- private static final ProfileMuseumData EMPTY = new ProfileMuseumData(0L, null);
+ private static final Supplier<ProfileMuseumData> EMPTY = () -> new ProfileMuseumData(0L, new ObjectOpenHashSet<>());
private static final long TIME_BETWEEN_RESYNCING_ALLOWED = 600_000L;
private static final Codec<ProfileMuseumData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.LONG.fieldOf("lastResync").forGetter(ProfileMuseumData::lastResync),
@@ -256,4 +256,4 @@ public class MuseumItemCache {
return this.lastResync + TIME_BETWEEN_RESYNCING_ALLOWED < System.currentTimeMillis();
}
}
-} \ No newline at end of file
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java
new file mode 100644
index 00000000..42a52a85
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java
@@ -0,0 +1,194 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.mojang.serialization.Codec;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.events.SkyblockEvents;
+import de.hysky.skyblocker.mixins.accessors.SlotAccessor;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ingame.InventoryScreen;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.inventory.Inventory;
+import net.minecraft.inventory.SimpleInventory;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.nbt.StringNbtReader;
+import net.minecraft.nbt.visitor.StringNbtWriter;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.util.Identifier;
+import org.apache.commons.lang3.ArrayUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+/**
+ * Opened here {@code de.hysky.skyblocker.mixins.MinecraftClientMixin#skyblocker$skyblockInventoryScreen}
+ * <br>
+ * Book button is moved here {@code de.hysky.skyblocker.mixins.InventoryScreenMixin#skyblocker}
+ */
+public class SkyblockInventoryScreen extends InventoryScreen {
+ private static final Logger LOGGER = LoggerFactory.getLogger("Equipment");
+ private static final Supplier<ItemStack[]> EMPTY_EQUIPMENT = () -> new ItemStack[]{ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY};
+ public static final ItemStack[] equipment = EMPTY_EQUIPMENT.get();
+ public static final ItemStack[] equipment_rift = EMPTY_EQUIPMENT.get();
+ private static final Codec<ItemStack[]> CODEC = ItemUtils.EMPTY_ALLOWING_ITEMSTACK_CODEC.listOf(4, 8) // min size at 4 for backwards compat
+ .xmap(itemStacks -> itemStacks.toArray(ItemStack[]::new), List::of).fieldOf("items").codec();
+
+ private static final Identifier SLOT_TEXTURE = Identifier.ofVanilla("container/slot");
+ private static final Identifier EMPTY_SLOT = Identifier.of(SkyblockerMod.NAMESPACE, "equipment/empty_icon");
+ private static final Path FOLDER = SkyblockerMod.CONFIG_DIR.resolve("equipment");
+
+ private final Slot[] equipmentSlots = new Slot[4];
+
+ private static void save(String profileId) {
+ try {
+ Files.createDirectories(FOLDER);
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to create folder for equipment!", e);
+ }
+ Path resolve = FOLDER.resolve(profileId + ".nbt");
+
+ try (BufferedWriter writer = Files.newBufferedWriter(resolve)) {
+
+ writer.write(new StringNbtWriter().apply(CODEC.encodeStart(NbtOps.INSTANCE, ArrayUtils.addAll(equipment, equipment_rift)).getOrThrow()));
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Failed to save Equipment data", e);
+ }
+ }
+
+ private static void load(String profileId) {
+ Path resolve = FOLDER.resolve(profileId + ".nbt");
+ CompletableFuture.supplyAsync(() -> {
+ try (BufferedReader reader = Files.newBufferedReader(resolve)) {
+ return CODEC.parse(NbtOps.INSTANCE, StringNbtReader.parse(reader.lines().collect(Collectors.joining()))).getOrThrow();
+ } catch (NoSuchFileException ignored) {
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Failed to load Equipment data", e);
+ }
+ return EMPTY_EQUIPMENT.get();
+ // Schedule on main thread to avoid any async weirdness
+ }).thenAccept(itemStacks -> MinecraftClient.getInstance().execute(() -> {
+ System.arraycopy(itemStacks, 0, equipment, 0, Math.min(itemStacks.length, 4));
+ if (itemStacks.length <= 4) return;
+ System.arraycopy(itemStacks, 4, equipment_rift, 0, Math.clamp(itemStacks.length - 4, 0, 4));
+ }));
+ }
+
+ public static void initEquipment() {
+
+ SkyblockEvents.PROFILE_CHANGE.register(((prevProfileId, profileId) -> {
+ if (!prevProfileId.isEmpty()) CompletableFuture.runAsync(() -> save(prevProfileId)).thenRun(() -> load(profileId));
+ else load(profileId);
+ }));
+
+ ClientLifecycleEvents.CLIENT_STOPPING.register(client1 -> {
+ String profileId = Utils.getProfileId();
+ if (!profileId.isBlank()) {
+ CompletableFuture.runAsync(() -> save(profileId));
+ }
+ });
+ }
+
+ public SkyblockInventoryScreen(PlayerEntity player) {
+ super(player);
+ SimpleInventory inventory = new SimpleInventory(Utils.isInTheRift() ? equipment_rift: equipment);
+
+ Slot slot = handler.slots.get(45);
+ ((SlotAccessor) slot).setX(slot.x + 21);
+ for (int i = 0; i < 4; i++) {
+ equipmentSlots[i] = new EquipmentSlot(inventory, i, 77, 8 + i * 18);
+ }
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ for (Slot equipmentSlot : equipmentSlots) {
+ if (isPointWithinBounds(equipmentSlot.x, equipmentSlot.y, 16, 16, mouseX, mouseY)) {
+ MessageScheduler.INSTANCE.sendMessageAfterCooldown("/equipment");
+ return true;
+ }
+ }
+ return super.mouseClicked(mouseX, mouseY, button);
+ }
+
+ /**
+ * Draws the equipment slots in the foreground layer after vanilla slots are drawn
+ * in {@link net.minecraft.client.gui.screen.ingame.HandledScreen#render(DrawContext, int, int, float) HandledScreen#render(DrawContext, int, int, float)}.
+ */
+ @Override
+ protected void drawForeground(DrawContext context, int mouseX, int mouseY) {
+ for (Slot equipmentSlot : equipmentSlots) {
+ drawSlot(context, equipmentSlot);
+ if (isPointWithinBounds(equipmentSlot.x, equipmentSlot.y, 16, 16, mouseX, mouseY)) drawSlotHighlight(context, equipmentSlot.x, equipmentSlot.y, 0);
+ }
+
+ super.drawForeground(context, mouseX, mouseY);
+ }
+
+ @Override
+ protected void drawMouseoverTooltip(DrawContext context, int x, int y) {
+ super.drawMouseoverTooltip(context, x, y);
+ if (!handler.getCursorStack().isEmpty()) return;
+ for (Slot equipmentSlot : equipmentSlots) {
+ if (isPointWithinBounds(equipmentSlot.x, equipmentSlot.y, 16, 16, x, y) && equipmentSlot.hasStack()) {
+ ItemStack itemStack = equipmentSlot.getStack();
+ context.drawTooltip(this.textRenderer, this.getTooltipFromItem(itemStack), itemStack.getTooltipData(), x, y);
+ }
+ }
+ }
+
+ @Override
+ public void removed() {
+ super.removed();
+ // put the handler back how it was, the handler is the same while the player is alive/in the same world
+ Slot slot = handler.slots.get(45);
+ ((SlotAccessor) slot).setX(slot.x - 21);
+ }
+
+ @Override
+ protected void drawBackground(DrawContext context, float delta, int mouseX, int mouseY) {
+ super.drawBackground(context, delta, mouseX, mouseY);
+ for (int i = 0; i < 4; i++) {
+ context.drawGuiTexture(SLOT_TEXTURE, x + 76 + (i == 3 ? 21 : 0), y + 7 + i * 18, 18, 18);
+ }
+ }
+
+ @Override
+ protected void drawSlot(DrawContext context, Slot slot) {
+ super.drawSlot(context, slot);
+ if (slot instanceof EquipmentSlot && !slot.hasStack()) {
+ context.drawGuiTexture(EMPTY_SLOT, slot.x, slot.y, 16, 16);
+ }
+ }
+
+ private static class EquipmentSlot extends Slot {
+
+ public EquipmentSlot(Inventory inventory, int index, int x, int y) {
+ super(inventory, index, x, y);
+ }
+
+ @Override
+ public boolean canTakeItems(PlayerEntity playerEntity) {
+ return false;
+ }
+
+ @Override
+ public boolean canInsert(ItemStack stack) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java
index c7ea17dc..d94d6405 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java
@@ -48,7 +48,7 @@ public class CommunityShopAdder extends SlotTextAdder {
String lastLine = lore.getLast().getString();
return List.of(SlotText.bottomLeft(switch (lastLine) {
case "Maxed out!" -> Text.literal("Max").withColor(0xfab387);
- case "Currently upgrading!" -> Text.literal("⏰").withColor(0xf9e2af).formatted(Formatting.BOLD);
+ case "Currently upgrading!", "Click to instantly upgrade!" -> Text.literal("⏰").withColor(0xf9e2af).formatted(Formatting.BOLD);
case "Click to claim!" -> Text.literal("✅").withColor(0xa6e3a1).formatted(Formatting.BOLD);
default -> Text.literal(String.valueOf(RomanNumerals.romanToDecimal(roman))).withColor(0xcba6f7);
}));
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java
index 97a73a80..955ebc87 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java
@@ -58,6 +58,7 @@ public class ItemTooltip {
case "CRIMSON_HELMET", "CRIMSON_CHESTPLATE", "CRIMSON_LEGGINGS", "CRIMSON_BOOTS",
"AURORA_HELMET", "AURORA_CHESTPLATE", "AURORA_LEGGINGS", "AURORA_BOOTS",
"TERROR_HELMET", "TERROR_CHESTPLATE", "TERROR_LEGGINGS", "TERROR_BOOTS" -> apiId = id;
+ case "MIDAS_SWORD", "MIDAS_STAFF" -> apiId = id;
default -> apiId = apiId.replace(":", "-");
}
return apiId;
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 7eda7646..01ffc144 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.Matcher;
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:\\[(?<color>[0-9]+)]\\}");
private static Map<String, Map<Rarity, PetNumbers>> petNums;
@@ -138,7 +138,7 @@ public class ItemStackBuilder {
return list;
}
- private static String injectData(String string, List<Pair<String, String>> injectors) {
+ public static String injectData(String string, List<Pair<String, String>> injectors) {
for (Pair<String, String> injector : injectors) {
string = string.replaceAll(injector.getLeft(), injector.getRight());
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java b/src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java
new file mode 100644
index 00000000..7131a567
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java
@@ -0,0 +1,37 @@
+package de.hysky.skyblocker.skyblock.mayors;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.sound.SoundCategory;
+import net.minecraft.sound.SoundEvents;
+import net.minecraft.text.HoverEvent;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+public final class JerryTimer {
+ private JerryTimer() {
+ }
+ public static void init() {
+ //Example message: "§b ☺ §eThere is a §aGreen Jerry§e!"
+ //There are various formats, all of which start with the "§b ☺ " prefix and contain the word "<color> Jerry"
+ ClientReceiveMessageEvents.GAME.register((message, overlay) -> {
+ if (overlay || !Utils.getMayor().equals("Jerry") || !SkyblockerConfigManager.get().helpers.jerry.enableJerryTimer) return;
+ String text = message.getString();
+ //This part of hypixel still uses legacy text formatting, so we can't directly check for the actual text
+ if (!text.startsWith("§b ☺ ") || !text.contains("Jerry")) return;
+ HoverEvent hoverEvent = message.getStyle().getHoverEvent();
+ if (hoverEvent == null || hoverEvent.getAction() != HoverEvent.Action.SHOW_TEXT) return;
+ ClientPlayerEntity player = MinecraftClient.getInstance().player;
+ Scheduler.INSTANCE.schedule(() -> {
+ if (player == null || !Utils.isOnSkyblock()) return;
+ player.sendMessage(Constants.PREFIX.get().append(Text.literal("Jerry cooldown is over!")).formatted(Formatting.GREEN), false);
+ player.playSoundToPlayer(SoundEvents.ENTITY_VILLAGER_TRADE, SoundCategory.NEUTRAL, 100f, 1.0f);
+ }, 20*60*6); // 6 minutes
+ });
+ }
+}
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 00000000..d867a0e6
--- /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<String, ItemStack> 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 00000000..f5a5ec40
--- /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<ClickableWidget> 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 00000000..1d0b21ca
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java
@@ -0,0 +1,230 @@
+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.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;
+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<String, Map<String, ?>> 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<ProfileViewerNavButton> 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<Void> skillsFuture = CompletableFuture.runAsync(() -> profileViewerPages[0] = new SkillsPage(hypixelProfile, playerProfile));
+ CompletableFuture<Void> slayersFuture = CompletableFuture.runAsync(() -> profileViewerPages[1] = new SlayersPage(playerProfile));
+ CompletableFuture<Void> dungeonsFuture = CompletableFuture.runAsync(() -> profileViewerPages[2] = new DungeonsPage(playerProfile));
+ CompletableFuture<Void> inventoriesFuture = CompletableFuture.runAsync(() -> profileViewerPages[3] = new InventoryPage(playerProfile));
+ CompletableFuture<Void> 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<Void> fetchPlayerData(String username) {
+ CompletableFuture<Void> 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<Void> 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) -> {
+ LiteralArgumentBuilder<FabricClientCommandSource> literalArgumentBuilder = ClientCommandManager.literal("pv")
+ .then(ClientCommandManager.argument("username", StringArgumentType.string())
+ .executes(Scheduler.queueOpenScreenFactoryCommand(context -> new ProfileViewerScreen(StringArgumentType.getString(context, "username"))))
+ )
+ .executes(Scheduler.queueOpenScreenCommand(() -> new ProfileViewerScreen(MinecraftClient.getInstance().getSession().getUsername())));
+ dispatcher.register(literalArgumentBuilder);
+ dispatcher.register(ClientCommandManager.literal(SkyblockerMod.NAMESPACE).then(literalArgumentBuilder));
+ });
+ }
+
+ @NotNull
+ public static Map<String, Map<String, ?>> fetchCollectionsData() {
+ if (!COLLECTIONS_CACHE.isEmpty()) return COLLECTIONS_CACHE;
+ try {
+ JsonObject jsonObject = JsonParser.parseString(Http.sendGetRequest(HYPIXEL_COLLECTIONS)).getAsJsonObject();
+ if (jsonObject.get("success").getAsBoolean()) {
+ Map<String, String[]> collectionsMap = new HashMap<>();
+ Map<String, List<Integer>> 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<Integer> 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 00000000..4ee2dbba
--- /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 00000000..b77c3e7a
--- /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<String, ItemStack> 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<SubPageSelectButton> 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<ClickableWidget> getButtons() {
+ List<ClickableWidget> 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 00000000..ef26332e
--- /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<ItemStack> 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<String, String[]> collectionsMap;
+ private final Map<String, List<Integer>> tierRequirementsMap;
+ private final Map<String, String> 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<String, Map<String, ?>> fetchedData = fetchCollectionsData();
+ //noinspection unchecked
+ collectionsMap = (Map<String, String[]>) fetchedData.get("COLLECTIONS");
+ //noinspection unchecked
+ tierRequirementsMap = (Map<String, List<Integer>>) fetchedData.get("TIER_REQS");
+ this.category = collection;
+ setupItemStacks(hProfile, pProfile);
+ }
+
+ private int calculateTier(int achieved, List<Integer> 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<String, NEUItem> 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<Integer> tierRequirements = tierRequirementsMap.get(collection);
+
+ List<Text> 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<Text> 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<Text> 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 00000000..3b847b1b
--- /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<String, ItemStack> 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 00000000..7c9206c0
--- /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<String, JsonElement> 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 00000000..1a62a47b
--- /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 00000000..679cc575
--- /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<String, Integer> 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<String, JsonElement> 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 00000000..b1398661
--- /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<DungeonClassWidget> 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 00000000..a2f7d9d6
--- /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<ItemStack> 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<Text> 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<ClickableWidget> getButtons() {
+ List<ClickableWidget> 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 00000000..8b0cbefc
--- /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<String, ItemStack> 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<SubPageSelectButton> 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<ClickableWidget> getButtons() {
+ List<ClickableWidget> 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 00000000..1a725e1a
--- /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 00000000..b3389d39
--- /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<String> heldItem;
+ private final int level;
+ private final ItemStack icon;
+
+ private static final Map<String, Integer> 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<String, NEUItem> 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.
+ * <p> 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 </p
+ *
+ * @param item The NEUItem representing the pet.
+ * @param helditem The NEUItem representing the held item, if any.
+ * @return The ItemStack representing the pet with all its properties set.
+ */
+ private ItemStack fromNEUItem(NEUItem item, NEUItem helditem) {
+ if (item == null) return Ico.BARRIER;
+ List<Pair<String, String>> 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.
+ * <p> 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. </p>
+ *
+ * @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<Pair<String, String>> createLoreReplacers(String itemSkyblockID, NEUItem helditem) {
+ List<Pair<String, String>> list = new ArrayList<>();
+ Map<@PetId String, Map<Rarity, PetNumbers>> 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<Double> 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<Pair<String, String>> injectors) {
+ for (Pair<String, String> 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 00000000..26673693
--- /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<ItemStack> 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<Text> 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 00000000..99e728be
--- /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<ItemStack> loadItems(JsonObject data) {
+ List<ItemStack> backpackItems = new ArrayList<>();
+
+ // Sort the data by keys numerically
+ List<Map.Entry<String, JsonElement>> 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 00000000..f73661a1
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java
@@ -0,0 +1,29 @@
+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.List;
+
+public class InventoryItemLoader extends ItemLoader {
+
+ private static final String[] INVENTORIES = {"inv_contents", "inv_armor", "equipment_contents"};
+
+ @Override
+ public List<ItemStack> loadItems(JsonObject data) {
+ List<ItemStack> inventoryItems = new ArrayList<>();
+ for (String inventory : INVENTORIES) {
+ List<ItemStack> 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 00000000..9d9b1b07
--- /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<ItemStack> loadItems(JsonObject data) {
+ NbtList containerContent = decompress(data);
+ List<ItemStack> 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<String, NEUItem> 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 00000000..cd3b7a26
--- /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<String> TIER_ORDER = List.of("MYTHIC", "LEGENDARY", "EPIC", "RARE", "UNCOMMON", "COMMON");
+
+ @Override
+ public List<ItemStack> loadItems(JsonObject data) {
+ List<Pet> 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<ItemStack> 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 00000000..9d434726
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java
@@ -0,0 +1,40 @@
+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<ItemStack> loadItems(JsonObject data) {
+ List<ItemStack> itemList = new ArrayList<>();
+
+ try {
+ itemList.addAll(super.loadItems(data));
+ if (activeSlot != -1) {
+ List<ItemStack> activeArmour = super.loadItems(activeArmorSet).reversed();
+ 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, activeArmour.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 00000000..3a3870f3
--- /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<String, ItemStack> 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<String, Integer> 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<String, Integer> SOFT_SKILL_CAP = Map.of(
+ "Taming", 50,
+ "Farming", 50
+ );
+
+ private static final Map<String, Integer> 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 00000000..c331bbdd
--- /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<SkillWidget> 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 00000000..a9c05c11
--- /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<String, Identifier> 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<String, ItemStack> 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 00000000..08e2ca06
--- /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<SlayerWidget> 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 00000000..b52fd579
--- /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<LevelInfo> GENERIC_SKILL_BOUNDARIES = createGenericSkillBoundaries();
+ private static final List<LevelInfo> CATACOMBS_SKILL_BOUNDARIES = createCatacombsSkillBoundaries();
+ private static final List<LevelInfo> RUNECRAFT_SKILL_BOUNDARIES = createRunecraftSkillBoundaries();
+ private static final List<LevelInfo> SOCIAL_SKILL_BOUNDARIES = createSocialSkillBoundaries();
+
+ private static final List<LevelInfo> COMMON_PET_BOUNDARIES = createCommonPetBoundaries();
+ private static final List<LevelInfo> UNCOMMON_PET_BOUNDARIES = createUncommonPetBoundaries();
+ private static final List<LevelInfo> RARE_PET_BOUNDARIES = createRarePetBoundaries();
+ private static final List<LevelInfo> EPIC_PET_BOUNDARIES = createEpicPetBoundaries();
+ private static final List<LevelInfo> LEGENDARY_PET_BOUNDARIES = createLegendaryPetBoundaries();
+
+
+ private static final List<LevelInfo> GENERIC_SLAYER_BOUNDARIES = createGenericSlayerBoundaries();
+ private static final List<LevelInfo> VAMPIRE_SLAYER_BOUNDARIES = createVampireSlayerBoundaries();
+
+ private static List<LevelInfo> createGenericSkillBoundaries() {
+ List<LevelInfo> 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<LevelInfo> createCatacombsSkillBoundaries() {
+ List<LevelInfo> 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<LevelInfo> createRunecraftSkillBoundaries() {
+ List<LevelInfo> 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<LevelInfo> createSocialSkillBoundaries() {
+ List<LevelInfo> 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<LevelInfo> createGenericSlayerBoundaries() {
+ List<LevelInfo> 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<LevelInfo> createVampireSlayerBoundaries() {
+ List<LevelInfo> 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<LevelInfo> createCommonPetBoundaries() {
+ List<LevelInfo> 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<LevelInfo> createUncommonPetBoundaries() {
+ List<LevelInfo> 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<LevelInfo> createRarePetBoundaries() {
+ List<LevelInfo> 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<LevelInfo> createEpicPetBoundaries() {
+ List<LevelInfo> 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<LevelInfo> createLegendaryPetBoundaries() {
+ List<LevelInfo> 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<LevelInfo> 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<LevelInfo> 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<LevelInfo> calculateCatacombsSkillBoundaries(long xp) {
+ if (xp >= CATACOMBS_SKILL_BOUNDARIES.getLast().xp) {
+ int additionalLevels = (int) ((xp - CATACOMBS_SKILL_BOUNDARIES.getLast().xp) / CATA_XP_PER_LEVEL) ;
+
+ List<LevelInfo> 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 00000000..b074952c
--- /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 00000000..4c9dcda4
--- /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 c2c952cf..21d66805 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 @@ public class Shortcuts {
// 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 818056f0..b37a3883 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 9c299210..79193b51 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 class DungeonDeathWidget extends Widget {
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 ec935faf..8f50f9ff 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 75652b33..9e1f3989 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 class GardenSkillsWidget extends Widget {
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 3c218fb1..34d15a28 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 class ReputationWidget extends Widget {
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 379fbb62..c9cf61aa 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 class SkillsWidget extends Widget {
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/ApiAuthentication.java b/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java
index fbf814ee..5f65c336 100644
--- a/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java
+++ b/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java
@@ -7,6 +7,8 @@ import java.util.Base64;
import java.util.Objects;
import java.util.UUID;
+import de.hysky.skyblocker.mixins.accessors.MinecraftClientAccessor;
+import net.minecraft.client.session.ProfileKeys;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
@@ -56,8 +58,9 @@ public class ApiAuthentication {
* was generated by Mojang and is tied to said player. For information about what the randomly signed data is used for and why see {@link #getRandomSignedData(PrivateKey)}
*/
private static void updateToken() {
+ ProfileKeys profileKeys = ((MinecraftClientAccessor) CLIENT).getProfileKeys();
//The fetching runs async in ProfileKeysImpl#getKeyPair
- CLIENT.getProfileKeys().fetchKeyPair().thenAcceptAsync(playerKeypairOpt -> {
+ profileKeys.fetchKeyPair().thenAcceptAsync(playerKeypairOpt -> {
if (playerKeypairOpt.isPresent()) {
PlayerKeyPair playerKeyPair = playerKeypairOpt.get();
diff --git a/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java b/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java
index c63af3ba..93e314a7 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 99db0316..051bd52e 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.time.Duration;
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
*/
@@ -34,7 +33,7 @@ public class Http {
.followRedirects(Redirect.NORMAL)
.build();
- private static ApiResponse sendCacheableGetRequest(String url, @Nullable String token) throws IOException, InterruptedException {
+ public static ApiResponse sendCacheableGetRequest(String url, @Nullable String token) throws IOException, InterruptedException {
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.GET()
.header("Accept", "application/json")
@@ -67,9 +66,8 @@ public class Http {
.build();
HttpResponse<InputStream> response = HTTP_CLIENT.send(request, BodyHandlers.ofInputStream());
- InputStream decodedInputStream = getDecodedInputStream(response);
- return decodedInputStream;
+ return getDecodedInputStream(response);
}
public static String sendGetRequest(String url) throws IOException, InterruptedException {
@@ -91,6 +89,7 @@ public class Http {
public static String sendPostRequest(String url, String requestBody, String contentType) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.POST(BodyPublishers.ofString(requestBody))
+ .header("Accept", contentType)
.header("Accept-Encoding", "gzip, deflate")
.header("Content-Type", contentType)
.header("User-Agent", USER_AGENT)
@@ -125,16 +124,12 @@ public class Http {
String encoding = getContentEncoding(response.headers());
try {
- switch (encoding) {
- case "":
- return response.body();
- case "gzip":
- return new GZIPInputStream(response.body());
- case "deflate":
- return new InflaterInputStream(response.body());
- default:
- throw new UnsupportedOperationException("The server sent content in an unexpected encoding: " + encoding);
- }
+ return switch (encoding) {
+ case "" -> response.body();
+ case "gzip" -> new GZIPInputStream(response.body());
+ case "deflate" -> new InflaterInputStream(response.body());
+ default -> throw new UnsupportedOperationException("The server sent content in an unexpected encoding: " + encoding);
+ };
} catch (IOException e) {
throw new UncheckedIOException(e);
}
diff --git a/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java b/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java
index a786e79f..aa7a0492 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.JsonElement;
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<String, ObjectLongPair<JsonObject>> players = new HashMap<>();
-
- public static CompletableFuture<JsonObject> updateProfile() {
- return updateProfile(MinecraftClient.getInstance().getSession().getUsername());
+ public static Map<String, ObjectLongPair<JsonObject>> profiles = new HashMap<>();
+
+ public static CompletableFuture<JsonObject> 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<JsonObject> updateProfile(String name) {
- ObjectLongPair<JsonObject> playerCache = players.get(name);
+ public static CompletableFuture<JsonObject> fetchFullProfile(String name) {
+ ObjectLongPair<JsonObject> playerCache = profiles.get(name);
if (playerCache != null && playerCache.rightLong() + HYPIXEL_API_COOLDOWN > System.currentTimeMillis()) {
return CompletableFuture.completedFuture(playerCache.left());
}
@@ -32,19 +42,12 @@ public class ProfileUtils {
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/java/de/hysky/skyblocker/utils/SkyblockTime.java b/src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java
index 045ecc4e..ec3effaf 100644
--- a/src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java
+++ b/src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java
@@ -21,10 +21,10 @@ public class SkyblockTime {
public static void init() {
updateTime();
//ScheduleCyclic already runs the task upon scheduling, so there's no need to call updateTime() here
- Scheduler.INSTANCE.schedule(() -> Scheduler.INSTANCE.scheduleCyclic(SkyblockTime::updateTime, 1200 * 24), (int) (1200000 - (getSkyblockMillis() % 1200000)) / 50);
+ Scheduler.INSTANCE.schedule(() -> Scheduler.INSTANCE.scheduleCyclic(SkyblockTime::updateTime, 1200 * 20), (int) (1200000 - (getSkyblockMillis() % 1200000)) / 50);
}
- private static long getSkyblockMillis() {
+ public static long getSkyblockMillis() {
return System.currentTimeMillis() - SKYBLOCK_EPOCH;
}
diff --git a/src/main/java/de/hysky/skyblocker/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java
index 84b3cb9e..051110b2 100644
--- a/src/main/java/de/hysky/skyblocker/utils/Utils.java
+++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java
@@ -3,7 +3,6 @@ package de.hysky.skyblocker.utils;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.util.UndashedUuid;
-
import de.hysky.skyblocker.events.SkyblockEvents;
import de.hysky.skyblocker.mixins.accessors.MessageHandlerAccessor;
import de.hysky.skyblocker.skyblock.item.MuseumItemCache;
@@ -22,11 +21,11 @@ import net.minecraft.scoreboard.*;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
+import org.apache.http.client.HttpResponseException;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.IOException;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
@@ -77,7 +76,8 @@ public class Utils {
private static boolean canSendLocRaw = false;
//This is required to prevent the location change event from being fired twice.
private static boolean locationChanged = true;
-
+ private static boolean mayorTickScheduled = false;
+ private static int mayorTickRetryAttempts = 0;
private static String mayor = "";
/**
@@ -95,15 +95,15 @@ public class Utils {
}
public static boolean isInDungeons() {
- return location == Location.DUNGEON || FabricLoader.getInstance().isDevelopmentEnvironment();
+ return location == Location.DUNGEON;
}
public static boolean isInCrystalHollows() {
- return location == Location.CRYSTAL_HOLLOWS || FabricLoader.getInstance().isDevelopmentEnvironment();
+ return location == Location.CRYSTAL_HOLLOWS;
}
public static boolean isInDwarvenMines() {
- return location == Location.DWARVEN_MINES || location == Location.GLACITE_MINESHAFT || FabricLoader.getInstance().isDevelopmentEnvironment();
+ return location == Location.DWARVEN_MINES || location == Location.GLACITE_MINESHAFT;
}
public static boolean isInTheRift() {
@@ -120,6 +120,9 @@ public class Utils {
public static boolean isInKuudra() {
return location == Location.KUUDRAS_HOLLOW;
}
+ public static boolean isInCrimson() {
+ return location == Location.CRIMSON_ISLE;
+ }
public static boolean isInModernForagingIsland() {
return location == Location.MODERN_FORAGING_ISLAND;
@@ -191,11 +194,16 @@ public class Utils {
}
public static void init() {
- SkyblockEvents.JOIN.register(() -> tickMayorCache(false));
+ SkyblockEvents.JOIN.register(() -> {
+ if (!mayorTickScheduled) {
+ tickMayorCache();
+ scheduleMayorTick();
+ mayorTickScheduled = true;
+ }
+ });
ClientPlayConnectionEvents.JOIN.register(Utils::onClientWorldJoin);
ClientReceiveMessageEvents.ALLOW_GAME.register(Utils::onChatMessage);
ClientReceiveMessageEvents.GAME_CANCELED.register(Utils::onChatMessage); // Somehow this works even though onChatMessage returns a boolean
- Scheduler.INSTANCE.scheduleCyclic(() -> tickMayorCache(true), 24_000, true); // Update every 20 minutes
}
/**
@@ -358,8 +366,8 @@ public class Utils {
// TODO: Combine with `ChocolateFactorySolver.formatTime` and move into `SkyblockTime`.
public static Text getDurationText(int timeInSeconds) {
int seconds = timeInSeconds % 60;
- int minutes = (timeInSeconds/60) % 60;
- int hours = (timeInSeconds/3600);
+ int minutes = (timeInSeconds / 60) % 60;
+ int hours = (timeInSeconds / 3600);
MutableText time = Text.empty();
if (hours > 0) {
@@ -460,7 +468,11 @@ public class Utils {
if (message.startsWith(PROFILE_MESSAGE_PREFIX)) {
profile = message.substring(PROFILE_MESSAGE_PREFIX.length()).split("§b")[0];
} else if (message.startsWith(PROFILE_ID_PREFIX)) {
+ String prevProfileId = profileId;
profileId = message.substring(PROFILE_ID_PREFIX.length());
+ if (!prevProfileId.equals(profileId)) {
+ SkyblockEvents.PROFILE_CHANGE.invoker().onSkyblockProfileChange(prevProfileId, profileId);
+ }
MuseumItemCache.tick(profileId);
}
@@ -479,27 +491,47 @@ public class Utils {
location = Location.UNKNOWN;
}
- private static void tickMayorCache(boolean refresh) {
- if (!mayor.isEmpty() && !refresh) return;
+ private static void scheduleMayorTick() {
+ long currentYearMillis = SkyblockTime.getSkyblockMillis() % 446400000L; //446400000ms is 1 year, 105600000ms is the amount of time from early spring 1st to late spring 27th
+ // If current time is past late spring 27th, the next mayor change is at next year's spring 27th, otherwise it's at this year's spring 27th
+ long millisUntilNextMayorChange = currentYearMillis > 105600000L ? 446400000L - currentYearMillis + 105600000L : 105600000L - currentYearMillis;
+ Scheduler.INSTANCE.schedule(Utils::tickMayorCache, (int) (millisUntilNextMayorChange / 50) + 5 * 60 * 20); // 5 extra minutes to allow the cache to expire. This is a simpler than checking age and subtracting from max age and rescheduling again.
+ }
+ private static void tickMayorCache() {
CompletableFuture.supplyAsync(() -> {
try {
- JsonObject json = JsonParser.parseString(Http.sendGetRequest("https://api.hypixel.net/v2/resources/skyblock/election")).getAsJsonObject();
- if (json.get("success").getAsBoolean()) return json.get("mayor").getAsJsonObject().get("name").getAsString();
- throw new IOException(json.get("cause").getAsString());
+ Http.ApiResponse response = Http.sendCacheableGetRequest("https://api.hypixel.net/v2/resources/skyblock/election", null); //Authentication is not required for this endpoint
+ if (!response.ok()) throw new HttpResponseException(response.statusCode(), response.content());
+ JsonObject json = JsonParser.parseString(response.content()).getAsJsonObject();
+ if (!json.get("success").getAsBoolean()) throw new RuntimeException("Request failed!"); //Can't find a more appropriate exception to throw here.
+ return json.get("mayor").getAsJsonObject().get("name").getAsString();
} catch (Exception e) {
- LOGGER.error("[Skyblocker] Failed to get mayor status!", e);
+ throw new RuntimeException(e); //Wrap the exception to be handled by the exceptionally block
+ }
+ }).exceptionally(throwable -> {
+ LOGGER.error("[Skyblocker] Failed to get mayor status!", throwable.getCause());
+ if (mayorTickRetryAttempts < 5) {
+ int minutes = 5 * 1 << mayorTickRetryAttempts; //5, 10, 20, 40, 80 minutes
+ mayorTickRetryAttempts++;
+ LOGGER.warn("[Skyblocker] Retrying in {} minutes.", minutes);
+ Scheduler.INSTANCE.schedule(Utils::tickMayorCache, minutes * 60 * 20);
+ } else {
+ LOGGER.warn("[Skyblocker] Failed to get mayor status after 5 retries! Stopping further retries until next reboot.");
+ }
+ return ""; //Have to return a value for the thenAccept block.
+ }).thenAccept(result -> {
+ if (!result.isEmpty()) {
+ mayor = result;
+ LOGGER.info("[Skyblocker] Mayor set to {}.", mayor);
+ scheduleMayorTick(); //Ends up as a cyclic task with finer control over scheduled time
}
- return "";
- }).thenAccept(s -> {
- if (!s.isEmpty()) mayor = s;
});
-
}
/**
* Used to avoid triggering things like chat rules or chat listeners infinitely, do not use otherwise.
- *
+ * <p>
* Bypasses MessageHandler#onGameMessage
*/
public static void sendMessageToBypassEvents(Text message) {
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java
index 8a5d32be..9a1e3072 100644
--- a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java
+++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java
@@ -12,6 +12,7 @@ import de.hysky.skyblocker.skyblock.dungeon.terminal.ColorTerminal;
import de.hysky.skyblocker.skyblock.dungeon.terminal.LightsOnTerminal;
import de.hysky.skyblocker.skyblock.dungeon.terminal.OrderTerminal;
import de.hysky.skyblocker.skyblock.dungeon.terminal.StartsWithTerminal;
+import de.hysky.skyblocker.skyblock.dwarven.CommissionHighlight;
import de.hysky.skyblocker.skyblock.experiment.ChronomatronSolver;
import de.hysky.skyblocker.skyblock.experiment.SuperpairsSolver;
import de.hysky.skyblocker.skyblock.experiment.UltrasequencerSolver;
@@ -51,6 +52,7 @@ public class ContainerSolverManager {
new StartsWithTerminal(),
new LightsOnTerminal(),
new CroesusHelper(),
+ new CommissionHighlight(),
new CroesusProfit(),
new ChronomatronSolver(),
new SuperpairsSolver(),
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 2f5375fe..547adc5c 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 @@ package de.hysky.skyblocker.utils.scheduler;
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.ArrayList;
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 class Scheduler {
}
}
+ /**
+ * 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<FabricClientCommandSource> queueOpenScreenFactoryCommand(Function<CommandContext<FabricClientCommandSource>, 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<FabricClientCommandSource> queueOpenScreenCommand(Supplier<Screen> 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<FabricClientCommandSource> 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<Screen> screenSupplier) {
- MinecraftClient.getInstance().send(() -> MinecraftClient.getInstance().setScreen(screenSupplier.get()));
+ @Deprecated(forRemoval = true)
+ public static int queueOpenScreen(Supplier<Screen> 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;
}
diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json
index 216a6e85..5ac9dbbb 100644
--- a/src/main/resources/assets/skyblocker/lang/en_us.json
+++ b/src/main/resources/assets/skyblocker/lang/en_us.json
@@ -19,6 +19,8 @@
"text.skyblocker.modrinth": "Modrinth",
"text.skyblocker.discord": "Discord",
+ "skyblocker.skyblockerScreen": "Skyblocker Main Screen",
+
"skyblocker.config.title": "Skyblocker Settings",
"skyblocker.config.crimsonIsle": "Crimson Isle",
@@ -321,6 +323,11 @@
"skyblocker.config.helpers.fishing.hideOtherPlayers": "Hide Other Players Rods",
"skyblocker.config.helpers.fishing.hideOtherPlayers.@Tooltip": "Hide other players fishing rods from showing for you",
+ "skyblocker.config.helpers.jerry": "Jerry",
+ "skyblocker.config.helpers.jerry.enableJerryTimer": "Enable Jerry Timer",
+ "skyblocker.config.helpers.jerry.enableJerryTimer.@Tooltip": "Sends a message in chat and plays a sound when the hidden jerry spawn cooldown is over.",
+
+
"skyblocker.config.helpers.mythologicalRitual": "Mythological Ritual Helper",
"skyblocker.config.helpers.mythologicalRitual.enableMythologicalRitualHelper": "Enable Mythological Ritual Helper",
@@ -434,6 +441,8 @@
"skyblocker.config.mining.crystalHollows": "Crystal Hollows",
"skyblocker.config.mining.crystalHollows.metalDetectorHelper": "Metal Detector Helper",
"skyblocker.config.mining.crystalHollows.metalDetectorHelper.@Tooltip": "Helper for the metal detector puzzle in the Mines of Divan.",
+ "skyblocker.config.mining.crystalHollows.nucleusWaypoints": "Nucleus Waypoints",
+ "skyblocker.config.mining.crystalHollows.nucleusWaypoints.@Tooltip": "Show waypoints to the Nucleus in the Crystal Hollows.",
"skyblocker.config.mining.crystalsHud": "Crystal Hollows Map",
"skyblocker.config.mining.crystalsHud.enabled": "Enabled",
@@ -466,6 +475,8 @@
"skyblocker.config.mining.enableDrillFuel": "Enable Drill Fuel",
+ "skyblocker.config.mining.commissionHighlight": "Highlights Completed Commissions",
+
"skyblocker.config.mining.glacite": "Glacite Tunnels",
"skyblocker.config.mining.glacite.coldOverlay": "Cold Overlay",
"skyblocker.config.mining.glacite.coldOverlay@Tooltip": "Shows a frosty overlay in the Glacite mines that gets stronger as you get colder.",
@@ -610,6 +621,8 @@
"skyblocker.config.uiAndVisuals.searchOverlay.maxSuggestions": "Maximum Suggestions",
"skyblocker.config.uiAndVisuals.searchOverlay.maxSuggestions.@Tooltip": "The maximum number of suggested items to show.",
+ "skyblocker.config.uiAndVisuals.showEquipmentInInventory": "Show Equipment in Inventory",
+
"skyblocker.config.uiAndVisuals.tabHud": "Fancy tab HUD (Temporarily disabled outside dungeons)",
"skyblocker.config.uiAndVisuals.tabHud.enableHudBackground": "Enable HUD Background",
"skyblocker.config.uiAndVisuals.tabHud.enableHudBackground.@Tooltip": "Enables the background of the non-tab HUD.",
@@ -692,7 +705,7 @@
"skyblocker.api.cache.HIT": "This data was cached!\nIt's %d seconds old.",
"skyblocker.api.cache.MISS": "This data wasn't cached!",
"skyblocker.api.token.authFailure": "Failed to refresh your Skyblocker API token, some features may not work temporarily!",
- "skyblocker.api.token.noProfileKeys": "Failed to get your profile keys! Some features of the mod may not work temporarily :( (Has your game been open for more than 24 hours?)",
+ "skyblocker.api.token.noProfileKeys": "Failed to get your profile keys! Some features of the mod may not work temporarily :( (Has your game been open for more than 24 hours?). To reactivate these features restart the game.",
"skyblocker.exotic.crystal": "CRYSTAL",
"skyblocker.exotic.fairy": "FAIRY",
@@ -817,8 +830,11 @@
"skyblocker.tips.enabled": "§aEnabled Tips.",
"skyblocker.tips.disabled": "§cDisabled Tips.",
+ "skyblocker.tips.previous": "Previous Tip",
+ "skyblocker.tips.next": "Next Tip",
"skyblocker.tips.clickEnable": "§a[Click to Enable Tips]",
"skyblocker.tips.clickDisable": "§c[Click to Disable Tips]",
+ "skyblocker.tips.clickPreviousTip": "§b[Click for Previous Tip]",
"skyblocker.tips.clickNextTip": "§a[Click for Next Tip]",
"skyblocker.tips.tip": "§aTip: %s\n",
"skyblocker.tips.customItemNames": "Customize the names of your items with /skyblocker custom renameItem",
@@ -835,12 +851,31 @@
"skyblocker.tips.modMenuUpdate": "ModMenu will let you know if there's an update available for Skyblocker for your game version.",
"skyblocker.tips.issues": "Submit bug reports and feature requests to https://github.com/SkyblockerMod/Skyblocker.",
"skyblocker.tips.beta": "We often have beta versions available from GitHub Actions that contain new and experimental features.",
+ "skyblocker.tips.contribute": "We welcome contributions to the mod! See https://github.com/SkyblockerMod/Skyblocker/wiki/contribute for more info.",
"skyblocker.tips.discord": "Join our discord at https://discord.gg/aNNJHQykck to keep up with the latest news about Skyblocker!",
"skyblocker.tips.flameOverlay": "Find that the flame overlay takes up too much screen space? Check out the config to make it smaller",
"skyblocker.tips.wikiLookup": "Press F4 while hovering over an item to open its wiki page in your web browser.",
"skyblocker.tips.protectItem": "Prevent accidentally dropping your items with /skyblocker protectItem.",
"skyblocker.tips.fairySoulsEnigmaSoulsRelics": "Don't know where to find Fairy Souls, Enigma Souls, or Relics? Enable the helpers to aid your exploration, they'll remember which souls you've already found.",
"skyblocker.tips.quickNav": "You can customize the QuickNav buttons in the config.",
+ "skyblocker.tips.waypoints": "You can import (including from skytils), add, remove, and toggle waypoints on any island with /skyblocker waypoints.",
+ "skyblocker.tips.orderedWaypoints": "You can import (including from coleweight), add, remove, and toggle ordered waypoints with /skyblocker waypoints ordered.",
+ "skyblocker.tips.visitorHelper": "Click on the item name in the visitor helper to buy from the bazaar or click on [Copy Amount] to copy the amount to your clipboard.",
+ "skyblocker.tips.slotText": "Slot text shows you the attribute shard info, catacombs level, collection level, enchantment book level, minion level, pet level, potion level, prehistoric egg blocks walked, rancher's boots speed cap, skill level, skyblock level in the slot.",
+ "skyblocker.tips.profileViewer": "You can view other player's profiles with /pv.",
+ "skyblocker.tips.configSearch": "You can search the entire config using the search bar on the bottom right of the config screen.",
+ "skyblocker.tips.compactDamage": "Customize your damage value with Compact Damage in the config.",
+ "skyblocker.tips.skyblockerScreen": "You can access the Skyblocker screen to open the config or checkout helpful links for the mod with /skyblocker.",
+ "skyblocker.tips.tipsClick": "Click on a tip chat message to run its suggestion!",
+ "skyblocker.tips.eventNotifications": "Check out the customizable event notifications in the config.",
+ "skyblocker.tips.signCalculator": "Type an math expression in a sign to have the mod calculate it for you.",
+ "skyblocker.tips.calculateCommand": "Enter an expression after /skyblocker calculate to have the mod calculate it for you.",
+ "skyblocker.tips.fancierBars": "Customize the look of your health, mana, defense, and experience bars with /skyblocker bars. You can snap and resize bars too!",
+ "skyblocker.tips.crystalWaypointsShare": "Share Crystal Hollows Waypoints with /skyblocker crystalWaypoints share.",
+ "skyblocker.tips.gardenMouseLock": "Lock your mouse while farming in the Skyblocker Garden config.",
+ "skyblocker.tips.newYearCakesHelper": "Open your New Year Cake Bag and Skyblocker will remember them and highlight duplicate cakes red and missing cakes green.",
+ "skyblocker.tips.accessoryHelper": "Open your accessory bag and Skyblocker will remember them. The accessory helper will tell you what accessories you have and what accessories are missing.",
+ "skyblocker.tips.fancyAuctionHouseCheapHighlight": "Cheap BINs are highlighted in green in the Fancy Auction House.",
"skyblocker.partyFinder.tabs.partyFinder": "Party Finder",
"skyblocker.partyFinder.tabs.searchSettings": "Search Filters",
@@ -872,6 +907,22 @@
"skyblocker.fancyAuctionHouse.yourAuction": "This is your auction!",
"skyblocker.fancyAuctionHouse.youPay": "You pay: %s",
+ "skyblocker.crimson.dojo": "Dojo",
+ "skyblocker.crimson.dojo.forceHelper": "Enable Force Helper",
+ "skyblocker.crimson.dojo.forceHelper.@Tooltip": "Shows timer showing how long until a zombie despawns and outlines negative zombies.",
+ "skyblocker.crimson.dojo.staminaHelper": "Enable Stamina Helper",
+ "skyblocker.crimson.dojo.staminaHelper.@Tooltip": "Highlights the holes in the walls turning orange once you have been through a wall.",
+ "skyblocker.crimson.dojo.masteryHelper": "Enable Mastery Helper",
+ "skyblocker.crimson.dojo.masteryHelper.@Tooltip": "Shows count down to when to relase the bow and a path to follow.",
+ "skyblocker.crimson.dojo.disciplineHelper": "Enable Discipline Helper",
+ "skyblocker.crimson.dojo.disciplineHelper.@Tooltip": "Outlines the zombies to attack with currently held sword.",
+ "skyblocker.crimson.dojo.swiftnessHelper": "Enable Swiftness Helper",
+ "skyblocker.crimson.dojo.swiftnessHelper.@Tooltip": "highlights the newest wool block to go to.",
+ "skyblocker.crimson.dojo.controlHelper": "Enable Control Helper",
+ "skyblocker.crimson.dojo.controlHelper.@Tooltip": "enders an outline around where to aim.",
+ "skyblocker.crimson.dojo.tenacityHelper": "Enable Tenacity Helper",
+ "skyblocker.crimson.dojo.tenacityHelper.@Tooltip": "Shows a path for each fireball and predicted block they are going to hit.",
+
"skyblocker.crimson.kuudra.noArrowPoison": "No Arrow Poison!",
"skyblocker.crimson.kuudra.lowArrowPoison": "Low on Arrow Poison!",
diff --git a/src/main/resources/assets/skyblocker/lang/lol_us.json b/src/main/resources/assets/skyblocker/lang/lol_us.json
new file mode 100644
index 00000000..4cda6799
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/lang/lol_us.json
@@ -0,0 +1,3 @@
+{
+ "key.categories.skyblocker": "Skibidiblocker"
+}
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 00000000..0edf5705
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/base_plate.png
Binary files differ
diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/blaze.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/blaze.png
new file mode 100644
index 00000000..ed168ddf
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/blaze.png
Binary files differ
diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon.png
new file mode 100644
index 00000000..d29969be
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon.png
Binary files differ
diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_highlighted.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_highlighted.png
new file mode 100644
index 00000000..11340a55
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_highlighted.png
Binary files differ
diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled.png
new file mode 100644
index 00000000..109cd859
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled.png
Binary files differ
diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled_highlighted.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled_highlighted.png
new file mode 100644
index 00000000..581cdefe
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled_highlighted.png
Binary files differ
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 00000000..379557e4
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_body.png
Binary files differ
diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_header.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_header.png
new file mode 100644
index 00000000..384dc0e3
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_header.png
Binary files differ
diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/enderman.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/enderman.png
new file mode 100644
index 00000000..84650c7f
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/enderman.png
Binary files differ
diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/icon_data_widget.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/icon_data_widget.png
new file mode 100644
index 00000000..c28aeed4
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/icon_data_widget.png
Binary files differ
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 00000000..d83fad70
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/run_icon.png
Binary files differ
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 00000000..a5daa3d6
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/spider.png
Binary files differ
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 00000000..efbe985d
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/vampire.png
Binary files differ
diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/wolf.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/wolf.png
new file mode 100644
index 00000000..f7bec9a4
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/wolf.png
Binary files differ
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 00000000..37ea069f
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/zombie.png
Binary files differ
diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/equipment/empty_icon.png b/src/main/resources/assets/skyblocker/textures/gui/sprites/equipment/empty_icon.png
new file mode 100644
index 00000000..be89af40
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/sprites/equipment/empty_icon.png
Binary files differ
diff --git a/src/main/resources/skyblocker.accesswidener b/src/main/resources/skyblocker.accesswidener
index 2464d6d5..9fcdfa45 100644
--- a/src/main/resources/skyblocker.accesswidener
+++ b/src/main/resources/skyblocker.accesswidener
@@ -1 +1,20 @@
-accessWidener v2 named \ No newline at end of file
+accessWidener v2 named
+
+accessible class net/minecraft/client/render/RenderLayer$MultiPhase
+accessible class net/minecraft/client/render/RenderLayer$MultiPhaseParameters
+accessible class net/minecraft/client/render/RenderPhase$Transparency
+accessible class net/minecraft/client/render/RenderPhase$ShaderProgram
+accessible class net/minecraft/client/render/RenderPhase$Texture
+accessible class net/minecraft/client/render/RenderPhase$TextureBase
+accessible class net/minecraft/client/render/RenderPhase$Texturing
+accessible class net/minecraft/client/render/RenderPhase$Lightmap
+accessible class net/minecraft/client/render/RenderPhase$Overlay
+accessible class net/minecraft/client/render/RenderPhase$Cull
+accessible class net/minecraft/client/render/RenderPhase$DepthTest
+accessible class net/minecraft/client/render/RenderPhase$WriteMaskState
+accessible class net/minecraft/client/render/RenderPhase$Layering
+accessible class net/minecraft/client/render/RenderPhase$Target
+accessible class net/minecraft/client/render/RenderPhase$LineWidth
+accessible class net/minecraft/client/render/RenderPhase$ColorLogic
+accessible class net/minecraft/client/render/RenderPhase$OffsetTexturing
+accessible class net/minecraft/client/render/RenderPhase$Textures
diff --git a/src/main/resources/skyblocker.mixins.json b/src/main/resources/skyblocker.mixins.json
index 9b96ba61..685758f7 100644
--- a/src/main/resources/skyblocker.mixins.json
+++ b/src/main/resources/skyblocker.mixins.json
@@ -8,6 +8,7 @@
"BatEntityMixin",
"ClientPlayerEntityMixin",
"ClientPlayNetworkHandlerMixin",
+ "ClientWorldMixin",
"CommandTreeS2CPacketMixin",
"ComponentHolderMixin",
"DataTrackerMixin",
@@ -26,6 +27,7 @@
"LivingEntityRendererMixin",
"MinecraftClientMixin",
"MouseMixin",
+ "PingMeasurerMixin",
"PlayerInventoryMixin",
"PlayerListHudMixin",
"PlayerSkinProviderMixin",
@@ -45,6 +47,7 @@
"accessors.FrustumInvoker",
"accessors.HandledScreenAccessor",
"accessors.MessageHandlerAccessor",
+ "accessors.MinecraftClientAccessor",
"accessors.PlayerListHudAccessor",
"accessors.RecipeBookWidgetAccessor",
"accessors.ScreenAccessor",