aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/de/hysky/skyblocker')
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerMod.java17
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerScreen.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockerEMIPlugin.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/config/ConfigUtils.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java1
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/EventNotificationsCategory.java75
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/EventNotificationsConfig.java44
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java11
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/QuickNavigationConfig.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/config/datafixer/ConfigDataFixer.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/config/datafixer/ConfigFix2QuickNav.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/events/HudRenderEvents.java5
-rw-r--r--src/main/java/de/hysky/skyblocker/injected/SkyblockerStack.java20
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/ComponentHolderMixin.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/DrawContextMixin.java52
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java548
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/InGameHudMixin.java16
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java176
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/accessors/BeaconBlockEntityRendererInvoker.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/accessors/CheckboxWidgetAccessor.java11
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/accessors/MinecraftClientAccessor.java12
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java66
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/PetCache.java148
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java5
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionBrowserScreen.java12
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionViewScreen.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/AuctionTypeWidget.java6
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/CategoryTabWidget.java31
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/RarityWidget.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/SortWidget.java6
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleAnnouncementScreen.java9
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java391
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java37
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java13
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/crimson/kuudra/KuudraWaypoints.java5
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java28
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMapConfigScreen.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScoreHUD.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonTextures.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/FireFreezeStaffTimer.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/partyfinder/PartyEntry.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/partyfinder/PartyFinderScreen.java6
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java13
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/IceFill.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Silverfish.java6
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/boulder/Boulder.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Waterboard.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java16
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/ColorTerminal.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsHud.java11
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dwarven/DwarvenHud.java7
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dwarven/GlaciteColdOverlay.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dwarven/MetalDetector.java110
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/end/EndHudWidget.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/end/EnderNodes.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/end/TheEnd.java5
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java185
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/events/EventToast.java93
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/events/JacobEventToast.java60
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java10
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBar.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHud.java60
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java94
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/garden/LowerSensitivity.java17
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java52
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java14
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java44
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java117
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockCraftingTableScreen.java16
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotText.java21
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextAdder.java58
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java75
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/TextPosition.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/AttributeShardAdder.java (renamed from src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java)48
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CatacombsLevelAdder.java105
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CollectionAdder.java32
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java60
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/EnchantmentLevelAdder.java46
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/EssenceShopAdder.java51
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/MinionLevelAdder.java34
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PetLevelAdder.java29
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PotionLevelAdder.java32
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PowerStonesGuideAdder.java38
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PrehistoricEggAdder.java34
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/RancherBootsSpeedAdder.java36
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkillLevelAdder.java34
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkyblockLevelAdder.java29
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/StatsTuningAdder.java42
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/YourEssenceAdder.java46
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/BackpackPreview.java17
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorPreviewTooltipComponent.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ExoticTooltip.java96
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java392
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipAdder.java45
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java11
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java87
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AccessoryTooltip.java45
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AvgBinTooltip.java63
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/BazaarPriceTooltip.java57
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ColorTooltip.java126
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/DungeonQualityTooltip.java59
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LBinTooltip.java40
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LineSmoothener.java34
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MotesTooltip.java50
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MuseumTooltip.java49
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java28
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ObtainedDateTooltip.java73
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/SupercraftReminder.java32
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java92
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java138
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRepository.java17
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ResultButtonWidget.java32
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java19
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/UpcomingEventsTab.java168
-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/quicknav/QuickNavButton.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/rift/EffigyWaypoints.java9
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/rift/EnigmaSouls.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/rift/MirrorverseWaypoints.java5
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/OverlayScreen.java105
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/SearchOverManager.java122
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java1
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigListWidget.java6
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigScreen.java15
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/ScreenMaster.java4
-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/JacobsContestWidget.java2
-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/skyblock/waypoint/AbstractWaypointsScreen.java73
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/waypoint/DropdownWidget.java141
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/waypoint/FairySouls.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/waypoint/Relics.java5
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/waypoint/Waypoints.java132
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsListWidget.java318
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsScreen.java76
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsShareScreen.java86
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java172
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ApiUtils.java6
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ColorUtils.java10
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Http.java46
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ItemUtils.java87
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/PosUtils.java5
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java41
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/RomanNumerals.java54
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Utils.java27
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/config/DurationController.java70
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/config/DurationControllerWidget.java38
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/datafixer/ItemStackComponentizationFixer.java28
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java53
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractContainerMatcher.java29
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractPopupScreen.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java10
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/gui/SideTabButtonWidget.java39
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainer.java5
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java46
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/waypoint/NamedWaypoint.java128
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/waypoint/ProfileAwareWaypoint.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java48
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/waypoint/WaypointCategory.java43
204 files changed, 8353 insertions, 1653 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
index a9aab3b6..05c7fb1e 100644
--- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
@@ -2,7 +2,6 @@ package de.hysky.skyblocker;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
-import de.hysky.skyblocker.config.ImageRepoLoader;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.config.datafixer.ConfigDataFixer;
import de.hysky.skyblocker.debug.Debug;
@@ -26,15 +25,18 @@ import de.hysky.skyblocker.skyblock.end.EnderNodes;
import de.hysky.skyblocker.skyblock.end.TheEnd;
import de.hysky.skyblocker.skyblock.entity.MobBoundingBoxes;
import de.hysky.skyblocker.skyblock.fancybars.FancyStatusBars;
+import de.hysky.skyblocker.skyblock.events.EventNotifications;
import de.hysky.skyblocker.skyblock.garden.FarmingHud;
import de.hysky.skyblocker.skyblock.garden.LowerSensitivity;
import de.hysky.skyblocker.skyblock.garden.VisitorHelper;
import de.hysky.skyblocker.skyblock.item.*;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextManager;
import de.hysky.skyblocker.skyblock.item.tooltip.AccessoriesHelper;
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.quicknav.QuickNav;
+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;
@@ -46,6 +48,7 @@ import de.hysky.skyblocker.skyblock.waypoint.FairySouls;
import de.hysky.skyblocker.skyblock.waypoint.MythologicalRitual;
import de.hysky.skyblocker.skyblock.waypoint.OrderedWaypoints;
import de.hysky.skyblocker.skyblock.waypoint.Relics;
+import de.hysky.skyblocker.skyblock.waypoint.Waypoints;
import de.hysky.skyblocker.utils.*;
import de.hysky.skyblocker.utils.chat.ChatMessageListener;
import de.hysky.skyblocker.utils.discord.DiscordRPCManager;
@@ -103,15 +106,17 @@ public class SkyblockerMod implements ClientModInitializer {
Utils.init();
SkyblockerConfigManager.init();
SkyblockerScreen.initClass();
+ ProfileViewerScreen.initClass();
Tips.init();
NEURepoManager.init();
- ImageRepoLoader.init();
+ //ImageRepoLoader.init();
ItemRepository.init();
PlayerHeadHashCache.init();
HotbarSlotLock.init();
ItemTooltip.init();
AccessoriesHelper.init();
WikiLookup.init();
+ Waypoints.init();
FairySouls.init();
Relics.init();
MythologicalRitual.init();
@@ -170,13 +175,15 @@ public class SkyblockerMod implements ClientModInitializer {
VisitorHelper.init();
ItemRarityBackgrounds.init();
MuseumItemCache.init();
+ PetCache.init();
SecretsTracker.init();
+ ApiAuthentication.init();
ApiUtils.init();
- ProfileUtils.init();
Debug.init();
Kuudra.init();
RenderHelper.init();
FancyStatusBars.init();
+ EventNotifications.init();
containerSolverManager.init();
statusBarTracker.init();
BeaconHighlighter.init();
@@ -185,6 +192,8 @@ public class SkyblockerMod implements ClientModInitializer {
EggFinder.init();
TimeTowerReminder.init();
SkyblockTime.init();
+ TooltipManager.init();
+ SlotTextManager.init();
Scheduler.INSTANCE.scheduleCyclic(Utils::update, 20);
Scheduler.INSTANCE.scheduleCyclic(DiscordRPCManager::updateDataAndPresence, 200);
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java b/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java
index 15c855a0..0196a37a 100644
--- a/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java
@@ -41,7 +41,7 @@ public class SkyblockerScreen extends Screen {
static {
LocalDate date = LocalDate.now();
- ICON = date.getMonthValue() == 4 && date.getDayOfMonth() == 1 ? new Identifier(SkyblockerMod.NAMESPACE, "icons.png") : new Identifier(SkyblockerMod.NAMESPACE, "icon.png");
+ ICON = date.getMonthValue() == 4 && date.getDayOfMonth() == 1 ? Identifier.of(SkyblockerMod.NAMESPACE, "icons.png") : Identifier.of(SkyblockerMod.NAMESPACE, "icon.png");
}
private SkyblockerScreen() {
diff --git a/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockerEMIPlugin.java b/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockerEMIPlugin.java
index 8dfc5dc9..c02300ec 100644
--- a/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockerEMIPlugin.java
+++ b/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockerEMIPlugin.java
@@ -16,9 +16,9 @@ import net.minecraft.util.Identifier;
* EMI integration
*/
public class SkyblockerEMIPlugin implements EmiPlugin {
- public static final Identifier SIMPLIFIED_TEXTURES = new Identifier("emi", "textures/gui/widgets.png");
+ public static final Identifier SIMPLIFIED_TEXTURES = Identifier.of("emi", "textures/gui/widgets.png");
// TODO: Custom simplified texture for Skyblock
- public static final EmiRecipeCategory SKYBLOCK = new EmiRecipeCategory(new Identifier(SkyblockerMod.NAMESPACE, "skyblock"), EmiStack.of(ItemUtils.getSkyblockerStack()), new EmiTexture(SIMPLIFIED_TEXTURES, 240, 240, 16, 16));
+ public static final EmiRecipeCategory SKYBLOCK = new EmiRecipeCategory(Identifier.of(SkyblockerMod.NAMESPACE, "skyblock"), EmiStack.of(ItemUtils.getSkyblockerStack()), new EmiTexture(SIMPLIFIED_TEXTURES, 240, 240, 16, 16));
@Override
public void register(EmiRegistry registry) {
diff --git a/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java b/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java
index 781f7f15..ee8dfb19 100644
--- a/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java
+++ b/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java
@@ -52,7 +52,7 @@ public class ConfigUtils {
public static OptionDescription withImage(Path imagePath, @Nullable Text... texts) {
return OptionDescription.createBuilder()
.text(ArrayUtils.isNotEmpty(texts) ? texts : new Text[] {})
- .image(IMAGE_DIRECTORY.resolve(imagePath), new Identifier(SkyblockerMod.NAMESPACE, "config_image_" + FileUtils.normalizePath(imagePath)))
+ .image(IMAGE_DIRECTORY.resolve(imagePath), Identifier.of(SkyblockerMod.NAMESPACE, "config_image_" + FileUtils.normalizePath(imagePath)))
.build();
}
}
diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
index 9c495382..c9246599 100644
--- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
@@ -44,5 +44,8 @@ public class SkyblockerConfig {
public QuickNavigationConfig quickNav = new QuickNavigationConfig();
@SerialEntry
+ public EventNotificationsConfig eventNotifications = new EventNotificationsConfig();
+
+ @SerialEntry
public MiscConfig misc = new MiscConfig();
}
diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java
index dd406b8a..f519473c 100644
--- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java
+++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java
@@ -83,6 +83,7 @@ public class SkyblockerConfigManager {
.category(SlayersCategory.create(defaults, config))
.category(ChatCategory.create(defaults, config))
.category(QuickNavigationCategory.create(defaults, config))
+ .category(EventNotificationsCategory.create(defaults, config))
.category(MiscCategory.create(defaults, config))).generateScreen(parent);
}
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/EventNotificationsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/EventNotificationsCategory.java
new file mode 100644
index 00000000..ae4882f2
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/categories/EventNotificationsCategory.java
@@ -0,0 +1,75 @@
+package de.hysky.skyblocker.config.categories;
+
+import de.hysky.skyblocker.config.ConfigUtils;
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.config.configs.EventNotificationsConfig;
+import de.hysky.skyblocker.skyblock.events.EventNotifications;
+import de.hysky.skyblocker.utils.config.DurationController;
+import dev.isxander.yacl3.api.*;
+import it.unimi.dsi.fastutil.ints.IntImmutableList;
+import it.unimi.dsi.fastutil.ints.IntList;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.sound.PositionedSoundInstance;
+import net.minecraft.text.Text;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class EventNotificationsCategory {
+
+ private static boolean shouldPlaySound = false;
+
+ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig config) {
+ shouldPlaySound = false;
+ return ConfigCategory.createBuilder()
+ .name(Text.translatable("skyblocker.config.eventNotifications"))
+ .option(Option.<EventNotificationsConfig.Criterion>createBuilder()
+ .binding(defaults.eventNotifications.criterion,
+ () -> config.eventNotifications.criterion,
+ criterion -> config.eventNotifications.criterion = criterion)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .name(Text.translatable("skyblocker.config.eventNotifications.criterion"))
+ .build())
+ .option(Option.<EventNotificationsConfig.Sound>createBuilder()
+ .binding(defaults.eventNotifications.reminderSound,
+ () -> config.eventNotifications.reminderSound,
+ sound -> config.eventNotifications.reminderSound = sound)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .name(Text.translatable("skyblocker.config.eventNotifications.notificationSound"))
+ .listener((soundOption, sound) -> {
+ if (!shouldPlaySound) {
+ shouldPlaySound = true;
+ return;
+ }
+ if (sound.getSoundEvent() != null)
+ MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(sound.getSoundEvent(), 1f, 1f));
+ })
+ .build())
+ .groups(createGroups(config))
+ .build();
+
+ }
+
+ private static List<OptionGroup> createGroups(SkyblockerConfig config) {
+ Map<String, IntList> eventsReminderTimes = config.eventNotifications.eventsReminderTimes;
+ List<OptionGroup> groups = new ArrayList<>(eventsReminderTimes.size());
+ if (eventsReminderTimes.isEmpty()) return List.of(OptionGroup.createBuilder().option(LabelOption.create(Text.translatable("skyblocker.config.eventNotifications.monologue"))).build());
+ for (Map.Entry<String, IntList> entry : eventsReminderTimes.entrySet()) {
+ groups.add(ListOption.<Integer>createBuilder()
+ .name(Text.literal(entry.getKey()))
+ .binding(EventNotifications.DEFAULT_REMINDERS, entry::getValue, integers -> entry.setValue(new IntImmutableList(integers)))
+ .controller(option -> () -> new DurationController(option)) // yea
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.eventNotifications.@Tooltip[0]"),
+ Text.empty(),
+ Text.translatable("skyblocker.config.eventNotifications.@Tooltip[1]"),
+ Text.empty(),
+ Text.translatable("skyblocker.config.eventNotifications.@Tooltip[2]", entry.getKey())))
+ .initial(60)
+ .collapsed(true)
+ .build()
+ );
+ }
+ return groups;
+ }
+}
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 1477d669..fa87be3d 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
@@ -211,6 +211,14 @@ public class GeneralCategory {
.name(Text.translatable("skyblocker.config.general.itemInfoDisplay"))
.collapsed(true)
.option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.general.itemInfoDisplay.slotText"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.general.itemInfoDisplay.slotText.@Tooltip")))
+ .binding(defaults.general.itemInfoDisplay.slotText,
+ () -> config.general.itemInfoDisplay.slotText,
+ newValue -> config.general.itemInfoDisplay.slotText = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
.name(Text.translatable("skyblocker.config.general.itemInfoDisplay.attributeShardInfo"))
.description(OptionDescription.of(Text.translatable("skyblocker.config.general.itemInfoDisplay.attributeShardInfo.@Tooltip")))
.binding(defaults.general.itemInfoDisplay.attributeShardInfo,
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 369a9a90..a2a0f815 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java
@@ -3,6 +3,7 @@ package de.hysky.skyblocker.config.categories;
import de.hysky.skyblocker.config.ConfigUtils;
import de.hysky.skyblocker.config.SkyblockerConfig;
import de.hysky.skyblocker.skyblock.fancybars.StatusBarsConfigScreen;
+import de.hysky.skyblocker.skyblock.waypoint.WaypointsScreen;
import de.hysky.skyblocker.utils.render.title.TitleContainerConfigScreen;
import de.hysky.skyblocker.config.configs.UIAndVisualsConfig;
import de.hysky.skyblocker.utils.waypoint.Waypoint;
@@ -226,12 +227,17 @@ public class UIAndVisualsCategory {
.option(Option.<Waypoint.Type>createBuilder()
.name(Text.translatable("skyblocker.config.uiAndVisuals.waypoints.waypointType"))
.description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.waypoints.waypointType.@Tooltip"),
- Text.translatable("skyblocker.config.uiAndVisuals.waypoints.waypointType")))
+ Text.translatable("skyblocker.config.uiAndVisuals.waypoints.waypointType.generalNote")))
.binding(defaults.uiAndVisuals.waypoints.waypointType,
() -> config.uiAndVisuals.waypoints.waypointType,
newValue -> config.uiAndVisuals.waypoints.waypointType = newValue)
.controller(ConfigUtils::createEnumCyclingListController)
.build())
+ .option(ButtonOption.createBuilder()
+ .name(Text.translatable("skyblocker.waypoints.config"))
+ .text(Text.translatable("text.skyblocker.open"))
+ .action((screen, opt) -> MinecraftClient.getInstance().setScreen(new WaypointsScreen(screen)))
+ .build())
.build())
//Teleport Overlays
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/EventNotificationsConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/EventNotificationsConfig.java
new file mode 100644
index 00000000..1fa7016c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/configs/EventNotificationsConfig.java
@@ -0,0 +1,44 @@
+package de.hysky.skyblocker.config.configs;
+
+import dev.isxander.yacl3.config.v2.api.SerialEntry;
+import it.unimi.dsi.fastutil.ints.IntList;
+import net.minecraft.sound.SoundEvent;
+import net.minecraft.sound.SoundEvents;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class EventNotificationsConfig {
+ @SerialEntry
+ public Criterion criterion = Criterion.SKYBLOCK;
+
+ @SerialEntry
+ public Sound reminderSound = Sound.PLING;
+
+ @SerialEntry
+ public Map<String, IntList> eventsReminderTimes = new HashMap<>();
+
+ public enum Criterion {
+ NONE,
+ SKYBLOCK,
+ HYPIXEL,
+ EVERYWHERE
+ }
+
+ public enum Sound {
+ NONE(null),
+ BELL(SoundEvents.BLOCK_BELL_USE),
+ DING(SoundEvents.ENTITY_ARROW_HIT_PLAYER),
+ PLING(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value()),
+ GOAT(SoundEvents.GOAT_HORN_SOUNDS.getFirst().value());
+
+ public SoundEvent getSoundEvent() {
+ return soundEvent;
+ }
+
+ final SoundEvent soundEvent;
+ Sound(SoundEvent soundEvent) {
+ this.soundEvent = soundEvent;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java
index 56e110b6..9f612028 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java
@@ -141,6 +141,9 @@ public class GeneralConfig {
public static class ItemInfoDisplay {
@SerialEntry
+ public boolean slotText = true;
+
+ @SerialEntry
public boolean attributeShardInfo = true;
@SerialEntry
@@ -154,8 +157,8 @@ public class GeneralConfig {
}
public enum RarityBackgroundStyle {
- CIRCULAR(new Identifier(SkyblockerMod.NAMESPACE, "item_rarity_background_circular")),
- SQUARE(new Identifier(SkyblockerMod.NAMESPACE, "item_rarity_background_square"));
+ CIRCULAR(Identifier.of(SkyblockerMod.NAMESPACE, "item_rarity_background_circular")),
+ SQUARE(Identifier.of(SkyblockerMod.NAMESPACE, "item_rarity_background_square"));
public final Identifier tex;
@@ -178,8 +181,8 @@ public class GeneralConfig {
}
public enum SlotLockStyle {
- CLASSIC(new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/slot_lock.png")),
- FANCY(new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/fancy_slot_lock.png"));
+ CLASSIC(Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/slot_lock.png")),
+ FANCY(Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/fancy_slot_lock.png"));
public final Identifier tex;
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/QuickNavigationConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/QuickNavigationConfig.java
index 20a0c9cc..e8e9c27f 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/QuickNavigationConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/QuickNavigationConfig.java
@@ -91,7 +91,7 @@ public class QuickNavigationConfig {
}
public ItemData(String item, int count, String components) {
- this(Registries.ITEM.get(new Identifier(item)), count, components);
+ this(Registries.ITEM.get(Identifier.ofVanilla(item)), count, components);
}
public ItemData(Item item) {
diff --git a/src/main/java/de/hysky/skyblocker/config/datafixer/ConfigDataFixer.java b/src/main/java/de/hysky/skyblocker/config/datafixer/ConfigDataFixer.java
index f4e4aad1..b887d415 100644
--- a/src/main/java/de/hysky/skyblocker/config/datafixer/ConfigDataFixer.java
+++ b/src/main/java/de/hysky/skyblocker/config/datafixer/ConfigDataFixer.java
@@ -69,7 +69,7 @@ public class ConfigDataFixer {
Schema schema3 = builder.addSchema(3, Schema::new);
builder.addFixer(new ConfigFix2QuickNav(schema3, true));
- return builder.buildUnoptimized();
+ return builder.build().fixer();
}
private static JsonObject loadConfig(Path path) {
diff --git a/src/main/java/de/hysky/skyblocker/config/datafixer/ConfigFix2QuickNav.java b/src/main/java/de/hysky/skyblocker/config/datafixer/ConfigFix2QuickNav.java
index bd67b1b0..48dea0a7 100644
--- a/src/main/java/de/hysky/skyblocker/config/datafixer/ConfigFix2QuickNav.java
+++ b/src/main/java/de/hysky/skyblocker/config/datafixer/ConfigFix2QuickNav.java
@@ -33,6 +33,6 @@ public class ConfigFix2QuickNav extends ConfigDataFix {
}
private <T> Dynamic<T> fixButton(Dynamic<T> button) {
- return button.renameAndFixField("item", "itemData", itemData -> itemData.renameAndFixField("id", "item", id -> id.createString(new Identifier(id.asString().getOrThrow()).toString())));
+ return button.renameAndFixField("item", "itemData", itemData -> itemData.renameAndFixField("id", "item", id -> id.createString(Identifier.of(id.asString().getOrThrow()).toString())));
}
}
diff --git a/src/main/java/de/hysky/skyblocker/events/HudRenderEvents.java b/src/main/java/de/hysky/skyblocker/events/HudRenderEvents.java
index 13e70498..f6ee1ff5 100644
--- a/src/main/java/de/hysky/skyblocker/events/HudRenderEvents.java
+++ b/src/main/java/de/hysky/skyblocker/events/HudRenderEvents.java
@@ -3,6 +3,7 @@ package de.hysky.skyblocker.events;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.render.RenderTickCounter;
/**
* HUD render events that allow for proper layering between different HUD elements.
@@ -43,8 +44,8 @@ public class HudRenderEvents {
* Called sometime during a specific HUD render stage.
*
* @param drawContext The {@link DrawContext} instance
- * @param tickDelta Progress for linearly interpolating between the previous and current game state
+ * @param tickCounter The {@link RenderTickCounter} instance
*/
- void onRender(DrawContext context, float tickDelta);
+ void onRender(DrawContext context, RenderTickCounter tickCounter);
}
}
diff --git a/src/main/java/de/hysky/skyblocker/injected/SkyblockerStack.java b/src/main/java/de/hysky/skyblocker/injected/SkyblockerStack.java
new file mode 100644
index 00000000..2f54917b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/injected/SkyblockerStack.java
@@ -0,0 +1,20 @@
+package de.hysky.skyblocker.injected;
+
+import org.jetbrains.annotations.Nullable;
+
+public interface SkyblockerStack {
+ @Nullable
+ default String getSkyblockId() {
+ return "";
+ }
+
+ @Nullable
+ default String getSkyblockApiId() {
+ return "";
+ }
+
+ @Nullable
+ default String getNeuName() {
+ return "";
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/ComponentHolderMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ComponentHolderMixin.java
index 8fa03cdc..199f5c26 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/ComponentHolderMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/ComponentHolderMixin.java
@@ -11,7 +11,7 @@ import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.component.ComponentHolder;
-import net.minecraft.component.DataComponentType;
+import net.minecraft.component.ComponentType;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.item.ItemStack;
import net.minecraft.item.trim.ArmorTrim;
@@ -21,7 +21,7 @@ public interface ComponentHolderMixin {
@SuppressWarnings("unchecked")
@ModifyReturnValue(method = "get", at = @At("RETURN"))
- private <T> T skyblocker$customArmorTrims(T original, DataComponentType<? extends T> dataComponentType) {
+ private <T> T skyblocker$customArmorTrims(T original, ComponentType<? extends T> dataComponentType) {
if (Utils.isOnSkyblock() && ((Object) this) instanceof ItemStack stack) {
if (dataComponentType == DataComponentTypes.TRIM) {
Object2ObjectOpenHashMap<String, CustomArmorTrims.ArmorTrimId> customTrims = SkyblockerConfigManager.get().general.customArmorTrims;
diff --git a/src/main/java/de/hysky/skyblocker/mixins/DrawContextMixin.java b/src/main/java/de/hysky/skyblocker/mixins/DrawContextMixin.java
index 7964b114..9f09c37a 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/DrawContextMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/DrawContextMixin.java
@@ -2,67 +2,15 @@ package de.hysky.skyblocker.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.sugar.Local;
-import com.llamalad7.mixinextras.sugar.ref.LocalRef;
-import de.hysky.skyblocker.config.SkyblockerConfigManager;
-import de.hysky.skyblocker.skyblock.item.AttributeShards;
import de.hysky.skyblocker.skyblock.item.ItemCooldowns;
-import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
-import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
-import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NbtCompound;
-import net.minecraft.util.Formatting;
-import org.jetbrains.annotations.Nullable;
-import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(DrawContext.class)
public abstract class DrawContextMixin {
- @Shadow
- @Final
- private MatrixStack matrices;
-
- @Shadow
- public abstract int drawText(TextRenderer textRenderer, @Nullable String text, int x, int y, int color, boolean shadow);
-
- @Inject(method = "drawItemInSlot(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V", at = @At("HEAD"))
- private void skyblocker$renderAttributeShardDisplay(CallbackInfo ci, @Local(argsOnly = true) TextRenderer textRenderer, @Local(argsOnly = true) ItemStack stack, @Local(argsOnly = true, ordinal = 0) int x, @Local(argsOnly = true, ordinal = 1) int y, @Local(argsOnly = true) LocalRef<String> countOverride) {
- if (!SkyblockerConfigManager.get().general.itemInfoDisplay.attributeShardInfo) return;
-
- if (Utils.isOnSkyblock()) {
- NbtCompound customData = ItemUtils.getCustomData(stack);
-
- if (ItemUtils.getItemId(stack).equals("ATTRIBUTE_SHARD")) {
- NbtCompound attributesTag = customData.getCompound("attributes");
- String[] attributes = attributesTag.getKeys().toArray(String[]::new);
-
- if (attributes.length != 0) {
- String attributeId = attributes[0];
- int attributeLevel = attributesTag.getInt(attributeId);
-
- //Set item count
- countOverride.set(Integer.toString(attributeLevel));
-
- //Draw the attribute name
- this.matrices.push();
- this.matrices.translate(0f, 0f, 200f);
-
- String attributeInitials = AttributeShards.getShortName(attributeId);
-
- this.drawText(textRenderer, attributeInitials, x, y, Formatting.AQUA.getColorValue(), true);
-
- this.matrices.pop();
- }
- }
- }
- }
-
@ModifyExpressionValue(method = "drawItemInSlot(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/ItemCooldownManager;getCooldownProgress(Lnet/minecraft/item/Item;F)F"))
private float skyblocker$modifyItemCooldown(float cooldownProgress, @Local(argsOnly = true) ItemStack stack) {
diff --git a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java
index f662be7c..f2e3e907 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java
@@ -4,6 +4,7 @@ import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.blaze3d.systems.RenderSystem;
import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.PetCache;
import de.hysky.skyblocker.skyblock.experiment.ChronomatronSolver;
import de.hysky.skyblocker.skyblock.experiment.ExperimentSolver;
import de.hysky.skyblocker.skyblock.experiment.SuperpairsSolver;
@@ -11,7 +12,10 @@ import de.hysky.skyblocker.skyblock.experiment.UltrasequencerSolver;
import de.hysky.skyblocker.skyblock.garden.VisitorHelper;
import de.hysky.skyblocker.skyblock.item.ItemProtection;
import de.hysky.skyblocker.skyblock.item.ItemRarityBackgrounds;
+import de.hysky.skyblocker.skyblock.item.MuseumItemCache;
import de.hysky.skyblocker.skyblock.item.WikiLookup;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextManager;
import de.hysky.skyblocker.skyblock.item.tooltip.BackpackPreview;
import de.hysky.skyblocker.skyblock.item.tooltip.CompactorDeletorPreview;
import de.hysky.skyblocker.skyblock.quicknav.QuickNav;
@@ -22,6 +26,7 @@ import de.hysky.skyblocker.utils.render.gui.ContainerSolver;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.inventory.SimpleInventory;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
@@ -33,6 +38,7 @@ import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import org.lwjgl.glfw.GLFW;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@@ -50,251 +56,299 @@ import java.util.regex.Matcher;
@Mixin(HandledScreen.class)
public abstract class HandledScreenMixin<T extends ScreenHandler> extends Screen {
- /**
- * This is the slot id returned for when a click is outside the screen's bounds
- */
- @Unique
- private static final int OUT_OF_BOUNDS_SLOT = -999;
-
- @Unique
- private static final Identifier ITEM_PROTECTION = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/item_protection.png");
-
- @Unique
- private static final Set<String> FILLER_ITEMS = Set.of(
- " ", // Empty menu item
- "Locked Page",
- "Quick Crafting Slot",
- "Locked Backpack Slot 2", //Regular expressions won't be utilized here since the search by contains is based on plain text rather than regex syntax
- "Locked Backpack Slot 3",
- "Locked Backpack Slot 4",
- "Locked Backpack Slot 5",
- "Locked Backpack Slot 6",
- "Locked Backpack Slot 7",
- "Locked Backpack Slot 8",
- "Locked Backpack Slot 9",
- "Locked Backpack Slot 10",
- "Locked Backpack Slot 11",
- "Locked Backpack Slot 12",
- "Locked Backpack Slot 13",
- "Locked Backpack Slot 14",
- "Locked Backpack Slot 15",
- "Locked Backpack Slot 16",
- "Locked Backpack Slot 17",
- "Locked Backpack Slot 18",
- "Preparing"
- );
-
- @Shadow
- @Nullable
- protected Slot focusedSlot;
-
- @Shadow
- @Final
- protected T handler;
-
- @Unique
- private List<QuickNavButton> quickNavButtons;
-
- protected HandledScreenMixin(Text title) {
- super(title);
- }
-
- @Inject(method = "init", at = @At("RETURN"))
- private void skyblocker$initQuickNav(CallbackInfo ci) {
- if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().quickNav.enableQuickNav && client != null && client.player != null && !client.player.isCreative()) {
- for (QuickNavButton quickNavButton : quickNavButtons = QuickNav.init(getTitle().getString().trim())) {
- addSelectableChild(quickNavButton);
- }
- }
- }
-
- @Inject(at = @At("HEAD"), method = "keyPressed")
- public void skyblocker$keyPressed(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable<Boolean> cir) {
- if (this.client != null && this.focusedSlot != null && keyCode != 256) {
- //wiki lookup
- if (!this.client.options.inventoryKey.matchesKey(keyCode, scanCode) && WikiLookup.wikiLookup.matchesKey(keyCode, scanCode) && client.player != null) {
- WikiLookup.openWiki(this.focusedSlot, client.player);
- }
- //item protection
- if (!this.client.options.inventoryKey.matchesKey(keyCode, scanCode) && ItemProtection.itemProtection.matchesKey(keyCode, scanCode)) {
- ItemProtection.handleKeyPressed(this.focusedSlot.getStack());
- }
- }
- }
-
- @Inject(at = @At("HEAD"), method = "mouseClicked")
- public void skyblocker$mouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> cir) {
- if (SkyblockerConfigManager.get().farming.garden.visitorHelper && (Utils.getLocationRaw().equals("garden") && !getTitle().getString().contains("Logbook") || getTitle().getString().startsWith("Bazaar"))) {
- VisitorHelper.onMouseClicked(mouseX, mouseY, button, this.textRenderer);
- }
- }
-
- /**
- * Draws the unselected tabs in front of the background blur, but behind the main inventory, similar to creative inventory tabs
- */
- @Inject(method = "renderBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;drawBackground(Lnet/minecraft/client/gui/DrawContext;FII)V"))
- private void skyblocker$drawUnselectedQuickNavButtons(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
- if (quickNavButtons != null) for (QuickNavButton quickNavButton : quickNavButtons) {
- if (!quickNavButton.toggled()) {
- quickNavButton.render(context, mouseX, mouseY, delta);
- }
- }
- }
-
- /**
- * Draws the selected tab in front of the background blur and the main inventory, similar to creative inventory tabs
- */
- @Inject(method = "renderBackground", at = @At("RETURN"))
- private void skyblocker$drawSelectedQuickNavButtons(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
- if (quickNavButtons != null) for (QuickNavButton quickNavButton : quickNavButtons) {
- if (quickNavButton.toggled()) {
- quickNavButton.render(context, mouseX, mouseY, delta);
- }
- }
- }
-
- @SuppressWarnings("DataFlowIssue")
- // makes intellij be quiet about this.focusedSlot maybe being null. It's already null checked in mixined method.
- @Inject(method = "drawMouseoverTooltip", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTooltip(Lnet/minecraft/client/font/TextRenderer;Ljava/util/List;Ljava/util/Optional;II)V"), cancellable = true)
- public void skyblocker$drawMouseOverTooltip(DrawContext context, int x, int y, CallbackInfo ci, @Local(ordinal = 0) ItemStack stack) {
- if (!Utils.isOnSkyblock()) return;
-
- // Hide Empty Tooltips
- if (SkyblockerConfigManager.get().uiAndVisuals.hideEmptyTooltips && stack.getName().getString().equals(" ")) {
- ci.cancel();
- }
-
- // Backpack Preview
- boolean shiftDown = SkyblockerConfigManager.get().uiAndVisuals.backpackPreviewWithoutShift ^ Screen.hasShiftDown();
- if (shiftDown && getTitle().getString().equals("Storage") && focusedSlot.inventory != client.player.getInventory() && BackpackPreview.renderPreview(context, this, focusedSlot.getIndex(), x, y)) {
- ci.cancel();
- }
-
- // Compactor Preview
- if (SkyblockerConfigManager.get().uiAndVisuals.compactorDeletorPreview) {
- Matcher matcher = CompactorDeletorPreview.NAME.matcher(ItemUtils.getItemId(stack));
- if (matcher.matches() && CompactorDeletorPreview.drawPreview(context, stack, matcher.group("type"), matcher.group("size"), x, y)) {
- ci.cancel();
- }
- }
- }
-
- @ModifyVariable(method = "drawMouseoverTooltip", at = @At(value = "LOAD", ordinal = 0))
- private ItemStack skyblocker$experimentSolvers$replaceTooltipDisplayStack(ItemStack stack) {
- return skyblocker$experimentSolvers$getStack(focusedSlot, stack);
- }
-
- @ModifyVariable(method = "drawSlot", at = @At(value = "LOAD", ordinal = 3), ordinal = 0)
- private ItemStack skyblocker$experimentSolvers$replaceDisplayStack(ItemStack stack, DrawContext context, Slot slot) {
- return skyblocker$experimentSolvers$getStack(slot, stack);
- }
-
- /**
- * Redirects getStack calls to account for different stacks in experiment solvers.
- */
- @Unique
- private ItemStack skyblocker$experimentSolvers$getStack(Slot slot, @NotNull ItemStack stack) {
- ContainerSolver currentSolver = SkyblockerMod.getInstance().containerSolverManager.getCurrentSolver();
- if ((currentSolver instanceof SuperpairsSolver || currentSolver instanceof UltrasequencerSolver) && ((ExperimentSolver) currentSolver).getState() == ExperimentSolver.State.SHOW && slot.inventory instanceof SimpleInventory) {
- ItemStack itemStack = ((ExperimentSolver) currentSolver).getSlots().get(slot.getIndex());
- return itemStack == null ? stack : itemStack;
- }
- return stack;
- }
-
- /**
- * The naming of this method in yarn is half true, its mostly to handle slot/item interactions (which are mouse or keyboard clicks)
- * For example, using the drop key bind while hovering over an item will invoke this method to drop the players item
- *
- * @implNote This runs before {@link ScreenHandler#onSlotClick(int, int, SlotActionType, net.minecraft.entity.player.PlayerEntity)}
- */
- @Inject(method = "onMouseClick(Lnet/minecraft/screen/slot/Slot;IILnet/minecraft/screen/slot/SlotActionType;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;clickSlot(IIILnet/minecraft/screen/slot/SlotActionType;Lnet/minecraft/entity/player/PlayerEntity;)V"), cancellable = true)
- private void skyblocker$onSlotClick(Slot slot, int slotId, int button, SlotActionType actionType, CallbackInfo ci) {
- if (!Utils.isOnSkyblock()) return;
-
- // Item Protection
- // When you try and drop the item by picking it up then clicking outside the screen
- if (slotId == OUT_OF_BOUNDS_SLOT && ItemProtection.isItemProtected(this.handler.getCursorStack())) {
- ci.cancel();
- return;
- }
-
- if (slot == null) return;
- String title = getTitle().getString();
- ItemStack stack = skyblocker$experimentSolvers$getStack(slot, slot.getStack());
- ContainerSolver currentSolver = SkyblockerMod.getInstance().containerSolverManager.getCurrentSolver();
-
- // Prevent clicks on filler items
- if (SkyblockerConfigManager.get().uiAndVisuals.hideEmptyTooltips && FILLER_ITEMS.contains(stack.getName().getString()) &&
- // Allow clicks in Ultrasequencer and Superpairs
- (!UltrasequencerSolver.INSTANCE.getName().matcher(title).matches() || SkyblockerConfigManager.get().helpers.experiments.enableUltrasequencerSolver)) {
- ci.cancel();
- return;
- }
- // Item Protection
- // When you click your drop key while hovering over an item
- if (actionType == SlotActionType.THROW && ItemProtection.isItemProtected(stack)) {
- ci.cancel();
- return;
- }
- // Prevent salvaging
- if (title.equals("Salvage Items") && ItemProtection.isItemProtected(stack)) {
- ci.cancel();
- return;
- }
- if (this.handler instanceof GenericContainerScreenHandler genericContainerScreenHandler && genericContainerScreenHandler.getRows() == 6) {
- VisitorHelper.onSlotClick(slot, slotId, title, genericContainerScreenHandler.getSlot(13).getStack());
-
- // Prevent selling to NPC shops
- ItemStack sellStack = this.handler.slots.get(49).getStack();
- if (sellStack.getName().getString().equals("Sell Item") || ItemUtils.getLoreLineIf(sellStack, text -> text.contains("buyback")) != null) {
- if (slotId != 49 && ItemProtection.isItemProtected(stack)) {
- ci.cancel();
- return;
- }
- }
- }
-
- if (currentSolver != null) {
- boolean disallowed = SkyblockerMod.getInstance().containerSolverManager.onSlotClick(slotId, stack);
-
- if (disallowed) ci.cancel();
- }
-
- // Experiment Solvers
- if (currentSolver instanceof ExperimentSolver experimentSolver && experimentSolver.getState() == ExperimentSolver.State.SHOW && slot.inventory instanceof SimpleInventory) {
- switch (experimentSolver) {
- case ChronomatronSolver chronomatronSolver -> {
- Item item = chronomatronSolver.getChronomatronSlots().get(chronomatronSolver.getChronomatronCurrentOrdinal());
- if ((stack.isOf(item) || ChronomatronSolver.TERRACOTTA_TO_GLASS.get(stack.getItem()) == item) && chronomatronSolver.incrementChronomatronCurrentOrdinal() >= chronomatronSolver.getChronomatronSlots().size()) {
- chronomatronSolver.setState(ExperimentSolver.State.END);
- }
- }
-
- case SuperpairsSolver superpairsSolver -> {
- superpairsSolver.setSuperpairsPrevClickedSlot(slot.getIndex());
- superpairsSolver.setSuperpairsCurrentSlot(ItemStack.EMPTY);
- }
-
- case UltrasequencerSolver ultrasequencerSolver when slot.getIndex() == ultrasequencerSolver.getUltrasequencerNextSlot() -> {
- int count = ultrasequencerSolver.getSlots().get(ultrasequencerSolver.getUltrasequencerNextSlot()).getCount() + 1;
- ultrasequencerSolver.getSlots().entrySet().stream().filter(entry -> entry.getValue().getCount() == count).findAny().map(Map.Entry::getKey).ifPresentOrElse(ultrasequencerSolver::setUltrasequencerNextSlot, () -> ultrasequencerSolver.setState(ExperimentSolver.State.END));
- }
-
- default -> { /*Do Nothing*/ }
- }
- }
- }
-
- @Inject(method = "drawSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawItem(Lnet/minecraft/item/ItemStack;III)V"))
- private void skyblocker$drawItemRarityBackground(DrawContext context, Slot slot, CallbackInfo ci) {
- if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().general.itemInfoDisplay.itemRarityBackgrounds)
- ItemRarityBackgrounds.tryDraw(slot.getStack(), context, slot.x, slot.y);
- // Item protection
- if (ItemProtection.isItemProtected(slot.getStack())) {
- RenderSystem.enableBlend();
- context.drawTexture(ITEM_PROTECTION, slot.x, slot.y, 0, 0, 16, 16, 16, 16);
- RenderSystem.disableBlend();
- }
- }
+ /**
+ * This is the slot id returned for when a click is outside the screen's bounds
+ */
+ @Unique
+ private static final int OUT_OF_BOUNDS_SLOT = -999;
+
+ @Unique
+ private static final Identifier ITEM_PROTECTION = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/item_protection.png");
+
+ @Unique
+ private static final Set<String> FILLER_ITEMS = Set.of(
+ " ", // Empty menu item
+ "Locked Page",
+ "Quick Crafting Slot",
+ "Locked Backpack Slot 2", //Regular expressions won't be utilized here since the search by contains is based on plain text rather than regex syntax
+ "Locked Backpack Slot 3",
+ "Locked Backpack Slot 4",
+ "Locked Backpack Slot 5",
+ "Locked Backpack Slot 6",
+ "Locked Backpack Slot 7",
+ "Locked Backpack Slot 8",
+ "Locked Backpack Slot 9",
+ "Locked Backpack Slot 10",
+ "Locked Backpack Slot 11",
+ "Locked Backpack Slot 12",
+ "Locked Backpack Slot 13",
+ "Locked Backpack Slot 14",
+ "Locked Backpack Slot 15",
+ "Locked Backpack Slot 16",
+ "Locked Backpack Slot 17",
+ "Locked Backpack Slot 18",
+ "Preparing"
+ );
+
+ @Shadow
+ @Nullable
+ protected Slot focusedSlot;
+
+ @Shadow
+ @Final
+ protected T handler;
+
+ @Shadow
+ protected abstract List<Text> getTooltipFromItem(ItemStack stack);
+
+ @Unique
+ private List<QuickNavButton> quickNavButtons;
+
+ protected HandledScreenMixin(Text title) {
+ super(title);
+ }
+
+ @Inject(method = "init", at = @At("RETURN"))
+ private void skyblocker$initQuickNav(CallbackInfo ci) {
+ if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().quickNav.enableQuickNav && client != null && client.player != null && !client.player.isCreative()) {
+ for (QuickNavButton quickNavButton : quickNavButtons = QuickNav.init(getTitle().getString().trim())) {
+ addSelectableChild(quickNavButton);
+ }
+ }
+ }
+
+ @Inject(at = @At("HEAD"), method = "keyPressed")
+ public void skyblocker$keyPressed(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable<Boolean> cir) {
+ if (this.client != null && this.focusedSlot != null && keyCode != 256) {
+ //wiki lookup
+ if (!this.client.options.inventoryKey.matchesKey(keyCode, scanCode) && WikiLookup.wikiLookup.matchesKey(keyCode, scanCode) && client.player != null) {
+ WikiLookup.openWiki(this.focusedSlot, client.player);
+ }
+ //item protection
+ if (!this.client.options.inventoryKey.matchesKey(keyCode, scanCode) && ItemProtection.itemProtection.matchesKey(keyCode, scanCode)) {
+ ItemProtection.handleKeyPressed(this.focusedSlot.getStack());
+ }
+ }
+ }
+
+ @Inject(at = @At("HEAD"), method = "mouseClicked")
+ public void skyblocker$mouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> cir) {
+ if (SkyblockerConfigManager.get().farming.garden.visitorHelper && (Utils.getLocationRaw().equals("garden") && !getTitle().getString().contains("Logbook") || getTitle().getString().startsWith("Bazaar"))) {
+ VisitorHelper.onMouseClicked(mouseX, mouseY, button, this.textRenderer);
+ }
+ }
+
+ /**
+ * Draws the unselected tabs in front of the background blur, but behind the main inventory, similar to creative inventory tabs
+ */
+ @Inject(method = "renderBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;drawBackground(Lnet/minecraft/client/gui/DrawContext;FII)V"))
+ private void skyblocker$drawUnselectedQuickNavButtons(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
+ if (quickNavButtons != null) for (QuickNavButton quickNavButton : quickNavButtons) {
+ if (!quickNavButton.toggled()) {
+ quickNavButton.render(context, mouseX, mouseY, delta);
+ }
+ }
+ }
+
+ /**
+ * Draws the selected tab in front of the background blur and the main inventory, similar to creative inventory tabs
+ */
+ @Inject(method = "renderBackground", at = @At("RETURN"))
+ private void skyblocker$drawSelectedQuickNavButtons(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
+ if (quickNavButtons != null) for (QuickNavButton quickNavButton : quickNavButtons) {
+ if (quickNavButton.toggled()) {
+ quickNavButton.render(context, mouseX, mouseY, delta);
+ }
+ }
+ }
+
+ @SuppressWarnings("DataFlowIssue")
+ // makes intellij be quiet about this.focusedSlot maybe being null. It's already null checked in mixined method.
+ @Inject(method = "drawMouseoverTooltip", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTooltip(Lnet/minecraft/client/font/TextRenderer;Ljava/util/List;Ljava/util/Optional;II)V"), cancellable = true)
+ public void skyblocker$drawMouseOverTooltip(DrawContext context, int x, int y, CallbackInfo ci, @Local(ordinal = 0) ItemStack stack) {
+ if (!Utils.isOnSkyblock()) return;
+
+ // Hide Empty Tooltips
+ if (SkyblockerConfigManager.get().uiAndVisuals.hideEmptyTooltips && stack.getName().getString().equals(" ")) {
+ ci.cancel();
+ }
+
+ // Backpack Preview
+ boolean shiftDown = SkyblockerConfigManager.get().uiAndVisuals.backpackPreviewWithoutShift ^ Screen.hasShiftDown();
+ if (shiftDown && getTitle().getString().equals("Storage") && focusedSlot.inventory != client.player.getInventory() && BackpackPreview.renderPreview(context, this, focusedSlot.getIndex(), x, y)) {
+ ci.cancel();
+ }
+
+ // Compactor Preview
+ if (SkyblockerConfigManager.get().uiAndVisuals.compactorDeletorPreview) {
+ Matcher matcher = CompactorDeletorPreview.NAME.matcher(ItemUtils.getItemId(stack));
+ if (matcher.matches() && CompactorDeletorPreview.drawPreview(context, stack, getTooltipFromItem(stack), matcher.group("type"), matcher.group("size"), x, y)) {
+ ci.cancel();
+ }
+ }
+ }
+
+ @ModifyVariable(method = "drawMouseoverTooltip", at = @At(value = "LOAD", ordinal = 0))
+ private ItemStack skyblocker$experimentSolvers$replaceTooltipDisplayStack(ItemStack stack) {
+ return skyblocker$experimentSolvers$getStack(focusedSlot, stack);
+ }
+
+ @ModifyVariable(method = "drawSlot", at = @At(value = "LOAD", ordinal = 3), ordinal = 0)
+ private ItemStack skyblocker$experimentSolvers$replaceDisplayStack(ItemStack stack, DrawContext context, Slot slot) {
+ return skyblocker$experimentSolvers$getStack(slot, stack);
+ }
+
+ /**
+ * Redirects getStack calls to account for different stacks in experiment solvers.
+ */
+ @Unique
+ private ItemStack skyblocker$experimentSolvers$getStack(Slot slot, @NotNull ItemStack stack) {
+ ContainerSolver currentSolver = SkyblockerMod.getInstance().containerSolverManager.getCurrentSolver();
+ if ((currentSolver instanceof SuperpairsSolver || currentSolver instanceof UltrasequencerSolver) && ((ExperimentSolver) currentSolver).getState() == ExperimentSolver.State.SHOW && slot.inventory instanceof SimpleInventory) {
+ ItemStack itemStack = ((ExperimentSolver) currentSolver).getSlots().get(slot.getIndex());
+ return itemStack == null ? stack : itemStack;
+ }
+ return stack;
+ }
+
+ /**
+ * The naming of this method in yarn is half true, its mostly to handle slot/item interactions (which are mouse or keyboard clicks)
+ * For example, using the drop key bind while hovering over an item will invoke this method to drop the players item
+ *
+ * @implNote This runs before {@link ScreenHandler#onSlotClick(int, int, SlotActionType, net.minecraft.entity.player.PlayerEntity)}
+ */
+ @Inject(method = "onMouseClick(Lnet/minecraft/screen/slot/Slot;IILnet/minecraft/screen/slot/SlotActionType;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;clickSlot(IIILnet/minecraft/screen/slot/SlotActionType;Lnet/minecraft/entity/player/PlayerEntity;)V"), cancellable = true)
+ private void skyblocker$onSlotClick(Slot slot, int slotId, int button, SlotActionType actionType, CallbackInfo ci) {
+ if (!Utils.isOnSkyblock()) return;
+
+ // Item Protection
+ // When you try and drop the item by picking it up then clicking outside the screen
+ if (slotId == OUT_OF_BOUNDS_SLOT && ItemProtection.isItemProtected(this.handler.getCursorStack())) {
+ ci.cancel();
+ return;
+ }
+
+ if (slot == null) return;
+ String title = getTitle().getString();
+ ItemStack stack = skyblocker$experimentSolvers$getStack(slot, slot.getStack());
+ ContainerSolver currentSolver = SkyblockerMod.getInstance().containerSolverManager.getCurrentSolver();
+
+ // Prevent clicks on filler items
+ if (SkyblockerConfigManager.get().uiAndVisuals.hideEmptyTooltips && FILLER_ITEMS.contains(stack.getName().getString()) &&
+ // Allow clicks in Ultrasequencer and Superpairs
+ (!UltrasequencerSolver.INSTANCE.getName().matcher(title).matches() || SkyblockerConfigManager.get().helpers.experiments.enableUltrasequencerSolver)) {
+ ci.cancel();
+ return;
+ }
+ // Item Protection
+ // When you click your drop key while hovering over an item
+ if (actionType == SlotActionType.THROW && ItemProtection.isItemProtected(stack)) {
+ ci.cancel();
+ return;
+ }
+ // Prevent salvaging
+ if (title.equals("Salvage Items") && ItemProtection.isItemProtected(stack)) {
+ ci.cancel();
+ return;
+ }
+
+ switch (this.handler) {
+ case GenericContainerScreenHandler genericContainerScreenHandler when genericContainerScreenHandler.getRows() == 6 -> {
+ VisitorHelper.onSlotClick(slot, slotId, title, genericContainerScreenHandler.getSlot(13).getStack());
+
+ // Prevent selling to NPC shops
+ ItemStack sellStack = this.handler.slots.get(49).getStack();
+ if (sellStack.getName().getString().equals("Sell Item") || ItemUtils.getLoreLineIf(sellStack, text -> text.contains("buyback")) != null) {
+ if (slotId != 49 && ItemProtection.isItemProtected(stack)) {
+ ci.cancel();
+ return;
+ }
+ }
+ }
+
+ case GenericContainerScreenHandler genericContainerScreenHandler when title.equals(MuseumItemCache.DONATION_CONFIRMATION_SCREEN_TITLE) -> {
+ //Museum Item Cache donation tracking
+ MuseumItemCache.handleClick(slot, slotId, genericContainerScreenHandler.slots);
+ }
+
+ case null, default -> {}
+ }
+
+ //Pet Caching
+ if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT && title.startsWith("Pets")) {
+ PetCache.handlePetEquip(slot, slotId);
+ }
+
+ if (currentSolver != null) {
+ boolean disallowed = SkyblockerMod.getInstance().containerSolverManager.onSlotClick(slotId, stack);
+
+ if (disallowed) ci.cancel();
+ }
+
+ // Experiment Solvers
+ if (currentSolver instanceof ExperimentSolver experimentSolver && experimentSolver.getState() == ExperimentSolver.State.SHOW && slot.inventory instanceof SimpleInventory) {
+ switch (experimentSolver) {
+ case ChronomatronSolver chronomatronSolver -> {
+ Item item = chronomatronSolver.getChronomatronSlots().get(chronomatronSolver.getChronomatronCurrentOrdinal());
+ if ((stack.isOf(item) || ChronomatronSolver.TERRACOTTA_TO_GLASS.get(stack.getItem()) == item) && chronomatronSolver.incrementChronomatronCurrentOrdinal() >= chronomatronSolver.getChronomatronSlots().size()) {
+ chronomatronSolver.setState(ExperimentSolver.State.END);
+ }
+ }
+
+ case SuperpairsSolver superpairsSolver -> {
+ superpairsSolver.setSuperpairsPrevClickedSlot(slot.getIndex());
+ superpairsSolver.setSuperpairsCurrentSlot(ItemStack.EMPTY);
+ }
+
+ case UltrasequencerSolver ultrasequencerSolver when slot.getIndex() == ultrasequencerSolver.getUltrasequencerNextSlot() -> {
+ int count = ultrasequencerSolver.getSlots().get(ultrasequencerSolver.getUltrasequencerNextSlot()).getCount() + 1;
+ ultrasequencerSolver.getSlots().entrySet().stream().filter(entry -> entry.getValue().getCount() == count).findAny().map(Map.Entry::getKey).ifPresentOrElse(ultrasequencerSolver::setUltrasequencerNextSlot, () -> ultrasequencerSolver.setState(ExperimentSolver.State.END));
+ }
+
+ default -> { /*Do Nothing*/ }
+ }
+ }
+ }
+
+ @Inject(method = "drawSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawItem(Lnet/minecraft/item/ItemStack;III)V"))
+ private void skyblocker$drawItemRarityBackground(DrawContext context, Slot slot, CallbackInfo ci) {
+ if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().general.itemInfoDisplay.itemRarityBackgrounds)
+ ItemRarityBackgrounds.tryDraw(slot.getStack(), context, slot.x, slot.y);
+ // Item protection
+ if (ItemProtection.isItemProtected(slot.getStack())) {
+ RenderSystem.enableBlend();
+ context.drawTexture(ITEM_PROTECTION, slot.x, slot.y, 0, 0, 16, 16, 16, 16);
+ RenderSystem.disableBlend();
+ }
+ }
+
+ @Inject(method = "drawSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawItemInSlot(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V"))
+ private void skyblocker$drawSlotText(DrawContext context, Slot slot, CallbackInfo ci) {
+ List<SlotText> textList = SlotTextManager.getText(slot);
+ if (textList.isEmpty()) return;
+ MatrixStack matrices = context.getMatrices();
+
+ for (SlotText slotText : textList) {
+ matrices.push();
+ matrices.translate(0.0f, 0.0f, 200.0f);
+ int length = textRenderer.getWidth(slotText.text());
+ if (length > 16) {
+ matrices.scale(16.0f / length, 16.0f / length, 1.0f); //Make them fit in the slot. FYI, a slot is sized 16×16.
+ final float x = (slot.x * length / 16.0f) - slot.x; //Save in a variable to not recalculate
+ switch (slotText.position()) {
+ case TOP_LEFT, TOP_RIGHT -> matrices.translate(x, (slot.y * length / 16.0f) - slot.y, 0.0f);
+ case BOTTOM_LEFT, BOTTOM_RIGHT -> matrices.translate(x, ((slot.y + 16f - textRenderer.fontHeight + 2f + 0.7f) * length / 16.0f) - slot.y, 0.0f);
+ }
+ } else {
+ switch (slotText.position()) {
+ case TOP_LEFT -> { /*Do Nothing*/ }
+ case TOP_RIGHT -> matrices.translate(16f - length, 0.0f, 0.0f);
+ case BOTTOM_LEFT -> matrices.translate(0.0f, 16f - textRenderer.fontHeight + 2f, 0.0f);
+ case BOTTOM_RIGHT -> matrices.translate(16f - length, 16f - textRenderer.fontHeight + 2f, 0.0f);
+ }
+ }
+ context.drawText(textRenderer, slotText.text(), slot.x, slot.y, 0xFFFFFF, true);
+ matrices.pop();
+ }
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/InGameHudMixin.java b/src/main/java/de/hysky/skyblocker/mixins/InGameHudMixin.java
index 7f4721a5..fffd74f9 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/InGameHudMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/InGameHudMixin.java
@@ -43,7 +43,7 @@ public abstract class InGameHudMixin {
private static final Supplier<Identifier> SLOT_LOCK_ICON = () -> SkyblockerConfigManager.get().general.itemProtection.slotLockStyle.tex;
@Unique
- private static final Identifier ITEM_PROTECTION = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/item_protection.png");
+ private static final Identifier ITEM_PROTECTION = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/item_protection.png");
@Unique
private static final Pattern DICER_TITLE_BLACKLIST = Pattern.compile(".+? DROP!");
@@ -58,7 +58,7 @@ public abstract class InGameHudMixin {
@Final
private LayeredDrawer layeredDrawer;
- @Inject(method = "renderHotbar", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/InGameHud;renderHotbarItem(Lnet/minecraft/client/gui/DrawContext;IIFLnet/minecraft/entity/player/PlayerEntity;Lnet/minecraft/item/ItemStack;I)V", ordinal = 0))
+ @Inject(method = "renderHotbar", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/InGameHud;renderHotbarItem(Lnet/minecraft/client/gui/DrawContext;IILnet/minecraft/client/render/RenderTickCounter;Lnet/minecraft/entity/player/PlayerEntity;Lnet/minecraft/item/ItemStack;I)V", ordinal = 0))
public void skyblocker$renderHotbarItemLockOrRarityBg(CallbackInfo ci, @Local(argsOnly = true) DrawContext context, @Local(ordinal = 4, name = "m") int index, @Local(ordinal = 5, name = "n") int x, @Local(ordinal = 6, name = "o") int y, @Local PlayerEntity player) {
if (Utils.isOnSkyblock()) {
// slot lock
@@ -141,17 +141,17 @@ public abstract class InGameHudMixin {
@ModifyArg(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/LayeredDrawer;addLayer(Lnet/minecraft/client/gui/LayeredDrawer$Layer;)Lnet/minecraft/client/gui/LayeredDrawer;", ordinal = 2))
private LayeredDrawer.Layer skyblocker$afterMainHud(LayeredDrawer.Layer mainHudLayer) {
- return (context, tickDelta) -> {
- mainHudLayer.render(context, tickDelta);
- HudRenderEvents.AFTER_MAIN_HUD.invoker().onRender(context, tickDelta);
+ return (context, tickCounter) -> {
+ mainHudLayer.render(context, tickCounter);
+ HudRenderEvents.AFTER_MAIN_HUD.invoker().onRender(context, tickCounter);
};
}
@ModifyArg(method = "<init>", slice = @Slice(from = @At(value = "NEW", target = "Lnet/minecraft/client/gui/LayeredDrawer;", ordinal = 1)), at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/LayeredDrawer;addLayer(Lnet/minecraft/client/gui/LayeredDrawer$Layer;)Lnet/minecraft/client/gui/LayeredDrawer;", ordinal = 5))
private LayeredDrawer.Layer skyblocker$beforeChat(LayeredDrawer.Layer beforeChatLayer) {
- return (context, tickDelta) -> {
- HudRenderEvents.BEFORE_CHAT.invoker().onRender(context, tickDelta);
- beforeChatLayer.render(context, tickDelta);
+ return (context, tickCounter) -> {
+ HudRenderEvents.BEFORE_CHAT.invoker().onRender(context, tickCounter);
+ beforeChatLayer.render(context, tickCounter);
};
}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java
index 878a93ac..1493cf26 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java
@@ -1,14 +1,26 @@
package de.hysky.skyblocker.mixins;
+import com.google.gson.JsonParser;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
+import com.mojang.serialization.JsonOps;
+
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.injected.SkyblockerStack;
+import de.hysky.skyblocker.skyblock.PetCache.PetInfo;
+import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.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;
-import net.minecraft.item.TooltipAppender;
+import net.minecraft.item.tooltip.TooltipAppender;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtElement;
import net.minecraft.text.Text;
+import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
@@ -17,8 +29,11 @@ import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import java.util.Locale;
+import java.util.Optional;
+
@Mixin(ItemStack.class)
-public abstract class ItemStackMixin {
+public abstract class ItemStackMixin implements ComponentHolder, SkyblockerStack {
@Shadow
public abstract int getDamage();
@@ -29,10 +44,19 @@ public abstract class ItemStackMixin {
@Unique
private int maxDamage;
+ @Unique
+ private String skyblockId;
+
+ @Unique
+ private String skyblockApiId;
+
+ @Unique
+ private String neuName;
+
@ModifyReturnValue(method = "getName", at = @At("RETURN"))
private Text skyblocker$customItemNames(Text original) {
if (Utils.isOnSkyblock()) {
- return SkyblockerConfigManager.get().general.customItemNames.getOrDefault(ItemUtils.getItemUuid((ItemStack) (Object) this), original);
+ return SkyblockerConfigManager.get().general.customItemNames.getOrDefault(ItemUtils.getItemUuid(this), original);
}
return original;
@@ -86,8 +110,8 @@ public abstract class ItemStackMixin {
}
@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
@@ -103,4 +127,146 @@ public abstract class ItemStackMixin {
setDamage(durability.rightInt() - durability.leftInt());
return true;
}
+
+ @Override
+ @Nullable
+ public String getSkyblockId() {
+ if (skyblockId != null && !skyblockId.isEmpty()) return skyblockId;
+ return skyblockId = skyblocker$getSkyblockId(true);
+ }
+
+ @Override
+ @Nullable
+ public String getSkyblockApiId() {
+ if (skyblockApiId != null && !skyblockApiId.isEmpty()) return skyblockApiId;
+ return skyblockApiId = skyblocker$getSkyblockId(false);
+ }
+
+ @Override
+ @Nullable
+ public String getNeuName() {
+ if (neuName != null && !neuName.isEmpty()) return neuName;
+ String apiId = getSkyblockApiId();
+ String id = getSkyblockId();
+ if (apiId == null || id == null) return null;
+
+ if (apiId.startsWith("ISSHINY_")) apiId = id;
+
+ return neuName = ItemTooltip.getNeuName(id, apiId);
+ }
+
+ @Unique
+ private String skyblocker$getSkyblockId(boolean internalIDOnly) {
+ NbtCompound customData = ItemUtils.getCustomData((ItemStack) (Object) this);
+
+ if (customData == null || !customData.contains(ItemUtils.ID, NbtElement.STRING_TYPE)) {
+ return null;
+ }
+ String customDataString = customData.getString(ItemUtils.ID);
+
+ if (internalIDOnly) {
+ return customDataString;
+ }
+
+ // Transformation to API format.
+ //TODO future - remove this and just handle it directly for the NEU id conversion because this whole system is confusing and hard to follow
+ if (customData.contains("is_shiny")) {
+ return "ISSHINY_" + customDataString;
+ }
+
+ switch (customDataString) {
+ case "ENCHANTED_BOOK" -> {
+ if (customData.contains("enchantments")) {
+ NbtCompound enchants = customData.getCompound("enchantments");
+ Optional<String> firstEnchant = enchants.getKeys().stream().findFirst();
+ String enchant = firstEnchant.orElse("");
+ return "ENCHANTMENT_" + enchant.toUpperCase(Locale.ENGLISH) + "_" + enchants.getInt(enchant);
+ }
+ }
+
+ case "PET" -> {
+ if (customData.contains("petInfo")) {
+ PetInfo petInfo = PetInfo.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(customData.getString("petInfo"))).getOrThrow();
+ return "LVL_1_" + petInfo.tier() + "_" + petInfo.type();
+ }
+ }
+
+ case "POTION" -> {
+ String enhanced = customData.contains("enhanced") ? "_ENHANCED" : "";
+ String extended = customData.contains("extended") ? "_EXTENDED" : "";
+ String splash = customData.contains("splash") ? "_SPLASH" : "";
+ if (customData.contains("potion") && customData.contains("potion_level")) {
+ return (customData.getString("potion") + "_" + customDataString + "_" + customData.getInt("potion_level")
+ + enhanced + extended + splash).toUpperCase(Locale.ENGLISH);
+ }
+ }
+
+ case "RUNE" -> {
+ if (customData.contains("runes")) {
+ NbtCompound runes = customData.getCompound("runes");
+ Optional<String> firstRunes = runes.getKeys().stream().findFirst();
+ String rune = firstRunes.orElse("");
+ return rune.toUpperCase(Locale.ENGLISH) + "_RUNE_" + runes.getInt(rune);
+ }
+ }
+
+ case "ATTRIBUTE_SHARD" -> {
+ if (customData.contains("attributes")) {
+ NbtCompound shards = customData.getCompound("attributes");
+ Optional<String> firstShards = shards.getKeys().stream().findFirst();
+ String shard = firstShards.orElse("");
+ return customDataString + "-" + shard.toUpperCase(Locale.ENGLISH) + "_" + shards.getInt(shard);
+ }
+ }
+
+ case "NEW_YEAR_CAKE" -> {
+ return customDataString + "_" + customData.getInt("new_years_cake");
+ }
+
+ case "PARTY_HAT_CRAB", "PARTY_HAT_CRAB_ANIMATED", "BALLOON_HAT_2024" -> {
+ return customDataString + "_" + customData.getString("party_hat_color").toUpperCase(Locale.ENGLISH);
+ }
+
+ case "PARTY_HAT_SLOTH" -> {
+ return customDataString + "_" + customData.getString("party_hat_emoji").toUpperCase(Locale.ENGLISH);
+ }
+
+ case "CRIMSON_HELMET", "CRIMSON_CHESTPLATE", "CRIMSON_LEGGINGS", "CRIMSON_BOOTS" -> {
+ NbtCompound attributes = customData.getCompound("attributes");
+
+ if (attributes.contains("magic_find") && attributes.contains("veteran")) {
+ return customDataString + "-MAGIC_FIND-VETERAN";
+ }
+ }
+
+ case "AURORA_HELMET", "AURORA_CHESTPLATE", "AURORA_LEGGINGS", "AURORA_BOOTS" -> {
+ NbtCompound attributes = customData.getCompound("attributes");
+
+ if (attributes.contains("mana_pool") && attributes.contains("mana_regeneration")) {
+ return customDataString + "-MANA_POOL-MANA_REGENERATION";
+ }
+ }
+
+ case "TERROR_HELMET", "TERROR_CHESTPLATE", "TERROR_LEGGINGS", "TERROR_BOOTS" -> {
+ NbtCompound attributes = customData.getCompound("attributes");
+
+ if (attributes.contains("lifeline") && attributes.contains("mana_pool")) {
+ return customDataString + "-LIFELINE-MANA_POOL";
+ }
+ }
+
+ 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/accessors/BeaconBlockEntityRendererInvoker.java b/src/main/java/de/hysky/skyblocker/mixins/accessors/BeaconBlockEntityRendererInvoker.java
index 833cfc22..a2a98a68 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/accessors/BeaconBlockEntityRendererInvoker.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/accessors/BeaconBlockEntityRendererInvoker.java
@@ -10,7 +10,7 @@ import org.spongepowered.asm.mixin.gen.Invoker;
public interface BeaconBlockEntityRendererInvoker {
@SuppressWarnings("unused")
@Invoker("renderBeam")
- static void renderBeam(MatrixStack matrices, VertexConsumerProvider vertexConsumers, float tickDelta, long worldTime, int yOffset, int maxY, float[] color) {
+ static void renderBeam(MatrixStack matrices, VertexConsumerProvider vertexConsumers, float tickDelta, long worldTime, int yOffset, int maxY, int color) {
throw new UnsupportedOperationException();
}
}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/accessors/CheckboxWidgetAccessor.java b/src/main/java/de/hysky/skyblocker/mixins/accessors/CheckboxWidgetAccessor.java
new file mode 100644
index 00000000..5ec4a8e8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixins/accessors/CheckboxWidgetAccessor.java
@@ -0,0 +1,11 @@
+package de.hysky.skyblocker.mixins.accessors;
+
+import net.minecraft.client.gui.widget.CheckboxWidget;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(CheckboxWidget.class)
+public interface CheckboxWidgetAccessor {
+ @Accessor
+ void setChecked(boolean checked);
+}
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/ChestValue.java b/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java
index 7f8b2c71..908525e1 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java
@@ -7,13 +7,11 @@ import de.hysky.skyblocker.config.configs.UIAndVisualsConfig;
import de.hysky.skyblocker.mixins.accessors.HandledScreenAccessor;
import de.hysky.skyblocker.mixins.accessors.ScreenAccessor;
import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
-import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
-import it.unimi.dsi.fastutil.longs.LongBooleanPair;
+import it.unimi.dsi.fastutil.doubles.DoubleBooleanPair;
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
import net.fabricmc.fabric.api.client.screen.v1.Screens;
-import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
import net.minecraft.client.gui.tooltip.Tooltip;
import net.minecraft.client.gui.widget.ButtonWidget;
@@ -45,7 +43,7 @@ public class ChestValue {
if (DUNGEON_CHESTS.contains(titleString)) {
if (SkyblockerConfigManager.get().dungeons.dungeonChestProfit.enableProfitCalculator) {
ScreenEvents.afterTick(screen).register(screen_ ->
- ((ScreenAccessor) screen).setTitle(getDungeonChestProfit(genericContainerScreen.getScreenHandler(), title, titleString, client))
+ ((ScreenAccessor) screen).setTitle(getDungeonChestProfit(genericContainerScreen.getScreenHandler(), title, titleString))
);
}
} else if (SkyblockerConfigManager.get().uiAndVisuals.chestValue.enableChestValue && !titleString.equals("SkyBlock Menu")) {
@@ -65,9 +63,9 @@ public class ChestValue {
});
}
- private static Text getDungeonChestProfit(GenericContainerScreenHandler handler, Text title, String titleString, MinecraftClient client) {
+ private static Text getDungeonChestProfit(GenericContainerScreenHandler handler, Text title, String titleString) {
try {
- long profit = 0;
+ double profit = 0;
boolean hasIncompleteData = false, usedKismet = false;
List<Slot> slots = handler.slots.subList(0, handler.getRows() * 9);
@@ -81,16 +79,16 @@ public class ChestValue {
}
String name = stack.getName().getString();
- String id = ItemTooltip.getInternalNameFromNBT(stack, false);
+ String id = stack.getSkyblockApiId();
//Regular item price
if (id != null) {
- LongBooleanPair priceData = getItemPrice(id);
+ DoubleBooleanPair priceData = ItemUtils.getItemPrice(id);
if (!priceData.rightBoolean()) hasIncompleteData = true;
//Add the item price to the profit
- profit += priceData.leftLong() * stack.getCount();
+ profit += priceData.leftDouble() * stack.getCount();
continue;
}
@@ -103,12 +101,12 @@ public class ChestValue {
String type = matcher.group("type");
int amount = Integer.parseInt(matcher.group("amount"));
- LongBooleanPair priceData = getItemPrice(("ESSENCE_" + type).toUpperCase());
+ DoubleBooleanPair priceData = ItemUtils.getItemPrice(("ESSENCE_" + type).toUpperCase());
if (!priceData.rightBoolean()) hasIncompleteData = true;
//Add the price of the essence to the profit
- profit += priceData.leftLong() * amount;
+ profit += priceData.leftDouble() * amount;
continue;
}
@@ -116,7 +114,7 @@ public class ChestValue {
//Determine the cost of the chest
if (name.contains("Open Reward Chest")) {
- String foundString = searchLoreFor(stack, client, "Coins");
+ String foundString = searchLoreFor(stack, "Coins");
//Incase we're searching the free chest
if (!StringUtils.isBlank(foundString)) {
@@ -128,19 +126,19 @@ public class ChestValue {
//Determine if a kismet was used or not
if (name.contains("Reroll Chest")) {
- usedKismet = !StringUtils.isBlank(searchLoreFor(stack, client, "You already rerolled a chest!"));
+ usedKismet = !StringUtils.isBlank(searchLoreFor(stack, "You already rerolled a chest!"));
}
}
if (SkyblockerConfigManager.get().dungeons.dungeonChestProfit.includeKismet && usedKismet) {
- LongBooleanPair kismetPriceData = getItemPrice("KISMET_FEATHER");
+ DoubleBooleanPair kismetPriceData = ItemUtils.getItemPrice("KISMET_FEATHER");
if (!kismetPriceData.rightBoolean()) hasIncompleteData = true;
- profit -= kismetPriceData.leftLong();
+ profit -= kismetPriceData.leftDouble();
}
- return Text.literal(titleString).append(getProfitText(profit, hasIncompleteData));
+ return Text.literal(titleString).append(getProfitText((long) profit, hasIncompleteData));
} catch (Exception e) {
LOGGER.error("[Skyblocker Profit Calculator] Failed to calculate dungeon chest profit! ", e);
}
@@ -150,7 +148,7 @@ public class ChestValue {
private static Text getChestValue(GenericContainerScreenHandler handler, Text title, String titleString) {
try {
- long value = 0;
+ double value = 0;
boolean hasIncompleteData = false;
List<Slot> slots = handler.slots.subList(0, handler.getRows() * 9);
@@ -160,18 +158,18 @@ public class ChestValue {
continue;
}
- String id = ItemTooltip.getInternalNameFromNBT(stack, false);
+ String id = stack.getSkyblockApiId();
if (id != null) {
- LongBooleanPair priceData = getItemPrice(id);
+ DoubleBooleanPair priceData = ItemUtils.getItemPrice(id);
if (!priceData.rightBoolean()) hasIncompleteData = true;
- value += priceData.leftLong() * stack.getCount();
+ value += priceData.leftDouble() * stack.getCount();
}
}
- return Text.literal(titleString).append(getValueText(value, hasIncompleteData));
+ return Text.literal(titleString).append(getValueText((long) value, hasIncompleteData));
} catch (Exception e) {
LOGGER.error("[Skyblocker Value Calculator] Failed to calculate dungeon chest value! ", e);
}
@@ -180,33 +178,9 @@ public class ChestValue {
}
/**
- * @return An {@link LongBooleanPair} with the {@code left long} representing the item's price, and the {@code right boolean} indicating if the price
- * was based on complete data.
- */
- private static LongBooleanPair getItemPrice(String id) {
- JsonObject bazaarPrices = TooltipInfoType.BAZAAR.getData();
- JsonObject lbinPrices = TooltipInfoType.LOWEST_BINS.getData();
-
- if (bazaarPrices == null || lbinPrices == null) return LongBooleanPair.of(0L, false);
-
- if (bazaarPrices.has(id)) {
- JsonObject item = bazaarPrices.get(id).getAsJsonObject();
- boolean isPriceNull = item.get("sellPrice").isJsonNull();
-
- return LongBooleanPair.of(isPriceNull ? 0L : (long) item.get("sellPrice").getAsDouble(), !isPriceNull);
- }
-
- if (lbinPrices.has(id)) {
- return LongBooleanPair.of((long) lbinPrices.get(id).getAsDouble(), true);
- }
-
- return LongBooleanPair.of(0L, false);
- }
-
- /**
* Searches for a specific string of characters in the name and lore of an item
*/
- private static String searchLoreFor(ItemStack stack, MinecraftClient client, String searchString) {
+ private static String searchLoreFor(ItemStack stack, String searchString) {
return ItemUtils.getLoreLineIf(stack, line -> line.contains(searchString));
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java b/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java
new file mode 100644
index 00000000..8d0406cb
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java
@@ -0,0 +1,148 @@
+package de.hysky.skyblocker.skyblock;
+
+import com.google.gson.JsonParser;
+import com.mojang.logging.LogUtils;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.JsonOps;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.Utils;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.screen.slot.Slot;
+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.
+ *
+ * Want support? Ask the Admins for a Mod API event or open your pets menu.
+ */
+public class PetCache {
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private static final Path FILE = SkyblockerMod.CONFIG_DIR.resolve("pet_cache.json");
+ private static final Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, PetInfo>> CACHED_PETS = new Object2ObjectOpenHashMap<>();
+
+ /**
+ * Used in case the server lags to prevent the screen tick check from overwriting the clicked pet logic
+ */
+ private static boolean shouldLook4Pets;
+
+ public static void init() {
+ load();
+
+ ScreenEvents.BEFORE_INIT.register((_client, screen, _scaledWidth, _scaledHeight) -> {
+ if (Utils.isOnSkyblock() && screen instanceof GenericContainerScreen genericContainerScreen) {
+ if (genericContainerScreen.getTitle().getString().startsWith("Pets")) {
+ shouldLook4Pets = true;
+
+ ScreenEvents.afterTick(screen).register(screen1 -> {
+ if (shouldLook4Pets) {
+ for (Slot slot : genericContainerScreen.getScreenHandler().slots) {
+ ItemStack stack = slot.getStack();
+
+ if (!stack.isEmpty() && ItemUtils.getLoreLineIf(stack, line -> line.equals("Click to despawn!")) != null) {
+ shouldLook4Pets = false;
+ parsePet(stack, false);
+
+ break;
+ }
+ }
+ }
+ });
+ }
+ }
+ });
+ }
+
+ private static void load() {
+ CompletableFuture.runAsync(() -> {
+ try (BufferedReader reader = Files.newBufferedReader(FILE)) {
+ CACHED_PETS.putAll(PetInfo.SERIALIZATION_CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).getOrThrow());
+ } catch (NoSuchFileException ignored) {
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Pet Cache] Failed to load saved pet!", e);
+ }
+ });
+ }
+
+ private static void save() {
+ CompletableFuture.runAsync(() -> {
+ try (BufferedWriter writer = Files.newBufferedWriter(FILE)) {
+ SkyblockerMod.GSON.toJson(PetInfo.SERIALIZATION_CODEC.encodeStart(JsonOps.INSTANCE, CACHED_PETS).getOrThrow(), writer);
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Pet Cache] Failed to save pet data to the cache!", e);
+ }
+ });
+ }
+
+ public static void handlePetEquip(Slot slot, int slotId) {
+ //Ignore inventory clicks
+ if (slotId >= 0 && slotId <= 53) {
+ ItemStack stack = slot.getStack();
+
+ if (!stack.isEmpty()) parsePet(stack, true);
+ }
+ }
+
+ private static void parsePet(ItemStack stack, boolean clicked) {
+ String id = ItemUtils.getItemId(stack);
+ String profileId = Utils.getProfileId();
+
+ if (id.equals("PET") && !profileId.isEmpty()) {
+ NbtCompound customData = ItemUtils.getCustomData(stack);
+
+ //Should never fail, all pets must have this but you never know with Hypixel
+ try {
+ PetInfo petInfo = PetInfo.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(customData.getString("petInfo"))).getOrThrow();
+ shouldLook4Pets = false;
+
+ Object2ObjectOpenHashMap<String, PetInfo> playerData = CACHED_PETS.computeIfAbsent(Utils.getUndashedUuid(), _uuid -> new Object2ObjectOpenHashMap<>());
+
+ //Handle deselecting pets
+ if (clicked && getCurrentPet() != null && getCurrentPet().uuid().orElse("").equals(petInfo.uuid().orElse(""))) {
+ playerData.remove(profileId);
+ } else {
+ playerData.put(profileId, petInfo);
+ }
+
+ save();
+ } catch (Exception e) {
+ LOGGER.error(LogUtils.FATAL_MARKER, "[Skyblocker Pet Cache] Failed to parse pet's pet info!", e);
+ }
+ }
+ }
+
+ @Nullable
+ public static PetInfo getCurrentPet() {
+ String uuid = Utils.getUndashedUuid();
+ String profileId = Utils.getProfileId();
+
+ return CACHED_PETS.containsKey(uuid) && CACHED_PETS.get(uuid).containsKey(profileId) ? CACHED_PETS.get(uuid).get(profileId) : null;
+ }
+
+ public record PetInfo(String type, double exp, String tier, Optional<String> uuid, 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("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)
+ ).xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java b/src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java
index 042b126b..354c92fb 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java
@@ -1,7 +1,6 @@
package de.hysky.skyblocker.skyblock;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
-import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.render.RenderHelper;
@@ -27,7 +26,7 @@ public class TeleportOverlay {
private static void render(WorldRenderContext wrc) {
if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().uiAndVisuals.teleportOverlay.enableTeleportOverlays && client.player != null && client.world != null) {
ItemStack heldItem = client.player.getMainHandStack();
- String itemId = ItemTooltip.getInternalNameFromNBT(heldItem, true);
+ String itemId = heldItem.getSkyblockId();
NbtCompound customData = ItemUtils.getCustomData(heldItem);
if (itemId != null) {
@@ -86,7 +85,7 @@ public class TeleportOverlay {
render(wrc, blockHitResult);
} else if (client.interactionManager != null && range > client.player.getAttributeInstance(EntityAttributes.PLAYER_BLOCK_INTERACTION_RANGE).getValue()) {
@SuppressWarnings("DataFlowIssue")
- HitResult result = client.player.raycast(range, wrc.tickDelta(), false);
+ HitResult result = client.player.raycast(range, wrc.tickCounter().getTickDelta(true), false);
if (result.getType() == HitResult.Type.BLOCK && result instanceof BlockHitResult blockHitResult) {
render(wrc, blockHitResult);
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionBrowserScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionBrowserScreen.java
index fd69d886..b690baa1 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionBrowserScreen.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionBrowserScreen.java
@@ -41,11 +41,11 @@ import java.util.function.Supplier;
public class AuctionBrowserScreen extends AbstractCustomHypixelGUI<AuctionHouseScreenHandler> {
private static final Logger LOGGER = LoggerFactory.getLogger(AuctionBrowserScreen.class);
- private static final Identifier TEXTURE = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/auctions_gui/browser/background.png");
- private static final Identifier SCROLLER_TEXTURE = new Identifier("container/creative_inventory/scroller");
+ private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/auctions_gui/browser/background.png");
+ private static final Identifier SCROLLER_TEXTURE = Identifier.ofVanilla("container/creative_inventory/scroller");
- private static final Identifier up_arrow_tex = new Identifier(SkyblockerMod.NAMESPACE, "up_arrow_even"); // Put them in their own fields to avoid object allocation on each frame
- private static final Identifier down_arrow_tex = new Identifier(SkyblockerMod.NAMESPACE, "down_arrow_even");
+ private static final Identifier up_arrow_tex = Identifier.of(SkyblockerMod.NAMESPACE, "up_arrow_even"); // Put them in their own fields to avoid object allocation on each frame
+ private static final Identifier down_arrow_tex = Identifier.of(SkyblockerMod.NAMESPACE, "down_arrow_even");
public static final Supplier<Sprite> UP_ARROW = () -> MinecraftClient.getInstance().getGuiAtlasManager().getSprite(up_arrow_tex);
public static final Supplier<Sprite> DOWN_ARROW = () -> MinecraftClient.getInstance().getGuiAtlasManager().getSprite(down_arrow_tex);
@@ -295,8 +295,8 @@ public class AuctionBrowserScreen extends AbstractCustomHypixelGUI<AuctionHouseS
String coins = split[1].replace(",", "").replace("coins", "").trim();
try {
long parsed = Long.parseLong(coins);
- String name = ItemTooltip.getInternalNameFromNBT(stack, false);
- String internalID = ItemTooltip.getInternalNameFromNBT(stack, true);
+ String name = stack.getSkyblockApiId();
+ String internalID = stack.getSkyblockId();
String neuName = name;
if (name == null || internalID == null) break;
if (name.startsWith("ISSHINY_")) {
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionViewScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionViewScreen.java
index f3db2a25..ed5e0f59 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionViewScreen.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionViewScreen.java
@@ -31,7 +31,7 @@ import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
public class AuctionViewScreen extends AbstractCustomHypixelGUI<AuctionHouseScreenHandler> {
- protected static final Identifier BACKGROUND_TEXTURE = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/auctions_gui/browser/background_view.png");
+ protected static final Identifier BACKGROUND_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/auctions_gui/browser/background_view.png");
public static final int BACK_BUTTON_SLOT = 49;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/AuctionTypeWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/AuctionTypeWidget.java
index 2204d39c..86202ffe 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/AuctionTypeWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/AuctionTypeWidget.java
@@ -23,11 +23,11 @@ public class AuctionTypeWidget extends SliderWidget<AuctionTypeWidget.Option> {
private final Identifier texture;
private static final String prefix = "textures/gui/auctions_gui/auction_type_widget/";
- private static final Identifier HOVER_TEXTURE = new Identifier(SkyblockerMod.NAMESPACE, prefix + "hover.png");
- private static final Identifier BACK_TEXTURE = new Identifier(SkyblockerMod.NAMESPACE, prefix + "back.png");
+ private static final Identifier HOVER_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, prefix + "hover.png");
+ private static final Identifier BACK_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, prefix + "back.png");
Option(String textureName) {
- texture = new Identifier(SkyblockerMod.NAMESPACE, prefix + textureName);
+ texture = Identifier.of(SkyblockerMod.NAMESPACE, prefix + textureName);
}
private static final AuctionTypeWidget.Option[] values = values();
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/CategoryTabWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/CategoryTabWidget.java
index 02dbc132..89744085 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/CategoryTabWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/CategoryTabWidget.java
@@ -1,42 +1,27 @@
package de.hysky.skyblocker.skyblock.auction.widgets;
import de.hysky.skyblocker.skyblock.auction.SlotClickHandler;
+import de.hysky.skyblocker.utils.render.gui.SideTabButtonWidget;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
-import net.minecraft.client.gui.screen.ButtonTextures;
-import net.minecraft.client.gui.widget.ToggleButtonWidget;
-import net.minecraft.client.item.TooltipType;
import net.minecraft.item.Item.TooltipContext;
import net.minecraft.item.ItemStack;
-import net.minecraft.util.Identifier;
-import org.jetbrains.annotations.NotNull;
-
-public class CategoryTabWidget extends ToggleButtonWidget {
- private static final ButtonTextures TEXTURES = new ButtonTextures(new Identifier("recipe_book/tab"), new Identifier("recipe_book/tab_selected"));
+import net.minecraft.item.tooltip.TooltipType;
- public void setIcon(@NotNull ItemStack icon) {
- this.icon = icon.copy();
- }
+import org.jetbrains.annotations.NotNull;
- private @NotNull ItemStack icon;
+public class CategoryTabWidget extends SideTabButtonWidget {
private final SlotClickHandler slotClick;
private int slotId = -1;
public CategoryTabWidget(@NotNull ItemStack icon, SlotClickHandler slotClick) {
- super(0, 0, 35, 27, false);
- this.icon = icon.copy(); // copy prevents item disappearing on click
+ super(0, 0, false, icon);
this.slotClick = slotClick;
- setTextures(TEXTURES);
}
@Override
public void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
- if (textures == null) return;
- Identifier identifier = textures.get(true, this.toggled);
- int x = getX();
- if (toggled) x -= 2;
- context.drawGuiTexture(identifier, x, this.getY(), this.width, this.height);
- context.drawItem(icon, x + 9, getY() + 5);
+ super.renderWidget(context, mouseX, mouseY, delta);
if (isMouseOver(mouseX, mouseY)) {
context.getMatrices().push();
@@ -52,8 +37,8 @@ public class CategoryTabWidget extends ToggleButtonWidget {
@Override
public void onClick(double mouseX, double mouseY) {
- if (this.toggled || slotId == -1) return;
+ if (isToggled() || slotId == -1) return;
+ super.onClick(mouseX, mouseY);
slotClick.click(slotId);
- this.setToggled(true);
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/RarityWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/RarityWidget.java
index b6bd42a9..696b9f50 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/RarityWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/RarityWidget.java
@@ -19,8 +19,8 @@ import java.util.Map;
public class RarityWidget extends ClickableWidget {
- private static final Identifier HOVER_TEXTURE = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/auctions_gui/rarity_widget/hover.png");
- private static final Identifier TEXTURE = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/auctions_gui/rarity_widget/background.png");
+ private static final Identifier HOVER_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/auctions_gui/rarity_widget/hover.png");
+ private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/auctions_gui/rarity_widget/background.png");
private final SlotClickHandler onClick;
private int slotId = -1;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/SortWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/SortWidget.java
index 84b8ae4d..205cba33 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/SortWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/SortWidget.java
@@ -24,11 +24,11 @@ public class SortWidget extends SliderWidget<SortWidget.Option> {
private final Identifier texture;
private static final String prefix = "textures/gui/auctions_gui/sort_widget/";
- private static final Identifier HOVER_TEXTURE = new Identifier(SkyblockerMod.NAMESPACE, prefix + "hover.png");
- private static final Identifier BACK_TEXTURE = new Identifier(SkyblockerMod.NAMESPACE, prefix + "back.png");
+ private static final Identifier HOVER_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, prefix + "hover.png");
+ private static final Identifier BACK_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, prefix + "back.png");
Option(String textureName) {
- texture = new Identifier(SkyblockerMod.NAMESPACE, prefix + textureName);
+ texture = Identifier.of(SkyblockerMod.NAMESPACE, prefix + textureName);
}
public Identifier getOptionTexture() {
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleAnnouncementScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleAnnouncementScreen.java
index b8d409ae..5fac426e 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleAnnouncementScreen.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleAnnouncementScreen.java
@@ -4,6 +4,7 @@ import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.events.HudRenderEvents;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.render.RenderTickCounter;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.Text;
@@ -13,11 +14,11 @@ public class ChatRuleAnnouncementScreen {
private static Text text = null;
public static void init() {
- HudRenderEvents.BEFORE_CHAT.register((context, tickDelta) -> {
+ HudRenderEvents.BEFORE_CHAT.register((context, tickCounter) -> {
if (timer <= 0 || text == null) {
return;
}
- render(context, tickDelta);
+ render(context, tickCounter);
});
}
@@ -26,10 +27,10 @@ public class ChatRuleAnnouncementScreen {
* @param context render context
* @param tickDelta difference from last render to remove from timer
*/
- private static void render(DrawContext context, float tickDelta) {
+ private static void render(DrawContext context, RenderTickCounter tickCounter) {
int scale = SkyblockerConfigManager.get().chat.chatRuleConfig.announcementScale;
//decrement timer
- timer -= tickDelta;
+ timer -= tickCounter.getTickDelta(true);
//scale text up and center
MatrixStack matrices = context.getMatrices();
matrices.push();
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 2d530b6d..f984d751 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java
@@ -1,25 +1,23 @@
package de.hysky.skyblocker.skyblock.chocolatefactory;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
-import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.adders.LineSmoothener;
import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.RegexUtils;
import de.hysky.skyblocker.utils.render.gui.ColorHighlight;
import de.hysky.skyblocker.utils.render.gui.ContainerSolver;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
-import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback;
-import net.minecraft.client.MinecraftClient;
-import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
-import net.minecraft.client.item.TooltipType;
-import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
+import net.minecraft.screen.slot.Slot;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
+import org.jetbrains.annotations.Nullable;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
@@ -28,6 +26,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ChocolateFactorySolver extends ContainerSolver {
+ //Patterns
private static final Pattern CPS_PATTERN = Pattern.compile("([\\d,.]+) Chocolate per second");
private static final Pattern CPS_INCREASE_PATTERN = Pattern.compile("\\+([\\d,]+) Chocolate per second");
private static final Pattern COST_PATTERN = Pattern.compile("Cost ([\\d,]+) Chocolate");
@@ -36,20 +35,51 @@ public class ChocolateFactorySolver extends ContainerSolver {
private static final Pattern CHOCOLATE_PATTERN = Pattern.compile("^([\\d,]+) Chocolate$");
private static final Pattern PRESTIGE_REQUIREMENT_PATTERN = Pattern.compile("Chocolate this Prestige: ([\\d,]+) +Requires (\\S+) Chocolate this Prestige!");
private static final Pattern TIME_TOWER_STATUS_PATTERN = Pattern.compile("Status: (ACTIVE|INACTIVE)");
- private static final ObjectArrayList<Rabbit> cpsIncreaseFactors = new ObjectArrayList<>(6);
+ private static final Pattern TIME_TOWER_MULTIPLIER_PATTERN = Pattern.compile("by \\+([\\d.]+)x for \\dh\\.");
+
+ private static final ObjectArrayList<Rabbit> cpsIncreaseFactors = new ObjectArrayList<>(8);
private static long totalChocolate = -1L;
private static double totalCps = -1.0;
private static double totalCpsMultiplier = -1.0;
private static long requiredUntilNextPrestige = -1L;
+ private static boolean canPrestige = false;
+ private static boolean reachedMaxPrestige = false;
private static double timeTowerMultiplier = -1.0;
+ private static boolean isTimeTowerMaxed = false;
private static boolean isTimeTowerActive = false;
+ private static int bestUpgrade = -1;
+ private static int bestAffordableUpgrade = -1;
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
- private static ItemStack bestUpgrade = null;
- private static ItemStack bestAffordableUpgrade = null;
+
+ @Override
+ protected void reset() {
+ cpsIncreaseFactors.clear();
+ totalChocolate = -1L;
+ totalCps = -1.0;
+ totalCpsMultiplier = -1.0;
+ requiredUntilNextPrestige = -1L;
+ canPrestige = false;
+ reachedMaxPrestige = false;
+ timeTowerMultiplier = -1.0;
+ isTimeTowerMaxed = false;
+ isTimeTowerActive = false;
+ bestUpgrade = -1;
+ bestAffordableUpgrade = -1;
+ }
+
+ //Slots, for ease of maintenance rather than using magic numbers everywhere.
+ private static final byte RABBITS_START = 28;
+ private static final byte RABBITS_END = 34;
+ private static final byte COACH_SLOT = 42;
+ private static final byte CHOCOLATE_SLOT = 13;
+ private static final byte CPS_SLOT = 45;
+ private static final byte PRESTIGE_SLOT = 27;
+ private static final byte TIME_TOWER_SLOT = 39;
+ private static final byte STRAY_RABBIT_START = 0;
+ private static final byte STRAY_RABBIT_END = 26;
public ChocolateFactorySolver() {
- super("^Chocolate Factory$");
- ItemTooltipCallback.EVENT.register(ChocolateFactorySolver::handleTooltip);
+ super("^Chocolate Factory$"); //There are multiple screens that fit the pattern `^Chocolate Factory`, so the $ is required
}
@Override
@@ -62,11 +92,12 @@ public class ChocolateFactorySolver extends ContainerSolver {
updateFactoryInfo(slots);
List<ColorHighlight> highlights = new ArrayList<>();
- getPrestigeHighlight(slots.get(28)).ifPresent(highlights::add);
+ getPrestigeHighlight().ifPresent(highlights::add);
+ highlights.addAll(getStrayRabbitHighlight(slots));
if (totalChocolate <= 0 || cpsIncreaseFactors.isEmpty()) return highlights; //Something went wrong or there's nothing we can afford.
Rabbit bestRabbit = cpsIncreaseFactors.getFirst();
- bestUpgrade = bestRabbit.itemStack;
+ bestUpgrade = bestRabbit.slot;
if (bestRabbit.cost <= totalChocolate) {
highlights.add(ColorHighlight.green(bestRabbit.slot));
return highlights;
@@ -75,7 +106,7 @@ public class ChocolateFactorySolver extends ContainerSolver {
for (Rabbit rabbit : cpsIncreaseFactors.subList(1, cpsIncreaseFactors.size())) {
if (rabbit.cost <= totalChocolate) {
- bestAffordableUpgrade = rabbit.itemStack;
+ bestAffordableUpgrade = rabbit.slot;
highlights.add(ColorHighlight.green(rabbit.slot));
break;
}
@@ -87,7 +118,7 @@ public class ChocolateFactorySolver extends ContainerSolver {
private static void updateFactoryInfo(Int2ObjectMap<ItemStack> slots) {
cpsIncreaseFactors.clear();
- for (int i = 29; i <= 33; i++) { // The 5 rabbits slots are in 29, 30, 31, 32 and 33.
+ for (int i = RABBITS_START; i <= RABBITS_END; i++) { // The 7 rabbits slots are in 28, 29, 30, 31, 32, 33 and 34.
ItemStack item = slots.get(i);
if (item.isOf(Items.PLAYER_HEAD)) {
getRabbit(item, i).ifPresent(cpsIncreaseFactors::add);
@@ -95,20 +126,21 @@ public class ChocolateFactorySolver extends ContainerSolver {
}
//Coach is in slot 42
- getCoach(slots.get(42)).ifPresent(cpsIncreaseFactors::add);
+ getCoach(slots.get(COACH_SLOT)).ifPresent(cpsIncreaseFactors::add);
//The clickable chocolate is in slot 13, holds the total chocolate
- RegexUtils.getLongFromMatcher(CHOCOLATE_PATTERN.matcher(slots.get(13).getName().getString())).ifPresent(l -> totalChocolate = l);
+ RegexUtils.getLongFromMatcher(CHOCOLATE_PATTERN.matcher(slots.get(CHOCOLATE_SLOT).getName().getString())).ifPresent(l -> totalChocolate = l);
//Cps item (cocoa bean) is in slot 45
- String cpsItemLore = getConcatenatedLore(slots.get(45));
+ String cpsItemLore = getConcatenatedLore(slots.get(CPS_SLOT));
Matcher cpsMatcher = CPS_PATTERN.matcher(cpsItemLore);
RegexUtils.getDoubleFromMatcher(cpsMatcher).ifPresent(d -> totalCps = d);
Matcher multiplierMatcher = TOTAL_MULTIPLIER_PATTERN.matcher(cpsItemLore);
RegexUtils.getDoubleFromMatcher(multiplierMatcher, cpsMatcher.hasMatch() ? cpsMatcher.end() : 0).ifPresent(d -> totalCpsMultiplier = d);
//Prestige item is in slot 28
- Matcher prestigeMatcher = PRESTIGE_REQUIREMENT_PATTERN.matcher(getConcatenatedLore(slots.get(28)));
+ String prestigeLore = getConcatenatedLore(slots.get(PRESTIGE_SLOT));
+ Matcher prestigeMatcher = PRESTIGE_REQUIREMENT_PATTERN.matcher(prestigeLore);
OptionalLong currentChocolate = RegexUtils.getLongFromMatcher(prestigeMatcher);
if (currentChocolate.isPresent()) {
String requirement = prestigeMatcher.group(2); //If the first one matched, we can assume the 2nd one is also matched since it's one whole regex
@@ -117,12 +149,22 @@ public class ChocolateFactorySolver extends ContainerSolver {
if (NumberUtils.isParsable(amountString)) {
requiredUntilNextPrestige = Long.parseLong(amountString) - currentChocolate.getAsLong();
}
+ canPrestige = false;
+ } else if (prestigeLore.endsWith("Click to prestige!")) {
+ canPrestige = true;
+ reachedMaxPrestige = false;
+ } else if (prestigeLore.endsWith("You have reached max prestige!")) {
+ canPrestige = false;
+ reachedMaxPrestige = true;
}
//Time Tower is in slot 39
- timeTowerMultiplier = romanToDecimal(StringUtils.substringAfterLast(slots.get(39).getName().getString(), ' ')) / 10.0; //The name holds the level, which is multiplier * 10 in roman numerals
- Matcher timeTowerStatusMatcher = TIME_TOWER_STATUS_PATTERN.matcher(getConcatenatedLore(slots.get(39)));
- if (timeTowerStatusMatcher.find()) {
+ isTimeTowerMaxed = StringUtils.substringAfterLast(slots.get(TIME_TOWER_SLOT).getName().getString(), ' ').equals("XV");
+ String timeTowerLore = getConcatenatedLore(slots.get(TIME_TOWER_SLOT));
+ Matcher timeTowerMultiplierMatcher = TIME_TOWER_MULTIPLIER_PATTERN.matcher(timeTowerLore);
+ RegexUtils.getDoubleFromMatcher(timeTowerMultiplierMatcher).ifPresent(d -> timeTowerMultiplier = d);
+ Matcher timeTowerStatusMatcher = TIME_TOWER_STATUS_PATTERN.matcher(timeTowerLore);
+ if (timeTowerStatusMatcher.find(timeTowerMultiplierMatcher.hasMatch() ? timeTowerMultiplierMatcher.end() : 0)) {
isTimeTowerActive = timeTowerStatusMatcher.group(1).equals("ACTIVE");
}
@@ -130,128 +172,6 @@ public class ChocolateFactorySolver extends ContainerSolver {
cpsIncreaseFactors.sort(Comparator.comparingDouble(rabbit -> rabbit.cost() / rabbit.cpsIncrease())); //Ascending order, lower = better
}
- private static void handleTooltip(ItemStack stack, Item.TooltipContext tooltipContext, TooltipType tooltipType, List<Text> lines) {
- if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableChocolateFactoryHelper) return;
- if (!(MinecraftClient.getInstance().currentScreen instanceof GenericContainerScreen screen) || !screen.getTitle().getString().equals("Chocolate Factory")) return;
-
- int lineIndex = lines.size();
- //This boolean is used to determine if we should add a smooth line to separate the added information from the rest of the tooltip.
- //It should be set to true if there's any information added, false otherwise.
- boolean shouldAddLine = false;
-
- String lore = concatenateLore(lines);
- Matcher costMatcher = COST_PATTERN.matcher(lore);
- OptionalLong cost = RegexUtils.getLongFromMatcher(costMatcher);
- //Available on all items with a chocolate cost
- if (cost.isPresent()) shouldAddLine = addUpgradeTimerToLore(lines, cost.getAsLong());
-
- //Prestige item
- if (stack.isOf(Items.DROPPER) && requiredUntilNextPrestige != -1L) {
- shouldAddLine = addPrestigeTimerToLore(lines) || shouldAddLine;
- }
- //Time tower
- else if (stack.isOf(Items.CLOCK)) {
- shouldAddLine = addTimeTowerStatsToLore(lines) || shouldAddLine;
- }
- //Rabbits
- else if (stack.isOf(Items.PLAYER_HEAD)) {
- shouldAddLine = addRabbitStatsToLore(lines, stack) || shouldAddLine;
- }
-
- //This is an ArrayList, so this operation is probably not very efficient, but logically it's pretty much the only way I can think of
- if (shouldAddLine) lines.add(lineIndex, ItemTooltip.createSmoothLine());
- }
-
- private static boolean addUpgradeTimerToLore(List<Text> lines, long cost) {
- if (totalChocolate < 0L || totalCps < 0.0) return false;
- lines.add(Text.empty()
- .append(Text.literal("Time until upgrade: ").formatted(Formatting.GRAY))
- .append(formatTime((cost - totalChocolate) / totalCps)));
- return true;
- }
-
- private static boolean addPrestigeTimerToLore(List<Text> lines) {
- if (requiredUntilNextPrestige == -1L || totalCps == -1.0) return false;
- lines.add(Text.empty()
- .append(Text.literal("Chocolate until next prestige: ").formatted(Formatting.GRAY))
- .append(Text.literal(DECIMAL_FORMAT.format(requiredUntilNextPrestige)).formatted(Formatting.GOLD)));
- lines.add(Text.empty()
- .append(Text.literal("Time until next prestige: ").formatted(Formatting.GRAY))
- .append(formatTime(requiredUntilNextPrestige / totalCps)));
- return true;
- }
-
- private static boolean addTimeTowerStatsToLore(List<Text> lines) {
- if (totalCps < 0.0 || totalCpsMultiplier < 0.0 || timeTowerMultiplier < 0.0) return false;
- lines.add(Text.literal("Current stats:").formatted(Formatting.GRAY));
- lines.add(Text.empty()
- .append(Text.literal(" CPS increase: ").formatted(Formatting.GRAY))
- .append(Text.literal(DECIMAL_FORMAT.format(totalCps / totalCpsMultiplier * timeTowerMultiplier)).formatted(Formatting.GOLD)));
- lines.add(Text.empty()
- .append(Text.literal(" CPS when active: ").formatted(Formatting.GRAY))
- .append(Text.literal(DECIMAL_FORMAT.format(isTimeTowerActive ? totalCps : totalCps / totalCpsMultiplier * (timeTowerMultiplier + totalCpsMultiplier))).formatted(Formatting.GOLD)));
- if (timeTowerMultiplier < 1.5) {
- lines.add(Text.literal("Stats after upgrade:").formatted(Formatting.GRAY));
- lines.add(Text.empty()
- .append(Text.literal(" CPS increase: ").formatted(Formatting.GRAY))
- .append(Text.literal(DECIMAL_FORMAT.format(totalCps / (totalCpsMultiplier) * (timeTowerMultiplier + 0.1))).formatted(Formatting.GOLD)));
- lines.add(Text.empty()
- .append(Text.literal(" CPS when active: ").formatted(Formatting.GRAY))
- .append(Text.literal(DECIMAL_FORMAT.format(isTimeTowerActive ? totalCps / totalCpsMultiplier * (totalCpsMultiplier + 0.1) : totalCps / totalCpsMultiplier * (timeTowerMultiplier + 0.1 + totalCpsMultiplier))).formatted(Formatting.GOLD)));
- }
- return true;
- }
-
- private static boolean addRabbitStatsToLore(List<Text> lines, ItemStack stack) {
- if (cpsIncreaseFactors.isEmpty()) return false;
- boolean changed = false;
- for (Rabbit rabbit : cpsIncreaseFactors) {
- if (rabbit.itemStack != stack) continue;
- changed = true;
- lines.add(Text.empty()
- .append(Text.literal("CPS Increase: ").formatted(Formatting.GRAY))
- .append(Text.literal(DECIMAL_FORMAT.format(rabbit.cpsIncrease)).formatted(Formatting.GOLD)));
-
- lines.add(Text.empty()
- .append(Text.literal("Cost per CPS: ").formatted(Formatting.GRAY))
- .append(Text.literal(DECIMAL_FORMAT.format(rabbit.cost / rabbit.cpsIncrease)).formatted(Formatting.GOLD)));
-
- if (rabbit.itemStack == bestUpgrade) {
- if (rabbit.cost <= totalChocolate) {
- lines.add(Text.literal("Best upgrade").formatted(Formatting.GREEN));
- } else {
- lines.add(Text.literal("Best upgrade, can't afford").formatted(Formatting.YELLOW));
- }
- } else if (rabbit.itemStack == bestAffordableUpgrade && rabbit.cost <= totalChocolate) {
- lines.add(Text.literal("Best upgrade you can afford").formatted(Formatting.GREEN));
- }
- }
- return changed;
- }
-
- private static MutableText formatTime(double seconds) {
- seconds = Math.ceil(seconds);
- if (seconds <= 0) return Text.literal("Now").formatted(Formatting.GREEN);
-
- StringBuilder builder = new StringBuilder();
- if (seconds >= 86400) {
- builder.append((int) (seconds / 86400)).append("d ");
- seconds %= 86400;
- }
- if (seconds >= 3600) {
- builder.append((int) (seconds / 3600)).append("h ");
- seconds %= 3600;
- }
- if (seconds >= 60) {
- builder.append((int) (seconds / 60)).append("m ");
- seconds %= 60;
- }
- if (seconds >= 1) {
- builder.append((int) seconds).append("s");
- }
- return Text.literal(builder.toString()).formatted(Formatting.GOLD);
- }
-
/**
* Utility method.
*/
@@ -276,7 +196,7 @@ public class ChocolateFactorySolver extends ContainerSolver {
if (!coachItem.isOf(Items.PLAYER_HEAD)) return Optional.empty();
String coachLore = getConcatenatedLore(coachItem);
- if (totalCpsMultiplier == -1.0) return Optional.empty(); //We need the total multiplier to calculate the increase in cps.
+ if (totalCps < 0 || totalCpsMultiplier < 0) return Optional.empty(); //We need these 2 to calculate the increase in cps.
Matcher multiplierIncreaseMatcher = MULTIPLIER_INCREASE_PATTERN.matcher(coachLore);
OptionalDouble currentCpsMultiplier = RegexUtils.getDoubleFromMatcher(multiplierIncreaseMatcher);
@@ -289,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(), 42, coachItem));
+ return Optional.of(new Rabbit(totalCps / totalCpsMultiplier * (nextCpsMultiplier.getAsDouble() - currentCpsMultiplier.getAsDouble()), cost.getAsLong(), COACH_SLOT));
}
private static Optional<Rabbit> getRabbit(ItemStack item, int slot) {
@@ -307,42 +227,161 @@ 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(), cost.getAsInt(), slot, item));
+ return Optional.of(new Rabbit((nextCps.getAsInt() - currentCps.getAsInt())*(totalCpsMultiplier < 0 ? 1 : totalCpsMultiplier), cost.getAsLong(), slot));
}
- private static Optional<ColorHighlight> getPrestigeHighlight(ItemStack item) {
- List<Text> loreList = ItemUtils.getLore(item);
- if (loreList.isEmpty()) return Optional.empty();
-
- String lore = loreList.getLast().getString(); //The last line holds the text we're looking for
- if (lore.equals("Click to prestige!")) return Optional.of(ColorHighlight.green(28));
- return Optional.of(ColorHighlight.red(28));
+ private static Optional<ColorHighlight> getPrestigeHighlight() {
+ if (reachedMaxPrestige) return Optional.empty();
+ if (canPrestige) return Optional.of(ColorHighlight.green(PRESTIGE_SLOT));
+ return Optional.of(ColorHighlight.red(PRESTIGE_SLOT));
}
- private record Rabbit(double cpsIncrease, int cost, int slot, ItemStack itemStack) {
+ private static List<ColorHighlight> getStrayRabbitHighlight(Int2ObjectMap<ItemStack> slots) {
+ final List<ColorHighlight> highlights = new ArrayList<>();
+ for (byte i = STRAY_RABBIT_START; i <= STRAY_RABBIT_END; i++) {
+ 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 - ")) {
+ highlights.add(ColorHighlight.green(i));
+ }
+ }
+ return highlights;
}
- //Perhaps the part below can go to a separate file later on, but I couldn't find a proper name for the class, so they're staying here.
- private static final Map<Character, Integer> romanMap = Map.of(
- 'I', 1,
- 'V', 5,
- 'X', 10,
- 'L', 50,
- 'C', 100,
- 'D', 500,
- 'M', 1000
- );
-
- public static int romanToDecimal(String romanNumeral) {
- int decimal = 0;
- int lastNumber = 0;
- for (int i = romanNumeral.length() - 1; i >= 0; i--) {
- char ch = romanNumeral.charAt(i);
- decimal = romanMap.get(ch) >= lastNumber ? decimal + romanMap.get(ch) : decimal - romanMap.get(ch);
- lastNumber = romanMap.get(ch);
+ private record Rabbit(double cpsIncrease, long cost, int slot) { }
+
+ public static final class Tooltip extends TooltipAdder {
+ public Tooltip() {
+ super("^Chocolate Factory$", 0); //The priority doesn't really matter here as this is the only tooltip adder for the Chocolate Factory.
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableChocolateFactoryHelper || focusedSlot == null) return;
+
+ int lineIndex = lines.size();
+ //This boolean is used to determine if we should add a smooth line to separate the added information from the rest of the tooltip.
+ //It should be set to true if there's any information added, false otherwise.
+ boolean shouldAddLine = false;
+
+ String lore = concatenateLore(lines);
+ Matcher costMatcher = COST_PATTERN.matcher(lore);
+ OptionalLong cost = RegexUtils.getLongFromMatcher(costMatcher);
+ //Available on all items with a chocolate cost
+ if (cost.isPresent()) shouldAddLine |= addUpgradeTimerToLore(lines, cost.getAsLong());
+
+ int index = focusedSlot.id;
+
+ //Prestige item
+ if (index == PRESTIGE_SLOT) {
+ shouldAddLine |= addPrestigeTimerToLore(lines);
+ }
+ //Time tower
+ else if (index == TIME_TOWER_SLOT) {
+ shouldAddLine |= addTimeTowerStatsToLore(lines);
+ }
+ //Rabbits
+ else if (index == COACH_SLOT || (index >= RABBITS_START && index <= RABBITS_END)) {
+ shouldAddLine |= addRabbitStatsToLore(lines, index);
+ }
+
+ //This is an ArrayList, so this operation is probably not very efficient, but logically it's pretty much the only way I can think of
+ if (shouldAddLine) lines.add(lineIndex, LineSmoothener.createSmoothLine());
+ }
+
+ private static boolean addUpgradeTimerToLore(List<Text> lines, long cost) {
+ if (totalChocolate < 0L || totalCps < 0.0) return false;
+ lines.add(Text.empty()
+ .append(Text.literal("Time until upgrade: ").formatted(Formatting.GRAY))
+ .append(formatTime((cost - totalChocolate) / totalCps)));
+ return true;
+ }
+
+ private static boolean addPrestigeTimerToLore(List<Text> lines) {
+ if (totalCps < 0.0 || reachedMaxPrestige) return false;
+ if (requiredUntilNextPrestige > 0 && !canPrestige) {
+ lines.add(Text.empty()
+ .append(Text.literal("Chocolate until next prestige: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(requiredUntilNextPrestige)).formatted(Formatting.GOLD)));
+ }
+ lines.add(Text.empty() //Keep this outside of the `if` to match the format of the upgrade tooltips, that say "Time until upgrade: Now" when it's possible
+ .append(Text.literal("Time until next prestige: ").formatted(Formatting.GRAY))
+ .append(formatTime(requiredUntilNextPrestige / totalCps)));
+ return true;
+ }
+
+ private static boolean addTimeTowerStatsToLore(List<Text> lines) {
+ if (totalCps < 0.0 || totalCpsMultiplier < 0.0 || timeTowerMultiplier < 0.0) return false;
+ lines.add(Text.literal("Current stats:").formatted(Formatting.GRAY));
+ lines.add(Text.empty()
+ .append(Text.literal(" CPS increase: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(totalCps / totalCpsMultiplier * timeTowerMultiplier)).formatted(Formatting.GOLD)));
+ lines.add(Text.empty()
+ .append(Text.literal(" CPS when active: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(isTimeTowerActive ? totalCps : totalCps / totalCpsMultiplier * (timeTowerMultiplier + totalCpsMultiplier))).formatted(Formatting.GOLD)));
+ if (!isTimeTowerMaxed) {
+ lines.add(Text.literal("Stats after upgrade:").formatted(Formatting.GRAY));
+ lines.add(Text.empty()
+ .append(Text.literal(" CPS increase: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(totalCps / (totalCpsMultiplier) * (timeTowerMultiplier + 0.1))).formatted(Formatting.GOLD)));
+ lines.add(Text.empty()
+ .append(Text.literal(" CPS when active: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(isTimeTowerActive ? totalCps / totalCpsMultiplier * (totalCpsMultiplier + 0.1) : totalCps / totalCpsMultiplier * (timeTowerMultiplier + 0.1 + totalCpsMultiplier))).formatted(Formatting.GOLD)));
+ }
+ return true;
+ }
+
+ private static boolean addRabbitStatsToLore(List<Text> lines, int slot) {
+ if (cpsIncreaseFactors.isEmpty()) return false;
+ for (Rabbit rabbit : cpsIncreaseFactors) {
+ if (rabbit.slot == slot) {
+ lines.add(Text.empty()
+ .append(Text.literal("CPS Increase: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(rabbit.cpsIncrease)).formatted(Formatting.GOLD)));
+
+ lines.add(Text.empty()
+ .append(Text.literal("Cost per CPS: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(rabbit.cost / rabbit.cpsIncrease)).formatted(Formatting.GOLD)));
+
+ if (rabbit.slot == bestUpgrade) {
+ if (rabbit.cost <= totalChocolate) {
+ lines.add(Text.literal("Best upgrade").formatted(Formatting.GREEN));
+ } else {
+ lines.add(Text.literal("Best upgrade, can't afford").formatted(Formatting.YELLOW));
+ }
+ } else if (rabbit.slot == bestAffordableUpgrade && rabbit.cost <= totalChocolate) {
+ lines.add(Text.literal("Best upgrade you can afford").formatted(Formatting.GREEN));
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static MutableText formatTime(double seconds) {
+ seconds = Math.ceil(seconds);
+ if (seconds <= 0) return Text.literal("Now").formatted(Formatting.GREEN);
+
+ StringBuilder builder = new StringBuilder();
+ if (seconds >= 86400) {
+ builder.append((int) (seconds / 86400)).append("d ");
+ seconds %= 86400;
+ }
+ if (seconds >= 3600) {
+ builder.append((int) (seconds / 3600)).append("h ");
+ seconds %= 3600;
+ }
+ if (seconds >= 60) {
+ builder.append((int) (seconds / 60)).append("m ");
+ seconds %= 60;
+ }
+ if (seconds >= 1) {
+ builder.append((int) seconds).append("s");
+ }
+ return Text.literal(builder.toString()).formatted(Formatting.GOLD);
}
- return decimal;
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java
index c4fd7d4d..42883c4f 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java
@@ -1,10 +1,17 @@
package de.hysky.skyblocker.skyblock.chocolatefactory;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.arguments.IntegerArgumentType;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.events.SkyblockEvents;
import de.hysky.skyblocker.utils.*;
+import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
import de.hysky.skyblocker.utils.waypoint.Waypoint;
import it.unimi.dsi.fastutil.objects.ObjectImmutableList;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
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;
@@ -13,6 +20,8 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.Entity;
import net.minecraft.entity.decoration.ArmorStandEntity;
import net.minecraft.item.ItemStack;
+import net.minecraft.text.ClickEvent;
+import net.minecraft.text.HoverEvent;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import org.apache.commons.lang3.mutable.MutableObject;
@@ -25,10 +34,9 @@ import java.util.regex.Pattern;
public class EggFinder {
private static final Pattern eggFoundPattern = Pattern.compile("^(?:HOPPITY'S HUNT You found a Chocolate|You have already collected this Chocolate) (Breakfast|Lunch|Dinner)");
- private static final Pattern newEggPattern = Pattern.compile("^HOPPITY'S HUNT A Chocolate (Breakfast|Lunch|Dinner) Egg has appeared!$");
private static final Logger logger = LoggerFactory.getLogger("Skyblocker Egg Finder");
private static final LinkedList<ArmorStandEntity> armorStandQueue = new LinkedList<>();
- private static final Location[] possibleLocations = {Location.CRIMSON_ISLE, Location.CRYSTAL_HOLLOWS, Location.DUNGEON_HUB, Location.DWARVEN_MINES, Location.HUB, Location.THE_END, Location.THE_PARK, Location.GOLD_MINE};
+ private static final Location[] possibleLocations = {Location.CRIMSON_ISLE, Location.CRYSTAL_HOLLOWS, Location.DUNGEON_HUB, Location.DWARVEN_MINES, Location.HUB, Location.THE_END, Location.THE_PARK, Location.GOLD_MINE, Location.DEEP_CAVERNS, Location.SPIDERS_DEN, Location.THE_FARMING_ISLAND};
private static boolean isLocationCorrect = false;
private EggFinder() {
@@ -39,6 +47,17 @@ public class EggFinder {
SkyblockEvents.LOCATION_CHANGE.register(EggFinder::handleLocationChange);
ClientReceiveMessageEvents.GAME.register(EggFinder::onChatMessage);
WorldRenderEvents.AFTER_TRANSLUCENT.register(EggFinder::renderWaypoints);
+ ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal(SkyblockerMod.NAMESPACE)
+ .then(ClientCommandManager.literal("eggFinder")
+ .then(ClientCommandManager.literal("shareLocation")
+ .then(ClientCommandManager.argument("x", IntegerArgumentType.integer())
+ .then(ClientCommandManager.argument("y", IntegerArgumentType.integer())
+ .then(ClientCommandManager.argument("z", IntegerArgumentType.integer())
+ .then(ClientCommandManager.argument("eggType", StringArgumentType.word())
+ .executes(context -> {
+ MessageScheduler.INSTANCE.sendMessageAfterCooldown("[Skyblocker] Chocolate " + context.getArgument("eggType", String.class) + " Egg found at " + context.getArgument("x", Integer.class) + " " + context.getArgument("y", Integer.class) + " " + context.getArgument("z", Integer.class) + "!");
+ return Command.SINGLE_SUCCESS;
+ })))))))));
}
private static void handleLocationChange(Location location) {
@@ -102,7 +121,9 @@ public class EggFinder {
.append("Found a ")
.append(Text.literal("Chocolate " + eggType + " Egg")
.withColor(eggType.color))
- .append(" at " + entity.getBlockPos().up(2).toShortString() + "!"));
+ .append(" at " + entity.getBlockPos().up(2).toShortString() + "!")
+ .styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker eggFinder shareLocation " + entity.getBlockX() + " " + (entity.getBlockY() + 2) + " " + entity.getBlockZ() + " " + eggType))
+ .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal("Click to share the location in chat!").formatted(Formatting.GREEN)))));
}
private static void renderWaypoints(WorldRenderContext context) {
@@ -124,16 +145,6 @@ public class EggFinder {
logger.error("[Skyblocker Egg Finder] Failed to find egg type for egg found message. Tried to match against: " + matcher.group(0), e);
}
}
-
- //There's only one egg of the same type at any given time, so we can set the changed egg to null
- matcher = newEggPattern.matcher(text.getString());
- if (matcher.find()) {
- try {
- EggType.valueOf(matcher.group(1).toUpperCase()).egg.setValue(null);
- } catch (IllegalArgumentException e) {
- logger.error("[Skyblocker Egg Finder] Failed to find egg type for egg spawn message. Tried to match against: " + matcher.group(0), e);
- }
- }
}
record Egg(ArmorStandEntity entity, Waypoint waypoint) { }
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java
index 72cbeb2a..6ed11676 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java
@@ -2,6 +2,7 @@ package de.hysky.skyblocker.skyblock.chocolatefactory;
import com.mojang.brigadier.Message;
import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.events.SkyblockEvents;
import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.Utils;
@@ -49,7 +50,7 @@ public class TimeTowerReminder {
}
try (FileWriter writer = new FileWriter(tempFile)) {
- writer.write(String.valueOf(System.currentTimeMillis()));
+ writer.write(String.valueOf(System.currentTimeMillis())); //Overwrites the file so no need to handle case where the file already exists and has text
} catch (IOException e) {
LOGGER.error("[Skyblocker Time Tower Reminder] Failed to write to temp file for Time Tower Reminder!", e);
}
@@ -57,8 +58,9 @@ public class TimeTowerReminder {
private static void sendMessage() {
if (MinecraftClient.getInstance().player == null || !Utils.isOnSkyblock()) return;
- MinecraftClient.getInstance().player.sendMessage(Constants.PREFIX.get().append(Text.literal("Your Chocolate Factory's Time Tower has deactivated!").formatted(Formatting.RED)));
-
+ if (SkyblockerConfigManager.get().helpers.chocolateFactory.enableTimeTowerReminder) {
+ MinecraftClient.getInstance().player.sendMessage(Constants.PREFIX.get().append(Text.literal("Your Chocolate Factory's Time Tower has deactivated!").formatted(Formatting.RED)));
+ }
File tempFile = SkyblockerMod.CONFIG_DIR.resolve(TIME_TOWER_FILE).toFile();
try {
scheduled = false;
@@ -81,6 +83,9 @@ public class TimeTowerReminder {
}
if (System.currentTimeMillis() - time >= 60 * 60 * 1000) sendMessage();
- else Scheduler.INSTANCE.schedule(TimeTowerReminder::sendMessage, 60 * 60 * 20 - (int) ((System.currentTimeMillis() - time) / 50)); // 50 milliseconds is 1 tick
+ else {
+ Scheduler.INSTANCE.schedule(TimeTowerReminder::sendMessage, 60 * 60 * 20 - (int) ((System.currentTimeMillis() - time) / 50)); // 50 milliseconds is 1 tick
+ scheduled = true;
+ }
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/kuudra/KuudraWaypoints.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/kuudra/KuudraWaypoints.java
index 5b586f71..dbf7409a 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/crimson/kuudra/KuudraWaypoints.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/kuudra/KuudraWaypoints.java
@@ -17,7 +17,6 @@ import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import de.hysky.skyblocker.SkyblockerMod;
-import de.hysky.skyblocker.config.SkyblockerConfig;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.utils.PosUtils;
import de.hysky.skyblocker.utils.Utils;
@@ -53,8 +52,8 @@ public class KuudraWaypoints {
private static boolean loaded;
static void load(MinecraftClient client) {
- CompletableFuture<Void> safeSpots = loadWaypoints(client, new Identifier(SkyblockerMod.NAMESPACE, "crimson/kuudra/safe_spot_waypoints.json"), SAFE_SPOT_WAYPOINTS, SAFE_SPOT_COLOR);
- CompletableFuture<Void> pearls = loadWaypoints(client, new Identifier(SkyblockerMod.NAMESPACE, "crimson/kuudra/pearl_waypoints.json"), PEARL_WAYPOINTS, PEARL_COLOR);
+ CompletableFuture<Void> safeSpots = loadWaypoints(client, Identifier.of(SkyblockerMod.NAMESPACE, "crimson/kuudra/safe_spot_waypoints.json"), SAFE_SPOT_WAYPOINTS, SAFE_SPOT_COLOR);
+ CompletableFuture<Void> pearls = loadWaypoints(client, Identifier.of(SkyblockerMod.NAMESPACE, "crimson/kuudra/pearl_waypoints.json"), PEARL_WAYPOINTS, PEARL_COLOR);
CompletableFuture.allOf(safeSpots, pearls).whenComplete((_result, _throwable) -> loaded = true);
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java
index 70e785cb..0f459c9d 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java
@@ -35,13 +35,13 @@ public class CroesusProfit extends ContainerSolver {
protected List<ColorHighlight> getColors(String[] groups, Int2ObjectMap<ItemStack> slots) {
List<ColorHighlight> highlights = new ArrayList<>();
ItemStack bestChest = null, secondBestChest = null;
- long bestValue = 0, secondBestValue = 0; // If negative value of chest - it is out of the question
- long dungeonKeyPriceData = getItemPrice("DUNGEON_CHEST_KEY") * 2; // lesser ones don't worth the hassle
+ double bestValue = 0, secondBestValue = 0; // If negative value of chest - it is out of the question
+ double dungeonKeyPriceData = getItemPrice("DUNGEON_CHEST_KEY") * 2; // lesser ones don't worth the hassle
for (Int2ObjectMap.Entry<ItemStack> entry : slots.int2ObjectEntrySet()) {
ItemStack stack = entry.getValue();
if (stack.getName().getString().contains("Chest")) {
- long value = valueChest(stack);
+ double value = valueChest(stack);
if (value > bestValue) {
secondBestChest = bestChest;
secondBestValue = bestValue;
@@ -68,8 +68,8 @@ public class CroesusProfit extends ContainerSolver {
}
- private long valueChest(@NotNull ItemStack chest) {
- long chestValue = 0;
+ private double valueChest(@NotNull ItemStack chest) {
+ double chestValue = 0;
int chestPrice = 0;
List<String> chestItems = new ArrayList<>();
@@ -107,22 +107,8 @@ public class CroesusProfit extends ContainerSolver {
}
- private long getItemPrice(String itemDisplayName) {
- JsonObject bazaarPrices = TooltipInfoType.BAZAAR.getData();
- JsonObject lbinPrices = TooltipInfoType.LOWEST_BINS.getData();
- long itemValue = 0;
- String id = dungeonDropsNameToId.get(itemDisplayName);
-
- if (bazaarPrices == null || lbinPrices == null) return 0;
-
- if (bazaarPrices.has(id)) {
- JsonObject item = bazaarPrices.get(id).getAsJsonObject();
- boolean isPriceNull = item.get("sellPrice").isJsonNull();
- return (isPriceNull ? 0L : item.get("sellPrice").getAsLong());
- } else if (lbinPrices.has(id)) {
- return lbinPrices.get(id).getAsLong();
- }
- return itemValue;
+ private double getItemPrice(String itemDisplayName) {
+ return ItemUtils.getItemPrice(dungeonDropsNameToId.get(itemDisplayName)).leftDouble();
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java
index 1e896794..a46daf52 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java
@@ -26,7 +26,7 @@ public class DungeonMap {
private static MapIdComponent cachedMapIdComponent = null;
public static void init() {
- HudRenderEvents.AFTER_MAIN_HUD.register((context, tickDelta) -> render(context));
+ HudRenderEvents.AFTER_MAIN_HUD.register((context, tickCounter) -> render(context));
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal("skyblocker")
.then(ClientCommandManager.literal("hud")
.then(ClientCommandManager.literal("dungeon")
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMapConfigScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMapConfigScreen.java
index 068510c2..bcddbee3 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMapConfigScreen.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMapConfigScreen.java
@@ -15,7 +15,7 @@ public class DungeonMapConfigScreen extends Screen {
private int mapY = SkyblockerConfigManager.get().dungeons.dungeonMap.mapY;
private int scoreX = SkyblockerConfigManager.get().dungeons.dungeonScore.scoreX;
private int scoreY = SkyblockerConfigManager.get().dungeons.dungeonScore.scoreY;
- private static final Identifier MAP_BACKGROUND = new Identifier("textures/map/map_background.png");
+ private static final Identifier MAP_BACKGROUND = Identifier.ofVanilla("textures/map/map_background.png");
private final Screen parent;
protected DungeonMapConfigScreen() {
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/dungeon/DungeonScoreHUD.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScoreHUD.java
index 1896eb53..cd8b9706 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScoreHUD.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScoreHUD.java
@@ -14,7 +14,7 @@ public class DungeonScoreHUD {
}
public static void init() {
- HudRenderEvents.AFTER_MAIN_HUD.register((context, tickDelta) -> render(context));
+ HudRenderEvents.AFTER_MAIN_HUD.register((context, tickCounter) -> render(context));
}
//This is 4+5 wide, needed to offset the extra width from bold numbers (3×1 wide) in S+ and the "+" (6 wide) so that it doesn't go off the screen if the score is S+ and the hud element is at the right edge of the screen
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonTextures.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonTextures.java
index 1d55491f..502efc6f 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonTextures.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonTextures.java
@@ -8,7 +8,7 @@ import net.minecraft.util.Identifier;
public class DungeonTextures {
public static void init() {
ResourceManagerHelper.registerBuiltinResourcePack(
- new Identifier(SkyblockerMod.NAMESPACE, "recolored_dungeon_items"),
+ Identifier.of(SkyblockerMod.NAMESPACE, "recolored_dungeon_items"),
SkyblockerMod.SKYBLOCKER_MOD,
ResourcePackActivationType.NORMAL
);
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/FireFreezeStaffTimer.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/FireFreezeStaffTimer.java
index b2a9b521..3422bc0f 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/FireFreezeStaffTimer.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/FireFreezeStaffTimer.java
@@ -7,6 +7,7 @@ import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.render.RenderTickCounter;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
@@ -19,7 +20,7 @@ public class FireFreezeStaffTimer {
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> FireFreezeStaffTimer.reset());
}
- private static void onDraw(DrawContext context, float v) {
+ private static void onDraw(DrawContext context, RenderTickCounter tickCounter) {
MinecraftClient client = MinecraftClient.getInstance();
if (client.currentScreen != null) return;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/partyfinder/PartyEntry.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/partyfinder/PartyEntry.java
index 588e2431..0b52f1ed 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/partyfinder/PartyEntry.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/partyfinder/PartyEntry.java
@@ -33,8 +33,8 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PartyEntry extends ElementListWidget.Entry<PartyEntry> {
- private static final Identifier PARTY_CARD_TEXTURE = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/party_card.png");
- private static final Identifier PARTY_CARD_TEXTURE_HOVER = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/party_card_hover.png");
+ private static final Identifier PARTY_CARD_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/party_card.png");
+ private static final Identifier PARTY_CARD_TEXTURE_HOVER = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/party_card_hover.png");
public static final Text JOIN_TEXT = Text.translatable("skyblocker.partyFinder.join");
private static final Map<String, ProfileComponent> SKULL_CACHE = new Object2ObjectOpenHashMap<>();
protected final PartyFinderScreen screen;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/partyfinder/PartyFinderScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/partyfinder/PartyFinderScreen.java
index d22084f2..ad002057 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/partyfinder/PartyFinderScreen.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/partyfinder/PartyFinderScreen.java
@@ -37,8 +37,8 @@ import java.util.concurrent.CompletableFuture;
public class PartyFinderScreen extends Screen {
protected static final Logger LOGGER = LoggerFactory.getLogger(PartyFinderScreen.class);
- protected static final Identifier BACKGROUND_TEXTURE = new Identifier("social_interactions/background");
- protected static final Identifier SEARCH_ICON_TEXTURE = new Identifier("icon/search");
+ protected static final Identifier BACKGROUND_TEXTURE = Identifier.ofVanilla("social_interactions/background");
+ protected static final Identifier SEARCH_ICON_TEXTURE = Identifier.ofVanilla("icon/search");
protected static final Text SEARCH_TEXT = Text.translatable("gui.socialInteractions.search_hint").formatted(Formatting.ITALIC).formatted(Formatting.GRAY);
public static boolean isInKuudraPartyFinder = false;
@@ -110,7 +110,7 @@ public class PartyFinderScreen extends Screen {
CompletableFuture.runAsync(() -> {
floorIconsNormal = new HashMap<>();
floorIconsMaster = new HashMap<>();
- try (BufferedReader skullTextureReader = client.getResourceManager().openAsReader(new Identifier(SkyblockerMod.NAMESPACE, "dungeons/catacombs/floorskulls.json"))) {
+ try (BufferedReader skullTextureReader = client.getResourceManager().openAsReader(Identifier.of(SkyblockerMod.NAMESPACE, "dungeons/catacombs/floorskulls.json"))) {
JsonObject json = SkyblockerMod.GSON.fromJson(skullTextureReader, JsonObject.class);
json.getAsJsonObject("normal").asMap().forEach((s, tex) -> floorIconsNormal.put(s, ItemUtils.propertyMapWithTexture(tex.getAsString())));
json.getAsJsonObject("master").asMap().forEach((s, tex) -> floorIconsMaster.put(s, ItemUtils.propertyMapWithTexture(tex.getAsString())));
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java
index 3d9e05ae..75dd7708 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java
@@ -1,6 +1,7 @@
package de.hysky.skyblocker.skyblock.dungeon.puzzle;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.ColorUtils;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.render.RenderHelper;
import it.unimi.dsi.fastutil.objects.ObjectDoublePair;
@@ -28,13 +29,13 @@ public class CreeperBeams extends DungeonPuzzle {
private static final Logger LOGGER = LoggerFactory.getLogger(CreeperBeams.class.getName());
private static final float[][] COLORS = {
- DyeColor.LIGHT_BLUE.getColorComponents(),
- DyeColor.LIME.getColorComponents(),
- DyeColor.YELLOW.getColorComponents(),
- DyeColor.MAGENTA.getColorComponents(),
- DyeColor.PINK.getColorComponents(),
+ ColorUtils.getFloatComponents(DyeColor.LIGHT_BLUE),
+ ColorUtils.getFloatComponents(DyeColor.LIME),
+ ColorUtils.getFloatComponents(DyeColor.YELLOW),
+ ColorUtils.getFloatComponents(DyeColor.MAGENTA),
+ ColorUtils.getFloatComponents(DyeColor.PINK),
};
- private static final float[] GREEN_COLOR_COMPONENTS = DyeColor.GREEN.getColorComponents();
+ private static final float[] GREEN_COLOR_COMPONENTS = ColorUtils.getFloatComponents(DyeColor.GREEN);
private static final int FLOOR_Y = 68;
private static final int BASE_Y = 74;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/IceFill.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/IceFill.java
index 8bc14b52..883a469a 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/IceFill.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/IceFill.java
@@ -7,6 +7,7 @@ import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.debug.Debug;
import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager;
import de.hysky.skyblocker.skyblock.dungeon.secrets.Room;
+import de.hysky.skyblocker.utils.ColorUtils;
import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.render.RenderHelper;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
@@ -27,7 +28,7 @@ import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.lit
public class IceFill extends DungeonPuzzle {
public static final IceFill INSTANCE = new IceFill();
- private static final float[] RED_COLOR_COMPONENTS = DyeColor.RED.getColorComponents();
+ private static final float[] RED_COLOR_COMPONENTS = ColorUtils.getFloatComponents(DyeColor.RED);
private static final BlockPos[] BOARD_ORIGINS = {
new BlockPos(16, 70, 9),
new BlockPos(17, 71, 16),
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Silverfish.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Silverfish.java
index 3b0ee65c..f4376c54 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Silverfish.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Silverfish.java
@@ -6,6 +6,7 @@ import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.debug.Debug;
import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager;
import de.hysky.skyblocker.skyblock.dungeon.secrets.Room;
+import de.hysky.skyblocker.utils.ColorUtils;
import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.render.RenderHelper;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
@@ -19,17 +20,14 @@ import net.minecraft.util.math.Direction;
import net.minecraft.util.math.Vec3d;
import org.joml.Vector2i;
import org.joml.Vector2ic;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import java.util.*;
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
public class Silverfish extends DungeonPuzzle {
- private static final Logger LOGGER = LoggerFactory.getLogger(Silverfish.class);
public static final Silverfish INSTANCE = new Silverfish();
- private static final float[] RED_COLOR_COMPONENTS = DyeColor.RED.getColorComponents();
+ private static final float[] RED_COLOR_COMPONENTS = ColorUtils.getFloatComponents(DyeColor.RED);
final boolean[][] silverfishBoard = new boolean[17][17];
Vector2ic silverfishPos;
final List<Vector2ic> silverfishPath = new ArrayList<>();
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java
index 3c0b0dc9..5a497c19 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java
@@ -55,7 +55,7 @@ public class TicTacToe extends DungeonPuzzle {
char[][] board = new char[3][3];
for (ItemFrameEntity itemFrame : itemFramesThatHoldMaps) {
- MapState mapState = client.world.getMapState(itemFrame.getMapId());
+ MapState mapState = client.world.getMapState(itemFrame.getMapId(itemFrame.getHeldItemStack()));
if (mapState == null) continue;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/boulder/Boulder.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/boulder/Boulder.java
index 68671896..aef9109f 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/boulder/Boulder.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/boulder/Boulder.java
@@ -4,6 +4,7 @@ import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.dungeon.puzzle.DungeonPuzzle;
import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager;
import de.hysky.skyblocker.skyblock.dungeon.secrets.Room;
+import de.hysky.skyblocker.utils.ColorUtils;
import de.hysky.skyblocker.utils.render.RenderHelper;
import de.hysky.skyblocker.utils.render.title.Title;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
@@ -18,17 +19,14 @@ import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.BlockView;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
public class Boulder extends DungeonPuzzle {
- private static final Logger LOGGER = LoggerFactory.getLogger(Boulder.class.getName());
private static final Boulder INSTANCE = new Boulder();
- private static final float[] RED_COLOR_COMPONENTS = DyeColor.RED.getColorComponents();
- private static final float[] ORANGE_COLOR_COMPONENTS = DyeColor.ORANGE.getColorComponents();
+ private static final float[] RED_COLOR_COMPONENTS = ColorUtils.getFloatComponents(DyeColor.RED);
+ private static final float[] ORANGE_COLOR_COMPONENTS = ColorUtils.getFloatComponents(DyeColor.ORANGE);
private static final int BASE_Y = 65;
static Vec3d[] linePoints;
static Box boundingBox;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Waterboard.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Waterboard.java
index 4e8122c3..511827b9 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Waterboard.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Waterboard.java
@@ -11,6 +11,7 @@ import de.hysky.skyblocker.skyblock.dungeon.puzzle.DungeonPuzzle;
import de.hysky.skyblocker.skyblock.dungeon.puzzle.waterboard.Cell.SwitchCell;
import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager;
import de.hysky.skyblocker.skyblock.dungeon.secrets.Room;
+import de.hysky.skyblocker.utils.ColorUtils;
import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.render.RenderHelper;
import de.hysky.skyblocker.utils.scheduler.Scheduler;
@@ -71,7 +72,7 @@ public class Waterboard extends DungeonPuzzle {
new BlockPos(10, 61, 10)
};
public static final BlockPos WATER_LEVER = new BlockPos(15, 60, 5);
- private static final float[] LIME_COLOR_COMPONENTS = DyeColor.LIME.getColorComponents();
+ private static final float[] LIME_COLOR_COMPONENTS = ColorUtils.getFloatComponents(DyeColor.LIME);
private CompletableFuture<Void> solve;
private final Cell[][] cells = new Cell[19][19];
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java
index e4504cc9..b9986731 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java
@@ -295,7 +295,7 @@ public class DungeonManager {
}));
}
dungeonFutures.add(CompletableFuture.runAsync(() -> {
- try (BufferedReader roomsReader = MinecraftClient.getInstance().getResourceManager().openAsReader(new Identifier(SkyblockerMod.NAMESPACE, "dungeons/dungeonrooms.json")); BufferedReader waypointsReader = MinecraftClient.getInstance().getResourceManager().openAsReader(new Identifier(SkyblockerMod.NAMESPACE, "dungeons/secretlocations.json"))) {
+ try (BufferedReader roomsReader = MinecraftClient.getInstance().getResourceManager().openAsReader(Identifier.of(SkyblockerMod.NAMESPACE, "dungeons/dungeonrooms.json")); BufferedReader waypointsReader = MinecraftClient.getInstance().getResourceManager().openAsReader(Identifier.of(SkyblockerMod.NAMESPACE, "dungeons/secretlocations.json"))) {
loadJson(roomsReader, roomsJson);
loadJson(waypointsReader, waypointsJson);
LOGGER.debug("[Skyblocker Dungeon Secrets] Loaded dungeon secret waypoints json");
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java
index e6a8b9d1..c0e54904 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java
@@ -242,7 +242,7 @@ public class Room implements Tickable, Renderable {
protected void removeCustomWaypoint(CommandContext<FabricClientCommandSource> context, BlockPos pos) {
SecretWaypoint waypoint = removeCustomWaypoint(pos);
if (waypoint != null) {
- context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.stringifiedTranslatable("skyblocker.dungeons.secrets.customWaypointRemoved", pos.getX(), pos.getY(), pos.getZ(), name, waypoint.secretIndex, waypoint.category, waypoint.name)));
+ context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.customWaypointRemoved", pos.getX(), pos.getY(), pos.getZ(), name, waypoint.secretIndex, waypoint.category.asString(), waypoint.getName())));
} else {
context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.customWaypointNotFound", pos.getX(), pos.getY(), pos.getZ(), name)));
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java
index 42fe6dbe..3779a66e 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java
@@ -5,10 +5,10 @@ import com.mojang.brigadier.context.CommandContext;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.codecs.RecordCodecBuilder;
-import de.hysky.skyblocker.config.SkyblockerConfig;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.config.configs.DungeonsConfig;
import de.hysky.skyblocker.utils.render.RenderHelper;
+import de.hysky.skyblocker.utils.waypoint.NamedWaypoint;
import de.hysky.skyblocker.utils.waypoint.Waypoint;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
import net.minecraft.client.MinecraftClient;
@@ -29,7 +29,7 @@ import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.ToDoubleFunction;
-public class SecretWaypoint extends Waypoint {
+public class SecretWaypoint extends NamedWaypoint {
private static final Logger LOGGER = LoggerFactory.getLogger(SecretWaypoint.class);
public static final Codec<SecretWaypoint> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.INT.fieldOf("secretIndex").forGetter(secretWaypoint -> secretWaypoint.secretIndex),
@@ -43,8 +43,6 @@ public class SecretWaypoint extends Waypoint {
static final Supplier<Type> TYPE_SUPPLIER = () -> CONFIG.get().waypointType;
final int secretIndex;
final Category category;
- final Text name;
- private final Vec3d centerPos;
SecretWaypoint(int secretIndex, JsonObject waypoint, String name, BlockPos pos) {
this(secretIndex, Category.get(waypoint), name, pos);
@@ -55,11 +53,9 @@ public class SecretWaypoint extends Waypoint {
}
SecretWaypoint(int secretIndex, Category category, Text name, BlockPos pos) {
- super(pos, TYPE_SUPPLIER, category.colorComponents);
+ super(pos, name, TYPE_SUPPLIER, category.colorComponents);
this.secretIndex = secretIndex;
this.category = category;
- this.name = name;
- this.centerPos = pos.toCenterPos();
}
static ToDoubleFunction<SecretWaypoint> getSquaredDistanceToFunction(Entity entity) {
@@ -96,6 +92,11 @@ public class SecretWaypoint extends Waypoint {
return super.equals(obj) || obj instanceof SecretWaypoint other && secretIndex == other.secretIndex && category == other.category && name.equals(other.name) && pos.equals(other.pos);
}
+ @Override
+ protected boolean shouldRenderName() {
+ return CONFIG.get().showSecretText;
+ }
+
/**
* Renders the secret waypoint, including a waypoint through {@link Waypoint#render(WorldRenderContext)}, the name, and the distance from the player.
*/
@@ -106,7 +107,6 @@ public class SecretWaypoint extends Waypoint {
if (CONFIG.get().showSecretText) {
Vec3d posUp = centerPos.add(0, 1, 0);
- RenderHelper.renderText(context, name, posUp, true);
double distance = context.camera().getPos().distanceTo(centerPos);
RenderHelper.renderText(context, Text.literal(Math.round(distance) + "m").formatted(Formatting.YELLOW), posUp, 1, MinecraftClient.getInstance().textRenderer.fontHeight + 1, true);
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/ColorTerminal.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/ColorTerminal.java
index 2abe5183..7a966b48 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/ColorTerminal.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/ColorTerminal.java
@@ -72,7 +72,7 @@ public final class ColorTerminal extends ContainerSolver implements TerminalSolv
itemColor = new HashMap<>();
for (DyeColor color : DyeColor.values())
for (String item : new String[]{"dye", "wool", "stained_glass", "terracotta"})
- itemColor.put(Registries.ITEM.get(new Identifier(color.getName() + '_' + item)), color);
+ itemColor.put(Registries.ITEM.get(Identifier.ofVanilla(color.getName() + '_' + item)), color);
itemColor.put(Items.BONE_MEAL, DyeColor.WHITE);
itemColor.put(Items.LAPIS_LAZULI, DyeColor.BLUE);
itemColor.put(Items.COCOA_BEANS, DyeColor.BROWN);
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsHud.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsHud.java
index 63430489..7f4dbdbf 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsHud.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsHud.java
@@ -21,8 +21,8 @@ import java.util.Map;
public class CrystalsHud {
private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
- protected static final Identifier MAP_TEXTURE = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/crystals_map.png");
- private static final Identifier MAP_ICON = new Identifier("textures/map/decorations/player.png");
+ protected static final Identifier MAP_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/crystals_map.png");
+ private static final Identifier MAP_ICON = Identifier.ofVanilla("textures/map/decorations/player.png");
private static final List<String> SMALL_LOCATIONS = List.of("Fairy Grotto", "King Yolkar", "Corleone", "Odawa", "Key Guardian");
@@ -34,13 +34,13 @@ public class CrystalsHud {
.then(ClientCommandManager.literal("crystals")
.executes(Scheduler.queueOpenScreenCommand(CrystalsHudConfigScreen::new))))));
- HudRenderEvents.AFTER_MAIN_HUD.register((context, tickDelta) -> {
+ HudRenderEvents.AFTER_MAIN_HUD.register((context, tickCounter) -> {
if (!SkyblockerConfigManager.get().mining.crystalsHud.enabled
|| CLIENT.player == null
|| !visible) {
return;
}
- render(context, tickDelta, SkyblockerConfigManager.get().mining.crystalsHud.x,
+ render(context, SkyblockerConfigManager.get().mining.crystalsHud.x,
SkyblockerConfigManager.get().mining.crystalsHud.y);
});
}
@@ -54,11 +54,10 @@ public class CrystalsHud {
* Renders the map to the players UI. renders the background image ({@link CrystalsHud#MAP_TEXTURE}) of the map then if enabled special locations on the map. then finally the player to the map.
*
* @param context DrawContext to draw map to
- * @param tickDelta For interpolating the player's yaw for map marker
* @param hudX Top left X coordinate of the map
* @param hudY Top left Y coordinate of the map
*/
- private static void render(DrawContext context, float tickDelta, int hudX, int hudY) {
+ private static void render(DrawContext context, int hudX, int hudY) {
float scale = SkyblockerConfigManager.get().mining.crystalsHud.mapScaling;
//make sure the map renders infront of some stuff - improve this in the future with better layering (1.20.5?)
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/DwarvenHud.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/DwarvenHud.java
index 2cf0ea9d..16c36dde 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/DwarvenHud.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/DwarvenHud.java
@@ -1,6 +1,7 @@
package de.hysky.skyblocker.skyblock.dwarven;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.config.configs.MiningConfig;
import de.hysky.skyblocker.events.HudRenderEvents;
import de.hysky.skyblocker.mixins.accessors.PlayerListHudAccessor;
import de.hysky.skyblocker.skyblock.tabhud.util.Colors;
@@ -61,7 +62,7 @@ public class DwarvenHud {
)
));
- HudRenderEvents.AFTER_MAIN_HUD.register((context, tickDelta) -> {
+ HudRenderEvents.AFTER_MAIN_HUD.register((context, tickCounter) -> {
if (!SkyblockerConfigManager.get().mining.dwarvenHud.enabledCommissions && !SkyblockerConfigManager.get().mining.dwarvenHud.enabledPowder
|| CLIENT.options.playerListKey.isPressed()
|| CLIENT.player == null
@@ -164,7 +165,9 @@ public class DwarvenHud {
public static void update() {
if (CLIENT.player == null || CLIENT.getNetworkHandler() == null
- || !SkyblockerConfigManager.get().mining.dwarvenHud.enabledCommissions && !SkyblockerConfigManager.get().mining.dwarvenHud.enabledPowder
+ || !SkyblockerConfigManager.get().mining.dwarvenHud.enabledCommissions
+ && !SkyblockerConfigManager.get().mining.dwarvenHud.enabledPowder
+ && SkyblockerConfigManager.get().mining.commissionWaypoints.mode == MiningConfig.CommissionWaypointMode.OFF
|| !Utils.isInCrystalHollows() && !Utils.isInDwarvenMines()) {
return;
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/GlaciteColdOverlay.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/GlaciteColdOverlay.java
index 3839a712..f2911f9a 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/GlaciteColdOverlay.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/GlaciteColdOverlay.java
@@ -13,7 +13,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GlaciteColdOverlay {
- private static final Identifier POWDER_SNOW_OUTLINE = new Identifier("textures/misc/powder_snow_outline.png");
+ private static final Identifier POWDER_SNOW_OUTLINE = Identifier.ofVanilla("textures/misc/powder_snow_outline.png");
private static final Pattern COLD_PATTERN = Pattern.compile("Cold: -(\\d+)❄");
private static int cold = 0;
private static long resetTime = System.currentTimeMillis();
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/MetalDetector.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/MetalDetector.java
index dd97e5d1..294b2c3d 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/MetalDetector.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/MetalDetector.java
@@ -12,16 +12,15 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.decoration.ArmorStandEntity;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
-import net.minecraft.util.Util;
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.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -30,56 +29,56 @@ public class MetalDetector {
private static final float[] LIGHT_GRAY = { 192 / 255f, 192 / 255f, 192 / 255f };
private static final Pattern TREASURE_PATTERN = Pattern.compile("(§3§lTREASURE: §b)(\\d+\\.?\\d?)m");
private static final Pattern KEEPER_PATTERN = Pattern.compile("Keeper of (\\w+)");
- private static final HashMap<String, Vec3i> keeperOffsets = Util.make(new HashMap<>(), map -> {
- map.put("Diamond", new Vec3i(33, 0, 3));
- map.put("Lapis", new Vec3i(-33, 0, -3));
- map.put("Emerald", new Vec3i(-3, 0, 33));
- map.put("Gold", new Vec3i(3, 0, -33));
- });
- private static final HashSet<Vec3i> knownChestOffsets = Util.make(new HashSet<>(), set -> {
- set.add(new Vec3i(-38, -22, 26)); // -38, -22, 26
- set.add(new Vec3i(38, -22, -26)); // 38, -22, -26
- set.add(new Vec3i(-40, -22, 18)); // -40, -22, 18
- set.add(new Vec3i(-41, -20, 22)); // -41, -20, 22
- set.add(new Vec3i(-5, -21, 16)); // -5, -21, 16
- set.add(new Vec3i(40, -22, -30)); // 40, -22, -30
- set.add(new Vec3i(-42, -20, -28)); // -42, -20, -28
- set.add(new Vec3i(-43, -22, -40)); // -43, -22, -40
- set.add(new Vec3i(42, -19, -41)); // 42, -19, -41
- set.add(new Vec3i(43, -21, -16)); // 43, -21, -16
- set.add(new Vec3i(-1, -22, -20)); // -1, -22, -20
- set.add(new Vec3i(6, -21, 28)); // 6, -21, 28
- set.add(new Vec3i(7, -21, 11)); // 7, -21, 11
- set.add(new Vec3i(7, -21, 22)); // 7, -21, 22
- set.add(new Vec3i(-12, -21, -44)); // -12, -21, -44
- set.add(new Vec3i(12, -22, 31)); // 12, -22, 31
- set.add(new Vec3i(12, -22, -22)); // 12, -22, -22
- set.add(new Vec3i(12, -21, 7)); // 12, -21, 7
- set.add(new Vec3i(12, -21, -43)); // 12, -21, -43
- set.add(new Vec3i(-14, -21, 43)); // -14, -21, 43
- set.add(new Vec3i(-14, -21, 22)); // -14, -21, 22
- set.add(new Vec3i(-17, -21, 20)); // -17, -21, 20
- set.add(new Vec3i(-20, -22, 0)); // -20, -22, 0
- set.add(new Vec3i(1, -21, 20)); // 1, -21, 20
- set.add(new Vec3i(19, -22, 29)); // 19, -22, 29
- set.add(new Vec3i(20, -22, 0)); // 20, -22, 0
- set.add(new Vec3i(20, -21, -26)); // 20, -21, -26
- set.add(new Vec3i(-23, -22, 40)); // -23, -22, 40
- set.add(new Vec3i(22, -21, -14)); // 22, -21, -14
- set.add(new Vec3i(-24, -22, 12)); // -24, -22, 12
- set.add(new Vec3i(23, -22, 26)); // 23, -22, 26
- set.add(new Vec3i(23, -22, -39)); // 23, -22, -39
- set.add(new Vec3i(24, -22, 27)); // 24, -22, 27
- set.add(new Vec3i(25, -22, 17)); // 25, -22, 17
- set.add(new Vec3i(29, -21, -44)); // 29, -21, -44
- set.add(new Vec3i(-31, -21, -12)); // -31, -21, -12
- set.add(new Vec3i(-31, -21, -40)); // -31, -21, -40
- set.add(new Vec3i(30, -21, -25)); // 30, -21, -25
- set.add(new Vec3i(-32, -21, -40)); // -32, -21, -40
- set.add(new Vec3i(-36, -20, 42)); // -36, -20, 42
- set.add(new Vec3i(-37, -21, -14)); // -37, -21, -14
- set.add(new Vec3i(-37, -21, -22)); // -37, -21, -22
- });
+ private static final Map<String, Vec3i> keeperOffsets = Map.of(
+ "Diamond", new Vec3i(33, 0, 3),
+ "Lapis", new Vec3i(-33, 0, -3),
+ "Emerald", new Vec3i(-3, 0, 33),
+ "Gold", new Vec3i(3, 0, -33)
+ );
+ private static final Set<Vec3i> knownChestOffsets = Set.of(
+ new Vec3i(-38, -22, 26), // -38, -22, 26
+ new Vec3i(38, -22, -26), // 38, -22, -26
+ new Vec3i(-40, -22, 18), // -40, -22, 18
+ new Vec3i(-41, -20, 22), // -41, -20, 22
+ new Vec3i(-5, -21, 16), // -5, -21, 16
+ new Vec3i(40, -22, -30), // 40, -22, -30
+ new Vec3i(-42, -20, -28), // -42, -20, -28
+ new Vec3i(-43, -22, -40), // -43, -22, -40
+ new Vec3i(42, -19, -41), // 42, -19, -41
+ new Vec3i(43, -21, -16), // 43, -21, -16
+ new Vec3i(-1, -22, -20), // -1, -22, -20
+ new Vec3i(6, -21, 28), // 6, -21, 28
+ new Vec3i(7, -21, 11), // 7, -21, 11
+ new Vec3i(7, -21, 22), // 7, -21, 22
+ new Vec3i(-12, -21, -44), // -12, -21, -44
+ new Vec3i(12, -22, 31), // 12, -22, 31
+ new Vec3i(12, -22, -22), // 12, -22, -22
+ new Vec3i(12, -21, 7), // 12, -21, 7
+ new Vec3i(12, -21, -43), // 12, -21, -43
+ new Vec3i(-14, -21, 43), // -14, -21, 43
+ new Vec3i(-14, -21, 22), // -14, -21, 22
+ new Vec3i(-17, -21, 20), // -17, -21, 20
+ new Vec3i(-20, -22, 0), // -20, -22, 0
+ new Vec3i(1, -21, 20), // 1, -21, 20
+ new Vec3i(19, -22, 29), // 19, -22, 29
+ new Vec3i(20, -22, 0), // 20, -22, 0
+ new Vec3i(20, -21, -26), // 20, -21, -26
+ new Vec3i(-23, -22, 40), // -23, -22, 40
+ new Vec3i(22, -21, -14), // 22, -21, -14
+ new Vec3i(-24, -22, 12), // -24, -22, 12
+ new Vec3i(23, -22, 26), // 23, -22, 26
+ new Vec3i(23, -22, -39), // 23, -22, -39
+ new Vec3i(24, -22, 27), // 24, -22, 27
+ new Vec3i(25, -22, 17), // 25, -22, 17
+ new Vec3i(29, -21, -44), // 29, -21, -44
+ new Vec3i(-31, -21, -12), // -31, -21, -12
+ new Vec3i(-31, -21, -40), // -31, -21, -40
+ new Vec3i(30, -21, -25), // 30, -21, -25
+ new Vec3i(-32, -21, -40), // -32, -21, -40
+ new Vec3i(-36, -20, 42), // -36, -20, 42
+ new Vec3i(-37, -21, -14), // -37, -21, -14
+ new Vec3i(-37, -21, -22) // -37, -21, -22
+ );
protected static Vec3i minesCenter = null;
private static double previousDistance;
@@ -107,7 +106,7 @@ public class MetalDetector {
}
//in the mines of divan
Matcher treasureDistanceMature = TREASURE_PATTERN.matcher(text.getString());
- if (!treasureDistanceMature.matches()) {
+ if (!treasureDistanceMature.find()) {
return;
}
//find new values
@@ -164,7 +163,7 @@ public class MetalDetector {
* Works out the possible locations the treasure could be using the distance the treasure is from the player and
* narrows down possible locations until there is one left.
*
- * @param distance the distance the treasure is from the player squared
+ * @param distance the distance the treasure is from the player squared
* @param playerPos the position of the player
*/
protected static void updatePossibleBlocks(double distance, Vec3d playerPos) {
@@ -232,6 +231,7 @@ public class MetalDetector {
/**
* Renders waypoints for the location of treasure or possible treasure.
+ *
* @param context world render context
*/
private static void render(WorldRenderContext context) {
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/end/EndHudWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/end/EndHudWidget.java
index 53c4336e..86e8d23e 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/end/EndHudWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/end/EndHudWidget.java
@@ -9,6 +9,8 @@ import net.minecraft.component.type.ProfileComponent;
import net.minecraft.enchantment.Enchantments;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
+import net.minecraft.registry.BuiltinRegistries;
+import net.minecraft.registry.RegistryKeys;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
@@ -35,7 +37,7 @@ public class EndHudWidget extends Widget {
static {
ENDERMAN_HEAD.set(DataComponentTypes.PROFILE, new ProfileComponent(Optional.of("MHF_Enderman"), Optional.empty(), new PropertyMap()));
- POPPY.addEnchantment(Enchantments.INFINITY, 1);
+ POPPY.set(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, true);
INSTANCE.setX(SkyblockerConfigManager.get().otherLocations.end.x);
INSTANCE.setY(SkyblockerConfigManager.get().otherLocations.end.y);
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/end/EnderNodes.java b/src/main/java/de/hysky/skyblocker/skyblock/end/EnderNodes.java
index b0282a69..e5a3a9fa 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/end/EnderNodes.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/end/EnderNodes.java
@@ -1,6 +1,7 @@
package de.hysky.skyblocker.skyblock.end;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.ColorUtils;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.scheduler.Scheduler;
import de.hysky.skyblocker.utils.waypoint.Waypoint;
@@ -117,7 +118,7 @@ public class EnderNodes {
private long lastConfirmed;
private EnderNode(BlockPos pos) {
- super(pos, () -> SkyblockerConfigManager.get().uiAndVisuals.waypoints.waypointType, DyeColor.CYAN.getColorComponents(), false);
+ super(pos, () -> SkyblockerConfigManager.get().uiAndVisuals.waypoints.waypointType, ColorUtils.getFloatComponents(DyeColor.CYAN), false);
}
private void updateParticles() {
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/end/TheEnd.java b/src/main/java/de/hysky/skyblocker/skyblock/end/TheEnd.java
index ac961dd8..872d2823 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/end/TheEnd.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/end/TheEnd.java
@@ -5,6 +5,7 @@ import com.google.gson.JsonObject;
import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.events.HudRenderEvents;
+import de.hysky.skyblocker.utils.ColorUtils;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.waypoint.Waypoint;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientChunkEvents;
@@ -73,7 +74,7 @@ public class TheEnd {
});
- HudRenderEvents.AFTER_MAIN_HUD.register((drawContext, tickDelta) -> {
+ HudRenderEvents.AFTER_MAIN_HUD.register((drawContext, tickCounter) -> {
if (!Utils.isInTheEnd()) return;
if (!SkyblockerConfigManager.get().otherLocations.end.hudEnabled) return;
@@ -268,7 +269,7 @@ public class TheEnd {
public record ProtectorLocation(int x, int z, Text name, Waypoint waypoint) {
public ProtectorLocation(int x, int z, Text name) {
- this(x, z, name, new Waypoint(new BlockPos(x, 0, z), Waypoint.Type.WAYPOINT, DyeColor.MAGENTA.getColorComponents()));
+ this(x, z, name, new Waypoint(new BlockPos(x, 0, z), Waypoint.Type.WAYPOINT, ColorUtils.getFloatComponents(DyeColor.MAGENTA)));
}
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java b/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java
new file mode 100644
index 00000000..e846a88a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java
@@ -0,0 +1,185 @@
+package de.hysky.skyblocker.skyblock.events;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.mojang.brigadier.arguments.BoolArgumentType;
+import com.mojang.brigadier.arguments.IntegerArgumentType;
+import com.mojang.logging.LogUtils;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.events.SkyblockEvents;
+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 net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.sound.PositionedSoundInstance;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.sound.SoundEvent;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+
+import java.util.Comparator;
+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();
+
+ private static long currentTime = System.currentTimeMillis() / 1000;
+
+ public static final String JACOBS = "Jacob's Farming Contest";
+
+ public static final IntList DEFAULT_REMINDERS = IntList.of(60, 60 * 5);
+
+ 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);
+
+ SkyblockEvents.JOIN.register(EventNotifications::refreshEvents);
+ ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(
+ ClientCommandManager.literal("skyblocker").then(
+ ClientCommandManager.literal("debug").then(
+ ClientCommandManager.literal("toasts").then(
+ ClientCommandManager.argument("time", IntegerArgumentType.integer(0))
+ .then(ClientCommandManager.argument("jacob", BoolArgumentType.bool()).executes(context -> {
+ long time = System.currentTimeMillis() / 1000 + context.getArgument("time", int.class);
+ if (context.getArgument("jacob", Boolean.class)) {
+ MinecraftClient.getInstance().getToastManager().add(
+ new JacobEventToast(time, "Jacob's farming contest", new String[]{"Cactus", "Cocoa Beans", "Pumpkin"})
+ );
+ } else {
+ MinecraftClient.getInstance().getToastManager().add(
+ new EventToast(time, "Jacob's or something idk", new ItemStack(Items.PAPER))
+ );
+ }
+ return 0;
+ }
+ )
+ )
+ )
+ )
+ )
+ ));
+ }
+
+ private static final Map<String, LinkedList<SkyblockEvent>> events = new ConcurrentHashMap<>();
+
+ public static Map<String, LinkedList<SkyblockEvent>> getEvents() {
+ return events;
+ }
+
+ public static void refreshEvents() {
+ CompletableFuture.supplyAsync(() -> {
+ try {
+ JsonArray jsonElements = SkyblockerMod.GSON.fromJson(Http.sendGetRequest("https://hysky.de/api/calendar"), JsonArray.class);
+ return jsonElements.asList().stream().map(JsonElement::getAsJsonObject).toList();
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Failed to download events list", e);
+ }
+ return List.<JsonObject>of();
+ }).thenAccept(eventsList -> {
+ events.clear();
+ for (JsonObject object : eventsList) {
+ if (object.get("timestamp").getAsLong() + object.get("duration").getAsInt() < currentTime) continue;
+ SkyblockEvent skyblockEvent = SkyblockEvent.of(object);
+ events.computeIfAbsent(object.get("event").getAsString(), s -> new LinkedList<>()).add(skyblockEvent);
+ }
+
+ for (Map.Entry<String, LinkedList<SkyblockEvent>> entry : events.entrySet()) {
+ entry.getValue().sort(Comparator.comparingLong(SkyblockEvent::start)); // Sort just in case it's not in order for some reason in API
+ //LOGGER.info("Next {} is at {}", entry.getKey(), entry.getValue().peekFirst());
+ }
+
+ for (String s : events.keySet()) {
+ SkyblockerConfigManager.get().eventNotifications.eventsReminderTimes.computeIfAbsent(s, s1 -> DEFAULT_REMINDERS);
+ }
+ }).exceptionally(EventNotifications::itBorked);
+ }
+
+ private static Void itBorked(Throwable throwable) {
+ LOGGER.error("[Skyblocker] Event loading borked, sowwy :(", throwable);
+ return null;
+ }
+
+
+ private static void timeUpdate() {
+
+ long newTime = System.currentTimeMillis() / 1000;
+ for (Map.Entry<String, LinkedList<SkyblockEvent>> entry : events.entrySet()) {
+ LinkedList<SkyblockEvent> nextEvents = entry.getValue();
+ SkyblockEvent skyblockEvent = nextEvents.peekFirst();
+ if (skyblockEvent == null) continue;
+
+ // Remove finished event
+ if (newTime > skyblockEvent.start() + skyblockEvent.duration()) {
+ nextEvents.pollFirst();
+ skyblockEvent = nextEvents.peekFirst();
+ if (skyblockEvent == null) continue;
+ }
+ String eventName = entry.getKey();
+ List<Integer> reminderTimes = SkyblockerConfigManager.get().eventNotifications.eventsReminderTimes.getOrDefault(eventName, DEFAULT_REMINDERS);
+ if (reminderTimes.isEmpty()) continue;
+
+ for (Integer reminderTime : reminderTimes) {
+ if (criterionMet() && currentTime + reminderTime < skyblockEvent.start() && newTime + reminderTime >= skyblockEvent.start()) {
+ MinecraftClient instance = MinecraftClient.getInstance();
+ if (eventName.equals(JACOBS)) {
+ instance.getToastManager().add(
+ new JacobEventToast(skyblockEvent.start(), eventName, skyblockEvent.extras())
+ );
+ } else {
+ instance.getToastManager().add(
+ new EventToast(skyblockEvent.start(), eventName, eventIcons.getOrDefault(eventName, new ItemStack(Items.PAPER)))
+ );
+ }
+ SoundEvent soundEvent = SkyblockerConfigManager.get().eventNotifications.reminderSound.getSoundEvent();
+ if (soundEvent != null)
+ instance.getSoundManager().play(PositionedSoundInstance.master(soundEvent, 1f, 1f));
+ break;
+ }
+ }
+ }
+ currentTime = newTime;
+ }
+
+ private static boolean criterionMet() {
+ return switch (SkyblockerConfigManager.get().eventNotifications.criterion) {
+ case NONE -> false;
+ case SKYBLOCK -> Utils.isOnSkyblock();
+ case HYPIXEL -> Utils.isOnHypixel();
+ case EVERYWHERE -> true;
+ };
+ }
+
+ public record SkyblockEvent(long start, int duration, String[] extras, @Nullable String warpCommand) {
+ public static SkyblockEvent of(JsonObject jsonObject) {
+ String location = jsonObject.get("location").getAsString();
+ location = location.isBlank() ? null : location;
+ return new SkyblockEvent(jsonObject.get("timestamp").getAsLong(),
+ jsonObject.get("duration").getAsInt(),
+ jsonObject.get("extras").getAsJsonArray().asList().stream().map(JsonElement::getAsString).toArray(String[]::new),
+ location);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/events/EventToast.java b/src/main/java/de/hysky/skyblocker/skyblock/events/EventToast.java
new file mode 100644
index 00000000..48ea05c4
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/events/EventToast.java
@@ -0,0 +1,93 @@
+package de.hysky.skyblocker.skyblock.events;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.toast.Toast;
+import net.minecraft.client.toast.ToastManager;
+import net.minecraft.item.ItemStack;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.OrderedText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Colors;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.Identifier;
+
+import java.util.List;
+
+public class EventToast implements Toast {
+ protected static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "notification");
+
+ private final long eventStartTime;
+
+ protected final List<OrderedText> message;
+ protected final List<OrderedText> messageNow;
+ protected final int messageWidth;
+ protected final int messageNowWidth;
+ protected final ItemStack icon;
+
+ protected boolean started;
+
+ public EventToast(long eventStartTime, String name, ItemStack icon) {
+ this.eventStartTime = eventStartTime;
+ MutableText formatted = Text.translatable("skyblocker.events.startsSoon", Text.literal(name).formatted(Formatting.YELLOW)).formatted(Formatting.WHITE);
+ TextRenderer renderer = MinecraftClient.getInstance().textRenderer;
+ message = renderer.wrapLines(formatted, 150);
+ messageWidth = message.stream().mapToInt(renderer::getWidth).max().orElse(150);
+
+ MutableText formattedNow = Text.translatable("skyblocker.events.startsNow", Text.literal(name).formatted(Formatting.YELLOW)).formatted(Formatting.WHITE);
+ messageNow = renderer.wrapLines(formattedNow, 150);
+ messageNowWidth = messageNow.stream().mapToInt(renderer::getWidth).max().orElse(150);
+ this.icon = icon;
+ this.started = eventStartTime - System.currentTimeMillis() / 1000 < 0;
+
+ }
+ @Override
+ public Visibility draw(DrawContext context, ToastManager manager, long startTime) {
+ context.drawGuiTexture(TEXTURE, 0, 0, getWidth(), getHeight());
+
+ int y = (getHeight() - getInnerContentsHeight())/2;
+ y = 2 + drawMessage(context, 30, y, Colors.WHITE);
+ drawTimer(context, 30, y);
+
+ context.drawItemWithoutEntity(icon, 8, getHeight()/2 - 8);
+ return startTime > 5_000 ? Visibility.HIDE: Visibility.SHOW;
+ }
+
+ protected int drawMessage(DrawContext context, int x, int y, int color) {
+ TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+ for (OrderedText orderedText : started ? messageNow: message) {
+ context.drawText(textRenderer, orderedText, x, y, color, false);
+ y += textRenderer.fontHeight;
+ }
+ return y;
+ }
+
+ protected void drawTimer(DrawContext context, int x, int y) {
+ long currentTime = System.currentTimeMillis() / 1000;
+ int timeTillEvent = (int) (eventStartTime - currentTime);
+ started = timeTillEvent < 0;
+ if (started) return;
+
+ Text time = Utils.getDurationText(timeTillEvent);
+
+ TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+ context.drawText(textRenderer, time, x, y, Colors.LIGHT_YELLOW, false);
+ }
+
+ @Override
+ public int getWidth() {
+ return (started ? messageNowWidth: messageWidth) + 30 + 6;
+ }
+
+ protected int getInnerContentsHeight() {
+ return message.size() * 9 + (started ? 0 : 9);
+ }
+
+ @Override
+ public int getHeight() {
+ return Math.max(getInnerContentsHeight() + 12 + 2, 32);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/events/JacobEventToast.java b/src/main/java/de/hysky/skyblocker/skyblock/events/JacobEventToast.java
new file mode 100644
index 00000000..43ed7d12
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/events/JacobEventToast.java
@@ -0,0 +1,60 @@
+package de.hysky.skyblocker.skyblock.events;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.JacobsContestWidget;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.toast.ToastManager;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.util.Colors;
+import net.minecraft.util.math.MathHelper;
+
+public class JacobEventToast extends EventToast {
+
+ private final String[] crops;
+
+ private static final ItemStack DEFAULT_ITEM = new ItemStack(Items.IRON_HOE);
+
+ public JacobEventToast(long eventStartTime, String name, String[] crops) {
+ super(eventStartTime, name, new ItemStack(Items.IRON_HOE));
+ this.crops = crops;
+ }
+
+ @Override
+ public Visibility draw(DrawContext context, ToastManager manager, long startTime) {
+ context.drawGuiTexture(TEXTURE, 0, 0, getWidth(), getHeight());
+
+ int y = (getHeight() - getInnerContentsHeight()) / 2;
+ TextRenderer textRenderer = manager.getClient().textRenderer;
+ MatrixStack matrices = context.getMatrices();
+ if (startTime < 3_000) {
+ int k = MathHelper.floor(Math.clamp((3_000 - startTime) / 200.0f, 0.0f, 1.0f) * 255.0f) << 24 | 0x4000000;
+ y = 2 + drawMessage(context, 30, y, 0xFFFFFF | k);
+ } else {
+ int k = (~MathHelper.floor(Math.clamp((startTime - 3_000) / 200.0f, 0.0f, 1.0f) * 255.0f)) << 24 | 0x4000000;
+
+
+ String s = "Crops:";
+ int x = 30 + textRenderer.getWidth(s) + 4;
+ context.drawText(textRenderer, s, 30, 7 + (16 - textRenderer.fontHeight) / 2, Colors.WHITE, false);
+ for (int i = 0; i < crops.length; i++) {
+ context.drawItem(JacobsContestWidget.FARM_DATA.getOrDefault(crops[i], DEFAULT_ITEM), x + i * (16 + 8), 7);
+ }
+ // IDK how to make the items transparent, so I just redraw the texture on top
+ matrices.push();
+ matrices.translate(0, 0, 400f);
+ RenderHelper.renderNineSliceColored(context, TEXTURE, 0, 0, getWidth(), getHeight(), 1f, 1f, 1f, (k >> 24) / 255f);
+ matrices.pop();
+ y += textRenderer.fontHeight * message.size();
+ }
+ matrices.push();
+ matrices.translate(0, 0, 400f);
+ drawTimer(context, 30, y);
+
+ context.drawItemWithoutEntity(icon, 8, getHeight() / 2 - 8);
+ matrices.pop();
+ return startTime > 5_000 ? Visibility.HIDE : Visibility.SHOW;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java
index 72c1370c..c60fdd1a 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java
@@ -52,16 +52,16 @@ public class FancyStatusBars {
}
public static void init() {
- statusBars.put("health", new StatusBar(new Identifier(SkyblockerMod.NAMESPACE, "bars/icons/health"),
+ statusBars.put("health", new StatusBar(Identifier.of(SkyblockerMod.NAMESPACE, "bars/icons/health"),
new Color[]{new Color(255, 0, 0), new Color(255, 220, 0)},
true, new Color(255, 85, 85), Text.translatable("skyblocker.bars.config.health")));
- statusBars.put("intelligence", new StatusBar(new Identifier(SkyblockerMod.NAMESPACE, "bars/icons/intelligence"),
+ statusBars.put("intelligence", new StatusBar(Identifier.of(SkyblockerMod.NAMESPACE, "bars/icons/intelligence"),
new Color[]{new Color(0, 255, 255), new Color(180, 0, 255)},
true, new Color(85, 255, 255), Text.translatable("skyblocker.bars.config.intelligence")));
- statusBars.put("defense", new StatusBar(new Identifier(SkyblockerMod.NAMESPACE, "bars/icons/defense"),
+ statusBars.put("defense", new StatusBar(Identifier.of(SkyblockerMod.NAMESPACE, "bars/icons/defense"),
new Color[]{new Color(255, 255, 255)},
false, new Color(185, 185, 185), Text.translatable("skyblocker.bars.config.defense")));
- statusBars.put("experience", new StatusBar(new Identifier(SkyblockerMod.NAMESPACE, "bars/icons/experience"),
+ statusBars.put("experience", new StatusBar(Identifier.of(SkyblockerMod.NAMESPACE, "bars/icons/experience"),
new Color[]{new Color(100, 230, 70)},
false, new Color(128, 255, 32), Text.translatable("skyblocker.bars.config.experience")));
@@ -292,7 +292,7 @@ public class FancyStatusBars {
Collection<StatusBar> barCollection = statusBars.values();
for (StatusBar statusBar : barCollection) {
- if (statusBar.anchor != null) statusBar.render(context, -1, -1, client.getLastFrameDuration());
+ if (statusBar.anchor != null) statusBar.render(context, -1, -1, client.getRenderTickCounter().getLastFrameDuration());
}
for (StatusBar statusBar : barCollection) {
if (statusBar.anchor != null && statusBar.showText()) statusBar.renderText(context);
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBar.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBar.java
index 467691fd..6392f08a 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBar.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBar.java
@@ -21,8 +21,8 @@ import java.util.function.Consumer;
public class StatusBar implements Widget, Drawable, Element, Selectable {
- private static final Identifier BAR_FILL = new Identifier(SkyblockerMod.NAMESPACE, "bars/bar_fill");
- private static final Identifier BAR_BACK = new Identifier(SkyblockerMod.NAMESPACE, "bars/bar_back");
+ 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");
/* public static final Codec<StatusBar> CODEC = RecordCodecBuilder.create(instance -> instance.group(
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java
index df072710..a412c1c5 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java
@@ -23,7 +23,7 @@ import java.util.HashMap;
import java.util.Map;
public class StatusBarsConfigScreen extends Screen {
- private static final Identifier HOTBAR_TEXTURE = new Identifier("hud/hotbar");
+ private static final Identifier HOTBAR_TEXTURE = Identifier.ofVanilla("hud/hotbar");
public static final long RESIZE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_HRESIZE_CURSOR);
@@ -293,6 +293,7 @@ public class StatusBarsConfigScreen extends Screen {
if (button == 0) {
cursorBar = statusBar;
cursorBar.inMouse = true;
+ currentInsertLocation = BarLocation.of(cursorBar);
if (statusBar.anchor != null)
FancyStatusBars.barPositioner.removeBar(statusBar.anchor, statusBar.gridY, statusBar);
FancyStatusBars.updatePositions();
@@ -322,6 +323,7 @@ public class StatusBarsConfigScreen extends Screen {
public boolean mouseReleased(double mouseX, double mouseY, int button) {
if (cursorBar != null) {
cursorBar.inMouse = false;
+ currentInsertLocation = BarLocation.of(cursorBar);
cursorBar = null;
FancyStatusBars.updatePositions();
checkNullAnchor(FancyStatusBars.statusBars.values());
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHud.java b/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHud.java
index 201e6716..8bed581e 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHud.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHud.java
@@ -16,6 +16,8 @@ import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
import net.fabricmc.fabric.api.event.client.player.ClientPlayerBlockBreakEvents;
import net.minecraft.client.MinecraftClient;
import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtElement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,17 +35,18 @@ import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.lit
public class FarmingHud {
private static final Logger LOGGER = LoggerFactory.getLogger(FarmingHud.class);
public static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US);
- private static final Pattern COUNTER = Pattern.compile("Counter: (?<count>[\\d,]+) .+");
private static final Pattern FARMING_XP = Pattern.compile("§3\\+(?<xp>\\d+.?\\d*) Farming \\((?<percent>[\\d,]+.?\\d*)%\\)");
+ private static final MinecraftClient client = MinecraftClient.getInstance();
+ private static CounterType counterType = CounterType.NONE;
private static final Deque<IntLongPair> counter = new ArrayDeque<>();
private static final LongPriorityQueue blockBreaks = new LongArrayFIFOQueue();
private static final Queue<FloatLongPair> farmingXp = new ArrayDeque<>();
private static float farmingXpPercentProgress;
public static void init() {
- HudRenderEvents.AFTER_MAIN_HUD.register((context, tickDelta) -> {
+ HudRenderEvents.AFTER_MAIN_HUD.register((context, tickCounter) -> {
if (shouldRender()) {
- if (!counter.isEmpty() && counter.peek().rightLong() + 10_000 < System.currentTimeMillis()) {
+ if (!counter.isEmpty() && counter.peek().rightLong() + 5000 < System.currentTimeMillis()) {
counter.poll();
}
if (!blockBreaks.isEmpty() && blockBreaks.firstLong() + 1000 < System.currentTimeMillis()) {
@@ -53,17 +56,9 @@ public class FarmingHud {
farmingXp.poll();
}
- ItemStack stack = MinecraftClient.getInstance().player.getMainHandStack();
- Matcher matcher = ItemUtils.getLoreLineIfMatch(stack, FarmingHud.COUNTER);
- if (matcher != null) {
- try {
- int count = NUMBER_FORMAT.parse(matcher.group("count")).intValue();
- if (counter.isEmpty() || counter.peekLast().leftInt() != count) {
- counter.offer(IntLongPair.of(count, System.currentTimeMillis()));
- }
- } catch (ParseException e) {
- LOGGER.error("[Skyblocker Farming HUD] Failed to parse counter", e);
- }
+ ItemStack stack = client.player.getMainHandStack();
+ if (stack == null || !tryGetCounter(stack, CounterType.CULTIVATING) && !tryGetCounter(stack, CounterType.COUNTER)) {
+ counterType = CounterType.NONE;
}
FarmingHudWidget.INSTANCE.update();
@@ -92,8 +87,26 @@ public class FarmingHud {
.executes(Scheduler.queueOpenScreenCommand(() -> new FarmingHudConfigScreen(null)))))));
}
+ private static boolean tryGetCounter(ItemStack stack, CounterType counterType) {
+ NbtCompound customData = ItemUtils.getCustomData(stack);
+ if (customData == null || !customData.contains(counterType.nbtKey, NbtElement.NUMBER_TYPE)) return false;
+ int count = customData.getInt(counterType.nbtKey);
+ if (FarmingHud.counterType != counterType) {
+ counter.clear();
+ FarmingHud.counterType = counterType;
+ }
+ if (counter.isEmpty() || counter.peekLast().leftInt() != count) {
+ counter.offer(IntLongPair.of(count, System.currentTimeMillis()));
+ }
+ return true;
+ }
+
private static boolean shouldRender() {
- return SkyblockerConfigManager.get().farming.garden.farmingHud.enableHud && Utils.getLocation() == Location.GARDEN;
+ return SkyblockerConfigManager.get().farming.garden.farmingHud.enableHud && client.player != null && Utils.getLocation() == Location.GARDEN;
+ }
+
+ public static String counterText() {
+ return counterType.text;
}
public static int counter() {
@@ -120,4 +133,21 @@ public class FarmingHud {
public static double farmingXpPerHour() {
return farmingXp.stream().mapToDouble(FloatLongPair::leftFloat).sum() * blockBreaks() * 1800; // Hypixel only sends xp updates around every half a second
}
+
+ public enum CounterType {
+ NONE("", "No Counter"),
+ COUNTER("mined_crops", "Counter: "),
+ CULTIVATING("farmed_cultivating", "Cultivating Counter: ");
+
+ private final String nbtKey;
+ private final String text;
+
+ CounterType(String nbtKey, String text) {
+ this.nbtKey = nbtKey;
+ this.text = text;
+ }
+ public boolean matchesText(String textToMatch) {
+ return this.text.equals(textToMatch);
+ }
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java
index f12f6fa8..84013f0b 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java
@@ -1,11 +1,15 @@
package de.hysky.skyblocker.skyblock.garden;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import de.hysky.skyblocker.skyblock.itemlist.ItemRepository;
import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
import de.hysky.skyblocker.skyblock.tabhud.widget.component.ProgressComponent;
import de.hysky.skyblocker.utils.ItemUtils;
+import it.unimi.dsi.fastutil.doubles.DoubleBooleanPair;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.Entity;
import net.minecraft.item.ItemStack;
@@ -18,31 +22,31 @@ import java.util.Map;
public class FarmingHudWidget extends Widget {
private static final MutableText TITLE = Text.literal("Farming").formatted(Formatting.YELLOW, Formatting.BOLD);
- public static final Map<String, ItemStack> FARMING_TOOLS = Map.ofEntries(
- Map.entry("THEORETICAL_HOE_WHEAT_1", Ico.WHEAT),
- Map.entry("THEORETICAL_HOE_WHEAT_2", Ico.WHEAT),
- Map.entry("THEORETICAL_HOE_WHEAT_3", Ico.WHEAT),
- Map.entry("THEORETICAL_HOE_CARROT_1", Ico.CARROT),
- Map.entry("THEORETICAL_HOE_CARROT_2", Ico.CARROT),
- Map.entry("THEORETICAL_HOE_CARROT_3", Ico.CARROT),
- Map.entry("THEORETICAL_HOE_POTATO_1", Ico.POTATO),
- Map.entry("THEORETICAL_HOE_POTATO_2", Ico.POTATO),
- Map.entry("THEORETICAL_HOE_POTATO_3", Ico.POTATO),
- Map.entry("THEORETICAL_HOE_CANE_1", Ico.SUGAR_CANE),
- Map.entry("THEORETICAL_HOE_CANE_2", Ico.SUGAR_CANE),
- Map.entry("THEORETICAL_HOE_CANE_3", Ico.SUGAR_CANE),
- Map.entry("THEORETICAL_HOE_WARTS_1", Ico.NETHER_WART),
- Map.entry("THEORETICAL_HOE_WARTS_2", Ico.NETHER_WART),
- Map.entry("THEORETICAL_HOE_WARTS_3", Ico.NETHER_WART),
- Map.entry("FUNGI_CUTTER", Ico.MUSHROOM),
- Map.entry("CACTUS_KNIFE", Ico.CACTUS),
- Map.entry("MELON_DICER", Ico.MELON),
- Map.entry("MELON_DICER_2", Ico.MELON),
- Map.entry("MELON_DICER_3", Ico.MELON),
- Map.entry("PUMPKIN_DICER", Ico.PUMPKIN),
- Map.entry("PUMPKIN_DICER_2", Ico.PUMPKIN),
- Map.entry("PUMPKIN_DICER_3", Ico.PUMPKIN),
- Map.entry("COCO_CHOPPER", Ico.COCOA_BEANS)
+ public static final Map<String, String> FARMING_TOOLS = Map.ofEntries(
+ Map.entry("THEORETICAL_HOE_WHEAT_1", "WHEAT"),
+ Map.entry("THEORETICAL_HOE_WHEAT_2", "WHEAT"),
+ Map.entry("THEORETICAL_HOE_WHEAT_3", "WHEAT"),
+ Map.entry("THEORETICAL_HOE_CARROT_1", "CARROT_ITEM"),
+ Map.entry("THEORETICAL_HOE_CARROT_2", "CARROT_ITEM"),
+ Map.entry("THEORETICAL_HOE_CARROT_3", "CARROT_ITEM"),
+ Map.entry("THEORETICAL_HOE_POTATO_1", "POTATO_ITEM"),
+ Map.entry("THEORETICAL_HOE_POTATO_2", "POTATO_ITEM"),
+ Map.entry("THEORETICAL_HOE_POTATO_3", "POTATO_ITEM"),
+ Map.entry("THEORETICAL_HOE_CANE_1", "SUGAR_CANE"),
+ Map.entry("THEORETICAL_HOE_CANE_2", "SUGAR_CANE"),
+ Map.entry("THEORETICAL_HOE_CANE_3", "SUGAR_CANE"),
+ Map.entry("THEORETICAL_HOE_WARTS_1", "NETHER_STALK"),
+ Map.entry("THEORETICAL_HOE_WARTS_2", "NETHER_STALK"),
+ Map.entry("THEORETICAL_HOE_WARTS_3", "NETHER_STALK"),
+ Map.entry("FUNGI_CUTTER", "RED_MUSHROOM"),
+ Map.entry("CACTUS_KNIFE", "CACTUS"),
+ Map.entry("MELON_DICER", "MELON"),
+ Map.entry("MELON_DICER_2", "MELON"),
+ Map.entry("MELON_DICER_3", "MELON"),
+ Map.entry("PUMPKIN_DICER", "PUMPKIN"),
+ Map.entry("PUMPKIN_DICER_2", "PUMPKIN"),
+ Map.entry("PUMPKIN_DICER_3", "PUMPKIN"),
+ Map.entry("COCO_CHOPPER", "INK_SACK:3")
);
public static final FarmingHudWidget INSTANCE = new FarmingHudWidget();
private final MinecraftClient client = MinecraftClient.getInstance();
@@ -56,21 +60,45 @@ public class FarmingHudWidget extends Widget {
@Override
public void updateContent() {
- ItemStack icon = client.player == null ? Ico.HOE : FARMING_TOOLS.getOrDefault(ItemUtils.getItemId(client.player.getMainHandStack()), Ico.HOE);
- addSimpleIcoText(icon, "Counter: ", Formatting.YELLOW, FarmingHud.NUMBER_FORMAT.format(FarmingHud.counter()));
- addSimpleIcoText(icon, "Crops/min: ", Formatting.YELLOW, FarmingHud.NUMBER_FORMAT.format((int) FarmingHud.cropsPerMinute() / 100 * 100));
- addSimpleIcoText(icon, "Blocks/s: ", Formatting.YELLOW, Integer.toString(FarmingHud.blockBreaks()));
+ if (client.player == null) return;
+ ItemStack farmingToolStack = client.player.getMainHandStack();
+ if (farmingToolStack == null) return;
+ String cropItemId = FARMING_TOOLS.get(ItemUtils.getItemId(farmingToolStack));
+ ItemStack cropStack = ItemRepository.getItemStack(ItemTooltip.getNeuName(cropItemId, cropItemId)); // The cropItemId is being used as the api id in the second parameter because the skyblock id and api id are the same for all crops.
+
+ String counterText = FarmingHud.counterText();
+ String counterNumber = FarmingHud.NUMBER_FORMAT.format(FarmingHud.counter());
+ if (FarmingHud.CounterType.NONE.matchesText(counterText)) counterNumber = "";
+ addSimpleIcoText(cropStack, counterText, Formatting.YELLOW, counterNumber);
+ float cropsPerMinute = FarmingHud.cropsPerMinute();
+ addSimpleIcoText(cropStack, "Crops/min: ", Formatting.YELLOW, FarmingHud.NUMBER_FORMAT.format((int) cropsPerMinute / 10 * 10));
+ addSimpleIcoText(Ico.GOLD, "Coins/h: ", Formatting.GOLD, getPriceText(cropItemId, cropsPerMinute));
+ addSimpleIcoText(cropStack, "Blocks/s: ", Formatting.YELLOW, Integer.toString(FarmingHud.blockBreaks()));
//noinspection DataFlowIssue
addComponent(new ProgressComponent(Ico.LANTERN, Text.literal("Farming Level: "), FarmingHud.farmingXpPercentProgress(), Formatting.GOLD.getColorValue()));
addSimpleIcoText(Ico.LIME_DYE, "Farming XP/h: ", Formatting.YELLOW, FarmingHud.NUMBER_FORMAT.format((int) FarmingHud.farmingXpPerHour()));
Entity cameraEntity = client.getCameraEntity();
- double yaw = cameraEntity == null ? 0.0d : cameraEntity.getYaw();
- double pitch = cameraEntity == null ? 0.0d : cameraEntity.getPitch();
- addComponent(new PlainTextComponent(Text.literal("Yaw: " + String.format("%.3f", MathHelper.wrapDegrees(yaw))).formatted(Formatting.YELLOW)));
- addComponent(new PlainTextComponent(Text.literal("Pitch: " + String.format("%.3f", MathHelper.wrapDegrees(pitch))).formatted(Formatting.YELLOW)));
+ String yaw = cameraEntity == null ? "No Camera Entity" : String.format("%.2f", MathHelper.wrapDegrees(cameraEntity.getYaw()));
+ String pitch = cameraEntity == null ? "No Camera Entity" : String.format("%.2f", MathHelper.wrapDegrees(cameraEntity.getPitch()));
+ addComponent(new PlainTextComponent(Text.literal("Yaw: " + yaw).formatted(Formatting.GOLD)));
+ addComponent(new PlainTextComponent(Text.literal("Pitch: " + pitch).formatted(Formatting.GOLD)));
if (LowerSensitivity.isSensitivityLowered()) {
addComponent(new PlainTextComponent(Text.translatable("skyblocker.garden.hud.mouseLocked").formatted(Formatting.ITALIC)));
}
}
+
+ /**
+ * Gets the price text of the given crop id, calculated with the given crops per minute and the npc price if it's higher than the bazaar sell price, or the bazaar sell price otherwise.
+ */
+ private String getPriceText(String cropItemId, float cropsPerMinute) {
+ DoubleBooleanPair itemBazaarPrice = ItemUtils.getItemPrice(cropItemId); // Gets the bazaar sell price of the crop.
+ double itemNpcPrice = TooltipInfoType.NPC.hasOrNullWarning(cropItemId) ? TooltipInfoType.NPC.getData().get(cropItemId).getAsDouble() : Double.MIN_VALUE; // Gets the npc sell price of the crop or set to the min double value if it doesn't exist.
+ boolean shouldUseNpcPrice = itemNpcPrice > itemBazaarPrice.leftDouble(); // Use the npc price if it's more than the bazaar sell price.
+ double price = shouldUseNpcPrice ? itemNpcPrice : itemBazaarPrice.leftDouble(); // same as above
+
+ // Return the formatted price if npc price is higher or bazaar price is present.
+ // Multiply by 60 to convert to hourly and divide by 100 for rounding is combined into multiplying by 0.6.
+ return shouldUseNpcPrice || itemBazaarPrice.rightBoolean() ? FarmingHud.NUMBER_FORMAT.format((int) (price * cropsPerMinute * 0.6) * 100) : "No Data";
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/garden/LowerSensitivity.java b/src/main/java/de/hysky/skyblocker/skyblock/garden/LowerSensitivity.java
index ee857618..2bfb7fb2 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/garden/LowerSensitivity.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/LowerSensitivity.java
@@ -9,28 +9,23 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.item.ItemStack;
public class LowerSensitivity {
-
private static boolean sensitivityLowered = false;
public static void init() {
ClientTickEvents.END_WORLD_TICK.register(world -> {
- if (!Utils.isOnSkyblock() || Utils.getLocation() != Location.GARDEN || MinecraftClient.getInstance().player == null) {
+ if (Utils.getLocation() != Location.GARDEN || MinecraftClient.getInstance().player == null || !SkyblockerConfigManager.get().farming.garden.lockMouseTool) {
if (sensitivityLowered) lowerSensitivity(false);
return;
}
- if (SkyblockerConfigManager.get().farming.garden.lockMouseTool) {
- ItemStack mainHandStack = MinecraftClient.getInstance().player.getMainHandStack();
- String itemId = ItemUtils.getItemId(mainHandStack);
- boolean shouldLockMouse = FarmingHudWidget.FARMING_TOOLS.containsKey(itemId) && (!SkyblockerConfigManager.get().farming.garden.lockMouseGroundOnly || MinecraftClient.getInstance().player.isOnGround());
- if (shouldLockMouse && !sensitivityLowered) lowerSensitivity(true);
- else if (!shouldLockMouse && sensitivityLowered) lowerSensitivity(false);
-
- }
+ ItemStack mainHandStack = MinecraftClient.getInstance().player.getMainHandStack();
+ String itemId = ItemUtils.getItemId(mainHandStack);
+ boolean shouldLockMouse = FarmingHudWidget.FARMING_TOOLS.containsKey(itemId) && (!SkyblockerConfigManager.get().farming.garden.lockMouseGroundOnly || MinecraftClient.getInstance().player.isOnGround());
+ if (shouldLockMouse && !sensitivityLowered) lowerSensitivity(true);
+ else if (!shouldLockMouse && sensitivityLowered) lowerSensitivity(false);
});
}
public static void lowerSensitivity(boolean lowerSensitivity) {
- if (sensitivityLowered == lowerSensitivity) return;
sensitivityLowered = lowerSensitivity;
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java
index 682933f4..d8f4dad7 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java
@@ -2,6 +2,7 @@ package de.hysky.skyblocker.skyblock.garden;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.itemlist.ItemRepository;
+import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.NEURepoManager;
import de.hysky.skyblocker.utils.Utils;
@@ -12,6 +13,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair;
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
@@ -40,7 +42,8 @@ public class VisitorHelper {
private static final Map<String, ItemStack> itemCache = new HashMap<>();
private static final int TEXT_START_X = 4;
private static final int TEXT_START_Y = 4;
- private static final int TEXT_INDENT = 8;
+ private static final int ENTRY_INDENT = 8;
+ private static final int ITEM_INDENT = 20;
private static final int LINE_SPACING = 3;
public static void init() {
@@ -59,22 +62,32 @@ public class VisitorHelper {
public static void onMouseClicked(double mouseX, double mouseY, int mouseButton, TextRenderer textRenderer) {
int yPosition = TEXT_START_Y;
-
for (Map.Entry<Pair<String, String>, Object2IntMap<String>> visitorEntry : itemMap.entrySet()) {
- int textWidth;
- int textHeight = textRenderer.fontHeight;
-
- yPosition += LINE_SPACING + textHeight;
+ yPosition += LINE_SPACING + textRenderer.fontHeight;
for (Object2IntMap.Entry<String> itemEntry : visitorEntry.getValue().object2IntEntrySet()) {
String itemText = itemEntry.getKey();
- textWidth = textRenderer.getWidth(itemText + " x" + itemEntry.getIntValue());
+ int textWidth = textRenderer.getWidth(itemText + " x" + itemEntry.getIntValue());
- if (isMouseOverText(mouseX, mouseY, TEXT_START_X + TEXT_INDENT, yPosition, textWidth, textHeight)) {
+ // Check if the mouse is over the item text
+ // The text starts at `TEXT_START_X + ENTRY_INDENT + ITEM_INDENT`
+ if (isMouseOverText(mouseX, mouseY, TEXT_START_X + ENTRY_INDENT + ITEM_INDENT, yPosition, textWidth, textRenderer.fontHeight)) {
+ // Send command to buy the item from the bazaar
MessageScheduler.INSTANCE.sendMessageAfterCooldown("/bz " + itemText);
return;
}
- yPosition += LINE_SPACING + textHeight;
+
+ // Check if the mouse is over the copy amount text
+ // The copy amount text starts at `TEXT_START_X + ENTRY_INDENT + ITEM_INDENT + textWidth`
+ MinecraftClient client = MinecraftClient.getInstance();
+ if (client.player != null && isMouseOverText(mouseX, mouseY, TEXT_START_X + ENTRY_INDENT + ITEM_INDENT + textWidth, yPosition, textRenderer.getWidth(" [Copy Amount]"), textRenderer.fontHeight)) {
+ // Copy the amount to the clipboard
+ client.keyboard.setClipboard(String.valueOf(itemEntry.getIntValue()));
+ client.player.sendMessage(Constants.PREFIX.get().append("Copied amount successfully"), false);
+ return;
+ }
+
+ yPosition += LINE_SPACING + textRenderer.fontHeight;
}
}
}
@@ -112,7 +125,7 @@ public class VisitorHelper {
}
}
- private static void updateItemMap(String visitorName, @Nullable String visitorTexture, Text lore) {
+ private static void updateItemMap(String visitorName, @Nullable String visitorTexture, Text lore) {
String[] splitItemText = lore.getString().split(" x");
String itemName = splitItemText[0].trim();
if (itemName.isEmpty()) return;
@@ -168,12 +181,23 @@ public class VisitorHelper {
return stack;
}
- private static void drawItemEntryWithHover(DrawContext context, TextRenderer textRenderer, @Nullable ItemStack stack, String itemName, int amount, int index, int mouseX, int mousseY) {
- Text text = stack != null ? stack.getName().copy().append(" x" + amount) : Text.literal(itemName + " x" + amount);
- drawTextWithOptionalUnderline(context, textRenderer, text, TEXT_START_X + TEXT_INDENT, TEXT_START_Y + (index * (LINE_SPACING + textRenderer.fontHeight)), mouseX, mousseY);
+ /**
+ * Draws the item entry, amount, and copy amount text with optional underline and the item icon
+ */
+ private static void drawItemEntryWithHover(DrawContext context, TextRenderer textRenderer, @Nullable ItemStack stack, String itemName, int amount, int index, int mouseX, int mouseY) {
+ Text text = stack != null ? stack.getName().copy().append(" x" + amount) : Text.literal(itemName + " x" + amount);
+ Text copyAmount = Text.literal(" [Copy Amount]");
+
+ // Calculate the y position of the text with index as the line number
+ int y = TEXT_START_Y + index * (LINE_SPACING + textRenderer.fontHeight);
+ // Draw the item and amount text
+ drawTextWithOptionalUnderline(context, textRenderer, text, TEXT_START_X + ENTRY_INDENT + ITEM_INDENT, y, mouseX, mouseY);
+ // Draw the copy amount text separately after the item and amount text
+ drawTextWithOptionalUnderline(context, textRenderer, copyAmount, TEXT_START_X + ENTRY_INDENT + ITEM_INDENT + textRenderer.getWidth(text), y, mouseX, mouseY);
+
// drawItem adds 150 to the z, which puts our z at 350, above the item in the slot (250) and their text (300) and below the cursor stack (382) and their text (432)
if (stack != null) {
- context.drawItem(stack, TEXT_START_X + TEXT_INDENT + 2 + textRenderer.getWidth(text), TEXT_START_Y + (index * (LINE_SPACING + textRenderer.fontHeight)) - textRenderer.fontHeight + 5);
+ context.drawItem(stack, TEXT_START_X + ENTRY_INDENT, y - textRenderer.fontHeight + 5);
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java
index 65e1b138..6170eab1 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java
@@ -47,14 +47,14 @@ public class CustomArmorTrims {
}
private static void initializeTrimCache() {
- ClientPlayerEntity player = MinecraftClient.getInstance().player;
+ MinecraftClient client = MinecraftClient.getInstance();
FabricLoader loader = FabricLoader.getInstance();
- if (trimsInitialized || (player == null && !Debug.debugEnabled())) {
+ if (trimsInitialized || (client == null && !Debug.debugEnabled())) {
return;
}
try {
TRIMS_CACHE.clear();
- RegistryWrapper.WrapperLookup wrapperLookup = getWrapperLookup(loader, player);
+ RegistryWrapper.WrapperLookup wrapperLookup = getWrapperLookup(loader, client);
for (Reference<ArmorTrimMaterial> material : wrapperLookup.getWrapperOrThrow(RegistryKeys.TRIM_MATERIAL).streamEntries().toList()) {
for (Reference<ArmorTrimPattern> pattern : wrapperLookup.getWrapperOrThrow(RegistryKeys.TRIM_PATTERN).streamEntries().toList()) {
ArmorTrim trim = new ArmorTrim(material, pattern);
@@ -70,8 +70,8 @@ public class CustomArmorTrims {
}
}
- private static RegistryWrapper.WrapperLookup getWrapperLookup(FabricLoader loader, ClientPlayerEntity player) {
- return !Debug.debugEnabled() ? player.networkHandler.getRegistryManager() : BuiltinRegistries.createWrapperLookup();
+ private static RegistryWrapper.WrapperLookup getWrapperLookup(FabricLoader loader, MinecraftClient client) {
+ return client != null && client.getNetworkHandler() != null && client.getNetworkHandler().getRegistryManager() != null ? client.getNetworkHandler().getRegistryManager() : BuiltinRegistries.createWrapperLookup();
}
private static void registerCommand(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
@@ -140,8 +140,8 @@ public class CustomArmorTrims {
public record ArmorTrimId(@SerialEntry Identifier material, @SerialEntry Identifier pattern) implements Pair<Identifier, Identifier> {
public static final Codec<ArmorTrimId> CODEC = RecordCodecBuilder.create(instance -> instance.group(
- Identifier.CODEC.fieldOf("material").forGetter(ArmorTrimId::material),
- Identifier.CODEC.fieldOf("pattern").forGetter(ArmorTrimId::pattern))
+ Identifier.CODEC.fieldOf("material").forGetter(ArmorTrimId::material),
+ Identifier.CODEC.fieldOf("pattern").forGetter(ArmorTrimId::pattern))
.apply(instance, ArmorTrimId::new));
@Override
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java
index 96c21d22..93d29714 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java
@@ -1,9 +1,9 @@
package de.hysky.skyblocker.skyblock.item;
-import com.google.gson.JsonElement;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.PetCache;
+import de.hysky.skyblocker.skyblock.PetCache.PetInfo;
import de.hysky.skyblocker.utils.ItemUtils;
-import de.hysky.skyblocker.utils.ProfileUtils;
import net.fabricmc.fabric.api.event.client.player.ClientPlayerBlockBreakEvents;
import net.fabricmc.fabric.api.event.player.UseItemCallback;
import net.minecraft.block.BlockState;
@@ -36,8 +36,8 @@ public class ItemCooldowns {
561700, 611700, 666700, 726700, 791700, 861700, 936700, 1016700, 1101700, 1191700,
1286700, 1386700, 1496700, 1616700, 1746700, 1886700
};
- public static int monkeyLevel = 1;
- public static double monkeyExp = 0;
+ private static int monkeyLevel = 1;
+ private static double monkeyExp = 0;
public static void init() {
ClientPlayerBlockBreakEvents.AFTER.register(ItemCooldowns::afterBlockBreak);
@@ -45,30 +45,24 @@ public class ItemCooldowns {
}
public static void updateCooldown() {
- ProfileUtils.updateProfile().thenAccept(player -> {
- for (JsonElement pet : player.getAsJsonObject("pets_data").getAsJsonArray("pets")) {
- if (!pet.getAsJsonObject().get("type").getAsString().equals("MONKEY")) continue;
- if (!pet.getAsJsonObject().get("active").getAsString().equals("true")) continue;
- if (pet.getAsJsonObject().get("tier").getAsString().equals("LEGENDARY")) {
- monkeyExp = Double.parseDouble(pet.getAsJsonObject().get("exp").getAsString());
- monkeyLevel = 0;
- for (int xpLevel : EXPERIENCE_LEVELS) {
- if (monkeyExp < xpLevel) {
- break;
- } else {
- monkeyExp -= xpLevel;
- monkeyLevel++;
- }
- }
+ PetInfo pet = PetCache.getCurrentPet();
+
+ if (pet != null && pet.tier().equals("LEGENDARY")) {
+ monkeyExp = pet.exp();
+
+ monkeyLevel = 0;
+ for (int xpLevel : EXPERIENCE_LEVELS) {
+ if (monkeyExp < xpLevel) {
+ break;
+ } else {
+ monkeyExp -= xpLevel;
+ monkeyLevel++;
}
}
- }).exceptionally(e -> {
- ProfileUtils.LOGGER.error("[Skyblocker Item Cooldown] Failed to get Player Pet Data, is the API Down/Limited?", e);
- return null;
- });
+ }
}
- private static int getCooldown() {
+ private static int getCooldown4Foraging() {
int baseCooldown = 2000;
int monkeyPetCooldownReduction = baseCooldown * monkeyLevel / 200;
return baseCooldown - monkeyPetCooldownReduction;
@@ -82,7 +76,7 @@ public class ItemCooldowns {
if (usedItemId.equals(JUNGLE_AXE_ID) || usedItemId.equals(TREECAPITATOR_ID)) {
updateCooldown();
if (!isOnCooldown(JUNGLE_AXE_ID) || !isOnCooldown(TREECAPITATOR_ID)) {
- ITEM_COOLDOWNS.put(usedItemId, new CooldownEntry(getCooldown()));
+ ITEM_COOLDOWNS.put(usedItemId, new CooldownEntry(getCooldown4Foraging()));
}
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java b/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java
index c78724ca..214ecc84 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java
@@ -1,22 +1,35 @@
package de.hysky.skyblocker.skyblock.item;
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
+
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.codecs.RecordCodecBuilder;
-import com.mojang.util.UndashedUuid;
import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.Http;
import de.hysky.skyblocker.utils.Http.ApiResponse;
+import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.minecraft.client.MinecraftClient;
+import net.minecraft.command.CommandRegistryAccess;
+import net.minecraft.item.ItemStack;
import net.minecraft.nbt.*;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.collection.DefaultedList;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -30,17 +43,36 @@ 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);
private static final Path CACHE_FILE = SkyblockerMod.CONFIG_DIR.resolve("museum_item_cache.json");
private static final Map<String, Object2ObjectOpenHashMap<String, ProfileMuseumData>> MUSEUM_ITEM_CACHE = new Object2ObjectOpenHashMap<>();
private static final String ERROR_LOG_TEMPLATE = "[Skyblocker] Failed to refresh museum item data for profile {}";
+ public static final String DONATION_CONFIRMATION_SCREEN_TITLE = "Confirm Donation";
+ private static final int CONFIRM_DONATION_BUTTON_SLOT = 20;
private static CompletableFuture<Void> loaded;
public static void init() {
ClientLifecycleEvents.CLIENT_STARTED.register(MuseumItemCache::load);
+ ClientCommandRegistrationCallback.EVENT.register(MuseumItemCache::registerCommands);
+ }
+
+ private static void registerCommands(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+ dispatcher.register(literal(SkyblockerMod.NAMESPACE)
+ .then(literal("museum")
+ .then(literal("resync")
+ .executes(context -> {
+ if (tryResync(context.getSource())) {
+ context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.museum.attemptingResync")));
+ } else {
+ context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.museum.cannotResync")));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }))));
}
private static void load(MinecraftClient client) {
@@ -67,7 +99,35 @@ public class MuseumItemCache {
});
}
+ public static void handleClick(Slot slot, int slotId, DefaultedList<Slot> slots) {
+ if (slotId == CONFIRM_DONATION_BUTTON_SLOT) {
+ //Slots 0 to 17 can have items, well not all but thats the general range
+ for (int i = 0; i < 17; i++) {
+ ItemStack stack = slots.get(i).getStack();
+
+ if (!stack.isEmpty()) {
+ String itemId = ItemUtils.getItemId(stack);
+ String profileId = Utils.getProfileId();
+
+ if (!itemId.isEmpty() && !profileId.isEmpty()) {
+ String uuid = Utils.getUndashedUuid();
+ //Be safe about access to avoid NPEs
+ Map<String, ProfileMuseumData> playerData = MUSEUM_ITEM_CACHE.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>());
+ playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY.get());
+
+ playerData.get(profileId).collectedItemIds().add(itemId);
+ save();
+ }
+ }
+ }
+ }
+ }
+
private static void updateData4ProfileMember(String uuid, String profileId) {
+ updateData4ProfileMember(uuid, profileId, null);
+ }
+
+ private static void updateData4ProfileMember(String uuid, String profileId, FabricClientCommandSource source) {
CompletableFuture.runAsync(() -> {
try (ApiResponse response = Http.sendHypixelRequest("skyblock/museum", "?profile=" + profileId)) {
//The request was successful
@@ -103,58 +163,85 @@ public class MuseumItemCache {
MUSEUM_ITEM_CACHE.get(uuid).put(profileId, new ProfileMuseumData(System.currentTimeMillis(), itemIds));
save();
+ if (source != null) source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.museum.resyncSuccess")));
+
LOGGER.info("[Skyblocker] Successfully updated museum item cache for profile {}", profileId);
} else {
//If the player's Museum API is disabled
putEmpty(uuid, profileId);
+ if (source != null) source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.museum.resyncFailure")));
+
LOGGER.warn(ERROR_LOG_TEMPLATE + " because the Museum API is disabled!", profileId);
}
} else {
//If the request returns a non 200 status code
putEmpty(uuid, profileId);
+ if (source != null) source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.museum.resyncFailure")));
+
LOGGER.error(ERROR_LOG_TEMPLATE + " because a non 200 status code was encountered! Status Code: {}", profileId, response.statusCode());
}
} catch (Exception e) {
//If an exception was somehow thrown
putEmpty(uuid, profileId);
+ if (source != null) source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.museum.resyncFailure")));
+
LOGGER.error(ERROR_LOG_TEMPLATE, profileId, e);
}
});
}
private static void putEmpty(String uuid, String profileId) {
- MUSEUM_ITEM_CACHE.get(uuid).put(profileId, new ProfileMuseumData(System.currentTimeMillis(), ObjectOpenHashSet.of()));
+ //Only put new data if they didn't have any before
+ if (!MUSEUM_ITEM_CACHE.get(uuid).containsKey(profileId)) {
+ MUSEUM_ITEM_CACHE.get(uuid).put(profileId, new ProfileMuseumData(System.currentTimeMillis(), ObjectOpenHashSet.of()));
+ }
+
save();
}
+ private static boolean tryResync(FabricClientCommandSource source) {
+ String uuid = Utils.getUndashedUuid();
+ String profileId = Utils.getProfileId();
+
+ //Only allow resyncing if the data is actually present yet, otherwise the player needs to swap servers for the tick method to be called
+ if (loaded.isDone() && !profileId.isEmpty() && MUSEUM_ITEM_CACHE.containsKey(uuid) && MUSEUM_ITEM_CACHE.get(uuid).containsKey(profileId) && MUSEUM_ITEM_CACHE.get(uuid).get(profileId).canResync()) {
+ updateData4ProfileMember(uuid, profileId, source);
+
+ return true;
+ }
+
+ return false;
+ }
+
/**
- * The cache is ticked upon switching skyblock servers
+ * The cache is ticked upon switching Skyblock servers. Only loads from the API if the profile wasn't cached yet.
*/
public static void tick(String profileId) {
- if (loaded.isDone()) {
- String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull());
+ String uuid = Utils.getUndashedUuid();
+
+ if (loaded.isDone() && (!MUSEUM_ITEM_CACHE.containsKey(uuid) || !MUSEUM_ITEM_CACHE.getOrDefault(uuid, new Object2ObjectOpenHashMap<>()).containsKey(profileId))) {
Map<String, ProfileMuseumData> playerData = MUSEUM_ITEM_CACHE.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>());
- playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY);
+ playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY.get());
- if (playerData.get(profileId).stale()) updateData4ProfileMember(uuid, profileId);
+ updateData4ProfileMember(uuid, profileId);
}
}
public static boolean hasItemInMuseum(String id) {
- String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull());
+ String uuid = Utils.getUndashedUuid();
ObjectOpenHashSet<String> collectedItemIds = (!MUSEUM_ITEM_CACHE.containsKey(uuid) || Utils.getProfileId().isBlank() || !MUSEUM_ITEM_CACHE.get(uuid).containsKey(Utils.getProfileId())) ? null : MUSEUM_ITEM_CACHE.get(uuid).get(Utils.getProfileId()).collectedItemIds();
return collectedItemIds != null && collectedItemIds.contains(id);
}
- private record ProfileMuseumData(long lastUpdated, ObjectOpenHashSet<String> collectedItemIds) {
- private static final ProfileMuseumData EMPTY = new ProfileMuseumData(0L, null);
- private static final long MAX_AGE = 86_400_000;
+ private record ProfileMuseumData(long lastResync, ObjectOpenHashSet<String> collectedItemIds) {
+ 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("lastUpdated").forGetter(ProfileMuseumData::lastUpdated),
+ Codec.LONG.fieldOf("lastResync").forGetter(ProfileMuseumData::lastResync),
Codec.STRING.listOf()
.xmap(ObjectOpenHashSet::new, ObjectArrayList::new)
.fieldOf("collectedItemIds")
@@ -165,8 +252,8 @@ public class MuseumItemCache {
.xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new)
).xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new);
- private boolean stale() {
- return System.currentTimeMillis() > lastUpdated + MAX_AGE;
+ private boolean canResync() {
+ return this.lastResync + TIME_BETWEEN_RESYNCING_ALLOWED < System.currentTimeMillis();
}
}
-} \ No newline at end of file
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockCraftingTableScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockCraftingTableScreen.java
index a09b260a..e7cc61ae 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockCraftingTableScreen.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockCraftingTableScreen.java
@@ -15,10 +15,12 @@ import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.inventory.SimpleInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
+import net.minecraft.recipe.CraftingRecipe;
import net.minecraft.recipe.Recipe;
import net.minecraft.recipe.RecipeEntry;
import net.minecraft.recipe.RecipeMatcher;
import net.minecraft.recipe.book.RecipeBookCategory;
+import net.minecraft.recipe.input.CraftingRecipeInput;
import net.minecraft.screen.AbstractRecipeScreenHandler;
import net.minecraft.screen.ScreenHandlerType;
import net.minecraft.screen.slot.Slot;
@@ -28,14 +30,14 @@ import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
public class SkyblockCraftingTableScreen extends HandledScreen<SkyblockCraftingTableScreenHandler> {
- private static final Identifier TEXTURE = new Identifier("textures/gui/container/crafting_table.png");
+ private static final Identifier TEXTURE = Identifier.ofVanilla("textures/gui/container/crafting_table.png");
protected static final ButtonTextures MORE_CRAFTS_TEXTURES = new ButtonTextures(
- new Identifier(SkyblockerMod.NAMESPACE, "quick_craft/more_button"),
- new Identifier(SkyblockerMod.NAMESPACE, "quick_craft/more_button_disabled"),
- new Identifier(SkyblockerMod.NAMESPACE, "quick_craft/more_button_highlighted")
+ Identifier.of(SkyblockerMod.NAMESPACE, "quick_craft/more_button"),
+ Identifier.of(SkyblockerMod.NAMESPACE, "quick_craft/more_button_disabled"),
+ Identifier.of(SkyblockerMod.NAMESPACE, "quick_craft/more_button_highlighted")
);
- protected static final Identifier QUICK_CRAFT = new Identifier(SkyblockerMod.NAMESPACE, "quick_craft/quick_craft_overlay");
+ protected static final Identifier QUICK_CRAFT = Identifier.of(SkyblockerMod.NAMESPACE, "quick_craft/quick_craft_overlay");
private final ItemListWidget recipeBook = new ItemListWidget();
private boolean narrow;
private TexturedButtonWidget moreCraftsButton;
@@ -139,7 +141,7 @@ public class SkyblockCraftingTableScreen extends HandledScreen<SkyblockCraftingT
}
- static class DummyRecipeScreenHandler extends AbstractRecipeScreenHandler<SimpleInventory> {
+ static class DummyRecipeScreenHandler extends AbstractRecipeScreenHandler<CraftingRecipeInput, CraftingRecipe> {
public DummyRecipeScreenHandler() {
super(ScreenHandlerType.GENERIC_9X6, -69);
@@ -152,7 +154,7 @@ public class SkyblockCraftingTableScreen extends HandledScreen<SkyblockCraftingT
public void clearCraftingSlots() {}
@Override
- public boolean matches(RecipeEntry<? extends Recipe<SimpleInventory>> recipe) {
+ public boolean matches(RecipeEntry<CraftingRecipe> recipe) {
return false;
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotText.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotText.java
new file mode 100644
index 00000000..66c02ca1
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotText.java
@@ -0,0 +1,21 @@
+package de.hysky.skyblocker.skyblock.item.slottext;
+
+import net.minecraft.text.Text;
+
+public record SlotText(Text text, TextPosition position) {
+ public static SlotText bottomLeft(Text text) {
+ return new SlotText(text, TextPosition.BOTTOM_LEFT);
+ }
+
+ public static SlotText bottomRight(Text text) {
+ return new SlotText(text, TextPosition.BOTTOM_RIGHT);
+ }
+
+ public static SlotText topLeft(Text text) {
+ return new SlotText(text, TextPosition.TOP_LEFT);
+ }
+
+ public static SlotText topRight(Text text) {
+ return new SlotText(text, TextPosition.TOP_RIGHT);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextAdder.java
new file mode 100644
index 00000000..71d9aa30
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextAdder.java
@@ -0,0 +1,58 @@
+package de.hysky.skyblocker.skyblock.item.slottext;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.render.gui.AbstractContainerMatcher;
+import net.minecraft.screen.slot.Slot;
+import org.intellij.lang.annotations.Language;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Extend this class and add it to {@link SlotTextManager#adders} to add text to any arbitrary slot.
+ */
+public abstract class SlotTextAdder extends AbstractContainerMatcher {
+ /**
+ * Utility constructor that will compile the given string into a pattern.
+ *
+ * @see #SlotTextAdder(Pattern)
+ */
+ protected SlotTextAdder(@NotNull @Language("RegExp") String titlePattern) {
+ super(titlePattern);
+ }
+
+ /**
+ * Creates a SlotTextAdder that will be applied to screens with titles that match the given pattern.
+ *
+ * @param titlePattern The pattern to match the screen title against.
+ */
+ protected SlotTextAdder(@NotNull Pattern titlePattern) {
+ super(titlePattern);
+ }
+
+ /**
+ * Creates a SlotTextAdder that will be applied to all screens.
+ */
+ protected SlotTextAdder() {
+ super();
+ }
+
+ /**
+ * This method will be called for each rendered slot. Consider using a switch statement on {@link Slot#id} if you wish to add different text to different slots.
+ *
+ * @return A list of positioned text to be rendered. Return {@link List#of()} if no text should be rendered.
+ * @implNote By minecraft's design, scaled text inexplicably moves around.
+ * So, limit your text to 3 characters (or roughly less than 20 width) if you want it to not look horrible.
+ */
+ public abstract @NotNull List<SlotText> getText(Slot slot);
+
+ /**
+ * Override this method to add conditions to enable or disable this adder.
+ * @return Whether this adder is enabled.
+ * @implNote The slot text adders only work while in skyblock, so no need to check for that again.
+ */
+ public boolean isEnabled() {
+ return SkyblockerConfigManager.get().general.itemInfoDisplay.slotText;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java
new file mode 100644
index 00000000..d3941d77
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java
@@ -0,0 +1,75 @@
+package de.hysky.skyblocker.skyblock.item.slottext;
+
+import de.hysky.skyblocker.skyblock.item.slottext.adders.*;
+import de.hysky.skyblocker.utils.Utils;
+import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.screen.slot.Slot;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SlotTextManager {
+ private static final SlotTextAdder[] adders = new SlotTextAdder[]{
+ new EssenceShopAdder(),
+ new EnchantmentLevelAdder(),
+ new MinionLevelAdder(),
+ new PetLevelAdder(),
+ new SkyblockLevelAdder(),
+ new SkillLevelAdder(),
+ new CatacombsLevelAdder.Dungeoneering(),
+ new CatacombsLevelAdder.DungeonClasses(),
+ new CatacombsLevelAdder.ReadyUp(),
+ new RancherBootsSpeedAdder(),
+ new AttributeShardAdder(),
+ new PrehistoricEggAdder(),
+ new PotionLevelAdder(),
+ new CollectionAdder(),
+ new CommunityShopAdder(),
+ new YourEssenceAdder(),
+ new PowerStonesGuideAdder(),
+ new StatsTuningAdder()
+ };
+ private static final ArrayList<SlotTextAdder> currentScreenAdders = new ArrayList<>();
+
+ private SlotTextManager() {
+ }
+
+ public static void init() {
+ ScreenEvents.AFTER_INIT.register((client, screen, width, height) -> {
+ if (screen instanceof HandledScreen<?> && Utils.isOnSkyblock()) {
+ onScreenChange(screen);
+ ScreenEvents.remove(screen).register(ignored -> currentScreenAdders.clear());
+ }
+ });
+ }
+
+ private static void onScreenChange(Screen screen) {
+ final String title = screen.getTitle().getString();
+ for (SlotTextAdder adder : adders) {
+ if (!adder.isEnabled()) continue;
+ if (adder.titlePattern == null || adder.titlePattern.matcher(title).find()) {
+ currentScreenAdders.add(adder);
+ }
+ }
+ }
+
+ /**
+ * The returned text is rendered on top of the slot. The text will be scaled if it doesn't fit in the slot,
+ * but 3 characters should be seen as the maximum to keep it readable and in place as it tends to move around when scaled.
+ *
+ * @implNote Only the first adder that returns a non-null text will be used.
+ * The order of the adders remains the same as they were added to the {@link SlotTextManager#adders} array.
+ */
+ @NotNull
+ public static List<SlotText> getText(Slot slot) {
+ if (currentScreenAdders.isEmpty()) return List.of();
+ for (SlotTextAdder adder : currentScreenAdders) {
+ List<SlotText> text = adder.getText(slot);
+ if (!text.isEmpty()) return text;
+ }
+ return List.of();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/TextPosition.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/TextPosition.java
new file mode 100644
index 00000000..052b228d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/TextPosition.java
@@ -0,0 +1,8 @@
+package de.hysky.skyblocker.skyblock.item.slottext;
+
+public enum TextPosition {
+ TOP_LEFT,
+ TOP_RIGHT,
+ BOTTOM_LEFT,
+ BOTTOM_RIGHT
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/AttributeShardAdder.java
index ed650e26..81f543f8 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/AttributeShardAdder.java
@@ -1,9 +1,22 @@
-package de.hysky.skyblocker.skyblock.item;
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.NotNull;
-public class AttributeShards {
- private static final Object2ObjectOpenHashMap<String, String> ID_2_SHORT_NAME = new Object2ObjectOpenHashMap<>();
+import java.util.List;
+
+public class AttributeShardAdder extends SlotTextAdder {
+ private static final Object2ObjectMap<String, String> ID_2_SHORT_NAME = new Object2ObjectOpenHashMap<>();
static {
//Weapons
@@ -50,10 +63,35 @@ public class AttributeShards {
ID_2_SHORT_NAME.put("fishing_speed", "FS");
ID_2_SHORT_NAME.put("hunter", "H");
ID_2_SHORT_NAME.put("trophy_hunter", "TH");
+ }
+
+ public AttributeShardAdder() {
+ super();
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack stack = slot.getStack();
+ NbtCompound customData = ItemUtils.getCustomData(stack);
+
+ if (!ItemUtils.getItemId(stack).equals("ATTRIBUTE_SHARD")) return List.of();
+
+ NbtCompound attributesTag = customData.getCompound("attributes");
+ String[] attributes = attributesTag.getKeys().toArray(String[]::new);
+
+ if (attributes.length != 1) return List.of();
+ String attributeId = attributes[0];
+ int attributeLevel = attributesTag.getInt(attributeId);
+ String attributeInitials = ID_2_SHORT_NAME.getOrDefault(attributeId, "");
+ return List.of(
+ SlotText.bottomRight(Text.literal(String.valueOf(attributeLevel)).withColor(0xFFDDC1)),
+ SlotText.topLeft(Text.literal(attributeInitials).withColor(0xCFF8F8))
+ );
}
- public static String getShortName(String id) {
- return ID_2_SHORT_NAME.getOrDefault(id, "");
+ @Override
+ public boolean isEnabled() {
+ return SkyblockerConfigManager.get().general.itemInfoDisplay.attributeShardInfo;
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CatacombsLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CatacombsLevelAdder.java
new file mode 100644
index 00000000..6c99ebf9
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CatacombsLevelAdder.java
@@ -0,0 +1,105 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.RomanNumerals;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+//This class is split into 3 inner classes as there are multiple screens for showing catacombs levels, each with different slot ids or different style of showing the level.
+//It's still kept in 1 main class for organization purposes.
+public class CatacombsLevelAdder {
+ private CatacombsLevelAdder() {
+ }
+
+ public static class Dungeoneering extends SlotTextAdder {
+ private static final Pattern LEVEL_PATTERN = Pattern.compile(".*?(?:(?<arabic>\\d+)|(?<roman>\\S+))? ?✯?");
+ public Dungeoneering() {
+ super("^Dungeoneering");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ switch (slot.id) {
+ case 12, 29, 30, 31, 32, 33 -> {
+ Matcher matcher = LEVEL_PATTERN.matcher(slot.getStack().getName().getString());
+ if (!matcher.matches()) return List.of();
+ String arabic = matcher.group("arabic");
+ String roman = matcher.group("roman");
+ if (arabic == null && roman == null) return List.of(SlotText.bottomLeft(Text.literal("0").formatted(Formatting.RED)));
+ String level;
+ if (arabic != null) {
+ if (!NumberUtils.isDigits(arabic)) return List.of(); //Sanity check
+ level = arabic;
+ } else { // roman != null
+ if (!RomanNumerals.isValidRomanNumeral(roman)) return List.of(); //Sanity check
+ level = String.valueOf(RomanNumerals.romanToDecimal(roman));
+ }
+
+ return List.of(SlotText.bottomLeft(Text.literal(level).withColor(0xFFDDC1)));
+ }
+ default -> {
+ return List.of();
+ }
+ }
+ }
+ }
+
+ public static class DungeonClasses extends SlotTextAdder {
+
+ public DungeonClasses() {
+ super("^Dungeon Classes"); //Applies to both screens as they are same in both the placement and the style of the level text.
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ switch (slot.id) {
+ case 11, 12, 13, 14, 15 -> {
+ String level = getBracketedLevelFromName(slot.getStack());
+ if (!NumberUtils.isDigits(level)) return List.of();
+ return List.of(SlotText.bottomLeft(Text.literal(level).withColor(0xFFDDC1)));
+ }
+ default -> {
+ return List.of();
+ }
+ }
+ }
+ }
+
+ public static class ReadyUp extends SlotTextAdder {
+
+ public ReadyUp() {
+ super("^Ready Up");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ switch (slot.id) {
+ case 29, 30, 31, 32, 33 -> {
+ String level = getBracketedLevelFromName(slot.getStack());
+ if (!NumberUtils.isDigits(level)) return List.of();
+ return List.of(SlotText.bottomLeft(Text.literal(level).withColor(0xFFDDC1)));
+ }
+ default -> {
+ return List.of();
+ }
+ }
+ }
+ }
+
+ public static String getBracketedLevelFromName(ItemStack itemStack) {
+ String name = itemStack.getName().getString();
+ if (!name.startsWith("[Lvl ")) return null;
+ int index = name.indexOf(']');
+ if (index == -1) return null;
+ return name.substring(5, index);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CollectionAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CollectionAdder.java
new file mode 100644
index 00000000..207190c2
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CollectionAdder.java
@@ -0,0 +1,32 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.RomanNumerals;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class CollectionAdder extends SlotTextAdder {
+ private static final Pattern COLLECTION = Pattern.compile("^[\\w ]+ (?<level>[IVXLCDM]+)$");
+
+ public CollectionAdder() {
+ super("^\\w+ Collections");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack stack = slot.getStack();
+ Matcher matcher = COLLECTION.matcher(stack.getName().getString());
+ if (matcher.matches()) {
+ int level = RomanNumerals.romanToDecimal(matcher.group("level"));
+ return List.of(SlotText.bottomRight(Text.literal(String.valueOf(level)).withColor(0xFFDDC1)));
+ }
+ return List.of();
+ }
+}
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
new file mode 100644
index 00000000..c7ea17dc
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java
@@ -0,0 +1,60 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.RomanNumerals;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class CommunityShopAdder extends SlotTextAdder {
+ private static final byte CATEGORIES_START = 10;
+ private static final byte CATEGORIES_END = 14; //Inclusive
+
+ public CommunityShopAdder() {
+ super("^Community Shop");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ for (byte i = CATEGORIES_START; i <= CATEGORIES_END; i++) {
+ if (slot.inventory.getStack(i).isOf(Items.LIME_STAINED_GLASS_PANE)) { //Only the selected category has a lime stained glass pane, the others have a gray one.
+ return switch (i) { //This is a switch to allow adding more categories easily in the future, if we ever add more.
+ case 11 -> getTextForUpgradesScreen(slot);
+ default -> List.of();
+ };
+ }
+ }
+ return List.of();
+ }
+
+ private static List<SlotText> getTextForUpgradesScreen(Slot slot) {
+ final ItemStack stack = slot.getStack();
+ switch (slot.id) {
+ case 30, 31, 32, 33, 34, 38, 39, 40, 41, 42, 43, 44 -> {
+ String name = stack.getName().getString();
+ int lastIndex = name.lastIndexOf(' ');
+ String roman = name.substring(lastIndex + 1); // + 1 as we don't want the space
+ if (!RomanNumerals.isValidRomanNumeral(roman)) return List.of();
+
+ List<Text> lore = ItemUtils.getLore(stack);
+ if (lore.isEmpty()) return List.of();
+ 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 "Click to claim!" -> Text.literal("✅").withColor(0xa6e3a1).formatted(Formatting.BOLD);
+ default -> Text.literal(String.valueOf(RomanNumerals.romanToDecimal(roman))).withColor(0xcba6f7);
+ }));
+
+ }
+ }
+ return List.of();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/EnchantmentLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/EnchantmentLevelAdder.java
new file mode 100644
index 00000000..5530035d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/EnchantmentLevelAdder.java
@@ -0,0 +1,46 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.RomanNumerals;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtElement;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class EnchantmentLevelAdder extends SlotTextAdder {
+ public EnchantmentLevelAdder() {
+ super();
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack itemStack = slot.getStack();
+ if (!itemStack.isOf(Items.ENCHANTED_BOOK)) return List.of();
+ String name = itemStack.getName().getString();
+ if (name.equals("Enchanted Book")) {
+ NbtCompound nbt = ItemUtils.getCustomData(itemStack);
+ if (nbt.isEmpty() || !nbt.contains("enchantments", NbtElement.COMPOUND_TYPE)) return List.of();
+ NbtCompound enchantments = nbt.getCompound("enchantments");
+ if (enchantments.getSize() != 1) return List.of(); //Only makes sense to display the level when there's one enchant.
+ int level = enchantments.getInt(enchantments.getKeys().iterator().next());
+ return List.of(SlotText.bottomLeft(Text.literal(String.valueOf(level)).withColor(0xFFDDC1)));
+ } else { //In bazaar, the books have the enchantment level in the name
+ int level = getEnchantLevelFromString(name);
+ if (level == 0) return List.of();
+ return List.of(SlotText.bottomLeft(Text.literal(String.valueOf(level)).withColor(0xFFDDC1)));
+ }
+ }
+
+ private static int getEnchantLevelFromString(String str) {
+ String romanNumeral = str.substring(str.lastIndexOf(' ') + 1); //+1 because we don't need the space itself
+ return RomanNumerals.romanToDecimal(romanNumeral); //Temporary line. The method will be moved out later.
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/EssenceShopAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/EssenceShopAdder.java
new file mode 100644
index 00000000..65574a5a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/EssenceShopAdder.java
@@ -0,0 +1,51 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.RomanNumerals;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.NotNull;
+
+import java.text.NumberFormat;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class EssenceShopAdder extends SlotTextAdder {
+ private static final Pattern ESSENCELEVEL = Pattern.compile("^[\\w ]+ (?<level>[IVXLCDM]+)$");
+ private static final Pattern UNLOCKED = Pattern.compile("UNLOCKED");
+ private static final Pattern ESSENCE = Pattern.compile("Your \\w+ Essence: (?<essence>[\\d,]+)");
+
+ public EssenceShopAdder() {
+ super("Essence Shop");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack stack = slot.getStack();
+
+ Matcher essenceLevelMatcher = ESSENCELEVEL.matcher(stack.getName().getString());
+ Matcher essenceAmountMatcher = ItemUtils.getLoreLineIfMatch(stack, ESSENCE);
+
+ if (essenceLevelMatcher.matches()) {
+ int level = RomanNumerals.romanToDecimal(essenceLevelMatcher.group("level"));
+ Matcher unlockedMatcher = ItemUtils.getLoreLineIfMatch(stack, UNLOCKED);
+ if (unlockedMatcher == null) {
+ level -= 1;
+ }
+ return List.of(SlotText.bottomRight(Text.literal(String.valueOf(level)).withColor(0xFFDDC1)));
+ }
+ if (essenceAmountMatcher == null) return List.of();
+ String essenceAmount = essenceAmountMatcher.group("essence").replace(",", "");
+ if (!essenceAmount.matches("-?\\d+")) return List.of();
+ NumberFormat NUMBER_FORMATTER_S = NumberFormat.getCompactNumberInstance(Locale.CANADA, NumberFormat.Style.SHORT);
+ NUMBER_FORMATTER_S.setMinimumFractionDigits(1);
+ int amount = Integer.parseInt(essenceAmount);
+
+ return List.of(SlotText.bottomRight(Text.literal(NUMBER_FORMATTER_S.format(amount)).withColor(0xFFDDC1)));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/MinionLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/MinionLevelAdder.java
new file mode 100644
index 00000000..fc46f153
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/MinionLevelAdder.java
@@ -0,0 +1,34 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.RomanNumerals;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class MinionLevelAdder extends SlotTextAdder {
+ private static final Pattern MINION_PATTERN = Pattern.compile(".* Minion ([IVXLCDM]+)");
+ public MinionLevelAdder() {
+ super();
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ ItemStack itemStack = slot.getStack();
+ if (!itemStack.isOf(Items.PLAYER_HEAD)) return List.of();
+ Matcher matcher = MINION_PATTERN.matcher(itemStack.getName().getString());
+ if (!matcher.matches()) return List.of();
+ String romanNumeral = matcher.group(1);
+ if (!RomanNumerals.isValidRomanNumeral(romanNumeral)) return List.of();
+ int level = RomanNumerals.romanToDecimal(romanNumeral);
+ return List.of(SlotText.topRight(Text.literal(String.valueOf(level)).withColor(0xFFDDC1)));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PetLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PetLevelAdder.java
new file mode 100644
index 00000000..88d48fbf
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PetLevelAdder.java
@@ -0,0 +1,29 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class PetLevelAdder extends SlotTextAdder {
+ public PetLevelAdder() {
+ super();
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ ItemStack itemStack = slot.getStack();
+ if (!itemStack.isOf(Items.PLAYER_HEAD)) return List.of();
+ String level = CatacombsLevelAdder.getBracketedLevelFromName(itemStack);
+ if (!NumberUtils.isDigits(level)) return List.of();
+ if ("100".equals(level) || "200".equals(level)) return List.of();
+ return List.of(SlotText.topLeft(Text.literal(level).withColor(0xFFDDC1)));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PotionLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PotionLevelAdder.java
new file mode 100644
index 00000000..1c3ef4bc
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PotionLevelAdder.java
@@ -0,0 +1,32 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtElement;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class PotionLevelAdder extends SlotTextAdder {
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack stack = slot.getStack();
+ NbtCompound customData = ItemUtils.getCustomData(stack);
+ String title = stack.getName().getString();
+ if (customData.contains("potion_level", NbtElement.INT_TYPE) && !title.contains("Healer Class") && !title.contains("Class Passives")) {
+ if (title.contains("Healer Level ")){
+ String level = title.replaceAll("[^0-9]", "");
+ return List.of(SlotText.bottomRight(Text.literal(level).withColor(0xFFFFFF)));
+ } else {
+ int level = customData.getInt("potion_level");
+ return List.of(SlotText.bottomRight(Text.literal(String.valueOf(level)).withColor(0xFFDDC1)));
+ }
+ }
+ return List.of();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PowerStonesGuideAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PowerStonesGuideAdder.java
new file mode 100644
index 00000000..0bb37165
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PowerStonesGuideAdder.java
@@ -0,0 +1,38 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class PowerStonesGuideAdder extends SlotTextAdder {
+ private static final Pattern LEARNED = Pattern.compile("Learned: (Yes|Not Yet) (?<symbol>[✖✔])");
+
+ public PowerStonesGuideAdder() {
+ super("^Power Stones Guide");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack stack = slot.getStack();
+
+ Matcher match = ItemUtils.getLoreLineIfMatch(stack, LEARNED);
+ if (match == null) return List.of();
+ String symbol = match.group("symbol");
+ Text text;
+ if (symbol.equals("✖")) {
+ text = Text.literal("✖").withColor(0xFF7276);
+ } else {
+ text = Text.literal("✔").withColor(0x90ee90);
+ }
+
+ return List.of(SlotText.bottomRight(text));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PrehistoricEggAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PrehistoricEggAdder.java
new file mode 100644
index 00000000..ce29297e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PrehistoricEggAdder.java
@@ -0,0 +1,34 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtElement;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class PrehistoricEggAdder extends SlotTextAdder {
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack stack = slot.getStack();
+ if (!stack.isOf(Items.PLAYER_HEAD) || !StringUtils.equals(stack.getSkyblockId(), "PREHISTORIC_EGG")) return List.of();
+ NbtCompound nbt = ItemUtils.getCustomData(stack);
+ if (!nbt.contains("blocks_walked", NbtElement.INT_TYPE)) return List.of();
+ int walked = nbt.getInt("blocks_walked");
+
+ String walkedstr;
+ if (walked < 1000) walkedstr = String.valueOf(walked);
+ else if (walked < 10000) walkedstr = String.format("%.1fk", walked/1000.0f);
+ else walkedstr = walked / 1000 + "k";
+
+ return List.of(SlotText.bottomLeft(Text.literal(walkedstr).withColor(0xFFDDC1)));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/RancherBootsSpeedAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/RancherBootsSpeedAdder.java
new file mode 100644
index 00000000..7de5a5be
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/RancherBootsSpeedAdder.java
@@ -0,0 +1,36 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class RancherBootsSpeedAdder extends SlotTextAdder {
+ private static final Pattern SPEED_PATTERN = Pattern.compile("Current Speed Cap: (\\d+) ?(\\d+)?");
+
+ public RancherBootsSpeedAdder() {
+ super();
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack itemStack = slot.getStack();
+ // V null-safe equals.
+ if (!itemStack.isOf(Items.LEATHER_BOOTS) && !StringUtils.equals(itemStack.getSkyblockId(), "RANCHERS_BOOTS")) return List.of();
+ Matcher matcher = ItemUtils.getLoreLineIfMatch(slot.getStack(), SPEED_PATTERN);
+ if (matcher == null) return List.of();
+ String speed = matcher.group(2);
+ if (speed == null) speed = matcher.group(1); //2nd group only matches when the speed cap is set to a number beyond the player's actual speed cap.
+ return List.of(SlotText.bottomLeft(Text.literal(speed).withColor(0xFFDDC1)));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkillLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkillLevelAdder.java
new file mode 100644
index 00000000..07b8dd9b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkillLevelAdder.java
@@ -0,0 +1,34 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.RomanNumerals;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class SkillLevelAdder extends SlotTextAdder {
+ public SkillLevelAdder() {
+ super("^Your Skills");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ switch (slot.id) {
+ case 19, 20, 21, 22, 23, 24, 25, 29, 30, 31, 32 -> { //These are the slots that contain the skill items. Note that they aren't continuous, as there are 2 rows.
+ String name = slot.getStack().getName().getString();
+ int lastIndex = name.lastIndexOf(' ');
+ if (lastIndex == -1) return List.of(SlotText.bottomLeft(Text.literal("0").formatted(Formatting.LIGHT_PURPLE))); //Skills without any levels don't display any roman numerals. Probably because 0 doesn't exist.
+ String romanNumeral = name.substring(lastIndex + 1); //+1 because we don't need the space itself
+ //The "romanNumeral" might be a latin numeral, too. There's a skyblock setting for this, so we have to do it this way V
+ return List.of(SlotText.bottomLeft(Text.literal(String.valueOf(RomanNumerals.isValidRomanNumeral(romanNumeral) ? RomanNumerals.romanToDecimal(romanNumeral) : Integer.parseInt(romanNumeral))).withColor(0xFFDDC1)));
+ }
+ default -> {
+ return List.of();
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkyblockLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkyblockLevelAdder.java
new file mode 100644
index 00000000..0fc07922
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkyblockLevelAdder.java
@@ -0,0 +1,29 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class SkyblockLevelAdder extends SlotTextAdder {
+ public SkyblockLevelAdder() {
+ super("^SkyBlock Menu");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ if (slot.getIndex() != 22) return List.of();
+ List<Text> lore = ItemUtils.getLore(slot.getStack());
+ if (lore.isEmpty()) return List.of();
+ List<Text> siblings = lore.getFirst().getSiblings();
+ if (siblings.size() < 3) return List.of();
+ String levelText = siblings.get(2).getString(); //The 3rd child is the level text itself
+ if (!NumberUtils.isDigits(levelText)) return List.of();
+ return List.of(SlotText.bottomLeft(Text.literal(levelText).withColor(0xFFDDC1)));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/StatsTuningAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/StatsTuningAdder.java
new file mode 100644
index 00000000..3079d8f2
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/StatsTuningAdder.java
@@ -0,0 +1,42 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class StatsTuningAdder extends SlotTextAdder {
+ private static final Pattern STATHAS = Pattern.compile("Stat has: (?<points>\\d+) (points|point)");
+ private static final Pattern UNASSIGNEDPOINTS = Pattern.compile("Unassigned Points: (?<points>\\d+)!!!");
+
+ public StatsTuningAdder() {
+ super("^Stats Tuning");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack stack = slot.getStack();
+
+ Matcher statMatcher = ItemUtils.getLoreLineIfMatch(stack, STATHAS);
+ Matcher unassignedMatcher = ItemUtils.getLoreLineIfMatch(stack, UNASSIGNEDPOINTS);
+
+ if (stack.getName().getString().equals("Stats Tuning")) {
+ if (unassignedMatcher == null) return List.of();
+ String unassignedPoints = unassignedMatcher.group("points");
+ return List.of(SlotText.bottomRight(Text.literal(unassignedPoints).withColor(0xFFDDC1)));
+ }
+
+ if (statMatcher == null) return List.of();
+ String assignedPoints = statMatcher.group("points");
+ if (assignedPoints.equals("0")) return List.of();
+ return List.of(SlotText.bottomRight(Text.literal(assignedPoints).withColor(0xFFDDC1)));
+
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/YourEssenceAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/YourEssenceAdder.java
new file mode 100644
index 00000000..ec1bd561
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/YourEssenceAdder.java
@@ -0,0 +1,46 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.NotNull;
+
+import java.text.NumberFormat;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class YourEssenceAdder extends SlotTextAdder {
+ private static final Pattern ESSENCE = Pattern.compile("You currently own (?<essence>[\\d,]+)");
+
+ public YourEssenceAdder() {
+ super("^Your Essence");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack stack = slot.getStack();
+ String name = stack.getName().getString();
+ if (name.contains("Essence")) {
+ List<Text> lore = ItemUtils.getLore(stack);
+ if (lore.isEmpty()) return List.of();
+ String essenceAmountText = lore.getFirst().getString();
+
+ Matcher essenceAmountMatcher = ESSENCE.matcher(essenceAmountText);
+ if (essenceAmountMatcher.find()) {
+ String essenceAmount = essenceAmountMatcher.group("essence").replace(",", "");
+ if (!essenceAmount.matches("-?\\d+")) return List.of();
+ NumberFormat NUMBER_FORMATTER_S = NumberFormat.getCompactNumberInstance(Locale.CANADA, NumberFormat.Style.SHORT);
+ NUMBER_FORMATTER_S.setMinimumFractionDigits(1);
+ int amount = Integer.parseInt(essenceAmount);
+
+ return List.of(SlotText.bottomRight(Text.literal(NUMBER_FORMATTER_S.format(amount)).withColor(0xFFDDC1)));
+ }
+ }
+ return List.of();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java
index 8798a139..992206ad 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java
@@ -108,7 +108,7 @@ public class AccessoriesHelper {
.put(page, new ObjectOpenHashSet<>(accessoryIds));
}
- static Pair<AccessoryReport, String> calculateReport4Accessory(String accessoryId) {
+ public static Pair<AccessoryReport, String> calculateReport4Accessory(String accessoryId) {
if (!ACCESSORY_DATA.containsKey(accessoryId) || Utils.getProfileId().isEmpty()) return Pair.of(AccessoryReport.INELIGIBLE, null);
Accessory accessory = ACCESSORY_DATA.get(accessoryId);
@@ -208,7 +208,7 @@ public class AccessoriesHelper {
}
}
- enum AccessoryReport {
+ public enum AccessoryReport {
HAS_HIGHEST_TIER, //You've collected the highest tier - Collected
IS_GREATER_TIER, //This accessory is an upgrade from the one in the same family that you already have - Upgrade -- Shows you what tier this accessory is in its family
HAS_GREATER_TIER, //This accessory has a higher tier upgrade - Upgradable -- Shows you the highest tier accessory you've collected in that family
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/BackpackPreview.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/BackpackPreview.java
index 50772789..cad1eaab 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/BackpackPreview.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/BackpackPreview.java
@@ -15,13 +15,17 @@ import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.inventory.Inventory;
import net.minecraft.inventory.SimpleInventory;
import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtElement;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.StringNbtReader;
import net.minecraft.nbt.visitor.StringNbtWriter;
+import net.minecraft.registry.BuiltinRegistries;
+import net.minecraft.registry.RegistryOps;
import net.minecraft.util.Identifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -38,8 +42,8 @@ import java.util.stream.Collectors;
public class BackpackPreview {
private static final Logger LOGGER = LoggerFactory.getLogger(BackpackPreview.class);
- private static final Identifier ITEM_PROTECTION = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/item_protection.png");
- private static final Identifier TEXTURE = new Identifier("textures/gui/container/generic_54.png");
+ private static final Identifier ITEM_PROTECTION = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/item_protection.png");
+ private static final Identifier TEXTURE = Identifier.ofVanilla("textures/gui/container/generic_54.png");
private static final Pattern ECHEST_PATTERN = Pattern.compile("Ender Chest.*\\((\\d+)/\\d+\\)");
private static final Pattern BACKPACK_PATTERN = Pattern.compile("Backpack.*\\(Slot #(\\d+)\\)");
private static final int STORAGE_SIZE = 27;
@@ -88,7 +92,7 @@ public class BackpackPreview {
Path storageFile = saveDir.resolve(index + ".nbt");
if (Files.isRegularFile(storageFile)) {
try (BufferedReader reader = Files.newBufferedReader(storageFile)) {
- storages[index] = Storage.CODEC.parse(NbtOps.INSTANCE, StringNbtReader.parse(reader.lines().collect(Collectors.joining()))).getOrThrow();
+ storages[index] = Storage.CODEC.parse(getOps(), StringNbtReader.parse(reader.lines().collect(Collectors.joining()))).getOrThrow();
} catch (Exception e) {
LOGGER.error("Failed to load backpack preview file: {}", storageFile.getFileName().toString(), e);
}
@@ -96,6 +100,11 @@ public class BackpackPreview {
}
}
+ private static RegistryOps<NbtElement> getOps() {
+ MinecraftClient client = MinecraftClient.getInstance();
+ return client != null && client.getNetworkHandler() != null && client.getNetworkHandler().getRegistryManager() != null ? client.getNetworkHandler().getRegistryManager().getOps(NbtOps.INSTANCE) : BuiltinRegistries.createWrapperLookup().getOps(NbtOps.INSTANCE);
+ }
+
private static void saveStorages() {
for (int index = 0; index < STORAGE_SIZE; ++index) {
if (storages[index] != null && storages[index].dirty) {
@@ -107,7 +116,7 @@ public class BackpackPreview {
private static void saveStorage(int index) {
Path storageFile = saveDir.resolve(index + ".nbt");
try (BufferedWriter writer = Files.newBufferedWriter(storageFile)) {
- writer.write(new StringNbtWriter().apply(Storage.CODEC.encodeStart(NbtOps.INSTANCE, storages[index]).getOrThrow()));
+ writer.write(new StringNbtWriter().apply(Storage.CODEC.encodeStart(getOps(), storages[index]).getOrThrow()));
storages[index].markClean();
} catch (Exception e) {
LOGGER.error("Failed to save backpack preview file: {}", storageFile.getFileName().toString(), e);
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java
index c5279c61..b93ca77a 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java
@@ -7,7 +7,6 @@ import it.unimi.dsi.fastutil.ints.IntIntPair;
import it.unimi.dsi.fastutil.ints.IntObjectPair;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
-import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.tooltip.HoveredTooltipPositioner;
import net.minecraft.client.gui.tooltip.TooltipComponent;
import net.minecraft.item.ItemStack;
@@ -34,8 +33,7 @@ public class CompactorDeletorPreview {
public static final Pattern NAME = Pattern.compile("PERSONAL_(?<type>COMPACTOR|DELETOR)_(?<size>\\d+)");
private static final MinecraftClient client = MinecraftClient.getInstance();
- public static boolean drawPreview(DrawContext context, ItemStack stack, String type, String size, int x, int y) {
- List<Text> tooltips = Screen.getTooltipFromItem(client, stack);
+ public static boolean drawPreview(DrawContext context, ItemStack stack, List<Text> tooltips, String type, String size, int x, int y) {
int targetIndex = getTargetIndex(tooltips);
if (targetIndex == -1) return false;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorPreviewTooltipComponent.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorPreviewTooltipComponent.java
index 8f2c0901..b7ac2856 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorPreviewTooltipComponent.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorPreviewTooltipComponent.java
@@ -10,7 +10,7 @@ import net.minecraft.item.Items;
import net.minecraft.util.Identifier;
public class CompactorPreviewTooltipComponent implements TooltipComponent {
- private static final Identifier TEXTURE = new Identifier("textures/gui/container/generic_54.png");
+ private static final Identifier TEXTURE = Identifier.ofVanilla("textures/gui/container/generic_54.png");
private static final ItemStack BLACK_STAINED_GLASS_PANE = new ItemStack(Items.BLACK_STAINED_GLASS_PANE);
private final Iterable<IntObjectPair<ItemStack>> items;
private final IntIntPair dimensions;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ExoticTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ExoticTooltip.java
deleted file mode 100644
index 46babc8b..00000000
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ExoticTooltip.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package de.hysky.skyblocker.skyblock.item.tooltip;
-
-import de.hysky.skyblocker.utils.Constants;
-import net.minecraft.nbt.NbtCompound;
-import net.minecraft.text.MutableText;
-import net.minecraft.text.Text;
-import net.minecraft.util.Formatting;
-import net.minecraft.util.StringIdentifiable;
-
-public class ExoticTooltip {
- public static String getExpectedHex(String id) {
- String color = TooltipInfoType.COLOR.getData().get(id).getAsString();
- if (color != null) {
- String[] RGBValues = color.split(",");
- return String.format("%02X%02X%02X", Integer.parseInt(RGBValues[0]), Integer.parseInt(RGBValues[1]), Integer.parseInt(RGBValues[2]));
- } else {
- ItemTooltip.LOGGER.warn("[Skyblocker Exotics] No expected color data found for id {}", id);
- return null;
- }
- }
-
- public static boolean isException(String id, String hex) {
- if (id.startsWith("LEATHER") || id.equals("GHOST_BOOTS") || Constants.SEYMOUR_IDS.contains(id)) {
- return true;
- }
- if (id.startsWith("RANCHER")) {
- return Constants.RANCHERS.contains(hex);
- }
- if (id.contains("ADAPTIVE_CHESTPLATE")) {
- return Constants.ADAPTIVE_CHEST.contains(hex);
- } else if (id.contains("ADAPTIVE")) {
- return Constants.ADAPTIVE.contains(hex);
- }
- if (id.startsWith("REAPER")) {
- return Constants.REAPER.contains(hex);
- }
- if (id.startsWith("FAIRY")) {
- return Constants.FAIRY_HEXES.contains(hex);
- }
- if (id.startsWith("CRYSTAL")) {
- return Constants.CRYSTAL_HEXES.contains(hex);
- }
- if (id.contains("SPOOK")) {
- return Constants.SPOOK.contains(hex);
- }
- return false;
- }
-
- public static DyeType checkDyeType(String hex) {
- if (Constants.CRYSTAL_HEXES.contains(hex)) {
- return DyeType.CRYSTAL;
- }
- if (Constants.FAIRY_HEXES.contains(hex)) {
- return DyeType.FAIRY;
- }
- if (Constants.OG_FAIRY_HEXES.contains(hex)) {
- return DyeType.OG_FAIRY;
- }
- if (Constants.SPOOK.contains(hex)) {
- return DyeType.SPOOK;
- }
- if (Constants.GLITCHED.contains(hex)) {
- return DyeType.GLITCHED;
- }
- return DyeType.EXOTIC;
- }
-
- public static boolean intendedDyed(NbtCompound customData) {
- return customData.contains("dye_item");
- }
-
- public enum DyeType implements StringIdentifiable {
- CRYSTAL("crystal", Formatting.AQUA),
- FAIRY("fairy", Formatting.LIGHT_PURPLE),
- OG_FAIRY("og_fairy", Formatting.DARK_PURPLE),
- SPOOK("spook", Formatting.RED),
- GLITCHED("glitched", Formatting.BLUE),
- EXOTIC("exotic", Formatting.GOLD);
- private final String name;
- private final Formatting formatting;
-
- DyeType(String name, Formatting formatting) {
- this.name = name;
- this.formatting = formatting;
- }
-
- @Override
- public String asString() {
- return name;
- }
-
- public MutableText getTranslatedText() {
- return Text.translatable("skyblocker.exotic." + name).formatted(formatting);
- }
- }
-}
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 8c083e25..49d170b9 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
@@ -1,269 +1,46 @@
package de.hysky.skyblocker.skyblock.item.tooltip;
-import com.google.gson.JsonObject;
-import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.config.configs.GeneralConfig;
-import de.hysky.skyblocker.skyblock.item.MuseumItemCache;
-import de.hysky.skyblocker.skyblock.item.tooltip.AccessoriesHelper.AccessoryReport;
import de.hysky.skyblocker.utils.Constants;
-import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.scheduler.Scheduler;
-import it.unimi.dsi.fastutil.Pair;
import net.minecraft.client.MinecraftClient;
-import net.minecraft.client.item.TooltipType;
-import net.minecraft.component.DataComponentTypes;
-import net.minecraft.component.type.DyedColorComponent;
-import net.minecraft.item.Item;
-import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NbtCompound;
-import net.minecraft.nbt.NbtElement;
-import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
-
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
import java.util.concurrent.CompletableFuture;
public class ItemTooltip {
protected static final Logger LOGGER = LoggerFactory.getLogger(ItemTooltip.class.getName());
private static final MinecraftClient client = MinecraftClient.getInstance();
- protected static final GeneralConfig.ItemTooltip config = SkyblockerConfigManager.get().general.itemTooltip;
+ public static final GeneralConfig.ItemTooltip config = SkyblockerConfigManager.get().general.itemTooltip;
private static volatile boolean sentNullWarning = false;
- public static void getTooltip(ItemStack stack, Item.TooltipContext tooltipContext, TooltipType tooltipType, List<Text> lines) {
- if (!Utils.isOnSkyblock() || client.player == null) return;
-
- smoothenLines(lines);
-
- String name = getInternalNameFromNBT(stack, false);
- String internalID = getInternalNameFromNBT(stack, true);
- String neuName = name;
- if (name == null || internalID == null) return;
-
- if (name.startsWith("ISSHINY_")) {
- name = "SHINY_" + internalID;
- neuName = internalID;
- }
-
- if (lines.isEmpty()) {
- return;
- }
-
- int count = stack.getCount();
- boolean bazaarOpened = lines.stream().anyMatch(each -> each.getString().contains("Buy price:") || each.getString().contains("Sell price:"));
-
- if (TooltipInfoType.NPC.isTooltipEnabledAndHasOrNullWarning(internalID)) {
- lines.add(Text.literal(String.format("%-21s", "NPC Sell Price:"))
- .formatted(Formatting.YELLOW)
- .append(getCoinsMessage(TooltipInfoType.NPC.getData().get(internalID).getAsDouble(), count)));
- }
-
- boolean bazaarExist = false;
-
- if (TooltipInfoType.BAZAAR.isTooltipEnabledAndHasOrNullWarning(name) && !bazaarOpened) {
- JsonObject getItem = TooltipInfoType.BAZAAR.getData().getAsJsonObject(name);
- lines.add(Text.literal(String.format("%-18s", "Bazaar buy Price:"))
- .formatted(Formatting.GOLD)
- .append(getItem.get("buyPrice").isJsonNull()
- ? Text.literal("No data").formatted(Formatting.RED)
- : getCoinsMessage(getItem.get("buyPrice").getAsDouble(), count)));
- lines.add(Text.literal(String.format("%-19s", "Bazaar sell Price:"))
- .formatted(Formatting.GOLD)
- .append(getItem.get("sellPrice").isJsonNull()
- ? Text.literal("No data").formatted(Formatting.RED)
- : getCoinsMessage(getItem.get("sellPrice").getAsDouble(), count)));
- bazaarExist = true;
- }
-
- // bazaarOpened & bazaarExist check for lbin, because Skytils keeps some bazaar item data in lbin api
- boolean lbinExist = false;
- if (TooltipInfoType.LOWEST_BINS.isTooltipEnabledAndHasOrNullWarning(name) && !bazaarOpened && !bazaarExist) {
- lines.add(Text.literal(String.format("%-19s", "Lowest BIN Price:"))
- .formatted(Formatting.GOLD)
- .append(getCoinsMessage(TooltipInfoType.LOWEST_BINS.getData().get(name).getAsDouble(), count)));
- lbinExist = true;
- }
-
- if (SkyblockerConfigManager.get().general.itemTooltip.enableAvgBIN) {
- if (TooltipInfoType.ONE_DAY_AVERAGE.getData() == null || TooltipInfoType.THREE_DAY_AVERAGE.getData() == null) {
- nullWarning();
- } else {
- /*
- We are skipping check average prices for potions, runes
- and enchanted books because there is no data for their in API.
- */
- neuName = getNeuName(internalID, neuName);
-
- if (!neuName.isEmpty() && lbinExist) {
- GeneralConfig.Average type = config.avg;
-
- // "No data" line because of API not keeping old data, it causes NullPointerException
- if (type == GeneralConfig.Average.ONE_DAY || type == GeneralConfig.Average.BOTH) {
- lines.add(
- Text.literal(String.format("%-19s", "1 Day Avg. Price:"))
- .formatted(Formatting.GOLD)
- .append(TooltipInfoType.ONE_DAY_AVERAGE.getData().get(neuName) == null
- ? Text.literal("No data").formatted(Formatting.RED)
- : getCoinsMessage(TooltipInfoType.ONE_DAY_AVERAGE.getData().get(neuName).getAsDouble(), count)
- )
- );
- }
- if (type == GeneralConfig.Average.THREE_DAY || type == GeneralConfig.Average.BOTH) {
- lines.add(
- Text.literal(String.format("%-19s", "3 Day Avg. Price:"))
- .formatted(Formatting.GOLD)
- .append(TooltipInfoType.THREE_DAY_AVERAGE.getData().get(neuName) == null
- ? Text.literal("No data").formatted(Formatting.RED)
- : getCoinsMessage(TooltipInfoType.THREE_DAY_AVERAGE.getData().get(neuName).getAsDouble(), count)
- )
- );
- }
- }
- }
- }
-
- final Map<Integer, String> itemTierFloors = Map.of(
- 1, "F1",
- 2, "F2",
- 3, "F3",
- 4, "F4/M1",
- 5, "F5/M2",
- 6, "F6/M3",
- 7, "F7/M4",
- 8, "M5",
- 9, "M6",
- 10, "M7"
- );
-
- if (SkyblockerConfigManager.get().general.itemTooltip.dungeonQuality) {
- NbtCompound customData = ItemUtils.getCustomData(stack);
- if (customData != null && customData.contains("baseStatBoostPercentage")) {
- int baseStatBoostPercentage = customData.getInt("baseStatBoostPercentage");
- boolean maxQuality = baseStatBoostPercentage == 50;
- if (maxQuality) {
- lines.add(Text.literal(String.format("%-17s", "Item Quality:") + baseStatBoostPercentage + "/50").formatted(Formatting.RED).formatted(Formatting.BOLD));
- } else {
- lines.add(Text.literal(String.format("%-21s", "Item Quality:") + baseStatBoostPercentage + "/50").formatted(Formatting.BLUE));
- }
- if (customData.contains("item_tier")) { // sometimes it just isn't here?
- int itemTier = customData.getInt("item_tier");
- if (maxQuality) {
- lines.add(Text.literal(String.format("%-17s", "Floor Tier:") + itemTier + " (" + itemTierFloors.get(itemTier) + ")").formatted(Formatting.RED).formatted(Formatting.BOLD));
- } else {
- lines.add(Text.literal(String.format("%-21s", "Floor Tier:") + itemTier + " (" + itemTierFloors.get(itemTier) + ")").formatted(Formatting.BLUE));
- }
- }
- }
- }
-
- if (TooltipInfoType.MOTES.isTooltipEnabledAndHasOrNullWarning(internalID)) {
- lines.add(Text.literal(String.format("%-20s", "Motes Price:"))
- .formatted(Formatting.LIGHT_PURPLE)
- .append(getMotesMessage(TooltipInfoType.MOTES.getData().get(internalID).getAsInt(), count)));
- }
-
- if (TooltipInfoType.OBTAINED.isTooltipEnabled()) {
- String timestamp = ItemUtils.getTimestamp(stack);
-
- if (!timestamp.isEmpty()) {
- lines.add(Text.literal(String.format("%-21s", "Obtained: "))
- .formatted(Formatting.LIGHT_PURPLE)
- .append(Text.literal(timestamp).formatted(Formatting.RED)));
- }
- }
-
- if (TooltipInfoType.MUSEUM.isTooltipEnabledAndHasOrNullWarning(internalID) && !bazaarOpened) {
- String itemCategory = TooltipInfoType.MUSEUM.getData().get(internalID).getAsString();
- String format = switch (itemCategory) {
- case "Weapons" -> "%-18s";
- case "Armor" -> "%-19s";
- default -> "%-20s";
- };
-
- //Special case the special category so that it doesn't always display not donated
- if (itemCategory.equals("Special")) {
- lines.add(Text.literal(String.format(format, "Museum: (" + itemCategory + ")"))
- .formatted(Formatting.LIGHT_PURPLE));
- } else {
- NbtCompound customData = ItemUtils.getCustomData(stack);
- boolean isInMuseum = (customData.contains("donated_museum") && customData.getBoolean("donated_museum")) || MuseumItemCache.hasItemInMuseum(internalID);
-
- Formatting donatedIndicatorFormatting = isInMuseum ? Formatting.GREEN : Formatting.RED;
-
- lines.add(Text.literal(String.format(format, "Museum (" + itemCategory + "):"))
- .formatted(Formatting.LIGHT_PURPLE)
- .append(Text.literal(isInMuseum ? "✔" : "✖").formatted(donatedIndicatorFormatting, Formatting.BOLD))
- .append(Text.literal(isInMuseum ? " Donated" : " Not Donated").formatted(donatedIndicatorFormatting)));
- }
- }
-
- if (TooltipInfoType.COLOR.isTooltipEnabledAndHasOrNullWarning(internalID) && stack.contains(DataComponentTypes.DYED_COLOR)) {
- String uuid = ItemUtils.getItemUuid(stack);
- boolean hasCustomDye = SkyblockerConfigManager.get().general.customDyeColors.containsKey(uuid) || SkyblockerConfigManager.get().general.customAnimatedDyes.containsKey(uuid);
- //DyedColorComponent#getColor returns ARGB so we mask out the alpha bits
- int dyeColor = DyedColorComponent.getColor(stack, 0);
-
- // dyeColor will have alpha = 255 if it's dyed, and alpha = 0 if it's not dyed,
- if (!hasCustomDye && dyeColor != 0) {
- dyeColor = dyeColor & 0x00FFFFFF;
- String colorHex = String.format("%06X", dyeColor);
- String expectedHex = ExoticTooltip.getExpectedHex(internalID);
-
- boolean correctLine = false;
- for (Text text : lines) {
- String existingTooltip = text.getString() + " ";
- if (existingTooltip.startsWith("Color: ")) {
- correctLine = true;
-
- addExoticTooltip(lines, internalID, ItemUtils.getCustomData(stack), colorHex, expectedHex, existingTooltip);
- break;
- }
- }
-
- if (!correctLine) {
- addExoticTooltip(lines, internalID, ItemUtils.getCustomData(stack), colorHex, expectedHex, "");
- }
- }
- }
-
- if (TooltipInfoType.ACCESSORIES.isTooltipEnabledAndHasOrNullWarning(internalID)) {
- Pair<AccessoryReport, String> report = AccessoriesHelper.calculateReport4Accessory(internalID);
-
- if (report.left() != AccessoryReport.INELIGIBLE) {
- MutableText title = Text.literal(String.format("%-19s", "Accessory: ")).withColor(0xf57542);
-
- Text stateText = switch (report.left()) {
- case HAS_HIGHEST_TIER -> Text.literal("✔ Collected").formatted(Formatting.GREEN);
- case IS_GREATER_TIER -> Text.literal("✦ Upgrade ").withColor(0x218bff).append(Text.literal(report.right()).withColor(0xf8f8ff));
- case HAS_GREATER_TIER -> Text.literal("↑ Upgradable ").withColor(0xf8d048).append(Text.literal(report.right()).withColor(0xf8f8ff));
- case OWNS_BETTER_TIER -> Text.literal("↓ Downgrade ").formatted(Formatting.GRAY).append(Text.literal(report.right()).withColor(0xf8f8ff));
- case MISSING -> Text.literal("✖ Missing ").formatted(Formatting.RED).append(Text.literal(report.right()).withColor(0xf8f8ff));
-
- //Should never be the case
- default -> Text.literal("? Unknown").formatted(Formatting.GRAY);
- };
-
- lines.add(title.append(stateText));
- }
- }
- }
-
+ /**
+ * Gets the NEU id from an id and an api id.
+ * @param id the id of the skyblock item, gotten from {@link de.hysky.skyblocker.utils.ItemUtils#getItemId(net.minecraft.item.ItemStack) ItemUtils#getItemId(ItemStack)} or {@link net.minecraft.item.ItemStack#getSkyblockId() ItemStack#getSkyblockId()}
+ * @param apiId the api id of the skyblock item, matching the id of the item on the Skyblocker api, gotten from {@link net.minecraft.item.ItemStack#getSkyblockApiId() ItemStack#getSkyblockApiId()}
+ * @return the NEU id of the skyblock item, matching the id of the item gotten from {@link io.github.moulberry.repo.data.NEUItem#getSkyblockItemId() NEUItem#getSkyblockItemId()} or {@link net.minecraft.item.ItemStack#getNeuName() ItemStack#getNeuName()},
+ * or an empty string if either id or apiId is null
+ */
@NotNull
- public static String getNeuName(String internalID, String neuName) {
- switch (internalID) {
+ public static String getNeuName(String id, String apiId) {
+ if (id == null || apiId == null) return "";
+ switch (id) {
case "PET" -> {
- neuName = neuName.replaceAll("LVL_\\d*_", "");
- String[] parts = neuName.split("_");
+ apiId = apiId.replaceAll("LVL_\\d*_", "");
+ String[] parts = apiId.split("_");
String type = parts[0];
- neuName = neuName.replaceAll(type + "_", "");
- neuName = neuName + "-" + type;
- neuName = neuName.replace("UNCOMMON", "1")
+ apiId = apiId.replaceAll(type + "_", "");
+ apiId = apiId + "-" + type;
+ apiId = apiId.replace("UNCOMMON", "1")
.replace("COMMON", "0")
.replace("RARE", "2")
.replace("EPIC", "3")
@@ -271,20 +48,19 @@ public class ItemTooltip {
.replace("MYTHIC", "5")
.replace("-", ";");
}
- case "RUNE" -> neuName = neuName.replaceAll("_(?!.*_)", ";");
- case "POTION" -> neuName = "";
+ case "RUNE" -> apiId = apiId.replaceAll("_(?!.*_)", ";");
+ case "POTION" -> apiId = "";
case "ATTRIBUTE_SHARD" ->
- neuName = internalID + "+" + neuName.replace("SHARD-", "").replaceAll("_(?!.*_)", ";");
- default -> neuName = neuName.replace(":", "-");
- }
- return neuName;
- }
-
- private static void addExoticTooltip(List<Text> lines, String internalID, NbtCompound customData, String colorHex, String expectedHex, String existingTooltip) {
- if (expectedHex != null && !colorHex.equalsIgnoreCase(expectedHex) && !ExoticTooltip.isException(internalID, colorHex) && !ExoticTooltip.intendedDyed(customData)) {
- final ExoticTooltip.DyeType type = ExoticTooltip.checkDyeType(colorHex);
- lines.add(1, Text.literal(existingTooltip + Formatting.DARK_GRAY + "(").append(type.getTranslatedText()).append(Formatting.DARK_GRAY + ")"));
- }
+ apiId = id + "+" + apiId.replace("SHARD-", "").replaceAll("_(?!.*_)", ";");
+ case "NEW_YEAR_CAKE" -> apiId = id + "+" + apiId.replace("NEW_YEAR_CAKE_", "");
+ case "PARTY_HAT_CRAB_ANIMATED" -> apiId = "PARTY_HAT_CRAB_" + apiId.replace("PARTY_HAT_CRAB_ANIMATED_", "") + "_ANIMATED";
+ case "CRIMSON_HELMET", "CRIMSON_CHESTPLATE", "CRIMSON_LEGGINGS", "CRIMSON_BOOTS",
+ "AURORA_HELMET", "AURORA_CHESTPLATE", "AURORA_LEGGINGS", "AURORA_BOOTS",
+ "TERROR_HELMET", "TERROR_CHESTPLATE", "TERROR_LEGGINGS", "TERROR_BOOTS" -> apiId = id;
+ case "MIDAS_SWORD", "MIDAS_STAFF" -> apiId = id;
+ default -> apiId = apiId.replace(":", "-");
+ }
+ return apiId;
}
public static void nullWarning() {
@@ -294,69 +70,7 @@ public class ItemTooltip {
}
}
- // TODO What in the world is this?
- public static String getInternalNameFromNBT(ItemStack stack, boolean internalIDOnly) {
- NbtCompound customData = ItemUtils.getCustomData(stack);
-
- if (customData == null || !customData.contains(ItemUtils.ID, NbtElement.STRING_TYPE)) {
- return null;
- }
- String internalName = customData.getString(ItemUtils.ID);
-
- if (internalIDOnly) {
- return internalName;
- }
-
- // Transformation to API format.
- if (customData.contains("is_shiny")) {
- return "ISSHINY_" + internalName;
- }
-
- switch (internalName) {
- case "ENCHANTED_BOOK" -> {
- if (customData.contains("enchantments")) {
- NbtCompound enchants = customData.getCompound("enchantments");
- Optional<String> firstEnchant = enchants.getKeys().stream().findFirst();
- String enchant = firstEnchant.orElse("");
- return "ENCHANTMENT_" + enchant.toUpperCase(Locale.ENGLISH) + "_" + enchants.getInt(enchant);
- }
- }
- case "PET" -> {
- if (customData.contains("petInfo")) {
- JsonObject petInfo = SkyblockerMod.GSON.fromJson(customData.getString("petInfo"), JsonObject.class);
- return "LVL_1_" + petInfo.get("tier").getAsString() + "_" + petInfo.get("type").getAsString();
- }
- }
- case "POTION" -> {
- String enhanced = customData.contains("enhanced") ? "_ENHANCED" : "";
- String extended = customData.contains("extended") ? "_EXTENDED" : "";
- String splash = customData.contains("splash") ? "_SPLASH" : "";
- if (customData.contains("potion") && customData.contains("potion_level")) {
- return (customData.getString("potion") + "_" + internalName + "_" + customData.getInt("potion_level")
- + enhanced + extended + splash).toUpperCase(Locale.ENGLISH);
- }
- }
- case "RUNE" -> {
- if (customData.contains("runes")) {
- NbtCompound runes = customData.getCompound("runes");
- Optional<String> firstRunes = runes.getKeys().stream().findFirst();
- String rune = firstRunes.orElse("");
- return rune.toUpperCase(Locale.ENGLISH) + "_RUNE_" + runes.getInt(rune);
- }
- }
- case "ATTRIBUTE_SHARD" -> {
- if (customData.contains("attributes")) {
- NbtCompound shards = customData.getCompound("attributes");
- Optional<String> firstShards = shards.getKeys().stream().findFirst();
- String shard = firstShards.orElse("");
- return internalName + "-" + shard.toUpperCase(Locale.ENGLISH) + "_" + shards.getInt(shard);
- }
- }
- }
- return internalName;
- }
-
- private static Text getCoinsMessage(double price, int count) {
+ public static Text getCoinsMessage(double price, int count) {
// Format the price string once
String priceString = String.format(Locale.ENGLISH, "%1$,.1f", price);
@@ -367,47 +81,9 @@ public class ItemTooltip {
// If count is greater than 1, include the "each" information
String priceStringTotal = String.format(Locale.ENGLISH, "%1$,.1f", price * count);
- MutableText message = Text.literal(priceStringTotal + " Coins ").formatted(Formatting.DARK_AQUA);
- message.append(Text.literal("(" + priceString + " each)").formatted(Formatting.GRAY));
-
- return message;
- }
-
- private static Text getMotesMessage(int price, int count) {
- float motesMultiplier = SkyblockerConfigManager.get().otherLocations.rift.mcGrubberStacks * 0.05f + 1;
-
- // Calculate the total price
- int totalPrice = price * count;
- String totalPriceString = String.format(Locale.ENGLISH, "%1$,.1f", totalPrice * motesMultiplier);
-
- // If count is 1, return a simple message
- if (count == 1) {
- return Text.literal(totalPriceString.replace(".0", "") + " Motes").formatted(Formatting.DARK_AQUA);
- }
-
- // If count is greater than 1, include the "each" information
- String eachPriceString = String.format(Locale.ENGLISH, "%1$,.1f", price * motesMultiplier);
- MutableText message = Text.literal(totalPriceString.replace(".0", "") + " Motes ").formatted(Formatting.DARK_AQUA);
- message.append(Text.literal("(" + eachPriceString.replace(".0", "") + " each)").formatted(Formatting.GRAY));
-
- return message;
- }
-
- //This is static to not create a new text object for each line in every item
- private static final Text BUMPY_LINE = Text.literal("-----------------").formatted(Formatting.DARK_GRAY, Formatting.STRIKETHROUGH);
-
- private static void smoothenLines(List<Text> lines) {
- for (int i = 0; i < lines.size(); i++) {
- List<Text> lineSiblings = lines.get(i).getSiblings();
- //Compare the first sibling rather than the whole object as the style of the root object can change while visually staying the same
- if (lineSiblings.size() == 1 && lineSiblings.getFirst().equals(BUMPY_LINE)) {
- lines.set(i, createSmoothLine());
- }
- }
- }
- public static Text createSmoothLine() {
- return Text.literal(" ").formatted(Formatting.DARK_GRAY, Formatting.STRIKETHROUGH, Formatting.BOLD);
+ return Text.literal(priceStringTotal + " Coins ").formatted(Formatting.DARK_AQUA)
+ .append(Text.literal("(" + priceString + " each)").formatted(Formatting.GRAY));
}
// If these options is true beforehand, the client will get first data of these options while loading.
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipAdder.java
new file mode 100644
index 00000000..9bd63adc
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipAdder.java
@@ -0,0 +1,45 @@
+package de.hysky.skyblocker.skyblock.item.tooltip;
+
+import de.hysky.skyblocker.utils.render.gui.AbstractContainerMatcher;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Extend this class and add it to {@link TooltipManager#adders} to add additional text to tooltips.
+ */
+public abstract class TooltipAdder extends AbstractContainerMatcher {
+ /**
+ * The priority of this adder. Lower priority means it will be applied first.
+ * @apiNote Consider taking this value on your class' constructor and setting it from {@link TooltipManager#adders} to make it easy to read and maintain.
+ */
+ public final int priority;
+
+ protected TooltipAdder(String titlePattern, int priority) {
+ super(titlePattern);
+ this.priority = priority;
+ }
+
+ protected TooltipAdder(Pattern titlePattern, int priority) {
+ super(titlePattern);
+ this.priority = priority;
+ }
+
+ /**
+ * Creates a TooltipAdder that will be applied to all screens.
+ */
+ protected TooltipAdder(int priority) {
+ super();
+ this.priority = priority;
+ }
+
+ /**
+ * @implNote The first element of the lines list holds the item's display name,
+ * as it's a list of all lines that will be displayed in the tooltip.
+ */
+ public abstract void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines);
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java
index 88f09496..d82b2682 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java
@@ -1,6 +1,9 @@
package de.hysky.skyblocker.skyblock.item.tooltip;
+import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.stream.JsonReader;
import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.config.SkyblockerConfig;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
@@ -8,6 +11,7 @@ import de.hysky.skyblocker.config.configs.GeneralConfig;
import de.hysky.skyblocker.utils.Http;
import de.hysky.skyblocker.utils.Utils;
+import java.io.StringReader;
import java.net.http.HttpHeaders;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -160,7 +164,12 @@ public enum TooltipInfoType implements Runnable {
if (this.hash == hash) return;
else this.hash = hash;
}
- data = SkyblockerMod.GSON.fromJson(Http.sendGetRequest(address), JsonObject.class);
+ String response = Http.sendGetRequest(address);
+ if (response.trim().startsWith("<!DOCTYPE") || response.trim().startsWith("<html")) {
+ ItemTooltip.LOGGER.warn("[Skyblocker] Received HTML content for " + this.name() + ". Expected JSON.");
+ return;
+ }
+ data = SkyblockerMod.GSON.fromJson(response, JsonObject.class);
if (callback != null) callback.accept(data);
} catch (Exception e) {
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java
new file mode 100644
index 00000000..e3a2ef04
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java
@@ -0,0 +1,87 @@
+package de.hysky.skyblocker.skyblock.item.tooltip;
+
+import de.hysky.skyblocker.mixins.accessors.HandledScreenAccessor;
+import de.hysky.skyblocker.skyblock.chocolatefactory.ChocolateFactorySolver;
+import de.hysky.skyblocker.skyblock.item.tooltip.adders.*;
+import de.hysky.skyblocker.utils.Utils;
+import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback;
+import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+public class TooltipManager {
+ private static final TooltipAdder[] adders = new TooltipAdder[]{
+ new LineSmoothener(), // Applies before anything else
+ new SupercraftReminder(),
+ new ChocolateFactorySolver.Tooltip(),
+ new NpcPriceTooltip(1),
+ new BazaarPriceTooltip(2),
+ new LBinTooltip(3),
+ new AvgBinTooltip(4),
+ new DungeonQualityTooltip(5),
+ new MotesTooltip(6),
+ new ObtainedDateTooltip(7),
+ new MuseumTooltip(8),
+ new ColorTooltip(9),
+ new AccessoryTooltip(10),
+ };
+ private static final ArrayList<TooltipAdder> currentScreenAdders = new ArrayList<>();
+
+ private TooltipManager() {
+ }
+
+ public static void init() {
+ ItemTooltipCallback.EVENT.register((stack, tooltipContext, tooltipType, lines) -> {
+ if (MinecraftClient.getInstance().currentScreen instanceof HandledScreen<?> handledScreen) {
+ addToTooltip(((HandledScreenAccessor) handledScreen).getFocusedSlot(), stack, lines);
+ } else {
+ addToTooltip(null, stack, lines);
+ }
+ });
+ ScreenEvents.AFTER_INIT.register((client, screen, width, height) -> {
+ onScreenChange(screen);
+ ScreenEvents.remove(screen).register(ignored -> currentScreenAdders.clear());
+ });
+ }
+
+ private static void onScreenChange(Screen screen) {
+ final String title = screen.getTitle().getString();
+ currentScreenAdders.clear();
+ for (TooltipAdder adder : adders) {
+ if (adder.titlePattern == null || adder.titlePattern.matcher(title).find()) {
+ currentScreenAdders.add(adder);
+ }
+ }
+ currentScreenAdders.sort(Comparator.comparingInt(adder -> adder.priority));
+ }
+
+ /**
+ * <p>Adds additional text from all adders that are applicable to the current screen.
+ * This method is run on each tooltip render, so don't do any heavy calculations here.</p>
+ *
+ * <p>If you want to add info to the tooltips of multiple items, consider using a switch statement with {@code focusedSlot.getIndex()}</p>
+ *
+ * @param focusedSlot The slot that is currently focused by the cursor.
+ * @param stack The stack to render the tooltip for.
+ * @param lines The tooltip lines of the focused item. This includes the display name, as it's a part of the tooltip (at index 0).
+ * @return The lines list itself after all adders have added their text.
+ * @deprecated This method is public only for the sake of the mixin. Don't call directly, not that there is any point to it.
+ */
+ @Deprecated
+ public static List<Text> addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ if (!Utils.isOnSkyblock()) return lines;
+ for (TooltipAdder adder : currentScreenAdders) {
+ adder.addToTooltip(focusedSlot, stack, lines);
+ }
+ return lines;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AccessoryTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AccessoryTooltip.java
new file mode 100644
index 00000000..caed0e0e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AccessoryTooltip.java
@@ -0,0 +1,45 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.skyblock.item.tooltip.AccessoriesHelper;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import it.unimi.dsi.fastutil.Pair;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class AccessoryTooltip extends TooltipAdder {
+ public AccessoryTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ final String internalID = stack.getSkyblockId();
+ if (TooltipInfoType.ACCESSORIES.isTooltipEnabledAndHasOrNullWarning(internalID)) {
+ Pair<AccessoriesHelper.AccessoryReport, String> report = AccessoriesHelper.calculateReport4Accessory(internalID);
+
+ if (report.left() != AccessoriesHelper.AccessoryReport.INELIGIBLE) {
+ MutableText title = Text.literal(String.format("%-19s", "Accessory: ")).withColor(0xf57542);
+
+ Text stateText = switch (report.left()) {
+ case HAS_HIGHEST_TIER -> Text.literal("✔ Collected").formatted(Formatting.GREEN);
+ case IS_GREATER_TIER -> Text.literal("✦ Upgrade ").withColor(0x218bff).append(Text.literal(report.right()).withColor(0xf8f8ff));
+ case HAS_GREATER_TIER -> Text.literal("↑ Upgradable ").withColor(0xf8d048).append(Text.literal(report.right()).withColor(0xf8f8ff));
+ case OWNS_BETTER_TIER -> Text.literal("↓ Downgrade ").formatted(Formatting.GRAY).append(Text.literal(report.right()).withColor(0xf8f8ff));
+ case MISSING -> Text.literal("✖ Missing ").formatted(Formatting.RED).append(Text.literal(report.right()).withColor(0xf8f8ff));
+
+ //Should never be the case
+ default -> Text.literal("? Unknown").formatted(Formatting.GRAY);
+ };
+
+ lines.add(title.append(stateText));
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AvgBinTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AvgBinTooltip.java
new file mode 100644
index 00000000..d7a56b95
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AvgBinTooltip.java
@@ -0,0 +1,63 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.config.configs.GeneralConfig;
+import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class AvgBinTooltip extends TooltipAdder {
+ public AvgBinTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ String neuName = stack.getNeuName();
+ String internalID = stack.getSkyblockId();
+ if (neuName == null || internalID == null) return;
+
+ if (SkyblockerConfigManager.get().general.itemTooltip.enableAvgBIN) {
+ if (TooltipInfoType.ONE_DAY_AVERAGE.getData() == null || TooltipInfoType.THREE_DAY_AVERAGE.getData() == null) {
+ ItemTooltip.nullWarning();
+ } else {
+ /*
+ We are skipping check average prices for potions, runes
+ and enchanted books because there is no data for their in API.
+ */
+ if (!neuName.isEmpty() && LBinTooltip.lbinExist) {
+ GeneralConfig.Average type = ItemTooltip.config.avg;
+
+ // "No data" line because of API not keeping old data, it causes NullPointerException
+ if (type == GeneralConfig.Average.ONE_DAY || type == GeneralConfig.Average.BOTH) {
+ lines.add(
+ Text.literal(String.format("%-19s", "1 Day Avg. Price:"))
+ .formatted(Formatting.GOLD)
+ .append(TooltipInfoType.ONE_DAY_AVERAGE.getData().get(neuName) == null
+ ? Text.literal("No data").formatted(Formatting.RED)
+ : ItemTooltip.getCoinsMessage(TooltipInfoType.ONE_DAY_AVERAGE.getData().get(neuName).getAsDouble(), stack.getCount())
+ )
+ );
+ }
+ if (type == GeneralConfig.Average.THREE_DAY || type == GeneralConfig.Average.BOTH) {
+ lines.add(
+ Text.literal(String.format("%-19s", "3 Day Avg. Price:"))
+ .formatted(Formatting.GOLD)
+ .append(TooltipInfoType.THREE_DAY_AVERAGE.getData().get(neuName) == null
+ ? Text.literal("No data").formatted(Formatting.RED)
+ : ItemTooltip.getCoinsMessage(TooltipInfoType.THREE_DAY_AVERAGE.getData().get(neuName).getAsDouble(), stack.getCount())
+ )
+ );
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/BazaarPriceTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/BazaarPriceTooltip.java
new file mode 100644
index 00000000..d2fa563b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/BazaarPriceTooltip.java
@@ -0,0 +1,57 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import com.google.gson.JsonObject;
+import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class BazaarPriceTooltip extends TooltipAdder {
+ public static boolean bazaarExist = false;
+
+ public BazaarPriceTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ bazaarExist = false;
+ final String internalID = stack.getSkyblockId();
+ if (internalID == null) return;
+ String name = stack.getSkyblockApiId();
+ if (name == null) return;
+
+ if (name.startsWith("ISSHINY_")) name = "SHINY_" + internalID;
+
+ if (TooltipInfoType.BAZAAR.isTooltipEnabledAndHasOrNullWarning(name)) {
+ int amount;
+ if (lines.get(1).getString().endsWith("Sack")) {
+ //The amount is in the 2nd sibling of the 3rd line of the lore. here V
+ //Example line: empty[style={color=dark_purple,!italic}, siblings=[literal{Stored: }[style={color=gray}], literal{0}[style={color=dark_gray}], literal{/20k}[style={color=gray}]]
+ String line = lines.get(3).getSiblings().get(1).getString().replace(",", "");
+ amount = NumberUtils.isParsable(line) && !line.equals("0") ? Integer.parseInt(line) : stack.getCount();
+ } else {
+ amount = stack.getCount();
+ }
+ JsonObject getItem = TooltipInfoType.BAZAAR.getData().getAsJsonObject(name);
+ lines.add(Text.literal(String.format("%-18s", "Bazaar buy Price:"))
+ .formatted(Formatting.GOLD)
+ .append(getItem.get("buyPrice").isJsonNull()
+ ? Text.literal("No data").formatted(Formatting.RED)
+ : ItemTooltip.getCoinsMessage(getItem.get("buyPrice").getAsDouble(), amount)));
+ lines.add(Text.literal(String.format("%-19s", "Bazaar sell Price:"))
+ .formatted(Formatting.GOLD)
+ .append(getItem.get("sellPrice").isJsonNull()
+ ? Text.literal("No data").formatted(Formatting.RED)
+ : ItemTooltip.getCoinsMessage(getItem.get("sellPrice").getAsDouble(), amount)));
+ bazaarExist = true;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ColorTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ColorTooltip.java
new file mode 100644
index 00000000..7546d37a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ColorTooltip.java
@@ -0,0 +1,126 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.component.DataComponentTypes;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.StringIdentifiable;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+public class ColorTooltip extends TooltipAdder {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ColorTooltip.class);
+
+ public ColorTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ final String internalID = stack.getSkyblockId();
+ if (TooltipInfoType.COLOR.isTooltipEnabledAndHasOrNullWarning(internalID) && stack.contains(DataComponentTypes.DYED_COLOR)) {
+ //DyedColorComponent#getColor can be ARGB so we mask out the alpha bits
+ int dyeColor = stack.get(DataComponentTypes.DYED_COLOR).rgb() & 0x00FFFFFF;
+ String colorHex = String.format("%06X", dyeColor);
+ String expectedHex = getExpectedHex(internalID);
+
+ boolean correctLine = false;
+ for (Text text : lines) {
+ String existingTooltip = text.getString() + " ";
+ if (existingTooltip.startsWith("Color: ")) {
+ correctLine = true;
+
+ addExoticTooltip(lines, internalID, ItemUtils.getCustomData(stack), colorHex, expectedHex, existingTooltip);
+ break;
+ }
+ }
+
+ if (!correctLine) {
+ addExoticTooltip(lines, internalID, ItemUtils.getCustomData(stack), colorHex, expectedHex, "");
+ }
+ }
+ }
+
+ private static void addExoticTooltip(List<Text> lines, String internalID, NbtCompound customData, String colorHex, String expectedHex, String existingTooltip) {
+ if (expectedHex != null && !colorHex.equalsIgnoreCase(expectedHex) && !isException(internalID, colorHex) && !intendedDyed(customData)) {
+ final DyeType type = checkDyeType(colorHex);
+ lines.add(1, Text.literal(existingTooltip + Formatting.DARK_GRAY + "(").append(type.getTranslatedText()).append(Formatting.DARK_GRAY + ")"));
+ }
+ }
+
+ public static String getExpectedHex(String id) {
+ String color = TooltipInfoType.COLOR.getData().get(id).getAsString();
+ if (color != null) {
+ String[] RGBValues = color.split(",");
+ return String.format("%02X%02X%02X", Integer.parseInt(RGBValues[0]), Integer.parseInt(RGBValues[1]), Integer.parseInt(RGBValues[2]));
+ } else {
+ LOGGER.warn("[Skyblocker Exotics] No expected color data found for id {}", id);
+ return null;
+ }
+ }
+
+ public static boolean isException(String id, String hex) {
+ return switch (id) {
+ case String it when it.startsWith("LEATHER") || it.equals("GHOST_BOOTS") || Constants.SEYMOUR_IDS.contains(it) -> true;
+ case String it when it.startsWith("RANCHER") -> Constants.RANCHERS.contains(hex);
+ case String it when it.contains("ADAPTIVE_CHESTPLATE") -> Constants.ADAPTIVE_CHEST.contains(hex);
+ case String it when it.contains("ADAPTIVE") -> Constants.ADAPTIVE.contains(hex);
+ case String it when it.contains("REAPER") -> Constants.REAPER.contains(hex);
+ case String it when it.contains("FAIRY") -> Constants.FAIRY_HEXES.contains(hex);
+ case String it when it.contains("CRYSTAL") -> Constants.CRYSTAL_HEXES.contains(hex);
+ case String it when it.contains("SPOOK") -> Constants.SPOOK.contains(hex);
+ default -> false;
+ };
+ }
+
+ public static DyeType checkDyeType(String hex) {
+ return switch (hex) {
+ case String it when Constants.CRYSTAL_HEXES.contains(it) -> DyeType.CRYSTAL;
+ case String it when Constants.FAIRY_HEXES.contains(it) -> DyeType.FAIRY;
+ case String it when Constants.OG_FAIRY_HEXES.contains(it) -> DyeType.OG_FAIRY;
+ case String it when Constants.SPOOK.contains(it) -> DyeType.SPOOK;
+ case String it when Constants.GLITCHED.contains(it) -> DyeType.GLITCHED;
+ default -> DyeType.EXOTIC;
+ };
+ }
+
+ public static boolean intendedDyed(NbtCompound customData) {
+ return customData.contains("dye_item");
+ }
+
+ public enum DyeType implements StringIdentifiable {
+ CRYSTAL("crystal", Formatting.AQUA),
+ FAIRY("fairy", Formatting.LIGHT_PURPLE),
+ OG_FAIRY("og_fairy", Formatting.DARK_PURPLE),
+ SPOOK("spook", Formatting.RED),
+ GLITCHED("glitched", Formatting.BLUE),
+ EXOTIC("exotic", Formatting.GOLD);
+ private final String name;
+ private final Formatting formatting;
+
+ DyeType(String name, Formatting formatting) {
+ this.name = name;
+ this.formatting = formatting;
+ }
+
+ @Override
+ public String asString() {
+ return name;
+ }
+
+ public MutableText getTranslatedText() {
+ return Text.translatable("skyblocker.exotic." + name).formatted(formatting);
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/DungeonQualityTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/DungeonQualityTooltip.java
new file mode 100644
index 00000000..0b1d993d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/DungeonQualityTooltip.java
@@ -0,0 +1,59 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class DungeonQualityTooltip extends TooltipAdder {
+ public DungeonQualityTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ if (!SkyblockerConfigManager.get().general.itemTooltip.dungeonQuality) return;
+ NbtCompound customData = ItemUtils.getCustomData(stack);
+ if (customData == null || !customData.contains("baseStatBoostPercentage")) return;
+ int baseStatBoostPercentage = customData.getInt("baseStatBoostPercentage");
+ boolean maxQuality = baseStatBoostPercentage == 50;
+ if (maxQuality) {
+ lines.add(Text.literal(String.format("%-17s", "Item Quality:") + baseStatBoostPercentage + "/50").formatted(Formatting.RED).formatted(Formatting.BOLD));
+ } else {
+ lines.add(Text.literal(String.format("%-21s", "Item Quality:") + baseStatBoostPercentage + "/50").formatted(Formatting.BLUE));
+ }
+
+ if (customData.contains("item_tier")) { // sometimes it just isn't here?
+ int itemTier = customData.getInt("item_tier");
+ if (maxQuality) {
+ lines.add(Text.literal(String.format("%-17s", "Floor Tier:") + itemTier + " (" + getItemTierFloor(itemTier) + ")").formatted(Formatting.RED).formatted(Formatting.BOLD));
+ } else {
+ lines.add(Text.literal(String.format("%-21s", "Floor Tier:") + itemTier + " (" + getItemTierFloor(itemTier) + ")").formatted(Formatting.BLUE));
+ }
+ }
+ }
+
+ final String getItemTierFloor(int tier) {
+ return switch (tier) {
+ case 0 -> "E";
+ case 1 -> "F1";
+ case 2 -> "F2";
+ case 3 -> "F3";
+ case 4 -> "F4/M1";
+ case 5 -> "F5/M2";
+ case 6 -> "F6/M3";
+ case 7 -> "F7/M4";
+ case 8 -> "M5";
+ case 9 -> "M6";
+ case 10 -> "M7";
+ default -> "Unknown";
+ };
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LBinTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LBinTooltip.java
new file mode 100644
index 00000000..e6930c32
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LBinTooltip.java
@@ -0,0 +1,40 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class LBinTooltip extends TooltipAdder {
+ public static boolean lbinExist = false;
+
+ public LBinTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ lbinExist = false;
+ final String internalID = stack.getSkyblockId();
+ if (internalID == null) return;
+ String name = stack.getSkyblockApiId();
+ if (name == null) return;
+
+ if (name.startsWith("ISSHINY_")) name = "SHINY_" + internalID;
+
+ // bazaarOpened & bazaarExist check for lbin, because Skytils keeps some bazaar item data in lbin api
+
+ if (TooltipInfoType.LOWEST_BINS.isTooltipEnabledAndHasOrNullWarning(name) && !BazaarPriceTooltip.bazaarExist) {
+ lines.add(Text.literal(String.format("%-19s", "Lowest BIN Price:"))
+ .formatted(Formatting.GOLD)
+ .append(ItemTooltip.getCoinsMessage(TooltipInfoType.LOWEST_BINS.getData().get(name).getAsDouble(), stack.getCount())));
+ lbinExist = true;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LineSmoothener.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LineSmoothener.java
new file mode 100644
index 00000000..0e997834
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LineSmoothener.java
@@ -0,0 +1,34 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class LineSmoothener extends TooltipAdder {
+ //This is static to not create a new text object for each line in every item
+ private static final Text BUMPY_LINE = Text.literal("-----------------").formatted(Formatting.DARK_GRAY, Formatting.STRIKETHROUGH);
+
+ public static Text createSmoothLine() {
+ return Text.literal(" ").formatted(Formatting.DARK_GRAY, Formatting.STRIKETHROUGH, Formatting.BOLD);
+ }
+
+ public LineSmoothener() {
+ super(Integer.MIN_VALUE);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ for (int i = 0; i < lines.size(); i++) {
+ List<Text> lineSiblings = lines.get(i).getSiblings();
+ //Compare the first sibling rather than the whole object as the style of the root object can change while visually staying the same
+ if (lineSiblings.size() == 1 && lineSiblings.getFirst().equals(BUMPY_LINE)) {
+ lines.set(i, createSmoothLine());
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MotesTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MotesTooltip.java
new file mode 100644
index 00000000..a0aa8d94
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MotesTooltip.java
@@ -0,0 +1,50 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Locale;
+
+public class MotesTooltip extends TooltipAdder {
+ public MotesTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ final String internalID = stack.getSkyblockId();
+ if (internalID != null && TooltipInfoType.MOTES.isTooltipEnabledAndHasOrNullWarning(internalID)) {
+ lines.add(Text.literal(String.format("%-20s", "Motes Price:"))
+ .formatted(Formatting.LIGHT_PURPLE)
+ .append(getMotesMessage(TooltipInfoType.MOTES.getData().get(internalID).getAsInt(), stack.getCount())));
+ }
+ }
+
+ private static Text getMotesMessage(int price, int count) {
+ float motesMultiplier = SkyblockerConfigManager.get().otherLocations.rift.mcGrubberStacks * 0.05f + 1;
+
+ // Calculate the total price
+ int totalPrice = price * count;
+ String totalPriceString = String.format(Locale.ENGLISH, "%1$,.1f", totalPrice * motesMultiplier);
+
+ // If count is 1, return a simple message
+ if (count == 1) {
+ return Text.literal(totalPriceString.replace(".0", "") + " Motes").formatted(Formatting.DARK_AQUA);
+ }
+
+ // If count is greater than 1, include the "each" information
+ String eachPriceString = String.format(Locale.ENGLISH, "%1$,.1f", price * motesMultiplier);
+ MutableText message = Text.literal(totalPriceString.replace(".0", "") + " Motes ").formatted(Formatting.DARK_AQUA);
+ message.append(Text.literal("(" + eachPriceString.replace(".0", "") + " each)").formatted(Formatting.GRAY));
+
+ return message;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MuseumTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MuseumTooltip.java
new file mode 100644
index 00000000..5c21d2df
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MuseumTooltip.java
@@ -0,0 +1,49 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.skyblock.item.MuseumItemCache;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class MuseumTooltip extends TooltipAdder {
+ public MuseumTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ final String internalID = stack.getSkyblockId();
+ if (TooltipInfoType.MUSEUM.isTooltipEnabledAndHasOrNullWarning(internalID)) {
+ String itemCategory = TooltipInfoType.MUSEUM.getData().get(internalID).getAsString();
+ String format = switch (itemCategory) {
+ case "Weapons" -> "%-18s";
+ case "Armor" -> "%-19s";
+ default -> "%-20s";
+ };
+
+ //Special case the special category so that it doesn't always display not donated
+ if (itemCategory.equals("Special")) {
+ lines.add(Text.literal(String.format(format, "Museum: (" + itemCategory + ")"))
+ .formatted(Formatting.LIGHT_PURPLE));
+ } else {
+ NbtCompound customData = ItemUtils.getCustomData(stack);
+ boolean isInMuseum = (customData.contains("donated_museum") && customData.getBoolean("donated_museum")) || MuseumItemCache.hasItemInMuseum(internalID);
+
+ Formatting donatedIndicatorFormatting = isInMuseum ? Formatting.GREEN : Formatting.RED;
+
+ lines.add(Text.literal(String.format(format, "Museum (" + itemCategory + "):"))
+ .formatted(Formatting.LIGHT_PURPLE)
+ .append(Text.literal(isInMuseum ? "✔" : "✖").formatted(donatedIndicatorFormatting, Formatting.BOLD))
+ .append(Text.literal(isInMuseum ? " Donated" : " Not Donated").formatted(donatedIndicatorFormatting)));
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java
new file mode 100644
index 00000000..672201d5
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java
@@ -0,0 +1,28 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class NpcPriceTooltip extends TooltipAdder {
+ public NpcPriceTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ final String internalID = stack.getSkyblockId();
+ if (internalID != null && TooltipInfoType.NPC.isTooltipEnabledAndHasOrNullWarning(internalID)) {
+ lines.add(Text.literal(String.format("%-21s", "NPC Sell Price:"))
+ .formatted(Formatting.YELLOW)
+ .append(ItemTooltip.getCoinsMessage(TooltipInfoType.NPC.getData().get(internalID).getAsDouble(), stack.getCount())));
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ObtainedDateTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ObtainedDateTooltip.java
new file mode 100644
index 00000000..9f405c58
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ObtainedDateTooltip.java
@@ -0,0 +1,73 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtElement;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
+import java.util.List;
+import java.util.Locale;
+
+public class ObtainedDateTooltip extends TooltipAdder {
+ private static final DateTimeFormatter OBTAINED_DATE_FORMATTER = DateTimeFormatter.ofPattern("MMMM d, yyyy").withZone(ZoneId.systemDefault()).localizedBy(Locale.ENGLISH);
+ private static final DateTimeFormatter OLD_OBTAINED_DATE_FORMAT = DateTimeFormatter.ofPattern("M/d/yy h:m a").withZone(ZoneId.of("UTC")).localizedBy(Locale.ENGLISH);
+
+ public ObtainedDateTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ if (TooltipInfoType.OBTAINED.isTooltipEnabled()) {
+ String timestamp = getTimestamp(stack);
+
+ if (!timestamp.isEmpty()) {
+ lines.add(Text.empty()
+ .append(Text.literal(String.format("%-21s", "Obtained: ")).formatted(Formatting.LIGHT_PURPLE))
+ .append(Text.literal(timestamp).formatted(Formatting.RED)));
+ }
+ }
+ }
+
+ /**
+ * This method converts the "timestamp" variable into the same date format as Hypixel represents it in the museum.
+ * Currently, there are two types of string timestamps the legacy which is built like this
+ * "dd/MM/yy hh:mm" ("25/04/20 16:38") and the current which is built like this
+ * "MM/dd/yy hh:mm aa" ("12/24/20 11:08 PM"). Since Hypixel transforms the two formats into one format without
+ * taking into account of their formats, we do the same. The final result looks like this
+ * "MMMM dd, yyyy" (December 24, 2020).
+ * Since the legacy format has a 25 as "month" SimpleDateFormat converts the 25 into 2 years and 1 month and makes
+ * "25/04/20 16:38" -> "January 04, 2022" instead of "April 25, 2020".
+ * This causes the museum rank to be much worse than it should be.
+ * <p>
+ * This also handles the long timestamp format introduced in January 2024 where the timestamp is in epoch milliseconds.
+ *
+ * @param stack the item under the pointer
+ * @return if the item have a "Timestamp" it will be shown formated on the tooltip
+ */
+ public static String getTimestamp(ItemStack stack) {
+ NbtCompound customData = ItemUtils.getCustomData(stack);
+
+ if (customData != null && customData.contains("timestamp", NbtElement.LONG_TYPE)) {
+ Instant date = Instant.ofEpochMilli(customData.getLong("timestamp"));
+ return OBTAINED_DATE_FORMATTER.format(date);
+ }
+
+ if (customData != null && customData.contains("timestamp", NbtElement.STRING_TYPE)) {
+ TemporalAccessor date = OLD_OBTAINED_DATE_FORMAT.parse(customData.getString("timestamp"));
+ return OBTAINED_DATE_FORMATTER.format(date);
+ }
+
+ return "";
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/SupercraftReminder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/SupercraftReminder.java
new file mode 100644
index 00000000..47d2bd48
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/SupercraftReminder.java
@@ -0,0 +1,32 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class SupercraftReminder extends TooltipAdder {
+ private static final byte SUPERCRAFT_SLOT = 32;
+ private static final byte RECIPE_RESULT_SLOT = 25;
+
+ public SupercraftReminder() {
+ super(Pattern.compile("^.+ Recipe$"), Integer.MIN_VALUE);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ if (focusedSlot == null || focusedSlot.id != SUPERCRAFT_SLOT || !stack.isOf(Items.GOLDEN_PICKAXE)) return;
+ String uuid = ItemUtils.getItemUuid(focusedSlot.inventory.getStack(RECIPE_RESULT_SLOT));
+ if (!uuid.isEmpty()) return; //Items with UUID can't be stacked, and therefore the shift-click feature doesn't matter
+ int index = lines.size() - 1;
+ if (lines.get(lines.size() - 2).getString().equals("Recipe not unlocked!")) index--; //Place it right below the "Right-Click to set amount" line
+ lines.add(index, Text.literal("Shift-Click to maximize the amount!").formatted(Formatting.GOLD));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java
new file mode 100644
index 00000000..10e12ace
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java
@@ -0,0 +1,92 @@
+package de.hysky.skyblocker.skyblock.itemlist;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import java.util.List;
+
+public class ItemListTab extends ItemListWidget.TabContainerWidget {
+
+ private SearchResultsWidget results;
+ private final MinecraftClient client;
+ private TextFieldWidget searchField;
+
+ public ItemListTab(int x, int y, MinecraftClient client, TextFieldWidget searchField) {
+ super(x, y, Text.literal("Item List Tab"));
+ this.client = client;
+ this.searchField = searchField;
+ if (ItemRepository.filesImported()) {
+ this.results = new SearchResultsWidget(this.client, x - 9, y - 9 );
+ this.results.updateSearchResult(searchField == null ? "": this.searchField.getText());
+ }
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return List.of(results, searchField);
+ }
+
+ @Override
+ protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
+ MatrixStack matrices = context.getMatrices();
+ matrices.push();
+ matrices.translate(0.0D, 0.0D, 100.0D);
+ RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
+ int x = getX();
+ int y = getY();
+
+ // all coordinates offseted -9
+ if (!ItemRepository.filesImported() && !this.searchField.isFocused() && this.searchField.getText().isEmpty()) {
+ Text hintText = (Text.literal("Loading...")).formatted(Formatting.ITALIC).formatted(Formatting.GRAY);
+ context.drawTextWithShadow(this.client.textRenderer, hintText, x + 16, y + 7, -1);
+ } else if (!this.searchField.isFocused() && this.searchField.getText().isEmpty()) {
+ Text hintText = (Text.translatable("gui.recipebook.search_hint")).formatted(Formatting.ITALIC).formatted(Formatting.GRAY);
+ context.drawTextWithShadow(this.client.textRenderer, hintText, x + 16, y + 7, -1);
+ } else {
+ this.searchField.render(context, mouseX, mouseY, delta);
+ }
+ if (ItemRepository.filesImported()) {
+ if (results == null) {
+ this.results = new SearchResultsWidget(this.client, x - 9, y - 9);
+ }
+ this.results.updateSearchResult(this.searchField.getText());
+ this.results.render(context, mouseX, mouseY, delta);
+ }
+ matrices.pop();
+ }
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {}
+
+ public void setSearchField(TextFieldWidget searchField) {
+ this.searchField = searchField;
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (!visible) return false;
+ if (this.searchField.mouseClicked(mouseX, mouseY, button) && this.results != null) {
+ this.results.closeRecipeView();
+ this.searchField.setFocused(true);
+ return true;
+ } else if (results != null) {
+ this.searchField.setFocused(false);
+
+ return this.results.mouseClicked(mouseX, mouseY, button);
+ }
+
+ return false;
+ }
+
+ @Override
+ public void drawTooltip(DrawContext context, int mouseX, int mouseY) {
+ if (this.results != null) this.results.drawTooltip(context, mouseX, mouseY);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java
index 6120528c..be1f4b98 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java
@@ -1,103 +1,135 @@
package de.hysky.skyblocker.skyblock.itemlist;
-import com.mojang.blaze3d.systems.RenderSystem;
-
import de.hysky.skyblocker.mixins.accessors.RecipeBookWidgetAccessor;
+import de.hysky.skyblocker.utils.render.gui.SideTabButtonWidget;
+import it.unimi.dsi.fastutil.Pair;
+import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget;
+import net.minecraft.client.gui.tooltip.Tooltip;
+import net.minecraft.client.gui.widget.ContainerWidget;
import net.minecraft.client.gui.widget.TextFieldWidget;
-import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
import net.minecraft.screen.AbstractRecipeScreenHandler;
import net.minecraft.text.Text;
-import net.minecraft.util.Formatting;
+
+import java.util.ArrayList;
+import java.util.List;
@Environment(value = EnvType.CLIENT)
public class ItemListWidget extends RecipeBookWidget {
private int parentWidth;
private int parentHeight;
private int leftOffset;
- private TextFieldWidget searchField;
- private SearchResultsWidget results;
+
+ private TabContainerWidget currentTabContent;
+ private final List<Pair<SideTabButtonWidget, TabContainerWidget>> tabs = new ArrayList<>(2);
+ private ItemListTab itemListTab;
+
+ private static int currentTab = 0;
public ItemListWidget() {
super();
}
- public void updateSearchResult() {
- this.results.updateSearchResult(((RecipeBookWidgetAccessor) this).getSearchText());
- }
-
@Override
- public void initialize(int parentWidth, int parentHeight, MinecraftClient client, boolean narrow, AbstractRecipeScreenHandler<?> craftingScreenHandler) {
+ public void initialize(int parentWidth, int parentHeight, MinecraftClient client, boolean narrow, AbstractRecipeScreenHandler<?, ?> craftingScreenHandler) {
super.initialize(parentWidth, parentHeight, client, narrow, craftingScreenHandler);
this.parentWidth = parentWidth;
this.parentHeight = parentHeight;
this.leftOffset = narrow ? 0 : 86;
- this.searchField = ((RecipeBookWidgetAccessor) this).getSearchField();
- int x = (this.parentWidth - 147) / 2 - this.leftOffset;
- int y = (this.parentHeight - 166) / 2;
- if (ItemRepository.filesImported()) {
- this.results = new SearchResultsWidget(this.client, x, y);
- this.updateSearchResult();
- }
+ TextFieldWidget searchField = ((RecipeBookWidgetAccessor) this).getSearchField();
+ int x = (parentWidth - 147) / 2 - leftOffset;
+ int y = (parentHeight - 166) / 2;
+
+ // Init all the tabs, content and the tab button on the left
+ tabs.clear();
+
+ // Item List
+ itemListTab = new ItemListTab(x + 9, y + 9, this.client, searchField);
+ SideTabButtonWidget itemListTabButton = new SideTabButtonWidget(x - 30, y + 3, currentTab == 0, new ItemStack(Items.CRAFTING_TABLE));
+ itemListTabButton.setTooltip(Tooltip.of(Text.literal("Item List")));
+ if (currentTab == 0) currentTabContent = itemListTab;
+ tabs.add(new ObjectObjectImmutablePair<>(
+ itemListTabButton,
+ this.itemListTab));
+
+ // Upcoming Events
+ UpcomingEventsTab upcomingEventsTab = new UpcomingEventsTab(x + 9, y + 9, this.client);
+ SideTabButtonWidget eventsTabButtonWidget = new SideTabButtonWidget(x - 30, y + 3 + 27, currentTab == 1, new ItemStack(Items.CLOCK));
+ eventsTabButtonWidget.setTooltip(Tooltip.of(Text.literal("Upcoming Events")));
+ if (currentTab == 1) currentTabContent = upcomingEventsTab;
+ tabs.add(new ObjectObjectImmutablePair<>(
+ eventsTabButtonWidget,
+ upcomingEventsTab
+ ));
+
+ }
+
+ @Override
+ public void reset() {
+ super.reset();
+ if (itemListTab != null) itemListTab.setSearchField(((RecipeBookWidgetAccessor) this).getSearchField());
}
@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
if (this.isOpen()) {
- MatrixStack matrices = context.getMatrices();
- matrices.push();
- matrices.translate(0.0D, 0.0D, 100.0D);
- RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
- this.searchField = ((RecipeBookWidgetAccessor) this).getSearchField();
int i = (this.parentWidth - 147) / 2 - this.leftOffset;
int j = (this.parentHeight - 166) / 2;
+ // Draw the texture
context.drawTexture(TEXTURE, i, j, 1, 1, 147, 166);
- this.searchField = ((RecipeBookWidgetAccessor) this).getSearchField();
-
- if (!ItemRepository.filesImported() && !this.searchField.isFocused() && this.searchField.getText().isEmpty()) {
- Text hintText = (Text.literal("Loading...")).formatted(Formatting.ITALIC).formatted(Formatting.GRAY);
- context.drawTextWithShadow(this.client.textRenderer, hintText, i + 25, j + 14, -1);
- } else if (!this.searchField.isFocused() && this.searchField.getText().isEmpty()) {
- Text hintText = (Text.translatable("gui.recipebook.search_hint")).formatted(Formatting.ITALIC).formatted(Formatting.GRAY);
- context.drawTextWithShadow(this.client.textRenderer, hintText, i + 25, j + 14, -1);
- } else {
- this.searchField.render(context, mouseX, mouseY, delta);
- }
- if (ItemRepository.filesImported()) {
- if (results == null) {
- int x = (this.parentWidth - 147) / 2 - this.leftOffset;
- int y = (this.parentHeight - 166) / 2;
- this.results = new SearchResultsWidget(this.client, x, y);
- }
- this.updateSearchResult();
- this.results.render(context, mouseX, mouseY, delta);
+ // Draw the tab's content
+ if (currentTabContent != null) currentTabContent.render(context, mouseX, mouseY, delta);
+ // Draw the tab buttons
+ for (Pair<SideTabButtonWidget, TabContainerWidget> tab : tabs) {
+ tab.left().render(context, mouseX, mouseY, delta);
}
- matrices.pop();
+
}
}
@Override
public void drawTooltip(DrawContext context, int x, int y, int mouseX, int mouseY) {
- if (this.isOpen() && ItemRepository.filesImported() && results != null) {
- this.results.drawTooltip(context, mouseX, mouseY);
+ if (this.isOpen() && currentTabContent != null) {
+ this.currentTabContent.drawTooltip(context, mouseX, mouseY);
}
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
- if (this.isOpen() && this.client.player != null && !this.client.player.isSpectator() && ItemRepository.filesImported() && this.searchField != null && results != null) {
- if (this.searchField.mouseClicked(mouseX, mouseY, button)) {
- this.results.closeRecipeView();
- this.searchField.setFocused(true);
- return true;
- } else {
- this.searchField.setFocused(false);
- return this.results.mouseClicked(mouseX, mouseY, button);
+ if (this.isOpen() && this.client.player != null && !this.client.player.isSpectator()) {
+ // check if a tab is clicked
+ for (Pair<SideTabButtonWidget, TabContainerWidget> tab : tabs) {
+ if (tab.first().mouseClicked(mouseX, mouseY, button) && currentTabContent != tab.right()) {
+ for (Pair<SideTabButtonWidget, TabContainerWidget> tab2 : tabs) {
+ tab2.first().setToggled(false);
+ }
+ tab.first().setToggled(true);
+ currentTabContent = tab.right();
+ currentTab = tabs.indexOf(tab);
+ return true;
+ }
}
+ // click the tab content
+ if (currentTabContent != null) return currentTabContent.mouseClicked(mouseX, mouseY, button);
+ else return false;
} else return false;
}
+
+ /**
+ * A container widget but with a fixed width and height and a drawTooltip method to implement
+ */
+ public abstract static class TabContainerWidget extends ContainerWidget {
+
+ public TabContainerWidget(int x, int y, Text text) {
+ super(x, y, 131, 150, text);
+ }
+
+ public abstract void drawTooltip(DrawContext context, int mouseX, int mouseY);
+ }
} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRepository.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRepository.java
index 5ac0ba8d..4dca91d6 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRepository.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRepository.java
@@ -51,9 +51,13 @@ public class ItemRepository {
}
private static void loadItem(NEUItem item) {
- ItemStack stack = ItemStackBuilder.fromNEUItem(item);
- items.add(stack);
- itemsMap.put(item.getSkyblockItemId(), stack);
+ try {
+ ItemStack stack = ItemStackBuilder.fromNEUItem(item);
+ items.add(stack);
+ itemsMap.put(item.getSkyblockItemId(), stack);
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Item Repo Loader] Failed to load item, please report this! Skyblock Id: {}", item.getSkyblockItemId(), e);
+ }
}
private static void loadRecipes(NEUItem item) {
@@ -114,9 +118,12 @@ public class ItemRepository {
return items.stream();
}
+ /**
+ * @param neuId the NEU item id gotten through {@link NEUItem#getSkyblockItemId()}, {@link ItemStack#getNeuName()}, or {@link de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip#getNeuName(String, String) ItemTooltip#getNeuName(String, String)}
+ */
@Nullable
- public static ItemStack getItemStack(String internalName) {
- return itemsMap.get(internalName);
+ public static ItemStack getItemStack(String neuId) {
+ return itemsMap.get(neuId);
}
public static Stream<SkyblockCraftingRecipe> getRecipesStream() {
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 6ee75161..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;
@@ -41,7 +41,7 @@ public class ItemStackBuilder {
List<Pair<String, String>> injectors = new ArrayList<>(petData(internalName));
String legacyId = item.getMinecraftItemId();
- Identifier itemId = new Identifier(ItemFixerUpper.convertItemId(legacyId, item.getDamage()));
+ Identifier itemId = Identifier.of(ItemFixerUpper.convertItemId(legacyId, item.getDamage()));
ItemStack stack = new ItemStack(Registries.ITEM.get(itemId));
@@ -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/itemlist/ResultButtonWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ResultButtonWidget.java
index d2d463c7..6c7d1cbc 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ResultButtonWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ResultButtonWidget.java
@@ -1,30 +1,27 @@
package de.hysky.skyblocker.skyblock.itemlist;
-import java.util.List;
-import java.util.ArrayList;
-
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.item.ItemStack;
-import net.minecraft.item.Items;
-import net.minecraft.text.OrderedText;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
+import java.util.List;
+
public class ResultButtonWidget extends ClickableWidget {
- private static final Identifier BACKGROUND_TEXTURE = new Identifier("recipe_book/slot_craftable");
+ private static final Identifier BACKGROUND_TEXTURE = Identifier.ofVanilla("recipe_book/slot_craftable");
protected ItemStack itemStack = null;
public ResultButtonWidget(int x, int y) {
- super(x, y, 25, 25, Text.of(""));
+ super(x, y, 25, 25, Text.literal(""));
}
protected void setItemStack(ItemStack itemStack) {
- this.active = !itemStack.getItem().equals(Items.AIR);
+ this.active = !itemStack.isEmpty();
this.visible = true;
this.itemStack = itemStack;
}
@@ -37,29 +34,18 @@ public class ResultButtonWidget extends ClickableWidget {
@Override
public void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
MinecraftClient client = MinecraftClient.getInstance();
- // this.drawTexture(matrices, this.x, this.y, 29, 206, this.width, this.height);
context.drawGuiTexture(BACKGROUND_TEXTURE, this.getX(), this.getY(), this.getWidth(), this.getHeight());
- // client.getItemRenderer().renderInGui(this.itemStack, this.x + 4, this.y + 4);
context.drawItem(this.itemStack, this.getX() + 4, this.getY() + 4);
- // client.getItemRenderer().renderGuiItemOverlay(client.textRenderer, itemStack, this.x + 4, this.y + 4);
context.drawItemInSlot(client.textRenderer, itemStack, this.getX() + 4, this.getY() + 4);
}
public void renderTooltip(DrawContext context, int mouseX, int mouseY) {
MinecraftClient client = MinecraftClient.getInstance();
+ if (client.currentScreen == null) return;
List<Text> tooltip = Screen.getTooltipFromItem(client, this.itemStack);
- List<OrderedText> orderedTooltip = new ArrayList<>();
-
- for(int i = 0; i < tooltip.size(); i++) {
- orderedTooltip.add(tooltip.get(i).asOrderedText());
- }
-
- client.currentScreen.setTooltip(orderedTooltip);
+ client.currentScreen.setTooltip(tooltip.stream().map(Text::asOrderedText).toList());
}
- @Override
- protected void appendClickableNarrations(NarrationMessageBuilder builder) {
- // TODO Auto-generated method stub
-
- }
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java
index 1ef352e3..dfb53628 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java
@@ -6,6 +6,7 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.Drawable;
+import net.minecraft.client.gui.Element;
import net.minecraft.client.gui.screen.ButtonTextures;
import net.minecraft.client.gui.widget.ToggleButtonWidget;
import net.minecraft.component.DataComponentTypes;
@@ -23,9 +24,9 @@ import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-public class SearchResultsWidget implements Drawable {
- private static final ButtonTextures PAGE_FORWARD_TEXTURES = new ButtonTextures(new Identifier("recipe_book/page_forward"), new Identifier("recipe_book/page_forward_highlighted"));
- private static final ButtonTextures PAGE_BACKWARD_TEXTURES = new ButtonTextures(new Identifier("recipe_book/page_backward"), new Identifier("recipe_book/page_backward_highlighted"));
+public class SearchResultsWidget implements Drawable, Element {
+ private static final ButtonTextures PAGE_FORWARD_TEXTURES = new ButtonTextures(Identifier.ofVanilla("recipe_book/page_forward"), Identifier.ofVanilla("recipe_book/page_forward_highlighted"));
+ private static final ButtonTextures PAGE_BACKWARD_TEXTURES = new ButtonTextures(Identifier.ofVanilla("recipe_book/page_backward"), Identifier.ofVanilla("recipe_book/page_backward_highlighted"));
private static final int COLS = 5;
private static final int MAX_TEXT_WIDTH = 124;
private static final String ELLIPSIS = "...";
@@ -225,4 +226,16 @@ public class SearchResultsWidget implements Drawable {
return false;
}
+ private boolean focused = false;
+
+ @Override
+ public void setFocused(boolean focused) {
+ this.focused = focused;
+ }
+
+ @Override
+ public boolean isFocused() {
+ return focused;
+ }
+
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/UpcomingEventsTab.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/UpcomingEventsTab.java
new file mode 100644
index 00000000..9552ae87
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/UpcomingEventsTab.java
@@ -0,0 +1,168 @@
+package de.hysky.skyblocker.skyblock.itemlist;
+
+import de.hysky.skyblocker.mixins.accessors.DrawContextInvoker;
+import de.hysky.skyblocker.skyblock.events.EventNotifications;
+import de.hysky.skyblocker.skyblock.tabhud.widget.JacobsContestWidget;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.tooltip.HoveredTooltipPositioner;
+import net.minecraft.client.gui.tooltip.TooltipComponent;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Style;
+import net.minecraft.text.Text;
+import net.minecraft.util.Colors;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+
+public class UpcomingEventsTab extends ItemListWidget.TabContainerWidget {
+ private static final ItemStack CLOCK = new ItemStack(Items.CLOCK);
+ private final MinecraftClient client;
+ private final List<EventRenderer> events;
+
+ public UpcomingEventsTab(int x, int y, MinecraftClient client) {
+ super(x, y, Text.literal("Upcoming Events Tab"));
+ this.client = client;
+ events = EventNotifications.getEvents().entrySet()
+ .stream()
+ .sorted(Comparator.comparingLong(a -> a.getValue().isEmpty() ? Long.MAX_VALUE : a.getValue().peekFirst().start()))
+ .map(stringLinkedListEntry -> new EventRenderer(stringLinkedListEntry.getKey(), stringLinkedListEntry.getValue()))
+ .toList();
+ }
+
+ @Override
+ public void drawTooltip(DrawContext context, int mouseX, int mouseY) {
+ if (hovered != null) {
+ ((DrawContextInvoker) context).invokeDrawTooltip(this.client.textRenderer, hovered.getTooltip(), mouseX, mouseY, HoveredTooltipPositioner.INSTANCE);
+ }
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return List.of();
+ }
+
+ private EventRenderer hovered = null;
+
+ @Override
+ protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
+ int x = getX();
+ int y = getY();
+ context.enableScissor(x, y, getRight(), getBottom());
+ context.drawItem(CLOCK, x, y + 4);
+ context.drawText(this.client.textRenderer, "Upcoming Events", x + 17, y + 7, -1, true);
+
+ int eventsY = y + 7 + 24;
+ hovered = null;
+ for (EventRenderer eventRenderer : events) {
+ eventRenderer.render(context, x + 1, eventsY, mouseX, mouseY);
+ if (isMouseOver(mouseX, mouseY) && eventRenderer.isMouseOver(mouseX, mouseY, x+1, eventsY)) hovered = eventRenderer;
+ eventsY += eventRenderer.getHeight();
+
+ }
+ context.disableScissor();
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (hovered != null && hovered.getWarpCommand() != null) {
+ MessageScheduler.INSTANCE.sendMessageAfterCooldown(hovered.getWarpCommand());
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {
+
+ }
+
+ public static class EventRenderer {
+
+ private final LinkedList<EventNotifications.SkyblockEvent> events;
+ private final String eventName;
+
+ public EventRenderer(String eventName, LinkedList<EventNotifications.SkyblockEvent> events) {
+ this.events = events;
+ this.eventName = eventName;
+ }
+
+ public void render(DrawContext context, int x, int y, int mouseX, int mouseY) {
+ long time = System.currentTimeMillis() / 1000;
+ TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+ context.drawText(textRenderer, Text.literal(eventName).fillStyle(Style.EMPTY.withUnderline(isMouseOver(mouseX, mouseY, x, y))), x, y, -1, true);
+ if (events.isEmpty()) {
+ context.drawText(textRenderer, Text.literal(" ").append(Text.translatable("skyblocker.events.tab.noMore")), x, y + textRenderer.fontHeight, Colors.GRAY, false);
+ } else if (events.peekFirst().start() > time) {
+ MutableText formatted = Text.literal(" ").append(Text.translatable("skyblocker.events.tab.startsIn", Utils.getDurationText((int) (events.peekFirst().start() - time)))).formatted(Formatting.YELLOW);
+ context.drawText(textRenderer, formatted, x, y + textRenderer.fontHeight, -1, true);
+ } else {
+ MutableText formatted = Text.literal(" ").append(Text.translatable( "skyblocker.events.tab.endsIn", Utils.getDurationText((int) (events.peekFirst().start() + events.peekFirst().duration() - time)))).formatted(Formatting.GREEN);
+ context.drawText(textRenderer, formatted, x, y + textRenderer.fontHeight, -1, true);
+ }
+
+ }
+
+ public int getHeight() {
+ return 20;
+ }
+
+ public boolean isMouseOver(int mouseX, int mouseY, int x, int y) {
+ return mouseX >= x && mouseX <= x + 131 && mouseY >= y && mouseY <= y+getHeight();
+ }
+
+ public List<TooltipComponent> getTooltip() {
+ List<TooltipComponent> components = new ArrayList<>();
+ if (events.peekFirst() == null) return components;
+ if (eventName.equals(EventNotifications.JACOBS)) {
+ components.add(new JacobsTooltip(events.peekFirst().extras()));
+ }
+ //noinspection DataFlowIssue
+ if (events.peekFirst().warpCommand() != null) {
+ components.add(TooltipComponent.of(Text.translatable("skyblocker.events.tab.clickToWarp").formatted(Formatting.ITALIC).asOrderedText()));
+ }
+
+ return components;
+ }
+
+ public @Nullable String getWarpCommand() {
+ if (events.isEmpty()) return null;
+ return events.peek().warpCommand();
+ }
+ }
+
+ private record JacobsTooltip(String[] crops) implements TooltipComponent {
+
+ private static final ItemStack BARRIER = new ItemStack(Items.BARRIER);
+
+ @Override
+ public int getHeight() {
+ return 20;
+ }
+
+ @Override
+ public int getWidth(TextRenderer textRenderer) {
+ return 16 * 3 + 4;
+ }
+
+ @Override
+ public void drawItems(TextRenderer textRenderer, int x, int y, DrawContext context) {
+ for (int i = 0; i < crops.length; i++) {
+ String crop = crops[i];
+ context.drawItem(JacobsContestWidget.FARM_DATA.getOrDefault(crop, BARRIER), x + 18 * i, y + 2);
+ }
+ }
+
+ }
+}
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/quicknav/QuickNavButton.java b/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNavButton.java
index f9ca0940..f7f33d0f 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNavButton.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNavButton.java
@@ -94,7 +94,7 @@ public class QuickNavButton extends ClickableWidget {
RenderSystem.disableDepthTest();
// Construct the texture identifier based on the index and toggled state
- Identifier tabTexture = new Identifier("container/creative_inventory/tab_" + (isTopTab() ? "top" : "bottom") + "_" + (toggled ? "selected" : "unselected") + "_" + (index % 7 + 1));
+ Identifier tabTexture = Identifier.ofVanilla("container/creative_inventory/tab_" + (isTopTab() ? "top" : "bottom") + "_" + (toggled ? "selected" : "unselected") + "_" + (index % 7 + 1));
// Render the button texture
context.drawGuiTexture(tabTexture, this.getX(), this.getY(), this.width, this.height);
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/rift/EffigyWaypoints.java b/src/main/java/de/hysky/skyblocker/skyblock/rift/EffigyWaypoints.java
index 11963857..ef77c602 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/rift/EffigyWaypoints.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/rift/EffigyWaypoints.java
@@ -1,6 +1,7 @@
package de.hysky.skyblocker.skyblock.rift;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.ColorUtils;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.render.RenderHelper;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
@@ -26,6 +27,7 @@ public class EffigyWaypoints {
new BlockPos(240, 129, 118) //Effigy 6
);
private static final List<BlockPos> UNBROKEN_EFFIGIES = new ArrayList<>();
+ private static final float[] RED = ColorUtils.getFloatComponents(DyeColor.RED);
protected static void updateEffigies() {
if (!SkyblockerConfigManager.get().slayers.vampireSlayer.enableEffigyWaypoints || !Utils.isOnSkyblock() || !Utils.isInTheRift() || !Utils.getIslandArea().contains("Stillgore Château")) return;
@@ -57,13 +59,12 @@ public class EffigyWaypoints {
protected static void render(WorldRenderContext context) {
if (SkyblockerConfigManager.get().slayers.vampireSlayer.enableEffigyWaypoints && Utils.getIslandArea().contains("Stillgore Château")) {
for (BlockPos effigy : UNBROKEN_EFFIGIES) {
- float[] colorComponents = DyeColor.RED.getColorComponents();
if (SkyblockerConfigManager.get().slayers.vampireSlayer.compactEffigyWaypoints) {
- RenderHelper.renderFilledWithBeaconBeam(context, effigy.down(6), colorComponents, 0.5F, true);
+ RenderHelper.renderFilledWithBeaconBeam(context, effigy.down(6), RED, 0.5F, true);
} else {
- RenderHelper.renderFilledWithBeaconBeam(context, effigy, colorComponents, 0.5F, true);
+ RenderHelper.renderFilledWithBeaconBeam(context, effigy, RED, 0.5F, true);
for (int i = 1; i < 6; i++) {
- RenderHelper.renderFilled(context, effigy.down(i), colorComponents, 0.5F - (0.075F * i), true);
+ RenderHelper.renderFilled(context, effigy.down(i), RED, 0.5F - (0.075F * i), true);
}
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/rift/EnigmaSouls.java b/src/main/java/de/hysky/skyblocker/skyblock/rift/EnigmaSouls.java
index 039d331c..8e3d1a91 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/rift/EnigmaSouls.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/rift/EnigmaSouls.java
@@ -7,9 +7,9 @@ import com.google.gson.JsonParser;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.CommandDispatcher;
import de.hysky.skyblocker.SkyblockerMod;
-import de.hysky.skyblocker.config.SkyblockerConfig;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.config.configs.OtherLocationsConfig;
+import de.hysky.skyblocker.utils.ColorUtils;
import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.PosUtils;
import de.hysky.skyblocker.utils.Utils;
@@ -43,11 +43,11 @@ import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.lit
public class EnigmaSouls {
private static final Logger LOGGER = LoggerFactory.getLogger(EnigmaSouls.class);
private static final Supplier<Waypoint.Type> TYPE_SUPPLIER = () -> SkyblockerConfigManager.get().uiAndVisuals.waypoints.waypointType;
- private static final Identifier WAYPOINTS_JSON = new Identifier(SkyblockerMod.NAMESPACE, "rift/enigma_soul_waypoints.json");
+ private static final Identifier WAYPOINTS_JSON = Identifier.of(SkyblockerMod.NAMESPACE, "rift/enigma_soul_waypoints.json");
private static final Map<BlockPos, ProfileAwareWaypoint> SOUL_WAYPOINTS = new HashMap<>(42);
private static final Path FOUND_SOULS_FILE = SkyblockerMod.CONFIG_DIR.resolve("found_enigma_souls.json");
- private static final float[] GREEN = DyeColor.GREEN.getColorComponents();
- private static final float[] RED = DyeColor.RED.getColorComponents();
+ private static final float[] GREEN = ColorUtils.getFloatComponents(DyeColor.GREEN);
+ private static final float[] RED = ColorUtils.getFloatComponents(DyeColor.RED);
private static CompletableFuture<Void> soulsLoaded;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/rift/MirrorverseWaypoints.java b/src/main/java/de/hysky/skyblocker/skyblock/rift/MirrorverseWaypoints.java
index 7d7d0c34..6a366122 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/rift/MirrorverseWaypoints.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/rift/MirrorverseWaypoints.java
@@ -5,6 +5,7 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.ColorUtils;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.waypoint.Waypoint;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
@@ -23,11 +24,11 @@ import java.util.function.Supplier;
public class MirrorverseWaypoints {
private static final Logger LOGGER = LoggerFactory.getLogger("skyblocker");
private static final Supplier<Waypoint.Type> WAYPOINT_TYPE = () -> Waypoint.Type.HIGHLIGHT;
- private static final Identifier WAYPOINTS_JSON = new Identifier(SkyblockerMod.NAMESPACE, "rift/mirrorverse_waypoints.json");
+ private static final Identifier WAYPOINTS_JSON = Identifier.of(SkyblockerMod.NAMESPACE, "rift/mirrorverse_waypoints.json");
private static Waypoint[] LAVA_PATH_WAYPOINTS;
private static Waypoint[] UPSIDE_DOWN_WAYPOINTS;
private static Waypoint[] TURBULATOR_WAYPOINTS;
- private static final float[] COLOR_COMPONENTS = DyeColor.RED.getColorComponents();
+ private static final float[] COLOR_COMPONENTS = ColorUtils.getFloatComponents(DyeColor.RED);
private static CompletableFuture<Void> waypointsLoaded;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/OverlayScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/OverlayScreen.java
index a03f3549..3e392f07 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/OverlayScreen.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/OverlayScreen.java
@@ -3,9 +3,11 @@ package de.hysky.skyblocker.skyblock.searchoverlay;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.tooltip.Tooltip;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.item.ItemStack;
+import net.minecraft.text.MutableText;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
@@ -16,10 +18,13 @@ import static de.hysky.skyblocker.skyblock.itemlist.ItemRepository.getItemStack;
public class OverlayScreen extends Screen {
- protected static final Identifier SEARCH_ICON_TEXTURE = new Identifier("icon/search");
+ protected static final Identifier SEARCH_ICON_TEXTURE = Identifier.ofVanilla("icon/search");
+ private static final Identifier BACKGROUND_TEXTURE = Identifier.ofVanilla("social_interactions/background");
private static final int rowHeight = 20;
private TextFieldWidget searchField;
private ButtonWidget finishedButton;
+ private ButtonWidget maxPetButton;
+ private ButtonWidget dungeonStarButton;
private ButtonWidget[] suggestionButtons;
private ButtonWidget[] historyButtons;
@@ -44,10 +49,11 @@ public class OverlayScreen extends Screen {
searchField.setMaxLength(30);
// finish buttons
- finishedButton = ButtonWidget.builder(Text.literal("").setStyle(Style.EMPTY.withColor(Formatting.GREEN)), a -> close())
+ finishedButton = ButtonWidget.builder(Text.literal(""), a -> close())
.position(startX + rowWidth - rowHeight, startY)
.size(rowHeight, rowHeight).build();
+
// suggested item buttons
int rowOffset = rowHeight;
int totalSuggestions = SkyblockerConfigManager.get().uiAndVisuals.searchOverlay.maxSuggestions;
@@ -80,6 +86,26 @@ public class OverlayScreen extends Screen {
break;
}
}
+ //auction only elements
+ if (SearchOverManager.isAuction) {
+ //max pet level button
+ maxPetButton = ButtonWidget.builder(Text.literal(""), a -> {
+ SearchOverManager.maxPetLevel = !SearchOverManager.maxPetLevel;
+ updateMaxPetText();
+ })
+ .tooltip(Tooltip.of(Text.translatable("skyblocker.config.general.searchOverlay.maxPet.@Tooltip")))
+ .position(startX, startY - rowHeight - 8)
+ .size(rowWidth / 2, rowHeight).build();
+ updateMaxPetText();
+
+ //dungeon star input
+ dungeonStarButton = ButtonWidget.builder(Text.literal("✪"), a -> updateStars())
+ .tooltip(Tooltip.of(Text.translatable("skyblocker.config.general.searchOverlay.starsTooltip")))
+ .position(startX + (int) (rowWidth * 0.5), startY - rowHeight - 8)
+ .size(rowWidth / 2, rowHeight).build();
+
+ updateStars();
+ }
//add drawables in order to make tab navigation sensible
addDrawableChild(searchField);
@@ -93,11 +119,86 @@ public class OverlayScreen extends Screen {
}
addDrawableChild(finishedButton);
+ if (SearchOverManager.isAuction) {
+ addDrawableChild(maxPetButton);
+ addDrawableChild(dungeonStarButton);
+ }
+
//focus the search box
this.setInitialFocus(searchField);
}
/**
+ * Finds if the mouse is clicked on the dungeon star button and if so works out what stars the user clicked on
+ *
+ * @param mouseX the X coordinate of the mouse
+ * @param mouseY the Y coordinate of the mouse
+ * @param button the mouse button number
+ * @return super
+ */
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (SearchOverManager.isAuction && dungeonStarButton.isHovered() && client != null) {
+ double actualTextWidth = client.textRenderer.getWidth(dungeonStarButton.getMessage());
+ double textOffset = (dungeonStarButton.getWidth() - actualTextWidth) / 2;
+ double offset = mouseX - (dungeonStarButton.getX() + textOffset);
+ int starCount = (int) ((offset / actualTextWidth) * 10);
+ starCount = Math.clamp(starCount + 1, 0, 10);
+ //if same as old value set stars to 0 else set to selected amount
+ if (starCount == SearchOverManager.dungeonStars) {
+ SearchOverManager.dungeonStars = 0;
+ } else {
+ SearchOverManager.dungeonStars = starCount;
+ }
+ }
+
+ return super.mouseClicked(mouseX, mouseY, button);
+ }
+
+ /**
+ * Updates the text displayed on the max pet level button to represent the settings current state
+ */
+ private void updateMaxPetText() {
+ if (SearchOverManager.maxPetLevel) {
+ maxPetButton.setMessage(Text.translatable("skyblocker.config.general.searchOverlay.maxPet").append(Text.literal(" ✔")).formatted(Formatting.GREEN));
+ } else {
+ maxPetButton.setMessage(Text.translatable("skyblocker.config.general.searchOverlay.maxPet").append(Text.literal(" ❌")).formatted(Formatting.RED));
+ }
+ }
+
+ /**
+ * Updates stars in dungeon star input to represent the current star value
+ */
+ private void updateStars() {
+ MutableText stars = Text.empty();
+ for (int i = 0; i < SearchOverManager.dungeonStars; i++) {
+ stars.append(Text.literal("✪").formatted(i < 5 ? Formatting.YELLOW : Formatting.RED));
+ }
+ for (int i = SearchOverManager.dungeonStars; i < 10; i++) {
+ stars.append(Text.literal("✪"));
+ }
+ dungeonStarButton.setMessage(stars);
+ }
+
+ /**
+ * Renders the background for the search using the social interactions background
+ * @param context context
+ * @param mouseX mouseX
+ * @param mouseY mouseY
+ * @param delta delta
+ */
+ @Override
+ public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) {
+ super.renderBackground(context, mouseX, mouseY, delta);
+ //find max height
+ int maxHeight = rowHeight * (1 + suggestionButtons.length + historyButtons.length);
+ if (historyButtons.length > 0) { //add space for history label if it could exist
+ maxHeight += (int) (rowHeight * 0.75);
+ }
+ context.drawGuiTexture(BACKGROUND_TEXTURE, searchField.getX() - 8, searchField.getY() - 8, (int) (this.width * 0.4) + 16, maxHeight + 16);
+ }
+
+ /**
* Renders the search icon, label for the history and item Stacks for item names
*/
@Override
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/SearchOverManager.java b/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/SearchOverManager.java
index 917a6aa0..70dd8ada 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/SearchOverManager.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/SearchOverManager.java
@@ -10,6 +10,7 @@ import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
import de.hysky.skyblocker.utils.NEURepoManager;
import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
import io.github.moulberry.repo.data.NEUItem;
+import io.github.moulberry.repo.util.NEUId;
import it.unimi.dsi.fastutil.Pair;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
@@ -24,10 +25,7 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
+import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -40,8 +38,7 @@ public class SearchOverManager {
private static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Search Overlay");
private static final Pattern BAZAAR_ENCHANTMENT_PATTERN = Pattern.compile("ENCHANTMENT_(\\D*)_(\\d+)");
- private static final Pattern AUCTION_PET_AND_RUNE_PATTERN = Pattern.compile("([A-Z0-9_]+);(\\d+)");
-
+ private static final String PET_NAME_START = "[Lvl {LVL}] ";
/**
* converts index (in array) +1 to a roman numeral
*/
@@ -52,15 +49,19 @@ public class SearchOverManager {
private static @Nullable SignBlockEntity sign = null;
private static boolean signFront = true;
- private static boolean isAuction;
+ protected static boolean isAuction;
private static boolean isCommand;
protected static String search = "";
+ protected static Boolean maxPetLevel = false;
+ protected static int dungeonStars = 0;
// Use non-final variables and swap them to prevent concurrent modification
- private static HashSet<String> bazaarItems;
- private static HashSet<String> auctionItems;
- private static HashMap<String, String> namesToId;
+ private static HashSet<String> bazaarItems = new HashSet<>();
+ private static HashSet<String> auctionItems = new HashSet<>();
+ private static HashSet<String> auctionPets = new HashSet<>();
+ private static HashSet<String> starableItems = new HashSet<>();
+ private static HashMap<String, String> namesToId = new HashMap<>();
public static String[] suggestionsArray = {};
@@ -91,6 +92,8 @@ public class SearchOverManager {
private static void loadItems() {
HashSet<String> bazaarItems = new HashSet<>();
HashSet<String> auctionItems = new HashSet<>();
+ HashSet<String> auctionPets = new HashSet<>();
+ HashSet<String> starableItems = new HashSet<>();
HashMap<String, String> namesToId = new HashMap<>();
//get bazaar items
@@ -139,28 +142,28 @@ public class SearchOverManager {
//get auction items
try {
+ Set<@NEUId String> essenceCosts = NEURepoManager.NEU_REPO.getConstants().getEssenceCost().getCosts().keySet();
if (TooltipInfoType.THREE_DAY_AVERAGE.getData() == null) {
TooltipInfoType.THREE_DAY_AVERAGE.run();
}
for (Map.Entry<String, JsonElement> entry : TooltipInfoType.THREE_DAY_AVERAGE.getData().entrySet()) {
String id = entry.getKey();
-
- Matcher matcher = AUCTION_PET_AND_RUNE_PATTERN.matcher(id);
- if (matcher.find()) {//is a pet or rune convert id to name
- String name = matcher.group(1).replace("_", " ");
- name = capitalizeFully(name);
- auctionItems.add(name);
- namesToId.put(name, id);
- continue;
- }
- //something else look up in NEU repo.
+ //look up in NEU repo.
id = id.split("[+-]")[0];
NEUItem neuItem = NEURepoManager.NEU_REPO.getItems().getItemBySkyblockId(id);
if (neuItem != null) {
String name = Formatting.strip(neuItem.getDisplayName());
+ //add names that are pets to the list of pets to work with the lvl 100 button
+ if (name != null && name.startsWith(PET_NAME_START)) {
+ name = name.replace(PET_NAME_START, "");
+ auctionPets.add(name.toLowerCase());
+ }
+ //if it has essence cost add to starable items
+ if (name != null && essenceCosts.contains(neuItem.getSkyblockItemId())) {
+ starableItems.add(name.toLowerCase());
+ }
auctionItems.add(name);
namesToId.put(name, id);
- continue;
}
}
} catch (Exception e) {
@@ -169,11 +172,14 @@ public class SearchOverManager {
SearchOverManager.bazaarItems = bazaarItems;
SearchOverManager.auctionItems = auctionItems;
+ SearchOverManager.auctionPets = auctionPets;
+ SearchOverManager.starableItems = starableItems;
SearchOverManager.namesToId = namesToId;
}
/**
* Capitalizes the first letter off every word in a string
+ *
* @param str string to capitalize
*/
private static String capitalizeFully(String str) {
@@ -188,8 +194,9 @@ public class SearchOverManager {
/**
* Receives data when a search is started and resets values
- * @param sign the sign that is being edited
- * @param front if it's the front of the sign
+ *
+ * @param sign the sign that is being edited
+ * @param front if it's the front of the sign
* @param isAuction if the sign is loaded from the auction house menu or bazaar
*/
public static void updateSign(@NotNull SignBlockEntity sign, boolean front, boolean isAuction) {
@@ -214,6 +221,7 @@ public class SearchOverManager {
/**
* Updates the search value and the suggestions based on that value.
+ *
* @param newValue new search value
*/
protected static void updateSearch(String newValue) {
@@ -221,11 +229,30 @@ public class SearchOverManager {
//update the suggestion values
int totalSuggestions = SkyblockerConfigManager.get().uiAndVisuals.searchOverlay.maxSuggestions;
if (newValue.isBlank() || totalSuggestions == 0) return; //do not search for empty value
- suggestionsArray = (isAuction ? auctionItems : bazaarItems).stream().filter(item -> item.toLowerCase().contains(search.toLowerCase())).limit(totalSuggestions).toArray(String[]::new);
+ suggestionsArray = (isAuction ? auctionItems : bazaarItems).stream().sorted(Comparator.comparing(SearchOverManager::shouldFrontLoad, Comparator.reverseOrder())).filter(item -> item.toLowerCase().contains(search.toLowerCase())).limit(totalSuggestions).toArray(String[]::new);
+ }
+
+ /**
+ * determines if a value should be moved to the front of the search
+ *
+ * @param name name of the suggested item
+ * @return if the value should be at the front of the search queue
+ */
+ private static boolean shouldFrontLoad(String name) {
+ if (!isAuction) {
+ return false;
+ }
+ //do nothing to non pets
+ if (!auctionPets.contains(name.toLowerCase())) {
+ return false;
+ }
+ //only front load pets when there is enough of the pet typed, so it does not spoil searching for other items
+ return (double) search.length() / name.length() > 0.5;
}
/**
* Gets the suggestion in the suggestion array at the index
+ *
* @param index index of suggestion
*/
protected static String getSuggestion(int index) {
@@ -242,6 +269,7 @@ public class SearchOverManager {
/**
* Gets the item name in the history array at the index
+ *
* @param index index of suggestion
*/
protected static String getHistory(int index) {
@@ -286,13 +314,18 @@ public class SearchOverManager {
}
/**
- *Saves the current value of ({@link SearchOverManager#search}) then pushes it to a command or sign depending on how the gui was opened
+ * Saves the current value of ({@link SearchOverManager#search}) then pushes it to a command or sign depending on how the gui was opened
*/
protected static void pushSearch() {
//save to history
if (!search.isEmpty()) {
saveHistory();
}
+ //add pet level or dungeon starts if in ah
+ if (isAuction) {
+ addExtras();
+ }
+ //push
if (isCommand) {
pushCommand();
} else {
@@ -301,6 +334,45 @@ public class SearchOverManager {
}
/**
+ * Adds pet level 100 or necessary dungeon starts if needed
+ */
+ private static void addExtras() {
+ // pet level
+ if (maxPetLevel) {
+ if (auctionPets.contains(search.toLowerCase())) {
+ if (search.equalsIgnoreCase("golden dragon")) {
+ search = "[Lvl 200] " + search;
+ } else {
+ search = "[Lvl 100] " + search;
+ }
+ }
+ } else {
+ // still filter for only pets
+ if (auctionPets.contains(search.toLowerCase())) {
+ // add bracket so only get pets
+ search = "] " + search;
+ }
+ }
+
+ // dungeon stars
+ // check if it's a dungeon item and if so add correct stars
+ if (dungeonStars > 0 && starableItems.contains(search.toLowerCase())) {
+ StringBuilder starString = new StringBuilder(" ");
+ //add stars up to 5
+ starString.append("✪".repeat(Math.max(0, Math.min(dungeonStars, 5))));
+ //add number for other stars
+ switch (dungeonStars) {
+ case 6 -> starString.append("➊");
+ case 7 -> starString.append("➋");
+ case 8 -> starString.append("➌");
+ case 9 -> starString.append("➍");
+ case 10 -> starString.append("➎");
+ }
+ search += starString.toString();
+ }
+ }
+
+ /**
* runs the command to search for the value in ({@link SearchOverManager#search})
*/
private static void pushCommand() {
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/shortcut/ShortcutsConfigListWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigListWidget.java
index 3918037f..a6b5e62d 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigListWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigListWidget.java
@@ -1,5 +1,6 @@
package de.hysky.skyblocker.skyblock.shortcut;
+import de.hysky.skyblocker.debug.Debug;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.Element;
@@ -77,6 +78,11 @@ public class ShortcutsConfigListWidget extends ElementListWidget<ShortcutsConfig
}
@Override
+ protected boolean isSelectedEntry(int index) {
+ return Debug.debugEnabled() ? Objects.equals(getSelectedOrNull(), children().get(index)) : super.isSelectedEntry(index);
+ }
+
+ @Override
protected boolean removeEntry(AbstractShortcutEntry entry) {
return super.removeEntry(entry);
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigScreen.java
index 120eb099..66735511 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigScreen.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigScreen.java
@@ -11,7 +11,6 @@ import net.minecraft.screen.ScreenTexts;
import net.minecraft.text.Text;
public class ShortcutsConfigScreen extends Screen {
-
private ShortcutsConfigListWidget shortcutsConfigListWidget;
private ButtonWidget buttonDelete;
private ButtonWidget buttonNew;
@@ -41,14 +40,14 @@ public class ShortcutsConfigScreen extends Screen {
shortcutsConfigListWidget.setDimensions(width, height - 96);
shortcutsConfigListWidget.updatePositions();
} else {
- shortcutsConfigListWidget = new ShortcutsConfigListWidget(client, this, width, height - 96, 32, 25);
+ shortcutsConfigListWidget = new ShortcutsConfigListWidget(client, this, width, height - 96, 32, 24);
initialized = true;
}
addDrawableChild(shortcutsConfigListWidget);
GridWidget gridWidget = new GridWidget();
gridWidget.getMainPositioner().marginX(5).marginY(2);
GridWidget.Adder adder = gridWidget.createAdder(2);
- buttonDelete = ButtonWidget.builder(Text.translatable("selectServer.delete"), button -> {
+ buttonDelete = ButtonWidget.builder(Text.translatable("selectServer.deleteButton"), button -> {
if (client != null && shortcutsConfigListWidget.getSelectedOrNull() instanceof ShortcutsConfigListWidget.ShortcutEntry shortcutEntry) {
scrollAmount = shortcutsConfigListWidget.getScrollAmount();
client.setScreen(new ConfirmScreen(this::deleteEntry, Text.translatable("skyblocker.shortcuts.deleteQuestion"), Text.stringifiedTranslatable("skyblocker.shortcuts.deleteWarning", shortcutEntry), Text.translatable("selectServer.deleteButton"), ScreenTexts.CANCEL));
@@ -57,16 +56,10 @@ public class ShortcutsConfigScreen extends Screen {
adder.add(buttonDelete);
buttonNew = ButtonWidget.builder(Text.translatable("skyblocker.shortcuts.new"), buttonNew -> shortcutsConfigListWidget.addShortcutAfterSelected()).build();
adder.add(buttonNew);
- adder.add(ButtonWidget.builder(ScreenTexts.CANCEL, button -> {
- if (client != null) {
- close();
- }
- }).build());
+ adder.add(ButtonWidget.builder(ScreenTexts.CANCEL, button -> close()).build());
buttonDone = ButtonWidget.builder(ScreenTexts.DONE, button -> {
shortcutsConfigListWidget.saveShortcuts();
- if (client != null) {
- close();
- }
+ close();
}).tooltip(Tooltip.of(Text.translatable("skyblocker.shortcuts.commandSuggestionTooltip"))).build();
adder.add(buttonDone);
gridWidget.refreshPositions();
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/ScreenMaster.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/ScreenMaster.java
index 982fa16e..18a65049 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/ScreenMaster.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/ScreenMaster.java
@@ -81,7 +81,7 @@ public class ScreenMaster {
// WHY MUST IT ALWAYS BE SUCH NESTED GARBAGE MINECRAFT KEEP THAT IN DFU FFS
ResourceManagerHelper.registerBuiltinResourcePack(
- new Identifier(SkyblockerMod.NAMESPACE, "top_aligned"),
+ Identifier.of(SkyblockerMod.NAMESPACE, "top_aligned"),
SkyblockerMod.SKYBLOCKER_MOD,
ResourcePackActivationType.NORMAL
);
@@ -91,7 +91,7 @@ public class ScreenMaster {
new SimpleSynchronousResourceReloadListener() {
@Override
public Identifier getFabricId() {
- return new Identifier("skyblocker", "tabhud");
+ return Identifier.of(SkyblockerMod.NAMESPACE, "tabhud");
}
@Override
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/JacobsContestWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/JacobsContestWidget.java
index 24dcc229..c28c8679 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/JacobsContestWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/JacobsContestWidget.java
@@ -26,7 +26,7 @@ public class JacobsContestWidget extends Widget {
//TODO Properly match the contest placement and display it
private static final Pattern CROP_PATTERN = Pattern.compile("(?<fortune>[☘○]) (?<crop>[A-Za-z ]+).*");
- private static final Map<String, ItemStack> FARM_DATA = Map.ofEntries(
+ public static final Map<String, ItemStack> FARM_DATA = Map.ofEntries(
entry("Wheat", new ItemStack(Items.WHEAT)),
entry("Sugar Cane", new ItemStack(Items.SUGAR_CANE)),
entry("Carrot", new ItemStack(Items.CARROT)),
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/skyblock/waypoint/AbstractWaypointsScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/AbstractWaypointsScreen.java
new file mode 100644
index 00000000..da6f52c8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/AbstractWaypointsScreen.java
@@ -0,0 +1,73 @@
+package de.hysky.skyblocker.skyblock.waypoint;
+
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import de.hysky.skyblocker.utils.Location;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.waypoint.NamedWaypoint;
+import de.hysky.skyblocker.utils.waypoint.WaypointCategory;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.text.Text;
+
+import java.util.Arrays;
+
+public abstract class AbstractWaypointsScreen<T extends Screen> extends Screen {
+ protected final T parent;
+ protected final Multimap<String, WaypointCategory> waypoints;
+ protected String island;
+ protected WaypointsListWidget waypointsListWidget;
+ protected DropdownWidget<Location> islandWidget;
+
+ public AbstractWaypointsScreen(Text title, T parent) {
+ this(title, parent, MultimapBuilder.hashKeys().arrayListValues().build());
+ }
+
+ public AbstractWaypointsScreen(Text title, T parent, Multimap<String, WaypointCategory> waypoints) {
+ this(title, parent, waypoints, Utils.getLocationRaw());
+ }
+
+ public AbstractWaypointsScreen(Text title, T parent, Multimap<String, WaypointCategory> waypoints, String island) {
+ super(title);
+ this.parent = parent;
+ this.waypoints = waypoints;
+ this.island = island;
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ waypointsListWidget = addDrawableChild(new WaypointsListWidget(client, this, width, height - 96, 32, 24));
+ islandWidget = addDrawableChild(new DropdownWidget<>(client, width - 160, 8, 150, height - 8, Arrays.asList(Location.values()), this::islandChanged, Location.from(island)));
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (islandWidget.mouseClicked(mouseX, mouseY, button)) {
+ return true;
+ }
+ boolean mouseClicked = super.mouseClicked(mouseX, mouseY, button);
+ updateButtons();
+ return mouseClicked;
+ }
+
+ @Override
+ public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
+ if (islandWidget.isMouseOver(mouseX, mouseY) && islandWidget.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) {
+ return true;
+ }
+ return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount);
+ }
+
+ protected void islandChanged(Location location) {
+ island = location.id();
+ waypointsListWidget.setIsland(island);
+ }
+
+ protected abstract boolean isEnabled(NamedWaypoint waypoint);
+
+ protected abstract void enabledChanged(NamedWaypoint waypoint, boolean enabled);
+
+ protected void updateButtons() {
+ waypointsListWidget.updateButtons();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/DropdownWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/DropdownWidget.java
new file mode 100644
index 00000000..724cb461
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/DropdownWidget.java
@@ -0,0 +1,141 @@
+package de.hysky.skyblocker.skyblock.waypoint;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.Selectable;
+import net.minecraft.client.gui.widget.ElementListWidget;
+import net.minecraft.text.Style;
+import net.minecraft.text.Text;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+public class DropdownWidget<T> extends ElementListWidget<DropdownWidget.Entry<T>> {
+ private static final MinecraftClient client = MinecraftClient.getInstance();
+ public static final int ENTRY_HEIGHT = 15;
+ protected final List<T> entries;
+ protected final Consumer<T> selectCallback;
+ protected T prevSelected;
+ protected T selected;
+ protected boolean open;
+
+ public DropdownWidget(MinecraftClient minecraftClient, int x, int y, int width, int maxHeight, List<T> entries, Consumer<T> selectCallback, T selected) {
+ super(minecraftClient, width, Math.min((entries.size() + 1) * ENTRY_HEIGHT + 8, maxHeight), y, ENTRY_HEIGHT);
+ setX(x);
+ this.entries = entries;
+ this.selectCallback = selectCallback;
+ this.selected = selected;
+ setRenderHeader(true, ENTRY_HEIGHT + 4);
+ for (T entry : entries) {
+ addEntry(new Entry<>(this, entry));
+ }
+ }
+
+ @Override
+ public int getRowLeft() {
+ return getX();
+ }
+
+ @Override
+ public int getRowWidth() {
+ return getWidth();
+ }
+
+ @Override
+ protected boolean clickedHeader(int x, int y) {
+ open = !open;
+ return true;
+ }
+
+ @Override
+ protected void renderHeader(DrawContext context, int x, int y) {
+ context.drawTextWithShadow(client.textRenderer, selected.toString(), x + 4, y + 2, 0xFFFFFFFF);
+ }
+
+ @Override
+ public void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
+ context.getMatrices().push();
+ context.getMatrices().translate(0, 0, 100);
+
+ int y = getY() - (int) getScrollAmount();
+ int height = getMaxPosition();
+
+ context.fill(getX(), y, getX() + width, y + headerHeight, 0xFF000000);
+ context.drawHorizontalLine(getX(), getX() + width, y, 0xFFFFFFFF);
+ context.drawHorizontalLine(getX(), getX() + width, y + headerHeight, 0xFFFFFFFF);
+ context.drawVerticalLine(getX(), y, y + headerHeight, 0xFFFFFFFF);
+ context.drawVerticalLine(getX() + width, y, y + headerHeight, 0xFFFFFFFF);
+
+ if (open) {
+ context.fill(getX(), y + headerHeight + 1, getX() + width, y + height, 0xFF000000);
+ context.drawHorizontalLine(getX(), getX() + width, y + height, 0xFFFFFFFF);
+ context.drawVerticalLine(getX(), y + headerHeight, y + height, 0xFFFFFFFF);
+ context.drawVerticalLine(getX() + width, y + headerHeight, y + height, 0xFFFFFFFF);
+
+ super.renderWidget(context, mouseX, mouseY, delta);
+ } else {
+ renderHeader(context, getRowLeft(), y + 4);
+ }
+
+ context.getMatrices().pop();
+ }
+
+ @Override
+ protected void drawMenuListBackground(DrawContext context) {}
+
+ @Override
+ protected void drawHeaderAndFooterSeparators(DrawContext context) {}
+
+ @Override
+ public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
+ return open && super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount);
+ }
+
+ protected void select(T entry) {
+ selected = entry;
+ open = false;
+ setScrollAmount(0);
+ if (selected != prevSelected) {
+ selectCallback.accept(entry);
+ prevSelected = selected;
+ }
+ }
+
+ static class Entry<T> extends ElementListWidget.Entry<Entry<T>> {
+ private final DropdownWidget<T> dropdownWidget;
+ private final T entry;
+
+ public Entry(DropdownWidget<T> dropdownWidget, T entry) {
+ this.dropdownWidget = dropdownWidget;
+ this.entry = entry;
+ }
+
+ @Override
+ public List<? extends Selectable> selectableChildren() {
+ return List.of();
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return List.of();
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) {
+ context.drawTextWithShadow(client.textRenderer, Text.literal(entry.toString()).fillStyle(Style.EMPTY.withUnderline(hovered)), x + 14, y + 2, 0xFFFFFFFF);
+ if (dropdownWidget.selected == this.entry) {
+ context.drawTextWithShadow(client.textRenderer, "✔", x + 4, y + 2, 0xFFFFFFFF);
+ }
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (button == 0 && dropdownWidget.open) {
+ dropdownWidget.select(entry);
+ return true;
+ }
+ return super.mouseClicked(mouseX, mouseY, button);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/FairySouls.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/FairySouls.java
index 25741b22..91b98e90 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/FairySouls.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/FairySouls.java
@@ -8,6 +8,7 @@ import com.mojang.brigadier.CommandDispatcher;
import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.config.configs.HelperConfig;
+import de.hysky.skyblocker.utils.ColorUtils;
import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.NEURepoManager;
import de.hysky.skyblocker.utils.PosUtils;
@@ -73,7 +74,7 @@ public class FairySouls {
private static void loadFairySouls() {
fairySoulsLoaded = NEURepoManager.runAsyncAfterLoad(() -> {
maxSouls = NEURepoManager.NEU_REPO.getConstants().getFairySouls().getMaxSouls();
- NEURepoManager.NEU_REPO.getConstants().getFairySouls().getSoulLocations().forEach((location, fairiesForLocation) -> fairySouls.put(location, fairiesForLocation.stream().map(coordinate -> new BlockPos(coordinate.getX(), coordinate.getY(), coordinate.getZ())).collect(Collectors.toUnmodifiableMap(pos -> pos, pos -> new ProfileAwareWaypoint(pos, TYPE_SUPPLIER, DyeColor.GREEN.getColorComponents(), DyeColor.RED.getColorComponents())))));
+ NEURepoManager.NEU_REPO.getConstants().getFairySouls().getSoulLocations().forEach((location, fairiesForLocation) -> fairySouls.put(location, fairiesForLocation.stream().map(coordinate -> new BlockPos(coordinate.getX(), coordinate.getY(), coordinate.getZ())).collect(Collectors.toUnmodifiableMap(pos -> pos, pos -> new ProfileAwareWaypoint(pos, TYPE_SUPPLIER, ColorUtils.getFloatComponents(DyeColor.GREEN), ColorUtils.getFloatComponents(DyeColor.RED))))));
LOGGER.debug("[Skyblocker] Loaded {} fairy souls across {} locations", fairySouls.values().stream().mapToInt(Map::size).sum(), fairySouls.size());
try (BufferedReader reader = Files.newBufferedReader(SkyblockerMod.CONFIG_DIR.resolve("found_fairy_souls.json"))) {
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java
index 254077ad..ffeba7ea 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java
@@ -3,6 +3,7 @@ package de.hysky.skyblocker.skyblock.waypoint;
import com.mojang.brigadier.Command;
import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.ColorUtils;
import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.render.RenderHelper;
@@ -46,7 +47,7 @@ import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.lit
public class MythologicalRitual {
private static final Pattern GRIFFIN_BURROW_DUG = Pattern.compile("(?<message>You dug out a Griffin Burrow!|You finished the Griffin burrow chain!) \\((?<index>\\d)/4\\)");
- private static final float[] ORANGE_COLOR_COMPONENTS = DyeColor.ORANGE.getColorComponents();
+ private static final float[] ORANGE_COLOR_COMPONENTS = ColorUtils.getFloatComponents(DyeColor.ORANGE);
private static long lastEchoTime;
private static final Map<BlockPos, GriffinBurrow> griffinBurrows = new HashMap<>();
@Nullable
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java
index bbc9a655..f8930882 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java
@@ -408,7 +408,7 @@ public class OrderedWaypoints {
}
@Override
- protected float[] getColorComponents() {
+ public float[] getColorComponents() {
if (this.colorComponents.length != 3) {
return switch (this.relativeIndex) {
case PREVIOUS -> RED;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Relics.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Relics.java
index 3a9af459..38e30ea8 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Relics.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Relics.java
@@ -8,6 +8,7 @@ import com.mojang.brigadier.CommandDispatcher;
import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.config.configs.OtherLocationsConfig;
+import de.hysky.skyblocker.utils.ColorUtils;
import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.PosUtils;
import de.hysky.skyblocker.utils.Utils;
@@ -58,7 +59,7 @@ public class Relics {
private static void loadRelics(MinecraftClient client) {
relicsLoaded = CompletableFuture.runAsync(() -> {
- try (BufferedReader reader = client.getResourceManager().openAsReader(new Identifier(SkyblockerMod.NAMESPACE, "spidersden/relics.json"))) {
+ try (BufferedReader reader = client.getResourceManager().openAsReader(Identifier.of(SkyblockerMod.NAMESPACE, "spidersden/relics.json"))) {
for (Map.Entry<String, JsonElement> json : JsonParser.parseReader(reader).getAsJsonObject().asMap().entrySet()) {
if (json.getKey().equals("total")) {
totalRelics = json.getValue().getAsInt();
@@ -66,7 +67,7 @@ public class Relics {
for (JsonElement locationJson : json.getValue().getAsJsonArray().asList()) {
JsonObject posData = locationJson.getAsJsonObject();
BlockPos pos = new BlockPos(posData.get("x").getAsInt(), posData.get("y").getAsInt(), posData.get("z").getAsInt());
- relics.put(pos, new ProfileAwareWaypoint(pos, TYPE_SUPPLIER, DyeColor.YELLOW.getColorComponents(), DyeColor.BROWN.getColorComponents()));
+ relics.put(pos, new ProfileAwareWaypoint(pos, TYPE_SUPPLIER, ColorUtils.getFloatComponents(DyeColor.YELLOW), ColorUtils.getFloatComponents(DyeColor.BROWN)));
}
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Waypoints.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Waypoints.java
new file mode 100644
index 00000000..18096117
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Waypoints.java
@@ -0,0 +1,132 @@
+package de.hysky.skyblocker.skyblock.waypoint;
+
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Multimaps;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.JsonOps;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import de.hysky.skyblocker.utils.waypoint.WaypointCategory;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.fabricmc.loader.api.FabricLoader;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.toast.SystemToast;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
+
+public class Waypoints {
+ public static final Logger LOGGER = LoggerFactory.getLogger(Waypoints.class);
+ private static final Codec<List<WaypointCategory>> CODEC = WaypointCategory.CODEC.listOf();
+ private static final Codec<List<WaypointCategory>> SKYTILS_CODEC = WaypointCategory.SKYTILS_CODEC.listOf();
+ protected static final SystemToast.Type WAYPOINTS_TOAST_TYPE = new SystemToast.Type();
+
+ private static final Path waypointsFile = FabricLoader.getInstance().getConfigDir().resolve(SkyblockerMod.NAMESPACE).resolve("waypoints.json");
+ protected static final Multimap<String, WaypointCategory> waypoints = MultimapBuilder.hashKeys().arrayListValues().build();
+
+ public static void init() {
+ loadWaypoints();
+ ClientLifecycleEvents.CLIENT_STOPPING.register(Waypoints::saveWaypoints);
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(Waypoints::render);
+ ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("waypoints").executes(Scheduler.queueOpenScreenCommand(() -> new WaypointsScreen(MinecraftClient.getInstance().currentScreen))))));
+ }
+
+ public static void loadWaypoints() {
+ waypoints.clear();
+ try (BufferedReader reader = Files.newBufferedReader(waypointsFile)) {
+ List<WaypointCategory> waypoints = CODEC.parse(JsonOps.INSTANCE, SkyblockerMod.GSON.fromJson(reader, JsonArray.class)).resultOrPartial(LOGGER::error).orElseThrow();
+ waypoints.forEach(waypointCategory -> Waypoints.waypoints.put(waypointCategory.island(), waypointCategory));
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Waypoints] Encountered exception while loading waypoints", e);
+ }
+ }
+
+ public static List<WaypointCategory> fromSkytilsBase64(String base64, String defaultIsland) {
+ try {
+ if (base64.startsWith("<Skytils-Waypoint-Data>(V")) {
+ int version = Integer.parseInt(base64.substring(26, base64.indexOf(')')));
+ if (version == 1) {
+ return fromSkytilsJson(new String(Base64.getDecoder().decode(base64.substring(base64.indexOf(':') + 1))), defaultIsland);
+ } else {
+ LOGGER.error("[Skyblocker Waypoints] Unknown Skytils waypoint data version: " + version);
+ }
+ } else return fromSkytilsJson(new String(Base64.getDecoder().decode(base64)), defaultIsland);
+ } catch (NumberFormatException e) {
+ LOGGER.error("[Skyblocker Waypoints] Encountered exception while parsing Skytils waypoint data version", e);
+ } catch (IllegalArgumentException e) {
+ LOGGER.error("[Skyblocker Waypoints] Encountered exception while decoding Skytils waypoint data", e);
+ }
+ return Collections.emptyList();
+ }
+
+ public static List<WaypointCategory> fromSkytilsJson(String waypointCategories, String defaultIsland) {
+ JsonArray waypointCategoriesJson;
+ try {
+ waypointCategoriesJson = SkyblockerMod.GSON.fromJson(waypointCategories, JsonObject.class).getAsJsonArray("categories");
+ } catch (JsonSyntaxException e) {
+ JsonObject waypointCategoryJson = new JsonObject();
+ waypointCategoryJson.addProperty("name", "New Category");
+ waypointCategoryJson.addProperty("island", defaultIsland);
+ waypointCategoryJson.add("waypoints", SkyblockerMod.GSON.fromJson(waypointCategories, JsonArray.class));
+ waypointCategoriesJson = new JsonArray();
+ waypointCategoriesJson.add(waypointCategoryJson);
+ }
+ return SKYTILS_CODEC.parse(JsonOps.INSTANCE, waypointCategoriesJson).resultOrPartial(LOGGER::error).orElseThrow();
+ }
+
+ public static String toSkytilsBase64(List<WaypointCategory> waypointCategories) {
+ return Base64.getEncoder().encodeToString(toSkytilsJson(waypointCategories).getBytes());
+ }
+
+ public static String toSkytilsJson(List<WaypointCategory> waypointCategories) {
+ JsonObject waypointCategoriesJson = new JsonObject();
+ waypointCategoriesJson.add("categories", SKYTILS_CODEC.encodeStart(JsonOps.INSTANCE, waypointCategories).resultOrPartial(LOGGER::error).orElseThrow());
+ return SkyblockerMod.GSON_COMPACT.toJson(waypointCategoriesJson);
+ }
+
+ public static void saveWaypoints(MinecraftClient client) {
+ try (BufferedWriter writer = Files.newBufferedWriter(waypointsFile)) {
+ JsonElement waypointsJson = CODEC.encodeStart(JsonOps.INSTANCE, List.copyOf(waypoints.values())).resultOrPartial(LOGGER::error).orElseThrow();
+ SkyblockerMod.GSON.toJson(waypointsJson, writer);
+ LOGGER.info("[Skyblocker Waypoints] Saved waypoints");
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Waypoints] Encountered exception while saving waypoints", e);
+ }
+ }
+
+ public static Multimap<String, WaypointCategory> waypointsDeepCopy() {
+ return waypoints.values().stream().map(WaypointCategory::deepCopy).collect(Multimaps.toMultimap(WaypointCategory::island, Function.identity(), () -> MultimapBuilder.hashKeys().arrayListValues().build()));
+ }
+
+ public static void render(WorldRenderContext context) {
+ if (SkyblockerConfigManager.get().uiAndVisuals.waypoints.enableWaypoints) {
+ Collection<WaypointCategory> categories = waypoints.get(Utils.getLocationRaw());
+ for (WaypointCategory category : categories) {
+ if (category != null) {
+ category.render(context);
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsListWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsListWidget.java
new file mode 100644
index 00000000..f4b54342
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsListWidget.java
@@ -0,0 +1,318 @@
+package de.hysky.skyblocker.skyblock.waypoint;
+
+import de.hysky.skyblocker.mixins.accessors.CheckboxWidgetAccessor;
+import de.hysky.skyblocker.utils.waypoint.NamedWaypoint;
+import de.hysky.skyblocker.utils.waypoint.WaypointCategory;
+import it.unimi.dsi.fastutil.ints.Int2ObjectFunction;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.Selectable;
+import net.minecraft.client.gui.widget.*;
+import net.minecraft.text.Text;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.hit.HitResult;
+import net.minecraft.util.math.BlockPos;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+public class WaypointsListWidget extends ElementListWidget<WaypointsListWidget.AbstractWaypointEntry> {
+ private final AbstractWaypointsScreen<?> screen;
+ private String island;
+ private List<WaypointCategory> waypoints;
+
+ public WaypointsListWidget(MinecraftClient client, AbstractWaypointsScreen<?> screen, int width, int height, int y, int itemHeight) {
+ super(client, width, height, y, itemHeight);
+ this.screen = screen;
+ setIsland(screen.island);
+ }
+
+ @Override
+ public int getRowWidth() {
+ return super.getRowWidth() + 100;
+ }
+
+ @Override
+ protected int getScrollbarX() {
+ return super.getScrollbarX();
+ }
+
+ Optional<WaypointCategoryEntry> getCategory() {
+ if (getSelectedOrNull() instanceof WaypointCategoryEntry category) {
+ return Optional.of(category);
+ } else if (getSelectedOrNull() instanceof WaypointEntry waypointEntry) {
+ return Optional.of(waypointEntry.category);
+ }
+ return Optional.empty();
+ }
+
+ void setIsland(String island) {
+ this.island = island;
+ waypoints = (List<WaypointCategory>) screen.waypoints.get(island);
+ updateEntries();
+ }
+
+ void addWaypointCategoryAfterSelected() {
+ WaypointCategoryEntry categoryEntry = new WaypointCategoryEntry();
+ Optional<WaypointCategoryEntry> selectedCategoryEntryOptional = getCategory();
+ int index = waypoints.size();
+ int entryIndex = children().size();
+ if (selectedCategoryEntryOptional.isPresent()) {
+ WaypointCategoryEntry selectedCategoryEntry = selectedCategoryEntryOptional.get();
+ index = waypoints.indexOf(selectedCategoryEntry.category) + 1;
+ entryIndex = children().indexOf(selectedCategoryEntry) + 1;
+ while (entryIndex < children().size() && !(children().get(entryIndex) instanceof WaypointCategoryEntry)) {
+ entryIndex++;
+ }
+ }
+ waypoints.add(index, categoryEntry.category);
+ children().add(entryIndex, categoryEntry);
+ }
+
+ void updateEntries() {
+ clearEntries();
+ for (WaypointCategory category : waypoints) {
+ WaypointCategoryEntry categoryEntry = new WaypointCategoryEntry(category);
+ addEntry(categoryEntry);
+ for (NamedWaypoint waypoint : category.waypoints()) {
+ addEntry(new WaypointEntry(categoryEntry, waypoint));
+ }
+ }
+ }
+
+ void updateButtons() {
+ for (Entry<AbstractWaypointEntry> entry : children()) {
+ if (entry instanceof WaypointCategoryEntry categoryEntry && categoryEntry.enabled.isChecked() != categoryEntry.category.waypoints().stream().allMatch(screen::isEnabled)) {
+ ((CheckboxWidgetAccessor) categoryEntry.enabled).setChecked(!categoryEntry.enabled.isChecked());
+ } else if (entry instanceof WaypointEntry waypointEntry && waypointEntry.enabled.isChecked() != screen.isEnabled(waypointEntry.waypoint)) {
+ waypointEntry.enabled.onPress();
+ }
+ }
+ }
+
+ private BlockPos getDefaultPos() {
+ return client.crosshairTarget instanceof BlockHitResult blockHitResult && client.crosshairTarget.getType() == HitResult.Type.BLOCK ? blockHitResult.getBlockPos() : client.player != null ? client.player.getBlockPos() : BlockPos.ORIGIN;
+ }
+
+ protected abstract static class AbstractWaypointEntry extends ElementListWidget.Entry<AbstractWaypointEntry> {
+ }
+
+ protected class WaypointCategoryEntry extends AbstractWaypointEntry {
+ private WaypointCategory category;
+ private final List<ClickableWidget> children;
+ private final CheckboxWidget enabled;
+ private final TextFieldWidget nameField;
+ private final ButtonWidget buttonNewWaypoint;
+ private final ButtonWidget buttonDelete;
+
+ public WaypointCategoryEntry() {
+ this(new WaypointCategory("New Category", island, new ArrayList<>()));
+ }
+
+ public WaypointCategoryEntry(WaypointCategory category) {
+ this.category = category;
+ enabled = CheckboxWidget.builder(Text.literal(""), client.textRenderer).checked(!category.waypoints().isEmpty() && category.waypoints().stream().allMatch(screen::isEnabled)).callback((checkbox, checked) -> category.waypoints().forEach(waypoint -> screen.enabledChanged(waypoint, checked))).build();
+ nameField = new TextFieldWidget(client.textRenderer, 70, 20, Text.literal("Name"));
+ nameField.setText(category.name());
+ nameField.setChangedListener(this::updateName);
+ buttonNewWaypoint = ButtonWidget.builder(Text.translatable("skyblocker.waypoints.new"), buttonNewWaypoint -> {
+ WaypointEntry waypointEntry = new WaypointEntry(this);
+ int entryIndex;
+ if (getSelectedOrNull() instanceof WaypointEntry selectedWaypointEntry && selectedWaypointEntry.category == this) {
+ entryIndex = WaypointsListWidget.this.children().indexOf(selectedWaypointEntry) + 1;
+ } else {
+ entryIndex = WaypointsListWidget.this.children().indexOf(this) + 1;
+ while (entryIndex < WaypointsListWidget.this.children().size() && !(WaypointsListWidget.this.children().get(entryIndex) instanceof WaypointCategoryEntry)) {
+ entryIndex++;
+ }
+ }
+ category.waypoints().add(waypointEntry.waypoint);
+ WaypointsListWidget.this.children().add(entryIndex, waypointEntry);
+ }).width(72).build();
+ buttonDelete = ButtonWidget.builder(Text.translatable("selectServer.deleteButton"), buttonDelete -> {
+ int entryIndex = WaypointsListWidget.this.children().indexOf(this) + 1;
+ while (entryIndex < WaypointsListWidget.this.children().size() && !(WaypointsListWidget.this.children().get(entryIndex) instanceof WaypointCategoryEntry)) {
+ WaypointsListWidget.this.children().remove(entryIndex);
+ }
+ WaypointsListWidget.this.children().remove(this);
+ waypoints.remove(category);
+ }).width(38).build();
+ children = List.of(enabled, nameField, buttonNewWaypoint, buttonDelete);
+ }
+
+ @Override
+ public List<? extends Selectable> selectableChildren() {
+ return children;
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return children;
+ }
+
+ private void updateName(String name) {
+ int index = waypoints.indexOf(category);
+ category = category.withName(name);
+ if (index >= 0) {
+ waypoints.set(index, category);
+ }
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) {
+ enabled.setPosition(x, y + 1);
+ nameField.setPosition(x + 22, y);
+ buttonNewWaypoint.setPosition(x + entryWidth - 115, y);
+ buttonDelete.setPosition(x + entryWidth - 38, y);
+ for (ClickableWidget child : children) {
+ child.render(context, mouseX, mouseY, tickDelta);
+ }
+ }
+ }
+
+ protected class WaypointEntry extends AbstractWaypointEntry {
+ private final WaypointCategoryEntry category;
+ private NamedWaypoint waypoint;
+ private final List<ClickableWidget> children;
+ private final CheckboxWidget enabled;
+ private final TextFieldWidget nameField;
+ private final TextFieldWidget xField;
+ private final TextFieldWidget yField;
+ private final TextFieldWidget zField;
+ private final TextFieldWidget colorField;
+ private final ButtonWidget buttonDelete;
+
+ public WaypointEntry(WaypointCategoryEntry category) {
+ this(category, new NamedWaypoint(getDefaultPos(), "New Waypoint", new float[]{0f, 1f, 0f}));
+ }
+
+ public WaypointEntry(WaypointCategoryEntry category, NamedWaypoint waypoint) {
+ this.category = category;
+ this.waypoint = waypoint;
+ enabled = CheckboxWidget.builder(Text.literal(""), client.textRenderer).checked(screen.isEnabled(waypoint)).callback((checkbox, checked) -> screen.enabledChanged(waypoint, checked)).build();
+ nameField = new TextFieldWidget(client.textRenderer, 65, 20, Text.literal("Name"));
+ nameField.setText(waypoint.getName().getString());
+ nameField.setChangedListener(this::updateName);
+ xField = new TextFieldWidget(client.textRenderer, 26, 20, Text.literal("X"));
+ xField.setText(Integer.toString(waypoint.pos.getX()));
+ xField.setTextPredicate(this::checkInt);
+ xField.setChangedListener(this::updateX);
+ yField = new TextFieldWidget(client.textRenderer, 26, 20, Text.literal("Y"));
+ yField.setText(Integer.toString(waypoint.pos.getY()));
+ yField.setTextPredicate(this::checkInt);
+ yField.setChangedListener(this::updateY);
+ zField = new TextFieldWidget(client.textRenderer, 26, 20, Text.literal("Z"));
+ zField.setText(Integer.toString(waypoint.pos.getZ()));
+ zField.setTextPredicate(this::checkInt);
+ zField.setChangedListener(this::updateZ);
+ colorField = new TextFieldWidget(client.textRenderer, 56, 20, Text.literal("Color"));
+ colorField.setText(String.format("%02X%02X%02X%02X", (int) (waypoint.alpha * 255), (int) (waypoint.getColorComponents()[0] * 255), (int) (waypoint.getColorComponents()[1] * 255), (int) (waypoint.getColorComponents()[2] * 255)));
+ colorField.setChangedListener(this::updateColor);
+ buttonDelete = ButtonWidget.builder(Text.translatable("selectServer.deleteButton"), button -> {
+ category.category.waypoints().remove(waypoint);
+ WaypointsListWidget.this.children().remove(this);
+ }).width(38).build();
+ children = List.of(enabled, nameField, xField, yField, zField, colorField, buttonDelete);
+ }
+
+ @Override
+ public List<? extends Selectable> selectableChildren() {
+ return children;
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return children;
+ }
+
+ private void updateName(String name) {
+ if (waypoint.name.getString().equals(name)) return;
+ int index = category.category.waypoints().indexOf(waypoint);
+ waypoint = waypoint.withName(name);
+ if (index >= 0) {
+ category.category.waypoints().set(index, waypoint);
+ }
+ }
+
+ private boolean checkInt(String string) {
+ try {
+ parseEmptiableInt(string);
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ private void updateX(String xString) {
+ updateInt(xString, waypoint.pos.getX(), waypoint::withX);
+ }
+
+ private void updateY(String yString) {
+ updateInt(yString, waypoint.pos.getY(), waypoint::withY);
+ }
+
+ private void updateZ(String zString) {
+ updateInt(zString, waypoint.pos.getZ(), waypoint::withZ);
+ }
+
+ private void updateInt(String newValueString, int currentValue, Int2ObjectFunction<NamedWaypoint> wither) {
+ try {
+ int index = category.category.waypoints().indexOf(waypoint);
+ int newValue = parseEmptiableInt(newValueString);
+ if (newValue == currentValue) return;
+ waypoint = wither.apply(newValue);
+ if (index >= 0) {
+ category.category.waypoints().set(index, waypoint);
+ }
+ } catch (NumberFormatException e) {
+ Waypoints.LOGGER.warn("[Skyblocker Waypoints] Failed to parse integer: {}", newValueString, e);
+ }
+ }
+
+ private void updateColor(String colorString) {
+ try {
+ int index = category.category.waypoints().indexOf(waypoint);
+ int colorInt = parseEmptiableInt(colorString, 16);
+ float[] colorComponents = {((colorInt & 0x00FF0000) >> 16) / 255f, ((colorInt & 0x0000FF00) >> 8) / 255f, (colorInt & 0x000000FF) / 255f};
+ float alpha = ((colorInt & 0xFF000000) >>> 24) / 255f;
+ if (Arrays.equals(waypoint.getColorComponents(), colorComponents) && waypoint.alpha == alpha) return;
+ waypoint = waypoint.withColor(colorComponents, alpha);
+ if (index >= 0) {
+ category.category.waypoints().set(index, waypoint);
+ }
+ } catch (NumberFormatException e) {
+ Waypoints.LOGGER.warn("[Skyblocker Waypoints] Failed to parse color: {}", colorString, e);
+ }
+ }
+
+ private int parseEmptiableInt(String value) throws NumberFormatException {
+ return value.isEmpty() || value.equals("-") ? 0 : Integer.parseInt(value);
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private int parseEmptiableInt(String value, int radix) throws NumberFormatException {
+ return value.isEmpty() || value.equals("-") ? 0 : Integer.parseInt(value, radix);
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) {
+ context.drawTextWithShadow(client.textRenderer, "X:", width / 2 - 56, y + 6, 0xFFFFFF);
+ context.drawTextWithShadow(client.textRenderer, "Y:", width / 2 - 19, y + 6, 0xFFFFFF);
+ context.drawTextWithShadow(client.textRenderer, "Z:", width / 2 + 18, y + 6, 0xFFFFFF);
+ context.drawTextWithShadow(client.textRenderer, "#", x + entryWidth - 105, y + 6, 0xFFFFFF);
+ enabled.setPosition(x + 10, y + 1);
+ nameField.setPosition(x + 32, y);
+ xField.setPosition(width / 2 - 48, y);
+ yField.setPosition(width / 2 - 11, y);
+ zField.setPosition(width / 2 + 26, y);
+ colorField.setPosition(x + entryWidth - 99, y);
+ buttonDelete.setPosition(x + entryWidth - 38, y);
+ for (ClickableWidget child : children) {
+ child.render(context, mouseX, mouseY, tickDelta);
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsScreen.java
new file mode 100644
index 00000000..23a24361
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsScreen.java
@@ -0,0 +1,76 @@
+package de.hysky.skyblocker.skyblock.waypoint;
+
+import de.hysky.skyblocker.utils.waypoint.NamedWaypoint;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ConfirmScreen;
+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.SimplePositioningWidget;
+import net.minecraft.screen.ScreenTexts;
+import net.minecraft.text.Text;
+
+public class WaypointsScreen extends AbstractWaypointsScreen<Screen> {
+ private ButtonWidget buttonNew;
+ private ButtonWidget buttonDone;
+
+ public WaypointsScreen(Screen parent) {
+ super(Text.translatable("skyblocker.waypoints.config"), parent, Waypoints.waypointsDeepCopy());
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ GridWidget gridWidget = new GridWidget();
+ gridWidget.getMainPositioner().marginX(5).marginY(2);
+ GridWidget.Adder adder = gridWidget.createAdder(2);
+ adder.add(ButtonWidget.builder(Text.translatable("skyblocker.waypoints.share"), buttonShare -> client.setScreen(new WaypointsShareScreen(this, waypoints))).build());
+ buttonNew = adder.add(ButtonWidget.builder(Text.translatable("skyblocker.waypoints.newCategory"), buttonNew -> waypointsListWidget.addWaypointCategoryAfterSelected()).build());
+ adder.add(ButtonWidget.builder(ScreenTexts.CANCEL, button -> close()).build());
+ buttonDone = adder.add(ButtonWidget.builder(ScreenTexts.DONE, button -> {
+ saveWaypoints();
+ close();
+ }).build());
+ gridWidget.refreshPositions();
+ SimplePositioningWidget.setPos(gridWidget, 0, this.height - 64, this.width, 64);
+ gridWidget.forEachChild(this::addDrawableChild);
+ updateButtons();
+ }
+
+ @Override
+ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
+ super.render(context, mouseX, mouseY, delta);
+ context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 16, 0xFFFFFF);
+ }
+
+ @Override
+ protected boolean isEnabled(NamedWaypoint waypoint) {
+ return waypoint.shouldRender();
+ }
+
+ @Override
+ protected void enabledChanged(NamedWaypoint waypoint, boolean enabled) {
+ waypoint.setShouldRender(enabled);
+ }
+
+ private void saveWaypoints() {
+ Waypoints.waypoints.clear();
+ Waypoints.waypoints.putAll(waypoints);
+ Waypoints.saveWaypoints(client);
+ }
+
+ @Override
+ public void close() {
+ assert client != null;
+ if (!waypoints.equals(Waypoints.waypoints)) {
+ client.setScreen(new ConfirmScreen(confirmedAction -> client.setScreen(confirmedAction ? parent : this),
+ Text.translatable("text.skyblocker.quit_config"),
+ Text.translatable("text.skyblocker.quit_config_sure"),
+ Text.translatable("text.skyblocker.quit_discard"),
+ ScreenTexts.CANCEL
+ ));
+ } else {
+ client.setScreen(parent);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsShareScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsShareScreen.java
new file mode 100644
index 00000000..aee21ec8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsShareScreen.java
@@ -0,0 +1,86 @@
+package de.hysky.skyblocker.skyblock.waypoint;
+
+import com.google.common.collect.Multimap;
+import de.hysky.skyblocker.utils.waypoint.NamedWaypoint;
+import de.hysky.skyblocker.utils.waypoint.WaypointCategory;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.tooltip.Tooltip;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.gui.widget.GridWidget;
+import net.minecraft.client.gui.widget.SimplePositioningWidget;
+import net.minecraft.client.toast.SystemToast;
+import net.minecraft.screen.ScreenTexts;
+import net.minecraft.text.Text;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class WaypointsShareScreen extends AbstractWaypointsScreen<WaypointsScreen> {
+ private final Set<NamedWaypoint> selectedWaypoints = new HashSet<>();
+
+ protected WaypointsShareScreen(WaypointsScreen parent, Multimap<String, WaypointCategory> waypoints) {
+ super(Text.translatable("skyblocker.waypoints.shareWaypoints"), parent, waypoints, parent.island);
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ GridWidget gridWidget = new GridWidget();
+ gridWidget.getMainPositioner().marginX(5).marginY(2);
+ GridWidget.Adder adder = gridWidget.createAdder(2);
+ adder.add(ButtonWidget.builder(Text.translatable("skyblocker.waypoints.importWaypointsSkytils"), buttonImport -> {
+ try {
+ List<WaypointCategory> waypointCategories = Waypoints.fromSkytilsBase64(client.keyboard.getClipboard(), island);
+ for (WaypointCategory waypointCategory : waypointCategories) {
+ selectedWaypoints.addAll(waypointCategory.waypoints());
+ waypoints.put(waypointCategory.island(), waypointCategory);
+ }
+ waypointsListWidget.updateEntries();
+ SystemToast.show(client.getToastManager(), Waypoints.WAYPOINTS_TOAST_TYPE, Text.translatable("skyblocker.waypoints.importSuccess"), Text.translatable("skyblocker.waypoints.importSuccessText", waypointCategories.stream().map(WaypointCategory::waypoints).mapToInt(List::size).sum(), waypointCategories.size()));
+ } catch (Exception e) {
+ Waypoints.LOGGER.error("[Skyblocker Waypoints] Encountered exception while parsing Skytils waypoint data", e);
+ SystemToast.show(client.getToastManager(), Waypoints.WAYPOINTS_TOAST_TYPE, Text.translatable("skyblocker.waypoints.importError"), Text.translatable("skyblocker.waypoints.importErrorText"));
+ }
+ }).tooltip(Tooltip.of(Text.translatable("skyblocker.waypoints.importWaypointsSkytils.tooltip"))).build());
+ adder.add(ButtonWidget.builder(Text.translatable("skyblocker.waypoints.importWaypointsSnoopy"), buttonImport -> {
+ }).tooltip(Tooltip.of(Text.translatable("skyblocker.waypoints.importWaypointsSnoopy.tooltip"))).build());
+ adder.add(ButtonWidget.builder(ScreenTexts.BACK, buttonBack -> close()).build());
+ adder.add(ButtonWidget.builder(Text.translatable("skyblocker.waypoints.exportWaypointsSkytils"), buttonExport -> {
+ try {
+ List<WaypointCategory> waypointCategories = waypoints.values().stream().filter(waypointCategory -> waypointCategory.island().equals(island)).map(WaypointCategory.filter(selectedWaypoints::contains)).filter(waypointCategory -> !waypointCategory.waypoints().isEmpty()).toList();
+ client.keyboard.setClipboard(Waypoints.toSkytilsBase64(waypointCategories));
+ SystemToast.show(client.getToastManager(), Waypoints.WAYPOINTS_TOAST_TYPE, Text.translatable("skyblocker.waypoints.exportSuccess"), Text.translatable("skyblocker.waypoints.exportSuccessText", waypointCategories.stream().map(WaypointCategory::waypoints).mapToInt(List::size).sum(), waypointCategories.size()));
+ } catch (Exception e) {
+ Waypoints.LOGGER.error("[Skyblocker Waypoints] Encountered exception while serializing Skytils waypoint data", e);
+ SystemToast.show(client.getToastManager(), Waypoints.WAYPOINTS_TOAST_TYPE, Text.translatable("skyblocker.waypoints.exportError"), Text.translatable("skyblocker.waypoints.exportErrorText"));
+ }
+ }).tooltip(Tooltip.of(Text.translatable("skyblocker.waypoints.exportWaypointsSkytils.tooltip"))).build());
+ gridWidget.refreshPositions();
+ SimplePositioningWidget.setPos(gridWidget, 0, this.height - 64, this.width, 64);
+ gridWidget.forEachChild(this::addDrawableChild);
+ }
+
+ @Override
+ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
+ super.render(context, mouseX, mouseY, delta);
+ context.drawCenteredTextWithShadow(this.textRenderer, this.title, this.width / 2, 16, 0xFFFFFF);
+ }
+
+ @Override
+ protected boolean isEnabled(NamedWaypoint waypoint) {
+ return selectedWaypoints.contains(waypoint);
+ }
+
+ @Override
+ protected void enabledChanged(NamedWaypoint waypoint, boolean enabled) {
+ if (enabled) selectedWaypoints.add(waypoint);
+ else selectedWaypoints.remove(waypoint);
+ }
+
+ @SuppressWarnings("DataFlowIssue")
+ @Override
+ public void close() {
+ client.setScreen(parent);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java b/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java
new file mode 100644
index 00000000..5f65c336
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java
@@ -0,0 +1,172 @@
+package de.hysky.skyblocker.utils;
+
+import java.nio.ByteBuffer;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.util.Base64;
+import java.util.Objects;
+import java.util.UUID;
+
+import de.hysky.skyblocker.mixins.accessors.MinecraftClientAccessor;
+import net.minecraft.client.session.ProfileKeys;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+
+import com.google.gson.JsonParser;
+import com.mojang.logging.LogUtils;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.JsonOps;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.minecraft.SharedConstants;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.network.encryption.PlayerKeyPair;
+import net.minecraft.text.Text;
+import net.minecraft.util.Uuids;
+import net.minecraft.util.dynamic.Codecs;
+
+/**
+ * This class is responsible for communicating with the API to retrieve a fully custom token used to gain access to more privileged APIs
+ * such as the Hypixel API Proxy. The main point of this is to verify that a person is most likely playing Minecraft, and thus is likely to be
+ * using the mod, and not somebody who is attempting to freeload off of our services.
+ */
+public class ApiAuthentication {
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+ private static final String MINECRAFT_VERSION = SharedConstants.getGameVersion().getName();
+ private static final String AUTH_URL = "https://hysky.de/api/aaron/authenticate";
+ private static final String CONTENT_TYPE = "application/json";
+ private static final String ALGORITHM = "SHA256withRSA";
+
+ private static TokenInfo tokenInfo = null;
+
+ public static void init() {
+ //Update token after the profileKeys instance is initialized
+ ClientLifecycleEvents.CLIENT_STARTED.register(_client -> updateToken());
+ }
+
+ /**
+ * Refreshes the token by fetching the player's key pair from the Minecraft Services API.
+ *
+ * We use the player's uuid, public key, public key signature, public key expiry date, and randomly signed data by the private key to verify the identity/legitimacy
+ * of the player without the server needing to handle any privileged information.
+ *
+ * Mojang provides a signature for each key pair which is comprised of the player's uuid, public key, and expiry time so we can easily validate if the key pair
+ * was generated by Mojang and is tied to said player. For information about what the randomly signed data is used for and why see {@link #getRandomSignedData(PrivateKey)}
+ */
+ private static void updateToken() {
+ ProfileKeys profileKeys = ((MinecraftClientAccessor) CLIENT).getProfileKeys();
+ //The fetching runs async in ProfileKeysImpl#getKeyPair
+ profileKeys.fetchKeyPair().thenAcceptAsync(playerKeypairOpt -> {
+ if (playerKeypairOpt.isPresent()) {
+ PlayerKeyPair playerKeyPair = playerKeypairOpt.get();
+
+ //The key header and footer can be sent but that doesn't matter to the server
+ String publicKey = Base64.getMimeEncoder().encodeToString(playerKeyPair.publicKey().data().key().getEncoded());
+ byte[] publicKeySignature = playerKeyPair.publicKey().data().keySignature();
+ long expiresAt = playerKeyPair.publicKey().data().expiresAt().toEpochMilli();
+
+ TokenRequest.KeyPairInfo keyPairInfo = new TokenRequest.KeyPairInfo(Objects.requireNonNull(CLIENT.getSession().getUuidOrNull()), publicKey, publicKeySignature, expiresAt);
+ TokenRequest.SignedData signedData = Objects.requireNonNull(getRandomSignedData(playerKeyPair.privateKey()));
+ TokenRequest tokenRequest = new TokenRequest(keyPairInfo, signedData, SkyblockerMod.SKYBLOCKER_MOD.getMetadata().getId(), MINECRAFT_VERSION, SkyblockerMod.VERSION);
+
+ String request = SkyblockerMod.GSON.toJson(TokenRequest.CODEC.encodeStart(JsonOps.INSTANCE, tokenRequest).getOrThrow());
+
+ try {
+ tokenInfo = TokenInfo.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(Http.sendPostRequest(AUTH_URL, request, CONTENT_TYPE))).getOrThrow();
+ int refreshAtTicks = (int) (((tokenInfo.expiresAt() - tokenInfo.issuedAt()) / 1000L) - 300L) * 20; //Refresh 5 minutes before expiry date
+
+ Scheduler.INSTANCE.schedule(ApiAuthentication::updateToken, refreshAtTicks, true);
+ } catch (Exception e) {
+ //Try again in 1 minute
+ logErrorAndScheduleRetry(Text.translatable("skyblocker.api.token.authFailure"), 60 * 20, "[Skyblocker Api Auth] Failed to refresh the api token! Some features might not work :(", e);
+ }
+ } else {
+ //The Minecraft Services API is probably down so we will retry in 5 minutes, either that or your access token has expired (game open for 24h) and you need to restart.
+ logErrorAndScheduleRetry(Text.translatable("skyblocker.api.token.noProfileKeys"), 300 * 20, "[Skyblocker Api Auth] Failed to fetch profile keys! Some features may not work temporarily :( (Has your game been open for more than 24 hours? If so restart.)");
+ }
+ }).exceptionally(throwable -> {
+ //Try again in 1 minute
+ logErrorAndScheduleRetry(Text.translatable("skyblocker.api.token.authFailure"), 60 * 20, "[Skyblocker Api Auth] Encountered an unexpected exception while refreshing the api token!", throwable);
+
+ return null;
+ });
+ }
+
+ /**
+ * Signs a string of random data with the key pair's private key. This is required to know if you are the real holder of the key pair or not. Why? Because your public key,
+ * public key signature, and expiry time are forwarded by the server to all players on the same server as you. This means that a malicious
+ * individual could scrape key pairs and pretend to be someone else when requesting a token from our API. So by signing data with the private key,
+ * the server can use the public key to verify its integrity which proves that the requester is the true holder of the complete key pair and not someone trying to pose as them for malicious purposes.
+ */
+ private static TokenRequest.SignedData getRandomSignedData(PrivateKey privateKey) {
+ try {
+ Signature signature = Signature.getInstance(ALGORITHM);
+ UUID uuid = UUID.randomUUID();
+ ByteBuffer buf = ByteBuffer.allocate(16)
+ .putLong(uuid.getMostSignificantBits())
+ .putLong(uuid.getLeastSignificantBits());
+
+ signature.initSign(privateKey);
+ signature.update(buf.array());
+
+ byte[] signedData = signature.sign();
+
+ return new TokenRequest.SignedData(buf.array(), signedData);
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Api Auth] Failed to sign random data!", e);
+ }
+
+ //This should never ever be the case, since we are signing data that is not invalid in any case
+ return null;
+ }
+
+ private static void logErrorAndScheduleRetry(Text warningMessage, int retryAfter, String logMessage, Object... logArgs) {
+ LOGGER.error(logMessage, logArgs);
+ Scheduler.INSTANCE.schedule(ApiAuthentication::updateToken, retryAfter, true);
+
+ if (CLIENT.player != null) CLIENT.player.sendMessage(Constants.PREFIX.get().append(warningMessage));
+ }
+
+ @Nullable
+ public static String getToken() {
+ return tokenInfo != null ? tokenInfo.token() : null;
+ }
+
+ private record TokenRequest(KeyPairInfo keyPairInfo, SignedData signedData, String mod, String minecraftVersion, String modVersion) {
+ private static final Codec<TokenRequest> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ KeyPairInfo.CODEC.fieldOf("keyPair").forGetter(TokenRequest::keyPairInfo),
+ SignedData.CODEC.fieldOf("signedData").forGetter(TokenRequest::signedData),
+ Codec.STRING.fieldOf("mod").forGetter(TokenRequest::mod),
+ Codec.STRING.fieldOf("minecraftVersion").forGetter(TokenRequest::minecraftVersion),
+ Codec.STRING.fieldOf("modVersion").forGetter(TokenRequest::modVersion))
+ .apply(instance, TokenRequest::new));
+
+ private record KeyPairInfo(UUID uuid, String publicKey, byte[] publicKeySignature, long expiresAt) {
+ private static final Codec<KeyPairInfo> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Uuids.STRING_CODEC.fieldOf("uuid").forGetter(KeyPairInfo::uuid),
+ Codec.STRING.fieldOf("publicKey").forGetter(KeyPairInfo::publicKey),
+ Codecs.BASE_64.fieldOf("publicKeySignature").forGetter(KeyPairInfo::publicKeySignature),
+ Codec.LONG.fieldOf("expiresAt").forGetter(KeyPairInfo::expiresAt))
+ .apply(instance, KeyPairInfo::new));
+ }
+
+ private record SignedData(byte[] original, byte[] signed) {
+ private static final Codec<SignedData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codecs.BASE_64.fieldOf("original").forGetter(SignedData::original),
+ Codecs.BASE_64.fieldOf("signed").forGetter(SignedData::signed))
+ .apply(instance, SignedData::new));
+ }
+ }
+
+ private record TokenInfo(String token, long issuedAt, long expiresAt) {
+ private static final Codec<TokenInfo> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codec.STRING.fieldOf("token").forGetter(TokenInfo::token),
+ Codec.LONG.fieldOf("issuedAt").forGetter(TokenInfo::issuedAt),
+ Codec.LONG.fieldOf("expiresAt").forGetter(TokenInfo::expiresAt))
+ .apply(instance, TokenInfo::new));
+ }
+}
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/ColorUtils.java b/src/main/java/de/hysky/skyblocker/utils/ColorUtils.java
index 0196edf2..2c8f5e4a 100644
--- a/src/main/java/de/hysky/skyblocker/utils/ColorUtils.java
+++ b/src/main/java/de/hysky/skyblocker/utils/ColorUtils.java
@@ -1,8 +1,11 @@
package de.hysky.skyblocker.utils;
+import net.minecraft.util.DyeColor;
+
public class ColorUtils {
/**
* Takes an RGB color as an integer and returns an array of the color's components as floats, in RGB format.
+ *
* @param color The color to get the components of.
* @return An array of the color's components as floats.
*/
@@ -13,4 +16,11 @@ public class ColorUtils {
(color & 0xFF) / 255f
};
}
+
+ /**
+ * @param dye The dye from which the entity color will be used for the components.
+ */
+ public static float[] getFloatComponents(DyeColor dye) {
+ return getFloatComponents(dye.getEntityColor());
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/utils/Http.java b/src/main/java/de/hysky/skyblocker/utils/Http.java
index 871eac78..1adf75d3 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,11 +21,6 @@ import java.time.Duration;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;
-import org.jetbrains.annotations.NotNull;
-
-import de.hysky.skyblocker.SkyblockerMod;
-import net.minecraft.SharedConstants;
-
/**
* @implNote All http requests are sent using HTTP 2
*/
@@ -33,15 +33,18 @@ public class Http {
.followRedirects(Redirect.NORMAL)
.build();
- private static ApiResponse sendCacheableGetRequest(String url) throws IOException, InterruptedException {
- HttpRequest request = HttpRequest.newBuilder()
+ private static ApiResponse sendCacheableGetRequest(String url, @Nullable String token) throws IOException, InterruptedException {
+ HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.GET()
.header("Accept", "application/json")
.header("Accept-Encoding", "gzip, deflate")
.header("User-Agent", USER_AGENT)
.version(Version.HTTP_2)
- .uri(URI.create(url))
- .build();
+ .uri(URI.create(url));
+
+ if (token != null) requestBuilder.header("Token", token);
+
+ HttpRequest request = requestBuilder.build();
HttpResponse<InputStream> response = HTTP_CLIENT.send(request, BodyHandlers.ofInputStream());
InputStream decodedInputStream = getDecodedInputStream(response);
@@ -69,7 +72,7 @@ public class Http {
}
public static String sendGetRequest(String url) throws IOException, InterruptedException {
- return sendCacheableGetRequest(url).content();
+ return sendCacheableGetRequest(url, null).content();
}
public static HttpHeaders sendHeadRequest(String url) throws IOException, InterruptedException {
@@ -84,8 +87,27 @@ public class Http {
return response.headers();
}
+ 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)
+ .version(Version.HTTP_2)
+ .uri(URI.create(url))
+ .build();
+
+ HttpResponse<InputStream> response = HTTP_CLIENT.send(request, BodyHandlers.ofInputStream());
+ InputStream decodedInputStream = getDecodedInputStream(response);
+
+ String responseBody = new String(decodedInputStream.readAllBytes());
+
+ return responseBody;
+ }
+
public static ApiResponse sendName2UuidRequest(String name) throws IOException, InterruptedException {
- return sendCacheableGetRequest(NAME_2_UUID + name);
+ return sendCacheableGetRequest(NAME_2_UUID + name, null);
}
/**
@@ -96,7 +118,7 @@ public class Http {
* @implNote the {@code v2} prefix is automatically added
*/
public static ApiResponse sendHypixelRequest(String endpoint, @NotNull String query) throws IOException, InterruptedException {
- return sendCacheableGetRequest(HYPIXEL_PROXY + endpoint + query);
+ return sendCacheableGetRequest(HYPIXEL_PROXY + endpoint + query, ApiAuthentication.getToken());
}
private static InputStream getDecodedInputStream(HttpResponse<InputStream> response) {
diff --git a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java
index 13b28808..de7a0f9e 100644
--- a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java
+++ b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java
@@ -1,5 +1,7 @@
package de.hysky.skyblocker.utils;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.authlib.properties.Property;
import com.mojang.authlib.properties.PropertyMap;
@@ -8,10 +10,15 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.codecs.RecordCodecBuilder;
+import de.hysky.skyblocker.skyblock.item.tooltip.adders.ObtainedDateTooltip;
import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import it.unimi.dsi.fastutil.doubles.DoubleBooleanPair;
import it.unimi.dsi.fastutil.ints.IntIntPair;
+import it.unimi.dsi.fastutil.longs.LongBooleanPair;
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
import net.minecraft.component.ComponentChanges;
+import net.minecraft.component.ComponentHolder;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.LoreComponent;
import net.minecraft.component.type.NbtComponent;
@@ -20,7 +27,6 @@ import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.nbt.NbtCompound;
-import net.minecraft.nbt.NbtElement;
import net.minecraft.registry.Registries;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.text.Text;
@@ -29,13 +35,7 @@ import net.minecraft.util.dynamic.Codecs;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.time.temporal.TemporalAccessor;
-import java.util.Iterator;
import java.util.List;
-import java.util.Locale;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.regex.Matcher;
@@ -46,8 +46,6 @@ import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.lit
public class ItemUtils {
public static final String ID = "id";
public static final String UUID = "uuid";
- private static final DateTimeFormatter OBTAINED_DATE_FORMATTER = DateTimeFormatter.ofPattern("MMMM d, yyyy").withZone(ZoneId.systemDefault()).localizedBy(Locale.ENGLISH);
- private static final DateTimeFormatter OLD_OBTAINED_DATE_FORMAT = DateTimeFormatter.ofPattern("M/d/yy h:m a").withZone(ZoneId.of("UTC")).localizedBy(Locale.ENGLISH);
public static final Pattern NOT_DURABILITY = Pattern.compile("[^0-9 /]");
public static final Predicate<String> FUEL_PREDICATE = line -> line.contains("Fuel: ");
private static final Codec<RegistryEntry<Item>> EMPTY_ALLOWING_ITEM_CODEC = Registries.ITEM.getEntryCodec();
@@ -65,7 +63,7 @@ public class ItemUtils {
}
@SuppressWarnings("deprecation")
- public static NbtCompound getCustomData(@NotNull ItemStack stack) {
+ public static NbtCompound getCustomData(@NotNull ComponentHolder stack) {
return stack.getOrDefault(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT).getNbt();
}
@@ -75,7 +73,7 @@ public class ItemUtils {
* @param stack the item stack to get the internal name from
* @return an optional containing the internal name of the item stack
*/
- public static Optional<String> getItemIdOptional(@NotNull ItemStack stack) {
+ public static Optional<String> getItemIdOptional(@NotNull ItemStack stack) {
NbtCompound customData = getCustomData(stack);
return customData.contains(ID) ? Optional.of(customData.getString(ID)) : Optional.empty();
}
@@ -86,7 +84,7 @@ public class ItemUtils {
* @param stack the item stack to get the internal name from
* @return the internal name of the item stack, or an empty string if the item stack is null or does not have an internal name
*/
- public static String getItemId(@NotNull ItemStack stack) {
+ public static String getItemId(@NotNull ItemStack stack) {
return getCustomData(stack).getString(ID);
}
@@ -96,7 +94,7 @@ public class ItemUtils {
* @param stack the item stack to get the UUID from
* @return an optional containing the UUID of the item stack
*/
- public static Optional<String> getItemUuidOptional(@NotNull ItemStack stack) {
+ public static Optional<String> getItemUuidOptional(@NotNull ItemStack stack) {
NbtCompound customData = getCustomData(stack);
return customData.contains(UUID) ? Optional.of(customData.getString(UUID)) : Optional.empty();
}
@@ -107,11 +105,46 @@ public class ItemUtils {
* @param stack the item stack to get the UUID from
* @return the UUID of the item stack, or an empty string if the item stack is null or does not have a UUID
*/
- public static String getItemUuid(@NotNull ItemStack stack) {
+ public static String getItemUuid(@NotNull ComponentHolder stack) {
return getCustomData(stack).getString(UUID);
}
/**
+ * Gets the bazaar sell price or the lowest bin based on the id of the item stack.
+ *
+ * @return An {@link LongBooleanPair} with the {@code left long} representing the item's price,
+ * and the {@code right boolean} indicating if the price was based on complete data.
+ */
+ public static DoubleBooleanPair getItemPrice(@NotNull ItemStack stack) {
+ return getItemPrice(getItemId(stack));
+ }
+
+ /**
+ * Gets the bazaar sell price or the lowest bin of the item with the specified id.
+ *
+ * @return An {@link LongBooleanPair} with the {@code left long} representing the item's price,
+ * and the {@code right boolean} indicating if the price was based on complete data.
+ */
+ public static DoubleBooleanPair getItemPrice(@Nullable String id) {
+ JsonObject bazaarPrices = TooltipInfoType.BAZAAR.getData();
+ JsonObject lowestBinPrices = TooltipInfoType.LOWEST_BINS.getData();
+
+ if (id == null || id.isEmpty() || bazaarPrices == null || lowestBinPrices == null) return DoubleBooleanPair.of(0, false);
+
+ if (bazaarPrices.has(id)) {
+ JsonElement sellPrice = bazaarPrices.get(id).getAsJsonObject().get("sellPrice");
+ boolean isPriceNull = sellPrice.isJsonNull();
+ return DoubleBooleanPair.of(isPriceNull ? 0 : sellPrice.getAsDouble(), !isPriceNull);
+ }
+
+ if (lowestBinPrices.has(id)) {
+ return DoubleBooleanPair.of(lowestBinPrices.get(id).getAsDouble(), true);
+ }
+
+ return DoubleBooleanPair.of(0, false);
+ }
+
+ /**
* This method converts the "timestamp" variable into the same date format as Hypixel represents it in the museum.
* Currently, there are two types of string timestamps the legacy which is built like this
* "dd/MM/yy hh:mm" ("25/04/20 16:38") and the current which is built like this
@@ -126,21 +159,11 @@ public class ItemUtils {
*
* @param stack the item under the pointer
* @return if the item have a "Timestamp" it will be shown formated on the tooltip
+ * @deprecated use {@link ObtainedDateTooltip#getTimestamp(ItemStack)} instead
*/
- public static String getTimestamp(ItemStack stack) {
- NbtCompound customData = getCustomData(stack);
-
- if (customData != null && customData.contains("timestamp", NbtElement.LONG_TYPE)) {
- Instant date = Instant.ofEpochMilli(customData.getLong("timestamp"));
- return OBTAINED_DATE_FORMATTER.format(date);
- }
-
- if (customData != null && customData.contains("timestamp", NbtElement.STRING_TYPE)) {
- TemporalAccessor date = OLD_OBTAINED_DATE_FORMAT.parse(customData.getString("timestamp"));
- return OBTAINED_DATE_FORMATTER.format(date);
- }
-
- return "";
+ @Deprecated
+ public static String getTimestamp(ItemStack stack) {
+ return ObtainedDateTooltip.getTimestamp(stack);
}
public static boolean hasCustomDurability(@NotNull ItemStack stack) {
@@ -198,7 +221,7 @@ public class ItemUtils {
public static List<Text> getLore(ItemStack item) {
return item.getOrDefault(DataComponentTypes.LORE, LoreComponent.DEFAULT).styledLines();
}
-
+
public static PropertyMap propertyMapWithTexture(String textureValue) {
return Codecs.GAME_PROFILE_PROPERTY_MAP.parse(JsonOps.INSTANCE, JsonParser.parseString("[{\"name\":\"textures\",\"value\":\"" + textureValue + "\"}]")).getOrThrow();
}
@@ -207,12 +230,12 @@ public class ItemUtils {
if (!stack.isOf(Items.PLAYER_HEAD) || !stack.contains(DataComponentTypes.PROFILE)) return "";
ProfileComponent profile = stack.get(DataComponentTypes.PROFILE);
- String texture = profile.properties().get("textures").stream()
+ if (profile == null) return "";
+
+ return profile.properties().get("textures").stream()
.map(Property::value)
.findFirst()
.orElse("");
-
- return texture;
}
public static Optional<String> getHeadTextureOptional(ItemStack stack) {
diff --git a/src/main/java/de/hysky/skyblocker/utils/PosUtils.java b/src/main/java/de/hysky/skyblocker/utils/PosUtils.java
index 73ada889..4ca37a83 100644
--- a/src/main/java/de/hysky/skyblocker/utils/PosUtils.java
+++ b/src/main/java/de/hysky/skyblocker/utils/PosUtils.java
@@ -1,5 +1,6 @@
package de.hysky.skyblocker.utils;
+import com.google.gson.JsonObject;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
@@ -17,6 +18,10 @@ public final class PosUtils {
return new BlockPos(Integer.parseInt(posArray[0]), Integer.parseInt(posArray[1]), Integer.parseInt(posArray[2]));
}
+ public static BlockPos parsePosJson(JsonObject posJson) {
+ return new BlockPos(posJson.get("x").getAsInt(), posJson.get("y").getAsInt(), posJson.get("z").getAsInt());
+ }
+
public static String getPosString(BlockPos blockPos) {
return blockPos.getX() + "," + blockPos.getY() + "," + blockPos.getZ();
}
diff --git a/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java b/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java
index 0a1f238a..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,17 +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 void init() {
- updateProfile();
- }
-
- 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());
}
@@ -36,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/RomanNumerals.java b/src/main/java/de/hysky/skyblocker/utils/RomanNumerals.java
new file mode 100644
index 00000000..007cb0b1
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/RomanNumerals.java
@@ -0,0 +1,54 @@
+package de.hysky.skyblocker.utils;
+
+public class RomanNumerals {
+ private RomanNumerals() {
+ }
+ private static int getDecimalValue(char romanChar) {
+ return switch (romanChar) {
+ case 'I' -> 1;
+ case 'V' -> 5;
+ case 'X' -> 10;
+ case 'L' -> 50;
+ case 'C' -> 100;
+ case 'D' -> 500;
+ case 'M' -> 1000;
+ default -> 0;
+ };
+ }
+
+ /**
+ * Checks if a string is a valid roman numeral.
+ * It's the caller's responsibility to clean up the string before calling this method (such as trimming it).
+ * @param romanNumeral The roman numeral to check.
+ * @return True if the string is a valid roman numeral, false otherwise.
+ * @implNote This will only check if the string contains valid roman numeral characters. It won't check if the numeral is well-formed.
+ */
+ public static boolean isValidRomanNumeral(String romanNumeral) {
+ if (romanNumeral == null || romanNumeral.isEmpty()) return false;
+ for (int i = 0; i < romanNumeral.length(); i++) {
+ if (getDecimalValue(romanNumeral.charAt(i)) == 0) return false;
+ }
+ return true;
+ }
+
+ /**
+ * Converts a roman numeral to a decimal number.
+ *
+ * @param romanNumeral The roman numeral to convert.
+ * @return The decimal number, or 0 if the roman numeral string has non-roman characters in it, or if the string is empty or null.
+ */
+ public static int romanToDecimal(String romanNumeral) {
+ if (romanNumeral == null || romanNumeral.isEmpty()) return 0;
+ romanNumeral = romanNumeral.trim().toUpperCase();
+ int decimal = 0;
+ int lastNumber = 0;
+ for (int i = romanNumeral.length() - 1; i >= 0; i--) {
+ char ch = romanNumeral.charAt(i);
+ int number = getDecimalValue(ch);
+ if (number == 0) return 0; //Malformed roman numeral
+ decimal = number >= lastNumber ? decimal + number : decimal - number;
+ lastNumber = number;
+ }
+ return decimal;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java
index 62a3b897..84b3cb9e 100644
--- a/src/main/java/de/hysky/skyblocker/utils/Utils.java
+++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java
@@ -2,14 +2,14 @@ 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;
-import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
import de.hysky.skyblocker.utils.scheduler.Scheduler;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
-import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback;
import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
@@ -19,6 +19,7 @@ import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.network.PlayerListEntry;
import net.minecraft.scoreboard.*;
+import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import org.jetbrains.annotations.NotNull;
@@ -234,7 +235,6 @@ public class Utils {
if (!isOnSkyblock) {
if (!isInjected) {
isInjected = true;
- ItemTooltipCallback.EVENT.register(ItemTooltip::getTooltip);
}
isOnSkyblock = true;
SkyblockEvents.JOIN.invoker().onSkyblockJoin();
@@ -355,6 +355,23 @@ 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);
+
+ MutableText time = Text.empty();
+ if (hours > 0) {
+ time.append(hours + "h").append(" ");
+ }
+ if (hours > 0 || minutes > 0) {
+ time.append(minutes + "m").append(" ");
+ }
+ time.append(seconds + "s");
+ return time;
+ }
+
private static void updateFromPlayerList(MinecraftClient client) {
if (client.getNetworkHandler() == null) {
return;
@@ -492,4 +509,8 @@ public class Utils {
((MessageHandlerAccessor) client.getMessageHandler()).invokeAddToChatLog(message, Instant.now());
client.getNarratorManager().narrateSystemMessage(message);
}
+
+ public static String getUndashedUuid() {
+ return UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull());
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/utils/config/DurationController.java b/src/main/java/de/hysky/skyblocker/utils/config/DurationController.java
new file mode 100644
index 00000000..09edcf3c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/config/DurationController.java
@@ -0,0 +1,70 @@
+package de.hysky.skyblocker.utils.config;
+
+import de.hysky.skyblocker.utils.Utils;
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.utils.Dimension;
+import dev.isxander.yacl3.gui.AbstractWidget;
+import dev.isxander.yacl3.gui.YACLScreen;
+import dev.isxander.yacl3.gui.controllers.string.IStringController;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public record DurationController(Option<Integer> option) implements IStringController<Integer> {
+
+ private static final Pattern secondsPattern = Pattern.compile("(^|\\s)(\\d+)s(\\s|$)");
+ private static final Pattern minutesPattern = Pattern.compile("(^|\\s)(\\d+)m(\\s|$)");
+ private static final Pattern hoursPattern = Pattern.compile("(^|\\s)(\\d+)h(\\s|$)");
+
+ @Override
+ public String getString() {
+ return Utils.getDurationText(option.pendingValue()).getString();
+ }
+
+
+ @Override
+ public void setFromString(String value) {
+ Matcher hoursMatcher = hoursPattern.matcher(value);
+ Matcher minutesMatcher = minutesPattern.matcher(value);
+ Matcher secondsMatcher = secondsPattern.matcher(value);
+
+ int result = 0;
+ if (hoursMatcher.find()) {
+ result += Integer.parseInt(hoursMatcher.group(2)) * 3600;
+ }
+ if (minutesMatcher.find()) {
+ result += Integer.parseInt(minutesMatcher.group(2)) * 60;
+ }
+ if (secondsMatcher.find()) {
+ result += Integer.parseInt(secondsMatcher.group(2));
+ }
+ option.requestSet(result);
+ }
+
+
+ @Override
+ public boolean isInputValid(String s) {
+ Matcher hoursMatcher = hoursPattern.matcher(s);
+ Matcher minutesMatcher = minutesPattern.matcher(s);
+ Matcher secondsMatcher = secondsPattern.matcher(s);
+
+ int hoursCount = 0;
+ while (hoursMatcher.find()) hoursCount++;
+ int minutesCount = 0;
+ while (minutesMatcher.find()) minutesCount++;
+ int secondsCount = 0;
+ while (secondsMatcher.find()) secondsCount++;
+
+ if (hoursCount == 0 && minutesCount == 0 && secondsCount == 0) return false;
+ if (hoursCount > 1 || minutesCount > 1 || secondsCount > 1) return false;
+ s = s.replaceAll(hoursPattern.pattern(), "");
+ s = s.replaceAll(minutesPattern.pattern(), "");
+ s = s.replaceAll(secondsPattern.pattern(), "");
+ return s.isBlank();
+ }
+
+ @Override
+ public AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) {
+ return new DurationControllerWidget(this, screen, widgetDimension);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/config/DurationControllerWidget.java b/src/main/java/de/hysky/skyblocker/utils/config/DurationControllerWidget.java
new file mode 100644
index 00000000..f25cd088
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/config/DurationControllerWidget.java
@@ -0,0 +1,38 @@
+package de.hysky.skyblocker.utils.config;
+
+import dev.isxander.yacl3.api.utils.Dimension;
+import dev.isxander.yacl3.gui.YACLScreen;
+import dev.isxander.yacl3.gui.controllers.string.IStringController;
+import dev.isxander.yacl3.gui.controllers.string.StringControllerElement;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import java.util.function.Consumer;
+
+public class DurationControllerWidget extends StringControllerElement {
+
+ public DurationControllerWidget(IStringController<?> control, YACLScreen screen, Dimension<Integer> dim) {
+ super(control, screen, dim, false);
+ }
+
+ @Override
+ public void unfocus() {
+ if (control.isInputValid(inputField)) super.unfocus();
+ else modifyInput(stringBuilder -> stringBuilder.replace(0, stringBuilder.length(), control.getString()));
+ }
+
+ @Override
+ public boolean modifyInput(Consumer<StringBuilder> consumer) {
+ StringBuilder temp = new StringBuilder(inputField);
+ consumer.accept(temp);
+ inputField = temp.toString();
+ return true;
+ }
+
+ @Override
+ protected Text getValueText() {
+ Text valueText = super.getValueText();
+ boolean inputValid = control.isInputValid(valueText.getString());
+ return valueText.copy().formatted(inputValid ? Formatting.WHITE: Formatting.RED);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/datafixer/ItemStackComponentizationFixer.java b/src/main/java/de/hysky/skyblocker/utils/datafixer/ItemStackComponentizationFixer.java
index 3543a2f1..a9b227a1 100644
--- a/src/main/java/de/hysky/skyblocker/utils/datafixer/ItemStackComponentizationFixer.java
+++ b/src/main/java/de/hysky/skyblocker/utils/datafixer/ItemStackComponentizationFixer.java
@@ -1,25 +1,26 @@
package de.hysky.skyblocker.utils.datafixer;
import java.util.Arrays;
-import java.util.List;
import java.util.Objects;
import java.util.Optional;
import com.mojang.brigadier.StringReader;
import com.mojang.serialization.Dynamic;
+import net.minecraft.client.MinecraftClient;
import net.minecraft.command.argument.ItemStringReader;
import net.minecraft.command.argument.ItemStringReader.ItemResult;
-import net.minecraft.component.DataComponentType;
+import net.minecraft.component.ComponentType;
import net.minecraft.datafixer.Schemas;
import net.minecraft.datafixer.TypeReferences;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.nbt.NbtElement;
import net.minecraft.nbt.NbtOps;
-import net.minecraft.registry.DynamicRegistryManager;
+import net.minecraft.registry.BuiltinRegistries;
import net.minecraft.registry.Registries;
import net.minecraft.registry.RegistryOps;
+import net.minecraft.registry.RegistryWrapper.WrapperLookup;
import net.minecraft.util.Identifier;
/**
@@ -30,10 +31,10 @@ import net.minecraft.util.Identifier;
public class ItemStackComponentizationFixer {
private static final int ITEM_NBT_DATA_VERSION = 3817;
private static final int ITEM_COMPONENTS_DATA_VERSION = 3825;
- private static final DynamicRegistryManager REGISTRY_MANAGER = new DynamicRegistryManager.ImmutableImpl(List.of(Registries.ITEM, Registries.DATA_COMPONENT_TYPE));
+ private static final WrapperLookup LOOKUP = BuiltinRegistries.createWrapperLookup();
public static ItemStack fixUpItem(NbtCompound nbt) {
- Dynamic<NbtElement> dynamic = Schemas.getFixer().update(TypeReferences.ITEM_STACK, new Dynamic<>(NbtOps.INSTANCE, nbt), ITEM_NBT_DATA_VERSION, ITEM_COMPONENTS_DATA_VERSION);
+ Dynamic<NbtElement> dynamic = Schemas.getFixer().update(TypeReferences.ITEM_STACK, new Dynamic<>(getRegistryLookup().getOps(NbtOps.INSTANCE), nbt), ITEM_NBT_DATA_VERSION, ITEM_COMPONENTS_DATA_VERSION);
return ItemStack.CODEC.parse(dynamic).getOrThrow();
}
@@ -44,11 +45,11 @@ public class ItemStackComponentizationFixer {
* @return The {@link ItemStack}'s components as a string which is in the format that the {@code /give} command accepts.
*/
public static String componentsAsString(ItemStack stack) {
- RegistryOps<NbtElement> nbtRegistryOps = REGISTRY_MANAGER.getOps(NbtOps.INSTANCE);
+ RegistryOps<NbtElement> nbtRegistryOps = getRegistryLookup().getOps(NbtOps.INSTANCE);
return Arrays.toString(stack.getComponentChanges().entrySet().stream().map(entry -> {
@SuppressWarnings("unchecked")
- DataComponentType<Object> dataComponentType = (DataComponentType<Object>) entry.getKey();
+ ComponentType<Object> dataComponentType = (ComponentType<Object>) entry.getKey();
Identifier componentId = Registries.DATA_COMPONENT_TYPE.getId(dataComponentType);
Optional<NbtElement> encodedComponent = dataComponentType.getCodec().encodeStart(nbtRegistryOps, entry.getValue().orElseThrow()).result();
@@ -66,17 +67,26 @@ public class ItemStackComponentizationFixer {
* @return an {@link ItemStack} or {@link ItemStack#EMPTY} if there was an exception thrown.
*/
public static ItemStack fromComponentsString(String itemId, int count, String componentsString) {
- ItemStringReader reader = new ItemStringReader(REGISTRY_MANAGER);
+ ItemStringReader reader = new ItemStringReader(getRegistryLookup());
try {
ItemResult result = reader.consume(new StringReader(itemId + componentsString));
ItemStack stack = new ItemStack(result.item(), count);
- stack.applyComponentsFrom(result.components());
+ //Vanilla skips validation with /give so we will too
+ stack.applyUnvalidatedChanges(result.components());
return stack;
} catch (Exception ignored) {}
return ItemStack.EMPTY;
}
+
+ /**
+ * Tries to get the dynamic registry manager instance currently in use or else returns {@link #LOOKUP}
+ */
+ public static WrapperLookup getRegistryLookup() {
+ MinecraftClient client = MinecraftClient.getInstance();
+ return client != null && client.getNetworkHandler() != null && client.getNetworkHandler().getRegistryManager() != null ? client.getNetworkHandler().getRegistryManager() : LOOKUP;
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java
index a6772fb2..a5b9bf6b 100644
--- a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java
+++ b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java
@@ -18,6 +18,7 @@ import net.minecraft.client.render.*;
import net.minecraft.client.render.VertexFormat.DrawMode;
import net.minecraft.client.texture.Scaling;
import net.minecraft.client.texture.Sprite;
+import net.minecraft.client.util.BufferAllocator;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.sound.SoundEvents;
import net.minecraft.text.OrderedText;
@@ -25,6 +26,7 @@ import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Box;
+import net.minecraft.util.math.ColorHelper;
import net.minecraft.util.math.Vec3d;
import org.joml.Matrix3f;
@@ -40,11 +42,12 @@ import java.lang.invoke.MethodType;
public class RenderHelper {
private static final Logger LOGGER = LogUtils.getLogger();
- private static final Identifier TRANSLUCENT_DRAW = new Identifier(SkyblockerMod.NAMESPACE, "translucent_draw");
+ private static final Identifier TRANSLUCENT_DRAW = Identifier.of(SkyblockerMod.NAMESPACE, "translucent_draw");
private static final MethodHandle SCHEDULE_DEFERRED_RENDER_TASK = getDeferredRenderTaskHandle();
private static final Vec3d ONE = new Vec3d(1, 1, 1);
private static final int MAX_OVERWORLD_BUILD_HEIGHT = 319;
- private static final MinecraftClient client = MinecraftClient.getInstance();
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+ private static final BufferAllocator ALLOCATOR = new BufferAllocator(1536);
public static void init() {
WorldRenderEvents.AFTER_TRANSLUCENT.addPhaseOrdering(Event.DEFAULT_PHASE, TRANSLUCENT_DRAW);
@@ -95,7 +98,7 @@ public class RenderHelper {
matrices.push();
matrices.translate(pos.getX() - camera.getX(), pos.getY() - camera.getY(), pos.getZ() - camera.getZ());
- BeaconBlockEntityRendererInvoker.renderBeam(matrices, context.consumers(), context.tickDelta(), context.world().getTime(), 0, MAX_OVERWORLD_BUILD_HEIGHT, colorComponents);
+ BeaconBlockEntityRendererInvoker.renderBeam(matrices, context.consumers(), context.tickCounter().getTickDelta(true), context.world().getTime(), 0, MAX_OVERWORLD_BUILD_HEIGHT, ColorHelper.Argb.fromFloats(1f, colorComponents[0], colorComponents[1], colorComponents[2]));
matrices.pop();
}
@@ -110,7 +113,6 @@ public class RenderHelper {
MatrixStack matrices = context.matrixStack();
Vec3d camera = context.camera().getPos();
Tessellator tessellator = RenderSystem.renderThreadTesselator();
- BufferBuilder buffer = tessellator.getBuffer();
RenderSystem.setShader(GameRenderer::getRenderTypeLinesProgram);
RenderSystem.setShaderColor(1f, 1f, 1f, 1f);
@@ -122,9 +124,9 @@ public class RenderHelper {
matrices.push();
matrices.translate(-camera.getX(), -camera.getY(), -camera.getZ());
- buffer.begin(DrawMode.LINES, VertexFormats.LINES);
+ BufferBuilder buffer = tessellator.begin(DrawMode.LINES, VertexFormats.LINES);
WorldRenderer.drawBox(matrices, buffer, box, colorComponents[0], colorComponents[1], colorComponents[2], 1f);
- tessellator.draw();
+ BufferRenderer.drawWithGlobalProgram(buffer.end());
matrices.pop();
RenderSystem.lineWidth(1f);
@@ -156,7 +158,6 @@ public class RenderHelper {
matrices.translate(-camera.x, -camera.y, -camera.z);
Tessellator tessellator = RenderSystem.renderThreadTesselator();
- BufferBuilder buffer = tessellator.getBuffer();
Matrix4f positionMatrix = matrices.peek().getPositionMatrix();
Matrix3f normalMatrix = matrices.peek().getNormalMatrix();
@@ -172,7 +173,7 @@ public class RenderHelper {
RenderSystem.enableDepthTest();
RenderSystem.depthFunc(throughWalls ? GL11.GL_ALWAYS : GL11.GL_LEQUAL);
- buffer.begin(DrawMode.LINE_STRIP, VertexFormats.LINES);
+ BufferBuilder buffer = tessellator.begin(DrawMode.LINE_STRIP, VertexFormats.LINES);
for (int i = 0; i < points.length; i++) {
Vec3d nextPoint = points[i + 1 == points.length ? i - 1 : i + 1];
@@ -180,11 +181,10 @@ public class RenderHelper {
buffer
.vertex(positionMatrix, (float) points[i].getX(), (float) points[i].getY(), (float) points[i].getZ())
.color(colorComponents[0], colorComponents[1], colorComponents[2], alpha)
- .normal(normalVec.x, normalVec.y, normalVec.z)
- .next();
+ .normal(normalVec.x, normalVec.y, normalVec.z);
}
- tessellator.draw();
+ BufferRenderer.drawWithGlobalProgram(buffer.end());
matrices.pop();
GL11.glDisable(GL11.GL_LINE_SMOOTH);
@@ -201,7 +201,6 @@ public class RenderHelper {
matrices.translate(-camera.x, -camera.y, -camera.z);
Tessellator tessellator = RenderSystem.renderThreadTesselator();
- BufferBuilder buffer = tessellator.getBuffer();
Matrix4f positionMatrix = matrices.peek().getPositionMatrix();
GL11.glEnable(GL11.GL_LINE_SMOOTH);
@@ -219,22 +218,21 @@ public class RenderHelper {
Vec3d offset = Vec3d.fromPolar(context.camera().getPitch(), context.camera().getYaw());
Vec3d cameraPoint = camera.add(offset);
- buffer.begin(DrawMode.LINES, VertexFormats.LINES);
+ BufferBuilder buffer = tessellator.begin(DrawMode.LINES, VertexFormats.LINES);
+
Vector3f normal = new Vector3f((float) offset.x, (float) offset.y, (float) offset.z);
buffer
.vertex(positionMatrix, (float) cameraPoint.x , (float) cameraPoint.y, (float) cameraPoint.z)
.color(colorComponents[0], colorComponents[1], colorComponents[2], alpha)
- .normal(normal.x, normal.y, normal.z)
- .next();
+ .normal(normal.x, normal.y, normal.z);
buffer
.vertex(positionMatrix, (float) point.getX(), (float) point.getY(), (float) point.getZ())
.color(colorComponents[0], colorComponents[1], colorComponents[2], alpha)
- .normal(normal.x, normal.y, normal.z)
- .next();
+ .normal(normal.x, normal.y, normal.z);
- tessellator.draw();
+ BufferRenderer.drawWithGlobalProgram(buffer.end());
matrices.pop();
GL11.glDisable(GL11.GL_LINE_SMOOTH);
@@ -250,7 +248,6 @@ public class RenderHelper {
positionMatrix.translate((float) -camera.x, (float) -camera.y, (float) -camera.z);
Tessellator tessellator = RenderSystem.renderThreadTesselator();
- BufferBuilder buffer = tessellator.getBuffer();
RenderSystem.setShader(GameRenderer::getPositionColorProgram);
RenderSystem.setShaderColor(1f, 1f, 1f, 1f);
@@ -259,11 +256,11 @@ public class RenderHelper {
RenderSystem.disableCull();
RenderSystem.depthFunc(throughWalls ? GL11.GL_ALWAYS : GL11.GL_LEQUAL);
- buffer.begin(DrawMode.QUADS, VertexFormats.POSITION_COLOR);
+ BufferBuilder buffer = tessellator.begin(DrawMode.QUADS, VertexFormats.POSITION_COLOR);
for (int i = 0; i < 4; i++) {
- buffer.vertex(positionMatrix, (float) points[i].getX(), (float) points[i].getY(), (float) points[i].getZ()).color(colorComponents[0], colorComponents[1], colorComponents[2], alpha).next();
+ buffer.vertex(positionMatrix, (float) points[i].getX(), (float) points[i].getY(), (float) points[i].getZ()).color(colorComponents[0], colorComponents[1], colorComponents[2], alpha);
}
- tessellator.draw();
+ BufferRenderer.drawWithGlobalProgram(buffer.end());
RenderSystem.enableCull();
RenderSystem.depthFunc(GL11.GL_LEQUAL);
@@ -290,20 +287,18 @@ public class RenderHelper {
Matrix4f positionMatrix = new Matrix4f();
Camera camera = context.camera();
Vec3d cameraPos = camera.getPos();
- TextRenderer textRenderer = client.textRenderer;
+ TextRenderer textRenderer = CLIENT.textRenderer;
scale *= 0.025f;
positionMatrix
.translate((float) (pos.getX() - cameraPos.getX()), (float) (pos.getY() - cameraPos.getY()), (float) (pos.getZ() - cameraPos.getZ()))
.rotate(camera.getRotation())
- .scale(-scale, -scale, scale);
+ .scale(scale, -scale, scale);
float xOffset = -textRenderer.getWidth(text) / 2f;
- Tessellator tessellator = RenderSystem.renderThreadTesselator();
- BufferBuilder buffer = tessellator.getBuffer();
- VertexConsumerProvider.Immediate consumers = VertexConsumerProvider.immediate(buffer);
+ VertexConsumerProvider.Immediate consumers = VertexConsumerProvider.immediate(ALLOCATOR);
RenderSystem.depthFunc(throughWalls ? GL11.GL_ALWAYS : GL11.GL_LEQUAL);
@@ -361,8 +356,8 @@ public class RenderHelper {
}
private static void playNotificationSound() {
- if (client.player != null) {
- client.player.playSound(SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP, 100f, 0.1f);
+ if (CLIENT.player != null) {
+ CLIENT.player.playSound(SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP, 100f, 0.1f);
}
}
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractContainerMatcher.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractContainerMatcher.java
new file mode 100644
index 00000000..bf255218
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractContainerMatcher.java
@@ -0,0 +1,29 @@
+package de.hysky.skyblocker.utils.render.gui;
+
+import de.hysky.skyblocker.skyblock.ChestValue;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.regex.Pattern;
+
+public abstract class AbstractContainerMatcher {
+ /**
+ * The title of the screen must match this pattern for this to be applied. Null means it will be applied to all screens.
+ * @implNote Don't end your regex with a {@code $} as {@link ChestValue} appends text to the end of the title,
+ * so the regex will stop matching if the player uses chest value.
+ */
+ @Nullable
+ public final Pattern titlePattern;
+
+ protected AbstractContainerMatcher() {
+ this((Pattern) null);
+ }
+
+ protected AbstractContainerMatcher(@NotNull String titlePattern) {
+ this(Pattern.compile(titlePattern));
+ }
+
+ protected AbstractContainerMatcher(@Nullable Pattern titlePattern) {
+ this.titlePattern = titlePattern;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractPopupScreen.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractPopupScreen.java
index e7a3e8b2..903611ae 100644
--- a/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractPopupScreen.java
+++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractPopupScreen.java
@@ -12,13 +12,11 @@ import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
-import java.util.function.Consumer;
-
/**
* A more bare-bones version of Vanilla's Popup Screen. Meant to be extended.
*/
public class AbstractPopupScreen extends Screen {
- private static final Identifier BACKGROUND_TEXTURE = new Identifier("popup/background");
+ private static final Identifier BACKGROUND_TEXTURE = Identifier.ofVanilla("popup/background");
private final Screen backgroundScreen;
protected AbstractPopupScreen(Text title, Screen backgroundScreen) {
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java
index 0417dc3c..81c9ebec 100644
--- a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java
+++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java
@@ -11,17 +11,15 @@ import java.util.regex.Pattern;
/**
* Abstract class for gui solvers. Extend this class to add a new gui solver, like terminal solvers or experiment solvers.
*/
-public abstract class ContainerSolver {
- private final Pattern containerName;
-
- protected ContainerSolver(String containerName) {
- this.containerName = Pattern.compile(containerName);
+public abstract class ContainerSolver extends AbstractContainerMatcher {
+ protected ContainerSolver(String titlePattern) {
+ super(titlePattern);
}
protected abstract boolean isEnabled();
public final Pattern getName() {
- return containerName;
+ return titlePattern;
}
protected void start(GenericContainerScreen screen) {
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/SideTabButtonWidget.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/SideTabButtonWidget.java
new file mode 100644
index 00000000..889d3b02
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/SideTabButtonWidget.java
@@ -0,0 +1,39 @@
+package de.hysky.skyblocker.utils.render.gui;
+
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ButtonTextures;
+import net.minecraft.client.gui.widget.ToggleButtonWidget;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.NotNull;
+
+public class SideTabButtonWidget extends ToggleButtonWidget {
+ private static final ButtonTextures TEXTURES = new ButtonTextures(Identifier.ofVanilla("recipe_book/tab"), Identifier.ofVanilla("recipe_book/tab_selected"));
+ protected @NotNull ItemStack icon;
+
+ public void setIcon(@NotNull ItemStack icon) {
+ this.icon = icon.copy();
+ }
+
+ public SideTabButtonWidget(int x, int y, boolean toggled, @NotNull ItemStack icon) {
+ super(x, y, 35, 27, toggled);
+ this.icon = icon.copy();
+ setTextures(TEXTURES);
+ }
+
+ @Override
+ public void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
+ if (textures == null) return;
+ Identifier identifier = textures.get(true, this.toggled);
+ int x = getX();
+ if (toggled) x -= 2;
+ context.drawGuiTexture(identifier, x, this.getY(), this.width, this.height);
+ context.drawItem(icon, x + 9, getY() + 5);
+ }
+
+ @Override
+ public void onClick(double mouseX, double mouseY) {
+ super.onClick(mouseX, mouseY);
+ if (!isToggled()) this.setToggled(true);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainer.java b/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainer.java
index c21485e2..cb754308 100644
--- a/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainer.java
+++ b/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainer.java
@@ -9,6 +9,7 @@ import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallba
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.render.RenderTickCounter;
import net.minecraft.util.math.MathHelper;
import java.util.LinkedHashSet;
@@ -81,8 +82,8 @@ public class TitleContainer {
titles.remove(title);
}
- private static void render(DrawContext context, float tickDelta) {
- render(context, titles, SkyblockerConfigManager.get().uiAndVisuals.titleContainer.x, SkyblockerConfigManager.get().uiAndVisuals.titleContainer.y, tickDelta);
+ private static void render(DrawContext context, RenderTickCounter tickCounter) {
+ render(context, titles, SkyblockerConfigManager.get().uiAndVisuals.titleContainer.x, SkyblockerConfigManager.get().uiAndVisuals.titleContainer.y, tickCounter.getTickDelta(true));
}
protected static void render(DrawContext context, Set<Title> titles, int xPos, int yPos, float tickDelta) {
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/java/de/hysky/skyblocker/utils/waypoint/NamedWaypoint.java b/src/main/java/de/hysky/skyblocker/utils/waypoint/NamedWaypoint.java
new file mode 100644
index 00000000..2f02b51f
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/waypoint/NamedWaypoint.java
@@ -0,0 +1,128 @@
+package de.hysky.skyblocker.utils.waypoint;
+
+import com.google.common.primitives.Floats;
+import com.mojang.datafixers.util.Either;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.DataResult;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.text.Text;
+import net.minecraft.text.TextCodecs;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Vec3d;
+
+import java.util.Objects;
+import java.util.function.Supplier;
+
+public class NamedWaypoint extends Waypoint {
+ public static final Codec<NamedWaypoint> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ BlockPos.CODEC.fieldOf("pos").forGetter(secretWaypoint -> secretWaypoint.pos),
+ TextCodecs.CODEC.fieldOf("name").forGetter(secretWaypoint -> secretWaypoint.name),
+ Codec.floatRange(0, 1).listOf().comapFlatMap(
+ colorComponentsList -> colorComponentsList.size() == 3 ? DataResult.success(Floats.toArray(colorComponentsList)) : DataResult.error(() -> "Expected 3 color components, got " + colorComponentsList.size() + " instead"),
+ Floats::asList
+ ).fieldOf("colorComponents").forGetter(secretWaypoint -> secretWaypoint.colorComponents),
+ Codec.FLOAT.fieldOf("alpha").forGetter(secretWaypoint -> secretWaypoint.alpha),
+ Codec.BOOL.fieldOf("shouldRender").forGetter(Waypoint::shouldRender)
+ ).apply(instance, NamedWaypoint::new));
+ public static final Codec<NamedWaypoint> SKYTILS_CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codec.INT.fieldOf("x").forGetter(waypoint -> waypoint.pos.getX()),
+ Codec.INT.fieldOf("y").forGetter(waypoint -> waypoint.pos.getY()),
+ Codec.INT.fieldOf("z").forGetter(waypoint -> waypoint.pos.getZ()),
+ Codec.either(Codec.STRING, Codec.INT).xmap(either -> either.map(str -> str, Object::toString), Either::left).fieldOf("name").forGetter(waypoint -> waypoint.name.getString()),
+ Codec.INT.fieldOf("color").forGetter(waypoint -> (int) (waypoint.alpha * 255) << 24 | (int) (waypoint.colorComponents[0] * 255) << 16 | (int) (waypoint.colorComponents[1] * 255) << 8 | (int) (waypoint.colorComponents[2] * 255)),
+ Codec.BOOL.fieldOf("enabled").forGetter(Waypoint::shouldRender)
+ ).apply(instance, NamedWaypoint::fromSkytils));
+ public final Text name;
+ public final Vec3d centerPos;
+
+ public NamedWaypoint(BlockPos pos, String name, float[] colorComponents) {
+ this(pos, name, colorComponents, true);
+ }
+
+ public NamedWaypoint(BlockPos pos, String name, float[] colorComponents, boolean shouldRender) {
+ this(pos, name, colorComponents, DEFAULT_HIGHLIGHT_ALPHA, shouldRender);
+ }
+
+ public NamedWaypoint(BlockPos pos, String name, float[] colorComponents, float alpha, boolean shouldRender) {
+ this(pos, Text.of(name), colorComponents, alpha, shouldRender);
+ }
+
+ public NamedWaypoint(BlockPos pos, Text name, float[] colorComponents, float alpha, boolean shouldRender) {
+ this(pos, name, () -> SkyblockerConfigManager.get().uiAndVisuals.waypoints.waypointType, colorComponents, alpha, shouldRender);
+ }
+
+ public NamedWaypoint(BlockPos pos, Text name, Supplier<Type> typeSupplier, float[] colorComponents) {
+ this(pos, name, typeSupplier, colorComponents, DEFAULT_HIGHLIGHT_ALPHA, true);
+ }
+
+ public NamedWaypoint(BlockPos pos, Text name, Supplier<Type> typeSupplier, float[] colorComponents, float alpha, boolean shouldRender) {
+ super(pos, typeSupplier, colorComponents, alpha, DEFAULT_LINE_WIDTH, true, shouldRender);
+ this.name = name;
+ this.centerPos = pos.toCenterPos();
+ }
+
+ public static NamedWaypoint fromSkytils(int x, int y, int z, String name, int color, boolean enabled) {
+ float alpha = ((color & 0xFF000000) >>> 24) / 255f;
+ if (alpha == 0) {
+ alpha = DEFAULT_HIGHLIGHT_ALPHA;
+ }
+ return new NamedWaypoint(new BlockPos(x, y, z), name, new float[]{((color & 0x00FF0000) >> 16) / 255f, ((color & 0x0000FF00) >> 8) / 255f, (color & 0x000000FF) / 255f}, alpha, enabled);
+ }
+
+ public NamedWaypoint copy() {
+ return new NamedWaypoint(pos, name, typeSupplier, getColorComponents(), alpha, shouldRender());
+ }
+
+ @Override
+ public NamedWaypoint withX(int x) {
+ return new NamedWaypoint(new BlockPos(x, pos.getY(), pos.getZ()), name, typeSupplier, getColorComponents(), alpha, shouldRender());
+ }
+
+ @Override
+ public NamedWaypoint withY(int y) {
+ return new NamedWaypoint(pos.withY(y), name, typeSupplier, getColorComponents(), alpha, shouldRender());
+ }
+
+ @Override
+ public NamedWaypoint withZ(int z) {
+ return new NamedWaypoint(new BlockPos(pos.getX(), pos.getY(), z), name, typeSupplier, getColorComponents(), alpha, shouldRender());
+ }
+
+ @Override
+ public NamedWaypoint withColor(float[] colorComponents, float alpha) {
+ return new NamedWaypoint(pos, name, typeSupplier, colorComponents, alpha, shouldRender());
+ }
+
+ public Text getName() {
+ return name;
+ }
+
+ public NamedWaypoint withName(String name) {
+ return new NamedWaypoint(pos, Text.literal(name), typeSupplier, getColorComponents(), alpha, shouldRender());
+ }
+
+ protected boolean shouldRenderName() {
+ return true;
+ }
+
+ @Override
+ public void render(WorldRenderContext context) {
+ super.render(context);
+ if (shouldRenderName()) {
+ RenderHelper.renderText(context, name, centerPos.add(0, 1, 0), true);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), name);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return this == obj || super.equals(obj) && obj instanceof NamedWaypoint waypoint && name.equals(waypoint.name);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/waypoint/ProfileAwareWaypoint.java b/src/main/java/de/hysky/skyblocker/utils/waypoint/ProfileAwareWaypoint.java
index 7aa99d14..7369a2ef 100644
--- a/src/main/java/de/hysky/skyblocker/utils/waypoint/ProfileAwareWaypoint.java
+++ b/src/main/java/de/hysky/skyblocker/utils/waypoint/ProfileAwareWaypoint.java
@@ -38,7 +38,7 @@ public class ProfileAwareWaypoint extends Waypoint {
}
@Override
- protected float[] getColorComponents() {
+ public float[] getColorComponents() {
return foundProfiles.contains(Utils.getProfile()) ? foundColor : missingColor;
}
}
diff --git a/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java b/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java
index 622e1658..c991fb9c 100644
--- a/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java
+++ b/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java
@@ -3,9 +3,12 @@ package de.hysky.skyblocker.utils.waypoint;
import de.hysky.skyblocker.utils.render.RenderHelper;
import de.hysky.skyblocker.utils.render.Renderable;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.util.StringIdentifiable;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Box;
+import java.util.Arrays;
+import java.util.Objects;
import java.util.function.Supplier;
public class Waypoint implements Renderable {
@@ -15,9 +18,9 @@ public class Waypoint implements Renderable {
final Box box;
final Supplier<Type> typeSupplier;
protected final float[] colorComponents;
- final float alpha;
- final float lineWidth;
- final boolean throughWalls;
+ public final float alpha;
+ public final float lineWidth;
+ public final boolean throughWalls;
private boolean shouldRender;
public Waypoint(BlockPos pos, Type type, float[] colorComponents) {
@@ -55,6 +58,22 @@ public class Waypoint implements Renderable {
this.shouldRender = shouldRender;
}
+ public Waypoint withX(int x) {
+ return new Waypoint(new BlockPos(x, pos.getY(), pos.getZ()), typeSupplier, getColorComponents(), alpha, lineWidth, throughWalls, shouldRender());
+ }
+
+ public Waypoint withY(int y) {
+ return new Waypoint(pos.withY(y), typeSupplier, getColorComponents(), alpha, lineWidth, throughWalls, shouldRender());
+ }
+
+ public Waypoint withZ(int z) {
+ return new Waypoint(new BlockPos(pos.getX(), pos.getY(), z), typeSupplier, getColorComponents(), alpha, lineWidth, throughWalls, shouldRender());
+ }
+
+ public Waypoint withColor(float[] colorComponents, float alpha) {
+ return new Waypoint(pos, typeSupplier, colorComponents, alpha, lineWidth, throughWalls, shouldRender());
+ }
+
public boolean shouldRender() {
return shouldRender;
}
@@ -71,7 +90,11 @@ public class Waypoint implements Renderable {
this.shouldRender = !this.shouldRender;
}
- protected float[] getColorComponents() {
+ public void setShouldRender(boolean shouldRender) {
+ this.shouldRender = shouldRender;
+ }
+
+ public float[] getColorComponents() {
return colorComponents;
}
@@ -94,7 +117,17 @@ public class Waypoint implements Renderable {
}
}
- public enum Type {
+ @Override
+ public int hashCode() {
+ return Objects.hash(pos, typeSupplier.get(), Arrays.hashCode(colorComponents), alpha, lineWidth, throughWalls, shouldRender);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return super.equals(obj) || obj instanceof Waypoint other && pos.equals(other.pos) && typeSupplier.get() == other.typeSupplier.get() && Arrays.equals(colorComponents, other.colorComponents) && alpha == other.alpha && lineWidth == other.lineWidth && throughWalls == other.throughWalls && shouldRender == other.shouldRender;
+ }
+
+ public enum Type implements StringIdentifiable {
WAYPOINT,
OUTLINED_WAYPOINT,
HIGHLIGHT,
@@ -102,6 +135,11 @@ public class Waypoint implements Renderable {
OUTLINE;
@Override
+ public String asString() {
+ return name().toLowerCase();
+ }
+
+ @Override
public String toString() {
return switch (this) {
case WAYPOINT -> "Waypoint";
diff --git a/src/main/java/de/hysky/skyblocker/utils/waypoint/WaypointCategory.java b/src/main/java/de/hysky/skyblocker/utils/waypoint/WaypointCategory.java
new file mode 100644
index 00000000..db2a6d82
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/waypoint/WaypointCategory.java
@@ -0,0 +1,43 @@
+package de.hysky.skyblocker.utils.waypoint;
+
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
+
+public record WaypointCategory(String name, String island, List<NamedWaypoint> waypoints) {
+ public static final Codec<WaypointCategory> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codec.STRING.fieldOf("name").forGetter(WaypointCategory::name),
+ Codec.STRING.fieldOf("island").forGetter(WaypointCategory::island),
+ NamedWaypoint.CODEC.listOf().fieldOf("waypoints").forGetter(WaypointCategory::waypoints)
+ ).apply(instance, WaypointCategory::new));
+ public static final Codec<WaypointCategory> SKYTILS_CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codec.STRING.fieldOf("name").forGetter(WaypointCategory::name),
+ Codec.STRING.fieldOf("island").forGetter(WaypointCategory::island),
+ NamedWaypoint.SKYTILS_CODEC.listOf().fieldOf("waypoints").forGetter(WaypointCategory::waypoints)
+ ).apply(instance, WaypointCategory::new));
+
+ public static UnaryOperator<WaypointCategory> filter(Predicate<NamedWaypoint> predicate) {
+ return waypointCategory -> new WaypointCategory(waypointCategory.name(), waypointCategory.island(), waypointCategory.waypoints().stream().filter(predicate).toList());
+ }
+
+ public WaypointCategory withName(String name) {
+ return new WaypointCategory(name, island(), waypoints());
+ }
+
+ public WaypointCategory deepCopy() {
+ return new WaypointCategory(name(), island(), waypoints().stream().map(NamedWaypoint::copy).collect(Collectors.toList()));
+ }
+
+ public void render(WorldRenderContext context) {
+ for (NamedWaypoint waypoint : waypoints) {
+ if (waypoint.shouldRender()) {
+ waypoint.render(context);
+ }
+ }
+ }
+}