aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/de')
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerMod.java130
-rw-r--r--src/main/java/de/hysky/skyblocker/compatibility/MixinPlugin.java52
-rw-r--r--src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockEmiRecipe.java38
-rw-r--r--src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockerEMIPlugin.java29
-rw-r--r--src/main/java/de/hysky/skyblocker/compatibility/modmenu/ModMenuEntry.java15
-rw-r--r--src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockCategory.java84
-rw-r--r--src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockCraftingDisplay.java40
-rw-r--r--src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockCraftingDisplayGenerator.java65
-rw-r--r--src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockerREIClientPlugin.java34
-rw-r--r--src/main/java/de/hysky/skyblocker/config/ConfigUtils.java25
-rw-r--r--src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java787
-rw-r--r--src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java86
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/DiscordRPCCategory.java49
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java316
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/DwarvenMinesCategory.java94
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java508
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/LocationsCategory.java80
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java98
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/QuickNavigationCategory.java605
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/SlayersCategory.java116
-rw-r--r--src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownController.java93
-rw-r--r--src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerBuilder.java27
-rw-r--r--src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerBuilderImpl.java27
-rw-r--r--src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerElement.java26
-rw-r--r--src/main/java/de/hysky/skyblocker/events/ClientPlayerBlockBreakEvent.java23
-rw-r--r--src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java33
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/AbstractInventoryScreenMixin.java19
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/ArmorTrimMixin.java37
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/BatEntityMixin.java21
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java48
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/ClientPlayerEntityMixin.java35
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/ClientPlayerInteractionManagerMixin.java27
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/DrawContextMixin.java72
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/DyeableItemMixin.java27
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/FarmlandBlockMixin.java38
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/GenericContainerScreenHandlerMixin.java30
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/HandledScreenMixin.java193
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/InGameHudMixin.java93
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/InventoryScreenMixin.java18
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/ItemMixin.java22
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/ItemStackMixin.java61
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/LeverBlockMixin.java29
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/MinecraftClientMixin.java25
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/PlayerListHudMixin.java57
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/PlayerSkinProviderMixin.java29
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/ScoreboardMixin.java16
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/SocialInteractionsPlayerListWidgetMixin.java24
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/WorldRendererMixin.java33
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/YggdrasilMinecraftSessionServiceMixin.java20
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/YggdrasilServicesKeyInfoMixin.java59
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/accessor/BeaconBlockEntityRendererInvoker.java16
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/accessor/DrawContextInvoker.java17
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/accessor/FrustumInvoker.java15
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/accessor/HandledScreenAccessor.java20
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/accessor/PlayerListHudAccessor.java17
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/accessor/RecipeBookWidgetAccessor.java14
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/accessor/ScreenAccessor.java14
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/accessor/WorldRendererAccessor.java13
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/FairySouls.java215
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/FancyStatusBars.java192
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/FishingHelper.java62
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/HotbarSlotLock.java40
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/QuiverWarning.java66
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/StatusBarTracker.java109
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java114
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/barn/HungryHiker.java47
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/barn/TreasureHunter.java61
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusHelper.java34
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java152
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonChestProfit.java169
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java61
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMapConfigScreen.java62
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/LividColor.java42
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/OldLever.java40
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/Reparty.java94
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/StarredMobGlow.java56
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdos.java39
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java136
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/Trivia.java100
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java275
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java451
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java473
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java142
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/ColorTerminal.java72
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/OrderTerminal.java58
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/StartsWithTerminal.java35
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dwarven/DwarvenHud.java144
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dwarven/DwarvenHudConfigScreen.java66
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dwarven/Fetchur.java53
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dwarven/Puzzler.java39
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/experiment/ChronomatronSolver.java129
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/experiment/ExperimentSolver.java60
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/experiment/SuperpairsSolver.java81
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/experiment/UltrasequencerSolver.java80
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/filters/AbilityFilter.java15
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/filters/AdFilter.java39
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/filters/AoteFilter.java15
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/filters/AutopetFilter.java35
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/filters/ComboFilter.java16
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/filters/HealFilter.java15
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/filters/ImplosionFilter.java15
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/filters/MoltenWaveFilter.java15
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/filters/ShowOffFilter.java18
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/filters/SimpleChatFilter.java17
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/filters/TeleportPadFilter.java16
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java59
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/BackpackPreview.java235
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/CompactorDeletorPreview.java92
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/CompactorPreviewTooltipComponent.java54
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorDyeColors.java82
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java154
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/CustomItemNames.java74
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java115
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/ItemProtection.java75
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/ItemRarityBackgrounds.java109
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/PriceInfoTooltip.java443
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockItemRarity.java29
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/WikiLookup.java56
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemFixerUpper.java341
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java102
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRegistry.java137
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java154
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ResultButtonWidget.java65
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java228
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/SkyblockCraftingRecipe.java60
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNav.java80
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNavButton.java107
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/rift/EffigyWaypoints.java71
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/rift/HealingMelonIndicator.java27
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/rift/ManiaIndicator.java42
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/rift/MirrorverseWaypoints.java88
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/rift/StakeIndicator.java27
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/rift/TheRift.java22
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/rift/TwinClawsIndicator.java43
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java208
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigListWidget.java232
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigScreen.java113
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/special/SpecialEffects.java96
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/spidersden/Relics.java171
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/TabHud.java39
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/ScreenBuilder.java179
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/ScreenMaster.java144
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/AlignStage.java83
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/CollideStage.java153
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/PipelineStage.java14
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/PlaceStage.java94
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/StackStage.java114
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/Ico.java60
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/PlayerListMgr.java171
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/PlayerLocator.java87
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/ScreenConst.java13
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/CameraPositionWidget.java37
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/CommsWidget.java63
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ComposterWidget.java30
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/CookieWidget.java50
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonBuffWidget.java68
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDeathWidget.java47
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDownedWidget.java44
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonPlayerWidget.java103
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonPuzzleWidget.java57
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonSecretWidget.java26
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonServerWidget.java48
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/EffectWidget.java67
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ElectionWidget.java104
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ErrorWidget.java32
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/EssenceWidget.java47
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/EventWidget.java35
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/FireSaleWidget.java68
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ForgeWidget.java81
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenServerWidget.java54
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenSkillsWidget.java80
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenVisitorsWidget.java30
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GuestServerWidget.java30
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandGuestsWidget.java47
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandOwnersWidget.java66
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandSelfWidget.java43
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandServerWidget.java32
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/JacobsContestWidget.java62
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/MinionWidget.java151
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ParkServerWidget.java30
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/PlayerListWidget.java71
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/PowderWidget.java29
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ProfileWidget.java28
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/QuestWidget.java33
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ReputationWidget.java69
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ServerWidget.java30
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/SkillsWidget.java78
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/TrapperWidget.java25
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/UpgradeWidget.java51
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/VolcanoWidget.java59
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/Widget.java216
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/Component.java31
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/IcoFatTextComponent.java45
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/IcoTextComponent.java40
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/PlainTextComponent.java30
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/PlayerComponent.java39
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/ProgressComponent.java69
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/TableComponent.java58
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/hud/HudCommsWidget.java73
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/AdvertisementWidget.java35
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/GoodToKnowWidget.java69
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftProfileWidget.java21
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftProgressWidget.java123
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftServerInfoWidget.java27
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftStatsWidget.java43
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/ShenWidget.java22
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Boxes.java50
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Constants.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Http.java89
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ItemUtils.java111
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/NEURepo.java101
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/PosUtils.java14
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/SlayerUtils.java54
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Utils.java370
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/chat/ChatFilterResult.java18
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java89
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/chat/ChatPatternListener.java30
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/discord/DiscordRPCManager.java122
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/FrustumUtils.java21
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java247
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCulling.java47
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/culling/WorldProvider.java28
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/culling/package-info.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/gui/ColorHighlight.java24
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java44
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java125
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/title/Title.java53
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainer.java175
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainerConfigScreen.java170
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/scheduler/MessageScheduler.java66
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java140
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/tictactoe/TicTacToeUtils.java104
232 files changed, 19396 insertions, 0 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
new file mode 100644
index 00000000..2cf46706
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
@@ -0,0 +1,130 @@
+package de.hysky.skyblocker;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import de.hysky.skyblocker.skyblock.*;
+import de.hysky.skyblocker.skyblock.dungeon.*;
+import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonSecrets;
+import de.hysky.skyblocker.skyblock.item.*;
+import de.hysky.skyblocker.skyblock.tabhud.screenbuilder.ScreenMaster;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.*;
+import de.hysky.skyblocker.skyblock.dungeon.*;
+import de.hysky.skyblocker.skyblock.dwarven.DwarvenHud;
+import de.hysky.skyblocker.skyblock.item.*;
+import de.hysky.skyblocker.skyblock.itemlist.ItemRegistry;
+import de.hysky.skyblocker.skyblock.quicknav.QuickNav;
+import de.hysky.skyblocker.skyblock.rift.TheRift;
+import de.hysky.skyblocker.skyblock.shortcut.Shortcuts;
+import de.hysky.skyblocker.skyblock.special.SpecialEffects;
+import de.hysky.skyblocker.skyblock.spidersden.Relics;
+import de.hysky.skyblocker.skyblock.tabhud.TabHud;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.utils.NEURepo;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.chat.ChatMessageListener;
+import de.hysky.skyblocker.utils.discord.DiscordRPCManager;
+import de.hysky.skyblocker.utils.render.culling.OcclusionCulling;
+import de.hysky.skyblocker.utils.render.gui.ContainerSolverManager;
+import de.hysky.skyblocker.utils.render.title.TitleContainer;
+import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.api.ClientModInitializer;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
+import net.fabricmc.loader.api.FabricLoader;
+import net.minecraft.client.MinecraftClient;
+
+import java.nio.file.Path;
+
+/**
+ * Main class for Skyblocker which initializes features, registers events, and
+ * manages ticks. This class will be instantiated by Fabric. Do not instantiate
+ * this class.
+ */
+public class SkyblockerMod implements ClientModInitializer {
+ public static final String VERSION = FabricLoader.getInstance().getModContainer("skyblocker").get().getMetadata().getVersion().getFriendlyString();
+ public static final String NAMESPACE = "skyblocker";
+ public static final Path CONFIG_DIR = FabricLoader.getInstance().getConfigDir().resolve(NAMESPACE);
+ public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
+ private static SkyblockerMod INSTANCE;
+ public final ContainerSolverManager containerSolverManager = new ContainerSolverManager();
+ public final StatusBarTracker statusBarTracker = new StatusBarTracker();
+
+ /**
+ * Do not instantiate this class. Use {@link #getInstance()} instead.
+ */
+ @Deprecated
+ public SkyblockerMod() {
+ INSTANCE = this;
+ }
+
+ public static SkyblockerMod getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Register {@link #tick(MinecraftClient)} to
+ * {@link ClientTickEvents#END_CLIENT_TICK}, initialize all features, and
+ * schedule tick events.
+ */
+ @Override
+ public void onInitializeClient() {
+ ClientTickEvents.END_CLIENT_TICK.register(this::tick);
+ Utils.init();
+ HotbarSlotLock.init();
+ SkyblockerConfigManager.init();
+ PriceInfoTooltip.init();
+ WikiLookup.init();
+ ItemRegistry.init();
+ NEURepo.init();
+ FairySouls.init();
+ Relics.init();
+ BackpackPreview.init();
+ QuickNav.init();
+ ItemCooldowns.init();
+ DwarvenHud.init();
+ ChatMessageListener.init();
+ Shortcuts.init();
+ DiscordRPCManager.init();
+ LividColor.init();
+ FishingHelper.init();
+ TabHud.init();
+ DungeonMap.init();
+ DungeonSecrets.init();
+ DungeonBlaze.init();
+ DungeonChestProfit.init();
+ TheRift.init();
+ TitleContainer.init();
+ ScreenMaster.init();
+ OcclusionCulling.init();
+ TeleportOverlay.init();
+ CustomItemNames.init();
+ CustomArmorDyeColors.init();
+ CustomArmorTrims.init();
+ TicTacToe.init();
+ QuiverWarning.init();
+ SpecialEffects.init();
+ ItemProtection.init();
+ ItemRarityBackgrounds.init();
+ containerSolverManager.init();
+ statusBarTracker.init();
+ Scheduler.INSTANCE.scheduleCyclic(Utils::update, 20);
+ Scheduler.INSTANCE.scheduleCyclic(DiscordRPCManager::updateDataAndPresence, 100);
+ Scheduler.INSTANCE.scheduleCyclic(TicTacToe::tick, 4);
+ Scheduler.INSTANCE.scheduleCyclic(LividColor::update, 10);
+ Scheduler.INSTANCE.scheduleCyclic(BackpackPreview::tick, 50);
+ Scheduler.INSTANCE.scheduleCyclic(DwarvenHud::update, 40);
+ Scheduler.INSTANCE.scheduleCyclic(PlayerListMgr::updateList, 20);
+ }
+
+ /**
+ * Ticks the scheduler. Called once at the end of every client tick through
+ * {@link ClientTickEvents#END_CLIENT_TICK}.
+ *
+ * @param client the Minecraft client.
+ */
+ public void tick(MinecraftClient client) {
+ Scheduler.INSTANCE.tick();
+ MessageScheduler.INSTANCE.tick();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/compatibility/MixinPlugin.java b/src/main/java/de/hysky/skyblocker/compatibility/MixinPlugin.java
new file mode 100644
index 00000000..c7fc6973
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/compatibility/MixinPlugin.java
@@ -0,0 +1,52 @@
+package de.hysky.skyblocker.compatibility;
+
+import java.util.List;
+import java.util.Set;
+
+import org.objectweb.asm.tree.ClassNode;
+import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
+import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
+
+import net.fabricmc.loader.api.FabricLoader;
+
+public class MixinPlugin implements IMixinConfigPlugin {
+ private static final boolean OPTIFABRIC_LOADED = FabricLoader.getInstance().isModLoaded("optifabric");
+
+ @Override
+ public void onLoad(String mixinPackage) {
+ //Do nothing
+ }
+
+ @Override
+ public String getRefMapperConfig() {
+ return null;
+ }
+
+ @Override
+ public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
+ //OptiFabric Compatibility
+ if (mixinClassName.endsWith("WorldRendererMixin") && OPTIFABRIC_LOADED) return false;
+
+ return true;
+ }
+
+ @Override
+ public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
+ //Do nothing
+ }
+
+ @Override
+ public List<String> getMixins() {
+ return null;
+ }
+
+ @Override
+ public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
+ //Do nothing
+ }
+
+ @Override
+ public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
+ //Do nothing
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockEmiRecipe.java b/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockEmiRecipe.java
new file mode 100644
index 00000000..5875327d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockEmiRecipe.java
@@ -0,0 +1,38 @@
+package de.hysky.skyblocker.compatibility.emi;
+
+import de.hysky.skyblocker.skyblock.itemlist.ItemRegistry;
+import de.hysky.skyblocker.skyblock.itemlist.SkyblockCraftingRecipe;
+import dev.emi.emi.api.recipe.EmiCraftingRecipe;
+import dev.emi.emi.api.recipe.EmiRecipeCategory;
+import dev.emi.emi.api.stack.Comparison;
+import dev.emi.emi.api.stack.EmiIngredient;
+import dev.emi.emi.api.stack.EmiStack;
+import dev.emi.emi.api.widget.WidgetHolder;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+
+public class SkyblockEmiRecipe extends EmiCraftingRecipe {
+ private final String craftText;
+
+ public SkyblockEmiRecipe(SkyblockCraftingRecipe recipe) {
+ super(recipe.getGrid().stream().map(EmiStack::of).map(EmiIngredient.class::cast).toList(), EmiStack.of(recipe.getResult()).comparison(Comparison.compareNbt()), Identifier.of("skyblock", ItemRegistry.getInternalName(recipe.getResult()).toLowerCase().replace(';', '_')));
+ this.craftText = recipe.getCraftText();
+ }
+
+ @Override
+ public EmiRecipeCategory getCategory() {
+ return SkyblockerEMIPlugin.SKYBLOCK;
+ }
+
+ @Override
+ public int getDisplayHeight() {
+ return super.getDisplayHeight() + (craftText.isEmpty() ? 0 : 10);
+ }
+
+ @Override
+ public void addWidgets(WidgetHolder widgets) {
+ super.addWidgets(widgets);
+ widgets.addText(Text.of(craftText), 59 - MinecraftClient.getInstance().textRenderer.getWidth(craftText) / 2, 55, 0xFFFFFF, true);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockerEMIPlugin.java b/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockerEMIPlugin.java
new file mode 100644
index 00000000..c6147016
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockerEMIPlugin.java
@@ -0,0 +1,29 @@
+package de.hysky.skyblocker.compatibility.emi;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.skyblock.itemlist.ItemRegistry;
+import de.hysky.skyblocker.utils.ItemUtils;
+import dev.emi.emi.api.EmiPlugin;
+import dev.emi.emi.api.EmiRegistry;
+import dev.emi.emi.api.recipe.EmiRecipeCategory;
+import dev.emi.emi.api.render.EmiTexture;
+import dev.emi.emi.api.stack.EmiStack;
+import net.minecraft.item.Items;
+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");
+ // 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));
+
+ @Override
+ public void register(EmiRegistry registry) {
+ ItemRegistry.getItemsStream().map(EmiStack::of).forEach(registry::addEmiStack);
+ registry.addCategory(SKYBLOCK);
+ registry.addWorkstation(SKYBLOCK, EmiStack.of(Items.CRAFTING_TABLE));
+ ItemRegistry.getRecipesStream().map(SkyblockEmiRecipe::new).forEach(registry::addRecipe);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/compatibility/modmenu/ModMenuEntry.java b/src/main/java/de/hysky/skyblocker/compatibility/modmenu/ModMenuEntry.java
new file mode 100644
index 00000000..e0b0bc2f
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/compatibility/modmenu/ModMenuEntry.java
@@ -0,0 +1,15 @@
+package de.hysky.skyblocker.compatibility.modmenu;
+
+import com.terraformersmc.modmenu.api.ConfigScreenFactory;
+import com.terraformersmc.modmenu.api.ModMenuApi;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+
+@Environment(EnvType.CLIENT)
+public class ModMenuEntry implements ModMenuApi {
+ @Override
+ public ConfigScreenFactory<?> getModConfigScreenFactory() {
+ return SkyblockerConfigManager::createGUI;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockCategory.java b/src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockCategory.java
new file mode 100644
index 00000000..dfc6e871
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockCategory.java
@@ -0,0 +1,84 @@
+package de.hysky.skyblocker.compatibility.rei;
+
+import com.google.common.collect.Lists;
+import de.hysky.skyblocker.utils.ItemUtils;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
+import me.shedaniel.rei.api.client.gui.Renderer;
+import me.shedaniel.rei.api.client.gui.widgets.Label;
+import me.shedaniel.rei.api.client.gui.widgets.Slot;
+import me.shedaniel.rei.api.client.gui.widgets.Widget;
+import me.shedaniel.rei.api.client.gui.widgets.Widgets;
+import me.shedaniel.rei.api.client.registry.display.DisplayCategory;
+import me.shedaniel.rei.api.common.category.CategoryIdentifier;
+import me.shedaniel.rei.api.common.entry.EntryIngredient;
+import me.shedaniel.rei.api.common.util.EntryStacks;
+import net.minecraft.text.Text;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Skyblock recipe category class for REI
+ */
+public class SkyblockCategory implements DisplayCategory<SkyblockCraftingDisplay> {
+ @Override
+ public CategoryIdentifier<SkyblockCraftingDisplay> getCategoryIdentifier() {
+ return SkyblockerREIClientPlugin.SKYBLOCK;
+ }
+
+ @Override
+ public Text getTitle() {
+ return Text.translatable("emi.category.skyblocker.skyblock");
+ }
+
+ @Override
+ public Renderer getIcon() {
+ return EntryStacks.of(ItemUtils.getSkyblockerStack());
+ }
+
+ @Override
+ public int getDisplayHeight() {
+ return 73;
+ }
+
+ /**
+ * Draws display for SkyblockCraftingDisplay
+ *
+ * @param display the display
+ * @param bounds the bounds of the display, configurable with overriding the width, height methods.
+ */
+ @Override
+ public List<Widget> setupDisplay(SkyblockCraftingDisplay display, Rectangle bounds) {
+ List<Widget> out = new ArrayList<>();
+ out.add(Widgets.createRecipeBase(bounds));
+
+ Point startPoint;
+ if (!display.getCraftText().isEmpty() && display.getCraftText() != null) {
+ startPoint = new Point(bounds.getCenterX() - 58, bounds.getCenterY() - 31);
+ }
+ else {
+ startPoint = new Point(bounds.getCenterX() - 58, bounds.getCenterY() - 26);
+ }
+ Point resultPoint = new Point(startPoint.x + 95, startPoint.y + 19);
+ out.add(Widgets.createArrow(new Point(startPoint.x + 60, startPoint.y + 18)));
+ out.add(Widgets.createResultSlotBackground(resultPoint));
+
+ // Generate Slots
+ List<EntryIngredient> input = display.getInputEntries();
+ List<Slot> slots = Lists.newArrayList();
+ for (int y = 0; y < 3; y++)
+ for (int x = 0; x < 3; x++)
+ slots.add(Widgets.createSlot(new Point(startPoint.x + 1 + x * 18, startPoint.y + 1 + y * 18)).markInput());
+ for (int i = 0; i < input.size(); i++) {
+ slots.get(i).entries(input.get(i)).markInput();
+ }
+ out.addAll(slots);
+ out.add(Widgets.createSlot(resultPoint).entries(display.getOutputEntries().get(0)).disableBackground().markOutput());
+
+ // Add craftingText Label
+ Label craftTextLabel = Widgets.createLabel(new Point(bounds.getCenterX(), startPoint.y + 55), Text.of(display.getCraftText()));
+ out.add(craftTextLabel);
+ return out;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockCraftingDisplay.java b/src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockCraftingDisplay.java
new file mode 100644
index 00000000..7cd712f2
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockCraftingDisplay.java
@@ -0,0 +1,40 @@
+package de.hysky.skyblocker.compatibility.rei;
+
+
+import me.shedaniel.rei.api.common.category.CategoryIdentifier;
+import me.shedaniel.rei.api.common.display.SimpleGridMenuDisplay;
+import me.shedaniel.rei.api.common.display.basic.BasicDisplay;
+import me.shedaniel.rei.api.common.entry.EntryIngredient;
+
+import java.util.List;
+
+/**
+ * Skyblock Crafting Recipe display class for REI
+ */
+public class SkyblockCraftingDisplay extends BasicDisplay implements SimpleGridMenuDisplay {
+ private final String craftText;
+
+ public SkyblockCraftingDisplay(List<EntryIngredient> input, List<EntryIngredient> output, String craftText) {
+ super(input, output);
+ this.craftText = craftText;
+ }
+
+ public String getCraftText() {
+ return craftText;
+ }
+
+ @Override
+ public int getWidth() {
+ return 3;
+ }
+
+ @Override
+ public int getHeight() {
+ return 3;
+ }
+
+ @Override
+ public CategoryIdentifier<?> getCategoryIdentifier() {
+ return SkyblockerREIClientPlugin.SKYBLOCK;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockCraftingDisplayGenerator.java b/src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockCraftingDisplayGenerator.java
new file mode 100644
index 00000000..8db617dc
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockCraftingDisplayGenerator.java
@@ -0,0 +1,65 @@
+package de.hysky.skyblocker.compatibility.rei;
+
+import de.hysky.skyblocker.skyblock.itemlist.ItemRegistry;
+import de.hysky.skyblocker.skyblock.itemlist.SkyblockCraftingRecipe;
+import me.shedaniel.rei.api.client.registry.display.DynamicDisplayGenerator;
+import me.shedaniel.rei.api.common.entry.EntryIngredient;
+import me.shedaniel.rei.api.common.entry.EntryStack;
+import me.shedaniel.rei.api.common.util.EntryStacks;
+import net.minecraft.item.ItemStack;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+public class SkyblockCraftingDisplayGenerator implements DynamicDisplayGenerator<SkyblockCraftingDisplay> {
+
+ @Override
+ public Optional<List<SkyblockCraftingDisplay>> getRecipeFor(EntryStack<?> entry) {
+ if (!(entry.getValue() instanceof ItemStack)) return Optional.empty();
+ EntryStack<ItemStack> inputItem = EntryStacks.of((ItemStack) entry.getValue());
+ List<SkyblockCraftingRecipe> filteredRecipes = ItemRegistry.getRecipesStream()
+ .filter(recipe -> ItemRegistry.getInternalName(recipe.getResult()).equals(ItemRegistry.getInternalName(inputItem.getValue())))
+ .toList();
+
+ return Optional.of(generateDisplays(filteredRecipes));
+ }
+
+ @Override
+ public Optional<List<SkyblockCraftingDisplay>> getUsageFor(EntryStack<?> entry) {
+ if (!(entry.getValue() instanceof ItemStack)) return Optional.empty();
+ EntryStack<ItemStack> inputItem = EntryStacks.of((ItemStack) entry.getValue());
+ List<SkyblockCraftingRecipe> filteredRecipes = ItemRegistry.getRecipesStream()
+ .filter(recipe -> {
+ for (ItemStack item : recipe.getGrid()) {
+ if(!ItemRegistry.getInternalName(item).isEmpty() && ItemRegistry.getInternalName(item).equals(ItemRegistry.getInternalName(inputItem.getValue())))
+ return true;
+ }
+ return false;
+ })
+ .toList();
+ return Optional.of(generateDisplays(filteredRecipes));
+ }
+
+ /**
+ * Generate Displays from a list of recipes
+ */
+ private List<SkyblockCraftingDisplay> generateDisplays(List<SkyblockCraftingRecipe> recipes) {
+ List<SkyblockCraftingDisplay> displays = new ArrayList<>();
+ for (SkyblockCraftingRecipe recipe : recipes) {
+ List<EntryIngredient> inputs = new ArrayList<>();
+ List<EntryIngredient> outputs = new ArrayList<>();
+
+ ArrayList<EntryStack<ItemStack>> inputEntryStacks = new ArrayList<>();
+ recipe.getGrid().forEach((item) -> inputEntryStacks.add(EntryStacks.of(item)));
+
+ for (EntryStack<ItemStack> entryStack : inputEntryStacks) {
+ inputs.add(EntryIngredient.of(entryStack));
+ }
+ outputs.add(EntryIngredient.of(EntryStacks.of(recipe.getResult())));
+
+ displays.add(new SkyblockCraftingDisplay(inputs, outputs, recipe.getCraftText()));
+ }
+ return displays;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockerREIClientPlugin.java b/src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockerREIClientPlugin.java
new file mode 100644
index 00000000..97651718
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/compatibility/rei/SkyblockerREIClientPlugin.java
@@ -0,0 +1,34 @@
+package de.hysky.skyblocker.compatibility.rei;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.skyblock.itemlist.ItemRegistry;
+import me.shedaniel.rei.api.client.plugins.REIClientPlugin;
+import me.shedaniel.rei.api.client.registry.category.CategoryRegistry;
+import me.shedaniel.rei.api.client.registry.display.DisplayRegistry;
+import me.shedaniel.rei.api.client.registry.entry.EntryRegistry;
+import me.shedaniel.rei.api.common.category.CategoryIdentifier;
+import me.shedaniel.rei.api.common.util.EntryStacks;
+import net.minecraft.item.Items;
+
+/**
+ * REI integration
+ */
+public class SkyblockerREIClientPlugin implements REIClientPlugin {
+ public static final CategoryIdentifier<SkyblockCraftingDisplay> SKYBLOCK = CategoryIdentifier.of(SkyblockerMod.NAMESPACE, "skyblock");
+
+ @Override
+ public void registerCategories(CategoryRegistry categoryRegistry) {
+ categoryRegistry.addWorkstations(SKYBLOCK, EntryStacks.of(Items.CRAFTING_TABLE));
+ categoryRegistry.add(new SkyblockCategory());
+ }
+
+ @Override
+ public void registerDisplays(DisplayRegistry displayRegistry) {
+ displayRegistry.registerDisplayGenerator(SKYBLOCK, new SkyblockCraftingDisplayGenerator());
+ }
+
+ @Override
+ public void registerEntries(EntryRegistry entryRegistry) {
+ entryRegistry.addEntries(ItemRegistry.getItemsStream().map(EntryStacks::of).toList());
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java b/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java
new file mode 100644
index 00000000..9a7a41b5
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java
@@ -0,0 +1,25 @@
+package de.hysky.skyblocker.config;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.controller.BooleanControllerBuilder;
+import dev.isxander.yacl3.api.controller.EnumControllerBuilder;
+import dev.isxander.yacl3.api.controller.ValueFormatter;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.function.Function;
+
+public class ConfigUtils {
+ public static final Function<Formatting, String> FORMATTING_TO_STRING = formatting -> StringUtils.capitalize(formatting.getName().replaceAll("_", " "));
+ public static final ValueFormatter<Float> FLOAT_TWO_FORMATTER = value -> Text.literal(String.format("%,.2f", value).replaceAll("[\u00a0\u202F]", " "));
+
+ public static BooleanControllerBuilder createBooleanController(Option<Boolean> opt) {
+ return BooleanControllerBuilder.create(opt).yesNoFormatter().coloured(true);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <E extends Enum<E>> EnumControllerBuilder<E> createEnumCyclingListController(Option<E> opt) {
+ return EnumControllerBuilder.create(opt).enumClass((Class<E>) opt.binding().defaultValue().getClass());
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
new file mode 100644
index 00000000..cb51afdc
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
@@ -0,0 +1,787 @@
+package de.hysky.skyblocker.config;
+
+import dev.isxander.yacl3.config.v2.api.SerialEntry;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+import de.hysky.skyblocker.skyblock.item.CustomArmorTrims;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SkyblockerConfig {
+ @SerialEntry
+ public int version = 1;
+
+ @SerialEntry
+ public General general = new General();
+
+ @SerialEntry
+ public Locations locations = new Locations();
+
+ @SerialEntry
+ public Slayer slayer = new Slayer();
+
+ @SerialEntry
+ public QuickNav quickNav = new QuickNav();
+
+ @SerialEntry
+ public Messages messages = new Messages();
+
+ @SerialEntry
+ public RichPresence richPresence = new RichPresence();
+
+ public static class QuickNav {
+ @SerialEntry
+ public boolean enableQuickNav = true;
+
+ @SerialEntry
+ public QuickNavItem button1 = new QuickNavItem(true, new ItemData("diamond_sword"), "Your Skills", "/skills");
+
+ @SerialEntry
+ public QuickNavItem button2 = new QuickNavItem(true, new ItemData("painting"), "Collections", "/collection");
+
+ /* REGEX Explanation
+ * "Pets" : simple match on letters
+ * "(?: \\(\\d+\\/\\d+\\))?" : optional match on the non-capturing group for the page in the format " ($number/$number)"
+ */
+ @SerialEntry
+ public QuickNavItem button3 = new QuickNavItem(true, new ItemData("bone"), "Pets(:? \\(\\d+\\/\\d+\\))?", "/pets");
+
+ /* REGEX Explanation
+ * "Wardrobe" : simple match on letters
+ * " \\([12]\\/2\\)" : match on the page either " (1/2)" or " (2/2)"
+ */
+ @SerialEntry
+ public QuickNavItem button4 = new QuickNavItem(true,
+ new ItemData("leather_chestplate", 1, "tag:{display:{color:8991416}}"), "Wardrobe \\([12]/2\\)",
+ "/wardrobe");
+
+ @SerialEntry
+ public QuickNavItem button5 = new QuickNavItem(true, new ItemData("player_head", 1,
+ "tag:{SkullOwner:{Id:[I;-2081424676,-57521078,-2073572414,158072763],Properties:{textures:[{Value:\"ewogICJ0aW1lc3RhbXAiIDogMTU5MTMxMDU4NTYwOSwKICAicHJvZmlsZUlkIiA6ICI0MWQzYWJjMmQ3NDk0MDBjOTA5MGQ1NDM0ZDAzODMxYiIsCiAgInByb2ZpbGVOYW1lIiA6ICJNZWdha2xvb24iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODBhMDc3ZTI0OGQxNDI3NzJlYTgwMDg2NGY4YzU3OGI5ZDM2ODg1YjI5ZGFmODM2YjY0YTcwNjg4MmI2ZWMxMCIKICAgIH0KICB9Cn0=\"}]}}}"),
+ "Sack of Sacks", "/sacks");
+
+ /* REGEX Explanation
+ * "(?:Rift )?" : optional match on the non-capturing group "Rift "
+ * "Storage" : simple match on letters
+ * "(?: \\([12]\\/2\\))?" : optional match on the non-capturing group " (1/2)" or " (2/2)"
+ */
+ @SerialEntry
+ public QuickNavItem button6 = new QuickNavItem(true, new ItemData("ender_chest"),
+ "(?:Rift )?Storage(?: \\(1/2\\))?", "/storage");
+
+ @SerialEntry
+ public QuickNavItem button7 = new QuickNavItem(true, new ItemData("player_head", 1,
+ "tag:{SkullOwner:{Id:[I;-300151517,-631415889,-1193921967,-1821784279],Properties:{textures:[{Value:\"e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDdjYzY2ODc0MjNkMDU3MGQ1NTZhYzUzZTA2NzZjYjU2M2JiZGQ5NzE3Y2Q4MjY5YmRlYmVkNmY2ZDRlN2JmOCJ9fX0=\"}]}}}"),
+ "none", "/hub");
+
+ @SerialEntry
+ public QuickNavItem button8 = new QuickNavItem(true, new ItemData("player_head", 1,
+ "tag:{SkullOwner:{Id:[I;1605800870,415127827,-1236127084,15358548],Properties:{textures:[{Value:\"e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzg5MWQ1YjI3M2ZmMGJjNTBjOTYwYjJjZDg2ZWVmMWM0MGExYjk0MDMyYWU3MWU3NTQ3NWE1NjhhODI1NzQyMSJ9fX0=\"}]}}}"),
+ "none", "/warp dungeon_hub");
+
+ @SerialEntry
+ public QuickNavItem button9 = new QuickNavItem(true, new ItemData("player_head", 1,
+ "tag:{SkullOwner:{Id:[I;-562285948,532499670,-1705302742,775653035],Properties:{textures:[{Value:\"eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjVkZjU1NTkyNjQzMGQ1ZDc1YWRlZDIxZGQ5NjE5Yjc2YzViN2NhMmM3ZjU0MDE0NDA1MjNkNTNhOGJjZmFhYiJ9fX0=\"}]}}}"),
+ "Visit prtl", "/visit prtl");
+
+ @SerialEntry
+ public QuickNavItem button10 = new QuickNavItem(true, new ItemData("enchanting_table"), "Enchant Item",
+ "/etable");
+
+ @SerialEntry
+ public QuickNavItem button11 = new QuickNavItem(true, new ItemData("anvil"), "Anvil", "/anvil");
+
+ @SerialEntry
+ public QuickNavItem button12 = new QuickNavItem(true, new ItemData("crafting_table"), "Craft Item", "/craft");
+ }
+
+ public static class QuickNavItem {
+ public QuickNavItem(Boolean render, ItemData itemData, String uiTitle, String clickEvent) {
+ this.render = render;
+ this.item = itemData;
+ this.clickEvent = clickEvent;
+ this.uiTitle = uiTitle;
+ }
+
+ @SerialEntry
+ public Boolean render;
+
+ @SerialEntry
+ public ItemData item;
+
+ @SerialEntry
+ public String uiTitle;
+
+ @SerialEntry
+ public String clickEvent;
+ }
+
+ public static class ItemData {
+ public ItemData(String itemName, int count, String nbt) {
+ this.itemName = itemName;
+ this.count = count;
+ this.nbt = nbt;
+ }
+
+ public ItemData(String itemName) {
+ this.itemName = itemName;
+ this.count = 1;
+ this.nbt = "";
+ }
+
+ @SerialEntry
+ public String itemName;
+
+ @SerialEntry
+ public int count;
+
+ @SerialEntry
+ public String nbt;
+ }
+
+ public static class General {
+ @SerialEntry
+ public boolean acceptReparty = true;
+
+ @SerialEntry
+ public boolean backpackPreviewWithoutShift = false;
+
+ @SerialEntry
+ public boolean compactorDeletorPreview = true;
+
+ @SerialEntry
+ public boolean hideEmptyTooltips = true;
+
+ @SerialEntry
+ public boolean hideStatusEffectOverlay = false;
+
+ @SerialEntry
+ public TabHudConf tabHud = new TabHudConf();
+
+ @SerialEntry
+ public Bars bars = new Bars();
+
+ @SerialEntry
+ public Experiments experiments = new Experiments();
+
+ @SerialEntry
+ public Fishing fishing = new Fishing();
+
+ @SerialEntry
+ public FairySouls fairySouls = new FairySouls();
+
+ @SerialEntry
+ public ItemCooldown itemCooldown = new ItemCooldown();
+
+ @SerialEntry
+ public Shortcuts shortcuts = new Shortcuts();
+
+ @SerialEntry
+ public QuiverWarning quiverWarning = new QuiverWarning();
+
+ @SerialEntry
+ public ItemList itemList = new ItemList();
+
+ @SerialEntry
+ public ItemTooltip itemTooltip = new ItemTooltip();
+
+ @SerialEntry
+ public ItemInfoDisplay itemInfoDisplay = new ItemInfoDisplay();
+
+ @SerialEntry
+ public SpecialEffects specialEffects = new SpecialEffects();
+
+ @SerialEntry
+ public Hitbox hitbox = new Hitbox();
+
+ @SerialEntry
+ public TitleContainer titleContainer = new TitleContainer();
+
+ @SerialEntry
+ public TeleportOverlay teleportOverlay = new TeleportOverlay();
+
+ @SerialEntry
+ public List<Integer> lockedSlots = new ArrayList<>();
+
+ @SerialEntry
+ public ObjectOpenHashSet<String> protectedItems = new ObjectOpenHashSet<>();
+
+ @SerialEntry
+ public Object2ObjectOpenHashMap<String, Text> customItemNames = new Object2ObjectOpenHashMap<>();
+
+ @SerialEntry
+ public Object2IntOpenHashMap<String> customDyeColors = new Object2IntOpenHashMap<>();
+
+ @SerialEntry
+ public Object2ObjectOpenHashMap<String, CustomArmorTrims.ArmorTrimId> customArmorTrims = new Object2ObjectOpenHashMap<>();
+ }
+
+ public static class TabHudConf {
+ @SerialEntry
+ public boolean tabHudEnabled = true;
+
+ @SerialEntry
+ public int tabHudScale = 100;
+
+ @SerialEntry
+ public boolean plainPlayerNames = false;
+
+ @SerialEntry
+ public NameSorting nameSorting = NameSorting.DEFAULT;
+ }
+
+ public enum NameSorting {
+ DEFAULT, ALPHABETICAL;
+
+ @Override
+ public String toString() {
+ return switch (this) {
+ case DEFAULT -> "Default";
+ case ALPHABETICAL -> "Alphabetical";
+ };
+ }
+ }
+
+ public static class Bars {
+ @SerialEntry
+ public boolean enableBars = true;
+
+ @SerialEntry
+ public BarPositions barPositions = new BarPositions();
+ }
+
+ public static class BarPositions {
+ @SerialEntry
+ public BarPosition healthBarPosition = BarPosition.LAYER1;
+
+ @SerialEntry
+ public BarPosition manaBarPosition = BarPosition.LAYER1;
+
+ @SerialEntry
+ public BarPosition defenceBarPosition = BarPosition.LAYER1;
+
+ @SerialEntry
+ public BarPosition experienceBarPosition = BarPosition.LAYER1;
+
+ }
+
+ public enum BarPosition {
+ LAYER1, LAYER2, RIGHT, NONE;
+
+ @Override
+ public String toString() {
+ return I18n.translate("text.autoconfig.skyblocker.option.general.bars.barpositions." + name());
+ }
+
+ public int toInt() {
+ return switch (this) {
+ case LAYER1 -> 0;
+ case LAYER2 -> 1;
+ case RIGHT -> 2;
+ case NONE -> -1;
+ };
+ }
+ }
+
+ public static class Experiments {
+ @SerialEntry
+ public boolean enableChronomatronSolver = true;
+
+ @SerialEntry
+ public boolean enableSuperpairsSolver = true;
+
+ @SerialEntry
+ public boolean enableUltrasequencerSolver = true;
+ }
+
+ public static class Fishing {
+ @SerialEntry
+ public boolean enableFishingHelper = true;
+ }
+
+ public static class FairySouls {
+ @SerialEntry
+ public boolean enableFairySoulsHelper = false;
+
+ @SerialEntry
+ public boolean highlightFoundSouls = true;
+
+ @SerialEntry
+ public boolean highlightOnlyNearbySouls = false;
+ }
+
+ public static class ItemCooldown {
+ @SerialEntry
+ public boolean enableItemCooldowns = true;
+ }
+
+ public static class Shortcuts {
+ @SerialEntry
+ public boolean enableShortcuts = true;
+
+ @SerialEntry
+ public boolean enableCommandShortcuts = true;
+
+ @SerialEntry
+ public boolean enableCommandArgShortcuts = true;
+ }
+
+ public static class QuiverWarning {
+ @SerialEntry
+ public boolean enableQuiverWarning = true;
+
+ @SerialEntry
+ public boolean enableQuiverWarningInDungeons = true;
+
+ @SerialEntry
+ public boolean enableQuiverWarningAfterDungeon = true;
+ }
+
+ public static class Hitbox {
+ @SerialEntry
+ public boolean oldFarmlandHitbox = true;
+
+ @SerialEntry
+ public boolean oldLeverHitbox = false;
+ }
+
+ public static class TitleContainer {
+ @SerialEntry
+ public float titleContainerScale = 100;
+
+ @SerialEntry
+ public int x = 540;
+
+ @SerialEntry
+ public int y = 10;
+
+ @SerialEntry
+ public Direction direction = Direction.HORIZONTAL;
+
+ @SerialEntry
+ public Alignment alignment = Alignment.MIDDLE;
+ }
+
+ public static class TeleportOverlay {
+ @SerialEntry
+ public boolean enableTeleportOverlays = true;
+
+ @SerialEntry
+ public boolean enableWeirdTransmission = true;
+
+ @SerialEntry
+ public boolean enableInstantTransmission = true;
+
+ @SerialEntry
+ public boolean enableEtherTransmission = true;
+
+ @SerialEntry
+ public boolean enableSinrecallTransmission = true;
+
+ @SerialEntry
+ public boolean enableWitherImpact = true;
+ }
+
+ public enum Direction {
+ HORIZONTAL, VERTICAL;
+
+ @Override
+ public String toString() {
+ return switch (this) {
+ case HORIZONTAL -> "Horizontal";
+ case VERTICAL -> "Vertical";
+ };
+ }
+ }
+
+ public enum Alignment {
+ LEFT, RIGHT, MIDDLE;
+
+ @Override
+ public String toString() {
+ return switch (this) {
+ case LEFT -> "Left";
+ case RIGHT -> "Right";
+ case MIDDLE -> "Middle";
+ };
+ }
+ }
+
+ public static class RichPresence {
+ @SerialEntry
+ public boolean enableRichPresence = false;
+
+ @SerialEntry
+ public Info info = Info.LOCATION;
+
+ @SerialEntry
+ public boolean cycleMode = false;
+
+ @SerialEntry
+ public String customMessage = "Playing Skyblock";
+ }
+
+ public static class ItemList {
+ @SerialEntry
+ public boolean enableItemList = true;
+ }
+
+ public enum Average {
+ ONE_DAY, THREE_DAY, BOTH;
+
+ @Override
+ public String toString() {
+ return I18n.translate("text.autoconfig.skyblocker.option.general.itemTooltip.avg." + name());
+ }
+ }
+
+ public static class ItemTooltip {
+ @SerialEntry
+ public boolean enableNPCPrice = true;
+
+ @SerialEntry
+ public boolean enableMotesPrice = true;
+
+ @SerialEntry
+ public boolean enableAvgBIN = true;
+
+ @SerialEntry
+ public Average avg = Average.THREE_DAY;
+
+ @SerialEntry
+ public boolean enableLowestBIN = true;
+
+ @SerialEntry
+ public boolean enableBazaarPrice = true;
+
+ @SerialEntry
+ public boolean enableMuseumDate = true;
+ }
+
+ public static class ItemInfoDisplay {
+ @SerialEntry
+ public boolean attributeShardInfo = true;
+
+ @SerialEntry
+ public boolean itemRarityBackgrounds = false;
+
+ @SerialEntry
+ public float itemRarityBackgroundsOpacity = 1f;
+ }
+
+ public static class SpecialEffects {
+ @SerialEntry
+ public boolean rareDungeonDropEffects = true;
+ }
+
+ public static class Locations {
+ @SerialEntry
+ public Barn barn = new Barn();
+
+ @SerialEntry
+ public Dungeons dungeons = new Dungeons();
+
+ @SerialEntry
+ public DwarvenMines dwarvenMines = new DwarvenMines();
+
+ @SerialEntry
+ public Rift rift = new Rift();
+
+ @SerialEntry
+ public SpidersDen spidersDen = new SpidersDen();
+ }
+
+ public static class Dungeons {
+ @SerialEntry
+ public SecretWaypoints secretWaypoints = new SecretWaypoints();
+
+ @SerialEntry
+ public DungeonChestProfit dungeonChestProfit = new DungeonChestProfit();
+
+ @SerialEntry
+ public boolean croesusHelper = true;
+
+ @SerialEntry
+ public boolean enableMap = true;
+
+ @SerialEntry
+ public float mapScaling = 1f;
+
+ @SerialEntry
+ public int mapX = 2;
+
+ @SerialEntry
+ public int mapY = 2;
+
+ @SerialEntry
+ public boolean starredMobGlow = true;
+
+ @SerialEntry
+ public boolean solveThreeWeirdos = true;
+
+ @SerialEntry
+ public boolean blazesolver = true;
+
+ @SerialEntry
+ public boolean solveTrivia = true;
+
+ @SerialEntry
+ public boolean solveTicTacToe = true;
+
+ @SerialEntry
+ public LividColor lividColor = new LividColor();
+
+ @SerialEntry
+ public Terminals terminals = new Terminals();
+ }
+
+ public static class SecretWaypoints {
+ @SerialEntry
+ public boolean enableSecretWaypoints = true;
+
+ @SerialEntry
+ public boolean noInitSecretWaypoints = false;
+
+ @SerialEntry
+ public boolean enableEntranceWaypoints = true;
+
+ @SerialEntry
+ public boolean enableSuperboomWaypoints = true;
+
+ @SerialEntry
+ public boolean enableChestWaypoints = true;
+
+ @SerialEntry
+ public boolean enableItemWaypoints = true;
+
+ @SerialEntry
+ public boolean enableBatWaypoints = true;
+
+ @SerialEntry
+ public boolean enableWitherWaypoints = true;
+
+ @SerialEntry
+ public boolean enableLeverWaypoints = true;
+
+ @SerialEntry
+ public boolean enableFairySoulWaypoints = true;
+
+ @SerialEntry
+ public boolean enableStonkWaypoints = true;
+
+ @SerialEntry
+ public boolean enableDefaultWaypoints = true;
+ }
+
+ public static class DungeonChestProfit {
+ @SerialEntry
+ public boolean enableProfitCalculator = true;
+
+ @SerialEntry
+ public boolean includeKismet = false;
+
+ @SerialEntry
+ public boolean includeEssence = true;
+
+ @SerialEntry
+ public int neutralThreshold = 1000;
+
+ @SerialEntry
+ public Formatting neutralColor = Formatting.DARK_GRAY;
+
+ @SerialEntry
+ public Formatting profitColor = Formatting.DARK_GREEN;
+
+ @SerialEntry
+ public Formatting lossColor = Formatting.RED;
+
+ @SerialEntry
+ public Formatting incompleteColor = Formatting.BLUE;
+
+ }
+
+ public static class LividColor {
+ @SerialEntry
+ public boolean enableLividColor = true;
+
+ @SerialEntry
+ public String lividColorText = "The livid color is [color]";
+ }
+
+ public static class Terminals {
+ @SerialEntry
+ public boolean solveColor = true;
+
+ @SerialEntry
+ public boolean solveOrder = true;
+
+ @SerialEntry
+ public boolean solveStartsWith = true;
+ }
+
+ public static class DwarvenMines {
+ @SerialEntry
+ public boolean enableDrillFuel = true;
+
+ @SerialEntry
+ public boolean solveFetchur = true;
+
+ @SerialEntry
+ public boolean solvePuzzler = true;
+
+ @SerialEntry
+ public DwarvenHud dwarvenHud = new DwarvenHud();
+ }
+
+ public static class DwarvenHud {
+ @SerialEntry
+ public boolean enabled = true;
+
+ @SerialEntry
+ public DwarvenHudStyle style = DwarvenHudStyle.SIMPLE;
+
+ @SerialEntry
+ public boolean enableBackground = true;
+
+ @SerialEntry
+ public int x = 10;
+
+ @SerialEntry
+ public int y = 10;
+ }
+
+ public enum DwarvenHudStyle {
+ SIMPLE, FANCY, CLASSIC;
+
+ @Override
+ public String toString() {
+ return switch (this) {
+ case SIMPLE -> "Simple";
+ case FANCY -> "Fancy";
+ case CLASSIC -> "Classic";
+ };
+ }
+ }
+
+ public static class Barn {
+ @SerialEntry
+ public boolean solveHungryHiker = true;
+
+ @SerialEntry
+ public boolean solveTreasureHunter = true;
+ }
+
+ public static class Rift {
+ @SerialEntry
+ public boolean mirrorverseWaypoints = true;
+
+ @SerialEntry
+ public int mcGrubberStacks = 0;
+ }
+
+ public static class SpidersDen {
+ @SerialEntry
+ public Relics relics = new Relics();
+ }
+
+ public static class Relics {
+ @SerialEntry
+ public boolean enableRelicsHelper = false;
+
+ @SerialEntry
+ public boolean highlightFoundRelics = true;
+ }
+
+ public static class Slayer {
+ @SerialEntry
+ public VampireSlayer vampireSlayer = new VampireSlayer();
+ }
+
+ public static class VampireSlayer {
+ @SerialEntry
+ public boolean enableEffigyWaypoints = true;
+
+ @SerialEntry
+ public boolean compactEffigyWaypoints;
+
+ @SerialEntry
+ public int effigyUpdateFrequency = 5;
+
+ @SerialEntry
+ public boolean enableHolyIceIndicator = true;
+
+ @SerialEntry
+ public int holyIceIndicatorTickDelay = 10;
+
+ @SerialEntry
+ public int holyIceUpdateFrequency = 5;
+
+ @SerialEntry
+ public boolean enableHealingMelonIndicator = true;
+
+ @SerialEntry
+ public float healingMelonHealthThreshold = 4f;
+
+ @SerialEntry
+ public boolean enableSteakStakeIndicator = true;
+
+ @SerialEntry
+ public int steakStakeUpdateFrequency = 5;
+
+ @SerialEntry
+ public boolean enableManiaIndicator = true;
+
+ @SerialEntry
+ public int maniaUpdateFrequency = 5;
+ }
+
+ public static class Messages {
+ @SerialEntry
+ public ChatFilterResult hideAbility = ChatFilterResult.PASS;
+
+ @SerialEntry
+ public ChatFilterResult hideHeal = ChatFilterResult.PASS;
+
+ @SerialEntry
+ public ChatFilterResult hideAOTE = ChatFilterResult.PASS;
+
+ @SerialEntry
+ public ChatFilterResult hideImplosion = ChatFilterResult.PASS;
+
+ @SerialEntry
+ public ChatFilterResult hideMoltenWave = ChatFilterResult.PASS;
+
+ @SerialEntry
+ public ChatFilterResult hideAds = ChatFilterResult.PASS;
+
+ @SerialEntry
+ public ChatFilterResult hideTeleportPad = ChatFilterResult.PASS;
+
+ @SerialEntry
+ public ChatFilterResult hideCombo = ChatFilterResult.PASS;
+
+ @SerialEntry
+ public ChatFilterResult hideAutopet = ChatFilterResult.PASS;
+
+ @SerialEntry
+ public ChatFilterResult hideShowOff = ChatFilterResult.PASS;
+
+ @SerialEntry
+ public boolean hideMana = false;
+ }
+
+ public enum Info {
+ PURSE, BITS, LOCATION;
+
+ @Override
+ public String toString() {
+ return I18n.translate("text.autoconfig.skyblocker.option.richPresence.info." + name());
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java
new file mode 100644
index 00000000..98c83975
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java
@@ -0,0 +1,86 @@
+package de.hysky.skyblocker.config;
+
+import java.lang.StackWalker.Option;
+import java.nio.file.Path;
+
+import com.google.gson.FieldNamingPolicy;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import dev.isxander.yacl3.api.YetAnotherConfigLib;
+import dev.isxander.yacl3.config.v2.api.ConfigClassHandler;
+import dev.isxander.yacl3.config.v2.api.serializer.GsonConfigSerializerBuilder;
+import de.hysky.skyblocker.config.categories.DiscordRPCCategory;
+import de.hysky.skyblocker.config.categories.DungeonsCategory;
+import de.hysky.skyblocker.config.categories.DwarvenMinesCategory;
+import de.hysky.skyblocker.config.categories.GeneralCategory;
+import de.hysky.skyblocker.config.categories.LocationsCategory;
+import de.hysky.skyblocker.config.categories.MessageFilterCategory;
+import de.hysky.skyblocker.config.categories.QuickNavigationCategory;
+import de.hysky.skyblocker.config.categories.SlayersCategory;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+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.fabricmc.loader.api.FabricLoader;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+
+public class SkyblockerConfigManager {
+ private static final Path PATH = FabricLoader.getInstance().getConfigDir().resolve("skyblocker.json");
+ private static final ConfigClassHandler<SkyblockerConfig> HANDLER = ConfigClassHandler.createBuilder(SkyblockerConfig.class)
+ .serializer(config -> GsonConfigSerializerBuilder.create(config)
+ .setPath(PATH)
+ .setJson5(false)
+ .appendGsonBuilder(builder -> builder
+ .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
+ .registerTypeHierarchyAdapter(Identifier.class, new Identifier.Serializer()))
+ .build())
+ .build();
+
+ public static SkyblockerConfig get() {
+ return HANDLER.instance();
+ }
+
+ /**
+ * This method is caller sensitive and can only be called by the mod initializer,
+ * this is enforced.
+ */
+ public static void init() {
+ if (StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass() != SkyblockerMod.class) {
+ throw new RuntimeException("Skyblocker: Called config init from an illegal place!");
+ }
+
+ HANDLER.load();
+ ClientCommandRegistrationCallback.EVENT.register(((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal(SkyblockerMod.NAMESPACE).then(optionsLiteral("config")).then(optionsLiteral("options")))));
+ }
+
+ public static void save() {
+ HANDLER.save();
+ }
+
+ public static Screen createGUI(Screen parent) {
+ return YetAnotherConfigLib.create(HANDLER, (defaults, config, builder) -> builder
+ .title(Text.translatable("text.autoconfig.skyblocker.title"))
+ .category(GeneralCategory.create(defaults, config))
+ .category(DungeonsCategory.create(defaults, config))
+ .category(DwarvenMinesCategory.create(defaults, config))
+ .category(LocationsCategory.create(defaults, config))
+ .category(SlayersCategory.create(defaults, config))
+ .category(QuickNavigationCategory.create(defaults, config))
+ .category(MessageFilterCategory.create(defaults, config))
+ .category(DiscordRPCCategory.create(defaults, config))).generateScreen(parent);
+ }
+
+ /**
+ * Registers an options command with the given name. Used for registering both options and config as valid commands.
+ *
+ * @param name the name of the command node
+ * @return the command builder
+ */
+ private static LiteralArgumentBuilder<FabricClientCommandSource> optionsLiteral(String name) {
+ // Don't immediately open the next screen as it will be closed by ChatScreen right after this command is executed
+ return ClientCommandManager.literal(name).executes(Scheduler.queueOpenScreenCommand(() -> createGUI(null)));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/DiscordRPCCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/DiscordRPCCategory.java
new file mode 100644
index 00000000..fcdc3d8d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/categories/DiscordRPCCategory.java
@@ -0,0 +1,49 @@
+package de.hysky.skyblocker.config.categories;
+
+import de.hysky.skyblocker.config.ConfigUtils;
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import dev.isxander.yacl3.api.ConfigCategory;
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.OptionDescription;
+import dev.isxander.yacl3.api.controller.StringControllerBuilder;
+import net.minecraft.text.Text;
+
+public class DiscordRPCCategory {
+
+ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig config) {
+ return ConfigCategory.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.category.richPresence"))
+
+ //Uncategorized Options
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.richPresence.enableRichPresence"))
+ .binding(defaults.richPresence.enableRichPresence,
+ () -> config.richPresence.enableRichPresence,
+ newValue -> config.richPresence.enableRichPresence = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<SkyblockerConfig.Info>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.richPresence.info"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.richPresence.info.@Tooltip")))
+ .binding(defaults.richPresence.info,
+ () -> config.richPresence.info,
+ newValue -> config.richPresence.info = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.richPresence.cycleMode"))
+ .binding(defaults.richPresence.cycleMode,
+ () -> config.richPresence.cycleMode,
+ newValue -> config.richPresence.cycleMode = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.richPresence.customMessage"))
+ .binding(defaults.richPresence.customMessage,
+ () -> config.richPresence.customMessage,
+ newValue -> config.richPresence.customMessage = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .build();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java
new file mode 100644
index 00000000..ffd979eb
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java
@@ -0,0 +1,316 @@
+package de.hysky.skyblocker.config.categories;
+
+import de.hysky.skyblocker.config.ConfigUtils;
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import dev.isxander.yacl3.api.ButtonOption;
+import dev.isxander.yacl3.api.ConfigCategory;
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.OptionDescription;
+import dev.isxander.yacl3.api.OptionFlag;
+import dev.isxander.yacl3.api.OptionGroup;
+import dev.isxander.yacl3.api.controller.FloatFieldControllerBuilder;
+import dev.isxander.yacl3.api.controller.IntegerFieldControllerBuilder;
+import dev.isxander.yacl3.api.controller.StringControllerBuilder;
+import de.hysky.skyblocker.config.controllers.EnumDropdownControllerBuilder;
+import de.hysky.skyblocker.skyblock.dungeon.DungeonMapConfigScreen;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+public class DungeonsCategory {
+
+ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig config) {
+ return ConfigCategory.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons"))
+
+ //Dungeon Secret Waypoints
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableSecretWaypoints"))
+ .binding(defaults.locations.dungeons.secretWaypoints.enableSecretWaypoints,
+ () -> config.locations.dungeons.secretWaypoints.enableSecretWaypoints,
+ newValue -> config.locations.dungeons.secretWaypoints.enableSecretWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints.@Tooltip")))
+ .binding(defaults.locations.dungeons.secretWaypoints.noInitSecretWaypoints,
+ () -> config.locations.dungeons.secretWaypoints.noInitSecretWaypoints,
+ newValue -> config.locations.dungeons.secretWaypoints.noInitSecretWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .flag(OptionFlag.GAME_RESTART)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableEntranceWaypoints"))
+ .binding(defaults.locations.dungeons.secretWaypoints.enableEntranceWaypoints,
+ () -> config.locations.dungeons.secretWaypoints.enableEntranceWaypoints,
+ newValue -> config.locations.dungeons.secretWaypoints.enableEntranceWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableSuperboomWaypoints"))
+ .binding(defaults.locations.dungeons.secretWaypoints.enableSuperboomWaypoints,
+ () -> config.locations.dungeons.secretWaypoints.enableSuperboomWaypoints,
+ newValue -> config.locations.dungeons.secretWaypoints.enableSuperboomWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableChestWaypoints"))
+ .binding(defaults.locations.dungeons.secretWaypoints.enableChestWaypoints,
+ () -> config.locations.dungeons.secretWaypoints.enableChestWaypoints,
+ newValue -> config.locations.dungeons.secretWaypoints.enableChestWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableItemWaypoints"))
+ .binding(defaults.locations.dungeons.secretWaypoints.enableItemWaypoints,
+ () -> config.locations.dungeons.secretWaypoints.enableItemWaypoints,
+ newValue -> config.locations.dungeons.secretWaypoints.enableItemWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableBatWaypoints"))
+ .binding(defaults.locations.dungeons.secretWaypoints.enableBatWaypoints,
+ () -> config.locations.dungeons.secretWaypoints.enableBatWaypoints,
+ newValue -> config.locations.dungeons.secretWaypoints.enableBatWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableWitherWaypoints"))
+ .binding(defaults.locations.dungeons.secretWaypoints.enableWitherWaypoints,
+ () -> config.locations.dungeons.secretWaypoints.enableWitherWaypoints,
+ newValue -> config.locations.dungeons.secretWaypoints.enableWitherWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableLeverWaypoints"))
+ .binding(defaults.locations.dungeons.secretWaypoints.enableLeverWaypoints,
+ () -> config.locations.dungeons.secretWaypoints.enableLeverWaypoints,
+ newValue -> config.locations.dungeons.secretWaypoints.enableLeverWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableFairySoulWaypoints"))
+ .binding(defaults.locations.dungeons.secretWaypoints.enableFairySoulWaypoints,
+ () -> config.locations.dungeons.secretWaypoints.enableFairySoulWaypoints,
+ newValue -> config.locations.dungeons.secretWaypoints.enableFairySoulWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableStonkWaypoints"))
+ .binding(defaults.locations.dungeons.secretWaypoints.enableStonkWaypoints,
+ () -> config.locations.dungeons.secretWaypoints.enableStonkWaypoints,
+ newValue -> config.locations.dungeons.secretWaypoints.enableStonkWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableDefaultWaypoints"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableDefaultWaypoints.@Tooltip")))
+ .binding(defaults.locations.dungeons.secretWaypoints.enableDefaultWaypoints,
+ () -> config.locations.dungeons.secretWaypoints.enableDefaultWaypoints,
+ newValue -> config.locations.dungeons.secretWaypoints.enableDefaultWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.enableProfitCalculator"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.enableProfitCalculator.@Tooltip")))
+ .binding(defaults.locations.dungeons.dungeonChestProfit.enableProfitCalculator,
+ () -> config.locations.dungeons.dungeonChestProfit.enableProfitCalculator,
+ newValue -> config.locations.dungeons.dungeonChestProfit.enableProfitCalculator = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.includeKismet"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.includeKismet.@Tooltip")))
+ .binding(defaults.locations.dungeons.dungeonChestProfit.includeKismet,
+ () -> config.locations.dungeons.dungeonChestProfit.includeKismet,
+ newValue -> config.locations.dungeons.dungeonChestProfit.includeKismet = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.includeEssence"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.includeEssence.@Tooltip")))
+ .binding(defaults.locations.dungeons.dungeonChestProfit.includeEssence,
+ () -> config.locations.dungeons.dungeonChestProfit.includeEssence,
+ newValue -> config.locations.dungeons.dungeonChestProfit.includeEssence = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.neutralThreshold"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.neutralThreshold.@Tooltip")))
+ .binding(defaults.locations.dungeons.dungeonChestProfit.neutralThreshold,
+ () -> config.locations.dungeons.dungeonChestProfit.neutralThreshold,
+ newValue -> config.locations.dungeons.dungeonChestProfit.neutralThreshold = newValue)
+ .controller(IntegerFieldControllerBuilder::create)
+ .build())
+ .option(Option.<Formatting>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.neutralColor"))
+ .binding(defaults.locations.dungeons.dungeonChestProfit.neutralColor,
+ () -> config.locations.dungeons.dungeonChestProfit.neutralColor,
+ newValue -> config.locations.dungeons.dungeonChestProfit.neutralColor = newValue)
+ .controller(EnumDropdownControllerBuilder.getFactory(ConfigUtils.FORMATTING_TO_STRING))
+ .build())
+ .option(Option.<Formatting>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.profitColor"))
+ .binding(defaults.locations.dungeons.dungeonChestProfit.profitColor,
+ () -> config.locations.dungeons.dungeonChestProfit.profitColor,
+ newValue -> config.locations.dungeons.dungeonChestProfit.profitColor = newValue)
+ .controller(EnumDropdownControllerBuilder.getFactory(ConfigUtils.FORMATTING_TO_STRING))
+ .build())
+ .option(Option.<Formatting>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.lossColor"))
+ .binding(defaults.locations.dungeons.dungeonChestProfit.lossColor,
+ () -> config.locations.dungeons.dungeonChestProfit.lossColor,
+ newValue -> config.locations.dungeons.dungeonChestProfit.lossColor = newValue)
+ .controller(EnumDropdownControllerBuilder.getFactory(ConfigUtils.FORMATTING_TO_STRING))
+ .build())
+ .option(Option.<Formatting>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.incompleteColor"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.incompleteColor.@Tooltip")))
+ .binding(defaults.locations.dungeons.dungeonChestProfit.incompleteColor,
+ () -> config.locations.dungeons.dungeonChestProfit.incompleteColor,
+ newValue -> config.locations.dungeons.dungeonChestProfit.incompleteColor = newValue)
+ .controller(EnumDropdownControllerBuilder.getFactory(ConfigUtils.FORMATTING_TO_STRING))
+ .build())
+ .build())
+
+ //Others
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.croesusHelper"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.croesusHelper.@Tooltip")))
+ .binding(defaults.locations.dungeons.croesusHelper,
+ () -> config.locations.dungeons.croesusHelper,
+ newValue -> config.locations.dungeons.croesusHelper = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.enableMap"))
+ .binding(defaults.locations.dungeons.enableMap,
+ () -> config.locations.dungeons.enableMap,
+ newValue -> config.locations.dungeons.enableMap = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(ButtonOption.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.mapScreen"))
+ .text(Text.translatable("text.skyblocker.open"))
+ .action((screen, opt) -> MinecraftClient.getInstance().setScreen(new DungeonMapConfigScreen(screen)))
+ .build())
+ .option(Option.<Float>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.mapScaling"))
+ .binding(defaults.locations.dungeons.mapScaling,
+ () -> config.locations.dungeons.mapScaling,
+ newValue -> config.locations.dungeons.mapScaling = newValue)
+ .controller(FloatFieldControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.mapX"))
+ .binding(defaults.locations.dungeons.mapX,
+ () -> config.locations.dungeons.mapX,
+ newValue -> config.locations.dungeons.mapX = newValue)
+ .controller(IntegerFieldControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.mapY"))
+ .binding(defaults.locations.dungeons.mapY,
+ () -> config.locations.dungeons.mapY,
+ newValue -> config.locations.dungeons.mapY = newValue)
+ .controller(IntegerFieldControllerBuilder::create)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.starredMobGlow"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.starredMobGlow.@Tooltip")))
+ .binding(defaults.locations.dungeons.starredMobGlow,
+ () -> config.locations.dungeons.starredMobGlow,
+ newValue -> config.locations.dungeons.starredMobGlow = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.solveThreeWeirdos"))
+ .binding(defaults.locations.dungeons.solveThreeWeirdos,
+ () -> config.locations.dungeons.solveThreeWeirdos,
+ newValue -> config.locations.dungeons.solveThreeWeirdos = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.blazesolver"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.blazesolver.@Tooltip")))
+ .binding(defaults.locations.dungeons.blazesolver,
+ () -> config.locations.dungeons.blazesolver,
+ newValue -> config.locations.dungeons.blazesolver = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.solveTrivia"))
+ .binding(defaults.locations.dungeons.solveTrivia,
+ () -> config.locations.dungeons.solveTrivia,
+ newValue -> config.locations.dungeons.solveTrivia = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.solveTicTacToe"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.solveTicTacToe.@Tooltip")))
+ .binding(defaults.locations.dungeons.solveTicTacToe,
+ () -> config.locations.dungeons.solveTicTacToe,
+ newValue -> config.locations.dungeons.solveTicTacToe = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+
+ //Livid Color
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.lividColor"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.lividColor.enableLividColor"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.lividColor.enableLividColor.@Tooltip")))
+ .binding(defaults.locations.dungeons.lividColor.enableLividColor,
+ () -> config.locations.dungeons.lividColor.enableLividColor,
+ newValue -> config.locations.dungeons.lividColor.enableLividColor = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.lividColor.lividColorText"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.lividColor.lividColorText.@Tooltip")))
+ .binding(defaults.locations.dungeons.lividColor.lividColorText,
+ () -> config.locations.dungeons.lividColor.lividColorText,
+ newValue -> config.locations.dungeons.lividColor.lividColorText = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .build())
+
+ //Terminal Solvers
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.terminals"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.terminals.solveColor"))
+ .binding(defaults.locations.dungeons.terminals.solveColor,
+ () -> config.locations.dungeons.terminals.solveColor,
+ newValue -> config.locations.dungeons.terminals.solveColor = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.terminals.solveOrder"))
+ .binding(defaults.locations.dungeons.terminals.solveOrder,
+ () -> config.locations.dungeons.terminals.solveOrder,
+ newValue -> config.locations.dungeons.terminals.solveOrder = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.terminals.solveStartsWith"))
+ .binding(defaults.locations.dungeons.terminals.solveStartsWith,
+ () -> config.locations.dungeons.terminals.solveStartsWith,
+ newValue -> config.locations.dungeons.terminals.solveStartsWith = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+ .build();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/DwarvenMinesCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/DwarvenMinesCategory.java
new file mode 100644
index 00000000..35c91d64
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/categories/DwarvenMinesCategory.java
@@ -0,0 +1,94 @@
+package de.hysky.skyblocker.config.categories;
+
+import de.hysky.skyblocker.config.ConfigUtils;
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import dev.isxander.yacl3.api.ButtonOption;
+import dev.isxander.yacl3.api.ConfigCategory;
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.OptionDescription;
+import dev.isxander.yacl3.api.OptionGroup;
+import dev.isxander.yacl3.api.controller.IntegerFieldControllerBuilder;
+import de.hysky.skyblocker.skyblock.dwarven.DwarvenHudConfigScreen;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.text.Text;
+
+public class DwarvenMinesCategory {
+
+ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig config) {
+ return ConfigCategory.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dwarvenMines"))
+
+ //Uncategorized Options
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dwarvenMines.enableDrillFuel"))
+ .binding(defaults.locations.dwarvenMines.enableDrillFuel,
+ () -> config.locations.dwarvenMines.enableDrillFuel,
+ newValue -> config.locations.dwarvenMines.enableDrillFuel = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dwarvenMines.solveFetchur"))
+ .binding(defaults.locations.dwarvenMines.solveFetchur,
+ () -> config.locations.dwarvenMines.solveFetchur,
+ newValue -> config.locations.dwarvenMines.solveFetchur = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dwarvenMines.solvePuzzler"))
+ .binding(defaults.locations.dwarvenMines.solvePuzzler,
+ () -> config.locations.dwarvenMines.solvePuzzler,
+ newValue -> config.locations.dwarvenMines.solvePuzzler = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+
+ //Dwarven HUD
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dwarvenMines.dwarvenHud"))
+ .collapsed(false)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dwarvenMines.dwarvenHud.enabled"))
+ .binding(defaults.locations.dwarvenMines.dwarvenHud.enabled,
+ () -> config.locations.dwarvenMines.dwarvenHud.enabled,
+ newValue -> config.locations.dwarvenMines.dwarvenHud.enabled = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<SkyblockerConfig.DwarvenHudStyle>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dwarvenMines.dwarvenHud.style"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dwarvenMines.dwarvenHud.style.@Tooltip[0]"),
+ Text.translatable("text.autoconfig.skyblocker.option.locations.dwarvenMines.dwarvenHud.style.@Tooltip[1]"),
+ Text.translatable("text.autoconfig.skyblocker.option.locations.dwarvenMines.dwarvenHud.style.@Tooltip[2]")))
+ .binding(defaults.locations.dwarvenMines.dwarvenHud.style,
+ () -> config.locations.dwarvenMines.dwarvenHud.style,
+ newValue -> config.locations.dwarvenMines.dwarvenHud.style = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(ButtonOption.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dwarvenMines.dwarvenHud.screen"))
+ .text(Text.translatable("text.skyblocker.open"))
+ .action((screen, opt) -> MinecraftClient.getInstance().setScreen(new DwarvenHudConfigScreen(screen)))
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dwarvenMines.dwarvenHud.enableBackground"))
+ .binding(defaults.locations.dwarvenMines.dwarvenHud.enableBackground,
+ () -> config.locations.dwarvenMines.dwarvenHud.enableBackground,
+ newValue -> config.locations.dwarvenMines.dwarvenHud.enableBackground = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dwarvenMines.dwarvenHud.x"))
+ .binding(defaults.locations.dwarvenMines.dwarvenHud.x,
+ () -> config.locations.dwarvenMines.dwarvenHud.x,
+ newValue -> config.locations.dwarvenMines.dwarvenHud.x = newValue)
+ .controller(IntegerFieldControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dwarvenMines.dwarvenHud.y"))
+ .binding(defaults.locations.dwarvenMines.dwarvenHud.y,
+ () -> config.locations.dwarvenMines.dwarvenHud.y,
+ newValue -> config.locations.dwarvenMines.dwarvenHud.y = newValue)
+ .controller(IntegerFieldControllerBuilder::create)
+ .build())
+ .build())
+ .build();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
new file mode 100644
index 00000000..6a393868
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
@@ -0,0 +1,508 @@
+package de.hysky.skyblocker.config.categories;
+
+import de.hysky.skyblocker.config.ConfigUtils;
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import dev.isxander.yacl3.api.*;
+import dev.isxander.yacl3.api.controller.FloatFieldControllerBuilder;
+import dev.isxander.yacl3.api.controller.FloatSliderControllerBuilder;
+import dev.isxander.yacl3.api.controller.IntegerFieldControllerBuilder;
+import dev.isxander.yacl3.api.controller.IntegerSliderControllerBuilder;
+import de.hysky.skyblocker.skyblock.shortcut.ShortcutsConfigScreen;
+import de.hysky.skyblocker.utils.render.title.TitleContainerConfigScreen;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.text.Text;
+
+public class GeneralCategory {
+
+ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig config) {
+ return ConfigCategory.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.category.general"))
+
+ //Ungrouped Options
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.acceptReparty"))
+ .binding(defaults.general.acceptReparty,
+ () -> config.general.acceptReparty,
+ newValue -> config.general.acceptReparty = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.backpackPreviewWithoutShift"))
+ .binding(defaults.general.backpackPreviewWithoutShift,
+ () -> config.general.backpackPreviewWithoutShift,
+ newValue -> config.general.backpackPreviewWithoutShift = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.compactorDeletorPreview"))
+ .binding(defaults.general.compactorDeletorPreview,
+ () -> config.general.compactorDeletorPreview,
+ newValue -> config.general.compactorDeletorPreview = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.hideEmptyTooltips"))
+ .binding(defaults.general.hideEmptyTooltips,
+ () -> config.general.hideEmptyTooltips,
+ newValue -> config.general.hideEmptyTooltips = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.hideStatusEffectOverlay"))
+ .binding(defaults.general.hideStatusEffectOverlay,
+ () -> config.general.hideStatusEffectOverlay,
+ newValue -> config.general.hideStatusEffectOverlay = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+
+ //Tab Hud
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.tabHud"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.tabHud.tabHudEnabled"))
+ .binding(defaults.general.tabHud.tabHudEnabled,
+ () -> config.general.tabHud.tabHudEnabled,
+ newValue -> config.general.tabHud.tabHudEnabled = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.tabHud.tabHudScale"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.tabHud.tabHudScale.@Tooltip")))
+ .binding(defaults.general.tabHud.tabHudScale,
+ () -> config.general.tabHud.tabHudScale,
+ newValue -> config.general.tabHud.tabHudScale = newValue)
+ .controller(opt -> IntegerSliderControllerBuilder.create(opt).range(10, 200).step(1))
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.tabHud.plainPlayerNames"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.tabHud.plainPlayerNames.@Tooltip")))
+ .binding(defaults.general.tabHud.plainPlayerNames,
+ () -> config.general.tabHud.plainPlayerNames,
+ newValue -> config.general.tabHud.plainPlayerNames = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<SkyblockerConfig.NameSorting>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.tabHud.nameSorting"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.tabHud.nameSorting.@Tooltip")))
+ .binding(defaults.general.tabHud.nameSorting,
+ () -> config.general.tabHud.nameSorting,
+ newValue -> config.general.tabHud.nameSorting = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .build())
+
+ //Fancy Bars
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.bars"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.bars.enableBars"))
+ .binding(defaults.general.bars.enableBars,
+ () -> config.general.bars.enableBars,
+ newValue -> config.general.bars.enableBars = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<SkyblockerConfig.BarPosition>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.bars.barpositions.healthBarPosition"))
+ .binding(defaults.general.bars.barPositions.healthBarPosition,
+ () -> config.general.bars.barPositions.healthBarPosition,
+ newValue -> config.general.bars.barPositions.healthBarPosition = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<SkyblockerConfig.BarPosition>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.bars.barpositions.manaBarPosition"))
+ .binding(defaults.general.bars.barPositions.manaBarPosition,
+ () -> config.general.bars.barPositions.manaBarPosition,
+ newValue -> config.general.bars.barPositions.manaBarPosition = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<SkyblockerConfig.BarPosition>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.bars.barpositions.defenceBarPosition"))
+ .binding(defaults.general.bars.barPositions.defenceBarPosition,
+ () -> config.general.bars.barPositions.defenceBarPosition,
+ newValue -> config.general.bars.barPositions.defenceBarPosition = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<SkyblockerConfig.BarPosition>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.bars.barpositions.experienceBarPosition"))
+ .binding(defaults.general.bars.barPositions.experienceBarPosition,
+ () -> config.general.bars.barPositions.experienceBarPosition,
+ newValue -> config.general.bars.barPositions.experienceBarPosition = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .build())
+
+ //Experiments Solver
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.experiments"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.experiments.enableChronomatronSolver"))
+ .binding(defaults.general.experiments.enableChronomatronSolver,
+ () -> config.general.experiments.enableChronomatronSolver,
+ newValue -> config.general.experiments.enableChronomatronSolver = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.experiments.enableSuperpairsSolver"))
+ .binding(defaults.general.experiments.enableSuperpairsSolver,
+ () -> config.general.experiments.enableSuperpairsSolver,
+ newValue -> config.general.experiments.enableSuperpairsSolver = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.experiments.enableUltrasequencerSolver"))
+ .binding(defaults.general.experiments.enableUltrasequencerSolver,
+ () -> config.general.experiments.enableUltrasequencerSolver,
+ newValue -> config.general.experiments.enableUltrasequencerSolver = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
+ //Fishing Helper
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.fishing"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.fishing.enableFishingHelper"))
+ .binding(defaults.general.fishing.enableFishingHelper,
+ () -> config.general.fishing.enableFishingHelper,
+ newValue -> config.general.fishing.enableFishingHelper = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
+ //Fairy Souls Helper
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.fairySouls"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.fairySouls.enableFairySoulsHelper"))
+ .binding(defaults.general.fairySouls.enableFairySoulsHelper,
+ () -> config.general.fairySouls.enableFairySoulsHelper,
+ newValue -> config.general.fairySouls.enableFairySoulsHelper = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.fairySouls.highlightFoundSouls"))
+ .binding(defaults.general.fairySouls.highlightFoundSouls,
+ () -> config.general.fairySouls.highlightFoundSouls,
+ newValue -> config.general.fairySouls.highlightFoundSouls = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.fairySouls.highlightOnlyNearbySouls"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.fairySouls.highlightOnlyNearbySouls.@Tooltip")))
+ .binding(defaults.general.fairySouls.highlightOnlyNearbySouls,
+ () -> config.general.fairySouls.highlightOnlyNearbySouls,
+ newValue -> config.general.fairySouls.highlightOnlyNearbySouls = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
+ //Item Cooldown
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemCooldown"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemCooldown.enableItemCooldowns"))
+ .binding(defaults.general.itemCooldown.enableItemCooldowns,
+ () -> config.general.itemCooldown.enableItemCooldowns,
+ newValue -> config.general.itemCooldown.enableItemCooldowns = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
+ //Shortcuts
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.shortcuts"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.shortcuts.enableShortcuts"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.shortcuts.enableShortcuts.@Tooltip")))
+ .binding(defaults.general.shortcuts.enableShortcuts,
+ () -> config.general.shortcuts.enableShortcuts,
+ newValue -> config.general.shortcuts.enableShortcuts = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.shortcuts.enableCommandShortcuts"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.shortcuts.enableCommandShortcuts.@Tooltip")))
+ .binding(defaults.general.shortcuts.enableCommandShortcuts,
+ () -> config.general.shortcuts.enableCommandShortcuts,
+ newValue -> config.general.shortcuts.enableCommandShortcuts = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.shortcuts.enableCommandArgShortcuts"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.shortcuts.enableCommandArgShortcuts.@Tooltip")))
+ .binding(defaults.general.shortcuts.enableCommandArgShortcuts,
+ () -> config.general.shortcuts.enableCommandArgShortcuts,
+ newValue -> config.general.shortcuts.enableCommandArgShortcuts = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(ButtonOption.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.shortcuts.config"))
+ .text(Text.translatable("text.skyblocker.open"))
+ .action((screen, opt) -> MinecraftClient.getInstance().setScreen(new ShortcutsConfigScreen(screen)))
+ .build())
+ .build())
+
+ //Quiver Warning
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.quiverWarning"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.quiverWarning.enableQuiverWarning"))
+ .binding(defaults.general.quiverWarning.enableQuiverWarning,
+ () -> config.general.quiverWarning.enableQuiverWarning,
+ newValue -> config.general.quiverWarning.enableQuiverWarning = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.quiverWarning.enableQuiverWarningInDungeons"))
+ .binding(defaults.general.quiverWarning.enableQuiverWarningInDungeons,
+ () -> config.general.quiverWarning.enableQuiverWarningInDungeons,
+ newValue -> config.general.quiverWarning.enableQuiverWarningInDungeons = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.quiverWarning.enableQuiverWarningAfterDungeon"))
+ .binding(defaults.general.quiverWarning.enableQuiverWarningAfterDungeon,
+ () -> config.general.quiverWarning.enableQuiverWarningAfterDungeon,
+ newValue -> config.general.quiverWarning.enableQuiverWarningAfterDungeon = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
+ //Item List
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemList"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemList.enableItemList"))
+ .binding(defaults.general.itemList.enableItemList,
+ () -> config.general.itemList.enableItemList,
+ newValue -> config.general.itemList.enableItemList = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
+ //Item Tooltip
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableNPCPrice"))
+ .binding(defaults.general.itemTooltip.enableNPCPrice,
+ () -> config.general.itemTooltip.enableNPCPrice,
+ newValue -> config.general.itemTooltip.enableNPCPrice = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableMotesPrice"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableMotesPrice.@Tooltip")))
+ .binding(defaults.general.itemTooltip.enableMotesPrice,
+ () -> config.general.itemTooltip.enableMotesPrice,
+ newValue -> config.general.itemTooltip.enableMotesPrice = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableAvgBIN"))
+ .binding(defaults.general.itemTooltip.enableAvgBIN,
+ () -> config.general.itemTooltip.enableAvgBIN,
+ newValue -> config.general.itemTooltip.enableAvgBIN = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<SkyblockerConfig.Average>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.avg"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.avg.@Tooltip")))
+ .binding(defaults.general.itemTooltip.avg,
+ () -> config.general.itemTooltip.avg,
+ newValue -> config.general.itemTooltip.avg = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableLowestBIN"))
+ .binding(defaults.general.itemTooltip.enableLowestBIN,
+ () -> config.general.itemTooltip.enableLowestBIN,
+ newValue -> config.general.itemTooltip.enableLowestBIN = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableBazaarPrice"))
+ .binding(defaults.general.itemTooltip.enableBazaarPrice,
+ () -> config.general.itemTooltip.enableBazaarPrice,
+ newValue -> config.general.itemTooltip.enableBazaarPrice = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableMuseumDate"))
+ .binding(defaults.general.itemTooltip.enableMuseumDate,
+ () -> config.general.itemTooltip.enableMuseumDate,
+ newValue -> config.general.itemTooltip.enableMuseumDate = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
+ //Item Info Display
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemInfoDisplay"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemInfoDisplay.attributeShardInfo"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.itemInfoDisplay.attributeShardInfo.@Tooltip")))
+ .binding(defaults.general.itemInfoDisplay.attributeShardInfo,
+ () -> config.general.itemInfoDisplay.attributeShardInfo,
+ newValue -> config.general.itemInfoDisplay.attributeShardInfo = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemInfoDisplay.itemRarityBackgrounds"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.itemInfoDisplay.itemRarityBackgrounds.@Tooltip")))
+ .binding(defaults.general.itemInfoDisplay.itemRarityBackgrounds,
+ () -> config.general.itemInfoDisplay.itemRarityBackgrounds,
+ newValue -> config.general.itemInfoDisplay.itemRarityBackgrounds = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Float>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemInfoDisplay.itemRarityBackgroundsOpacity"))
+ .binding(defaults.general.itemInfoDisplay.itemRarityBackgroundsOpacity,
+ () -> config.general.itemInfoDisplay.itemRarityBackgroundsOpacity,
+ newValue -> config.general.itemInfoDisplay.itemRarityBackgroundsOpacity = newValue)
+ .controller(opt -> FloatSliderControllerBuilder.create(opt).range(0f, 1f).step(0.05f).formatValue(ConfigUtils.FLOAT_TWO_FORMATTER))
+ .build())
+ .build())
+
+ //Special Effects
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.specialEffects"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.specialEffects.rareDungeonDropEffects"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.specialEffects.rareDungeonDropEffects.@Tooltip")))
+ .binding(defaults.general.specialEffects.rareDungeonDropEffects,
+ () -> config.general.specialEffects.rareDungeonDropEffects,
+ newValue -> config.general.specialEffects.rareDungeonDropEffects = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
+ //Hitboxes
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.hitbox"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.hitbox.oldFarmlandHitbox"))
+ .binding(defaults.general.hitbox.oldFarmlandHitbox,
+ () -> config.general.hitbox.oldFarmlandHitbox,
+ newValue -> config.general.hitbox.oldFarmlandHitbox = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.hitbox.oldLeverHitbox"))
+ .binding(defaults.general.hitbox.oldLeverHitbox,
+ () -> config.general.hitbox.oldLeverHitbox,
+ newValue -> config.general.hitbox.oldLeverHitbox = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
+ //Title Container
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.titleContainer"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.titleContainer.@Tooltip")))
+ .collapsed(true)
+ .option(Option.<Float>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.titleContainer.titleContainerScale"))
+ .binding(defaults.general.titleContainer.titleContainerScale,
+ () -> config.general.titleContainer.titleContainerScale,
+ newValue -> config.general.titleContainer.titleContainerScale = newValue)
+ .controller(opt -> FloatFieldControllerBuilder.create(opt).range(30f, 140f))
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.titleContainer.x"))
+ .binding(defaults.general.titleContainer.x,
+ () -> config.general.titleContainer.x,
+ newValue -> config.general.titleContainer.x = newValue)
+ .controller(IntegerFieldControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.titleContainer.y"))
+ .binding(defaults.general.titleContainer.y,
+ () -> config.general.titleContainer.y,
+ newValue -> config.general.titleContainer.y = newValue)
+ .controller(IntegerFieldControllerBuilder::create)
+ .build())
+ .option(Option.<SkyblockerConfig.Direction>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.titleContainer.direction"))
+ .binding(defaults.general.titleContainer.direction,
+ () -> config.general.titleContainer.direction,
+ newValue -> config.general.titleContainer.direction = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<SkyblockerConfig.Alignment>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.titleContainer.alignment"))
+ .binding(defaults.general.titleContainer.alignment,
+ () -> config.general.titleContainer.alignment,
+ newValue -> config.general.titleContainer.alignment = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(ButtonOption.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.titleContainer.config"))
+ .text(Text.translatable("text.skyblocker.open"))
+ .action((screen, opt) -> MinecraftClient.getInstance().setScreen(new TitleContainerConfigScreen(screen)))
+ .build())
+ .build())
+
+ //Teleport Overlays
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.teleportOverlay"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.teleportOverlay.enableTeleportOverlays"))
+ .binding(defaults.general.teleportOverlay.enableTeleportOverlays,
+ () -> config.general.teleportOverlay.enableTeleportOverlays,
+ newValue -> config.general.teleportOverlay.enableTeleportOverlays = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.teleportOverlay.enableWeirdTransmission"))
+ .binding(defaults.general.teleportOverlay.enableWeirdTransmission,
+ () -> config.general.teleportOverlay.enableWeirdTransmission,
+ newValue -> config.general.teleportOverlay.enableWeirdTransmission = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.teleportOverlay.enableInstantTransmission"))
+ .binding(defaults.general.teleportOverlay.enableInstantTransmission,
+ () -> config.general.teleportOverlay.enableInstantTransmission,
+ newValue -> config.general.teleportOverlay.enableInstantTransmission = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.teleportOverlay.enableEtherTransmission"))
+ .binding(defaults.general.teleportOverlay.enableEtherTransmission,
+ () -> config.general.teleportOverlay.enableEtherTransmission,
+ newValue -> config.general.teleportOverlay.enableEtherTransmission = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.teleportOverlay.enableSinrecallTransmission"))
+ .binding(defaults.general.teleportOverlay.enableSinrecallTransmission,
+ () -> config.general.teleportOverlay.enableSinrecallTransmission,
+ newValue -> config.general.teleportOverlay.enableSinrecallTransmission = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.teleportOverlay.enableWitherImpact"))
+ .binding(defaults.general.teleportOverlay.enableWitherImpact,
+ () -> config.general.teleportOverlay.enableWitherImpact,
+ newValue -> config.general.teleportOverlay.enableWitherImpact = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+ .build();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/LocationsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/LocationsCategory.java
new file mode 100644
index 00000000..399bb9f6
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/categories/LocationsCategory.java
@@ -0,0 +1,80 @@
+package de.hysky.skyblocker.config.categories;
+
+import de.hysky.skyblocker.config.ConfigUtils;
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import dev.isxander.yacl3.api.ConfigCategory;
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.OptionDescription;
+import dev.isxander.yacl3.api.OptionGroup;
+import dev.isxander.yacl3.api.controller.IntegerSliderControllerBuilder;
+import net.minecraft.text.Text;
+
+public class LocationsCategory {
+
+ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig config) {
+ return ConfigCategory.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.category.locations"))
+
+ //Barn
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.barn"))
+ .collapsed(false)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.barn.solveHungryHiker"))
+ .binding(defaults.locations.barn.solveHungryHiker,
+ () -> config.locations.barn.solveHungryHiker,
+ newValue -> config.locations.barn.solveHungryHiker = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.barn.solveTreasureHunter"))
+ .binding(defaults.locations.barn.solveTreasureHunter,
+ () -> config.locations.barn.solveTreasureHunter,
+ newValue -> config.locations.barn.solveTreasureHunter = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
+ //The Rift
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.rift"))
+ .collapsed(false)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.rift.mirrorverseWaypoints"))
+ .binding(defaults.locations.rift.mirrorverseWaypoints,
+ () -> config.locations.rift.mirrorverseWaypoints,
+ newValue -> config.locations.rift.mirrorverseWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.rift.mcGrubberStacks"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.rift.mcGrubberStacks.@Tooltip")))
+ .binding(defaults.locations.rift.mcGrubberStacks,
+ () -> config.locations.rift.mcGrubberStacks,
+ newValue -> config.locations.rift.mcGrubberStacks = newValue)
+ .controller(opt -> IntegerSliderControllerBuilder.create(opt).range(0, 5).step(1))
+ .build())
+ .build())
+
+ //Spider's Den
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.spidersDen"))
+ .collapsed(false)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.spidersDen.relics.enableRelicsHelper"))
+ .binding(defaults.locations.spidersDen.relics.enableRelicsHelper,
+ () -> config.locations.spidersDen.relics.enableRelicsHelper,
+ newValue -> config.locations.spidersDen.relics.enableRelicsHelper = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.spidersDen.relics.highlightFoundRelics"))
+ .binding(defaults.locations.spidersDen.relics.highlightFoundRelics,
+ () -> config.locations.spidersDen.relics.highlightFoundRelics,
+ newValue -> config.locations.spidersDen.relics.highlightFoundRelics = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+ .build();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java
new file mode 100644
index 00000000..ba76a903
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java
@@ -0,0 +1,98 @@
+package de.hysky.skyblocker.config.categories;
+
+import de.hysky.skyblocker.config.ConfigUtils;
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+import dev.isxander.yacl3.api.ConfigCategory;
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.OptionDescription;
+import net.minecraft.text.Text;
+
+public class MessageFilterCategory {
+
+ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig config) {
+ return ConfigCategory.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.category.messages"))
+
+ //Uncategorized Options
+ .option(Option.<ChatFilterResult>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.messages.hideAbility"))
+ .binding(defaults.messages.hideAbility,
+ () -> config.messages.hideAbility,
+ newValue -> config.messages.hideAbility = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<ChatFilterResult>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.messages.hideHeal"))
+ .binding(defaults.messages.hideHeal,
+ () -> config.messages.hideHeal,
+ newValue -> config.messages.hideHeal = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<ChatFilterResult>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.messages.hideAOTE"))
+ .binding(defaults.messages.hideAOTE,
+ () -> config.messages.hideAOTE,
+ newValue -> config.messages.hideAOTE = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<ChatFilterResult>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.messages.hideImplosion"))
+ .binding(defaults.messages.hideImplosion,
+ () -> config.messages.hideImplosion,
+ newValue -> config.messages.hideImplosion = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<ChatFilterResult>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.messages.hideMoltenWave"))
+ .binding(defaults.messages.hideMoltenWave,
+ () -> config.messages.hideMoltenWave,
+ newValue -> config.messages.hideMoltenWave = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<ChatFilterResult>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.messages.hideAds"))
+ .binding(defaults.messages.hideAds,
+ () -> config.messages.hideAds,
+ newValue -> config.messages.hideAds = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<ChatFilterResult>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.messages.hideTeleportPad"))
+ .binding(defaults.messages.hideTeleportPad,
+ () -> config.messages.hideTeleportPad,
+ newValue -> config.messages.hideTeleportPad = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<ChatFilterResult>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.messages.hideCombo"))
+ .binding(defaults.messages.hideCombo,
+ () -> config.messages.hideCombo,
+ newValue -> config.messages.hideCombo = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<ChatFilterResult>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.messages.hideAutopet"))
+ .binding(defaults.messages.hideAutopet,
+ () -> config.messages.hideAutopet,
+ newValue -> config.messages.hideAutopet = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<ChatFilterResult>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.messages.hideShowOff"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.messages.hideShowOff.@Tooltip")))
+ .binding(defaults.messages.hideShowOff,
+ () -> config.messages.hideShowOff,
+ newValue -> config.messages.hideShowOff = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.messages.hideMana"))
+ .binding(defaults.messages.hideMana,
+ () -> config.messages.hideMana,
+ newValue -> config.messages.hideMana = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/QuickNavigationCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/QuickNavigationCategory.java
new file mode 100644
index 00000000..b17fed23
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/categories/QuickNavigationCategory.java
@@ -0,0 +1,605 @@
+package de.hysky.skyblocker.config.categories;
+
+import de.hysky.skyblocker.config.ConfigUtils;
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import dev.isxander.yacl3.api.ConfigCategory;
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.OptionGroup;
+import dev.isxander.yacl3.api.controller.IntegerFieldControllerBuilder;
+import dev.isxander.yacl3.api.controller.StringControllerBuilder;
+import net.minecraft.text.Text;
+
+public class QuickNavigationCategory {
+
+ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig config) {
+ return ConfigCategory.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.category.quickNav"))
+
+ //Toggle
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.enableQuickNav"))
+ .binding(defaults.quickNav.enableQuickNav,
+ () -> config.quickNav.enableQuickNav,
+ newValue -> config.quickNav.enableQuickNav = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+
+ //Button 1
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.render"))
+ .binding(defaults.quickNav.button1.render,
+ () -> config.quickNav.button1.render,
+ newValue -> config.quickNav.button1.render = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.itemName"))
+ .binding(defaults.quickNav.button1.item.itemName,
+ () -> config.quickNav.button1.item.itemName,
+ newValue -> config.quickNav.button1.item.itemName = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.count"))
+ .binding(defaults.quickNav.button1.item.count,
+ () -> config.quickNav.button1.item.count,
+ newValue -> config.quickNav.button1.item.count = newValue)
+ .controller(opt -> IntegerFieldControllerBuilder.create(opt).range(1, 64))
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.nbt"))
+ .binding(defaults.quickNav.button1.item.nbt,
+ () -> config.quickNav.button1.item.nbt,
+ newValue -> config.quickNav.button1.item.nbt = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.uiTitle"))
+ .binding(defaults.quickNav.button1.uiTitle,
+ () -> config.quickNav.button1.uiTitle,
+ newValue -> config.quickNav.button1.uiTitle = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.clickEvent"))
+ .binding(defaults.quickNav.button1.clickEvent,
+ () -> config.quickNav.button1.clickEvent,
+ newValue -> config.quickNav.button1.clickEvent = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .build())
+
+ //Button 2
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button2"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.render"))
+ .binding(defaults.quickNav.button2.render,
+ () -> config.quickNav.button2.render,
+ newValue -> config.quickNav.button2.render = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.itemName"))
+ .binding(defaults.quickNav.button2.item.itemName,
+ () -> config.quickNav.button2.item.itemName,
+ newValue -> config.quickNav.button2.item.itemName = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.count"))
+ .binding(defaults.quickNav.button2.item.count,
+ () -> config.quickNav.button2.item.count,
+ newValue -> config.quickNav.button2.item.count = newValue)
+ .controller(opt -> IntegerFieldControllerBuilder.create(opt).range(1, 64))
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.nbt"))
+ .binding(defaults.quickNav.button2.item.nbt,
+ () -> config.quickNav.button2.item.nbt,
+ newValue -> config.quickNav.button2.item.nbt = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.uiTitle"))
+ .binding(defaults.quickNav.button2.uiTitle,
+ () -> config.quickNav.button2.uiTitle,
+ newValue -> config.quickNav.button2.uiTitle = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.clickEvent"))
+ .binding(defaults.quickNav.button2.clickEvent,
+ () -> config.quickNav.button2.clickEvent,
+ newValue -> config.quickNav.button2.clickEvent = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .build())
+
+ //Button 3
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button3"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.render"))
+ .binding(defaults.quickNav.button3.render,
+ () -> config.quickNav.button3.render,
+ newValue -> config.quickNav.button3.render = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.itemName"))
+ .binding(defaults.quickNav.button3.item.itemName,
+ () -> config.quickNav.button3.item.itemName,
+ newValue -> config.quickNav.button3.item.itemName = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.count"))
+ .binding(defaults.quickNav.button3.item.count,
+ () -> config.quickNav.button3.item.count,
+ newValue -> config.quickNav.button3.item.count = newValue)
+ .controller(opt -> IntegerFieldControllerBuilder.create(opt).range(1, 64))
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.nbt"))
+ .binding(defaults.quickNav.button3.item.nbt,
+ () -> config.quickNav.button3.item.nbt,
+ newValue -> config.quickNav.button3.item.nbt = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.uiTitle"))
+ .binding(defaults.quickNav.button3.uiTitle,
+ () -> config.quickNav.button3.uiTitle,
+ newValue -> config.quickNav.button3.uiTitle = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.clickEvent"))
+ .binding(defaults.quickNav.button3.clickEvent,
+ () -> config.quickNav.button3.clickEvent,
+ newValue -> config.quickNav.button3.clickEvent = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .build())
+
+ //Button 4
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button4"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.render"))
+ .binding(defaults.quickNav.button4.render,
+ () -> config.quickNav.button4.render,
+ newValue -> config.quickNav.button4.render = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.itemName"))
+ .binding(defaults.quickNav.button4.item.itemName,
+ () -> config.quickNav.button4.item.itemName,
+ newValue -> config.quickNav.button4.item.itemName = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.count"))
+ .binding(defaults.quickNav.button4.item.count,
+ () -> config.quickNav.button4.item.count,
+ newValue -> config.quickNav.button4.item.count = newValue)
+ .controller(opt -> IntegerFieldControllerBuilder.create(opt).range(1, 64))
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.nbt"))
+ .binding(defaults.quickNav.button4.item.nbt,
+ () -> config.quickNav.button4.item.nbt,
+ newValue -> config.quickNav.button4.item.nbt = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.uiTitle"))
+ .binding(defaults.quickNav.button4.uiTitle,
+ () -> config.quickNav.button4.uiTitle,
+ newValue -> config.quickNav.button4.uiTitle = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.clickEvent"))
+ .binding(defaults.quickNav.button4.clickEvent,
+ () -> config.quickNav.button4.clickEvent,
+ newValue -> config.quickNav.button4.clickEvent = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .build())
+
+ //Button 5
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button5"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.render"))
+ .binding(defaults.quickNav.button5.render,
+ () -> config.quickNav.button5.render,
+ newValue -> config.quickNav.button5.render = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.itemName"))
+ .binding(defaults.quickNav.button5.item.itemName,
+ () -> config.quickNav.button5.item.itemName,
+ newValue -> config.quickNav.button5.item.itemName = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.count"))
+ .binding(defaults.quickNav.button5.item.count,
+ () -> config.quickNav.button5.item.count,
+ newValue -> config.quickNav.button5.item.count = newValue)
+ .controller(opt -> IntegerFieldControllerBuilder.create(opt).range(1, 64))
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.nbt"))
+ .binding(defaults.quickNav.button5.item.nbt,
+ () -> config.quickNav.button5.item.nbt,
+ newValue -> config.quickNav.button5.item.nbt = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.uiTitle"))
+ .binding(defaults.quickNav.button5.uiTitle,
+ () -> config.quickNav.button5.uiTitle,
+ newValue -> config.quickNav.button5.uiTitle = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.clickEvent"))
+ .binding(defaults.quickNav.button5.clickEvent,
+ () -> config.quickNav.button5.clickEvent,
+ newValue -> config.quickNav.button5.clickEvent = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .build())
+
+ //Button 6
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button6"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.render"))
+ .binding(defaults.quickNav.button6.render,
+ () -> config.quickNav.button6.render,
+ newValue -> config.quickNav.button6.render = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.itemName"))
+ .binding(defaults.quickNav.button6.item.itemName,
+ () -> config.quickNav.button6.item.itemName,
+ newValue -> config.quickNav.button6.item.itemName = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.count"))
+ .binding(defaults.quickNav.button6.item.count,
+ () -> config.quickNav.button6.item.count,
+ newValue -> config.quickNav.button6.item.count = newValue)
+ .controller(opt -> IntegerFieldControllerBuilder.create(opt).range(1, 64))
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.nbt"))
+ .binding(defaults.quickNav.button6.item.nbt,
+ () -> config.quickNav.button6.item.nbt,
+ newValue -> config.quickNav.button6.item.nbt = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.uiTitle"))
+ .binding(defaults.quickNav.button6.uiTitle,
+ () -> config.quickNav.button6.uiTitle,
+ newValue -> config.quickNav.button6.uiTitle = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.clickEvent"))
+ .binding(defaults.quickNav.button6.clickEvent,
+ () -> config.quickNav.button6.clickEvent,
+ newValue -> config.quickNav.button6.clickEvent = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .build())
+
+ //Button 7
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button7"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.render"))
+ .binding(defaults.quickNav.button7.render,
+ () -> config.quickNav.button7.render,
+ newValue -> config.quickNav.button7.render = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.itemName"))
+ .binding(defaults.quickNav.button7.item.itemName,
+ () -> config.quickNav.button7.item.itemName,
+ newValue -> config.quickNav.button7.item.itemName = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.count"))
+ .binding(defaults.quickNav.button7.item.count,
+ () -> config.quickNav.button7.item.count,
+ newValue -> config.quickNav.button7.item.count = newValue)
+ .controller(opt -> IntegerFieldControllerBuilder.create(opt).range(1, 64))
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.nbt"))
+ .binding(defaults.quickNav.button7.item.nbt,
+ () -> config.quickNav.button7.item.nbt,
+ newValue -> config.quickNav.button7.item.nbt = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.uiTitle"))
+ .binding(defaults.quickNav.button7.uiTitle,
+ () -> config.quickNav.button7.uiTitle,
+ newValue -> config.quickNav.button7.uiTitle = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.clickEvent"))
+ .binding(defaults.quickNav.button7.clickEvent,
+ () -> config.quickNav.button7.clickEvent,
+ newValue -> config.quickNav.button7.clickEvent = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .build())
+
+ //Button 8
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button8"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.render"))
+ .binding(defaults.quickNav.button8.render,
+ () -> config.quickNav.button8.render,
+ newValue -> config.quickNav.button8.render = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.itemName"))
+ .binding(defaults.quickNav.button8.item.itemName,
+ () -> config.quickNav.button8.item.itemName,
+ newValue -> config.quickNav.button8.item.itemName = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.count"))
+ .binding(defaults.quickNav.button8.item.count,
+ () -> config.quickNav.button8.item.count,
+ newValue -> config.quickNav.button8.item.count = newValue)
+ .controller(opt -> IntegerFieldControllerBuilder.create(opt).range(1, 64))
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.nbt"))
+ .binding(defaults.quickNav.button8.item.nbt,
+ () -> config.quickNav.button8.item.nbt,
+ newValue -> config.quickNav.button8.item.nbt = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.uiTitle"))
+ .binding(defaults.quickNav.button8.uiTitle,
+ () -> config.quickNav.button8.uiTitle,
+ newValue -> config.quickNav.button8.uiTitle = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.clickEvent"))
+ .binding(defaults.quickNav.button8.clickEvent,
+ () -> config.quickNav.button8.clickEvent,
+ newValue -> config.quickNav.button8.clickEvent = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .build())
+
+ //Button 9
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button9"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.render"))
+ .binding(defaults.quickNav.button9.render,
+ () -> config.quickNav.button9.render,
+ newValue -> config.quickNav.button9.render = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.itemName"))
+ .binding(defaults.quickNav.button9.item.itemName,
+ () -> config.quickNav.button9.item.itemName,
+ newValue -> config.quickNav.button9.item.itemName = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.count"))
+ .binding(defaults.quickNav.button9.item.count,
+ () -> config.quickNav.button9.item.count,
+ newValue -> config.quickNav.button9.item.count = newValue)
+ .controller(opt -> IntegerFieldControllerBuilder.create(opt).range(1, 64))
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.nbt"))
+ .binding(defaults.quickNav.button9.item.nbt,
+ () -> config.quickNav.button9.item.nbt,
+ newValue -> config.quickNav.button9.item.nbt = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.uiTitle"))
+ .binding(defaults.quickNav.button9.uiTitle,
+ () -> config.quickNav.button9.uiTitle,
+ newValue -> config.quickNav.button9.uiTitle = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.clickEvent"))
+ .binding(defaults.quickNav.button9.clickEvent,
+ () -> config.quickNav.button9.clickEvent,
+ newValue -> config.quickNav.button9.clickEvent = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .build())
+
+ //Button 10
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button10"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.render"))
+ .binding(defaults.quickNav.button10.render,
+ () -> config.quickNav.button10.render,
+ newValue -> config.quickNav.button10.render = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.itemName"))
+ .binding(defaults.quickNav.button10.item.itemName,
+ () -> config.quickNav.button10.item.itemName,
+ newValue -> config.quickNav.button10.item.itemName = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.count"))
+ .binding(defaults.quickNav.button10.item.count,
+ () -> config.quickNav.button10.item.count,
+ newValue -> config.quickNav.button10.item.count = newValue)
+ .controller(opt -> IntegerFieldControllerBuilder.create(opt).range(1, 64))
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.nbt"))
+ .binding(defaults.quickNav.button10.item.nbt,
+ () -> config.quickNav.button10.item.nbt,
+ newValue -> config.quickNav.button10.item.nbt = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.uiTitle"))
+ .binding(defaults.quickNav.button10.uiTitle,
+ () -> config.quickNav.button10.uiTitle,
+ newValue -> config.quickNav.button10.uiTitle = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.clickEvent"))
+ .binding(defaults.quickNav.button10.clickEvent,
+ () -> config.quickNav.button10.clickEvent,
+ newValue -> config.quickNav.button10.clickEvent = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .build())
+
+ //Button 11
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button11"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.render"))
+ .binding(defaults.quickNav.button11.render,
+ () -> config.quickNav.button11.render,
+ newValue -> config.quickNav.button11.render = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.itemName"))
+ .binding(defaults.quickNav.button11.item.itemName,
+ () -> config.quickNav.button11.item.itemName,
+ newValue -> config.quickNav.button11.item.itemName = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.count"))
+ .binding(defaults.quickNav.button11.item.count,
+ () -> config.quickNav.button11.item.count,
+ newValue -> config.quickNav.button11.item.count = newValue)
+ .controller(opt -> IntegerFieldControllerBuilder.create(opt).range(1, 64))
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.nbt"))
+ .binding(defaults.quickNav.button11.item.nbt,
+ () -> config.quickNav.button11.item.nbt,
+ newValue -> config.quickNav.button11.item.nbt = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.uiTitle"))
+ .binding(defaults.quickNav.button11.uiTitle,
+ () -> config.quickNav.button11.uiTitle,
+ newValue -> config.quickNav.button11.uiTitle = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.clickEvent"))
+ .binding(defaults.quickNav.button11.clickEvent,
+ () -> config.quickNav.button11.clickEvent,
+ newValue -> config.quickNav.button11.clickEvent = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .build())
+
+ //Button 12
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button12"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.render"))
+ .binding(defaults.quickNav.button12.render,
+ () -> config.quickNav.button12.render,
+ newValue -> config.quickNav.button12.render = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.itemName"))
+ .binding(defaults.quickNav.button12.item.itemName,
+ () -> config.quickNav.button12.item.itemName,
+ newValue -> config.quickNav.button12.item.itemName = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.count"))
+ .binding(defaults.quickNav.button12.item.count,
+ () -> config.quickNav.button12.item.count,
+ newValue -> config.quickNav.button12.item.count = newValue)
+ .controller(opt -> IntegerFieldControllerBuilder.create(opt).range(1, 64))
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.item.nbt"))
+ .binding(defaults.quickNav.button12.item.nbt,
+ () -> config.quickNav.button12.item.nbt,
+ newValue -> config.quickNav.button12.item.nbt = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.uiTitle"))
+ .binding(defaults.quickNav.button12.uiTitle,
+ () -> config.quickNav.button12.uiTitle,
+ newValue -> config.quickNav.button12.uiTitle = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .option(Option.<String>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.quickNav.button1.clickEvent"))
+ .binding(defaults.quickNav.button12.clickEvent,
+ () -> config.quickNav.button12.clickEvent,
+ newValue -> config.quickNav.button12.clickEvent = newValue)
+ .controller(StringControllerBuilder::create)
+ .build())
+ .build())
+
+ .build();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/SlayersCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/SlayersCategory.java
new file mode 100644
index 00000000..2d8b1332
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/categories/SlayersCategory.java
@@ -0,0 +1,116 @@
+package de.hysky.skyblocker.config.categories;
+
+import de.hysky.skyblocker.config.ConfigUtils;
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import dev.isxander.yacl3.api.ConfigCategory;
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.OptionDescription;
+import dev.isxander.yacl3.api.OptionGroup;
+import dev.isxander.yacl3.api.controller.FloatFieldControllerBuilder;
+import dev.isxander.yacl3.api.controller.IntegerFieldControllerBuilder;
+import dev.isxander.yacl3.api.controller.IntegerSliderControllerBuilder;
+import net.minecraft.text.Text;
+
+public class SlayersCategory {
+
+ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig config) {
+ return ConfigCategory.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.category.slayer"))
+
+ //Vampire Slayer
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.enableEffigyWaypoints"))
+ .binding(defaults.slayer.vampireSlayer.enableEffigyWaypoints,
+ () -> config.slayer.vampireSlayer.enableEffigyWaypoints,
+ newValue -> config.slayer.vampireSlayer.enableEffigyWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.compactEffigyWaypoints"))
+ .binding(defaults.slayer.vampireSlayer.compactEffigyWaypoints,
+ () -> config.slayer.vampireSlayer.compactEffigyWaypoints,
+ newValue -> config.slayer.vampireSlayer.compactEffigyWaypoints = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.effigyUpdateFrequency"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.effigyUpdateFrequency.@Tooltip")))
+ .binding(defaults.slayer.vampireSlayer.effigyUpdateFrequency,
+ () -> config.slayer.vampireSlayer.effigyUpdateFrequency,
+ newValue -> config.slayer.vampireSlayer.effigyUpdateFrequency = newValue)
+ .controller(opt -> IntegerSliderControllerBuilder.create(opt).range(1, 10).step(1))
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.enableHolyIceIndicator"))
+ .binding(defaults.slayer.vampireSlayer.enableHolyIceIndicator,
+ () -> config.slayer.vampireSlayer.enableHolyIceIndicator,
+ newValue -> config.slayer.vampireSlayer.enableHolyIceIndicator = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.holyIceIndicatorTickDelay"))
+ .binding(defaults.slayer.vampireSlayer.holyIceIndicatorTickDelay,
+ () -> config.slayer.vampireSlayer.holyIceIndicatorTickDelay,
+ newValue -> config.slayer.vampireSlayer.holyIceIndicatorTickDelay = newValue)
+ .controller(IntegerFieldControllerBuilder::create)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.holyIceUpdateFrequency"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.holyIceUpdateFrequency.@Tooltip")))
+ .binding(defaults.slayer.vampireSlayer.holyIceUpdateFrequency,
+ () -> config.slayer.vampireSlayer.holyIceUpdateFrequency,
+ newValue -> config.slayer.vampireSlayer.holyIceUpdateFrequency = newValue)
+ .controller(opt -> IntegerSliderControllerBuilder.create(opt).range(1, 10).step(1))
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.enableHealingMelonIndicator"))
+ .binding(defaults.slayer.vampireSlayer.enableHealingMelonIndicator,
+ () -> config.slayer.vampireSlayer.enableHealingMelonIndicator,
+ newValue -> config.slayer.vampireSlayer.enableHealingMelonIndicator = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Float>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.healingMelonHealthThreshold"))
+ .binding(defaults.slayer.vampireSlayer.healingMelonHealthThreshold,
+ () -> config.slayer.vampireSlayer.healingMelonHealthThreshold,
+ newValue -> config.slayer.vampireSlayer.healingMelonHealthThreshold = newValue)
+ .controller(FloatFieldControllerBuilder::create)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.enableSteakStakeIndicator"))
+ .binding(defaults.slayer.vampireSlayer.enableSteakStakeIndicator,
+ () -> config.slayer.vampireSlayer.enableSteakStakeIndicator,
+ newValue -> config.slayer.vampireSlayer.enableSteakStakeIndicator = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.steakStakeUpdateFrequency"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.steakStakeUpdateFrequency.@Tooltip")))
+ .binding(defaults.slayer.vampireSlayer.steakStakeUpdateFrequency,
+ () -> config.slayer.vampireSlayer.steakStakeUpdateFrequency,
+ newValue -> config.slayer.vampireSlayer.steakStakeUpdateFrequency = newValue)
+ .controller(opt -> IntegerSliderControllerBuilder.create(opt).range(1, 10).step(1))
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.enableManiaIndicator"))
+ .binding(defaults.slayer.vampireSlayer.enableManiaIndicator,
+ () -> config.slayer.vampireSlayer.enableManiaIndicator,
+ newValue -> config.slayer.vampireSlayer.enableManiaIndicator = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Integer>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.maniaUpdateFrequency"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.slayer.vampireSlayer.maniaUpdateFrequency.@Tooltip")))
+ .binding(defaults.slayer.vampireSlayer.maniaUpdateFrequency,
+ () -> config.slayer.vampireSlayer.maniaUpdateFrequency,
+ newValue -> config.slayer.vampireSlayer.maniaUpdateFrequency = newValue)
+ .controller(opt -> IntegerSliderControllerBuilder.create(opt).range(1, 10).step(1))
+ .build())
+ .build())
+
+ .build();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownController.java b/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownController.java
new file mode 100644
index 00000000..0b9a809d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownController.java
@@ -0,0 +1,93 @@
+package de.hysky.skyblocker.config.controllers;
+
+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.dropdown.AbstractDropdownController;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+public class EnumDropdownController<E extends Enum<E>> extends AbstractDropdownController<E> {
+ /**
+ * The function used to convert enum constants to strings used for display, suggestion, and validation. Defaults to {@link Enum#toString}.
+ */
+ protected final Function<E, String> toString;
+
+ protected EnumDropdownController(Option<E> option, Function<E, String> toString) {
+ super(option);
+ this.toString = toString;
+ }
+
+ @Override
+ public String getString() {
+ return toString.apply(option().pendingValue());
+ }
+
+ @Override
+ public void setFromString(String value) {
+ option().requestSet(getEnumFromString(value));
+ }
+
+ /**
+ * Searches through enum constants for one whose {@link #toString} result equals {@code value}
+ *
+ * @return The enum constant associated with the {@code value} or the pending value if none are found
+ * @implNote The return value of {@link #toString} on each enum constant should be unique in order to ensure accuracy
+ */
+ private E getEnumFromString(String value) {
+ value = value.toLowerCase();
+ for (E constant : option().pendingValue().getDeclaringClass().getEnumConstants()) {
+ if (toString.apply(constant).toLowerCase().equals(value)) return constant;
+ }
+
+ return option().pendingValue();
+ }
+
+ @Override
+ public boolean isValueValid(String value) {
+ value = value.toLowerCase();
+ for (E constant : option().pendingValue().getDeclaringClass().getEnumConstants()) {
+ if (toString.apply(constant).equals(value)) return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ protected String getValidValue(String value, int offset) {
+ return getValidEnumConstants(value)
+ .skip(offset)
+ .findFirst()
+ .orElseGet(this::getString);
+ }
+
+ /**
+ * Filters and sorts through enum constants for those whose {@link #toString} result equals {@code value}
+ *
+ * @return a sorted stream containing enum constants associated with the {@code value}
+ * @implNote The return value of {@link #toString} on each enum constant should be unique in order to ensure accuracy
+ */
+ @NotNull
+ protected Stream<String> getValidEnumConstants(String value) {
+ String valueLowerCase = value.toLowerCase();
+ return Arrays.stream(option().pendingValue().getDeclaringClass().getEnumConstants())
+ .map(this.toString)
+ .filter(constant -> constant.toLowerCase().contains(valueLowerCase))
+ .sorted((s1, s2) -> {
+ String s1LowerCase = s1.toLowerCase();
+ String s2LowerCase = s2.toLowerCase();
+ if (s1LowerCase.startsWith(valueLowerCase) && !s2LowerCase.startsWith(valueLowerCase)) return -1;
+ if (!s1LowerCase.startsWith(valueLowerCase) && s2LowerCase.startsWith(valueLowerCase)) return 1;
+ return s1.compareTo(s2);
+ });
+ }
+
+ @Override
+ public AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) {
+ return new EnumDropdownControllerElement<>(this, screen, widgetDimension);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerBuilder.java b/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerBuilder.java
new file mode 100644
index 00000000..d451a88c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerBuilder.java
@@ -0,0 +1,27 @@
+package de.hysky.skyblocker.config.controllers;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.controller.ControllerBuilder;
+
+import java.util.function.Function;
+
+public interface EnumDropdownControllerBuilder<E extends Enum<E>> extends ControllerBuilder<E> {
+ EnumDropdownControllerBuilder<E> toString(Function<E, String> toString);
+
+ static <E extends Enum<E>> EnumDropdownControllerBuilder<E> create(Option<E> option) {
+ return new EnumDropdownControllerBuilderImpl<>(option);
+ }
+
+ /**
+ * Creates a factory for {@link EnumDropdownControllerBuilder}s with the given function for converting enum constants to strings.
+ * Use this if a custom toString function for an enum is needed.
+ * Use it like this:
+ * <pre>{@code Option.<MyEnum>createBuilder().controller(createEnumDropdownControllerBuilder.getFactory(MY_CUSTOM_ENUM_TO_STRING_FUNCTION))}</pre>
+ * @param toString The function used to convert enum constants to strings used for display, suggestion, and validation
+ * @return a factory for {@link EnumDropdownControllerBuilder}s
+ * @param <E> the enum type
+ */
+ static <E extends Enum<E>> Function<Option<E>, ControllerBuilder<E>> getFactory(Function<E, String> toString) {
+ return opt -> EnumDropdownControllerBuilder.create(opt).toString(toString);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerBuilderImpl.java b/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerBuilderImpl.java
new file mode 100644
index 00000000..8f6dbb2a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerBuilderImpl.java
@@ -0,0 +1,27 @@
+package de.hysky.skyblocker.config.controllers;
+
+import dev.isxander.yacl3.api.Controller;
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.impl.controller.AbstractControllerBuilderImpl;
+
+import java.util.function.Function;
+
+public class EnumDropdownControllerBuilderImpl<E extends Enum<E>> extends AbstractControllerBuilderImpl<E> implements EnumDropdownControllerBuilder<E> {
+ private Function<E, String> toString = Enum::toString;
+
+ public EnumDropdownControllerBuilderImpl(Option<E> option) {
+ super(option);
+ }
+
+ @Override
+ public EnumDropdownControllerBuilder<E> toString(Function<E, String> toString) {
+ this.toString = toString;
+ return this;
+ }
+
+ @SuppressWarnings("UnstableApiUsage")
+ @Override
+ public Controller<E> build() {
+ return new EnumDropdownController<>(option, toString);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerElement.java b/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerElement.java
new file mode 100644
index 00000000..2a8de609
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerElement.java
@@ -0,0 +1,26 @@
+package de.hysky.skyblocker.config.controllers;
+
+import dev.isxander.yacl3.api.utils.Dimension;
+import dev.isxander.yacl3.gui.YACLScreen;
+import dev.isxander.yacl3.gui.controllers.dropdown.AbstractDropdownControllerElement;
+
+import java.util.List;
+
+public class EnumDropdownControllerElement<E extends Enum<E>> extends AbstractDropdownControllerElement<E, String> {
+ private final EnumDropdownController<E> controller;
+
+ public EnumDropdownControllerElement(EnumDropdownController<E> control, YACLScreen screen, Dimension<Integer> dim) {
+ super(control, screen, dim);
+ this.controller = control;
+ }
+
+ @Override
+ public List<String> computeMatchingValues() {
+ return controller.getValidEnumConstants(inputField).toList();
+ }
+
+ @Override
+ public String getString(String object) {
+ return object;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/events/ClientPlayerBlockBreakEvent.java b/src/main/java/de/hysky/skyblocker/events/ClientPlayerBlockBreakEvent.java
new file mode 100644
index 00000000..83ac716f
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/events/ClientPlayerBlockBreakEvent.java
@@ -0,0 +1,23 @@
+package de.hysky.skyblocker.events;
+
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.api.event.EventFactory;
+import net.minecraft.block.BlockState;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+
+// Fabric API currently doesn't have an event for this
+public class ClientPlayerBlockBreakEvent {
+ public static final Event<AfterBlockBreak> AFTER = EventFactory.createArrayBacked(AfterBlockBreak.class,
+ (listeners) -> (world, player, pos, state) -> {
+ for (AfterBlockBreak listener : listeners) {
+ listener.afterBlockBreak(world, player, pos, state);
+ }
+ });
+
+ @FunctionalInterface
+ public interface AfterBlockBreak {
+ void afterBlockBreak(World world, PlayerEntity player, BlockPos pos, BlockState state);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java b/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java
new file mode 100644
index 00000000..303e454f
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java
@@ -0,0 +1,33 @@
+package de.hysky.skyblocker.events;
+
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.api.event.EventFactory;
+
+@Environment(EnvType.CLIENT)
+public final class SkyblockEvents {
+ public static final Event<SkyblockEvents.SkyblockJoin> JOIN = EventFactory.createArrayBacked(SkyblockEvents.SkyblockJoin.class, callbacks -> () -> {
+ for (SkyblockEvents.SkyblockJoin callback : callbacks) {
+ callback.onSkyblockJoin();
+ }
+ });
+
+ public static final Event<SkyblockEvents.SkyblockLeave> LEAVE = EventFactory.createArrayBacked(SkyblockEvents.SkyblockLeave.class, callbacks -> () -> {
+ for (SkyblockEvents.SkyblockLeave callback : callbacks) {
+ callback.onSkyblockLeave();
+ }
+ });
+
+ @Environment(EnvType.CLIENT)
+ @FunctionalInterface
+ public interface SkyblockJoin {
+ void onSkyblockJoin();
+ }
+
+ @Environment(EnvType.CLIENT)
+ @FunctionalInterface
+ public interface SkyblockLeave {
+ void onSkyblockLeave();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/AbstractInventoryScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixin/AbstractInventoryScreenMixin.java
new file mode 100644
index 00000000..d0d4b9f7
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/AbstractInventoryScreenMixin.java
@@ -0,0 +1,19 @@
+package de.hysky.skyblocker.mixin;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.client.gui.screen.ingame.AbstractInventoryScreen;
+
+@Mixin(AbstractInventoryScreen.class)
+public class AbstractInventoryScreenMixin {
+
+ @Inject(method = "drawStatusEffects", at = @At("HEAD"), cancellable = true)
+ private void skyblocker$dontDrawStatusEffects(CallbackInfo ci) {
+ if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().general.hideStatusEffectOverlay) ci.cancel();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/ArmorTrimMixin.java b/src/main/java/de/hysky/skyblocker/mixin/ArmorTrimMixin.java
new file mode 100644
index 00000000..02d75409
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/ArmorTrimMixin.java
@@ -0,0 +1,37 @@
+package de.hysky.skyblocker.mixin;
+
+import com.llamalad7.mixinextras.injector.ModifyReturnValue;
+import com.llamalad7.mixinextras.sugar.Local;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.item.CustomArmorTrims;
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.trim.ArmorTrim;
+import net.minecraft.nbt.NbtCompound;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+import java.util.Optional;
+
+@Mixin(ArmorTrim.class)
+public class ArmorTrimMixin {
+
+ @ModifyReturnValue(method = "getTrim", at = @At("RETURN"))
+ private static Optional<ArmorTrim> skyblocker$customArmorTrims(@SuppressWarnings("OptionalUsedAsFieldOrParameterType") Optional<ArmorTrim> original, @Local ItemStack stack) {
+ NbtCompound nbt = stack.getNbt();
+
+ if (Utils.isOnSkyblock() && nbt != null && nbt.contains("ExtraAttributes")) {
+ Object2ObjectOpenHashMap<String, CustomArmorTrims.ArmorTrimId> customTrims = SkyblockerConfigManager.get().general.customArmorTrims;
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ String itemUuid = extraAttributes.contains("uuid") ? extraAttributes.getString("uuid") : null;
+
+ if (customTrims.containsKey(itemUuid)) {
+ CustomArmorTrims.ArmorTrimId trimKey = customTrims.get(itemUuid);
+ return CustomArmorTrims.TRIMS_CACHE.getOrDefault(trimKey, original);
+ }
+ }
+
+ return original;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/BatEntityMixin.java b/src/main/java/de/hysky/skyblocker/mixin/BatEntityMixin.java
new file mode 100644
index 00000000..dc2fa673
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/BatEntityMixin.java
@@ -0,0 +1,21 @@
+package de.hysky.skyblocker.mixin;
+
+import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonSecrets;
+import net.minecraft.entity.EntityType;
+import net.minecraft.entity.mob.AmbientEntity;
+import net.minecraft.entity.passive.BatEntity;
+import net.minecraft.world.World;
+import org.spongepowered.asm.mixin.Mixin;
+
+@Mixin(BatEntity.class)
+public abstract class BatEntityMixin extends AmbientEntity {
+ protected BatEntityMixin(EntityType<? extends AmbientEntity> entityType, World world) {
+ super(entityType, world);
+ }
+
+ @Override
+ public void onRemoved() {
+ super.onRemoved();
+ DungeonSecrets.onBatRemoved(this);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java
new file mode 100644
index 00000000..fff534b2
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java
@@ -0,0 +1,48 @@
+package de.hysky.skyblocker.mixin;
+
+import com.llamalad7.mixinextras.injector.WrapWithCondition;
+import com.llamalad7.mixinextras.sugar.Local;
+import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonSecrets;
+import dev.cbyrne.betterinject.annotations.Inject;
+import de.hysky.skyblocker.skyblock.FishingHelper;
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.entity.ItemEntity;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.network.packet.s2c.play.PlaySoundS2CPacket;
+import org.slf4j.Logger;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+
+@Mixin(ClientPlayNetworkHandler.class)
+public abstract class ClientPlayNetworkHandlerMixin {
+
+ @Inject(method = "onPlaySound", at = @At("RETURN"))
+ private void skyblocker$onPlaySound(PlaySoundS2CPacket packet) {
+ FishingHelper.onSound(packet);
+ }
+
+ @SuppressWarnings("resource")
+ @ModifyVariable(method = "onItemPickupAnimation", at = @At(value = "STORE", ordinal = 0))
+ private ItemEntity skyblocker$onItemPickup(ItemEntity itemEntity, @Local LivingEntity collector) {
+ DungeonSecrets.onItemPickup(itemEntity, collector, collector == MinecraftClient.getInstance().player);
+ return itemEntity;
+ }
+
+ @WrapWithCondition(method = "onEntityPassengersSet", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;)V", remap = false))
+ private boolean skyblocker$cancelEntityPassengersWarning(Logger instance, String msg) {
+ return !Utils.isOnHypixel();
+ }
+
+ @WrapWithCondition(method = "onPlayerList", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;)V", remap = false))
+ private boolean skyblocker$cancelPlayerListWarning(Logger instance, String format, Object arg) {
+ return !Utils.isOnHypixel();
+ }
+
+ @WrapWithCondition(method = "onTeam", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;[Ljava/lang/Object;)V", remap = false))
+ private boolean skyblocker$cancelTeamWarning(Logger instance, String format, Object... arg) {
+ return !Utils.isOnHypixel();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/ClientPlayerEntityMixin.java b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayerEntityMixin.java
new file mode 100644
index 00000000..37ae92e8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayerEntityMixin.java
@@ -0,0 +1,35 @@
+package de.hysky.skyblocker.mixin;
+
+import com.mojang.authlib.GameProfile;
+
+import dev.cbyrne.betterinject.annotations.Inject;
+import de.hysky.skyblocker.skyblock.HotbarSlotLock;
+import de.hysky.skyblocker.skyblock.item.ItemProtection;
+import de.hysky.skyblocker.skyblock.rift.HealingMelonIndicator;
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.client.network.AbstractClientPlayerEntity;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.client.world.ClientWorld;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+@Mixin(ClientPlayerEntity.class)
+public abstract class ClientPlayerEntityMixin extends AbstractClientPlayerEntity {
+ public ClientPlayerEntityMixin(ClientWorld world, GameProfile profile) {
+ super(world, profile);
+ }
+
+ @Inject(method = "dropSelectedItem", at = @At("HEAD"), cancellable = true)
+ public void skyblocker$dropSelectedItem(CallbackInfoReturnable<Boolean> cir) {
+ if (Utils.isOnSkyblock()) {
+ if (ItemProtection.isItemProtected(this.getInventory().getMainHandStack())) cir.setReturnValue(false);
+ HotbarSlotLock.handleDropSelectedItem(this.getInventory().selectedSlot, cir);
+ }
+ }
+
+ @Inject(method = "updateHealth", at = @At("RETURN"))
+ public void skyblocker$updateHealth() {
+ HealingMelonIndicator.updateHealth();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/mixin/ClientPlayerInteractionManagerMixin.java b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayerInteractionManagerMixin.java
new file mode 100644
index 00000000..fab9a1ea
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayerInteractionManagerMixin.java
@@ -0,0 +1,27 @@
+package de.hysky.skyblocker.mixin;
+
+import de.hysky.skyblocker.events.ClientPlayerBlockBreakEvent;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerInteractionManager;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+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.CallbackInfoReturnable;
+import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
+
+@Mixin(ClientPlayerInteractionManager.class)
+public class ClientPlayerInteractionManagerMixin {
+ @Shadow
+ @Final
+ private MinecraftClient client;
+
+ @Inject(method = "breakBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/Block;onBroken(Lnet/minecraft/world/WorldAccess;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;)V"), locals = LocalCapture.CAPTURE_FAILHARD)
+ private void skyblocker$onBlockBroken(BlockPos pos, CallbackInfoReturnable<Boolean> cir, World world, BlockState blockState) {
+ ClientPlayerBlockBreakEvent.AFTER.invoker().afterBlockBreak(world, this.client.player, pos, blockState);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/DrawContextMixin.java b/src/main/java/de/hysky/skyblocker/mixin/DrawContextMixin.java
new file mode 100644
index 00000000..41b8e985
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/DrawContextMixin.java
@@ -0,0 +1,72 @@
+package de.hysky.skyblocker.mixin;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import com.llamalad7.mixinextras.sugar.Local;
+import com.llamalad7.mixinextras.sugar.ref.LocalRef;
+import dev.cbyrne.betterinject.annotations.Arg;
+import dev.cbyrne.betterinject.annotations.Inject;
+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.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;
+
+@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(@Arg TextRenderer textRenderer, @Arg ItemStack stack, @Arg(ordinal = 0) int x, @Arg(ordinal = 1) int y, @Local(argsOnly = true) LocalRef<String> countOverride) {
+ if (!SkyblockerConfigManager.get().general.itemInfoDisplay.attributeShardInfo) return;
+
+ NbtCompound nbt = stack.getNbt();
+
+ if (Utils.isOnSkyblock() && nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+
+ if (extraAttributes.getString("id").equals("ATTRIBUTE_SHARD")) {
+ NbtCompound attributesTag = extraAttributes.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 ItemStack stack) {
+ return Utils.isOnSkyblock() && ItemCooldowns.isOnCooldown(stack) ? ItemCooldowns.getItemCooldownEntry(stack).getRemainingCooldownPercent() : cooldownProgress;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/DyeableItemMixin.java b/src/main/java/de/hysky/skyblocker/mixin/DyeableItemMixin.java
new file mode 100644
index 00000000..51ab3852
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/DyeableItemMixin.java
@@ -0,0 +1,27 @@
+package de.hysky.skyblocker.mixin;
+
+import com.llamalad7.mixinextras.injector.ModifyReturnValue;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.item.DyeableItem;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(DyeableItem.class)
+public interface DyeableItemMixin {
+ @ModifyReturnValue(method = "getColor", at = @At("RETURN"))
+ private int skyblocker$customDyeColor(int originalColor, ItemStack stack) {
+ NbtCompound nbt = stack.getNbt();
+
+ if (Utils.isOnSkyblock() && nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ String itemUuid = extraAttributes.contains("uuid") ? extraAttributes.getString("uuid") : null;
+
+ return SkyblockerConfigManager.get().general.customDyeColors.getOrDefault(itemUuid, originalColor);
+ }
+
+ return originalColor;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/FarmlandBlockMixin.java b/src/main/java/de/hysky/skyblocker/mixin/FarmlandBlockMixin.java
new file mode 100644
index 00000000..dfa886c4
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/FarmlandBlockMixin.java
@@ -0,0 +1,38 @@
+package de.hysky.skyblocker.mixin;
+
+import com.llamalad7.mixinextras.injector.ModifyReturnValue;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.block.Block;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.FarmlandBlock;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.shape.VoxelShape;
+import net.minecraft.util.shape.VoxelShapes;
+import net.minecraft.world.BlockView;
+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;
+
+@Mixin(FarmlandBlock.class)
+public abstract class FarmlandBlockMixin extends Block {
+ @Shadow
+ @Final
+ protected static VoxelShape SHAPE;
+
+ protected FarmlandBlockMixin(Settings settings) {
+ super(settings);
+ }
+
+ @ModifyReturnValue(method = "getOutlineShape", at = @At("RETURN"))
+ private VoxelShape skyblocker$replaceOutlineShape(VoxelShape original) {
+ return Utils.isOnSkyblock() && SkyblockerConfigManager.get().general.hitbox.oldFarmlandHitbox ? VoxelShapes.fullCube() : original;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public VoxelShape getCullingShape(BlockState state, BlockView world, BlockPos pos) {
+ return SHAPE;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/GenericContainerScreenHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixin/GenericContainerScreenHandlerMixin.java
new file mode 100644
index 00000000..9929c5d4
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/GenericContainerScreenHandlerMixin.java
@@ -0,0 +1,30 @@
+package de.hysky.skyblocker.mixin;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.GenericContainerScreenHandler;
+import net.minecraft.screen.ScreenHandler;
+import net.minecraft.screen.ScreenHandlerType;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+
+import java.util.List;
+
+@Mixin(GenericContainerScreenHandler.class)
+public abstract class GenericContainerScreenHandlerMixin extends ScreenHandler {
+ protected GenericContainerScreenHandlerMixin(@Nullable ScreenHandlerType<?> type, int syncId) {
+ super(type, syncId);
+ }
+
+ @Override
+ public void setStackInSlot(int slot, int revision, ItemStack stack) {
+ SkyblockerMod.getInstance().containerSolverManager.markDirty();
+ super.setStackInSlot(slot, revision, stack);
+ }
+
+ @Override
+ public void updateSlotStacks(int revision, List<ItemStack> stacks, ItemStack cursorStack) {
+ SkyblockerMod.getInstance().containerSolverManager.markDirty();
+ super.updateSlotStacks(revision, stacks, cursorStack);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/HandledScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixin/HandledScreenMixin.java
new file mode 100644
index 00000000..689974c8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/HandledScreenMixin.java
@@ -0,0 +1,193 @@
+package de.hysky.skyblocker.mixin;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.experiment.ChronomatronSolver;
+import de.hysky.skyblocker.skyblock.experiment.ExperimentSolver;
+import de.hysky.skyblocker.skyblock.experiment.SuperpairsSolver;
+import de.hysky.skyblocker.skyblock.experiment.UltrasequencerSolver;
+import de.hysky.skyblocker.skyblock.item.BackpackPreview;
+import de.hysky.skyblocker.skyblock.item.CompactorDeletorPreview;
+import de.hysky.skyblocker.skyblock.item.ItemProtection;
+import de.hysky.skyblocker.skyblock.item.ItemRarityBackgrounds;
+import de.hysky.skyblocker.skyblock.item.WikiLookup;
+import de.hysky.skyblocker.skyblock.itemlist.ItemRegistry;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.gui.ContainerSolver;
+import net.minecraft.client.MinecraftClient;
+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.item.TooltipContext;
+import net.minecraft.inventory.SimpleInventory;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.GenericContainerScreenHandler;
+import net.minecraft.screen.ScreenHandler;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.screen.slot.SlotActionType;
+import net.minecraft.text.Text;
+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.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.Redirect;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import java.util.Map;
+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 of the screen's bounds
+ */
+ @Unique
+ private static final int OUT_OF_BOUNDS_SLOT = -999;
+
+ @Shadow
+ @Nullable
+ protected Slot focusedSlot;
+
+ @Shadow
+ @Final
+ protected T handler;
+
+ protected HandledScreenMixin(Text title) {
+ super(title);
+ }
+
+ @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 && !this.client.options.inventoryKey.matchesKey(keyCode, scanCode) && WikiLookup.wikiLookup.matchesKey(keyCode, scanCode)) {
+ WikiLookup.openWiki(this.focusedSlot);
+ }
+ }
+
+ @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) {
+ if (!Utils.isOnSkyblock()) return;
+
+ // Hide Empty Tooltips
+ if (SkyblockerConfigManager.get().general.hideEmptyTooltips && focusedSlot.getStack().getName().getString().equals(" ")) {
+ ci.cancel();
+ }
+
+ // Backpack Preview
+ boolean shiftDown = SkyblockerConfigManager.get().general.backpackPreviewWithoutShift ^ Screen.hasShiftDown();
+ if (shiftDown && getTitle().getString().equals("Storage") && focusedSlot.inventory != client.player.getInventory() && BackpackPreview.renderPreview(context, focusedSlot.getIndex(), x, y)) {
+ ci.cancel();
+ }
+
+ // Compactor Preview
+ if (SkyblockerConfigManager.get().general.compactorDeletorPreview) {
+ ItemStack stack = focusedSlot.getStack();
+ Matcher matcher = CompactorDeletorPreview.NAME.matcher(ItemRegistry.getInternalName(stack));
+ if (matcher.matches() && CompactorDeletorPreview.drawPreview(context, stack, matcher.group("type"), matcher.group("size"), x, y)) {
+ ci.cancel();
+ }
+ }
+ }
+
+ @Redirect(method = "drawMouseoverTooltip", at = @At(value = "INVOKE", target = "Lnet/minecraft/screen/slot/Slot;getStack()Lnet/minecraft/item/ItemStack;", ordinal = 0))
+ private ItemStack skyblocker$experimentSolvers$replaceTooltipDisplayStack(Slot slot) {
+ return skyblocker$experimentSolvers$getStack(slot, null);
+ }
+
+ @ModifyVariable(method = "drawSlot", at = @At(value = "LOAD", ordinal = 4), ordinal = 0)
+ private ItemStack skyblocker$experimentSolvers$replaceDisplayStack(ItemStack stack, DrawContext context, Slot slot) {
+ return skyblocker$experimentSolvers$getStack(slot, stack);
+ }
+
+
+ @Unique
+ private ItemStack skyblocker$experimentSolvers$getStack(Slot slot, 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 ? slot.getStack() : itemStack;
+ }
+ return (stack != null) ? stack : slot.getStack();
+ }
+
+ @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"))
+ private void skyblocker$experimentSolvers$onSlotClick(Slot slot, int slotId, int button, SlotActionType actionType, CallbackInfo ci) {
+ if (slot != null) {
+ ContainerSolver currentSolver = SkyblockerMod.getInstance().containerSolverManager.getCurrentSolver();
+ if (currentSolver instanceof ExperimentSolver experimentSolver && experimentSolver.getState() == ExperimentSolver.State.SHOW && slot.inventory instanceof SimpleInventory) {
+ if (experimentSolver instanceof ChronomatronSolver chronomatronSolver) {
+ Item item = chronomatronSolver.getChronomatronSlots().get(chronomatronSolver.getChronomatronCurrentOrdinal());
+ if ((slot.getStack().isOf(item) || ChronomatronSolver.TERRACOTTA_TO_GLASS.get(slot.getStack().getItem()) == item) && chronomatronSolver.incrementChronomatronCurrentOrdinal() >= chronomatronSolver.getChronomatronSlots().size()) {
+ chronomatronSolver.setState(ExperimentSolver.State.END);
+ }
+ } else if (experimentSolver instanceof SuperpairsSolver superpairsSolver) {
+ superpairsSolver.setSuperpairsPrevClickedSlot(slot.getIndex());
+ superpairsSolver.setSuperpairsCurrentSlot(ItemStack.EMPTY);
+ } else if (experimentSolver instanceof UltrasequencerSolver ultrasequencerSolver && 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));
+ }
+ }
+ }
+ }
+
+ /**
+ * 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
+ */
+ @Inject(method = "onMouseClick(Lnet/minecraft/screen/slot/Slot;IILnet/minecraft/screen/slot/SlotActionType;)V", at = @At("HEAD"), cancellable = true)
+ private void skyblocker$onSlotInteract(Slot slot, int slotId, int button, SlotActionType actionType, CallbackInfo ci) {
+ if (Utils.isOnSkyblock()) {
+ // When you try and drop the item by picking it up then clicking outside of the screen
+ if (slotId == OUT_OF_BOUNDS_SLOT) {
+ ItemStack cursorStack = this.handler.getCursorStack();
+
+ if (ItemProtection.isItemProtected(cursorStack)) ci.cancel();
+ }
+
+ if (slot != null) {
+ // When you click your drop key while hovering over an item
+ if (actionType == SlotActionType.THROW) {
+ ItemStack stack = slot.getStack();
+
+ if (ItemProtection.isItemProtected(stack)) ci.cancel();
+ }
+
+ //Prevent salvaging
+ if (this.getTitle().getString().equals("Salvage Items")) {
+ ItemStack stack = slot.getStack();
+
+ if (ItemProtection.isItemProtected(stack)) ci.cancel();
+ }
+
+ //Prevent selling to NPC shops
+ if (this.client != null && this.handler instanceof GenericContainerScreenHandler genericContainerScreenHandler && genericContainerScreenHandler.getRows() == 6) {
+ ItemStack sellItem = this.handler.slots.get(49).getStack();
+
+ if (sellItem.getName().getString().equals("Sell Item") || skyblocker$doesLoreContain(sellItem, this.client, "buyback")) {
+ ItemStack stack = slot.getStack();
+
+ if (ItemProtection.isItemProtected(stack)) ci.cancel();
+ }
+ }
+ }
+ }
+ }
+
+ //TODO make this a util method somewhere else, eventually
+ private static boolean skyblocker$doesLoreContain(ItemStack stack, MinecraftClient client, String searchString) {
+ return stack.getTooltip(client.player, TooltipContext.BASIC).stream().map(Text::getString).anyMatch(line -> line.contains(searchString));
+ }
+
+ @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);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/InGameHudMixin.java b/src/main/java/de/hysky/skyblocker/mixin/InGameHudMixin.java
new file mode 100644
index 00000000..1b6d62d4
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/InGameHudMixin.java
@@ -0,0 +1,93 @@
+package de.hysky.skyblocker.mixin;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import com.llamalad7.mixinextras.sugar.Local;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.FancyStatusBars;
+import de.hysky.skyblocker.skyblock.HotbarSlotLock;
+import de.hysky.skyblocker.skyblock.item.ItemCooldowns;
+import de.hysky.skyblocker.skyblock.dungeon.DungeonMap;
+import de.hysky.skyblocker.skyblock.item.ItemRarityBackgrounds;
+import de.hysky.skyblocker.utils.Utils;
+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.hud.InGameHud;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.Identifier;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Environment(EnvType.CLIENT)
+@Mixin(InGameHud.class)
+public abstract class InGameHudMixin {
+ @Unique
+ private static final Identifier SLOT_LOCK = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/slot_lock.png");
+ @Unique
+ private final FancyStatusBars statusBars = new FancyStatusBars();
+
+ @Shadow
+ private int scaledHeight;
+ @Shadow
+ private int scaledWidth;
+
+ @Shadow
+ @Final
+ private MinecraftClient client;
+
+ @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))
+ public void skyblocker$renderHotbarItemLockOrRarityBg(float tickDelta, DrawContext context, CallbackInfo ci, @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()) {
+ if (SkyblockerConfigManager.get().general.itemInfoDisplay.itemRarityBackgrounds) ItemRarityBackgrounds.tryDraw(player.getInventory().main.get(index), context, x, y);
+ if (HotbarSlotLock.isLocked(index)) context.drawTexture(SLOT_LOCK, x, y, 0, 0, 16, 16);
+ }
+ }
+
+ @Inject(method = "renderExperienceBar", at = @At("HEAD"), cancellable = true)
+ private void skyblocker$renderExperienceBar(CallbackInfo ci) {
+ if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().general.bars.enableBars && !Utils.isInTheRift())
+ ci.cancel();
+ }
+
+ @Inject(method = "renderStatusBars", at = @At("HEAD"), cancellable = true)
+ private void skyblocker$renderStatusBars(DrawContext context, CallbackInfo ci) {
+ if (!Utils.isOnSkyblock())
+ return;
+ if (statusBars.render(context, scaledWidth, scaledHeight))
+ ci.cancel();
+
+ if (Utils.isInDungeons() && SkyblockerConfigManager.get().locations.dungeons.enableMap)
+ DungeonMap.render(context.getMatrices());
+ }
+
+ @Inject(method = "renderMountHealth", at = @At("HEAD"), cancellable = true)
+ private void skyblocker$renderMountHealth(CallbackInfo ci) {
+ if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().general.bars.enableBars && !Utils.isInTheRift())
+ ci.cancel();
+ }
+
+ @Inject(method = "renderStatusEffectOverlay", at = @At("HEAD"), cancellable = true)
+ private void skyblocker$dontRenderStatusEffects(CallbackInfo ci) {
+ if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().general.hideStatusEffectOverlay) ci.cancel();
+ }
+
+ @ModifyExpressionValue(method = "renderCrosshair", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;getAttackCooldownProgress(F)F"))
+ private float skyblocker$modifyAttackIndicatorCooldown(float cooldownProgress) {
+ if (Utils.isOnSkyblock() && client.player != null) {
+ ItemStack stack = client.player.getMainHandStack();
+ if (ItemCooldowns.isOnCooldown(stack)) {
+ return ItemCooldowns.getItemCooldownEntry(stack).getRemainingCooldownPercent();
+ }
+ }
+
+ return cooldownProgress;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/InventoryScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixin/InventoryScreenMixin.java
new file mode 100644
index 00000000..8e6b9230
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/InventoryScreenMixin.java
@@ -0,0 +1,18 @@
+package de.hysky.skyblocker.mixin;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.itemlist.ItemListWidget;
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.client.gui.screen.ingame.InventoryScreen;
+import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(InventoryScreen.class)
+public abstract class InventoryScreenMixin {
+ @ModifyExpressionValue(method = "<init>", at = @At(value = "NEW", target = "net/minecraft/client/gui/screen/recipebook/RecipeBookWidget"))
+ private RecipeBookWidget skyblocker$replaceRecipeBook(RecipeBookWidget original) {
+ return SkyblockerConfigManager.get().general.itemList.enableItemList && Utils.isOnSkyblock() ? new ItemListWidget() : original;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/ItemMixin.java b/src/main/java/de/hysky/skyblocker/mixin/ItemMixin.java
new file mode 100644
index 00000000..98bea52b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/ItemMixin.java
@@ -0,0 +1,22 @@
+package de.hysky.skyblocker.mixin;
+
+import org.objectweb.asm.Opcodes;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+
+@Mixin(Item.class)
+public abstract class ItemMixin {
+ @WrapOperation(
+ method = {"getItemBarColor", "getItemBarStep"},
+ at = @At(value = "FIELD", target = "Lnet/minecraft/item/Item;maxDamage:I", opcode = Opcodes.GETFIELD)
+ )
+ private int skyblocker$handlePickoDrillBar(Item item, Operation<Integer> original, ItemStack stack) {
+ return stack.getMaxDamage();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/ItemStackMixin.java b/src/main/java/de/hysky/skyblocker/mixin/ItemStackMixin.java
new file mode 100644
index 00000000..c7f5fac9
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/ItemStackMixin.java
@@ -0,0 +1,61 @@
+package de.hysky.skyblocker.mixin;
+
+import de.hysky.skyblocker.utils.ItemUtils;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+
+import com.llamalad7.mixinextras.injector.ModifyReturnValue;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.text.Text;
+
+@Mixin(ItemStack.class)
+public abstract class ItemStackMixin {
+ @Shadow
+ @Nullable
+ private NbtCompound nbt;
+
+ @ModifyReturnValue(method = "getName", at = @At("RETURN"))
+ private Text skyblocker$customItemNames(Text original) {
+ if (Utils.isOnSkyblock() && nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ String itemUuid = extraAttributes.contains("uuid") ? extraAttributes.getString("uuid") : null;
+
+ return SkyblockerConfigManager.get().general.customItemNames.getOrDefault(itemUuid, original);
+ }
+
+ return original;
+ }
+
+ @ModifyReturnValue(method = "getDamage", at = @At("RETURN"))
+ private int skyblocker$handleDamage(int original) {
+ ItemUtils.Durability dur = ItemUtils.getDurability((ItemStack) (Object) this);
+ if (dur != null) {
+ return dur.max() - dur.current();
+ }
+ return original;
+ }
+
+ @ModifyReturnValue(method = "getMaxDamage", at = @At("RETURN"))
+ private int skyblocker$handleMaxDamage(int original) {
+ ItemUtils.Durability dur = ItemUtils.getDurability((ItemStack) (Object) this);
+ if (dur != null) {
+ return dur.max();
+ }
+ return original;
+ }
+
+ @ModifyReturnValue(method = "isDamageable", at = @At("RETURN"))
+ private boolean skyblocker$handleDamageable(boolean original) {
+ ItemUtils.Durability dur = ItemUtils.getDurability((ItemStack) (Object) this);
+ if (dur != null) {
+ return true;
+ }
+ return original;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/LeverBlockMixin.java b/src/main/java/de/hysky/skyblocker/mixin/LeverBlockMixin.java
new file mode 100644
index 00000000..97c0a7c0
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/LeverBlockMixin.java
@@ -0,0 +1,29 @@
+package de.hysky.skyblocker.mixin;
+
+import de.hysky.skyblocker.skyblock.dungeon.OldLever;
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.LeverBlock;
+import net.minecraft.block.WallMountedBlock;
+import net.minecraft.util.shape.VoxelShape;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import dev.cbyrne.betterinject.annotations.Arg;
+import dev.cbyrne.betterinject.annotations.Inject;
+
+@Mixin(LeverBlock.class)
+public abstract class LeverBlockMixin extends WallMountedBlock {
+ protected LeverBlockMixin(Settings settings) {
+ super(settings);
+ }
+
+ @Inject(method = "getOutlineShape", at = @At("HEAD"), cancellable = true)
+ public void skyblocker$onGetOutlineShape(@Arg BlockState state, CallbackInfoReturnable<VoxelShape> cir) {
+ if (Utils.isOnSkyblock()) {
+ VoxelShape shape = OldLever.getShape(state.get(FACE), state.get(FACING));
+ if (shape != null) cir.setReturnValue(shape);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/MinecraftClientMixin.java b/src/main/java/de/hysky/skyblocker/mixin/MinecraftClientMixin.java
new file mode 100644
index 00000000..066490d5
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/MinecraftClientMixin.java
@@ -0,0 +1,25 @@
+package de.hysky.skyblocker.mixin;
+
+import de.hysky.skyblocker.skyblock.HotbarSlotLock;
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import dev.cbyrne.betterinject.annotations.Inject;
+
+@Mixin(MinecraftClient.class)
+public abstract class MinecraftClientMixin {
+ @Shadow
+ @Nullable
+ public ClientPlayerEntity player;
+
+ @Inject(method = "handleInputEvents", at = @At("HEAD"))
+ public void skyblocker$handleInputEvents() {
+ if (Utils.isOnSkyblock()) {
+ HotbarSlotLock.handleInputEvents(player);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/mixin/PlayerListHudMixin.java b/src/main/java/de/hysky/skyblocker/mixin/PlayerListHudMixin.java
new file mode 100644
index 00000000..7330b1c1
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/PlayerListHudMixin.java
@@ -0,0 +1,57 @@
+package de.hysky.skyblocker.mixin;
+
+import de.hysky.skyblocker.skyblock.tabhud.screenbuilder.ScreenMaster;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.tabhud.TabHud;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.utils.Utils;
+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.hud.PlayerListHud;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.text.Text;
+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.callback.CallbackInfo;
+
+import dev.cbyrne.betterinject.annotations.Arg;
+import dev.cbyrne.betterinject.annotations.Inject;
+
+@Environment(EnvType.CLIENT)
+@Mixin(PlayerListHud.class)
+public class PlayerListHudMixin {
+ @Shadow
+ private Text footer;
+
+ @Inject(at = @At("HEAD"), method = "render(Lnet/minecraft/client/gui/DrawContext;ILnet/minecraft/scoreboard/Scoreboard;Lnet/minecraft/scoreboard/ScoreboardObjective;)V", cancellable = true)
+ public void skyblocker$renderTabHud(@Arg DrawContext context, @Arg int w, CallbackInfo info) {
+ if (!Utils.isOnSkyblock() || !SkyblockerConfigManager.get().general.tabHud.tabHudEnabled || TabHud.defaultTgl.isPressed()) {
+ return;
+ }
+
+ ClientPlayNetworkHandler nwH = MinecraftClient.getInstance().getNetworkHandler();
+ if (nwH == null) {
+ return;
+ }
+
+ int h = MinecraftClient.getInstance().getWindow().getScaledHeight();
+ float scale = SkyblockerConfigManager.get().general.tabHud.tabHudScale / 100f;
+ w = (int) (w / scale);
+ h = (int) (h / scale);
+
+ PlayerListMgr.updateFooter(footer);
+
+ try {
+ ScreenMaster.render(context, w,h);
+ // Screen screen = Screen.getCorrect(w, h, footer);
+ // screen.render(context);
+ info.cancel();
+ } catch (Exception e) {
+ TabHud.LOGGER.error("[Skyblocker] Encountered unknown exception while drawing default hud", e);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/mixin/PlayerSkinProviderMixin.java b/src/main/java/de/hysky/skyblocker/mixin/PlayerSkinProviderMixin.java
new file mode 100644
index 00000000..978835d2
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/PlayerSkinProviderMixin.java
@@ -0,0 +1,29 @@
+package de.hysky.skyblocker.mixin;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+import com.llamalad7.mixinextras.injector.ModifyReturnValue;
+import com.llamalad7.mixinextras.sugar.Local;
+import com.mojang.authlib.GameProfile;
+import com.mojang.authlib.minecraft.MinecraftSessionService;
+
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.client.texture.PlayerSkinProvider.Textures;
+
+@Mixin(targets = "net.minecraft.client.texture.PlayerSkinProvider$1")
+public class PlayerSkinProviderMixin {
+
+ @ModifyReturnValue(method = "method_52867", at = @At("RETURN"))
+ private static Textures skyblocker$fixTexturesThatHadAnInvalidSignature(Textures texture, @Local MinecraftSessionService sessionService, @Local GameProfile profile) {
+ if (Utils.isOnHypixel() && texture == Textures.MISSING) {
+ try {
+ return Textures.fromMap(sessionService.getTextures(profile, false), false);
+ } catch (Throwable t) {
+ return Textures.MISSING;
+ }
+ }
+
+ return texture;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/ScoreboardMixin.java b/src/main/java/de/hysky/skyblocker/mixin/ScoreboardMixin.java
new file mode 100644
index 00000000..2cfb658a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/ScoreboardMixin.java
@@ -0,0 +1,16 @@
+package de.hysky.skyblocker.mixin;
+
+import com.llamalad7.mixinextras.injector.WrapWithCondition;
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.scoreboard.Scoreboard;
+import org.slf4j.Logger;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(Scoreboard.class)
+public abstract class ScoreboardMixin {
+ @WrapWithCondition(method = "addTeam", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;)V", remap = false))
+ private boolean skyblocker$cancelTeamWarning(Logger instance, String format, Object arg) {
+ return !Utils.isOnHypixel();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/SocialInteractionsPlayerListWidgetMixin.java b/src/main/java/de/hysky/skyblocker/mixin/SocialInteractionsPlayerListWidgetMixin.java
new file mode 100644
index 00000000..3a60bfbb
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/SocialInteractionsPlayerListWidgetMixin.java
@@ -0,0 +1,24 @@
+package de.hysky.skyblocker.mixin;
+
+import java.util.Map;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.client.gui.screen.multiplayer.SocialInteractionsPlayerListEntry;
+import net.minecraft.client.gui.screen.multiplayer.SocialInteractionsPlayerListWidget;
+
+@Mixin(SocialInteractionsPlayerListWidget.class)
+public class SocialInteractionsPlayerListWidgetMixin {
+
+ @WrapOperation(method = "setPlayers", at = @At(value = "INVOKE", target = "Ljava/util/Map;put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", remap = false))
+ private Object skyblocker$hideInvalidPlayers(Map<Object, Object> map, Object uuid, Object entry, Operation<Object> operation) {
+ if (Utils.isOnSkyblock() && !((SocialInteractionsPlayerListEntry) entry).getName().matches("[A-Za-z0-9_]+")) return null;
+
+ return operation.call(map, uuid, entry);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/WorldRendererMixin.java b/src/main/java/de/hysky/skyblocker/mixin/WorldRendererMixin.java
new file mode 100644
index 00000000..e723c998
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/WorldRendererMixin.java
@@ -0,0 +1,33 @@
+package de.hysky.skyblocker.mixin;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import com.llamalad7.mixinextras.sugar.Local;
+import com.llamalad7.mixinextras.sugar.Share;
+import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.dungeon.StarredMobGlow;
+import net.minecraft.client.render.WorldRenderer;
+import net.minecraft.entity.Entity;
+
+@Mixin(WorldRenderer.class)
+public class WorldRendererMixin {
+
+ @ModifyExpressionValue(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;hasOutline(Lnet/minecraft/entity/Entity;)Z"))
+ private boolean skyblocker$shouldStarredMobGlow(boolean original, @Local Entity entity, @Share("isGlowingStarredMob") LocalBooleanRef isGlowingStarredMob) {
+ boolean isAStarredMobThatShouldGlow = SkyblockerConfigManager.get().locations.dungeons.starredMobGlow && StarredMobGlow.shouldMobGlow(entity);
+
+ isGlowingStarredMob.set(isAStarredMobThatShouldGlow);
+
+ return original || isAStarredMobThatShouldGlow;
+ }
+
+ @ModifyVariable(method = "render", at = @At("STORE"), ordinal = 0)
+ private int skyblocker$modifyGlowColor(int color, @Local Entity entity, @Share("isGlowingStarredMob") LocalBooleanRef isGlowingStarredMob) {
+ return isGlowingStarredMob.get() ? StarredMobGlow.getGlowColor(entity) : color;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/YggdrasilMinecraftSessionServiceMixin.java b/src/main/java/de/hysky/skyblocker/mixin/YggdrasilMinecraftSessionServiceMixin.java
new file mode 100644
index 00000000..8da87be0
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/YggdrasilMinecraftSessionServiceMixin.java
@@ -0,0 +1,20 @@
+package de.hysky.skyblocker.mixin;
+
+import org.slf4j.Logger;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService;
+
+import de.hysky.skyblocker.utils.Utils;
+
+@Mixin(value = YggdrasilMinecraftSessionService.class, remap = false)
+public class YggdrasilMinecraftSessionServiceMixin {
+
+ @WrapOperation(method = "getSecurePropertyValue", remap = false, at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;error(Ljava/lang/String;Ljava/lang/Object;)V", remap = false))
+ private void skyblocker$dontLogMissingSignaturesOrTamperedProperties(Logger logger, String message, Object property, Operation<Void> operation) {
+ if (!Utils.isOnHypixel()) operation.call(logger, message, property);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/YggdrasilServicesKeyInfoMixin.java b/src/main/java/de/hysky/skyblocker/mixin/YggdrasilServicesKeyInfoMixin.java
new file mode 100644
index 00000000..d38e40cc
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/YggdrasilServicesKeyInfoMixin.java
@@ -0,0 +1,59 @@
+package de.hysky.skyblocker.mixin;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import com.mojang.authlib.yggdrasil.YggdrasilServicesKeyInfo;
+
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
+import de.hysky.skyblocker.utils.Utils;
+import org.slf4j.Logger;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+
+import java.util.Base64;
+import java.util.Map;
+
+@Mixin(value = YggdrasilServicesKeyInfo.class, remap = false)
+public class YggdrasilServicesKeyInfoMixin {
+ @Shadow
+ @Final
+ private static Logger LOGGER;
+ @Unique
+ private static final Map<String, String> REPLACEMENT_MAP = Map.of();
+ @Unique
+ private static final IntList ERRONEUS_SIGNATURE_HASHES = new IntArrayList();
+
+ @WrapOperation(method = "validateProperty", at = @At(value = "INVOKE", target = "Ljava/util/Base64$Decoder;decode(Ljava/lang/String;)[B", remap = false), remap = false)
+ private byte[] skyblocker$replaceKnownWrongBase64(Base64.Decoder decoder, String signature, Operation<byte[]> decode) {
+ try {
+ return decode.call(decoder, signature);
+ } catch (IllegalArgumentException e) {
+ try {
+ return decode.call(decoder, signature.replaceAll("[^A-Za-z0-9+/=]", ""));
+ } catch (IllegalArgumentException e2) {
+ if (Utils.isOnSkyblock()) {
+ if (REPLACEMENT_MAP.containsKey(signature)) {
+ return decode.call(decoder, REPLACEMENT_MAP.get(signature));
+ }
+ int signatureHashCode = signature.hashCode();
+ if (!ERRONEUS_SIGNATURE_HASHES.contains(signatureHashCode)) {
+ ERRONEUS_SIGNATURE_HASHES.add(signatureHashCode);
+ LOGGER.warn("[Skyblocker Base64 Fixer] Failed to decode base64 string No.{}: {}", ERRONEUS_SIGNATURE_HASHES.size() - 1, signature);
+ } else {
+ LOGGER.warn("[Skyblocker Base64 Fixer] Failed to decode the base64 string No.{} again", ERRONEUS_SIGNATURE_HASHES.indexOf(signatureHashCode));
+ }
+ }
+ }
+ throw e;
+ }
+ }
+
+ @WrapOperation(method = "validateProperty", remap = false, at = @At(value = "INVOKE", target = "org/slf4j/Logger.error(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V", remap = false))
+ private void skyblocker$dontLogFailedSignatureValidation(Logger logger, String message, Object property, Object exception, Operation<Void> operation) {
+ if (!Utils.isOnHypixel()) operation.call(logger, message, property, exception);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/mixin/accessor/BeaconBlockEntityRendererInvoker.java b/src/main/java/de/hysky/skyblocker/mixin/accessor/BeaconBlockEntityRendererInvoker.java
new file mode 100644
index 00000000..0b607fce
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/accessor/BeaconBlockEntityRendererInvoker.java
@@ -0,0 +1,16 @@
+package de.hysky.skyblocker.mixin.accessor;
+
+import net.minecraft.client.render.VertexConsumerProvider;
+import net.minecraft.client.render.block.entity.BeaconBlockEntityRenderer;
+import net.minecraft.client.util.math.MatrixStack;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Invoker;
+
+@Mixin(BeaconBlockEntityRenderer.class)
+public interface BeaconBlockEntityRendererInvoker {
+ @SuppressWarnings("unused")
+ @Invoker("renderBeam")
+ static void renderBeam(MatrixStack matrices, VertexConsumerProvider vertexConsumers, float tickDelta, long worldTime, int yOffset, int maxY, float[] color) {
+ throw new IllegalStateException("Mixin invoker failed to apply.");
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/accessor/DrawContextInvoker.java b/src/main/java/de/hysky/skyblocker/mixin/accessor/DrawContextInvoker.java
new file mode 100644
index 00000000..8dcccf34
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/accessor/DrawContextInvoker.java
@@ -0,0 +1,17 @@
+package de.hysky.skyblocker.mixin.accessor;
+
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.tooltip.TooltipComponent;
+import net.minecraft.client.gui.tooltip.TooltipPositioner;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Invoker;
+
+import java.util.List;
+
+@Mixin(DrawContext.class)
+public interface DrawContextInvoker {
+
+ @Invoker
+ void invokeDrawTooltip(TextRenderer textRenderer, List<TooltipComponent> components, int x, int y, TooltipPositioner positioner);
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/accessor/FrustumInvoker.java b/src/main/java/de/hysky/skyblocker/mixin/accessor/FrustumInvoker.java
new file mode 100644
index 00000000..3a9e688b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/accessor/FrustumInvoker.java
@@ -0,0 +1,15 @@
+package de.hysky.skyblocker.mixin.accessor;
+
+import de.hysky.skyblocker.utils.render.FrustumUtils;
+import net.minecraft.client.render.Frustum;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Invoker;
+
+/**
+ * Use {@link FrustumUtils#isVisible(double, double, double, double, double, double) FrustumUtils#isVisible} which is shorter. For the purpose of avoiding object allocations!
+ */
+@Mixin(Frustum.class)
+public interface FrustumInvoker {
+ @Invoker
+ boolean invokeIsVisible(double minX, double minY, double minZ, double maxX, double maxY, double maxZ);
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/accessor/HandledScreenAccessor.java b/src/main/java/de/hysky/skyblocker/mixin/accessor/HandledScreenAccessor.java
new file mode 100644
index 00000000..d82422cb
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/accessor/HandledScreenAccessor.java
@@ -0,0 +1,20 @@
+package de.hysky.skyblocker.mixin.accessor;
+
+import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(HandledScreen.class)
+public interface HandledScreenAccessor {
+ @Accessor("x")
+ int getX();
+
+ @Accessor("y")
+ int getY();
+
+ @Accessor
+ int getBackgroundWidth();
+
+ @Accessor
+ int getBackgroundHeight();
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/accessor/PlayerListHudAccessor.java b/src/main/java/de/hysky/skyblocker/mixin/accessor/PlayerListHudAccessor.java
new file mode 100644
index 00000000..d82c568f
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/accessor/PlayerListHudAccessor.java
@@ -0,0 +1,17 @@
+package de.hysky.skyblocker.mixin.accessor;
+
+import net.minecraft.client.gui.hud.PlayerListHud;
+import net.minecraft.client.network.PlayerListEntry;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import java.util.Comparator;
+
+@Mixin(PlayerListHud.class)
+public interface PlayerListHudAccessor {
+
+ @Accessor("ENTRY_ORDERING")
+ static Comparator<PlayerListEntry> getOrdering() {
+ throw new AssertionError();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/accessor/RecipeBookWidgetAccessor.java b/src/main/java/de/hysky/skyblocker/mixin/accessor/RecipeBookWidgetAccessor.java
new file mode 100644
index 00000000..aecdf9b7
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/accessor/RecipeBookWidgetAccessor.java
@@ -0,0 +1,14 @@
+package de.hysky.skyblocker.mixin.accessor;
+
+import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget;
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(RecipeBookWidget.class)
+public interface RecipeBookWidgetAccessor {
+ @Accessor
+ String getSearchText();
+ @Accessor
+ TextFieldWidget getSearchField();
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/accessor/ScreenAccessor.java b/src/main/java/de/hysky/skyblocker/mixin/accessor/ScreenAccessor.java
new file mode 100644
index 00000000..c0196e5f
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/accessor/ScreenAccessor.java
@@ -0,0 +1,14 @@
+package de.hysky.skyblocker.mixin.accessor;
+
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.text.Text;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Mutable;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(Screen.class)
+public interface ScreenAccessor {
+ @Accessor
+ @Mutable
+ void setTitle(Text title);
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/accessor/WorldRendererAccessor.java b/src/main/java/de/hysky/skyblocker/mixin/accessor/WorldRendererAccessor.java
new file mode 100644
index 00000000..f1b3158d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/mixin/accessor/WorldRendererAccessor.java
@@ -0,0 +1,13 @@
+package de.hysky.skyblocker.mixin.accessor;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import net.minecraft.client.render.Frustum;
+import net.minecraft.client.render.WorldRenderer;
+
+@Mixin(WorldRenderer.class)
+public interface WorldRendererAccessor {
+ @Accessor
+ Frustum getFrustum();
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/FairySouls.java b/src/main/java/de/hysky/skyblocker/skyblock/FairySouls.java
new file mode 100644
index 00000000..24465e06
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/FairySouls.java
@@ -0,0 +1,215 @@
+package de.hysky.skyblocker.skyblock;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+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.utils.NEURepo;
+import de.hysky.skyblocker.utils.PosUtils;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+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.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.command.CommandRegistryAccess;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.text.Text;
+import net.minecraft.util.DyeColor;
+import net.minecraft.util.math.BlockPos;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
+
+public class FairySouls {
+ private static final Logger LOGGER = LoggerFactory.getLogger(FairySouls.class);
+ private static CompletableFuture<Void> fairySoulsLoaded;
+ private static int maxSouls = 0;
+ private static final Map<String, Set<BlockPos>> fairySouls = new HashMap<>();
+ private static final Map<String, Map<String, Set<BlockPos>>> foundFairies = new HashMap<>();
+
+ @SuppressWarnings("UnusedReturnValue")
+ public static CompletableFuture<Void> runAsyncAfterFairySoulsLoad(Runnable runnable) {
+ if (fairySoulsLoaded == null) {
+ LOGGER.error("Fairy Souls have not being initialized yet! Please ensure the Fairy Souls module is initialized before modules calling this method in SkyblockerMod#onInitializeClient. This error can be safely ignore in a test environment.");
+ return CompletableFuture.completedFuture(null);
+ }
+ return fairySoulsLoaded.thenRunAsync(runnable);
+ }
+
+ public static int getFairySoulsSize(@Nullable String location) {
+ return location == null ? maxSouls : fairySouls.get(location).size();
+ }
+
+ public static void init() {
+ loadFairySouls();
+ ClientLifecycleEvents.CLIENT_STOPPING.register(FairySouls::saveFoundFairySouls);
+ ClientCommandRegistrationCallback.EVENT.register(FairySouls::registerCommands);
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(FairySouls::render);
+ ClientReceiveMessageEvents.GAME.register(FairySouls::onChatMessage);
+ }
+
+ private static void loadFairySouls() {
+ fairySoulsLoaded = NEURepo.runAsyncAfterLoad(() -> {
+ try (BufferedReader reader = new BufferedReader(new FileReader(NEURepo.LOCAL_REPO_DIR.resolve("constants").resolve("fairy_souls.json").toFile()))) {
+ for (Map.Entry<String, JsonElement> fairySoulJson : JsonParser.parseReader(reader).getAsJsonObject().asMap().entrySet()) {
+ if (fairySoulJson.getKey().equals("//") || fairySoulJson.getKey().equals("Max Souls")) {
+ if (fairySoulJson.getKey().equals("Max Souls")) {
+ maxSouls = fairySoulJson.getValue().getAsInt();
+ }
+ continue;
+ }
+ ImmutableSet.Builder<BlockPos> fairySoulsForLocation = ImmutableSet.builder();
+ for (JsonElement fairySoul : fairySoulJson.getValue().getAsJsonArray().asList()) {
+ fairySoulsForLocation.add(PosUtils.parsePosString(fairySoul.getAsString()));
+ }
+ fairySouls.put(fairySoulJson.getKey(), fairySoulsForLocation.build());
+ }
+ LOGGER.debug("[Skyblocker] Loaded fairy soul locations");
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to load fairy soul locations", e);
+ }
+
+ try (BufferedReader reader = new BufferedReader(new FileReader(SkyblockerMod.CONFIG_DIR.resolve("found_fairy_souls.json").toFile()))) {
+ for (Map.Entry<String, JsonElement> foundFairiesForProfileJson : JsonParser.parseReader(reader).getAsJsonObject().asMap().entrySet()) {
+ Map<String, Set<BlockPos>> foundFairiesForProfile = new HashMap<>();
+ for (Map.Entry<String, JsonElement> foundFairiesForLocationJson : foundFairiesForProfileJson.getValue().getAsJsonObject().asMap().entrySet()) {
+ Set<BlockPos> foundFairiesForLocation = new HashSet<>();
+ for (JsonElement foundFairy : foundFairiesForLocationJson.getValue().getAsJsonArray().asList()) {
+ foundFairiesForLocation.add(PosUtils.parsePosString(foundFairy.getAsString()));
+ }
+ foundFairiesForProfile.put(foundFairiesForLocationJson.getKey(), foundFairiesForLocation);
+ }
+ foundFairies.put(foundFairiesForProfileJson.getKey(), foundFairiesForProfile);
+ }
+ LOGGER.debug("[Skyblocker] Loaded found fairy souls");
+ } catch (FileNotFoundException ignored) {
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to load found fairy souls", e);
+ }
+ });
+ }
+
+ private static void saveFoundFairySouls(MinecraftClient client) {
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(SkyblockerMod.CONFIG_DIR.resolve("found_fairy_souls.json").toFile()))) {
+ JsonObject foundFairiesJson = new JsonObject();
+ for (Map.Entry<String, Map<String, Set<BlockPos>>> foundFairiesForProfile : foundFairies.entrySet()) {
+ JsonObject foundFairiesForProfileJson = new JsonObject();
+ for (Map.Entry<String, Set<BlockPos>> foundFairiesForLocation : foundFairiesForProfile.getValue().entrySet()) {
+ JsonArray foundFairiesForLocationJson = new JsonArray();
+ for (BlockPos foundFairy : foundFairiesForLocation.getValue()) {
+ foundFairiesForLocationJson.add(PosUtils.getPosString(foundFairy));
+ }
+ foundFairiesForProfileJson.add(foundFairiesForLocation.getKey(), foundFairiesForLocationJson);
+ }
+ foundFairiesJson.add(foundFairiesForProfile.getKey(), foundFairiesForProfileJson);
+ }
+ SkyblockerMod.GSON.toJson(foundFairiesJson, writer);
+ writer.close();
+ LOGGER.info("[Skyblocker] Saved found fairy souls");
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to write found fairy souls to file", e);
+ }
+ }
+
+ private static void registerCommands(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+ dispatcher.register(literal(SkyblockerMod.NAMESPACE)
+ .then(literal("fairySouls")
+ .then(literal("markAllInCurrentIslandFound").executes(context -> {
+ FairySouls.markAllFairiesOnCurrentIslandFound();
+ context.getSource().sendFeedback(Text.translatable("skyblocker.fairySouls.markAllFound"));
+ return 1;
+ }))
+ .then(literal("markAllInCurrentIslandMissing").executes(context -> {
+ FairySouls.markAllFairiesOnCurrentIslandMissing();
+ context.getSource().sendFeedback(Text.translatable("skyblocker.fairySouls.markAllMissing"));
+ return 1;
+ }))));
+ }
+
+ private static void render(WorldRenderContext context) {
+ SkyblockerConfig.FairySouls fairySoulsConfig = SkyblockerConfigManager.get().general.fairySouls;
+
+ if (fairySoulsConfig.enableFairySoulsHelper && fairySoulsLoaded.isDone() && fairySouls.containsKey(Utils.getLocationRaw())) {
+ for (BlockPos fairySoulPos : fairySouls.get(Utils.getLocationRaw())) {
+ boolean fairySoulNotFound = isFairySoulMissing(fairySoulPos);
+ if (!fairySoulsConfig.highlightFoundSouls && !fairySoulNotFound || fairySoulsConfig.highlightOnlyNearbySouls && fairySoulPos.getSquaredDistance(context.camera().getPos()) > 2500) {
+ continue;
+ }
+ float[] colorComponents = fairySoulNotFound ? DyeColor.GREEN.getColorComponents() : DyeColor.RED.getColorComponents();
+ RenderHelper.renderFilledThroughWallsWithBeaconBeam(context, fairySoulPos, colorComponents, 0.5F);
+ }
+ }
+ }
+
+ private static void onChatMessage(Text text, boolean overlay) {
+ String message = text.getString();
+ if (message.equals("You have already found that Fairy Soul!") || message.equals("§d§lSOUL! §fYou found a §dFairy Soul§f!")) {
+ markClosestFairyFound();
+ }
+ }
+
+ private static void markClosestFairyFound() {
+ if (!fairySoulsLoaded.isDone()) return;
+ PlayerEntity player = MinecraftClient.getInstance().player;
+ if (player == null) {
+ LOGGER.warn("[Skyblocker] Failed to mark closest fairy soul as found because player is null");
+ return;
+ }
+ fairySouls.get(Utils.getLocationRaw()).stream()
+ .filter(FairySouls::isFairySoulMissing)
+ .min(Comparator.comparingDouble(fairySoulPos -> fairySoulPos.getSquaredDistance(player.getPos())))
+ .filter(fairySoulPos -> fairySoulPos.getSquaredDistance(player.getPos()) <= 16)
+ .ifPresent(fairySoulPos -> {
+ initializeFoundFairiesForCurrentProfileAndLocation();
+ foundFairies.get(Utils.getProfile()).get(Utils.getLocationRaw()).add(fairySoulPos);
+ });
+ }
+
+ private static boolean isFairySoulMissing(BlockPos fairySoulPos) {
+ Map<String, Set<BlockPos>> foundFairiesForProfile = foundFairies.get(Utils.getProfile());
+ if (foundFairiesForProfile == null) {
+ return true;
+ }
+ Set<BlockPos> foundFairiesForProfileAndLocation = foundFairiesForProfile.get(Utils.getLocationRaw());
+ if (foundFairiesForProfileAndLocation == null) {
+ return true;
+ }
+ return !foundFairiesForProfileAndLocation.contains(fairySoulPos);
+ }
+
+ public static void markAllFairiesOnCurrentIslandFound() {
+ initializeFoundFairiesForCurrentProfileAndLocation();
+ foundFairies.get(Utils.getProfile()).get(Utils.getLocationRaw()).addAll(fairySouls.get(Utils.getLocationRaw()));
+ }
+
+ public static void markAllFairiesOnCurrentIslandMissing() {
+ Map<String, Set<BlockPos>> foundFairiesForProfile = foundFairies.get(Utils.getProfile());
+ if (foundFairiesForProfile != null) {
+ foundFairiesForProfile.remove(Utils.getLocationRaw());
+ }
+ }
+
+ private static void initializeFoundFairiesForCurrentProfileAndLocation() {
+ initializeFoundFairiesForProfileAndLocation(Utils.getProfile(), Utils.getLocationRaw());
+ }
+
+ private static void initializeFoundFairiesForProfileAndLocation(String profile, String location) {
+ foundFairies.computeIfAbsent(profile, profileKey -> new HashMap<>());
+ foundFairies.get(profile).computeIfAbsent(location, locationKey -> new HashSet<>());
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/FancyStatusBars.java b/src/main/java/de/hysky/skyblocker/skyblock/FancyStatusBars.java
new file mode 100644
index 00000000..4cd356a8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/FancyStatusBars.java
@@ -0,0 +1,192 @@
+package de.hysky.skyblocker.skyblock;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+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.util.Identifier;
+
+public class FancyStatusBars {
+ private static final Identifier BARS = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/bars.png");
+
+ private final MinecraftClient client = MinecraftClient.getInstance();
+ private final StatusBarTracker statusBarTracker = SkyblockerMod.getInstance().statusBarTracker;
+
+ private final StatusBar[] bars = new StatusBar[]{
+ new StatusBar(0, 16733525, 2), // Health Bar
+ new StatusBar(1, 5636095, 2), // Intelligence Bar
+ new StatusBar(2, 12106180, 1), // Defence Bar
+ new StatusBar(3, 8453920, 1), // Experience Bar
+ };
+
+ // Positions to show the bars
+ // 0: Hotbar Layer 1, 1: Hotbar Layer 2, 2: Right of hotbar
+ // Anything outside the set values hides the bar
+ private final int[] anchorsX = new int[3];
+ private final int[] anchorsY = new int[3];
+
+ public FancyStatusBars() {
+ moveBar(0, 0);
+ moveBar(1, 0);
+ moveBar(2, 0);
+ moveBar(3, 0);
+ }
+
+ private int fill(int value, int max) {
+ return (100 * value) / max;
+ }
+
+ public boolean render(DrawContext context, int scaledWidth, int scaledHeight) {
+ var player = client.player;
+ if (!SkyblockerConfigManager.get().general.bars.enableBars || player == null || Utils.isInTheRift())
+ return false;
+ anchorsX[0] = scaledWidth / 2 - 91;
+ anchorsY[0] = scaledHeight - 33;
+ anchorsX[1] = anchorsX[0];
+ anchorsY[1] = anchorsY[0] - 10;
+ anchorsX[2] = (scaledWidth / 2 + 91) + 2;
+ anchorsY[2] = scaledHeight - 16;
+
+ bars[0].update(statusBarTracker.getHealth());
+ bars[1].update(statusBarTracker.getMana());
+ int def = statusBarTracker.getDefense();
+ bars[2].fill[0] = fill(def, def + 100);
+ bars[2].text = def;
+ bars[3].fill[0] = (int) (100 * player.experienceProgress);
+ bars[3].text = player.experienceLevel;
+
+ // Update positions of bars from config
+ for (int i = 0; i < 4; i++) {
+ int configAnchorNum = switch (i) {
+ case 0 -> SkyblockerConfigManager.get().general.bars.barPositions.healthBarPosition.toInt();
+ case 1 -> SkyblockerConfigManager.get().general.bars.barPositions.manaBarPosition.toInt();
+ case 2 -> SkyblockerConfigManager.get().general.bars.barPositions.defenceBarPosition.toInt();
+ case 3 -> SkyblockerConfigManager.get().general.bars.barPositions.experienceBarPosition.toInt();
+ default -> 0;
+ };
+
+ if (bars[i].anchorNum != configAnchorNum)
+ moveBar(i, configAnchorNum);
+ }
+
+ for (var bar : bars) {
+ bar.draw(context);
+ }
+ for (var bar : bars) {
+ bar.drawText(context);
+ }
+ return true;
+ }
+
+ public void moveBar(int bar, int location) {
+ // Set the bar to the new anchor
+ bars[bar].anchorNum = location;
+
+ // Count how many bars are in each location
+ int layer1Count = 0, layer2Count = 0, rightCount = 0;
+ for (int i = 0; i < 4; i++) {
+ switch (bars[i].anchorNum) {
+ case 0 -> layer1Count++;
+ case 1 -> layer2Count++;
+ case 2 -> rightCount++;
+ }
+ }
+
+ // Set the bars width and offsetX according to their anchor and how many bars are on that layer
+ int adjustedLayer1Count = 0, adjustedLayer2Count = 0, adjustedRightCount = 0;
+ for (int i = 0; i < 4; i++) {
+ switch (bars[i].anchorNum) {
+ case 0 -> {
+ bars[i].bar_width = (172 - ((layer1Count - 1) * 11)) / layer1Count;
+ bars[i].offsetX = adjustedLayer1Count * (bars[i].bar_width + 11 + (layer1Count == 3 ? 0 : 1));
+ adjustedLayer1Count++;
+ }
+ case 1 -> {
+ bars[i].bar_width = (172 - ((layer2Count - 1) * 11)) / layer2Count;
+ bars[i].offsetX = adjustedLayer2Count * (bars[i].bar_width + 11 + (layer2Count == 3 ? 0 : 1));
+ adjustedLayer2Count++;
+ }
+ case 2 -> {
+ bars[i].bar_width = 50;
+ bars[i].offsetX = adjustedRightCount * (50 + 11);
+ adjustedRightCount++;
+ }
+ }
+ }
+ }
+
+ private class StatusBar {
+ public final int[] fill;
+ public int offsetX;
+ private final int v;
+ private final int text_color;
+ public int anchorNum;
+ public int bar_width;
+ public Object text;
+
+ private StatusBar(int i, int textColor, int fillNum) {
+ this.v = i * 9;
+ this.text_color = textColor;
+ this.fill = new int[fillNum];
+ this.fill[0] = 100;
+ this.anchorNum = 0;
+ this.text = "";
+ }
+
+ public void update(StatusBarTracker.Resource resource) {
+ int max = resource.max();
+ int val = resource.value();
+ this.fill[0] = fill(val, max);
+ this.fill[1] = fill(resource.overflow(), max);
+ this.text = val;
+ }
+
+ public void draw(DrawContext context) {
+ // Dont draw if anchorNum is outside of range
+ if (anchorNum < 0 || anchorNum > 2) return;
+
+ // Draw the icon for the bar
+ context.drawTexture(BARS, anchorsX[anchorNum] + offsetX, anchorsY[anchorNum], 0, v, 9, 9);
+
+ // Draw the background for the bar
+ context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 10, anchorsY[anchorNum], 10, v, 2, 9);
+ for (int i = 2; i < bar_width - 2; i += 58) {
+ context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 10 + i, anchorsY[anchorNum], 12, v, Math.min(58, bar_width - 2 - i), 9);
+ }
+ context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 10 + bar_width - 2, anchorsY[anchorNum], 70, v, 2, 9);
+
+ // Draw the filled part of the bar
+ for (int i = 0; i < fill.length; i++) {
+ int fill_width = this.fill[i] * (bar_width - 2) / 100;
+ if (fill_width >= 1) {
+ context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 11, anchorsY[anchorNum], 72 + i * 60, v, 1, 9);
+ }
+ for (int j = 1; j < fill_width - 1; j += 58) {
+ context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 11 + j, anchorsY[anchorNum], 73 + i * 60, v, Math.min(58, fill_width - 1 - j), 9);
+ }
+ if (fill_width == bar_width - 2) {
+ context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 11 + fill_width - 1, anchorsY[anchorNum], 131 + i * 60, v, 1, 9);
+ }
+ }
+ }
+
+ public void drawText(DrawContext context) {
+ // Dont draw if anchorNum is outside of range
+ if (anchorNum < 0 || anchorNum > 2) return;
+
+ TextRenderer textRenderer = client.textRenderer;
+ String text = this.text.toString();
+ int x = anchorsX[anchorNum] + this.offsetX + 11 + (bar_width - textRenderer.getWidth(text)) / 2;
+ int y = anchorsY[anchorNum] - 3;
+
+ final int[] offsets = new int[]{-1, 1};
+ for (int i : offsets) {
+ context.drawText(textRenderer, text, x + i, y, 0, false);
+ context.drawText(textRenderer, text, x, y + i, 0, false);
+ }
+ context.drawText(textRenderer, text, x, y, text_color, false);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/FishingHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/FishingHelper.java
new file mode 100644
index 00000000..6edb416e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/FishingHelper.java
@@ -0,0 +1,62 @@
+package de.hysky.skyblocker.skyblock;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import de.hysky.skyblocker.utils.render.title.Title;
+import net.fabricmc.fabric.api.event.player.UseItemCallback;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.item.FishingRodItem;
+import net.minecraft.item.ItemStack;
+import net.minecraft.network.packet.s2c.play.PlaySoundS2CPacket;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.TypedActionResult;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.math.Vec3d;
+
+public class FishingHelper {
+ private static final Title title = new Title("skyblocker.fishing.reelNow", Formatting.GREEN);
+ private static long startTime;
+ private static Vec3d normalYawVector;
+
+ public static void init() {
+ UseItemCallback.EVENT.register((player, world, hand) -> {
+ ItemStack stack = player.getStackInHand(hand);
+ if (stack.getItem() instanceof FishingRodItem) {
+ if (player.fishHook == null) {
+ start(player);
+ } else {
+ reset();
+ }
+ }
+ return TypedActionResult.pass(stack);
+ });
+ }
+
+ public static void start(PlayerEntity player) {
+ startTime = System.currentTimeMillis();
+ float yawRad = player.getYaw() * 0.017453292F;
+ normalYawVector = new Vec3d(-MathHelper.sin(yawRad), 0, MathHelper.cos(yawRad));
+ }
+
+ public static void reset() {
+ startTime = 0;
+ }
+
+ public static void onSound(PlaySoundS2CPacket packet) {
+ String path = packet.getSound().value().getId().getPath();
+ if (SkyblockerConfigManager.get().general.fishing.enableFishingHelper && startTime != 0 && System.currentTimeMillis() >= startTime + 2000 && ("entity.generic.splash".equals(path) || "entity.player.splash".equals(path))) {
+ ClientPlayerEntity player = MinecraftClient.getInstance().player;
+ if (player != null && player.fishHook != null) {
+ Vec3d soundToFishHook = player.fishHook.getPos().subtract(packet.getX(), 0, packet.getZ());
+ if (Math.abs(normalYawVector.x * soundToFishHook.z - normalYawVector.z * soundToFishHook.x) < 0.2D && Math.abs(normalYawVector.dotProduct(soundToFishHook)) < 4D && player.getPos().squaredDistanceTo(packet.getX(), packet.getY(), packet.getZ()) > 1D) {
+ RenderHelper.displayInTitleContainerAndPlaySound(title, 10);
+ reset();
+ }
+ } else {
+ reset();
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/HotbarSlotLock.java b/src/main/java/de/hysky/skyblocker/skyblock/HotbarSlotLock.java
new file mode 100644
index 00000000..13f09ec6
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/HotbarSlotLock.java
@@ -0,0 +1,40 @@
+package de.hysky.skyblocker.skyblock;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.client.option.KeyBinding;
+import org.lwjgl.glfw.GLFW;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import java.util.List;
+
+public class HotbarSlotLock {
+ public static KeyBinding hotbarSlotLock;
+
+ public static void init() {
+ hotbarSlotLock = KeyBindingHelper.registerKeyBinding(new KeyBinding(
+ "key.hotbarSlotLock",
+ GLFW.GLFW_KEY_H,
+ "key.categories.skyblocker"
+ ));
+ }
+
+ public static boolean isLocked(int slot) {
+ return SkyblockerConfigManager.get().general.lockedSlots.contains(slot);
+ }
+
+ public static void handleDropSelectedItem(int slot, CallbackInfoReturnable<Boolean> cir) {
+ if (isLocked(slot)) cir.setReturnValue(false);
+ }
+
+ public static void handleInputEvents(ClientPlayerEntity player) {
+ while (hotbarSlotLock.wasPressed()) {
+ List<Integer> lockedSlots = SkyblockerConfigManager.get().general.lockedSlots;
+ int selected = player.getInventory().selectedSlot;
+ if (!isLocked(player.getInventory().selectedSlot)) lockedSlots.add(selected);
+ else lockedSlots.remove(Integer.valueOf(selected));
+ SkyblockerConfigManager.save();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/QuiverWarning.java b/src/main/java/de/hysky/skyblocker/skyblock/QuiverWarning.java
new file mode 100644
index 00000000..a6c45d21
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/QuiverWarning.java
@@ -0,0 +1,66 @@
+package de.hysky.skyblocker.skyblock;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.hud.InGameHud;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+public class QuiverWarning {
+ @Nullable
+ private static Type warning = null;
+
+ public static void init() {
+ ClientReceiveMessageEvents.ALLOW_GAME.register(QuiverWarning::onChatMessage);
+ Scheduler.INSTANCE.scheduleCyclic(QuiverWarning::update, 10);
+ }
+
+ public static boolean onChatMessage(Text text, boolean overlay) {
+ String message = text.getString();
+ if (SkyblockerConfigManager.get().general.quiverWarning.enableQuiverWarning && message.endsWith("left in your Quiver!")) {
+ MinecraftClient.getInstance().inGameHud.setDefaultTitleFade();
+ if (message.startsWith("You only have 50")) {
+ onChatMessage(Type.FIFTY_LEFT);
+ } else if (message.startsWith("You only have 10")) {
+ onChatMessage(Type.TEN_LEFT);
+ } else if (message.startsWith("You don't have any more")) {
+ onChatMessage(Type.EMPTY);
+ }
+ }
+ return true;
+ }
+
+ private static void onChatMessage(Type warning) {
+ if (!Utils.isInDungeons()) {
+ MinecraftClient.getInstance().inGameHud.setTitle(Text.translatable(warning.key).formatted(Formatting.RED));
+ } else if (SkyblockerConfigManager.get().general.quiverWarning.enableQuiverWarningInDungeons) {
+ MinecraftClient.getInstance().inGameHud.setTitle(Text.translatable(warning.key).formatted(Formatting.RED));
+ QuiverWarning.warning = warning;
+ }
+ }
+
+ public static void update() {
+ if (warning != null && SkyblockerConfigManager.get().general.quiverWarning.enableQuiverWarning && SkyblockerConfigManager.get().general.quiverWarning.enableQuiverWarningAfterDungeon && !Utils.isInDungeons()) {
+ InGameHud inGameHud = MinecraftClient.getInstance().inGameHud;
+ inGameHud.setDefaultTitleFade();
+ inGameHud.setTitle(Text.translatable(warning.key).formatted(Formatting.RED));
+ warning = null;
+ }
+ }
+
+ private enum Type {
+ NONE(""),
+ FIFTY_LEFT("50Left"),
+ TEN_LEFT("10Left"),
+ EMPTY("empty");
+ private final String key;
+
+ Type(String key) {
+ this.key = "skyblocker.quiverWarning." + key;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/StatusBarTracker.java b/src/main/java/de/hysky/skyblocker/skyblock/StatusBarTracker.java
new file mode 100644
index 00000000..c3483102
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/StatusBarTracker.java
@@ -0,0 +1,109 @@
+package de.hysky.skyblocker.skyblock;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.text.Text;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class StatusBarTracker {
+ private static final Pattern STATUS_HEALTH = Pattern.compile("§[6c](\\d+(,\\d\\d\\d)*)/(\\d+(,\\d\\d\\d)*)❤(?:(\\+§c(\\d+(,\\d\\d\\d)*). *)| *)");
+ private static final Pattern DEFENSE_STATUS = Pattern.compile("§a(\\d+(,\\d\\d\\d)*)§a❈ Defense *");
+ private static final Pattern MANA_USE = Pattern.compile("§b-(\\d+(,\\d\\d\\d)*) Mana \\(§\\S+(?:\\s\\S+)* *");
+ private static final Pattern MANA_STATUS = Pattern.compile("§b(\\d+(,\\d\\d\\d)*)/(\\d+(,\\d\\d\\d)*)✎ (?:Mana|§3(\\d+(,\\d\\d\\d)*)ʬ) *");
+
+ private Resource health = new Resource(100, 100, 0);
+ private Resource mana = new Resource(100, 100, 0);
+ private int defense = 0;
+
+ public void init() {
+ ClientReceiveMessageEvents.MODIFY_GAME.register(this::onOverlayMessage);
+ }
+
+ public Resource getHealth() {
+ return this.health;
+ }
+
+ public Resource getMana() {
+ return this.mana;
+ }
+
+ public int getDefense() {
+ return this.defense;
+ }
+
+ private int parseInt(Matcher m, int group) {
+ return Integer.parseInt(m.group(group).replace(",", ""));
+ }
+
+ private void updateMana(Matcher m) {
+ int value = parseInt(m, 1);
+ int max = parseInt(m, 3);
+ int overflow = m.group(5) == null ? 0 : parseInt(m, 5);
+ this.mana = new Resource(value, max, overflow);
+ }
+
+ private void updateHealth(Matcher m) {
+ int value = parseInt(m, 1);
+ int max = parseInt(m, 3);
+ int overflow = Math.max(0, value - max);
+ if (MinecraftClient.getInstance() != null && MinecraftClient.getInstance().player != null) {
+ ClientPlayerEntity player = MinecraftClient.getInstance().player;
+ value = (int) (player.getHealth() * max / player.getMaxHealth());
+ overflow = (int) (player.getAbsorptionAmount() * max / player.getMaxHealth());
+ }
+ this.health = new Resource(Math.min(value, max), max, Math.min(overflow, max));
+ }
+
+ private String reset(String str, Matcher m) {
+ str = str.substring(m.end());
+ m.reset(str);
+ return str;
+ }
+
+ private Text onOverlayMessage(Text text, boolean overlay) {
+ if (!overlay || !Utils.isOnSkyblock() || !SkyblockerConfigManager.get().general.bars.enableBars || Utils.isInTheRift()) {
+ return text;
+ }
+ return Text.of(update(text.getString(), SkyblockerConfigManager.get().messages.hideMana));
+ }
+
+ public String update(String actionBar, boolean filterManaUse) {
+ var sb = new StringBuilder();
+ Matcher matcher = STATUS_HEALTH.matcher(actionBar);
+ if (!matcher.lookingAt())
+ return actionBar;
+ updateHealth(matcher);
+ if (matcher.group(5) != null) {
+ sb.append("§c❤");
+ sb.append(matcher.group(5));
+ }
+ actionBar = reset(actionBar, matcher);
+ if (matcher.usePattern(MANA_STATUS).lookingAt()) {
+ defense = 0;
+ updateMana(matcher);
+ actionBar = reset(actionBar, matcher);
+ } else {
+ if (matcher.usePattern(DEFENSE_STATUS).lookingAt()) {
+ defense = parseInt(matcher, 1);
+ actionBar = reset(actionBar, matcher);
+ } else if (filterManaUse && matcher.usePattern(MANA_USE).lookingAt()) {
+ actionBar = reset(actionBar, matcher);
+ }
+ if (matcher.usePattern(MANA_STATUS).find()) {
+ updateMana(matcher);
+ matcher.appendReplacement(sb, "");
+ }
+ }
+ matcher.appendTail(sb);
+ String res = sb.toString().trim();
+ return res.isEmpty() ? null : res;
+ }
+
+ public record Resource(int value, int max, int overflow) {
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java b/src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java
new file mode 100644
index 00000000..e878d108
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java
@@ -0,0 +1,114 @@
+package de.hysky.skyblocker.skyblock;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.skyblock.item.PriceInfoTooltip;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.hit.HitResult;
+import net.minecraft.util.math.BlockPos;
+
+public class TeleportOverlay {
+ private static final float[] COLOR_COMPONENTS = {118f / 255f, 21f / 255f, 148f / 255f};
+ private static final MinecraftClient client = MinecraftClient.getInstance();
+
+ public static void init() {
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(TeleportOverlay::render);
+ }
+
+ private static void render(WorldRenderContext wrc) {
+ if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().general.teleportOverlay.enableTeleportOverlays && client.player != null && client.world != null) {
+ ItemStack heldItem = client.player.getMainHandStack();
+ String itemId = PriceInfoTooltip.getInternalNameFromNBT(heldItem, true);
+ NbtCompound nbt = heldItem.getNbt();
+
+ if (itemId != null) {
+ switch (itemId) {
+ case "ASPECT_OF_THE_LEECH_1" -> {
+ if (SkyblockerConfigManager.get().general.teleportOverlay.enableWeirdTransmission) {
+ render(wrc, 3);
+ }
+ }
+ case "ASPECT_OF_THE_LEECH_2" -> {
+ if (SkyblockerConfigManager.get().general.teleportOverlay.enableWeirdTransmission) {
+ render(wrc, 4);
+ }
+ }
+ case "ASPECT_OF_THE_END", "ASPECT_OF_THE_VOID" -> {
+ if (SkyblockerConfigManager.get().general.teleportOverlay.enableEtherTransmission && client.options.sneakKey.isPressed() && nbt != null && nbt.getCompound("ExtraAttributes").getInt("ethermerge") == 1) {
+ render(wrc, nbt, 57);
+ } else if (SkyblockerConfigManager.get().general.teleportOverlay.enableInstantTransmission) {
+ render(wrc, nbt, 8);
+ }
+ }
+ case "ETHERWARP_CONDUIT" -> {
+ if (SkyblockerConfigManager.get().general.teleportOverlay.enableEtherTransmission) {
+ render(wrc, nbt, 57);
+ }
+ }
+ case "SINSEEKER_SCYTHE" -> {
+ if (SkyblockerConfigManager.get().general.teleportOverlay.enableSinrecallTransmission) {
+ render(wrc, nbt, 4);
+ }
+ }
+ case "NECRON_BLADE", "ASTRAEA", "HYPERION", "SCYLLA", "VALKYRIE" -> {
+ if (SkyblockerConfigManager.get().general.teleportOverlay.enableWitherImpact) {
+ render(wrc, 10);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Renders the teleport overlay with a given base range and the tuned transmission stat.
+ */
+ private static void render(WorldRenderContext wrc, NbtCompound nbt, int baseRange) {
+ render(wrc, nbt != null && nbt.getCompound("ExtraAttributes").contains("tuned_transmission") ? baseRange + nbt.getCompound("ExtraAttributes").getInt("tuned_transmission") : baseRange);
+ }
+
+ /**
+ * Renders the teleport overlay with a given range. Uses {@link MinecraftClient#crosshairTarget} if it is a block and within range. Otherwise, raycasts from the player with the given range.
+ *
+ * @implNote {@link MinecraftClient#player} and {@link MinecraftClient#world} must not be null when calling this method.
+ */
+ private static void render(WorldRenderContext wrc, int range) {
+ if (client.crosshairTarget != null && client.crosshairTarget.getType() == HitResult.Type.BLOCK && client.crosshairTarget instanceof BlockHitResult blockHitResult && client.crosshairTarget.squaredDistanceTo(client.player) < range * range) {
+ render(wrc, blockHitResult);
+ } else if (client.interactionManager != null && range > client.interactionManager.getReachDistance()) {
+ @SuppressWarnings("DataFlowIssue")
+ HitResult result = client.player.raycast(range, wrc.tickDelta(), false);
+ if (result.getType() == HitResult.Type.BLOCK && result instanceof BlockHitResult blockHitResult) {
+ render(wrc, blockHitResult);
+ }
+ }
+ }
+
+ /**
+ * Renders the teleport overlay at the given {@link BlockHitResult}.
+ *
+ * @implNote {@link MinecraftClient#world} must not be null when calling this method.
+ */
+ private static void render(WorldRenderContext wrc, BlockHitResult blockHitResult) {
+ BlockPos pos = blockHitResult.getBlockPos();
+ @SuppressWarnings("DataFlowIssue")
+ BlockState state = client.world.getBlockState(pos);
+ if (!state.isAir() && client.world.getBlockState(pos.up()).isAir() && client.world.getBlockState(pos.up(2)).isAir()) {
+ RenderSystem.polygonOffset(-1f, -10f);
+ RenderSystem.enablePolygonOffset();
+
+ RenderHelper.renderFilledIfVisible(wrc, pos, COLOR_COMPONENTS, 0.5f);
+
+ RenderSystem.polygonOffset(0f, 0f);
+ RenderSystem.disablePolygonOffset();
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/barn/HungryHiker.java b/src/main/java/de/hysky/skyblocker/skyblock/barn/HungryHiker.java
new file mode 100644
index 00000000..abb4a76d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/barn/HungryHiker.java
@@ -0,0 +1,47 @@
+package de.hysky.skyblocker.skyblock.barn;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+import de.hysky.skyblocker.utils.chat.ChatPatternListener;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.text.Text;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+
+public class HungryHiker extends ChatPatternListener {
+
+ private static final Map<String, String> foods;
+
+ public HungryHiker() { super("^§e\\[NPC] Hungry Hiker§f: (The food I want is|(I asked for) food that is) ([a-zA-Z, '\\-]*\\.)$"); }
+
+ @Override
+ public ChatFilterResult state() {
+ return SkyblockerConfigManager.get().locations.barn.solveHungryHiker ? ChatFilterResult.FILTER : ChatFilterResult.PASS;
+ }
+
+ @Override
+ public boolean onMatch(Text message, Matcher matcher) {
+ MinecraftClient client = MinecraftClient.getInstance();
+ if (client.player == null) return false;
+ String foodDescription = matcher.group(3);
+ String food = foods.get(foodDescription);
+ if (food == null) return false;
+ String middlePartOfTheMessageToSend = matcher.group(2) != null ? matcher.group(2) : matcher.group(1);
+ client.player.sendMessage(Text.of("§e[NPC] Hungry Hiker§f: " + middlePartOfTheMessageToSend + " " + food + "."), false);
+ return true;
+ }
+
+ static {
+ foods = new HashMap<>();
+ foods.put("from a cow.", Text.translatable("item.minecraft.cooked_beef").getString());
+ foods.put("meat from a fowl.", Text.translatable("item.minecraft.cooked_chicken").getString());
+ foods.put("red on the inside, green on the outside.", Text.translatable("item.minecraft.melon_slice").getString());
+ foods.put("a cooked potato.", Text.translatable("item.minecraft.baked_potato").getString());
+ foods.put("a stew.", Text.translatable("item.minecraft.rabbit_stew").getString());
+ foods.put("a grilled meat.", Text.translatable("item.minecraft.cooked_porkchop").getString());
+ foods.put("red and crunchy.", Text.translatable("item.minecraft.apple").getString());
+ foods.put("made of wheat.", Text.translatable("item.minecraft.bread").getString());
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/barn/TreasureHunter.java b/src/main/java/de/hysky/skyblocker/skyblock/barn/TreasureHunter.java
new file mode 100644
index 00000000..191014d5
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/barn/TreasureHunter.java
@@ -0,0 +1,61 @@
+package de.hysky.skyblocker.skyblock.barn;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+import de.hysky.skyblocker.utils.chat.ChatPatternListener;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.text.Text;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+
+public class TreasureHunter extends ChatPatternListener {
+
+ private static final Map<String, String> locations;
+
+ public TreasureHunter() { super("^§e\\[NPC] Treasure Hunter§f: ([a-zA-Z, '\\-\\.]*)$"); }
+
+ @Override
+ public ChatFilterResult state() {
+ return SkyblockerConfigManager.get().locations.barn.solveTreasureHunter ? ChatFilterResult.FILTER : ChatFilterResult.PASS;
+ }
+
+ @Override
+ public boolean onMatch(Text message, Matcher matcher) {
+ MinecraftClient client = MinecraftClient.getInstance();
+ if (client.player == null) return false;
+ String hint = matcher.group(1);
+ String location = locations.get(hint);
+ if (location == null) return false;
+ client.player.sendMessage(Text.of("§e[NPC] Treasure Hunter§f: Go mine around " + location + "."), false);
+ return true;
+ }
+
+ static {
+ locations = new HashMap<>();
+ locations.put("There's a treasure chest somewhere in a small cave in the gorge.", "258 70 -492");
+ locations.put("I was in the desert earlier, and I saw something near a red sand rock.", "357 82 -319");
+ locations.put("There's this guy who collects animals to experiment on, I think I saw something near his house.", "259 184 -564");
+ locations.put("There's a small house in the gorge, I saw some treasure near there.", "297 87 -562");
+ locations.put("There's this guy who says he has the best sheep in the world. I think I saw something around his hut.", "392 85 -372");
+ locations.put("I spotted something by an odd looking mushroom on one of the ledges in the Mushroom Gorge, you should check it out.", "305 73 -557");
+ locations.put("There are some small ruins out in the desert, might want to check them out.", "320 102 -471");
+ locations.put("Some dirt was kicked up by the water pool in the overgrown Mushroom Cave. Have a look over there.", "234 56 -410");
+ locations.put("There are some old stone structures in the Mushroom Gorge, give them a look.", "223 54 -503");
+ locations.put("In the Mushroom Gorge where blue meets the ceiling and floor, you will find what you are looking for.", "205 42 -527");
+ locations.put("There was a haystack with a crop greener than usual around it, I think there is something near there.", "334 82 -389");
+ locations.put("There's a single piece of tall grass growing in the desert, I saw something there.", "283 76 -363");
+ locations.put("I saw some treasure by a cow skull near the village.", "141 77 -397");
+ locations.put("Near a melon patch inside a tunnel in the mountain I spotted something.", "257 100 -569");
+ locations.put("I saw something near a farmer's cart, you should check it out.", "155 90 -591");
+ locations.put("I remember there was a stone pillar made only of cobblestone in the oasis, could be something there.", "122 66 -409");
+ locations.put("I thought I saw something near the smallest stone pillar in the oasis.", "94 65 -455");
+ locations.put("I found something by a mossy stone pillar in the oasis, you should take a look.", "179 93 -537");
+ locations.put("Down in the glowing Mushroom Cave, there was a weird looking mushroom, check it out.", "182 44 -451");
+ locations.put("Something caught my eye by the red sand near the bridge over the gorge.", "306 105 -489");
+ locations.put("I seem to recall seeing something near the well in the village.", "170 77 -375");
+ locations.put("I was down near the lower oasis yesterday, I think I saw something under the bridge.", "142 69 -448");
+ locations.put("I was at the upper oasis today, I recall seeing something on the cobblestone stepping stones.", "188 77 -459");
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusHelper.java
new file mode 100644
index 00000000..e95b47c9
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusHelper.java
@@ -0,0 +1,34 @@
+package de.hysky.skyblocker.skyblock.dungeon;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.render.gui.ColorHighlight;
+import de.hysky.skyblocker.utils.render.gui.ContainerSolver;
+import net.minecraft.item.ItemStack;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class CroesusHelper extends ContainerSolver {
+
+ public CroesusHelper() {
+ super("^Croesus$");
+ }
+
+ @Override
+ protected boolean isEnabled() {
+ return SkyblockerConfigManager.get().locations.dungeons.croesusHelper;
+ }
+
+ @Override
+ protected List<ColorHighlight> getColors(String[] groups, Map<Integer, ItemStack> slots) {
+ List<ColorHighlight> highlights = new ArrayList<>();
+ for (Map.Entry<Integer, ItemStack> entry : slots.entrySet()) {
+ ItemStack stack = entry.getValue();
+ if (stack != null && stack.getNbt() != null && (stack.getNbt().toString().contains("No more Chests to open!") || stack.getNbt().toString().contains("Opened Chest:"))) {
+ highlights.add(ColorHighlight.gray(entry.getKey()));
+ }
+ }
+ return highlights;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java
new file mode 100644
index 00000000..9f247668
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java
@@ -0,0 +1,152 @@
+package de.hysky.skyblocker.skyblock.dungeon;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import it.unimi.dsi.fastutil.objects.ObjectIntPair;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.decoration.ArmorStandEntity;
+import net.minecraft.predicate.entity.EntityPredicates;
+import net.minecraft.util.math.Box;
+import net.minecraft.util.math.Vec3d;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * This class provides functionality to render outlines around Blaze entities
+ */
+public class DungeonBlaze {
+ private static final Logger LOGGER = LoggerFactory.getLogger(DungeonBlaze.class.getName());
+ private static final float[] GREEN_COLOR_COMPONENTS = {0.0F, 1.0F, 0.0F};
+ private static final float[] WHITE_COLOR_COMPONENTS = {1.0f, 1.0f, 1.0f};
+
+ private static ArmorStandEntity highestBlaze = null;
+ private static ArmorStandEntity lowestBlaze = null;
+ private static ArmorStandEntity nextHighestBlaze = null;
+ private static ArmorStandEntity nextLowestBlaze = null;
+
+ public static void init() {
+ Scheduler.INSTANCE.scheduleCyclic(DungeonBlaze::update, 4);
+ WorldRenderEvents.BEFORE_DEBUG_RENDER.register(DungeonBlaze::blazeRenderer);
+ }
+
+ /**
+ * Updates the state of Blaze entities and triggers the rendering process if necessary.
+ */
+ public static void update() {
+ ClientWorld world = MinecraftClient.getInstance().world;
+ ClientPlayerEntity player = MinecraftClient.getInstance().player;
+ if (world == null || player == null || !Utils.isInDungeons()) return;
+ List<ObjectIntPair<ArmorStandEntity>> blazes = getBlazesInWorld(world, player);
+ sortBlazes(blazes);
+ updateBlazeEntities(blazes);
+ }
+
+ /**
+ * Retrieves Blaze entities in the world and parses their health information.
+ *
+ * @param world The client world to search for Blaze entities.
+ * @return A list of Blaze entities and their associated health.
+ */
+ private static List<ObjectIntPair<ArmorStandEntity>> getBlazesInWorld(ClientWorld world, ClientPlayerEntity player) {
+ List<ObjectIntPair<ArmorStandEntity>> blazes = new ArrayList<>();
+ for (ArmorStandEntity blaze : world.getEntitiesByClass(ArmorStandEntity.class, player.getBoundingBox().expand(500D), EntityPredicates.NOT_MOUNTED)) {
+ String blazeName = blaze.getName().getString();
+ if (blazeName.contains("Blaze") && blazeName.contains("/")) {
+ try {
+ int health = Integer.parseInt(blazeName.substring(blazeName.indexOf("/") + 1, blazeName.length() - 1));
+ blazes.add(ObjectIntPair.of(blaze, health));
+ } catch (NumberFormatException e) {
+ handleException(e);
+ }
+ }
+ }
+ return blazes;
+ }
+
+ /**
+ * Sorts the Blaze entities based on their health values.
+ *
+ * @param blazes The list of Blaze entities to be sorted.
+ */
+ private static void sortBlazes(List<ObjectIntPair<ArmorStandEntity>> blazes) {
+ blazes.sort(Comparator.comparingInt(ObjectIntPair::rightInt));
+ }
+
+ /**
+ * Updates information about Blaze entities based on sorted list.
+ *
+ * @param blazes The sorted list of Blaze entities with associated health values.
+ */
+ private static void updateBlazeEntities(List<ObjectIntPair<ArmorStandEntity>> blazes) {
+ if (!blazes.isEmpty()) {
+ lowestBlaze = blazes.get(0).left();
+ int highestIndex = blazes.size() - 1;
+ highestBlaze = blazes.get(highestIndex).left();
+ if (blazes.size() > 1) {
+ nextLowestBlaze = blazes.get(1).left();
+ nextHighestBlaze = blazes.get(highestIndex - 1).left();
+ }
+ }
+ }
+
+ /**
+ * Renders outlines for Blaze entities based on health and position.
+ *
+ * @param wrc The WorldRenderContext used for rendering.
+ */
+ public static void blazeRenderer(WorldRenderContext wrc) {
+ try {
+ if (highestBlaze != null && lowestBlaze != null && highestBlaze.isAlive() && lowestBlaze.isAlive() && SkyblockerConfigManager.get().locations.dungeons.blazesolver) {
+ if (highestBlaze.getY() < 69) {
+ renderBlazeOutline(highestBlaze, nextHighestBlaze, wrc);
+ }
+ if (lowestBlaze.getY() > 69) {
+ renderBlazeOutline(lowestBlaze, nextLowestBlaze, wrc);
+ }
+ }
+ } catch (Exception e) {
+ handleException(e);
+ }
+ }
+
+ /**
+ * Renders outlines for Blaze entities and connections between them.
+ *
+ * @param blaze The Blaze entity for which to render an outline.
+ * @param nextBlaze The next Blaze entity for connection rendering.
+ * @param wrc The WorldRenderContext used for rendering.
+ */
+ private static void renderBlazeOutline(ArmorStandEntity blaze, ArmorStandEntity nextBlaze, WorldRenderContext wrc) {
+ Box blazeBox = blaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0);
+ RenderHelper.renderOutline(wrc, blazeBox, GREEN_COLOR_COMPONENTS, 5f);
+
+ if (nextBlaze != null && nextBlaze.isAlive() && nextBlaze != blaze) {
+ Box nextBlazeBox = nextBlaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0);
+ RenderHelper.renderOutline(wrc, nextBlazeBox, WHITE_COLOR_COMPONENTS, 5f);
+
+ Vec3d blazeCenter = blazeBox.getCenter();
+ Vec3d nextBlazeCenter = nextBlazeBox.getCenter();
+
+ RenderHelper.renderLinesFromPoints(wrc, new Vec3d[]{blazeCenter, nextBlazeCenter}, WHITE_COLOR_COMPONENTS, 1f, 5f);
+ }
+ }
+
+ /**
+ * Handles exceptions by logging and printing stack traces.
+ *
+ * @param e The exception to handle.
+ */
+ private static void handleException(Exception e) {
+ LOGGER.warn("[Skyblocker BlazeRenderer] Encountered an unknown exception", e);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonChestProfit.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonChestProfit.java
new file mode 100644
index 00000000..4e6a5240
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonChestProfit.java
@@ -0,0 +1,169 @@
+package de.hysky.skyblocker.skyblock.dungeon;
+
+import com.google.gson.JsonObject;
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.mixin.accessor.ScreenAccessor;
+import de.hysky.skyblocker.skyblock.item.PriceInfoTooltip;
+import de.hysky.skyblocker.utils.Utils;
+import it.unimi.dsi.fastutil.ints.IntBooleanPair;
+import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
+import net.minecraft.client.item.TooltipContext;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.GenericContainerScreenHandler;
+import net.minecraft.screen.ScreenHandlerType;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.text.DecimalFormat;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DungeonChestProfit {
+ private static final Logger LOGGER = LoggerFactory.getLogger(DungeonChestProfit.class);
+ private static final Pattern ESSENCE_PATTERN = Pattern.compile("(?<type>[A-Za-z]+) Essence x(?<amount>[0-9]+)");
+ private static final DecimalFormat FORMATTER = new DecimalFormat("#,###");
+
+ public static void init() {
+ ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> ScreenEvents.afterTick(screen).register(screen1 -> {
+ if (Utils.isOnSkyblock() && screen instanceof GenericContainerScreen genericContainerScreen && genericContainerScreen.getScreenHandler().getType() == ScreenHandlerType.GENERIC_9X6) {
+ ((ScreenAccessor) screen).setTitle(getChestProfit(genericContainerScreen.getScreenHandler(), screen.getTitle(), client));
+ }
+ }));
+ }
+
+ public static Text getChestProfit(GenericContainerScreenHandler handler, Text title, MinecraftClient client) {
+ try {
+ if (SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.enableProfitCalculator && isDungeonChest(title.getString())) {
+ int profit = 0;
+ boolean hasIncompleteData = false, usedKismet = false;
+ List<Slot> slots = handler.slots.subList(0, handler.getRows() * 9);
+
+ //If the item stack for the "Open Reward Chest" button or the kismet button hasn't been sent to the client yet
+ if (slots.get(31).getStack().isEmpty() || slots.get(50).getStack().isEmpty()) return title;
+
+ for (Slot slot : slots) {
+ ItemStack stack = slot.getStack();
+
+ if (!stack.isEmpty()) {
+ String name = stack.getName().getString();
+ String id = PriceInfoTooltip.getInternalNameFromNBT(stack, false);
+
+ //Regular item price
+ if (id != null) {
+ IntBooleanPair priceData = getItemPrice(id);
+
+ if (!priceData.rightBoolean()) hasIncompleteData = true;
+
+ //Add the item price to the profit
+ profit += priceData.leftInt();
+
+ continue;
+ }
+
+ //Essence price
+ if (name.contains("Essence") && SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.includeEssence) {
+ Matcher matcher = ESSENCE_PATTERN.matcher(name);
+
+ if (matcher.matches()) {
+ String type = matcher.group("type");
+ int amount = Integer.parseInt(matcher.group("amount"));
+
+ IntBooleanPair priceData = getItemPrice(("ESSENCE_" + type).toUpperCase());
+
+ if (!priceData.rightBoolean()) hasIncompleteData = true;
+
+ //Add the price of the essence to the profit
+ profit += priceData.leftInt() * amount;
+
+ continue;
+ }
+ }
+
+ //Determine the cost of the chest
+ if (name.contains("Open Reward Chest")) {
+ String foundString = searchLoreFor(stack, client, "Coins");
+
+ //Incase we're searching the free chest
+ if (!StringUtils.isBlank(foundString)) {
+ profit -= Integer.parseInt(foundString.replaceAll("[^0-9]", ""));
+ }
+
+ continue;
+ }
+
+ //Determine if a kismet was used or not
+ if (name.contains("Reroll Chest")) {
+ usedKismet = !StringUtils.isBlank(searchLoreFor(stack, client, "You already rerolled a chest!"));
+ }
+ }
+ }
+
+ if (SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.includeKismet && usedKismet) {
+ IntBooleanPair kismetPriceData = getItemPrice("KISMET_FEATHER");
+
+ if (!kismetPriceData.rightBoolean()) hasIncompleteData = true;
+
+ profit -= kismetPriceData.leftInt();
+ }
+
+ return Text.literal(title.getString()).append(getProfitText(profit, hasIncompleteData));
+ }
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Profit Calculator] Failed to calculate dungeon chest profit! ", e);
+ }
+
+ return title;
+ }
+
+ /**
+ * @return An {@link IntBooleanPair} with the {@code left int} representing the item's price, and the {@code right boolean} indicating if the price
+ * was based on complete data.
+ */
+ private static IntBooleanPair getItemPrice(String id) {
+ JsonObject bazaarPrices = PriceInfoTooltip.getBazaarPrices();
+ JsonObject lbinPrices = PriceInfoTooltip.getLBINPrices();
+
+ if (bazaarPrices == null || lbinPrices == null) return IntBooleanPair.of(0, false);
+
+ if (bazaarPrices.has(id)) {
+ JsonObject item = bazaarPrices.get(id).getAsJsonObject();
+ boolean isPriceNull = item.get("sellPrice").isJsonNull();
+
+ return IntBooleanPair.of(isPriceNull ? 0 : (int) item.get("sellPrice").getAsDouble(), !isPriceNull);
+ }
+
+ if (lbinPrices.has(id)) {
+ return IntBooleanPair.of((int) lbinPrices.get(id).getAsDouble(), true);
+ }
+
+ return IntBooleanPair.of(0, 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) {
+ return stack.getTooltip(client.player, TooltipContext.BASIC).stream().map(Text::getString).filter(line -> line.contains(searchString)).findAny().orElse(null);
+ }
+
+ private static boolean isDungeonChest(String name) {
+ return name.equals("Wood Chest") || name.equals("Gold Chest") || name.equals("Diamond Chest") || name.equals("Emerald Chest") || name.equals("Obsidian Chest") || name.equals("Bedrock Chest");
+ }
+
+ private static Text getProfitText(int profit, boolean hasIncompleteData) {
+ SkyblockerConfig.DungeonChestProfit config = SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit;
+ return getProfitText(profit, hasIncompleteData, config.neutralThreshold, config.neutralColor, config.profitColor, config.lossColor, config.incompleteColor);
+ }
+
+ static Text getProfitText(int profit, boolean hasIncompleteData, int neutralThreshold, Formatting neutralColor, Formatting profitColor, Formatting lossColor, Formatting incompleteColor) {
+ return Text.literal((profit > 0 ? " +" : " ") + FORMATTER.format(profit)).formatted(hasIncompleteData ? incompleteColor : (Math.abs(profit) < neutralThreshold) ? neutralColor : (profit > 0) ? profitColor : lossColor);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java
new file mode 100644
index 00000000..e1af85ea
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java
@@ -0,0 +1,61 @@
+package de.hysky.skyblocker.skyblock.dungeon;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+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.gui.DrawContext;
+import net.minecraft.client.render.MapRenderer;
+import net.minecraft.client.render.VertexConsumerProvider;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.item.FilledMapItem;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.map.MapState;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.util.Identifier;
+import org.apache.commons.lang3.StringUtils;
+
+public class DungeonMap {
+ private static final Identifier MAP_BACKGROUND = new Identifier("textures/map/map_background.png");
+
+ public static void render(MatrixStack matrices) {
+ MinecraftClient client = MinecraftClient.getInstance();
+ if (client.player == null || client.world == null) return;
+ ItemStack item = client.player.getInventory().main.get(8);
+ NbtCompound tag = item.getNbt();
+
+ if (tag != null && tag.contains("map")) {
+ String tag2 = tag.asString();
+ tag2 = StringUtils.substringBetween(tag2, "map:", "}");
+ int mapid = Integer.parseInt(tag2);
+ VertexConsumerProvider.Immediate vertices = client.getBufferBuilders().getEffectVertexConsumers();
+ MapRenderer map = client.gameRenderer.getMapRenderer();
+ MapState state = FilledMapItem.getMapState(mapid, client.world);
+ float scaling = SkyblockerConfigManager.get().locations.dungeons.mapScaling;
+ int x = SkyblockerConfigManager.get().locations.dungeons.mapX;
+ int y = SkyblockerConfigManager.get().locations.dungeons.mapY;
+
+ if (state == null) return;
+ matrices.push();
+ matrices.translate(x, y, 0);
+ matrices.scale(scaling, scaling, 0f);
+ map.draw(matrices, vertices, mapid, state, false, 15728880);
+ vertices.draw();
+ matrices.pop();
+ }
+ }
+
+ public static void renderHUDMap(DrawContext context, int x, int y) {
+ float scaling = SkyblockerConfigManager.get().locations.dungeons.mapScaling;
+ int size = (int) (128 * scaling);
+ context.drawTexture(MAP_BACKGROUND, x, y, 0, 0, size, size, size, size);
+ }
+
+ public static void init() {
+ ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal("skyblocker")
+ .then(ClientCommandManager.literal("hud")
+ .then(ClientCommandManager.literal("dungeonmap")
+ .executes(Scheduler.queueOpenScreenCommand(DungeonMapConfigScreen::new))))));
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMapConfigScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMapConfigScreen.java
new file mode 100644
index 00000000..145ee2bc
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMapConfigScreen.java
@@ -0,0 +1,62 @@
+package de.hysky.skyblocker.skyblock.dungeon;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.text.Text;
+
+import java.awt.*;
+
+public class DungeonMapConfigScreen extends Screen {
+
+ private int hudX = SkyblockerConfigManager.get().locations.dungeons.mapX;
+ private int hudY = SkyblockerConfigManager.get().locations.dungeons.mapY;
+ private final Screen parent;
+
+ protected DungeonMapConfigScreen() {
+ this(null);
+ }
+
+ public DungeonMapConfigScreen(Screen parent) {
+ super(Text.literal("Dungeon Map Config"));
+ this.parent = parent;
+ }
+
+ @Override
+ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
+ super.render(context, mouseX, mouseY, delta);
+ renderBackground(context, mouseX, mouseY, delta);
+ DungeonMap.renderHUDMap(context, hudX, hudY);
+ context.drawCenteredTextWithShadow(textRenderer, "Right Click To Reset Position", width >> 1, height >> 1, Color.GRAY.getRGB());
+ }
+
+ @Override
+ public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
+ float scaling = SkyblockerConfigManager.get().locations.dungeons.mapScaling;
+ int size = (int) (128 * scaling);
+ if (RenderHelper.pointIsInArea(mouseX, mouseY, hudX, hudY, hudX + size, hudY + size) && button == 0) {
+ hudX = (int) Math.max(Math.min(mouseX - (size >> 1), this.width - size), 0);
+ hudY = (int) Math.max(Math.min(mouseY - (size >> 1), this.height - size), 0);
+ }
+ return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (button == 1) {
+ hudX = 2;
+ hudY = 2;
+ }
+
+ return super.mouseClicked(mouseX, mouseY, button);
+ }
+
+ @Override
+ public void close() {
+ SkyblockerConfigManager.get().locations.dungeons.mapX = hudX;
+ SkyblockerConfigManager.get().locations.dungeons.mapY = hudY;
+ SkyblockerConfigManager.save();
+ this.client.setScreen(parent);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/LividColor.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/LividColor.java
new file mode 100644
index 00000000..762a6e17
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/LividColor.java
@@ -0,0 +1,42 @@
+package de.hysky.skyblocker.skyblock.dungeon;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.util.math.BlockPos;
+
+public class LividColor {
+ private static int tenTicks = 0;
+
+ public static void init() {
+ ClientReceiveMessageEvents.GAME.register((message, overlay) -> {
+ if (SkyblockerConfigManager.get().locations.dungeons.lividColor.enableLividColor && message.getString().equals("[BOSS] Livid: I respect you for making it to here, but I'll be your undoing.")) {
+ tenTicks = 8;
+ }
+ });
+ }
+
+ public static void update() {
+ MinecraftClient client = MinecraftClient.getInstance();
+ if (tenTicks != 0) {
+ if (SkyblockerConfigManager.get().locations.dungeons.lividColor.enableLividColor && Utils.isInDungeons() && client.world != null) {
+ if (tenTicks == 1) {
+ MessageScheduler.INSTANCE.sendMessageAfterCooldown(SkyblockerConfigManager.get().locations.dungeons.lividColor.lividColorText.replace("[color]", "red"));
+ tenTicks = 0;
+ return;
+ }
+ String key = client.world.getBlockState(new BlockPos(5, 110, 42)).getBlock().getTranslationKey();
+ if (key.startsWith("block.minecraft.") && key.endsWith("wool") && !key.endsWith("red_wool")) {
+ MessageScheduler.INSTANCE.sendMessageAfterCooldown(SkyblockerConfigManager.get().locations.dungeons.lividColor.lividColorText.replace("[color]", key.substring(16, key.length() - 5)));
+ tenTicks = 0;
+ return;
+ }
+ tenTicks--;
+ } else {
+ tenTicks = 0;
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/OldLever.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/OldLever.java
new file mode 100644
index 00000000..b9b76c59
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/OldLever.java
@@ -0,0 +1,40 @@
+package de.hysky.skyblocker.skyblock.dungeon;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import net.minecraft.block.Block;
+import net.minecraft.block.enums.BlockFace;
+import net.minecraft.util.math.Direction;
+import net.minecraft.util.shape.VoxelShape;
+
+public class OldLever {
+ protected static final VoxelShape FLOOR_SHAPE = Block.createCuboidShape(4.0D, 0.0D, 4.0D, 12.0D, 10.0D, 12.0D);
+ protected static final VoxelShape NORTH_SHAPE = Block.createCuboidShape(5.0D, 3.0D, 10.0D, 11.0D, 13.0D, 16.0D);
+ protected static final VoxelShape SOUTH_SHAPE = Block.createCuboidShape(5.0D, 3.0D, 0.0D, 11.0D, 13.0D, 6.0D);
+ protected static final VoxelShape EAST_SHAPE = Block.createCuboidShape(0.0D, 3.0D, 5.0D, 6.0D, 13.0D, 11.0D);
+ protected static final VoxelShape WEST_SHAPE = Block.createCuboidShape(10.0D, 3.0D, 5.0D, 16.0D, 13.0D, 11.0D);
+
+ public static VoxelShape getShape(BlockFace face, Direction direction) {
+ if (!SkyblockerConfigManager.get().general.hitbox.oldLeverHitbox)
+ return null;
+
+ if (face == BlockFace.FLOOR) {
+ return FLOOR_SHAPE;
+ } else if (face == BlockFace.WALL) {
+ switch (direction) {
+ case EAST -> {
+ return EAST_SHAPE;
+ }
+ case WEST -> {
+ return WEST_SHAPE;
+ }
+ case SOUTH -> {
+ return SOUTH_SHAPE;
+ }
+ case NORTH -> {
+ return NORTH_SHAPE;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Reparty.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Reparty.java
new file mode 100644
index 00000000..6165ac6a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Reparty.java
@@ -0,0 +1,94 @@
+package de.hysky.skyblocker.skyblock.dungeon;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+import de.hysky.skyblocker.utils.chat.ChatPatternListener;
+import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+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.network.ClientPlayerEntity;
+import net.minecraft.text.Text;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class Reparty extends ChatPatternListener {
+ private static final MinecraftClient client = MinecraftClient.getInstance();
+ public static final Pattern PLAYER = Pattern.compile(" ([a-zA-Z0-9_]{2,16}) ●");
+ private static final int BASE_DELAY = 10;
+
+ private String[] players;
+ private int playersSoFar;
+ private boolean repartying;
+ private String partyLeader;
+
+ public Reparty() {
+ super("^(?:You are not currently in a party\\." +
+ "|Party (?:Membe|Moderato)rs(?: \\(([0-9]+)\\)|:( .*))" +
+ "|([\\[A-z+\\]]* )?(?<disband>.*) has disbanded .*" +
+ "|.*\n([\\[A-z+\\]]* )?(?<invite>.*) has invited you to join their party!" +
+ "\nYou have 60 seconds to accept. Click here to join!\n.*)$");
+
+ this.repartying = false;
+ ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal("rp").executes(context -> {
+ if (!Utils.isOnSkyblock() || this.repartying || client.player == null) return 0;
+ this.repartying = true;
+ MessageScheduler.INSTANCE.sendMessageAfterCooldown("/p list");
+ return 0;
+ })));
+ }
+
+ @Override
+ public ChatFilterResult state() {
+ return (SkyblockerConfigManager.get().general.acceptReparty || this.repartying) ? ChatFilterResult.FILTER : ChatFilterResult.PASS;
+ }
+
+ @Override
+ public boolean onMatch(Text message, Matcher matcher) {
+ if (matcher.group(1) != null && repartying) {
+ this.playersSoFar = 0;
+ this.players = new String[Integer.parseInt(matcher.group(1)) - 1];
+ } else if (matcher.group(2) != null && repartying) {
+ Matcher m = PLAYER.matcher(matcher.group(2));
+ while (m.find()) {
+ this.players[playersSoFar++] = m.group(1);
+ }
+ } else if (matcher.group("disband") != null && !matcher.group("disband").equals(client.getSession().getUsername())) {
+ partyLeader = matcher.group("disband");
+ Scheduler.INSTANCE.schedule(() -> partyLeader = null, 61);
+ return false;
+ } else if (matcher.group("invite") != null && matcher.group("invite").equals(partyLeader)) {
+ String command = "/party accept " + partyLeader;
+ sendCommand(command, 0);
+ return false;
+ } else {
+ this.repartying = false;
+ return false;
+ }
+ if (this.playersSoFar == this.players.length) {
+ reparty();
+ }
+ return false;
+ }
+
+ private void reparty() {
+ ClientPlayerEntity playerEntity = client.player;
+ if (playerEntity == null) {
+ this.repartying = false;
+ return;
+ }
+ sendCommand("/p disband", 1);
+ for (int i = 0; i < this.players.length; ++i) {
+ String command = "/p invite " + this.players[i];
+ sendCommand(command, i + 2);
+ }
+ Scheduler.INSTANCE.schedule(() -> this.repartying = false, this.players.length + 2);
+ }
+
+ private void sendCommand(String command, int delay) {
+ MessageScheduler.INSTANCE.queueMessage(command, delay * BASE_DELAY);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/StarredMobGlow.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/StarredMobGlow.java
new file mode 100644
index 00000000..2072017d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/StarredMobGlow.java
@@ -0,0 +1,56 @@
+package de.hysky.skyblocker.skyblock.dungeon;
+
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.culling.OcclusionCulling;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.decoration.ArmorStandEntity;
+import net.minecraft.entity.passive.BatEntity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.predicate.entity.EntityPredicates;
+import net.minecraft.util.math.Box;
+
+import java.util.List;
+
+public class StarredMobGlow {
+
+ public static boolean shouldMobGlow(Entity entity) {
+ Box box = entity.getBoundingBox();
+
+ if (Utils.isInDungeons() && !entity.isInvisible() && OcclusionCulling.isVisible(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) {
+ // Minibosses
+ if (entity instanceof PlayerEntity) {
+ switch (entity.getName().getString()) {
+ case "Lost Adventurer", "Shadow Assassin", "Diamond Guy" -> {
+ return true;
+ }
+ }
+ }
+
+ // Regular Mobs
+ if (!(entity instanceof ArmorStandEntity)) {
+ Box searchBox = box.expand(0, 2, 0);
+ List<ArmorStandEntity> armorStands = entity.getWorld().getEntitiesByClass(ArmorStandEntity.class, searchBox, EntityPredicates.NOT_MOUNTED);
+
+ if (!armorStands.isEmpty() && armorStands.get(0).getName().getString().contains("✯")) return true;
+ }
+
+ // Bats
+ return entity instanceof BatEntity;
+ }
+
+ return false;
+ }
+
+ public static int getGlowColor(Entity entity) {
+ if (entity instanceof PlayerEntity) {
+ return switch (entity.getName().getString()) {
+ case "Lost Adventurer" -> 0xfee15c;
+ case "Shadow Assassin" -> 0x5b2cb2;
+ case "Diamond Guy" -> 0x57c2f7;
+ default -> 0xf57738;
+ };
+ }
+
+ return 0xf57738;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdos.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdos.java
new file mode 100644
index 00000000..e1ab2fa8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdos.java
@@ -0,0 +1,39 @@
+package de.hysky.skyblocker.skyblock.dungeon;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+import de.hysky.skyblocker.utils.chat.ChatPatternListener;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.entity.decoration.ArmorStandEntity;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import java.util.regex.Matcher;
+
+public class ThreeWeirdos extends ChatPatternListener {
+ public ThreeWeirdos() {
+ super("^§e\\[NPC] §c([A-Z][a-z]+)§f: (?:The reward is(?: not in my chest!|n't in any of our chests\\.)|My chest (?:doesn't have the reward\\. We are all telling the truth\\.|has the reward and I'm telling the truth!)|At least one of them is lying, and the reward is not in §c§c[A-Z][a-z]+'s §rchest\\!|Both of them are telling the truth\\. Also, §c§c[A-Z][a-z]+ §rhas the reward in their chest\\!)$");
+ }
+
+ @Override
+ public ChatFilterResult state() {
+ return SkyblockerConfigManager.get().locations.dungeons.solveThreeWeirdos ? null : ChatFilterResult.PASS;
+ }
+
+ @Override
+ public boolean onMatch(Text message, Matcher matcher) {
+ MinecraftClient client = MinecraftClient.getInstance();
+ if (client.player == null || client.world == null) return false;
+ client.world.getEntitiesByClass(
+ ArmorStandEntity.class,
+ client.player.getBoundingBox().expand(3),
+ entity -> {
+ Text customName = entity.getCustomName();
+ return customName != null && customName.getString().equals(matcher.group(1));
+ }
+ ).forEach(
+ entity -> entity.setCustomName(Text.of(Formatting.GREEN + matcher.group(1)))
+ );
+ return false;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java
new file mode 100644
index 00000000..2d56c8a0
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java
@@ -0,0 +1,136 @@
+package de.hysky.skyblocker.skyblock.dungeon;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import de.hysky.skyblocker.utils.tictactoe.TicTacToeUtils;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.minecraft.block.Block;
+import net.minecraft.block.Blocks;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.decoration.ItemFrameEntity;
+import net.minecraft.item.FilledMapItem;
+import net.minecraft.item.map.MapState;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Box;
+import net.minecraft.util.math.Direction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+/**
+ * Thanks to Danker for a reference implementation!
+ */
+public class TicTacToe {
+ private static final Logger LOGGER = LoggerFactory.getLogger(TicTacToe.class);
+ private static final float[] RED_COLOR_COMPONENTS = {1.0F, 0.0F, 0.0F};
+ private static Box nextBestMoveToMake = null;
+
+ public static void init() {
+ WorldRenderEvents.BEFORE_DEBUG_RENDER.register(TicTacToe::solutionRenderer);
+ }
+
+ public static void tick() {
+ MinecraftClient client = MinecraftClient.getInstance();
+ ClientWorld world = client.world;
+ ClientPlayerEntity player = client.player;
+
+ nextBestMoveToMake = null;
+
+ if (world == null || player == null || !Utils.isInDungeons()) return;
+
+ //Search within 21 blocks for item frames that contain maps
+ Box searchBox = new Box(player.getX() - 21, player.getY() - 21, player.getZ() - 21, player.getX() + 21, player.getY() + 21, player.getZ() + 21);
+ List<ItemFrameEntity> itemFramesThatHoldMaps = world.getEntitiesByClass(ItemFrameEntity.class, searchBox, ItemFrameEntity::containsMap);
+
+ try {
+ //Only attempt to solve if its the player's turn
+ if (itemFramesThatHoldMaps.size() != 9 && itemFramesThatHoldMaps.size() % 2 == 1) {
+ char[][] board = new char[3][3];
+ BlockPos leftmostRow = null;
+ int sign = 1;
+ char facing = 'X';
+
+ for (ItemFrameEntity itemFrame : itemFramesThatHoldMaps) {
+ MapState mapState = world.getMapState(FilledMapItem.getMapName(itemFrame.getMapId().getAsInt()));
+
+ if (mapState == null) continue;
+
+ int column = 0, row;
+ sign = 1;
+
+ //Find position of the item frame relative to where it is on the tic tac toe board
+ if (itemFrame.getHorizontalFacing() == Direction.SOUTH || itemFrame.getHorizontalFacing() == Direction.WEST) sign = -1;
+ BlockPos itemFramePos = BlockPos.ofFloored(itemFrame.getX(), itemFrame.getY(), itemFrame.getZ());
+
+ for (int i = 2; i >= 0; i--) {
+ int realI = i * sign;
+ BlockPos blockPos = itemFramePos;
+
+ if (itemFrame.getX() % 0.5 == 0) {
+ blockPos = itemFramePos.add(realI, 0, 0);
+ } else if (itemFrame.getZ() % 0.5 == 0) {
+ blockPos = itemFramePos.add(0, 0, realI);
+ facing = 'Z';
+ }
+
+ Block block = world.getBlockState(blockPos).getBlock();
+ if (block == Blocks.AIR || block == Blocks.STONE_BUTTON) {
+ leftmostRow = blockPos;
+ column = i;
+
+ break;
+ }
+ }
+
+ //Determine the row of the item frame
+ if (itemFrame.getY() == 72.5) {
+ row = 0;
+ } else if (itemFrame.getY() == 71.5) {
+ row = 1;
+ } else if (itemFrame.getY() == 70.5) {
+ row = 2;
+ } else {
+ continue;
+ }
+
+
+ //Get the color of the middle pixel of the map which determines whether its X or O
+ int middleColor = mapState.colors[8256] & 255;
+
+ if (middleColor == 114) {
+ board[row][column] = 'X';
+ } else if (middleColor == 33) {
+ board[row][column] = 'O';
+ }
+
+ int bestMove = TicTacToeUtils.getBestMove(board) - 1;
+
+ if (leftmostRow != null) {
+ double drawX = facing == 'X' ? leftmostRow.getX() - sign * (bestMove % 3) : leftmostRow.getX();
+ double drawY = 72 - (double) (bestMove / 3);
+ double drawZ = facing == 'Z' ? leftmostRow.getZ() - sign * (bestMove % 3) : leftmostRow.getZ();
+
+ nextBestMoveToMake = new Box(drawX, drawY, drawZ, drawX + 1, drawY + 1, drawZ + 1);
+ }
+ }
+ }
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Tic Tac Toe] Encountered an exception while determining a tic tac toe solution!", e);
+ }
+ }
+
+ private static void solutionRenderer(WorldRenderContext context) {
+ try {
+ if (SkyblockerConfigManager.get().locations.dungeons.solveTicTacToe && nextBestMoveToMake != null) {
+ RenderHelper.renderOutline(context, nextBestMoveToMake, RED_COLOR_COMPONENTS, 5);
+ }
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Tic Tac Toe] Encountered an exception while rendering the tic tac toe solution!", e);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Trivia.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Trivia.java
new file mode 100644
index 00000000..262d4a4e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Trivia.java
@@ -0,0 +1,100 @@
+package de.hysky.skyblocker.skyblock.dungeon;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.FairySouls;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+import de.hysky.skyblocker.utils.chat.ChatPatternListener;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.regex.Matcher;
+
+public class Trivia extends ChatPatternListener {
+ private static final Map<String, String[]> answers;
+ private List<String> solutions = Collections.emptyList();
+
+ public Trivia() {
+ super("^ +(?:([A-Za-z,' ]*\\?)|§6 ([ⓐⓑⓒ]) §a([a-zA-Z0-9 ]+))$");
+ }
+
+ @Override
+ public ChatFilterResult state() {
+ return SkyblockerConfigManager.get().locations.dungeons.solveTrivia ? ChatFilterResult.FILTER : ChatFilterResult.PASS;
+ }
+
+ @Override
+ public boolean onMatch(Text message, Matcher matcher) {
+ String riddle = matcher.group(3);
+ if (riddle != null) {
+ if (!solutions.contains(riddle)) {
+ ClientPlayerEntity player = MinecraftClient.getInstance().player;
+ if (player != null)
+ MinecraftClient.getInstance().player.sendMessage(Text.of(" " + Formatting.GOLD + matcher.group(2) + Formatting.RED + " " + riddle), false);
+ return player != null;
+ }
+ } else updateSolutions(matcher.group(0));
+ return false;
+ }
+
+ private void updateSolutions(String question) {
+ String trimmedQuestion = question.trim();
+ if (trimmedQuestion.equals("What SkyBlock year is it?")) {
+ long currentTime = System.currentTimeMillis() / 1000L;
+ long diff = currentTime - 1560276000;
+ int year = (int) (diff / 446400 + 1);
+ solutions = Collections.singletonList("Year " + year);
+ } else {
+ solutions = Arrays.asList(answers.get(trimmedQuestion));
+ }
+ }
+
+ static {
+ answers = Collections.synchronizedMap(new HashMap<>());
+ answers.put("What is the status of The Watcher?", new String[]{"Stalker"});
+ answers.put("What is the status of Bonzo?", new String[]{"New Necromancer"});
+ answers.put("What is the status of Scarf?", new String[]{"Apprentice Necromancer"});
+ answers.put("What is the status of The Professor?", new String[]{"Professor"});
+ answers.put("What is the status of Thorn?", new String[]{"Shaman Necromancer"});
+ answers.put("What is the status of Livid?", new String[]{"Master Necromancer"});
+ answers.put("What is the status of Sadan?", new String[]{"Necromancer Lord"});
+ answers.put("What is the status of Maxor?", new String[]{"The Wither Lords"});
+ answers.put("What is the status of Goldor?", new String[]{"The Wither Lords"});
+ answers.put("What is the status of Storm?", new String[]{"The Wither Lords"});
+ answers.put("What is the status of Necron?", new String[]{"The Wither Lords"});
+ answers.put("What is the status of Maxor, Storm, Goldor and Necron?", new String[]{"The Wither Lords"});
+ answers.put("Which brother is on the Spider's Den?", new String[]{"Rick"});
+ answers.put("What is the name of Rick's brother?", new String[]{"Pat"});
+ answers.put("What is the name of the Painter in the Hub?", new String[]{"Marco"});
+ answers.put("What is the name of the person that upgrades pets?", new String[]{"Kat"});
+ answers.put("What is the name of the lady of the Nether?", new String[]{"Elle"});
+ answers.put("Which villager in the Village gives you a Rogue Sword?", new String[]{"Jamie"});
+ answers.put("How many unique minions are there?", new String[]{"59 Minions"});
+ answers.put("Which of these enemies does not spawn in the Spider's Den?", new String[]{"Zombie Spider", "Cave Spider", "Wither Skeleton", "Dashing Spooder", "Broodfather", "Night Spider"});
+ answers.put("Which of these monsters only spawns at night?", new String[]{"Zombie Villager", "Ghast"});
+ answers.put("Which of these is not a dragon in The End?", new String[]{"Zoomer Dragon", "Weak Dragon", "Stonk Dragon", "Holy Dragon", "Boomer Dragon", "Booger Dragon", "Older Dragon", "Elder Dragon", "Stable Dragon", "Professor Dragon"});
+ FairySouls.runAsyncAfterFairySoulsLoad(() -> {
+ answers.put("How many total Fairy Souls are there?", getFairySoulsSizeString(null));
+ answers.put("How many Fairy Souls are there in Spider's Den?", getFairySoulsSizeString("combat_1"));
+ answers.put("How many Fairy Souls are there in The End?", getFairySoulsSizeString("combat_3"));
+ answers.put("How many Fairy Souls are there in The Farming Islands?", getFairySoulsSizeString("farming_1"));
+ answers.put("How many Fairy Souls are there in Crimson Isle?", getFairySoulsSizeString("crimson_isle"));
+ answers.put("How many Fairy Souls are there in The Park?", getFairySoulsSizeString("foraging_1"));
+ answers.put("How many Fairy Souls are there in Jerry's Workshop?", getFairySoulsSizeString("winter"));
+ answers.put("How many Fairy Souls are there in Hub?", getFairySoulsSizeString("hub"));
+ answers.put("How many Fairy Souls are there in The Hub?", getFairySoulsSizeString("hub"));
+ answers.put("How many Fairy Souls are there in Deep Caverns?", getFairySoulsSizeString("mining_2"));
+ answers.put("How many Fairy Souls are there in Gold Mine?", getFairySoulsSizeString("mining_1"));
+ answers.put("How many Fairy Souls are there in Dungeon Hub?", getFairySoulsSizeString("dungeon_hub"));
+ });
+ }
+
+ @NotNull
+ private static String[] getFairySoulsSizeString(@Nullable String location) {
+ return new String[]{"%d Fairy Souls".formatted(FairySouls.getFairySoulsSize(location))};
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java
new file mode 100644
index 00000000..259cc3f3
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java
@@ -0,0 +1,275 @@
+package de.hysky.skyblocker.skyblock.dungeon.secrets;
+
+import com.google.gson.JsonObject;
+import it.unimi.dsi.fastutil.ints.IntSortedSet;
+import it.unimi.dsi.fastutil.objects.ObjectIntPair;
+import net.minecraft.block.MapColor;
+import net.minecraft.item.map.MapIcon;
+import net.minecraft.item.map.MapState;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.util.math.Vec3i;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.joml.RoundingMode;
+import org.joml.Vector2i;
+import org.joml.Vector2ic;
+
+import java.util.*;
+
+public class DungeonMapUtils {
+ public static final byte BLACK_COLOR = MapColor.BLACK.getRenderColorByte(MapColor.Brightness.LOWEST);
+ public static final byte WHITE_COLOR = MapColor.WHITE.getRenderColorByte(MapColor.Brightness.HIGH);
+
+ public static byte getColor(MapState map, @Nullable Vector2ic pos) {
+ return pos == null ? -1 : getColor(map, pos.x(), pos.y());
+ }
+
+ public static byte getColor(MapState map, int x, int z) {
+ if (x < 0 || z < 0 || x >= 128 || z >= 128) {
+ return -1;
+ }
+ return map.colors[x + (z << 7)];
+ }
+
+ public static boolean isEntranceColor(MapState map, int x, int z) {
+ return getColor(map, x, z) == Room.Type.ENTRANCE.color;
+ }
+
+ public static boolean isEntranceColor(MapState map, @Nullable Vector2ic pos) {
+ return getColor(map, pos) == Room.Type.ENTRANCE.color;
+ }
+
+ @Nullable
+ private static Vector2i getMapPlayerPos(MapState map) {
+ for (MapIcon icon : map.getIcons()) {
+ if (icon.type() == MapIcon.Type.FRAME) {
+ return new Vector2i((icon.x() >> 1) + 64, (icon.z() >> 1) + 64);
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ public static ObjectIntPair<Vector2ic> getMapEntrancePosAndRoomSize(@NotNull MapState map) {
+ Vector2ic mapPos = getMapPlayerPos(map);
+ if (mapPos == null) {
+ return null;
+ }
+ Queue<Vector2ic> posToCheck = new ArrayDeque<>();
+ Set<Vector2ic> checked = new HashSet<>();
+ posToCheck.add(mapPos);
+ checked.add(mapPos);
+ while ((mapPos = posToCheck.poll()) != null) {
+ if (isEntranceColor(map, mapPos)) {
+ ObjectIntPair<Vector2ic> mapEntranceAndRoomSizePos = getMapEntrancePosAndRoomSizeAt(map, mapPos);
+ if (mapEntranceAndRoomSizePos.rightInt() > 0) {
+ return mapEntranceAndRoomSizePos;
+ }
+ }
+ Vector2ic pos = new Vector2i(mapPos).sub(10, 0);
+ if (checked.add(pos)) {
+ posToCheck.add(pos);
+ }
+ pos = new Vector2i(mapPos).sub(0, 10);
+ if (checked.add(pos)) {
+ posToCheck.add(pos);
+ }
+ pos = new Vector2i(mapPos).add(10, 0);
+ if (checked.add(pos)) {
+ posToCheck.add(pos);
+ }
+ pos = new Vector2i(mapPos).add(0, 10);
+ if (checked.add(pos)) {
+ posToCheck.add(pos);
+ }
+ }
+ return null;
+ }
+
+ private static ObjectIntPair<Vector2ic> getMapEntrancePosAndRoomSizeAt(MapState map, Vector2ic mapPosImmutable) {
+ Vector2i mapPos = new Vector2i(mapPosImmutable);
+ // noinspection StatementWithEmptyBody
+ while (isEntranceColor(map, mapPos.sub(1, 0))) {
+ }
+ mapPos.add(1, 0);
+ //noinspection StatementWithEmptyBody
+ while (isEntranceColor(map, mapPos.sub(0, 1))) {
+ }
+ return ObjectIntPair.of(mapPos.add(0, 1), getMapRoomSize(map, mapPos));
+ }
+
+ public static int getMapRoomSize(MapState map, Vector2ic mapEntrancePos) {
+ int i = -1;
+ //noinspection StatementWithEmptyBody
+ while (isEntranceColor(map, mapEntrancePos.x() + ++i, mapEntrancePos.y())) {
+ }
+ return i > 5 ? i : 0;
+ }
+
+ /**
+ * Gets the map position of the top left corner of the room the player is in.
+ *
+ * @param map the map
+ * @param mapEntrancePos the map position of the top left corner of the entrance
+ * @param mapRoomSize the size of a room on the map
+ * @return the map position of the top left corner of the room the player is in
+ * @implNote {@code mapPos} is shifted by 2 so room borders are evenly split.
+ * {@code mapPos} is then shifted by {@code offset} to align the top left most room at (0, 0)
+ * so subtracting the modulo will give the top left corner of the room shifted by {@code offset}.
+ * Finally, {@code mapPos} is shifted back by {@code offset} to its intended position.
+ */
+ @Nullable
+ public static Vector2ic getMapRoomPos(MapState map, Vector2ic mapEntrancePos, int mapRoomSize) {
+ int mapRoomSizeWithGap = mapRoomSize + 4;
+ Vector2i mapPos = getMapPlayerPos(map);
+ if (mapPos == null) {
+ return null;
+ }
+ Vector2ic offset = new Vector2i(mapEntrancePos.x() % mapRoomSizeWithGap, mapEntrancePos.y() % mapRoomSizeWithGap);
+ return mapPos.add(2, 2).sub(offset).sub(mapPos.x() % mapRoomSizeWithGap, mapPos.y() % mapRoomSizeWithGap).add(offset);
+ }
+
+ /**
+ * Gets the map position of the top left corner of the room corresponding to the physical position of the northwest corner of a room.
+ *
+ * @param physicalEntrancePos the physical position of the northwest corner of the entrance room
+ * @param mapEntrancePos the map position of the top left corner of the entrance room
+ * @param mapRoomSize the size of a room on the map
+ * @param physicalPos the physical position of the northwest corner of the room
+ * @return the map position of the top left corner of the room corresponding to the physical position of the northwest corner of a room
+ */
+ public static Vector2ic getMapPosFromPhysical(Vector2ic physicalEntrancePos, Vector2ic mapEntrancePos, int mapRoomSize, Vector2ic physicalPos) {
+ return new Vector2i(physicalPos).sub(physicalEntrancePos).div(32).mul(mapRoomSize + 4).add(mapEntrancePos);
+ }
+
+ /**
+ * @see #getPhysicalRoomPos(double, double)
+ */
+ @NotNull
+ public static Vector2ic getPhysicalRoomPos(@NotNull Vec3d pos) {
+ return getPhysicalRoomPos(pos.getX(), pos.getZ());
+ }
+
+ /**
+ * @see #getPhysicalRoomPos(double, double)
+ */
+ @NotNull
+ public static Vector2ic getPhysicalRoomPos(@NotNull Vec3i pos) {
+ return getPhysicalRoomPos(pos.getX(), pos.getZ());
+ }
+
+ /**
+ * Gets the physical position of the northwest corner of the room the given coordinate is in. Hypixel Skyblock Dungeons are aligned to a 32 by 32 blocks grid, allowing corners to be calculated through math.
+ *
+ * @param x the x position of the coordinate to calculate
+ * @param z the z position of the coordinate to calculate
+ * @return the physical position of the northwest corner of the room the player is in
+ * @implNote {@code physicalPos} is shifted by 0.5 so room borders are evenly split.
+ * {@code physicalPos} is further shifted by 8 because Hypixel offset dungeons by 8 blocks in Skyblock 0.12.3.
+ * Subtracting the modulo gives the northwest corner of the room shifted by 8. Finally, {@code physicalPos} is shifted back by 8 to its intended position.
+ */
+ @NotNull
+ public static Vector2ic getPhysicalRoomPos(double x, double z) {
+ Vector2i physicalPos = new Vector2i(x + 8.5, z + 8.5, RoundingMode.TRUNCATE);
+ return physicalPos.sub(MathHelper.floorMod(physicalPos.x(), 32), MathHelper.floorMod(physicalPos.y(), 32)).sub(8, 8);
+ }
+
+ public static Vector2ic[] getPhysicalPosFromMap(Vector2ic mapEntrancePos, int mapRoomSize, Vector2ic physicalEntrancePos, Vector2ic... mapPositions) {
+ for (int i = 0; i < mapPositions.length; i++) {
+ mapPositions[i] = getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, mapPositions[i]);
+ }
+ return mapPositions;
+ }
+
+ /**
+ * Gets the physical position of the northwest corner of the room corresponding to the map position of the top left corner of a room.
+ *
+ * @param mapEntrancePos the map position of the top left corner of the entrance room
+ * @param mapRoomSize the size of a room on the map
+ * @param physicalEntrancePos the physical position of the northwest corner of the entrance room
+ * @param mapPos the map position of the top left corner of the room
+ * @return the physical position of the northwest corner of the room corresponding to the map position of the top left corner of a room
+ */
+ public static Vector2ic getPhysicalPosFromMap(Vector2ic mapEntrancePos, int mapRoomSize, Vector2ic physicalEntrancePos, Vector2ic mapPos) {
+ return new Vector2i(mapPos).sub(mapEntrancePos).div(mapRoomSize + 4).mul(32).add(physicalEntrancePos);
+ }
+
+ public static Vector2ic getPhysicalCornerPos(Room.Direction direction, IntSortedSet segmentsX, IntSortedSet segmentsY) {
+ return switch (direction) {
+ case NW -> new Vector2i(segmentsX.firstInt(), segmentsY.firstInt());
+ case NE -> new Vector2i(segmentsX.lastInt() + 30, segmentsY.firstInt());
+ case SW -> new Vector2i(segmentsX.firstInt(), segmentsY.lastInt() + 30);
+ case SE -> new Vector2i(segmentsX.lastInt() + 30, segmentsY.lastInt() + 30);
+ };
+ }
+
+ public static BlockPos actualToRelative(Room.Direction direction, Vector2ic physicalCornerPos, BlockPos pos) {
+ return switch (direction) {
+ case NW -> new BlockPos(pos.getX() - physicalCornerPos.x(), pos.getY(), pos.getZ() - physicalCornerPos.y());
+ case NE -> new BlockPos(pos.getZ() - physicalCornerPos.y(), pos.getY(), -pos.getX() + physicalCornerPos.x());
+ case SW -> new BlockPos(-pos.getZ() + physicalCornerPos.y(), pos.getY(), pos.getX() - physicalCornerPos.x());
+ case SE -> new BlockPos(-pos.getX() + physicalCornerPos.x(), pos.getY(), -pos.getZ() + physicalCornerPos.y());
+ };
+ }
+
+ public static BlockPos relativeToActual(Room.Direction direction, Vector2ic physicalCornerPos, JsonObject posJson) {
+ return relativeToActual(direction, physicalCornerPos, new BlockPos(posJson.get("x").getAsInt(), posJson.get("y").getAsInt(), posJson.get("z").getAsInt()));
+ }
+
+ public static BlockPos relativeToActual(Room.Direction direction, Vector2ic physicalCornerPos, BlockPos pos) {
+ return switch (direction) {
+ case NW -> new BlockPos(pos.getX() + physicalCornerPos.x(), pos.getY(), pos.getZ() + physicalCornerPos.y());
+ case NE -> new BlockPos(-pos.getZ() + physicalCornerPos.x(), pos.getY(), pos.getX() + physicalCornerPos.y());
+ case SW -> new BlockPos(pos.getZ() + physicalCornerPos.x(), pos.getY(), -pos.getX() + physicalCornerPos.y());
+ case SE -> new BlockPos(-pos.getX() + physicalCornerPos.x(), pos.getY(), -pos.getZ() + physicalCornerPos.y());
+ };
+ }
+
+ public static Room.Type getRoomType(MapState map, Vector2ic mapPos) {
+ return switch (getColor(map, mapPos)) {
+ case 30 -> Room.Type.ENTRANCE;
+ case 63 -> Room.Type.ROOM;
+ case 66 -> Room.Type.PUZZLE;
+ case 62 -> Room.Type.TRAP;
+ case 74 -> Room.Type.MINIBOSS;
+ case 82 -> Room.Type.FAIRY;
+ case 18 -> Room.Type.BLOOD;
+ case 85 -> Room.Type.UNKNOWN;
+ default -> null;
+ };
+ }
+
+ public static Vector2ic[] getRoomSegments(MapState map, Vector2ic mapPos, int mapRoomSize, byte color) {
+ Set<Vector2ic> segments = new HashSet<>();
+ Queue<Vector2ic> queue = new ArrayDeque<>();
+ segments.add(mapPos);
+ queue.add(mapPos);
+ while (!queue.isEmpty()) {
+ Vector2ic curMapPos = queue.poll();
+ Vector2i newMapPos = new Vector2i();
+ if (getColor(map, newMapPos.set(curMapPos).sub(1, 0)) == color && !segments.contains(newMapPos.sub(mapRoomSize + 3, 0))) {
+ segments.add(newMapPos);
+ queue.add(newMapPos);
+ newMapPos = new Vector2i();
+ }
+ if (getColor(map, newMapPos.set(curMapPos).sub(0, 1)) == color && !segments.contains(newMapPos.sub(0, mapRoomSize + 3))) {
+ segments.add(newMapPos);
+ queue.add(newMapPos);
+ newMapPos = new Vector2i();
+ }
+ if (getColor(map, newMapPos.set(curMapPos).add(mapRoomSize, 0)) == color && !segments.contains(newMapPos.add(4, 0))) {
+ segments.add(newMapPos);
+ queue.add(newMapPos);
+ newMapPos = new Vector2i();
+ }
+ if (getColor(map, newMapPos.set(curMapPos).add(0, mapRoomSize)) == color && !segments.contains(newMapPos.add(0, 4))) {
+ segments.add(newMapPos);
+ queue.add(newMapPos);
+ }
+ }
+ DungeonSecrets.LOGGER.debug("[Skyblocker] Found dungeon room segments: {}", Arrays.toString(segments.toArray()));
+ return segments.toArray(Vector2ic[]::new);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java
new file mode 100644
index 00000000..7d20644a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java
@@ -0,0 +1,451 @@
+package de.hysky.skyblocker.skyblock.dungeon.secrets;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.arguments.IntegerArgumentType;
+import com.mojang.brigadier.builder.ArgumentBuilder;
+import com.mojang.brigadier.builder.RequiredArgumentBuilder;
+import it.unimi.dsi.fastutil.objects.Object2ByteMap;
+import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectIntPair;
+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 net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.fabricmc.fabric.api.event.player.UseBlockCallback;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.entity.ItemEntity;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.mob.AmbientEntity;
+import net.minecraft.entity.passive.BatEntity;
+import net.minecraft.item.FilledMapItem;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.item.map.MapState;
+import net.minecraft.resource.Resource;
+import net.minecraft.text.Text;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.world.World;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.joml.Vector2ic;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.zip.InflaterInputStream;
+
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument;
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
+
+public class DungeonSecrets {
+ protected static final Logger LOGGER = LoggerFactory.getLogger(DungeonSecrets.class);
+ private static final String DUNGEONS_PATH = "dungeons";
+ /**
+ * Maps the block identifier string to a custom numeric block id used in dungeon rooms data.
+ *
+ * @implNote Not using {@link net.minecraft.registry.Registry#getId(Object) Registry#getId(Block)} and {@link net.minecraft.block.Blocks Blocks} since this is also used by {@link de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonRoomsDFU DungeonRoomsDFU}, which runs outside of Minecraft.
+ */
+ @SuppressWarnings("JavadocReference")
+ protected static final Object2ByteMap<String> NUMERIC_ID = new Object2ByteOpenHashMap<>(Map.ofEntries(
+ Map.entry("minecraft:stone", (byte) 1),
+ Map.entry("minecraft:diorite", (byte) 2),
+ Map.entry("minecraft:polished_diorite", (byte) 3),
+ Map.entry("minecraft:andesite", (byte) 4),
+ Map.entry("minecraft:polished_andesite", (byte) 5),
+ Map.entry("minecraft:grass_block", (byte) 6),
+ Map.entry("minecraft:dirt", (byte) 7),
+ Map.entry("minecraft:coarse_dirt", (byte) 8),
+ Map.entry("minecraft:cobblestone", (byte) 9),
+ Map.entry("minecraft:bedrock", (byte) 10),
+ Map.entry("minecraft:oak_leaves", (byte) 11),
+ Map.entry("minecraft:gray_wool", (byte) 12),
+ Map.entry("minecraft:double_stone_slab", (byte) 13),
+ Map.entry("minecraft:mossy_cobblestone", (byte) 14),
+ Map.entry("minecraft:clay", (byte) 15),
+ Map.entry("minecraft:stone_bricks", (byte) 16),
+ Map.entry("minecraft:mossy_stone_bricks", (byte) 17),
+ Map.entry("minecraft:chiseled_stone_bricks", (byte) 18),
+ Map.entry("minecraft:gray_terracotta", (byte) 19),
+ Map.entry("minecraft:cyan_terracotta", (byte) 20),
+ Map.entry("minecraft:black_terracotta", (byte) 21)
+ ));
+ /**
+ * Block data for dungeon rooms. See {@link de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonRoomsDFU DungeonRoomsDFU} for format details and how it's generated.
+ * All access to this map must check {@link #isRoomsLoaded()} to prevent concurrent modification.
+ */
+ @SuppressWarnings("JavadocReference")
+ protected static final HashMap<String, Map<String, Map<String, int[]>>> ROOMS_DATA = new HashMap<>();
+ @NotNull
+ private static final Map<Vector2ic, Room> rooms = new HashMap<>();
+ private static final Map<String, JsonElement> roomsJson = new HashMap<>();
+ private static final Map<String, JsonElement> waypointsJson = new HashMap<>();
+ @Nullable
+ private static CompletableFuture<Void> roomsLoaded;
+ /**
+ * The map position of the top left corner of the entrance room.
+ */
+ @Nullable
+ private static Vector2ic mapEntrancePos;
+ /**
+ * The size of a room on the map.
+ */
+ private static int mapRoomSize;
+ /**
+ * The physical position of the northwest corner of the entrance room.
+ */
+ @Nullable
+ private static Vector2ic physicalEntrancePos;
+ private static Room currentRoom;
+
+ public static boolean isRoomsLoaded() {
+ return roomsLoaded != null && roomsLoaded.isDone();
+ }
+
+ @SuppressWarnings("unused")
+ public static JsonObject getRoomMetadata(String room) {
+ return roomsJson.get(room).getAsJsonObject();
+ }
+
+ public static JsonArray getRoomWaypoints(String room) {
+ return waypointsJson.get(room).getAsJsonArray();
+ }
+
+ /**
+ * Loads the dungeon secrets asynchronously from {@code /assets/skyblocker/dungeons}.
+ * Use {@link #isRoomsLoaded()} to check for completion of loading.
+ */
+ public static void init() {
+ if (SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.noInitSecretWaypoints) {
+ return;
+ }
+ // Execute with MinecraftClient as executor since we need to wait for MinecraftClient#resourceManager to be set
+ CompletableFuture.runAsync(DungeonSecrets::load, MinecraftClient.getInstance()).exceptionally(e -> {
+ LOGGER.error("[Skyblocker] Failed to load dungeon secrets", e);
+ return null;
+ });
+ Scheduler.INSTANCE.scheduleCyclic(DungeonSecrets::update, 10);
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(DungeonSecrets::render);
+ ClientReceiveMessageEvents.GAME.register(DungeonSecrets::onChatMessage);
+ ClientReceiveMessageEvents.GAME_CANCELED.register(DungeonSecrets::onChatMessage);
+ UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> onUseBlock(world, hitResult));
+ ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("secrets")
+ .then(literal("markAsFound").then(markSecretsCommand(true)))
+ .then(literal("markAsMissing").then(markSecretsCommand(false)))))));
+ ClientPlayConnectionEvents.JOIN.register(((handler, sender, client) -> reset()));
+ }
+
+ private static void load() {
+ long startTime = System.currentTimeMillis();
+ List<CompletableFuture<Void>> dungeonFutures = new ArrayList<>();
+ for (Map.Entry<Identifier, Resource> resourceEntry : MinecraftClient.getInstance().getResourceManager().findResources(DUNGEONS_PATH, id -> id.getPath().endsWith(".skeleton")).entrySet()) {
+ String[] path = resourceEntry.getKey().getPath().split("/");
+ if (path.length != 4) {
+ LOGGER.error("[Skyblocker] Failed to load dungeon secrets, invalid resource identifier {}", resourceEntry.getKey());
+ break;
+ }
+ String dungeon = path[1];
+ String roomShape = path[2];
+ String room = path[3].substring(0, path[3].length() - ".skeleton".length());
+ ROOMS_DATA.computeIfAbsent(dungeon, dungeonKey -> new HashMap<>());
+ ROOMS_DATA.get(dungeon).computeIfAbsent(roomShape, roomShapeKey -> new HashMap<>());
+ dungeonFutures.add(CompletableFuture.supplyAsync(() -> readRoom(resourceEntry.getValue())).thenAcceptAsync(rooms -> {
+ Map<String, int[]> roomsMap = ROOMS_DATA.get(dungeon).get(roomShape);
+ synchronized (roomsMap) {
+ roomsMap.put(room, rooms);
+ }
+ LOGGER.debug("[Skyblocker] Loaded dungeon secrets dungeon {} room shape {} room {}", dungeon, roomShape, room);
+ }).exceptionally(e -> {
+ LOGGER.error("[Skyblocker] Failed to load dungeon secrets dungeon {} room shape {} room {}", dungeon, roomShape, room, e);
+ return null;
+ }));
+ }
+ 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"))) {
+ loadJson(roomsReader, roomsJson);
+ loadJson(waypointsReader, waypointsJson);
+ LOGGER.debug("[Skyblocker] Loaded dungeon secrets json");
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Failed to load dungeon secrets json", e);
+ }
+ }));
+ roomsLoaded = CompletableFuture.allOf(dungeonFutures.toArray(CompletableFuture[]::new)).thenRun(() -> LOGGER.info("[Skyblocker] Loaded dungeon secrets for {} dungeon(s), {} room shapes, and {} rooms total in {} ms", ROOMS_DATA.size(), ROOMS_DATA.values().stream().mapToInt(Map::size).sum(), ROOMS_DATA.values().stream().map(Map::values).flatMap(Collection::stream).mapToInt(Map::size).sum(), System.currentTimeMillis() - startTime)).exceptionally(e -> {
+ LOGGER.error("[Skyblocker] Failed to load dungeon secrets", e);
+ return null;
+ });
+ LOGGER.info("[Skyblocker] Started loading dungeon secrets in (blocked main thread for) {} ms", System.currentTimeMillis() - startTime);
+ }
+
+ private static int[] readRoom(Resource resource) throws RuntimeException {
+ try (ObjectInputStream in = new ObjectInputStream(new InflaterInputStream(resource.getInputStream()))) {
+ return (int[]) in.readObject();
+ } catch (IOException | ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Loads the json from the given {@link BufferedReader} into the given {@link Map}.
+ * @param reader the reader to read the json from
+ * @param map the map to load into
+ */
+ private static void loadJson(BufferedReader reader, Map<String, JsonElement> map) {
+ SkyblockerMod.GSON.fromJson(reader, JsonObject.class).asMap().forEach((room, jsonElement) -> map.put(room.toLowerCase().replaceAll(" ", "-"), jsonElement));
+ }
+
+ private static ArgumentBuilder<FabricClientCommandSource, RequiredArgumentBuilder<FabricClientCommandSource, Integer>> markSecretsCommand(boolean found) {
+ return argument("secret", IntegerArgumentType.integer()).executes(context -> {
+ int secretIndex = IntegerArgumentType.getInteger(context, "secret");
+ if (markSecrets(secretIndex, found)) {
+ context.getSource().sendFeedback(Text.translatable(found ? "skyblocker.dungeons.secrets.markSecretFound" : "skyblocker.dungeons.secrets.markSecretMissing", secretIndex));
+ } else {
+ context.getSource().sendError(Text.translatable(found ? "skyblocker.dungeons.secrets.markSecretFoundUnable" : "skyblocker.dungeons.secrets.markSecretMissingUnable", secretIndex));
+ }
+ return Command.SINGLE_SUCCESS;
+ });
+ }
+
+ /**
+ * Updates the dungeon. The general idea is similar to the Dungeon Rooms Mod.
+ * <p></p>
+ * When entering a new dungeon, this method:
+ * <ul>
+ * <li> Gets the physical northwest corner position of the entrance room and saves it in {@link #physicalEntrancePos}. </li>
+ * <li> Do nothing until the dungeon map exists. </li>
+ * <li> Gets the upper left corner of entrance room on the map and saves it in {@link #mapEntrancePos}. </li>
+ * <li> Gets the size of a room on the map in pixels and saves it in {@link #mapRoomSize}. </li>
+ * <li> Creates a new {@link Room} with {@link Room.Type} {@link Room.Type.ENTRANCE ENTRANCE} and sets {@link #currentRoom}. </li>
+ * </ul>
+ * When processing an existing dungeon, this method:
+ * <ul>
+ * <li> Calculates the physical northwest corner and upper left corner on the map of the room the player is currently in. </li>
+ * <li> Gets the room type based on the map color. </li>
+ * <li> If the room has not been created (when the physical northwest corner is not in {@link #rooms}):</li>
+ * <ul>
+ * <li> If the room type is {@link Room.Type.ROOM}, gets the northwest corner of all connected room segments with {@link DungeonMapUtils#getRoomSegments(MapState, Vector2ic, int, byte)}. (For example, a 1x2 room has two room segments.) </li>
+ * <li> Create a new room. </li>
+ * </ul>
+ * <li> Sets {@link #currentRoom} to the current room, either created from the previous step or from {@link #rooms}. </li>
+ * <li> Calls {@link Room#update()} on {@link #currentRoom}. </li>
+ * </ul>
+ */
+ @SuppressWarnings("JavadocReference")
+ private static void update() {
+ if (!SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableSecretWaypoints) {
+ return;
+ }
+ if (!Utils.isInDungeons()) {
+ if (mapEntrancePos != null) {
+ reset();
+ }
+ return;
+ }
+ MinecraftClient client = MinecraftClient.getInstance();
+ ClientPlayerEntity player = client.player;
+ if (player == null || client.world == null) {
+ return;
+ }
+ if (physicalEntrancePos == null) {
+ Vec3d playerPos = player.getPos();
+ physicalEntrancePos = DungeonMapUtils.getPhysicalRoomPos(playerPos);
+ currentRoom = newRoom(Room.Type.ENTRANCE, physicalEntrancePos);
+ }
+ ItemStack stack = player.getInventory().main.get(8);
+ if (!stack.isOf(Items.FILLED_MAP)) {
+ return;
+ }
+ MapState map = FilledMapItem.getMapState(FilledMapItem.getMapId(stack), client.world);
+ if (map == null) {
+ return;
+ }
+ if (mapEntrancePos == null || mapRoomSize == 0) {
+ ObjectIntPair<Vector2ic> mapEntrancePosAndSize = DungeonMapUtils.getMapEntrancePosAndRoomSize(map);
+ if (mapEntrancePosAndSize == null) {
+ return;
+ }
+ mapEntrancePos = mapEntrancePosAndSize.left();
+ mapRoomSize = mapEntrancePosAndSize.rightInt();
+ LOGGER.info("[Skyblocker] Started dungeon with map room size {}, map entrance pos {}, player pos {}, and physical entrance pos {}", mapRoomSize, mapEntrancePos, client.player.getPos(), physicalEntrancePos);
+ }
+
+ Vector2ic physicalPos = DungeonMapUtils.getPhysicalRoomPos(client.player.getPos());
+ Vector2ic mapPos = DungeonMapUtils.getMapPosFromPhysical(physicalEntrancePos, mapEntrancePos, mapRoomSize, physicalPos);
+ Room room = rooms.get(physicalPos);
+ if (room == null) {
+ Room.Type type = DungeonMapUtils.getRoomType(map, mapPos);
+ if (type == null || type == Room.Type.UNKNOWN) {
+ return;
+ }
+ switch (type) {
+ case ENTRANCE, PUZZLE, TRAP, MINIBOSS, FAIRY, BLOOD -> room = newRoom(type, physicalPos);
+ case ROOM -> room = newRoom(type, DungeonMapUtils.getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, DungeonMapUtils.getRoomSegments(map, mapPos, mapRoomSize, type.color)));
+ }
+ }
+ if (room != null && currentRoom != room) {
+ currentRoom = room;
+ }
+ currentRoom.update();
+ }
+
+ /**
+ * Creates a new room with the given type and physical positions,
+ * adds the room to {@link #rooms}, and sets {@link #currentRoom} to the new room.
+ *
+ * @param type the type of room to create
+ * @param physicalPositions the physical positions of the room
+ */
+ @Nullable
+ private static Room newRoom(Room.Type type, Vector2ic... physicalPositions) {
+ try {
+ Room newRoom = new Room(type, physicalPositions);
+ for (Vector2ic physicalPos : physicalPositions) {
+ rooms.put(physicalPos, newRoom);
+ }
+ return newRoom;
+ } catch (IllegalArgumentException e) {
+ LOGGER.error("[Skyblocker] Failed to create room", e);
+ }
+ return null;
+ }
+
+ /**
+ * Renders the secret waypoints in {@link #currentRoom} if {@link #isCurrentRoomMatched()}.
+ */
+ private static void render(WorldRenderContext context) {
+ if (isCurrentRoomMatched()) {
+ currentRoom.render(context);
+ }
+ }
+
+ /**
+ * Calls {@link Room#onChatMessage(String)} on {@link #currentRoom} if the message is an overlay message and {@link #isCurrentRoomMatched()}.
+ * Used to detect when all secrets in a room are found.
+ */
+ private static void onChatMessage(Text text, boolean overlay) {
+ if (overlay && isCurrentRoomMatched()) {
+ currentRoom.onChatMessage(text.getString());
+ }
+ }
+
+ /**
+ * Calls {@link Room#onUseBlock(World, BlockHitResult)} on {@link #currentRoom} if {@link #isCurrentRoomMatched()}.
+ * Used to detect finding {@link SecretWaypoint.Category.CHEST} and {@link SecretWaypoint.Category.WITHER} secrets.
+ *
+ * @return {@link ActionResult#PASS}
+ */
+ @SuppressWarnings("JavadocReference")
+ private static ActionResult onUseBlock(World world, BlockHitResult hitResult) {
+ if (isCurrentRoomMatched()) {
+ currentRoom.onUseBlock(world, hitResult);
+ }
+ return ActionResult.PASS;
+ }
+
+ /**
+ * Calls {@link Room#onItemPickup(ItemEntity, LivingEntity)} on the room the {@code collector} is in if that room {@link #isRoomMatched(Room)}.
+ * Used to detect finding {@link SecretWaypoint.Category.ITEM} secrets.
+ * If the collector is the player, {@link #currentRoom} is used as an optimization.
+ */
+ @SuppressWarnings("JavadocReference")
+ public static void onItemPickup(ItemEntity itemEntity, LivingEntity collector, boolean isPlayer) {
+ if (isPlayer) {
+ if (isCurrentRoomMatched()) {
+ currentRoom.onItemPickup(itemEntity, collector);
+ }
+ } else {
+ Room room = getRoomAtPhysical(collector.getPos());
+ if (isRoomMatched(room)) {
+ room.onItemPickup(itemEntity, collector);
+ }
+ }
+ }
+
+ /**
+ * Calls {@link Room#onBatRemoved(BatEntity)} on the room the {@code bat} is in if that room {@link #isRoomMatched(Room)}.
+ * Used to detect finding {@link SecretWaypoint.Category.BAT} secrets.
+ */
+ @SuppressWarnings("JavadocReference")
+ public static void onBatRemoved(AmbientEntity bat) {
+ Room room = getRoomAtPhysical(bat.getPos());
+ if (isRoomMatched(room)) {
+ room.onBatRemoved(bat);
+ }
+ }
+
+ public static boolean markSecrets(int secretIndex, boolean found) {
+ if (isCurrentRoomMatched()) {
+ return currentRoom.markSecrets(secretIndex, found);
+ }
+ return false;
+ }
+
+ /**
+ * Gets the room at the given physical position.
+ *
+ * @param pos the physical position
+ * @return the room at the given physical position, or null if there is no room at the given physical position
+ * @see #rooms
+ * @see DungeonMapUtils#getPhysicalRoomPos(Vec3d)
+ */
+ @Nullable
+ private static Room getRoomAtPhysical(Vec3d pos) {
+ return rooms.get(DungeonMapUtils.getPhysicalRoomPos(pos));
+ }
+
+ /**
+ * Calls {@link #isRoomMatched(Room)} on {@link #currentRoom}.
+ *
+ * @return {@code true} if {@link #currentRoom} is not null and {@link #isRoomMatched(Room)}
+ */
+ private static boolean isCurrentRoomMatched() {
+ return isRoomMatched(currentRoom);
+ }
+
+ /**
+ * Calls {@link #shouldProcess()} and {@link Room#isMatched()} on the given room.
+ *
+ * @param room the room to check
+ * @return {@code true} if {@link #shouldProcess()}, the given room is not null, and {@link Room#isMatched()} on the given room
+ */
+ @Contract("null -> false")
+ private static boolean isRoomMatched(@Nullable Room room) {
+ return shouldProcess() && room != null && room.isMatched();
+ }
+
+ /**
+ * Checks if the player is in a dungeon and {@link SkyblockerConfigManager.Dungeons#secretWaypoints Secret Waypoints} is enabled.
+ *
+ * @return whether dungeon secrets should be processed
+ */
+ private static boolean shouldProcess() {
+ return SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableSecretWaypoints && Utils.isInDungeons();
+ }
+
+ /**
+ * Resets fields when leaving a dungeon.
+ */
+ private static void reset() {
+ mapEntrancePos = null;
+ mapRoomSize = 0;
+ physicalEntrancePos = null;
+ rooms.clear();
+ currentRoom = null;
+ }
+}
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
new file mode 100644
index 00000000..dd7dc91e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java
@@ -0,0 +1,473 @@
+package de.hysky.skyblocker.skyblock.dungeon.secrets;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableTable;
+import com.google.common.collect.Table;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import it.unimi.dsi.fastutil.ints.IntRBTreeSet;
+import it.unimi.dsi.fastutil.ints.IntSortedSet;
+import it.unimi.dsi.fastutil.ints.IntSortedSets;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.util.TriState;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.Blocks;
+import net.minecraft.block.MapColor;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.ItemEntity;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.mob.AmbientEntity;
+import net.minecraft.registry.Registries;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.world.World;
+import org.apache.commons.lang3.tuple.MutableTriple;
+import org.apache.commons.lang3.tuple.Triple;
+import org.jetbrains.annotations.NotNull;
+import org.joml.Vector2i;
+import org.joml.Vector2ic;
+
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class Room {
+ private static final Pattern SECRETS = Pattern.compile("§7(\\d{1,2})/(\\d{1,2}) Secrets");
+ @NotNull
+ private final Type type;
+ @NotNull
+ private final Set<Vector2ic> segments;
+ /**
+ * The shape of the room. See {@link #getShape(IntSortedSet, IntSortedSet)}.
+ */
+ @NotNull
+ private final Shape shape;
+ /**
+ * The room data containing all rooms for a specific dungeon and {@link #shape}.
+ */
+ private Map<String, int[]> roomsData;
+ /**
+ * Contains all possible dungeon rooms for this room. The list is gradually shrunk by checking blocks until only one room is left.
+ */
+ private List<MutableTriple<Direction, Vector2ic, List<String>>> possibleRooms;
+ /**
+ * Contains all blocks that have been checked to prevent checking the same block multiple times.
+ */
+ private Set<BlockPos> checkedBlocks = new HashSet<>();
+ /**
+ * The task that is used to check blocks. This is used to ensure only one such task can run at a time.
+ */
+ private CompletableFuture<Void> findRoom;
+ private int doubleCheckBlocks;
+ /**
+ * Represents the matching state of the room with the following possible values:
+ * <li>{@link TriState#DEFAULT} means that the room has not been checked, is being processed, or does not {@link Type#needsScanning() need to be processed}.
+ * <li>{@link TriState#FALSE} means that the room has been checked and there is no match.
+ * <li>{@link TriState#TRUE} means that the room has been checked and there is a match.
+ */
+ private TriState matched = TriState.DEFAULT;
+ private Table<Integer, BlockPos, SecretWaypoint> secretWaypoints;
+
+ public Room(@NotNull Type type, @NotNull Vector2ic... physicalPositions) {
+ this.type = type;
+ segments = Set.of(physicalPositions);
+ IntSortedSet segmentsX = IntSortedSets.unmodifiable(new IntRBTreeSet(segments.stream().mapToInt(Vector2ic::x).toArray()));
+ IntSortedSet segmentsY = IntSortedSets.unmodifiable(new IntRBTreeSet(segments.stream().mapToInt(Vector2ic::y).toArray()));
+ shape = getShape(segmentsX, segmentsY);
+ roomsData = DungeonSecrets.ROOMS_DATA.getOrDefault("catacombs", Collections.emptyMap()).getOrDefault(shape.shape.toLowerCase(), Collections.emptyMap());
+ possibleRooms = getPossibleRooms(segmentsX, segmentsY);
+ }
+
+ @NotNull
+ public Type getType() {
+ return type;
+ }
+
+ public boolean isMatched() {
+ return matched == TriState.TRUE;
+ }
+
+ @Override
+ public String toString() {
+ return "Room{type=" + type + ", shape=" + shape + ", matched=" + matched + ", segments=" + Arrays.toString(segments.toArray()) + "}";
+ }
+
+ @NotNull
+ private Shape getShape(IntSortedSet segmentsX, IntSortedSet segmentsY) {
+ return switch (segments.size()) {
+ case 1 -> Shape.ONE_BY_ONE;
+ case 2 -> Shape.ONE_BY_TWO;
+ case 3 -> segmentsX.size() == 2 && segmentsY.size() == 2 ? Shape.L_SHAPE : Shape.ONE_BY_THREE;
+ case 4 -> segmentsX.size() == 2 && segmentsY.size() == 2 ? Shape.TWO_BY_TWO : Shape.ONE_BY_FOUR;
+ default -> throw new IllegalArgumentException("There are no matching room shapes with this set of physical positions: " + Arrays.toString(segments.toArray()));
+ };
+ }
+
+ private List<MutableTriple<Direction, Vector2ic, List<String>>> getPossibleRooms(IntSortedSet segmentsX, IntSortedSet segmentsY) {
+ List<String> possibleDirectionRooms = new ArrayList<>(roomsData.keySet());
+ List<MutableTriple<Direction, Vector2ic, List<String>>> possibleRooms = new ArrayList<>();
+ for (Direction direction : getPossibleDirections(segmentsX, segmentsY)) {
+ possibleRooms.add(MutableTriple.of(direction, DungeonMapUtils.getPhysicalCornerPos(direction, segmentsX, segmentsY), possibleDirectionRooms));
+ }
+ return possibleRooms;
+ }
+
+ @NotNull
+ private Direction[] getPossibleDirections(IntSortedSet segmentsX, IntSortedSet segmentsY) {
+ return switch (shape) {
+ case ONE_BY_ONE, TWO_BY_TWO -> Direction.values();
+ case ONE_BY_TWO, ONE_BY_THREE, ONE_BY_FOUR -> {
+ if (segmentsX.size() > 1 && segmentsY.size() == 1) {
+ yield new Direction[]{Direction.NW, Direction.SE};
+ } else if (segmentsX.size() == 1 && segmentsY.size() > 1) {
+ yield new Direction[]{Direction.NE, Direction.SW};
+ }
+ throw new IllegalArgumentException("Shape " + shape.shape + " does not match segments: " + Arrays.toString(segments.toArray()));
+ }
+ case L_SHAPE -> {
+ if (!segments.contains(new Vector2i(segmentsX.firstInt(), segmentsY.firstInt()))) {
+ yield new Direction[]{Direction.SW};
+ } else if (!segments.contains(new Vector2i(segmentsX.firstInt(), segmentsY.lastInt()))) {
+ yield new Direction[]{Direction.SE};
+ } else if (!segments.contains(new Vector2i(segmentsX.lastInt(), segmentsY.firstInt()))) {
+ yield new Direction[]{Direction.NW};
+ } else if (!segments.contains(new Vector2i(segmentsX.lastInt(), segmentsY.lastInt()))) {
+ yield new Direction[]{Direction.NE};
+ }
+ throw new IllegalArgumentException("Shape " + shape.shape + " does not match segments: " + Arrays.toString(segments.toArray()));
+ }
+ };
+ }
+
+ /**
+ * Updates the room.
+ * <p></p>
+ * This method returns immediately if any of the following conditions are met:
+ * <ul>
+ * <li> The room does not need to be scanned and matched. (When the room is not of type {@link Type.ROOM}, {@link Type.PUZZLE}, or {@link Type.TRAP}. See {@link Type#needsScanning()}) </li>
+ * <li> The room has been matched or failed to match and is on cooldown. See {@link #matched}. </li>
+ * <li> {@link #findRoom The previous update} has not completed. </li>
+ * </ul>
+ * Then this method tries to match this room through:
+ * <ul>
+ * <li> Iterate over a 11 by 11 by 11 box around the player. </li>
+ * <li> Check it the block is part of this room and not part of a doorway. See {@link #segments} and {@link #notInDoorway(BlockPos)}. </li>
+ * <li> Checks if the position has been checked and adds it to {@link #checkedBlocks}. </li>
+ * <li> Calls {@link #checkBlock(ClientWorld, BlockPos)} </li>
+ * </ul>
+ */
+ @SuppressWarnings("JavadocReference")
+ protected void update() {
+ // Logical AND has higher precedence than logical OR
+ if (!type.needsScanning() || matched != TriState.DEFAULT || !DungeonSecrets.isRoomsLoaded() || findRoom != null && !findRoom.isDone()) {
+ return;
+ }
+ MinecraftClient client = MinecraftClient.getInstance();
+ ClientPlayerEntity player = client.player;
+ ClientWorld world = client.world;
+ if (player == null || world == null) {
+ return;
+ }
+ findRoom = CompletableFuture.runAsync(() -> {
+ for (BlockPos pos : BlockPos.iterate(player.getBlockPos().add(-5, -5, -5), player.getBlockPos().add(5, 5, 5))) {
+ if (segments.contains(DungeonMapUtils.getPhysicalRoomPos(pos)) && notInDoorway(pos) && checkedBlocks.add(pos) && checkBlock(world, pos)) {
+ break;
+ }
+ }
+ });
+ }
+
+ private static boolean notInDoorway(BlockPos pos) {
+ if (pos.getY() < 66 || pos.getY() > 73) {
+ return true;
+ }
+ int x = MathHelper.floorMod(pos.getX() - 8, 32);
+ int z = MathHelper.floorMod(pos.getZ() - 8, 32);
+ return (x < 13 || x > 17 || z > 2 && z < 28) && (z < 13 || z > 17 || x > 2 && x < 28);
+ }
+
+ /**
+ * Filters out dungeon rooms which does not contain the block at the given position.
+ * <p></p>
+ * This method:
+ * <ul>
+ * <li> Checks if the block type is included in the dungeon rooms data. See {@link DungeonSecrets#NUMERIC_ID}. </li>
+ * <li> For each possible direction: </li>
+ * <ul>
+ * <li> Rotate and convert the position to a relative position. See {@link DungeonMapUtils#actualToRelative(Direction, Vector2ic, BlockPos)}. </li>
+ * <li> Encode the block based on the relative position and the custom numeric block id. See {@link #posIdToInt(BlockPos, byte)}. </li>
+ * <li> For each possible room in the current direction: </li>
+ * <ul>
+ * <li> Check if {@link #roomsData} contains the encoded block. </li>
+ * <li> If so, add the room to the new list of possible rooms for this direction. </li>
+ * </ul>
+ * <li> Replace the old possible room list for the current direction with the new one. </li>
+ * </ul>
+ * <li> If there are no matching rooms left: </li>
+ * <ul>
+ * <li> Terminate matching by setting {@link #matched} to {@link TriState#FALSE}. </li>
+ * <li> Schedule another matching attempt in 50 ticks (2.5 seconds). </li>
+ * <li> Reset {@link #possibleRooms} and {@link #checkedBlocks} with {@link #reset()}. </li>
+ * <li> Return {@code true} </li>
+ * </ul>
+ * <li> If there are exactly one room matching: </li>
+ * <ul>
+ * <li> Call {@link #roomMatched(String, Direction, Vector2ic)}. </li>
+ * <li> Discard the no longer needed fields to save memory. </li>
+ * <li> Return {@code true} </li>
+ * </ul>
+ * <li> Return {@code false} </li>
+ * </ul>
+ *
+ * @param world the world to get the block from
+ * @param pos the position of the block to check
+ * @return whether room matching should end. Either a match is found or there are no valid rooms left
+ */
+ private boolean checkBlock(ClientWorld world, BlockPos pos) {
+ byte id = DungeonSecrets.NUMERIC_ID.getByte(Registries.BLOCK.getId(world.getBlockState(pos).getBlock()).toString());
+ if (id == 0) {
+ return false;
+ }
+ for (MutableTriple<Direction, Vector2ic, List<String>> directionRooms : possibleRooms) {
+ int block = posIdToInt(DungeonMapUtils.actualToRelative(directionRooms.getLeft(), directionRooms.getMiddle(), pos), id);
+ List<String> possibleDirectionRooms = new ArrayList<>();
+ for (String room : directionRooms.getRight()) {
+ if (Arrays.binarySearch(roomsData.get(room), block) >= 0) {
+ possibleDirectionRooms.add(room);
+ }
+ }
+ directionRooms.setRight(possibleDirectionRooms);
+ }
+
+ int matchingRoomsSize = possibleRooms.stream().map(Triple::getRight).mapToInt(Collection::size).sum();
+ if (matchingRoomsSize == 0) {
+ // If no rooms match, reset the fields and scan again after 50 ticks.
+ matched = TriState.FALSE;
+ DungeonSecrets.LOGGER.warn("[Skyblocker] No dungeon room matches after checking {} block(s)", checkedBlocks.size());
+ Scheduler.INSTANCE.schedule(() -> matched = TriState.DEFAULT, 50);
+ reset();
+ return true;
+ } else if (matchingRoomsSize == 1 && ++doubleCheckBlocks >= 10) {
+ // If one room matches, load the secrets for that room and discard the no longer needed fields.
+ for (Triple<Direction, Vector2ic, List<String>> directionRooms : possibleRooms) {
+ if (directionRooms.getRight().size() == 1) {
+ roomMatched(directionRooms.getRight().get(0), directionRooms.getLeft(), directionRooms.getMiddle());
+ discard();
+ return true;
+ }
+ }
+ return false; // This should never happen, we just checked that there is one possible room, and the return true in the loop should activate
+ } else {
+ DungeonSecrets.LOGGER.debug("[Skyblocker] {} room(s) remaining after checking {} block(s)", matchingRoomsSize, checkedBlocks.size());
+ return false;
+ }
+ }
+
+ /**
+ * Encodes a {@link BlockPos} and the custom numeric block id into an integer.
+ *
+ * @param pos the position of the block
+ * @param id the custom numeric block id
+ * @return the encoded integer
+ */
+ private int posIdToInt(BlockPos pos, byte id) {
+ return pos.getX() << 24 | pos.getY() << 16 | pos.getZ() << 8 | id;
+ }
+
+ /**
+ * Loads the secret waypoints for the room from {@link DungeonSecrets#waypointsJson} once it has been matched
+ * and sets {@link #matched} to {@link TriState#TRUE}.
+ *
+ * @param directionRooms the direction, position, and name of the room
+ */
+ @SuppressWarnings("JavadocReference")
+ private void roomMatched(String name, Direction direction, Vector2ic physicalCornerPos) {
+ Table<Integer, BlockPos, SecretWaypoint> secretWaypointsMutable = HashBasedTable.create();
+ for (JsonElement waypointElement : DungeonSecrets.getRoomWaypoints(name)) {
+ JsonObject waypoint = waypointElement.getAsJsonObject();
+ String secretName = waypoint.get("secretName").getAsString();
+ int secretIndex = Integer.parseInt(secretName.substring(0, Character.isDigit(secretName.charAt(1)) ? 2 : 1));
+ BlockPos pos = DungeonMapUtils.relativeToActual(direction, physicalCornerPos, waypoint);
+ secretWaypointsMutable.put(secretIndex, pos, new SecretWaypoint(secretIndex, waypoint, secretName, pos));
+ }
+ secretWaypoints = ImmutableTable.copyOf(secretWaypointsMutable);
+ matched = TriState.TRUE;
+ DungeonSecrets.LOGGER.info("[Skyblocker] Room {} matched after checking {} block(s)", name, checkedBlocks.size());
+ }
+
+ /**
+ * Resets fields for another round of matching after room matching fails.
+ */
+ private void reset() {
+ IntSortedSet segmentsX = IntSortedSets.unmodifiable(new IntRBTreeSet(segments.stream().mapToInt(Vector2ic::x).toArray()));
+ IntSortedSet segmentsY = IntSortedSets.unmodifiable(new IntRBTreeSet(segments.stream().mapToInt(Vector2ic::y).toArray()));
+ possibleRooms = getPossibleRooms(segmentsX, segmentsY);
+ checkedBlocks = new HashSet<>();
+ doubleCheckBlocks = 0;
+ }
+
+ /**
+ * Discards fields after room matching completes when a room is found.
+ * These fields are no longer needed and are discarded to save memory.
+ */
+ private void discard() {
+ roomsData = null;
+ possibleRooms = null;
+ checkedBlocks = null;
+ doubleCheckBlocks = 0;
+ }
+
+ /**
+ * Calls {@link SecretWaypoint#render(WorldRenderContext)} on {@link #secretWaypoints all secret waypoints}.
+ */
+ protected void render(WorldRenderContext context) {
+ for (SecretWaypoint secretWaypoint : secretWaypoints.values()) {
+ if (secretWaypoint.shouldRender()) {
+ secretWaypoint.render(context);
+ }
+ }
+ }
+
+ /**
+ * Sets all secrets as found if {@link #isAllSecretsFound(String)}.
+ */
+ protected void onChatMessage(String message) {
+ if (isAllSecretsFound(message)) {
+ secretWaypoints.values().forEach(SecretWaypoint::setFound);
+ }
+ }
+
+ /**
+ * Checks if the number of found secrets is equals or greater than the total number of secrets in the room.
+ *
+ * @param message the message to check in
+ * @return whether the number of found secrets is equals or greater than the total number of secrets in the room
+ */
+ protected static boolean isAllSecretsFound(String message) {
+ Matcher matcher = SECRETS.matcher(message);
+ if (matcher.find()) {
+ return Integer.parseInt(matcher.group(1)) >= Integer.parseInt(matcher.group(2));
+ }
+ return false;
+ }
+
+ /**
+ * Marks the secret at the interaction position as found when the player interacts with a chest or a player head,
+ * if there is a secret at the interaction position.
+ *
+ * @param world the world to get the block from
+ * @param hitResult the block being interacted with
+ * @see #onSecretFound(SecretWaypoint, String, Object...)
+ */
+ protected void onUseBlock(World world, BlockHitResult hitResult) {
+ BlockState state = world.getBlockState(hitResult.getBlockPos());
+ if (state.isOf(Blocks.CHEST) || state.isOf(Blocks.PLAYER_HEAD) || state.isOf(Blocks.PLAYER_WALL_HEAD)) {
+ secretWaypoints.column(hitResult.getBlockPos()).values().stream().filter(SecretWaypoint::needsInteraction).findAny()
+ .ifPresent(secretWaypoint -> onSecretFound(secretWaypoint, "[Skyblocker] Detected {} interaction, setting secret #{} as found", secretWaypoint.category, secretWaypoint.secretIndex));
+ } else if (state.isOf(Blocks.LEVER)) {
+ secretWaypoints.column(hitResult.getBlockPos()).values().stream().filter(SecretWaypoint::isLever).forEach(SecretWaypoint::setFound);
+ }
+ }
+
+ /**
+ * Marks the closest secret that requires item pickup no greater than 6 blocks away as found when the player picks up a secret item.
+ *
+ * @param itemEntity the item entity being picked up
+ * @param collector the collector of the item
+ * @see #onSecretFound(SecretWaypoint, String, Object...)
+ */
+ protected void onItemPickup(ItemEntity itemEntity, LivingEntity collector) {
+ if (SecretWaypoint.SECRET_ITEMS.stream().noneMatch(itemEntity.getStack().getName().getString()::contains)) {
+ return;
+ }
+ secretWaypoints.values().stream().filter(SecretWaypoint::needsItemPickup).min(Comparator.comparingDouble(SecretWaypoint.getSquaredDistanceToFunction(collector))).filter(SecretWaypoint.getRangePredicate(collector))
+ .ifPresent(secretWaypoint -> onSecretFound(secretWaypoint, "[Skyblocker] Detected {} picked up a {} from a {} secret, setting secret #{} as found", collector.getName().getString(), itemEntity.getName().getString(), secretWaypoint.category, secretWaypoint.secretIndex));
+ }
+
+ /**
+ * Marks the closest bat secret as found when a bat is killed.
+ *
+ * @param bat the bat being killed
+ * @see #onSecretFound(SecretWaypoint, String, Object...)
+ */
+ protected void onBatRemoved(AmbientEntity bat) {
+ secretWaypoints.values().stream().filter(SecretWaypoint::isBat).min(Comparator.comparingDouble(SecretWaypoint.getSquaredDistanceToFunction(bat)))
+ .ifPresent(secretWaypoint -> onSecretFound(secretWaypoint, "[Skyblocker] Detected {} killed for a {} secret, setting secret #{} as found", bat.getName().getString(), secretWaypoint.category, secretWaypoint.secretIndex));
+ }
+
+ /**
+ * Marks all secret waypoints with the same index as the given {@link SecretWaypoint} as found.
+ *
+ * @param secretWaypoint the secret waypoint to read the index from.
+ * @param msg the message to log
+ * @param args the args for the {@link org.slf4j.Logger#info(String, Object...) Logger#info(String, Object...)} call
+ */
+ private void onSecretFound(SecretWaypoint secretWaypoint, String msg, Object... args) {
+ secretWaypoints.row(secretWaypoint.secretIndex).values().forEach(SecretWaypoint::setFound);
+ DungeonSecrets.LOGGER.info(msg, args);
+ }
+
+ protected boolean markSecrets(int secretIndex, boolean found) {
+ Map<BlockPos, SecretWaypoint> secret = secretWaypoints.row(secretIndex);
+ if (secret.isEmpty()) {
+ return false;
+ } else {
+ secret.values().forEach(found ? SecretWaypoint::setFound : SecretWaypoint::setMissing);
+ return true;
+ }
+ }
+
+ public enum Type {
+ ENTRANCE(MapColor.DARK_GREEN.getRenderColorByte(MapColor.Brightness.HIGH)),
+ ROOM(MapColor.ORANGE.getRenderColorByte(MapColor.Brightness.LOWEST)),
+ PUZZLE(MapColor.MAGENTA.getRenderColorByte(MapColor.Brightness.HIGH)),
+ TRAP(MapColor.ORANGE.getRenderColorByte(MapColor.Brightness.HIGH)),
+ MINIBOSS(MapColor.YELLOW.getRenderColorByte(MapColor.Brightness.HIGH)),
+ FAIRY(MapColor.PINK.getRenderColorByte(MapColor.Brightness.HIGH)),
+ BLOOD(MapColor.BRIGHT_RED.getRenderColorByte(MapColor.Brightness.HIGH)),
+ UNKNOWN(MapColor.GRAY.getRenderColorByte(MapColor.Brightness.NORMAL));
+ final byte color;
+
+ Type(byte color) {
+ this.color = color;
+ }
+
+ /**
+ * @return whether this room type has secrets and needs to be scanned and matched.
+ */
+ private boolean needsScanning() {
+ return switch (this) {
+ case ROOM, PUZZLE, TRAP -> true;
+ default -> false;
+ };
+ }
+ }
+
+ private enum Shape {
+ ONE_BY_ONE("1x1"),
+ ONE_BY_TWO("1x2"),
+ ONE_BY_THREE("1x3"),
+ ONE_BY_FOUR("1x4"),
+ L_SHAPE("L-shape"),
+ TWO_BY_TWO("2x2");
+ final String shape;
+
+ Shape(String shape) {
+ this.shape = shape;
+ }
+
+ @Override
+ public String toString() {
+ return shape;
+ }
+ }
+
+ public enum Direction {
+ NW, NE, SW, SE
+ }
+}
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
new file mode 100644
index 00000000..d2a31ea3
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java
@@ -0,0 +1,142 @@
+package de.hysky.skyblocker.skyblock.dungeon.secrets;
+
+import com.google.gson.JsonObject;
+
+import de.hysky.skyblocker.config.SkyblockerConfig;
+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.client.MinecraftClient;
+import net.minecraft.entity.Entity;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Vec3d;
+
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.function.ToDoubleFunction;
+
+public class SecretWaypoint {
+ static final List<String> SECRET_ITEMS = List.of("Decoy", "Defuse Kit", "Dungeon Chest Key", "Healing VIII", "Inflatable Jerry", "Spirit Leap", "Training Weights", "Trap", "Treasure Talisman");
+ final int secretIndex;
+ final Category category;
+ private final Text name;
+ private final BlockPos pos;
+ private final Vec3d centerPos;
+ private boolean missing;
+
+ SecretWaypoint(int secretIndex, JsonObject waypoint, String name, BlockPos pos) {
+ this.secretIndex = secretIndex;
+ this.category = Category.get(waypoint);
+ this.name = Text.of(name);
+ this.pos = pos;
+ this.centerPos = pos.toCenterPos();
+ this.missing = true;
+ }
+
+ static ToDoubleFunction<SecretWaypoint> getSquaredDistanceToFunction(Entity entity) {
+ return secretWaypoint -> entity.squaredDistanceTo(secretWaypoint.centerPos);
+ }
+
+ static Predicate<SecretWaypoint> getRangePredicate(Entity entity) {
+ return secretWaypoint -> entity.squaredDistanceTo(secretWaypoint.centerPos) <= 36D;
+ }
+
+ boolean shouldRender() {
+ return category.isEnabled() && missing;
+ }
+
+ boolean needsInteraction() {
+ return category.needsInteraction();
+ }
+
+ boolean isLever() {
+ return category.isLever();
+ }
+
+ boolean needsItemPickup() {
+ return category.needsItemPickup();
+ }
+
+ boolean isBat() {
+ return category.isBat();
+ }
+
+ void setFound() {
+ this.missing = false;
+ }
+
+ void setMissing() {
+ this.missing = true;
+ }
+
+ /**
+ * Renders the secret waypoint, including a filled cube, a beacon beam, the name, and the distance from the player.
+ */
+ void render(WorldRenderContext context) {
+ RenderHelper.renderFilledThroughWallsWithBeaconBeam(context, pos, category.colorComponents, 0.5F);
+ 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);
+ }
+
+ enum Category {
+ ENTRANCE(secretWaypoints -> secretWaypoints.enableEntranceWaypoints, 0, 255, 0),
+ SUPERBOOM(secretWaypoints -> secretWaypoints.enableSuperboomWaypoints, 255, 0, 0),
+ CHEST(secretWaypoints -> secretWaypoints.enableChestWaypoints, 2, 213, 250),
+ ITEM(secretWaypoints -> secretWaypoints.enableItemWaypoints, 2, 64, 250),
+ BAT(secretWaypoints -> secretWaypoints.enableBatWaypoints, 142, 66, 0),
+ WITHER(secretWaypoints -> secretWaypoints.enableWitherWaypoints, 30, 30, 30),
+ LEVER(secretWaypoints -> secretWaypoints.enableLeverWaypoints, 250, 217, 2),
+ FAIRYSOUL(secretWaypoints -> secretWaypoints.enableFairySoulWaypoints, 255, 85, 255),
+ STONK(secretWaypoints -> secretWaypoints.enableStonkWaypoints, 146, 52, 235),
+ DEFAULT(secretWaypoints -> secretWaypoints.enableDefaultWaypoints, 190, 255, 252);
+ private final Predicate<SkyblockerConfig.SecretWaypoints> enabledPredicate;
+ private final float[] colorComponents;
+
+ Category(Predicate<SkyblockerConfig.SecretWaypoints> enabledPredicate, int... intColorComponents) {
+ this.enabledPredicate = enabledPredicate;
+ colorComponents = new float[intColorComponents.length];
+ for (int i = 0; i < intColorComponents.length; i++) {
+ colorComponents[i] = intColorComponents[i] / 255F;
+ }
+ }
+
+ private static Category get(JsonObject categoryJson) {
+ return switch (categoryJson.get("category").getAsString()) {
+ case "entrance" -> Category.ENTRANCE;
+ case "superboom" -> Category.SUPERBOOM;
+ case "chest" -> Category.CHEST;
+ case "item" -> Category.ITEM;
+ case "bat" -> Category.BAT;
+ case "wither" -> Category.WITHER;
+ case "lever" -> Category.LEVER;
+ case "fairysoul" -> Category.FAIRYSOUL;
+ case "stonk" -> Category.STONK;
+ default -> Category.DEFAULT;
+ };
+ }
+
+ boolean needsInteraction() {
+ return this == CHEST || this == WITHER;
+ }
+
+ boolean isLever() {
+ return this == LEVER;
+ }
+
+ boolean needsItemPickup() {
+ return this == ITEM;
+ }
+
+ boolean isBat() {
+ return this == BAT;
+ }
+
+ boolean isEnabled() {
+ return enabledPredicate.test(SkyblockerConfigManager.get().locations.dungeons.secretWaypoints);
+ }
+ }
+}
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
new file mode 100644
index 00000000..6e9eb02d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/ColorTerminal.java
@@ -0,0 +1,72 @@
+package de.hysky.skyblocker.skyblock.dungeon.terminal;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.render.gui.ColorHighlight;
+import de.hysky.skyblocker.utils.render.gui.ContainerSolver;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.registry.Registries;
+import net.minecraft.util.DyeColor;
+import net.minecraft.util.Identifier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+
+
+public class ColorTerminal extends ContainerSolver {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ColorTerminal.class.getName());
+ private static final Map<String, DyeColor> colorFromName;
+ private DyeColor targetColor;
+ private static final Map<Item, DyeColor> itemColor;
+
+ public ColorTerminal() {
+ super("^Select all the ([A-Z ]+) items!$");
+ }
+
+ @Override
+ protected boolean isEnabled() {
+ targetColor = null;
+ return SkyblockerConfigManager.get().locations.dungeons.terminals.solveColor;
+ }
+
+ @Override
+ protected List<ColorHighlight> getColors(String[] groups, Map<Integer, ItemStack> slots) {
+ trimEdges(slots, 6);
+ List<ColorHighlight> highlights = new ArrayList<>();
+ String colorString = groups[0];
+ if (targetColor == null) {
+ targetColor = colorFromName.get(colorString);
+ if (targetColor == null) {
+ LOGGER.error("[Skyblocker] Couldn't find dye color corresponding to \"" + colorString + "\"");
+ return Collections.emptyList();
+ }
+ }
+ for (Map.Entry<Integer, ItemStack> slot : slots.entrySet()) {
+ ItemStack itemStack = slot.getValue();
+ if (!itemStack.hasEnchantments() && targetColor.equals(itemColor.get(itemStack.getItem()))) {
+ highlights.add(ColorHighlight.green(slot.getKey()));
+ }
+ }
+ return highlights;
+ }
+
+
+ static {
+ colorFromName = new HashMap<>();
+ for (DyeColor color : DyeColor.values())
+ colorFromName.put(color.getName().toUpperCase(Locale.ENGLISH), color);
+ colorFromName.put("SILVER", DyeColor.LIGHT_GRAY);
+ colorFromName.put("LIGHT BLUE", DyeColor.LIGHT_BLUE);
+
+ 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(Items.BONE_MEAL, DyeColor.WHITE);
+ itemColor.put(Items.LAPIS_LAZULI, DyeColor.BLUE);
+ itemColor.put(Items.COCOA_BEANS, DyeColor.BROWN);
+ itemColor.put(Items.INK_SAC, DyeColor.BLACK);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/OrderTerminal.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/OrderTerminal.java
new file mode 100644
index 00000000..b2636373
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/OrderTerminal.java
@@ -0,0 +1,58 @@
+package de.hysky.skyblocker.skyblock.dungeon.terminal;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.render.gui.ColorHighlight;
+import de.hysky.skyblocker.utils.render.gui.ContainerSolver;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class OrderTerminal extends ContainerSolver {
+ private final int PANES_NUM = 14;
+ private int[] orderedSlots;
+ private int currentNum = Integer.MAX_VALUE;
+
+ public OrderTerminal() {
+ super("^Click in order!$");
+ }
+
+ @Override
+ protected boolean isEnabled() {
+ orderedSlots = null;
+ currentNum = 0;
+ return SkyblockerConfigManager.get().locations.dungeons.terminals.solveOrder;
+ }
+
+ @Override
+ protected List<ColorHighlight> getColors(String[] groups, Map<Integer, ItemStack> slots) {
+ if(orderedSlots == null && !orderSlots(slots))
+ return Collections.emptyList();
+ while(currentNum < PANES_NUM && Items.LIME_STAINED_GLASS_PANE.equals(slots.get(orderedSlots[currentNum]).getItem()))
+ currentNum++;
+ List<ColorHighlight> highlights = new ArrayList<>(3);
+ int last = Integer.min(3, PANES_NUM - currentNum);
+ for(int i = 0; i < last; i++) {
+ highlights.add(new ColorHighlight(orderedSlots[currentNum + i], (224 - 64 * i) << 24 | 64 << 16 | 96 << 8 | 255));
+ }
+ return highlights;
+ }
+
+ public boolean orderSlots(Map<Integer, ItemStack> slots) {
+ trimEdges(slots, 4);
+ orderedSlots = new int[PANES_NUM];
+ for(Map.Entry<Integer, ItemStack> slot : slots.entrySet()) {
+ if(Items.AIR.equals(slot.getValue().getItem())) {
+ orderedSlots = null;
+ return false;
+ }
+ else
+ orderedSlots[slot.getValue().getCount() - 1] = slot.getKey();
+ }
+ currentNum = 0;
+ return true;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/StartsWithTerminal.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/StartsWithTerminal.java
new file mode 100644
index 00000000..5f856af2
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/terminal/StartsWithTerminal.java
@@ -0,0 +1,35 @@
+package de.hysky.skyblocker.skyblock.dungeon.terminal;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.render.gui.ColorHighlight;
+import de.hysky.skyblocker.utils.render.gui.ContainerSolver;
+import net.minecraft.item.ItemStack;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class StartsWithTerminal extends ContainerSolver {
+ public StartsWithTerminal() {
+ super("^What starts with: '([A-Z])'\\?$");
+ }
+
+ @Override
+ protected boolean isEnabled() {
+ return SkyblockerConfigManager.get().locations.dungeons.terminals.solveStartsWith;
+ }
+
+ @Override
+ protected List<ColorHighlight> getColors(String[] groups, Map<Integer, ItemStack> slots) {
+ trimEdges(slots, 6);
+ String prefix = groups[0];
+ List<ColorHighlight> highlights = new ArrayList<>();
+ for (Map.Entry<Integer, ItemStack> slot : slots.entrySet()) {
+ ItemStack stack = slot.getValue();
+ if (!stack.hasEnchantments() && stack.getName().getString().startsWith(prefix)) {
+ highlights.add(ColorHighlight.green(slot.getKey()));
+ }
+ }
+ return highlights;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/DwarvenHud.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/DwarvenHud.java
new file mode 100644
index 00000000..b853d7cc
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/DwarvenHud.java
@@ -0,0 +1,144 @@
+package de.hysky.skyblocker.skyblock.dwarven;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.tabhud.widget.hud.HudCommsWidget;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import it.unimi.dsi.fastutil.ints.IntIntPair;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class DwarvenHud {
+
+ public static final MinecraftClient client = MinecraftClient.getInstance();
+ public static List<Commission> commissionList = new ArrayList<>();
+
+ public static final List<Pattern> COMMISSIONS = Stream.of(
+ "(?:Titanium|Mithril|Hard Stone) Miner",
+ "(?:Ice Walker|Goblin|Goblin Raid|Automaton|Sludge|Team Treasurite Member|Yog|Boss Corleone|Thyst) Slayer",
+ "(?:Lava Springs|Cliffside Veins|Rampart's Quarry|Upper Mines|Royal Mines) Mithril",
+ "(?:Lava Springs|Cliffside Veins|Rampart's Quarry|Upper Mines|Royal Mines) Titanium",
+ "Goblin Raid",
+ "(?:Powder Ghast|Star Sentry) Puncher",
+ "(?<!Lucky )Raffle",
+ "Lucky Raffle",
+ "2x Mithril Powder Collector",
+ "(?:Ruby|Amber|Sapphire|Jade|Amethyst|Topaz) Gemstone Collector",
+ "(?:Amber|Sapphire|Jade|Amethyst|Topaz) Crystal Hunter",
+ "Chest Looter").map(s -> Pattern.compile("^.*(" + s + "): (\\d+\\.?\\d*%|DONE)"))
+ .collect(Collectors.toList());
+
+ public static void init() {
+ ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal("skyblocker")
+ .then(ClientCommandManager.literal("hud")
+ .then(ClientCommandManager.literal("dwarven")
+ .executes(Scheduler.queueOpenScreenCommand(DwarvenHudConfigScreen::new))))));
+
+ HudRenderCallback.EVENT.register((context, tickDelta) -> {
+ if (!SkyblockerConfigManager.get().locations.dwarvenMines.dwarvenHud.enabled
+ || client.options.playerListKey.isPressed()
+ || client.player == null
+ || commissionList.isEmpty()) {
+ return;
+ }
+ render(HudCommsWidget.INSTANCE, context, SkyblockerConfigManager.get().locations.dwarvenMines.dwarvenHud.x,
+ SkyblockerConfigManager.get().locations.dwarvenMines.dwarvenHud.y, commissionList);
+ });
+ }
+
+ public static IntIntPair getDimForConfig(List<Commission> commissions) {
+ return switch (SkyblockerConfigManager.get().locations.dwarvenMines.dwarvenHud.style) {
+ case SIMPLE -> {
+ HudCommsWidget.INSTANCE_CFG.updateData(commissions, false);
+ yield IntIntPair.of(
+ HudCommsWidget.INSTANCE_CFG.getWidth(),
+ HudCommsWidget.INSTANCE_CFG.getHeight());
+ }
+ case FANCY -> {
+ HudCommsWidget.INSTANCE_CFG.updateData(commissions, true);
+ yield IntIntPair.of(
+ HudCommsWidget.INSTANCE_CFG.getWidth(),
+ HudCommsWidget.INSTANCE_CFG.getHeight());
+ }
+ default -> IntIntPair.of(200, 20 * commissions.size());
+ };
+ }
+
+ public static void render(HudCommsWidget hcw, DrawContext context, int hudX, int hudY, List<Commission> commissions) {
+
+ switch (SkyblockerConfigManager.get().locations.dwarvenMines.dwarvenHud.style) {
+ case SIMPLE -> renderSimple(hcw, context, hudX, hudY, commissions);
+ case FANCY -> renderFancy(hcw, context, hudX, hudY, commissions);
+ case CLASSIC -> renderClassic(context, hudX, hudY, commissions);
+ }
+ }
+
+ public static void renderClassic(DrawContext context, int hudX, int hudY, List<Commission> commissions) {
+ if (SkyblockerConfigManager.get().locations.dwarvenMines.dwarvenHud.enableBackground) {
+ context.fill(hudX, hudY, hudX + 200, hudY + (20 * commissions.size()), 0x64000000);
+ }
+
+ int y = 0;
+ for (Commission commission : commissions) {
+ context
+ .drawTextWithShadow(client.textRenderer,
+ Text.literal(commission.commission + ": ")
+ .styled(style -> style.withColor(Formatting.AQUA))
+ .append(Text.literal(commission.progression)
+ .styled(style -> style.withColor(Formatting.GREEN))),
+ hudX + 5, hudY + y + 5, 0xFFFFFFFF);
+ y += 20;
+ }
+ }
+
+ public static void renderSimple(HudCommsWidget hcw, DrawContext context, int hudX, int hudY, List<Commission> commissions) {
+ hcw.updateData(commissions, false);
+ hcw.update();
+ hcw.setX(hudX);
+ hcw.setY(hudY);
+ hcw.render(context,
+ SkyblockerConfigManager.get().locations.dwarvenMines.dwarvenHud.enableBackground);
+ }
+
+ public static void renderFancy(HudCommsWidget hcw, DrawContext context, int hudX, int hudY, List<Commission> commissions) {
+ hcw.updateData(commissions, true);
+ hcw.update();
+ hcw.setX(hudX);
+ hcw.setY(hudY);
+ hcw.render(context,
+ SkyblockerConfigManager.get().locations.dwarvenMines.dwarvenHud.enableBackground);
+ }
+
+ public static void update() {
+ commissionList = new ArrayList<>();
+ if (client.player == null || client.getNetworkHandler() == null || !SkyblockerConfigManager.get().locations.dwarvenMines.dwarvenHud.enabled)
+ return;
+
+ client.getNetworkHandler().getPlayerList().forEach(playerListEntry -> {
+ if (playerListEntry.getDisplayName() != null) {
+ for (Pattern pattern : COMMISSIONS) {
+ Matcher matcher = pattern.matcher(playerListEntry.getDisplayName().getString());
+ if (matcher.find()) {
+ commissionList.add(new Commission(matcher.group(1), matcher.group(2)));
+ }
+
+ }
+ }
+ });
+ }
+
+ // steamroller tactics to get visibility from outside classes (HudCommsWidget)
+ public record Commission(String commission, String progression) {
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/DwarvenHudConfigScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/DwarvenHudConfigScreen.java
new file mode 100644
index 00000000..7b62221e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/DwarvenHudConfigScreen.java
@@ -0,0 +1,66 @@
+package de.hysky.skyblocker.skyblock.dwarven;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.tabhud.widget.hud.HudCommsWidget;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import it.unimi.dsi.fastutil.ints.IntIntPair;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.text.Text;
+
+import java.awt.*;
+import java.util.List;
+
+public class DwarvenHudConfigScreen extends Screen {
+
+ private static final List<DwarvenHud.Commission> CFG_COMMS = List.of(new DwarvenHud.Commission("Test Commission 1", "1%"), new DwarvenHud.Commission("Test Commission 2", "2%"));
+
+ private int hudX = SkyblockerConfigManager.get().locations.dwarvenMines.dwarvenHud.x;
+ private int hudY = SkyblockerConfigManager.get().locations.dwarvenMines.dwarvenHud.y;
+ private final Screen parent;
+
+ protected DwarvenHudConfigScreen() {
+ this(null);
+ }
+
+ public DwarvenHudConfigScreen(Screen parent) {
+ super(Text.of("Dwarven HUD Config"));
+ this.parent = parent;
+ }
+
+ @Override
+ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
+ super.render(context, mouseX, mouseY, delta);
+ renderBackground(context, mouseX, mouseY, delta);
+ DwarvenHud.render(HudCommsWidget.INSTANCE_CFG, context, hudX, hudY, List.of(new DwarvenHud.Commission("Test Commission 1", "1%"), new DwarvenHud.Commission("Test Commission 2", "2%")));
+ context.drawCenteredTextWithShadow(textRenderer, "Right Click To Reset Position", width / 2, height / 2, Color.GRAY.getRGB());
+ }
+
+ @Override
+ public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
+ IntIntPair dims = DwarvenHud.getDimForConfig(CFG_COMMS);
+ if (RenderHelper.pointIsInArea(mouseX, mouseY, hudX, hudY, hudX + 200, hudY + 40) && button == 0) {
+ hudX = (int) Math.max(Math.min(mouseX - (double) dims.leftInt() / 2, this.width - dims.leftInt()), 0);
+ hudY = (int) Math.max(Math.min(mouseY - (double) dims.rightInt() / 2, this.height - dims.rightInt()), 0);
+ }
+ return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (button == 1) {
+ IntIntPair dims = DwarvenHud.getDimForConfig(CFG_COMMS);
+ hudX = this.width / 2 - dims.leftInt();
+ hudY = this.height / 2 - dims.rightInt();
+ }
+ return super.mouseClicked(mouseX, mouseY, button);
+ }
+
+ @Override
+ public void close() {
+ SkyblockerConfigManager.get().locations.dwarvenMines.dwarvenHud.x = hudX;
+ SkyblockerConfigManager.get().locations.dwarvenMines.dwarvenHud.y = hudY;
+ SkyblockerConfigManager.save();
+ client.setScreen(parent);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/Fetchur.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/Fetchur.java
new file mode 100644
index 00000000..9bfb77f7
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/Fetchur.java
@@ -0,0 +1,53 @@
+package de.hysky.skyblocker.skyblock.dwarven;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+import de.hysky.skyblocker.utils.chat.ChatPatternListener;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.text.Text;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+
+public class Fetchur extends ChatPatternListener {
+ private static final Map<String, String> answers;
+
+ public Fetchur() {
+ super("^§e\\[NPC] Fetchur§f: (?:its|theyre) ([a-zA-Z, \\-]*)$");
+ }
+
+ @Override
+ public ChatFilterResult state() {
+ return SkyblockerConfigManager.get().locations.dwarvenMines.solveFetchur ? ChatFilterResult.FILTER : ChatFilterResult.PASS;
+ }
+
+ @Override
+ public boolean onMatch(Text message, Matcher matcher) {
+ MinecraftClient client = MinecraftClient.getInstance();
+ if (client.player == null) return false;
+ String riddle = matcher.group(1);
+ String answer = answers.getOrDefault(riddle, riddle);
+ client.player.sendMessage(Text.of("§e[NPC] Fetchur§f: " + answer), false);
+ return true;
+ }
+
+ static {
+ answers = new HashMap<>();
+ answers.put("red and soft", Text.translatable("block.minecraft.red_wool").getString());
+ answers.put("yellow and see through", Text.translatable("block.minecraft.yellow_stained_glass").getString());
+ answers.put("circular and sometimes moves", Text.translatable("item.minecraft.compass").getString());
+ // TODO remove when typo fixed by hypixel
+ answers.put("circlular and sometimes moves", Text.translatable("item.minecraft.compass").getString());
+ answers.put("expensive minerals", "Mithril");
+ answers.put("useful during celebrations", Text.translatable("item.minecraft.firework_rocket").getString());
+ answers.put("hot and gives energy", "Cheap / Decent Coffee");
+ answers.put("tall and can be opened", Text.translatable("block.minecraft.oak_door").getString());
+ answers.put("brown and fluffy", Text.translatable("item.minecraft.rabbit_foot").getString());
+ answers.put("explosive but more than usual", "Superboom TNT");
+ answers.put("wearable and grows", Text.translatable("block.minecraft.pumpkin").getString());
+ answers.put("shiny and makes sparks", Text.translatable("item.minecraft.flint_and_steel").getString());
+ answers.put("red and white and you can mine it", Text.translatable("block.minecraft.nether_quartz_ore").getString());
+ answers.put("round and green, or purple", Text.translatable("item.minecraft.ender_pearl").getString());
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/Puzzler.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/Puzzler.java
new file mode 100644
index 00000000..fae845b5
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/Puzzler.java
@@ -0,0 +1,39 @@
+package de.hysky.skyblocker.skyblock.dwarven;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+import de.hysky.skyblocker.utils.chat.ChatPatternListener;
+import net.minecraft.block.Blocks;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.text.Text;
+import net.minecraft.util.math.BlockPos;
+
+import java.util.regex.Matcher;
+
+public class Puzzler extends ChatPatternListener {
+ public Puzzler() {
+ super("^§e\\[NPC] §dPuzzler§f: ((?:§d▲|§5▶|§b◀|§a▼){10})$");
+ }
+
+ @Override
+ public ChatFilterResult state() {
+ return SkyblockerConfigManager.get().locations.dwarvenMines.solvePuzzler ? null : ChatFilterResult.PASS;
+ }
+
+ @Override
+ public boolean onMatch(Text message, Matcher matcher) {
+ int x = 181;
+ int z = 135;
+ for (char c : matcher.group(1).toCharArray()) {
+ if (c == '▲') z++;
+ else if (c == '▼') z--;
+ else if (c == '◀') x++;
+ else if (c == '▶') x--;
+ }
+ ClientWorld world = MinecraftClient.getInstance().world;
+ if (world != null)
+ world.setBlockState(new BlockPos(x, 195, z), Blocks.CRIMSON_PLANKS.getDefaultState());
+ return false;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/experiment/ChronomatronSolver.java b/src/main/java/de/hysky/skyblocker/skyblock/experiment/ChronomatronSolver.java
new file mode 100644
index 00000000..19459b43
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/experiment/ChronomatronSolver.java
@@ -0,0 +1,129 @@
+package de.hysky.skyblocker.skyblock.experiment;
+
+import com.google.common.collect.ImmutableMap;
+
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.utils.render.gui.ColorHighlight;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
+import net.minecraft.inventory.Inventory;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class ChronomatronSolver extends ExperimentSolver {
+ public static final ImmutableMap<Item, Item> TERRACOTTA_TO_GLASS = ImmutableMap.ofEntries(
+ new AbstractMap.SimpleImmutableEntry<>(Items.RED_TERRACOTTA, Items.RED_STAINED_GLASS),
+ new AbstractMap.SimpleImmutableEntry<>(Items.ORANGE_TERRACOTTA, Items.ORANGE_STAINED_GLASS),
+ new AbstractMap.SimpleImmutableEntry<>(Items.YELLOW_TERRACOTTA, Items.YELLOW_STAINED_GLASS),
+ new AbstractMap.SimpleImmutableEntry<>(Items.LIME_TERRACOTTA, Items.LIME_STAINED_GLASS),
+ new AbstractMap.SimpleImmutableEntry<>(Items.GREEN_TERRACOTTA, Items.GREEN_STAINED_GLASS),
+ new AbstractMap.SimpleImmutableEntry<>(Items.CYAN_TERRACOTTA, Items.CYAN_STAINED_GLASS),
+ new AbstractMap.SimpleImmutableEntry<>(Items.LIGHT_BLUE_TERRACOTTA, Items.LIGHT_BLUE_STAINED_GLASS),
+ new AbstractMap.SimpleImmutableEntry<>(Items.BLUE_TERRACOTTA, Items.BLUE_STAINED_GLASS),
+ new AbstractMap.SimpleImmutableEntry<>(Items.PURPLE_TERRACOTTA, Items.PURPLE_STAINED_GLASS),
+ new AbstractMap.SimpleImmutableEntry<>(Items.PINK_TERRACOTTA, Items.PINK_STAINED_GLASS)
+ );
+
+ private final List<Item> chronomatronSlots = new ArrayList<>();
+ private int chronomatronChainLengthCount;
+ private int chronomatronCurrentSlot;
+ private int chronomatronCurrentOrdinal;
+
+ public ChronomatronSolver() {
+ super("^Chronomatron \\(\\w+\\)$");
+ }
+
+ public List<Item> getChronomatronSlots() {
+ return chronomatronSlots;
+ }
+
+ public int getChronomatronCurrentOrdinal() {
+ return chronomatronCurrentOrdinal;
+ }
+
+ public int incrementChronomatronCurrentOrdinal() {
+ return ++chronomatronCurrentOrdinal;
+ }
+
+ @Override
+ protected boolean isEnabled(SkyblockerConfig.Experiments experimentsConfig) {
+ return experimentsConfig.enableChronomatronSolver;
+ }
+
+ @Override
+ protected void tick(Screen screen) {
+ if (isEnabled() && screen instanceof GenericContainerScreen genericContainerScreen && genericContainerScreen.getTitle().getString().startsWith("Chronomatron (")) {
+ switch (getState()) {
+ case REMEMBER -> {
+ Inventory inventory = genericContainerScreen.getScreenHandler().getInventory();
+ if (chronomatronCurrentSlot == 0) {
+ for (int index = 10; index < 43; index++) {
+ if (inventory.getStack(index).hasEnchantments()) {
+ if (chronomatronSlots.size() <= chronomatronChainLengthCount) {
+ chronomatronSlots.add(TERRACOTTA_TO_GLASS.get(inventory.getStack(index).getItem()));
+ setState(State.WAIT);
+ } else {
+ chronomatronChainLengthCount++;
+ }
+ chronomatronCurrentSlot = index;
+ return;
+ }
+ }
+ } else if (!inventory.getStack(chronomatronCurrentSlot).hasEnchantments()) {
+ chronomatronCurrentSlot = 0;
+ }
+ }
+ case WAIT -> {
+ if (genericContainerScreen.getScreenHandler().getInventory().getStack(49).getName().getString().startsWith("Timer: ")) {
+ setState(State.SHOW);
+ }
+ }
+ case END -> {
+ String name = genericContainerScreen.getScreenHandler().getInventory().getStack(49).getName().getString();
+ if (!name.startsWith("Timer: ")) {
+ if (name.equals("Remember the pattern!")) {
+ chronomatronChainLengthCount = 0;
+ chronomatronCurrentOrdinal = 0;
+ setState(State.REMEMBER);
+ } else {
+ reset();
+ }
+ }
+ }
+ }
+ } else {
+ reset();
+ }
+ }
+
+ @Override
+ protected List<ColorHighlight> getColors(String[] groups, Map<Integer, ItemStack> slots) {
+ List<ColorHighlight> highlights = new ArrayList<>();
+ if (getState() == State.SHOW && chronomatronSlots.size() > chronomatronCurrentOrdinal) {
+ for (Map.Entry<Integer, ItemStack> indexStack : slots.entrySet()) {
+ int index = indexStack.getKey();
+ ItemStack stack = indexStack.getValue();
+ Item item = chronomatronSlots.get(chronomatronCurrentOrdinal);
+ if (stack.isOf(item) || TERRACOTTA_TO_GLASS.get(stack.getItem()) == item) {
+ highlights.add(ColorHighlight.green(index));
+ }
+ }
+ }
+ return highlights;
+ }
+
+ @Override
+ protected void reset() {
+ super.reset();
+ chronomatronSlots.clear();
+ chronomatronChainLengthCount = 0;
+ chronomatronCurrentSlot = 0;
+ chronomatronCurrentOrdinal = 0;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/experiment/ExperimentSolver.java b/src/main/java/de/hysky/skyblocker/skyblock/experiment/ExperimentSolver.java
new file mode 100644
index 00000000..6efcd420
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/experiment/ExperimentSolver.java
@@ -0,0 +1,60 @@
+package de.hysky.skyblocker.skyblock.experiment;
+
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.render.gui.ContainerSolver;
+import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
+import net.minecraft.item.ItemStack;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class ExperimentSolver extends ContainerSolver {
+ public enum State {
+ REMEMBER, WAIT, SHOW, END
+ }
+
+ private State state = State.REMEMBER;
+ private final Map<Integer, ItemStack> slots = new HashMap<>();
+
+ protected ExperimentSolver(String containerName) {
+ super(containerName);
+ }
+
+ public State getState() {
+ return state;
+ }
+
+ public void setState(State state) {
+ this.state = state;
+ }
+
+ public Map<Integer, ItemStack> getSlots() {
+ return slots;
+ }
+
+ @Override
+ protected final boolean isEnabled() {
+ return isEnabled(SkyblockerConfigManager.get().general.experiments);
+ }
+
+ protected abstract boolean isEnabled(SkyblockerConfig.Experiments experimentsConfig);
+
+ @Override
+ protected void start(GenericContainerScreen screen) {
+ super.start(screen);
+ state = State.REMEMBER;
+ ScreenEvents.afterTick(screen).register(this::tick);
+ }
+
+ @Override
+ protected void reset() {
+ super.reset();
+ state = State.REMEMBER;
+ slots.clear();
+ }
+
+ protected abstract void tick(Screen screen);
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/experiment/SuperpairsSolver.java b/src/main/java/de/hysky/skyblocker/skyblock/experiment/SuperpairsSolver.java
new file mode 100644
index 00000000..c00249fe
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/experiment/SuperpairsSolver.java
@@ -0,0 +1,81 @@
+package de.hysky.skyblocker.skyblock.experiment;
+
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.utils.render.gui.ColorHighlight;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+
+import java.util.*;
+
+public class SuperpairsSolver extends ExperimentSolver {
+ private int superpairsPrevClickedSlot;
+ private ItemStack superpairsCurrentSlot;
+ private final Set<Integer> superpairsDuplicatedSlots = new HashSet<>();
+
+ public SuperpairsSolver() {
+ super("^Superpairs \\(\\w+\\)$");
+ }
+
+ public void setSuperpairsPrevClickedSlot(int superpairsPrevClickedSlot) {
+ this.superpairsPrevClickedSlot = superpairsPrevClickedSlot;
+ }
+
+ public void setSuperpairsCurrentSlot(ItemStack superpairsCurrentSlot) {
+ this.superpairsCurrentSlot = superpairsCurrentSlot;
+ }
+
+ @Override
+ protected boolean isEnabled(SkyblockerConfig.Experiments experimentsConfig) {
+ return experimentsConfig.enableSuperpairsSolver;
+ }
+
+ @Override
+ protected void start(GenericContainerScreen screen) {
+ super.start(screen);
+ setState(State.SHOW);
+ }
+
+ @Override
+ protected void tick(Screen screen) {
+ if (isEnabled() && screen instanceof GenericContainerScreen genericContainerScreen && genericContainerScreen.getTitle().getString().startsWith("Superpairs (")) {
+ if (getState() == State.SHOW) {
+ if (genericContainerScreen.getScreenHandler().getInventory().getStack(4).isOf(Items.CAULDRON)) {
+ reset();
+ } else if (getSlots().get(superpairsPrevClickedSlot) == null) {
+ ItemStack itemStack = genericContainerScreen.getScreenHandler().getInventory().getStack(superpairsPrevClickedSlot);
+ if (!(itemStack.isOf(Items.CYAN_STAINED_GLASS) || itemStack.isOf(Items.BLACK_STAINED_GLASS_PANE) || itemStack.isOf(Items.AIR))) {
+ getSlots().entrySet().stream().filter((entry -> ItemStack.areEqual(entry.getValue(), itemStack))).findAny().ifPresent(entry -> superpairsDuplicatedSlots.add(entry.getKey()));
+ getSlots().put(superpairsPrevClickedSlot, itemStack);
+ superpairsCurrentSlot = itemStack;
+ }
+ }
+ }
+ } else {
+ reset();
+ }
+ }
+
+ @Override
+ protected List<ColorHighlight> getColors(String[] groups, Map<Integer, ItemStack> displaySlots) {
+ List<ColorHighlight> highlights = new ArrayList<>();
+ if (getState() == State.SHOW) {
+ for (Map.Entry<Integer, ItemStack> indexStack : displaySlots.entrySet()) {
+ int index = indexStack.getKey();
+ ItemStack displayStack = indexStack.getValue();
+ ItemStack stack = getSlots().get(index);
+ if (stack != null && !ItemStack.areEqual(stack, displayStack)) {
+ if (ItemStack.areEqual(superpairsCurrentSlot, stack) && displayStack.getName().getString().equals("Click a second button!")) {
+ highlights.add(ColorHighlight.green(index));
+ } else if (superpairsDuplicatedSlots.contains(index)) {
+ highlights.add(ColorHighlight.yellow(index));
+ } else {
+ highlights.add(ColorHighlight.red(index));
+ }
+ }
+ }
+ }
+ return highlights;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/experiment/UltrasequencerSolver.java b/src/main/java/de/hysky/skyblocker/skyblock/experiment/UltrasequencerSolver.java
new file mode 100644
index 00000000..1fcb976b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/experiment/UltrasequencerSolver.java
@@ -0,0 +1,80 @@
+package de.hysky.skyblocker.skyblock.experiment;
+
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.utils.render.gui.ColorHighlight;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
+import net.minecraft.inventory.Inventory;
+import net.minecraft.item.ItemStack;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class UltrasequencerSolver extends ExperimentSolver {
+ private int ultrasequencerNextSlot;
+
+ public UltrasequencerSolver() {
+ super("^Ultrasequencer \\(\\w+\\)$");
+ }
+
+ public int getUltrasequencerNextSlot() {
+ return ultrasequencerNextSlot;
+ }
+
+ public void setUltrasequencerNextSlot(int ultrasequencerNextSlot) {
+ this.ultrasequencerNextSlot = ultrasequencerNextSlot;
+ }
+
+ @Override
+ protected boolean isEnabled(SkyblockerConfig.Experiments experimentsConfig) {
+ return experimentsConfig.enableUltrasequencerSolver;
+ }
+
+ @Override
+ protected void tick(Screen screen) {
+ if (isEnabled() && screen instanceof GenericContainerScreen genericContainerScreen && genericContainerScreen.getTitle().getString().startsWith("Ultrasequencer (")) {
+ switch (getState()) {
+ case REMEMBER -> {
+ Inventory inventory = genericContainerScreen.getScreenHandler().getInventory();
+ if (inventory.getStack(49).getName().getString().equals("Remember the pattern!")) {
+ for (int index = 9; index < 45; index++) {
+ ItemStack itemStack = inventory.getStack(index);
+ String name = itemStack.getName().getString();
+ if (name.matches("\\d+")) {
+ if (name.equals("1")) {
+ ultrasequencerNextSlot = index;
+ }
+ getSlots().put(index, itemStack);
+ }
+ }
+ setState(State.WAIT);
+ }
+ }
+ case WAIT -> {
+ if (genericContainerScreen.getScreenHandler().getInventory().getStack(49).getName().getString().startsWith("Timer: ")) {
+ setState(State.SHOW);
+ }
+ }
+ case END -> {
+ String name = genericContainerScreen.getScreenHandler().getInventory().getStack(49).getName().getString();
+ if (!name.startsWith("Timer: ")) {
+ if (name.equals("Remember the pattern!")) {
+ getSlots().clear();
+ setState(State.REMEMBER);
+ } else {
+ reset();
+ }
+ }
+ }
+ }
+ } else {
+ reset();
+ }
+ }
+
+ @Override
+ protected List<ColorHighlight> getColors(String[] groups, Map<Integer, ItemStack> slots) {
+ return getState() == State.SHOW && ultrasequencerNextSlot != 0 ? List.of(ColorHighlight.green(ultrasequencerNextSlot)) : new ArrayList<>();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/AbilityFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/AbilityFilter.java
new file mode 100644
index 00000000..db10e952
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/AbilityFilter.java
@@ -0,0 +1,15 @@
+package de.hysky.skyblocker.skyblock.filters;
+
+import me.xmrvizzy.skyblocker.config.SkyblockerConfigManager;
+import me.xmrvizzy.skyblocker.utils.chat.ChatFilterResult;
+
+public class AbilityFilter extends SimpleChatFilter {
+ public AbilityFilter() {
+ super("^(?:This ability is on cooldown for " + NUMBER + "s\\.|No more charges, next one in " + NUMBER + "s!)$");
+ }
+
+ @Override
+ protected ChatFilterResult state() {
+ return SkyblockerConfigManager.get().messages.hideAbility;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/AdFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/AdFilter.java
new file mode 100644
index 00000000..5860b41e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/AdFilter.java
@@ -0,0 +1,39 @@
+package de.hysky.skyblocker.skyblock.filters;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+import de.hysky.skyblocker.utils.chat.ChatPatternListener;
+import net.minecraft.text.Text;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class AdFilter extends ChatPatternListener {
+ private static final Pattern[] AD_FILTERS = new Pattern[] {
+ Pattern.compile("^(?:i(?:m|'m| am)? |(?:is )?any(?: ?one|1) )?(?:buy|sell|lowball|trade?)(?:ing)?(?:\\W|$)", Pattern.CASE_INSENSITIVE),
+ Pattern.compile("(.)\\1{7,}"),
+ Pattern.compile("\\W(?:on|in|check|at) my (?:ah|bin)(?:\\W|$)", Pattern.CASE_INSENSITIVE), };
+
+ public AdFilter() {
+ // Groups:
+ // 1. Player name
+ // 2. Message
+ // (?:§8\[[§feadbc0-9]+§8\] )?(?:[§76l]+[<INSERT EMBLEMS>] )?§[67abc](?:\[[§A-Za-z0-9+]+\] )?([A-Za-z0-9_]+)§[f7]: (.+)
+ super("(?:§8\\[[§feadbc0-9]+§8\\] )?(?:[§76l]+[" + Constants.LEVEL_EMBLEMS + "] )?§[67abc](?:\\[[§A-Za-z0-9+]+\\] )?([A-Za-z0-9_]+)§[f7]: (.+)");
+ }
+
+ @Override
+ public boolean onMatch(Text _message, Matcher matcher) {
+ String message = matcher.group(2);
+ for (Pattern adFilter : AD_FILTERS)
+ if (adFilter.matcher(message).find())
+ return true;
+ return false;
+ }
+
+ @Override
+ protected ChatFilterResult state() {
+ return SkyblockerConfigManager.get().messages.hideAds;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/AoteFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/AoteFilter.java
new file mode 100644
index 00000000..5d660037
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/AoteFilter.java
@@ -0,0 +1,15 @@
+package de.hysky.skyblocker.skyblock.filters;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+
+public class AoteFilter extends SimpleChatFilter {
+ public AoteFilter() {
+ super("^There are blocks in the way!$");
+ }
+
+ @Override
+ public ChatFilterResult state() {
+ return SkyblockerConfigManager.get().messages.hideAOTE;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/AutopetFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/AutopetFilter.java
new file mode 100644
index 00000000..f97e8177
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/AutopetFilter.java
@@ -0,0 +1,35 @@
+package de.hysky.skyblocker.skyblock.filters;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+import de.hysky.skyblocker.utils.chat.ChatPatternListener;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.text.Text;
+
+import java.util.Objects;
+import java.util.regex.Matcher;
+
+public class AutopetFilter extends ChatPatternListener {
+ public AutopetFilter() {
+ super("^§cAutopet §eequipped your §7.*§e! §a§lVIEW RULE$");
+ }
+
+ @Override
+ public boolean onMatch(Text _message, Matcher matcher) {
+ if (SkyblockerConfigManager.get().messages.hideAutopet == ChatFilterResult.ACTION_BAR) {
+ Objects.requireNonNull(MinecraftClient.getInstance().player).sendMessage(
+ Text.literal(
+ _message.getString().replace("§a§lVIEW RULE", "")
+ ), true);
+ }
+ return true;
+ }
+
+ @Override
+ public ChatFilterResult state() {
+ if (SkyblockerConfigManager.get().messages.hideAutopet == ChatFilterResult.ACTION_BAR)
+ return ChatFilterResult.FILTER;
+ else
+ return SkyblockerConfigManager.get().messages.hideAutopet;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/ComboFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/ComboFilter.java
new file mode 100644
index 00000000..5fd6f741
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/ComboFilter.java
@@ -0,0 +1,16 @@
+package de.hysky.skyblocker.skyblock.filters;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+
+public class ComboFilter extends SimpleChatFilter {
+ public ComboFilter() {
+ super("^(\\+\\d+ Kill Combo \\+\\d+(% ✯ Magic Find| coins per kill|% Combat Exp)" +
+ "|Your Kill Combo has expired! You reached a \\d+ Kill Combo!)$");
+ }
+
+ @Override
+ public ChatFilterResult state() {
+ return SkyblockerConfigManager.get().messages.hideCombo;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/HealFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/HealFilter.java
new file mode 100644
index 00000000..371615b8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/HealFilter.java
@@ -0,0 +1,15 @@
+package de.hysky.skyblocker.skyblock.filters;
+
+import me.xmrvizzy.skyblocker.config.SkyblockerConfigManager;
+import me.xmrvizzy.skyblocker.utils.chat.ChatFilterResult;
+
+public class HealFilter extends SimpleChatFilter {
+ public HealFilter() {
+ super("^(?:You healed yourself for " + NUMBER + " health!|[a-zA-Z0-9_]{2,16} healed you for " + NUMBER + " health!)$");
+ }
+
+ @Override
+ public ChatFilterResult state() {
+ return SkyblockerConfigManager.get().messages.hideHeal;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/ImplosionFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/ImplosionFilter.java
new file mode 100644
index 00000000..454d7b78
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/ImplosionFilter.java
@@ -0,0 +1,15 @@
+package de.hysky.skyblocker.skyblock.filters;
+
+import me.xmrvizzy.skyblocker.config.SkyblockerConfigManager;
+import me.xmrvizzy.skyblocker.utils.chat.ChatFilterResult;
+
+public class ImplosionFilter extends SimpleChatFilter {
+ public ImplosionFilter() {
+ super("^Your Implosion hit " + NUMBER + " enem(?:y|ies) for " + NUMBER + " damage\\.$");
+ }
+
+ @Override
+ public ChatFilterResult state() {
+ return SkyblockerConfigManager.get().messages.hideImplosion;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/MoltenWaveFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/MoltenWaveFilter.java
new file mode 100644
index 00000000..afc15a2c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/MoltenWaveFilter.java
@@ -0,0 +1,15 @@
+package de.hysky.skyblocker.skyblock.filters;
+
+import me.xmrvizzy.skyblocker.config.SkyblockerConfigManager;
+import me.xmrvizzy.skyblocker.utils.chat.ChatFilterResult;
+
+public class MoltenWaveFilter extends SimpleChatFilter {
+ public MoltenWaveFilter() {
+ super("^Your Molten Wave hit " + NUMBER + " enem(?:y|ies) for " + NUMBER + " damage\\.$");
+ }
+
+ @Override
+ public ChatFilterResult state() {
+ return SkyblockerConfigManager.get().messages.hideMoltenWave;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/ShowOffFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/ShowOffFilter.java
new file mode 100644
index 00000000..a9c551fb
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/ShowOffFilter.java
@@ -0,0 +1,18 @@
+package de.hysky.skyblocker.skyblock.filters;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+
+public class ShowOffFilter extends SimpleChatFilter {
+ private static final String[] SHOW_TYPES = { "is holding", "is wearing", "is friends with a", "has" };
+
+ public ShowOffFilter() {
+ super("(?:§8\\[[§feadbc0-9]+§8\\] )?(?:[§76l]+[" + Constants.LEVEL_EMBLEMS + "] )?§[67abc](?:\\[[§A-Za-z0-9+]+\\] )?([A-Za-z0-9_]+)[§f7]+ (?:" + String.join("|", SHOW_TYPES) + ") §8\\[(.+)§8\\]");
+ }
+
+ @Override
+ protected ChatFilterResult state() {
+ return SkyblockerConfigManager.get().messages.hideShowOff;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/SimpleChatFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/SimpleChatFilter.java
new file mode 100644
index 00000000..025b3dce
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/SimpleChatFilter.java
@@ -0,0 +1,17 @@
+package de.hysky.skyblocker.skyblock.filters;
+
+import de.hysky.skyblocker.utils.chat.ChatPatternListener;
+import net.minecraft.text.Text;
+
+import java.util.regex.Matcher;
+
+public abstract class SimpleChatFilter extends ChatPatternListener {
+ public SimpleChatFilter(String pattern) {
+ super(pattern);
+ }
+
+ @Override
+ protected final boolean onMatch(Text message, Matcher matcher) {
+ return true;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/TeleportPadFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/TeleportPadFilter.java
new file mode 100644
index 00000000..57fac590
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/TeleportPadFilter.java
@@ -0,0 +1,16 @@
+package de.hysky.skyblocker.skyblock.filters;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.chat.ChatFilterResult;
+
+public class TeleportPadFilter extends SimpleChatFilter {
+ public TeleportPadFilter() {
+ super("^(Warped from the .* Teleport Pad to the .* Teleport Pad!" +
+ "|This Teleport Pad does not have a destination set!)$");
+ }
+
+ @Override
+ public ChatFilterResult state() {
+ return SkyblockerConfigManager.get().messages.hideTeleportPad;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java b/src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java
new file mode 100644
index 00000000..ed650e26
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java
@@ -0,0 +1,59 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+
+public class AttributeShards {
+ private static final Object2ObjectOpenHashMap<String, String> ID_2_SHORT_NAME = new Object2ObjectOpenHashMap<>();
+
+ static {
+ //Weapons
+ ID_2_SHORT_NAME.put("arachno", "A");
+ ID_2_SHORT_NAME.put("attack_speed", "AS");
+ ID_2_SHORT_NAME.put("blazing", "BL");
+ ID_2_SHORT_NAME.put("combo", "C");
+ ID_2_SHORT_NAME.put("elite", "E");
+ ID_2_SHORT_NAME.put("ender", "EN");
+ ID_2_SHORT_NAME.put("ignition", "I");
+ ID_2_SHORT_NAME.put("life_recovery", "LR");
+ ID_2_SHORT_NAME.put("mana_steal", "MS");
+ ID_2_SHORT_NAME.put("midas_touch", "MT");
+ ID_2_SHORT_NAME.put("undead", "U");
+
+ //Swords & Bows
+ ID_2_SHORT_NAME.put("warrior", "W");
+ ID_2_SHORT_NAME.put("deadeye", "DE");
+
+ //Armor or Equipment
+ ID_2_SHORT_NAME.put("arachno_resistance", "AR");
+ ID_2_SHORT_NAME.put("blazing_resistance", "BR");
+ ID_2_SHORT_NAME.put("breeze", "B");
+ ID_2_SHORT_NAME.put("dominance", "D");
+ ID_2_SHORT_NAME.put("ender_resistance", "ER");
+ ID_2_SHORT_NAME.put("experience", "XP");
+ ID_2_SHORT_NAME.put("fortitude", "F");
+ ID_2_SHORT_NAME.put("life_regeneration", "HR"); //Health regeneration
+ ID_2_SHORT_NAME.put("lifeline", "L");
+ ID_2_SHORT_NAME.put("magic_find", "MF");
+ ID_2_SHORT_NAME.put("mana_pool", "MP");
+ ID_2_SHORT_NAME.put("mana_regeneration", "MR");
+ ID_2_SHORT_NAME.put("mending", "V"); //Vitality
+ ID_2_SHORT_NAME.put("speed", "S");
+ ID_2_SHORT_NAME.put("undead_resistance", "UR");
+ ID_2_SHORT_NAME.put("veteran", "V");
+
+ //Fishing Gear
+ ID_2_SHORT_NAME.put("blazing_fortune", "BF");
+ ID_2_SHORT_NAME.put("fishing_experience", "FE");
+ ID_2_SHORT_NAME.put("infection", "IF");
+ ID_2_SHORT_NAME.put("double_hook", "DH");
+ ID_2_SHORT_NAME.put("fisherman", "FM");
+ ID_2_SHORT_NAME.put("fishing_speed", "FS");
+ ID_2_SHORT_NAME.put("hunter", "H");
+ ID_2_SHORT_NAME.put("trophy_hunter", "TH");
+
+ }
+
+ public static String getShortName(String id) {
+ return ID_2_SHORT_NAME.getOrDefault(id, "");
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/BackpackPreview.java b/src/main/java/de/hysky/skyblocker/skyblock/item/BackpackPreview.java
new file mode 100644
index 00000000..122ffe9b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/BackpackPreview.java
@@ -0,0 +1,235 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.utils.Utils;
+import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.fabricmc.loader.api.FabricLoader;
+import net.minecraft.client.MinecraftClient;
+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.util.math.MatrixStack;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.inventory.Inventory;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.*;
+import net.minecraft.util.Identifier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class BackpackPreview {
+ private static final Logger LOGGER = LoggerFactory.getLogger(BackpackPreview.class);
+ private static final Identifier TEXTURE = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/inventory_background.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;
+
+ private static final Inventory[] storage = new Inventory[STORAGE_SIZE];
+ private static final boolean[] dirty = new boolean[STORAGE_SIZE];
+
+ private static String loaded = ""; // uuid + sb profile currently loaded
+ private static Path save_dir = null;
+
+ public static void init() {
+ ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
+ if (screen instanceof HandledScreen<?> handledScreen) {
+ updateStorage(handledScreen);
+ }
+ });
+ }
+
+ public static void tick() {
+ Utils.update(); // force update isOnSkyblock to prevent crash on disconnect
+ if (Utils.isOnSkyblock()) {
+ // save all dirty storages
+ saveStorage();
+ // update save dir based on uuid and sb profile
+ String uuid = MinecraftClient.getInstance().getSession().getUuidOrNull().toString().replaceAll("-", "");
+ String profile = Utils.getProfile();
+ if (profile != null && !profile.isEmpty()) {
+ save_dir = FabricLoader.getInstance().getConfigDir().resolve("skyblocker/backpack-preview/" + uuid + "/" + profile);
+ save_dir.toFile().mkdirs();
+ if (loaded.equals(uuid + "/" + profile)) {
+ // mark currently opened storage as dirty
+ if (MinecraftClient.getInstance().currentScreen != null) {
+ String title = MinecraftClient.getInstance().currentScreen.getTitle().getString();
+ int index = getStorageIndexFromTitle(title);
+ if (index != -1) dirty[index] = true;
+ }
+ } else {
+ // load storage again because uuid/profile changed
+ loaded = uuid + "/" + profile;
+ loadStorage();
+ }
+ }
+ }
+ }
+
+ public static void loadStorage() {
+ assert (save_dir != null);
+ for (int index = 0; index < STORAGE_SIZE; ++index) {
+ storage[index] = null;
+ dirty[index] = false;
+ File file = save_dir.resolve(index + ".nbt").toFile();
+ if (file.isFile()) {
+ try {
+ NbtCompound root = NbtIo.read(file);
+ storage[index] = new DummyInventory(root);
+ } catch (Exception e) {
+ LOGGER.error("Failed to load backpack preview file: " + file.getName(), e);
+ }
+ }
+ }
+ }
+
+ private static void saveStorage() {
+ assert (save_dir != null);
+ for (int index = 0; index < STORAGE_SIZE; ++index) {
+ if (dirty[index]) {
+ if (storage[index] != null) {
+ try {
+ NbtCompound root = new NbtCompound();
+ NbtList list = new NbtList();
+ for (int i = 9; i < storage[index].size(); ++i) {
+ ItemStack stack = storage[index].getStack(i);
+ NbtCompound item = new NbtCompound();
+ if (stack.isEmpty()) {
+ item.put("id", NbtString.of("minecraft:air"));
+ item.put("Count", NbtInt.of(1));
+ } else {
+ item.put("id", NbtString.of(stack.getItem().toString()));
+ item.put("Count", NbtInt.of(stack.getCount()));
+ item.put("tag", stack.getNbt());
+ }
+ list.add(item);
+ }
+ root.put("list", list);
+ root.put("size", NbtInt.of(storage[index].size() - 9));
+ NbtIo.write(root, save_dir.resolve(index + ".nbt").toFile());
+ dirty[index] = false;
+ } catch (Exception e) {
+ LOGGER.error("Failed to save backpack preview file: " + index + ".nbt", e);
+ }
+ }
+ }
+ }
+ }
+
+ public static void updateStorage(HandledScreen<?> screen) {
+ String title = screen.getTitle().getString();
+ int index = getStorageIndexFromTitle(title);
+ if (index != -1) {
+ storage[index] = screen.getScreenHandler().slots.get(0).inventory;
+ dirty[index] = true;
+ }
+ }
+
+ public static boolean renderPreview(DrawContext context, int index, int mouseX, int mouseY) {
+ if (index >= 9 && index < 18) index -= 9;
+ else if (index >= 27 && index < 45) index -= 18;
+ else return false;
+
+ if (storage[index] == null) return false;
+ int rows = (storage[index].size() - 9) / 9;
+
+ Screen screen = MinecraftClient.getInstance().currentScreen;
+ if (screen == null) return false;
+ int x = mouseX + 184 >= screen.width ? mouseX - 188 : mouseX + 8;
+ int y = Math.max(0, mouseY - 16);
+
+ RenderSystem.disableDepthTest();
+ RenderSystem.setShaderTexture(0, TEXTURE);
+ context.drawTexture(TEXTURE, x, y, 0, 0, 176, 7);
+ for (int i = 0; i < rows; ++i) {
+ context.drawTexture(TEXTURE, x, y + i * 18 + 7, 0, 7, 176, 18);
+ }
+ context.drawTexture(TEXTURE, x, y + rows * 18 + 7, 0, 25, 176, 7);
+ RenderSystem.enableDepthTest();
+
+ MatrixStack matrices = context.getMatrices();
+ TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+ for (int i = 9; i < storage[index].size(); ++i) {
+ int itemX = x + (i - 9) % 9 * 18 + 8;
+ int itemY = y + (i - 9) / 9 * 18 + 8;
+ matrices.push();
+ matrices.translate(0, 0, 200);
+ context.drawItem(storage[index].getStack(i), itemX, itemY);
+ context.drawItemInSlot(textRenderer, storage[index].getStack(i), itemX, itemY);
+ matrices.pop();
+ }
+
+ return true;
+ }
+
+ private static int getStorageIndexFromTitle(String title) {
+ Matcher echest = ECHEST_PATTERN.matcher(title);
+ if (echest.find()) return Integer.parseInt(echest.group(1)) - 1;
+ Matcher backpack = BACKPACK_PATTERN.matcher(title);
+ if (backpack.find()) return Integer.parseInt(backpack.group(1)) + 8;
+ return -1;
+ }
+}
+
+class DummyInventory implements Inventory {
+ private final List<ItemStack> stacks;
+
+ public DummyInventory(NbtCompound root) {
+ stacks = new ArrayList<>(root.getInt("size") + 9);
+ for (int i = 0; i < 9; ++i) stacks.add(ItemStack.EMPTY);
+ root.getList("list", NbtCompound.COMPOUND_TYPE).forEach(item ->
+ stacks.add(ItemStack.fromNbt((NbtCompound) item))
+ );
+ }
+
+ @Override
+ public int size() {
+ return stacks.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return false;
+ }
+
+ @Override
+ public ItemStack getStack(int slot) {
+ return stacks.get(slot);
+ }
+
+ @Override
+ public ItemStack removeStack(int slot, int amount) {
+ return null;
+ }
+
+ @Override
+ public ItemStack removeStack(int slot) {
+ return null;
+ }
+
+ @Override
+ public void setStack(int slot, ItemStack stack) {
+ stacks.set(slot, stack);
+ }
+
+ @Override
+ public void markDirty() {
+ }
+
+ @Override
+ public boolean canPlayerUse(PlayerEntity player) {
+ return false;
+ }
+
+ @Override
+ public void clear() {
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CompactorDeletorPreview.java b/src/main/java/de/hysky/skyblocker/skyblock/item/CompactorDeletorPreview.java
new file mode 100644
index 00000000..2a6551c7
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/CompactorDeletorPreview.java
@@ -0,0 +1,92 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import de.hysky.skyblocker.mixin.accessor.DrawContextInvoker;
+import it.unimi.dsi.fastutil.ints.IntIntPair;
+import it.unimi.dsi.fastutil.ints.IntObjectPair;
+import de.hysky.skyblocker.skyblock.itemlist.ItemRegistry;
+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;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class CompactorDeletorPreview {
+ /**
+ * The width and height in slots of the compactor/deletor
+ */
+ private static final Map<String, IntIntPair> DIMENSIONS = Map.of(
+ "4000", IntIntPair.of(1, 1),
+ "5000", IntIntPair.of(1, 3),
+ "6000", IntIntPair.of(1, 7),
+ "7000", IntIntPair.of(2, 6)
+ );
+ private static final IntIntPair DEFAULT_DIMENSION = IntIntPair.of(1, 6);
+ 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);
+ int targetIndex = getTargetIndex(tooltips);
+ if (targetIndex == -1) return false;
+
+ // Get items in compactor or deletor
+ NbtCompound nbt = stack.getNbt();
+ if (nbt == null || !nbt.contains("ExtraAttributes", 10)) {
+ return false;
+ }
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ // Get the slots and their items from the nbt, which is in the format personal_compact_<slot_number> or personal_deletor_<slot_number>
+ List<IntObjectPair<ItemStack>> slots = extraAttributes.getKeys().stream().filter(slot -> slot.contains(type.toLowerCase().substring(0, 7))).map(slot -> IntObjectPair.of(Integer.parseInt(slot.substring(17)), ItemRegistry.getItemStack(extraAttributes.getString(slot)))).toList();
+
+ List<TooltipComponent> components = tooltips.stream().map(Text::asOrderedText).map(TooltipComponent::of).collect(Collectors.toList());
+ IntIntPair dimensions = DIMENSIONS.getOrDefault(size, DEFAULT_DIMENSION);
+
+ // If there are no items in compactor or deletor
+ if (slots.isEmpty()) {
+ int slotsCount = dimensions.leftInt() * dimensions.rightInt();
+ components.add(targetIndex, TooltipComponent.of(Text.literal(slotsCount + (slotsCount == 1 ? " slot" : " slots")).formatted(Formatting.GRAY).asOrderedText()));
+
+ ((DrawContextInvoker) context).invokeDrawTooltip(client.textRenderer, components, x, y, HoveredTooltipPositioner.INSTANCE);
+ return true;
+ }
+
+ // Add the preview tooltip component
+ components.add(targetIndex, new CompactorPreviewTooltipComponent(slots, dimensions));
+
+ // Render accompanying text
+ components.add(targetIndex, TooltipComponent.of(Text.literal("Contents:").asOrderedText()));
+ if (extraAttributes.contains("PERSONAL_DELETOR_ACTIVE")) {
+ components.add(targetIndex, TooltipComponent.of(Text.literal("Active: ")
+ .append(extraAttributes.getBoolean("PERSONAL_DELETOR_ACTIVE") ? Text.literal("YES").formatted(Formatting.BOLD).formatted(Formatting.GREEN) : Text.literal("NO").formatted(Formatting.BOLD).formatted(Formatting.RED)).asOrderedText()));
+ }
+ ((DrawContextInvoker) context).invokeDrawTooltip(client.textRenderer, components, x, y, HoveredTooltipPositioner.INSTANCE);
+ return true;
+ }
+
+ /**
+ * Finds the target index to insert the preview component, which is the second empty line
+ */
+ private static int getTargetIndex(List<Text> tooltips) {
+ int targetIndex = -1;
+ int lineCount = 0;
+ for (int i = 0; i < tooltips.size(); i++) {
+ if (tooltips.get(i).getString().isEmpty()) {
+ lineCount += 1;
+ }
+ if (lineCount == 2) {
+ targetIndex = i;
+ break;
+ }
+ }
+ return targetIndex;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CompactorPreviewTooltipComponent.java b/src/main/java/de/hysky/skyblocker/skyblock/item/CompactorPreviewTooltipComponent.java
new file mode 100644
index 00000000..f3634548
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/CompactorPreviewTooltipComponent.java
@@ -0,0 +1,54 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import it.unimi.dsi.fastutil.ints.IntIntPair;
+import it.unimi.dsi.fastutil.ints.IntObjectPair;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.tooltip.TooltipComponent;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.Identifier;
+
+public class CompactorPreviewTooltipComponent implements TooltipComponent {
+ private static final Identifier INVENTORY_TEXTURE = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/inventory_background.png");
+ private final Iterable<IntObjectPair<ItemStack>> items;
+ private final IntIntPair dimensions;
+
+ public CompactorPreviewTooltipComponent(Iterable<IntObjectPair<ItemStack>> items, IntIntPair dimensions) {
+ this.items = items;
+ this.dimensions = dimensions;
+ }
+
+ @Override
+ public int getHeight() {
+ return dimensions.leftInt() * 18 + 14;
+ }
+
+ @Override
+ public int getWidth(TextRenderer textRenderer) {
+ return dimensions.rightInt() * 18 + 14;
+ }
+
+ @Override
+ public void drawItems(TextRenderer textRenderer, int x, int y, DrawContext context) {
+ context.drawTexture(INVENTORY_TEXTURE, x, y, 0, 0, 7 + dimensions.rightInt() * 18, 7);
+ context.drawTexture(INVENTORY_TEXTURE, x + 7 + dimensions.rightInt() * 18, y, 169, 0, 7, 7);
+
+ for (int i = 0; i < dimensions.leftInt(); i++) {
+ context.drawTexture(INVENTORY_TEXTURE, x, y + 7 + i * 18, 0, 7, 7, 18);
+ for (int j = 0; j < dimensions.rightInt(); j++) {
+ context.drawTexture(INVENTORY_TEXTURE, x + 7 + j * 18, y + 7 + i * 18, 7, 7, 18, 18);
+ }
+ context.drawTexture(INVENTORY_TEXTURE, x + 7 + dimensions.rightInt() * 18, y + 7 + i * 18, 169, 7, 7, 18);
+ }
+ context.drawTexture(INVENTORY_TEXTURE, x, y + 7 + dimensions.leftInt() * 18, 0, 25, 7 + dimensions.rightInt() * 18, 7);
+ context.drawTexture(INVENTORY_TEXTURE, x + 7 + dimensions.rightInt() * 18, y + 7 + dimensions.leftInt() * 18, 169, 25, 7, 7);
+
+ for (IntObjectPair<ItemStack> entry : items) {
+ int itemX = x + entry.leftInt() % dimensions.rightInt() * 18 + 8;
+ int itemY = y + entry.leftInt() / dimensions.rightInt() * 18 + 8;
+ context.drawItem(entry.right(), itemX, itemY);
+ context.drawItemInSlot(textRenderer, entry.right(), itemX, itemY);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorDyeColors.java b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorDyeColors.java
new file mode 100644
index 00000000..76042a81
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorDyeColors.java
@@ -0,0 +1,82 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+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.command.CommandRegistryAccess;
+import net.minecraft.item.DyeableItem;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.text.Text;
+
+public class CustomArmorDyeColors {
+ public static void init() {
+ ClientCommandRegistrationCallback.EVENT.register(CustomArmorDyeColors::registerCommands);
+ }
+
+ private static void registerCommands(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+ dispatcher.register(ClientCommandManager.literal("skyblocker")
+ .then(ClientCommandManager.literal("custom")
+ .then(ClientCommandManager.literal("dyeColor")
+ .executes(context -> customizeDyeColor(context.getSource(), null))
+ .then(ClientCommandManager.argument("hexCode", StringArgumentType.string())
+ .executes(context -> customizeDyeColor(context.getSource(), StringArgumentType.getString(context, "hexCode")))))));
+ }
+
+ @SuppressWarnings("SameReturnValue")
+ private static int customizeDyeColor(FabricClientCommandSource source, String hex) {
+ ItemStack heldItem = source.getPlayer().getMainHandStack();
+ NbtCompound nbt = (heldItem != null) ? heldItem.getNbt() : null;
+
+ if (hex != null && !isHexadecimalColor(hex)) {
+ source.sendError(Text.translatable("skyblocker.customDyeColors.invalidHex"));
+ return Command.SINGLE_SUCCESS;
+ }
+
+ if (Utils.isOnSkyblock() && heldItem != null) {
+ if (heldItem.getItem() instanceof DyeableItem) {
+ if (nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ String itemUuid = extraAttributes.contains("uuid") ? extraAttributes.getString("uuid") : null;
+
+ if (itemUuid != null) {
+ Object2IntOpenHashMap<String> customDyeColors = SkyblockerConfigManager.get().general.customDyeColors;
+
+ if (hex == null) {
+ if (customDyeColors.containsKey(itemUuid)) {
+ customDyeColors.removeInt(itemUuid);
+ SkyblockerConfigManager.save();
+ source.sendFeedback(Text.translatable("skyblocker.customDyeColors.removed"));
+ } else {
+ source.sendFeedback(Text.translatable("skyblocker.customDyeColors.neverHad"));
+ }
+ } else {
+ customDyeColors.put(itemUuid, Integer.decode("0x" + hex.replace("#", "")).intValue());
+ SkyblockerConfigManager.save();
+ source.sendFeedback(Text.translatable("skyblocker.customDyeColors.added"));
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customDyeColors.noItemUuid"));
+ }
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customDyeColors.notDyeable"));
+ return Command.SINGLE_SUCCESS;
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customDyeColors.unableToSetColor"));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private static boolean isHexadecimalColor(String s) {
+ return s.replace("#", "").chars().allMatch(c -> "0123456789ABCDEFabcdef".indexOf(c) >= 0) && s.replace("#", "").length() == 6;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java
new file mode 100644
index 00000000..b8fa0797
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java
@@ -0,0 +1,154 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.suggestion.SuggestionProvider;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.events.SkyblockEvents;
+import de.hysky.skyblocker.utils.Utils;
+import dev.isxander.yacl3.config.v2.api.SerialEntry;
+import it.unimi.dsi.fastutil.Pair;
+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.network.ClientPlayerEntity;
+import net.minecraft.command.CommandRegistryAccess;
+import net.minecraft.command.CommandSource;
+import net.minecraft.command.argument.IdentifierArgumentType;
+import net.minecraft.item.ArmorItem;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.trim.ArmorTrim;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.registry.*;
+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.util.Optional;
+
+public class CustomArmorTrims {
+ private static final Logger LOGGER = LoggerFactory.getLogger(CustomArmorTrims.class);
+ public static final Object2ObjectOpenHashMap<ArmorTrimId, Optional<ArmorTrim>> TRIMS_CACHE = new Object2ObjectOpenHashMap<>();
+ private static boolean trimsInitialized = false;
+
+ public static void init() {
+ SkyblockEvents.JOIN.register(CustomArmorTrims::initializeTrimCache);
+ ClientCommandRegistrationCallback.EVENT.register(CustomArmorTrims::registerCommand);
+ }
+
+ private static void initializeTrimCache() {
+ ClientPlayerEntity player = MinecraftClient.getInstance().player;
+ if (trimsInitialized || player == null) {
+ return;
+ }
+ try {
+ TRIMS_CACHE.clear();
+ DynamicRegistryManager registryManager = player.networkHandler.getRegistryManager();
+ for (Identifier material : registryManager.get(RegistryKeys.TRIM_MATERIAL).getIds()) {
+ for (Identifier pattern : registryManager.get(RegistryKeys.TRIM_PATTERN).getIds()) {
+ NbtCompound compound = new NbtCompound();
+ compound.putString("material", material.toString());
+ compound.putString("pattern", pattern.toString());
+
+ ArmorTrim trim = ArmorTrim.CODEC.parse(RegistryOps.of(NbtOps.INSTANCE, registryManager), compound).resultOrPartial(LOGGER::error).orElse(null);
+
+ // Something went terribly wrong
+ if (trim == null) throw new IllegalStateException("Trim shouldn't be null! [" + "\"" + material + "\",\"" + pattern + "\"]");
+
+ TRIMS_CACHE.put(new ArmorTrimId(material, pattern), Optional.of(trim));
+ }
+ }
+
+ LOGGER.info("[Skyblocker] Successfully cached all armor trims!");
+ trimsInitialized = true;
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Encountered an exception while caching armor trims", e);
+ }
+ }
+
+ private static void registerCommand(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+ dispatcher.register(ClientCommandManager.literal("skyblocker")
+ .then(ClientCommandManager.literal("custom")
+ .then(ClientCommandManager.literal("armorTrim")
+ .executes(context -> customizeTrim(context.getSource(), null, null))
+ .then(ClientCommandManager.argument("material", IdentifierArgumentType.identifier())
+ .suggests(getIdSuggestionProvider(RegistryKeys.TRIM_MATERIAL))
+ .executes(context -> customizeTrim(context.getSource(), context.getArgument("material", Identifier.class), null))
+ .then(ClientCommandManager.argument("pattern", IdentifierArgumentType.identifier())
+ .suggests(getIdSuggestionProvider(RegistryKeys.TRIM_PATTERN))
+ .executes(context -> customizeTrim(context.getSource(), context.getArgument("material", Identifier.class), context.getArgument("pattern", Identifier.class))))))));
+ }
+
+ @NotNull
+ private static SuggestionProvider<FabricClientCommandSource> getIdSuggestionProvider(RegistryKey<? extends Registry<?>> registryKey) {
+ return (context, builder) -> context.getSource().listIdSuggestions(registryKey, CommandSource.SuggestedIdType.ELEMENTS, builder, context);
+ }
+
+ @SuppressWarnings("SameReturnValue")
+ private static int customizeTrim(FabricClientCommandSource source, Identifier material, Identifier pattern) {
+ ItemStack heldItem = source.getPlayer().getMainHandStack();
+ NbtCompound nbt = (heldItem != null) ? heldItem.getNbt() : null;
+
+ if (Utils.isOnSkyblock() && heldItem != null) {
+ if (heldItem.getItem() instanceof ArmorItem) {
+ if (nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ String itemUuid = extraAttributes.contains("uuid") ? extraAttributes.getString("uuid") : null;
+
+ if (itemUuid != null) {
+ Object2ObjectOpenHashMap<String, ArmorTrimId> customArmorTrims = SkyblockerConfigManager.get().general.customArmorTrims;
+
+ if (material == null && pattern == null) {
+ if (customArmorTrims.containsKey(itemUuid)) {
+ customArmorTrims.remove(itemUuid);
+ SkyblockerConfigManager.save();
+ source.sendFeedback(Text.translatable("skyblocker.customArmorTrims.removed"));
+ } else {
+ source.sendFeedback(Text.translatable("skyblocker.customArmorTrims.neverHad"));
+ }
+ } else {
+ // Ensure that the material & trim are valid
+ ArmorTrimId trimId = new ArmorTrimId(material, pattern);
+ if (TRIMS_CACHE.get(trimId) == null) {
+ source.sendError(Text.translatable("skyblocker.customArmorTrims.invalidMaterialOrPattern"));
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ customArmorTrims.put(itemUuid, trimId);
+ SkyblockerConfigManager.save();
+ source.sendFeedback(Text.translatable("skyblocker.customArmorTrims.added"));
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customArmorTrims.noItemUuid"));
+ }
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customArmorTrims.notAnArmorPiece"));
+ return Command.SINGLE_SUCCESS;
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customArmorTrims.unableToSetTrim"));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ public record ArmorTrimId(@SerialEntry Identifier material, @SerialEntry Identifier pattern) implements Pair<Identifier, Identifier> {
+ @Override
+ public Identifier left() {
+ return material();
+ }
+
+ @Override
+ public Identifier right() {
+ return pattern();
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomItemNames.java b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomItemNames.java
new file mode 100644
index 00000000..5fbff253
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomItemNames.java
@@ -0,0 +1,74 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+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.command.CommandRegistryAccess;
+import net.minecraft.command.argument.TextArgumentType;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Style;
+import net.minecraft.text.Text;
+
+public class CustomItemNames {
+ public static void init() {
+ ClientCommandRegistrationCallback.EVENT.register(CustomItemNames::registerCommands);
+ }
+
+ private static void registerCommands(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+ dispatcher.register(ClientCommandManager.literal("skyblocker")
+ .then(ClientCommandManager.literal("custom")
+ .then(ClientCommandManager.literal("renameItem")
+ .executes(context -> renameItem(context.getSource(), null))
+ .then(ClientCommandManager.argument("textComponent", TextArgumentType.text())
+ .executes(context -> renameItem(context.getSource(), context.getArgument("textComponent", Text.class)))))));
+ }
+
+ @SuppressWarnings("SameReturnValue")
+ private static int renameItem(FabricClientCommandSource source, Text text) {
+ ItemStack heldItem = source.getPlayer().getMainHandStack();
+ NbtCompound nbt = (heldItem != null) ? heldItem.getNbt() : null;
+
+ if (Utils.isOnSkyblock() && nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ String itemUuid = extraAttributes.contains("uuid") ? extraAttributes.getString("uuid") : null;
+
+ if (itemUuid != null) {
+ Object2ObjectOpenHashMap<String, Text> customItemNames = SkyblockerConfigManager.get().general.customItemNames;
+
+ if (text == null) {
+ if (customItemNames.containsKey(itemUuid)) {
+ //Remove custom item name when the text argument isn't passed
+ customItemNames.remove(itemUuid);
+ SkyblockerConfigManager.save();
+ source.sendFeedback(Text.translatable("skyblocker.customItemNames.removed"));
+ } else {
+ source.sendFeedback(Text.translatable("skyblocker.customItemNames.neverHad"));
+ }
+ } else {
+ //If the text is provided then set the item's custom name to it
+
+ //Set italic to false if it hasn't been changed (or was already false)
+ Style currentStyle = text.getStyle();
+ ((MutableText) text).setStyle(currentStyle.withItalic((currentStyle.isItalic() ? true : false)));
+
+ customItemNames.put(itemUuid, text);
+ SkyblockerConfigManager.save();
+ source.sendFeedback(Text.translatable("skyblocker.customItemNames.added"));
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customItemNames.noItemUuid"));
+ }
+ } else {
+ source.sendError(Text.translatable("skyblocker.customItemNames.unableToSetName"));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java
new file mode 100644
index 00000000..9c1fa6ad
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java
@@ -0,0 +1,115 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.google.common.collect.ImmutableList;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.events.ClientPlayerBlockBreakEvent;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.fabricmc.fabric.api.event.player.UseItemCallback;
+import net.minecraft.block.BlockState;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.registry.tag.BlockTags;
+import net.minecraft.util.Hand;
+import net.minecraft.util.TypedActionResult;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class ItemCooldowns {
+ private static final String JUNGLE_AXE_ID = "JUNGLE_AXE";
+ private static final String TREECAPITATOR_ID = "TREECAPITATOR_AXE";
+ private static final String GRAPPLING_HOOK_ID = "GRAPPLING_HOOK";
+ private static final ImmutableList<String> BAT_ARMOR_IDS = ImmutableList.of("BAT_PERSON_HELMET", "BAT_PERSON_CHESTPLATE", "BAT_PERSON_LEGGINGS", "BAT_PERSON_BOOTS");
+
+ private static final Map<String, CooldownEntry> ITEM_COOLDOWNS = new HashMap<>();
+
+ public static void init() {
+ ClientPlayerBlockBreakEvent.AFTER.register(ItemCooldowns::afterBlockBreak);
+ UseItemCallback.EVENT.register(ItemCooldowns::onItemInteract);
+ }
+
+ public static void afterBlockBreak(World world, PlayerEntity player, BlockPos pos, BlockState state) {
+ if (!SkyblockerConfigManager.get().general.itemCooldown.enableItemCooldowns) return;
+
+ String usedItemId = ItemUtils.getItemId(player.getMainHandStack());
+ if (usedItemId == null) return;
+
+ if (state.isIn(BlockTags.LOGS)) {
+ if (usedItemId.equals(JUNGLE_AXE_ID)) {
+ if (!isOnCooldown(JUNGLE_AXE_ID)) {
+ ITEM_COOLDOWNS.put(JUNGLE_AXE_ID, new CooldownEntry(2000));
+ }
+ } else if (usedItemId.equals(TREECAPITATOR_ID)) {
+ if (!isOnCooldown(TREECAPITATOR_ID)) {
+ ITEM_COOLDOWNS.put(TREECAPITATOR_ID, new CooldownEntry(2000));
+ }
+ }
+ }
+ }
+
+ private static TypedActionResult<ItemStack> onItemInteract(PlayerEntity player, World world, Hand hand) {
+ if (!SkyblockerConfigManager.get().general.itemCooldown.enableItemCooldowns) return TypedActionResult.pass(ItemStack.EMPTY);
+
+ String usedItemId = ItemUtils.getItemId(player.getMainHandStack());
+ if (usedItemId != null && usedItemId.equals(GRAPPLING_HOOK_ID) && player.fishHook != null) {
+ if (!isOnCooldown(GRAPPLING_HOOK_ID) && !isWearingBatArmor(player)) {
+ ITEM_COOLDOWNS.put(GRAPPLING_HOOK_ID, new CooldownEntry(2000));
+ }
+ }
+
+ return TypedActionResult.pass(ItemStack.EMPTY);
+ }
+
+ public static boolean isOnCooldown(ItemStack itemStack) {
+ return isOnCooldown(ItemUtils.getItemId(itemStack));
+ }
+
+ private static boolean isOnCooldown(String itemId) {
+ if (ITEM_COOLDOWNS.containsKey(itemId)) {
+ CooldownEntry cooldownEntry = ITEM_COOLDOWNS.get(itemId);
+ if (cooldownEntry.isOnCooldown()) {
+ return true;
+ } else {
+ ITEM_COOLDOWNS.remove(itemId);
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ public static CooldownEntry getItemCooldownEntry(ItemStack itemStack) {
+ return ITEM_COOLDOWNS.get(ItemUtils.getItemId(itemStack));
+ }
+
+ private static boolean isWearingBatArmor(PlayerEntity player) {
+ for (ItemStack stack : player.getArmorItems()) {
+ String itemId = ItemUtils.getItemId(stack);
+ if (!BAT_ARMOR_IDS.contains(itemId)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public record CooldownEntry(int cooldown, long startTime) {
+ public CooldownEntry(int cooldown) {
+ this(cooldown, System.currentTimeMillis());
+ }
+
+ public boolean isOnCooldown() {
+ return (this.startTime + this.cooldown) > System.currentTimeMillis();
+ }
+
+ public long getRemainingCooldown() {
+ long time = (this.startTime + this.cooldown) - System.currentTimeMillis();
+ return Math.max(time, 0);
+ }
+
+ public float getRemainingCooldownPercent() {
+ return this.isOnCooldown() ? (float) this.getRemainingCooldown() / cooldown : 0.0f;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/ItemProtection.java b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemProtection.java
new file mode 100644
index 00000000..d73e1545
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemProtection.java
@@ -0,0 +1,75 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+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.command.CommandRegistryAccess;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.text.Text;
+
+public class ItemProtection {
+
+ public static void init() {
+ ClientCommandRegistrationCallback.EVENT.register(ItemProtection::registerCommand);
+ }
+
+ public static boolean isItemProtected(ItemStack stack) {
+ if (stack == null || stack.isEmpty()) return false;
+
+ NbtCompound nbt = stack.getNbt();
+
+ if (nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ String itemUuid = extraAttributes.contains("uuid") ? extraAttributes.getString("uuid") : "";
+
+ return SkyblockerConfigManager.get().general.protectedItems.contains(itemUuid);
+ }
+
+ return false;
+ }
+
+ private static void registerCommand(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+ dispatcher.register(ClientCommandManager.literal("skyblocker")
+ .then(ClientCommandManager.literal("protectItem")
+ .executes(context -> protectMyItem(context.getSource()))));
+ }
+
+ private static int protectMyItem(FabricClientCommandSource source) {
+ ItemStack heldItem = source.getPlayer().getMainHandStack();
+ NbtCompound nbt = (heldItem != null) ? heldItem.getNbt() : null;
+
+ if (Utils.isOnSkyblock() && nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ String itemUuid = extraAttributes.contains("uuid") ? extraAttributes.getString("uuid") : null;
+
+ if (itemUuid != null) {
+ ObjectOpenHashSet<String> protectedItems = SkyblockerConfigManager.get().general.protectedItems;
+
+ if (!protectedItems.contains(itemUuid)) {
+ protectedItems.add(itemUuid);
+ SkyblockerConfigManager.save();
+
+ source.sendFeedback(Text.translatable("skyblocker.itemProtection.added", heldItem.getName()));
+ } else {
+ protectedItems.remove(itemUuid);
+ SkyblockerConfigManager.save();
+
+ source.sendFeedback(Text.translatable("skyblocker.itemProtection.removed", heldItem.getName()));
+ }
+ } else {
+ source.sendFeedback(Text.translatable("skyblocker.itemProtection.noItemUuid"));
+ }
+ } else {
+ source.sendFeedback(Text.translatable("skyblocker.itemProtection.unableToProtect"));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/ItemRarityBackgrounds.java b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemRarityBackgrounds.java
new file mode 100644
index 00000000..0af64bd9
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemRarityBackgrounds.java
@@ -0,0 +1,109 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import com.google.common.collect.ImmutableMap;
+import com.mojang.blaze3d.systems.RenderSystem;
+
+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 it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
+import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.item.TooltipContext;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+
+public class ItemRarityBackgrounds {
+ private static final Identifier RARITY_BG_TEX = new Identifier(SkyblockerMod.NAMESPACE, "item_rarity_background");
+ private static final Supplier<Sprite> SPRITE = () -> MinecraftClient.getInstance().getGuiAtlasManager().getSprite(RARITY_BG_TEX);
+ private static final ImmutableMap<String, SkyblockItemRarity> LORE_RARITIES = ImmutableMap.ofEntries(
+ Map.entry("ADMIN", SkyblockItemRarity.ADMIN),
+ Map.entry("SPECIAL", SkyblockItemRarity.SPECIAL), //Very special is the same color so this will cover it
+ Map.entry("DIVINE", SkyblockItemRarity.DIVINE),
+ Map.entry("MYTHIC", SkyblockItemRarity.MYTHIC),
+ Map.entry("LEGENDARY", SkyblockItemRarity.LEGENDARY),
+ Map.entry("LEGENJERRY", SkyblockItemRarity.LEGENDARY),
+ Map.entry("EPIC", SkyblockItemRarity.EPIC),
+ Map.entry("RARE", SkyblockItemRarity.RARE),
+ Map.entry("UNCOMMON", SkyblockItemRarity.UNCOMMON),
+ Map.entry("COMMON", SkyblockItemRarity.COMMON)
+ );
+ private static final Int2ReferenceOpenHashMap<SkyblockItemRarity> CACHE = new Int2ReferenceOpenHashMap<>();
+
+ public static void init() {
+ //Clear the cache every 5 minutes, ints are very compact!
+ Scheduler.INSTANCE.scheduleCyclic(CACHE::clear, 4800);
+
+ //Clear cache after a screen where items can be upgraded in rarity closes
+ ScreenEvents.BEFORE_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
+ String title = screen.getTitle().getString();
+
+ if (Utils.isOnSkyblock() && (title.equals("The Hex") || title.equals("Craft Item") || title.equals("Anvil") || title.equals("Reforge Anvil"))) {
+ ScreenEvents.remove(screen).register(screen1 -> CACHE.clear());
+ }
+ });
+ }
+
+ public static void tryDraw(ItemStack stack, DrawContext context, int x, int y) {
+ MinecraftClient client = MinecraftClient.getInstance();
+
+ if (client.player != null) {
+ SkyblockItemRarity itemRarity = getItemRarity(stack, client.player);
+
+ if (itemRarity != null) draw(context, x, y, itemRarity);
+ }
+ }
+
+ private static SkyblockItemRarity getItemRarity(ItemStack stack, ClientPlayerEntity player) {
+ if (stack == null || stack.isEmpty()) return null;
+
+ int hashCode = 0;
+ NbtCompound nbt = stack.getNbt();
+
+ if (nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ String itemUuid = extraAttributes.getString("uuid");
+
+ //If the item has an uuid, then use the hash code of the uuid otherwise use the identity hash code of the stack
+ hashCode = itemUuid.isEmpty() ? System.identityHashCode(stack) : itemUuid.hashCode();
+ }
+
+ if (CACHE.containsKey(hashCode)) return CACHE.get(hashCode);
+
+ List<Text> tooltip = stack.getTooltip(player, TooltipContext.BASIC);
+ String[] stringifiedTooltip = tooltip.stream().map(Text::getString).toArray(String[]::new);
+
+ for (String rarityString : LORE_RARITIES.keySet()) {
+ if (Arrays.stream(stringifiedTooltip).anyMatch(line -> line.contains(rarityString))) {
+ SkyblockItemRarity rarity = LORE_RARITIES.get(rarityString);
+
+ CACHE.put(hashCode, rarity);
+ return rarity;
+ }
+ }
+
+ CACHE.put(hashCode, null);
+ return null;
+ }
+
+ private static void draw(DrawContext context, int x, int y, SkyblockItemRarity rarity) {
+ //Enable blending to handle HUD translucency
+ RenderSystem.enableBlend();
+ RenderSystem.defaultBlendFunc();
+
+ context.drawSprite(x, y, 0, 16, 16, SPRITE.get(), rarity.r, rarity.g, rarity.b, SkyblockerConfigManager.get().general.itemInfoDisplay.itemRarityBackgroundsOpacity);
+
+ RenderSystem.disableBlend();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/PriceInfoTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/PriceInfoTooltip.java
new file mode 100644
index 00000000..05767558
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/PriceInfoTooltip.java
@@ -0,0 +1,443 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Http;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.item.TooltipContext;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.http.HttpHeaders;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+public class PriceInfoTooltip {
+ private static final Logger LOGGER = LoggerFactory.getLogger(PriceInfoTooltip.class.getName());
+ private static final MinecraftClient client = MinecraftClient.getInstance();
+ private static JsonObject npcPricesJson;
+ private static JsonObject bazaarPricesJson;
+ private static JsonObject oneDayAvgPricesJson;
+ private static JsonObject threeDayAvgPricesJson;
+ private static JsonObject lowestPricesJson;
+ private static JsonObject isMuseumJson;
+ private static JsonObject motesPricesJson;
+ private static boolean nullMsgSend = false;
+ private final static Gson gson = new Gson();
+ private static final Map<String, String> apiAddresses;
+ private static long npcHash = 0;
+ private static long museumHash = 0;
+ private static long motesHash = 0;
+
+ public static void onInjectTooltip(ItemStack stack, TooltipContext context, List<Text> lines) {
+ if (!Utils.isOnSkyblock() || client.player == null) return;
+
+ 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;
+ }
+
+ int count = stack.getCount();
+ boolean bazaarOpened = lines.stream().anyMatch(each -> each.getString().contains("Buy price:") || each.getString().contains("Sell price:"));
+
+ if (SkyblockerConfigManager.get().general.itemTooltip.enableNPCPrice) {
+ if (npcPricesJson == null) {
+ nullWarning();
+ } else if (npcPricesJson.has(internalID)) {
+ lines.add(Text.literal(String.format("%-21s", "NPC Price:"))
+ .formatted(Formatting.YELLOW)
+ .append(getCoinsMessage(npcPricesJson.get(internalID).getAsDouble(), count)));
+ }
+ }
+
+ if (SkyblockerConfigManager.get().general.itemTooltip.enableMotesPrice && Utils.isInTheRift()) {
+ if (motesPricesJson == null) {
+ nullWarning();
+ } else if (motesPricesJson.has(internalID)) {
+ lines.add(Text.literal(String.format("%-20s", "Motes Price:"))
+ .formatted(Formatting.LIGHT_PURPLE)
+ .append(getMotesMessage(motesPricesJson.get(internalID).getAsInt(), count)));
+ }
+ }
+
+ boolean bazaarExist = false;
+
+ if (SkyblockerConfigManager.get().general.itemTooltip.enableBazaarPrice && !bazaarOpened) {
+ if (bazaarPricesJson == null) {
+ nullWarning();
+ } else if (bazaarPricesJson.has(name)) {
+ JsonObject getItem = bazaarPricesJson.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 (SkyblockerConfigManager.get().general.itemTooltip.enableLowestBIN && !bazaarOpened && !bazaarExist) {
+ if (lowestPricesJson == null) {
+ nullWarning();
+ } else if (lowestPricesJson.has(name)) {
+ lines.add(Text.literal(String.format("%-19s", "Lowest BIN Price:"))
+ .formatted(Formatting.GOLD)
+ .append(getCoinsMessage(lowestPricesJson.get(name).getAsDouble(), count)));
+ lbinExist = true;
+ }
+ }
+
+ if (SkyblockerConfigManager.get().general.itemTooltip.enableAvgBIN) {
+ if (threeDayAvgPricesJson == null || oneDayAvgPricesJson == null) {
+ nullWarning();
+ } else {
+ /*
+ We are skipping check average prices for potions, runes
+ and enchanted books because there is no data for their in API.
+ */
+ switch (internalID) {
+ case "PET" -> {
+ neuName = neuName.replaceAll("LVL_\\d*_", "");
+ String[] parts = neuName.split("_");
+ String type = parts[0];
+ neuName = neuName.replaceAll(type + "_", "");
+ neuName = neuName + "-" + type;
+ neuName = neuName.replace("UNCOMMON", "1")
+ .replace("COMMON", "0")
+ .replace("RARE", "2")
+ .replace("EPIC", "3")
+ .replace("LEGENDARY", "4")
+ .replace("MYTHIC", "5")
+ .replace("-", ";");
+ }
+ case "RUNE" -> neuName = neuName.replaceAll("_(?!.*_)", ";");
+ case "POTION" -> neuName = "";
+ case "ATTRIBUTE_SHARD" ->
+ neuName = internalID + "+" + neuName.replace("SHARD-", "").replaceAll("_(?!.*_)", ";");
+ default -> neuName = neuName.replace(":", "-");
+ }
+
+ if (!neuName.isEmpty() && lbinExist) {
+ SkyblockerConfig.Average type = SkyblockerConfigManager.get().general.itemTooltip.avg;
+
+ // "No data" line because of API not keeping old data, it causes NullPointerException
+ if (type == SkyblockerConfig.Average.ONE_DAY || type == SkyblockerConfig.Average.BOTH) {
+ lines.add(
+ Text.literal(String.format("%-19s", "1 Day Avg. Price:"))
+ .formatted(Formatting.GOLD)
+ .append(oneDayAvgPricesJson.get(neuName) == null
+ ? Text.literal("No data").formatted(Formatting.RED)
+ : getCoinsMessage(oneDayAvgPricesJson.get(neuName).getAsDouble(), count)
+ )
+ );
+ }
+ if (type == SkyblockerConfig.Average.THREE_DAY || type == SkyblockerConfig.Average.BOTH) {
+ lines.add(
+ Text.literal(String.format("%-19s", "3 Day Avg. Price:"))
+ .formatted(Formatting.GOLD)
+ .append(threeDayAvgPricesJson.get(neuName) == null
+ ? Text.literal("No data").formatted(Formatting.RED)
+ : getCoinsMessage(threeDayAvgPricesJson.get(neuName).getAsDouble(), count)
+ )
+ );
+ }
+ }
+ }
+ }
+
+ if (SkyblockerConfigManager.get().general.itemTooltip.enableMuseumDate && !bazaarOpened) {
+ if (isMuseumJson == null) {
+ nullWarning();
+ } else {
+ String timestamp = getTimestamp(stack);
+
+ if (isMuseumJson.has(internalID)) {
+ String itemCategory = isMuseumJson.get(internalID).getAsString();
+ String format = switch (itemCategory) {
+ case "Weapons" -> "%-18s";
+ case "Armor" -> "%-19s";
+ default -> "%-20s";
+ };
+ lines.add(Text.literal(String.format(format, "Museum: (" + itemCategory + ")"))
+ .formatted(Formatting.LIGHT_PURPLE)
+ .append(Text.literal(timestamp).formatted(Formatting.RED)));
+ } else if (!timestamp.isEmpty()) {
+ lines.add(Text.literal(String.format("%-21s", "Obtained: "))
+ .formatted(Formatting.LIGHT_PURPLE)
+ .append(Text.literal(timestamp).formatted(Formatting.RED)));
+ }
+ }
+ }
+ }
+
+ private static void nullWarning() {
+ if (!nullMsgSend && client.player != null) {
+ client.player.sendMessage(Text.translatable("skyblocker.itemTooltip.nullMessage"), false);
+ nullMsgSend = true;
+ }
+ }
+
+ public static NbtCompound getItemNBT(ItemStack stack) {
+ if (stack == null) return null;
+ return stack.getNbt();
+ }
+
+ /**
+ * this method converts the "timestamp" variable into the same date format as Hypixel represents it in the museum.
+ * Currently, there are two types of 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.
+ *
+ * @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 tag = getItemNBT(stack);
+
+ if (tag != null && tag.contains("ExtraAttributes", 10)) {
+ NbtCompound ea = tag.getCompound("ExtraAttributes");
+
+ if (ea.contains("timestamp", 8)) {
+ SimpleDateFormat nbtFormat = new SimpleDateFormat("MM/dd/yy");
+
+ try {
+ Date date = nbtFormat.parse(ea.getString("timestamp"));
+ SimpleDateFormat skyblockerFormat = new SimpleDateFormat("MMMM dd, yyyy", Locale.ENGLISH);
+ return skyblockerFormat.format(date);
+ } catch (ParseException e) {
+ LOGGER.warn("[Skyblocker-tooltip] getTimestamp", e);
+ }
+ }
+ }
+
+ return "";
+ }
+
+ public static String getInternalNameFromNBT(ItemStack stack, boolean internalIDOnly) {
+ NbtCompound tag = getItemNBT(stack);
+ if (tag == null || !tag.contains("ExtraAttributes", 10)) {
+ return null;
+ }
+ NbtCompound ea = tag.getCompound("ExtraAttributes");
+
+ if (!ea.contains("id", 8)) {
+ return null;
+ }
+ String internalName = ea.getString("id");
+
+ if (internalIDOnly) {
+ return internalName;
+ }
+
+ // Transformation to API format.
+ if (ea.contains("is_shiny")){
+ return "ISSHINY_" + internalName;
+ }
+
+ switch (internalName) {
+ case "ENCHANTED_BOOK" -> {
+ if (ea.contains("enchantments")) {
+ NbtCompound enchants = ea.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 (ea.contains("petInfo")) {
+ JsonObject petInfo = gson.fromJson(ea.getString("petInfo"), JsonObject.class);
+ return "LVL_1_" + petInfo.get("tier").getAsString() + "_" + petInfo.get("type").getAsString();
+ }
+ }
+ case "POTION" -> {
+ String enhanced = ea.contains("enhanced") ? "_ENHANCED" : "";
+ String extended = ea.contains("extended") ? "_EXTENDED" : "";
+ String splash = ea.contains("splash") ? "_SPLASH" : "";
+ if (ea.contains("potion") && ea.contains("potion_level")) {
+ return (ea.getString("potion") + "_" + internalName + "_" + ea.getInt("potion_level")
+ + enhanced + extended + splash).toUpperCase(Locale.ENGLISH);
+ }
+ }
+ case "RUNE" -> {
+ if (ea.contains("runes")) {
+ NbtCompound runes = ea.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 (ea.contains("attributes")) {
+ NbtCompound shards = ea.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) {
+ // Format the price string once
+ String priceString = String.format(Locale.ENGLISH, "%1$,.1f", price);
+
+ // If count is 1, return a simple message
+ if (count == 1) {
+ return Text.literal(priceString + " Coins").formatted(Formatting.DARK_AQUA);
+ }
+
+ // 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().locations.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;
+ }
+
+ // If these options is true beforehand, the client will get first data of these options while loading.
+ // After then, it will only fetch the data if it is on Skyblock.
+ public static int minute = -1;
+
+ public static void init() {
+ Scheduler.INSTANCE.scheduleCyclic(() -> {
+ if (!Utils.isOnSkyblock() && 0 < minute++) {
+ nullMsgSend = false;
+ return;
+ }
+
+ List<CompletableFuture<Void>> futureList = new ArrayList<>();
+ if (SkyblockerConfigManager.get().general.itemTooltip.enableAvgBIN) {
+ SkyblockerConfig.Average type = SkyblockerConfigManager.get().general.itemTooltip.avg;
+
+ if (type == SkyblockerConfig.Average.BOTH || oneDayAvgPricesJson == null || threeDayAvgPricesJson == null || minute % 5 == 0) {
+ futureList.add(CompletableFuture.runAsync(() -> {
+ oneDayAvgPricesJson = downloadPrices("1 day avg");
+ threeDayAvgPricesJson = downloadPrices("3 day avg");
+ }));
+ } else if (type == SkyblockerConfig.Average.ONE_DAY) {
+ futureList.add(CompletableFuture.runAsync(() -> oneDayAvgPricesJson = downloadPrices("1 day avg")));
+ } else if (type == SkyblockerConfig.Average.THREE_DAY) {
+ futureList.add(CompletableFuture.runAsync(() -> threeDayAvgPricesJson = downloadPrices("3 day avg")));
+ }
+ }
+ if (SkyblockerConfigManager.get().general.itemTooltip.enableLowestBIN || SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.enableProfitCalculator)
+ futureList.add(CompletableFuture.runAsync(() -> lowestPricesJson = downloadPrices("lowest bins")));
+
+ if (SkyblockerConfigManager.get().general.itemTooltip.enableBazaarPrice || SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.enableProfitCalculator)
+ futureList.add(CompletableFuture.runAsync(() -> bazaarPricesJson = downloadPrices("bazaar")));
+
+ if (SkyblockerConfigManager.get().general.itemTooltip.enableNPCPrice && npcPricesJson == null)
+ futureList.add(CompletableFuture.runAsync(() -> npcPricesJson = downloadPrices("npc")));
+
+ if (SkyblockerConfigManager.get().general.itemTooltip.enableMuseumDate && isMuseumJson == null)
+ futureList.add(CompletableFuture.runAsync(() -> isMuseumJson = downloadPrices("museum")));
+
+ if (SkyblockerConfigManager.get().general.itemTooltip.enableMotesPrice && motesPricesJson == null)
+ futureList.add(CompletableFuture.runAsync(() -> motesPricesJson = downloadPrices("motes")));
+
+ minute++;
+ CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0]))
+ .whenComplete((unused, throwable) -> nullMsgSend = false);
+ }, 1200);
+ }
+
+ private static JsonObject downloadPrices(String type) {
+ try {
+ String url = apiAddresses.get(type);
+
+ if (type.equals("npc") || type.equals("museum") || type.equals("motes")) {
+ HttpHeaders headers = Http.sendHeadRequest(url);
+ long combinedHash = Http.getEtag(headers).hashCode() + Http.getLastModified(headers).hashCode();
+
+ switch (type) {
+ case "npc": if (npcHash == combinedHash) return npcPricesJson; else npcHash = combinedHash;
+ case "museum": if (museumHash == combinedHash) return isMuseumJson; else museumHash = combinedHash;
+ case "motes": if (motesHash == combinedHash) return motesPricesJson; else motesHash = combinedHash;
+ }
+ }
+
+ String apiResponse = Http.sendGetRequest(url);
+
+ return new Gson().fromJson(apiResponse, JsonObject.class);
+ } catch (Exception e) {
+ LOGGER.warn("[Skyblocker] Failed to download " + type + " prices!", e);
+ return null;
+ }
+ }
+
+ public static JsonObject getBazaarPrices() {
+ return bazaarPricesJson;
+ }
+
+ public static JsonObject getLBINPrices() {
+ return lowestPricesJson;
+ }
+
+ static {
+ apiAddresses = new HashMap<>();
+ apiAddresses.put("1 day avg", "https://moulberry.codes/auction_averages_lbin/1day.json");
+ apiAddresses.put("3 day avg", "https://moulberry.codes/auction_averages_lbin/3day.json");
+ apiAddresses.put("bazaar", "https://hysky.de/api/bazaar");
+ apiAddresses.put("lowest bins", "https://hysky.de/api/auctions/lowestbins");
+ apiAddresses.put("npc", "https://hysky.de/api/npcprice");
+ apiAddresses.put("museum", "https://hysky.de/api/museum");
+ apiAddresses.put("motes", "https://hysky.de/api/motesprice");
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockItemRarity.java b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockItemRarity.java
new file mode 100644
index 00000000..08cc5377
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockItemRarity.java
@@ -0,0 +1,29 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import net.minecraft.util.Formatting;
+
+public enum SkyblockItemRarity {
+ ADMIN(Formatting.DARK_RED),
+ VERY_SPECIAL(Formatting.RED),
+ SPECIAL(Formatting.RED),
+ DIVINE(Formatting.AQUA),
+ MYTHIC(Formatting.LIGHT_PURPLE),
+ LEGENDARY(Formatting.GOLD),
+ EPIC(Formatting.DARK_PURPLE),
+ RARE(Formatting.BLUE),
+ UNCOMMON(Formatting.GREEN),
+ COMMON(Formatting.WHITE);
+
+ public final float r;
+ public final float g;
+ public final float b;
+
+ SkyblockItemRarity(Formatting formatting) {
+ @SuppressWarnings("DataFlowIssue")
+ int rgb = formatting.getColorValue();
+
+ this.r = ((rgb >> 16) & 0xFF) / 255f;
+ this.g = ((rgb >> 8) & 0xFF) / 255f;
+ this.b = (rgb & 0xFF) / 255f;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/WikiLookup.java b/src/main/java/de/hysky/skyblocker/skyblock/item/WikiLookup.java
new file mode 100644
index 00000000..3ab478d0
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/WikiLookup.java
@@ -0,0 +1,56 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.skyblock.itemlist.ItemRegistry;
+import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.option.KeyBinding;
+import net.minecraft.client.util.InputUtil;
+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.Util;
+import org.lwjgl.glfw.GLFW;
+
+import java.util.concurrent.CompletableFuture;
+
+public class WikiLookup {
+ public static KeyBinding wikiLookup;
+ static final MinecraftClient client = MinecraftClient.getInstance();
+ static String id;
+
+ public static void init() {
+ wikiLookup = KeyBindingHelper.registerKeyBinding(new KeyBinding(
+ "key.wikiLookup",
+ InputUtil.Type.KEYSYM,
+ GLFW.GLFW_KEY_F4,
+ "key.categories.skyblocker"
+ ));
+ }
+
+ public static String getSkyblockId(Slot slot) {
+ //Grabbing the skyblock NBT data
+ ItemStack selectedStack = slot.getStack();
+ NbtCompound nbt = selectedStack.getSubNbt("ExtraAttributes");
+ if (nbt != null) {
+ id = nbt.getString("id");
+ }
+ return id;
+ }
+
+ public static void openWiki(Slot slot) {
+ if (Utils.isOnSkyblock()) {
+ id = getSkyblockId(slot);
+ try {
+ String wikiLink = ItemRegistry.getWikiLink(id);
+ CompletableFuture.runAsync(() -> Util.getOperatingSystem().open(wikiLink));
+ } catch (IndexOutOfBoundsException | IllegalStateException e) {
+ e.printStackTrace();
+ if (client.player != null)
+ client.player.sendMessage(Text.of("Error while retrieving wiki article..."), false);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemFixerUpper.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemFixerUpper.java
new file mode 100644
index 00000000..488c5f48
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemFixerUpper.java
@@ -0,0 +1,341 @@
+package de.hysky.skyblocker.skyblock.itemlist;
+
+import java.util.Map;
+
+public class ItemFixerUpper {
+ private final static String[] ANVIL_VARIANTS = {
+ "minecraft:anvil",
+ "minecraft:chipped_anvil",
+ "minecraft:damaged_anvil"
+ };
+
+ private final static String[] COAL_VARIANTS = {
+ "minecraft:coal",
+ "minecraft:charcoal"
+ };
+
+ private final static String[] COBBLESTONE_WALL_VARIANTS = {
+ "minecraft:cobblestone_wall",
+ "minecraft:mossy_cobblestone_wall"
+ };
+
+ private final static String[] COOKED_FISH_VARIANTS = {
+ "minecraft:cooked_cod",
+ "minecraft:cooked_salmon"
+ };
+
+ private final static String[] DIRT_VARIANTS = {
+ "minecraft:dirt",
+ "minecraft:coarse_dirt",
+ "minecraft:podzol"
+ };
+
+ private final static String[] DOUBLE_PLANT_VARIANTS = {
+ "minecraft:sunflower",
+ "minecraft:lilac",
+ "minecraft:tall_grass",
+ "minecraft:large_fern",
+ "minecraft:rose_bush",
+ "minecraft:peony"
+ };
+
+ private final static String[] DYE_VARIANTS = {
+ "minecraft:ink_sac",
+ "minecraft:red_dye",
+ "minecraft:green_dye",
+ "minecraft:cocoa_beans",
+ "minecraft:lapis_lazuli",
+ "minecraft:purple_dye",
+ "minecraft:cyan_dye",
+ "minecraft:light_gray_dye",
+ "minecraft:gray_dye",
+ "minecraft:pink_dye",
+ "minecraft:lime_dye",
+ "minecraft:yellow_dye",
+ "minecraft:light_blue_dye",
+ "minecraft:magenta_dye",
+ "minecraft:orange_dye",
+ "minecraft:bone_meal"
+ };
+
+ private final static String[] FISH_VARIANTS = {
+ "minecraft:cod",
+ "minecraft:salmon",
+ "minecraft:tropical_fish",
+ "minecraft:pufferfish"
+ };
+
+ private final static String[] GOLDEN_APPLE_VARIANTS = {
+ "minecraft:golden_apple",
+ "minecraft:enchanted_golden_apple"
+ };
+
+ private final static String[] LOG_VARIANTS = {
+ "minecraft:oak_log",
+ "minecraft:spruce_log",
+ "minecraft:birch_log",
+ "minecraft:jungle_log",
+ "minecraft:oak_wood",
+ "minecraft:spruce_wood",
+ "minecraft:birch_wood",
+ "minecraft:jungle_wood",
+ };
+
+ private final static String[] LOG2_VARIANTS = {
+ "minecraft:acacia_log",
+ "minecraft:dark_oak_log",
+ "minecraft:acacia_wood",
+ "minecraft:dark_oak_wood"
+ };
+
+ private final static String[] MONSTER_EGG_VARIANTS = {
+ "minecraft:infested_stone",
+ "minecraft:infested_cobblestone",
+ "minecraft:infested_stone_bricks",
+ "minecraft:infested_mossy_stone_bricks",
+ "minecraft:infested_cracked_stone_bricks",
+ "minecraft:infested_chiseled_stone_bricks"
+ };
+
+ private final static String[] PRISMARINE_VARIANTS = {
+ "minecraft:prismarine",
+ "minecraft:prismarine_bricks",
+ "minecraft:dark_prismarine"
+ };
+
+ private final static String[] QUARTZ_BLOCK_VARIANTS = {
+ "minecraft:quartz_block",
+ "minecraft:chiseled_quartz_block",
+ "minecraft:quartz_pillar"
+ };
+
+ private final static String[] RED_FLOWER_VARIANTS = {
+ "minecraft:poppy",
+ "minecraft:blue_orchid",
+ "minecraft:allium",
+ "minecraft:azure_bluet",
+ "minecraft:red_tulip",
+ "minecraft:orange_tulip",
+ "minecraft:white_tulip",
+ "minecraft:pink_tulip",
+ "minecraft:oxeye_daisy"
+ };
+
+ private final static String[] SAND_VARIANTS = {
+ "minecraft:sand",
+ "minecraft:red_sand"
+ };
+
+ private final static String[] SKULL_VARIANTS = {
+ "minecraft:skeleton_skull",
+ "minecraft:wither_skeleton_skull",
+ "minecraft:zombie_head",
+ "minecraft:player_head",
+ "minecraft:creeper_head"
+ };
+
+ private final static String[] SPONGE_VARIANTS = {
+ "minecraft:sponge",
+ "minecraft:wet_sponge"
+ };
+
+ private final static String[] STONE_VARIANTS = {
+ "minecraft:stone",
+ "minecraft:granite",
+ "minecraft:polished_granite",
+ "minecraft:diorite",
+ "minecraft:polished_diorite",
+ "minecraft:andesite",
+ "minecraft:polished_andesite"
+ };
+
+ private final static String[] STONE_SLAB_VARIANTS = {
+ "minecraft:smooth_stone_slab",
+ "minecraft:sandstone_slab",
+ "minecraft:petrified_oak_slab",
+ "minecraft:cobblestone_slab",
+ "minecraft:brick_slab",
+ "minecraft:stone_brick_slab",
+ "minecraft:nether_brick_slab",
+ "minecraft:quartz_slab"
+ };
+
+ private final static String[] STONEBRICK_VARIANTS = {
+ "minecraft:stone_bricks",
+ "minecraft:mossy_stone_bricks",
+ "minecraft:cracked_stone_bricks",
+ "minecraft:chiseled_stone_bricks"
+ };
+
+ private final static String[] TALLGRASS_VARIANTS = {
+ "minecraft:dead_bush",
+ "minecraft:grass",
+ "minecraft:fern"
+ };
+
+ private final static Map<Integer, String> SPAWN_EGG_VARIANTS = Map.ofEntries(
+ //This entry 0 is technically not right but Hypixel decided to make it polar bear so well we use that
+ Map.entry(0, "minecraft:polar_bear_spawn_egg"),
+ Map.entry(50, "minecraft:creeper_spawn_egg"),
+ Map.entry(51, "minecraft:skeleton_spawn_egg"),
+ Map.entry(52, "minecraft:spider_spawn_egg"),
+ Map.entry(54, "minecraft:zombie_spawn_egg"),
+ Map.entry(55, "minecraft:slime_spawn_egg"),
+ Map.entry(56, "minecraft:ghast_spawn_egg"),
+ Map.entry(57, "minecraft:zombified_piglin_spawn_egg"),
+ Map.entry(58, "minecraft:enderman_spawn_egg"),
+ Map.entry(59, "minecraft:cave_spider_spawn_egg"),
+ Map.entry(60, "minecraft:silverfish_spawn_egg"),
+ Map.entry(61, "minecraft:blaze_spawn_egg"),
+ Map.entry(62, "minecraft:magma_cube_spawn_egg"),
+ Map.entry(65, "minecraft:bat_spawn_egg"),
+ Map.entry(66, "minecraft:witch_spawn_egg"),
+ Map.entry(67, "minecraft:endermite_spawn_egg"),
+ Map.entry(68, "minecraft:guardian_spawn_egg"),
+ Map.entry(90, "minecraft:pig_spawn_egg"),
+ Map.entry(91, "minecraft:sheep_spawn_egg"),
+ Map.entry(92, "minecraft:cow_spawn_egg"),
+ Map.entry(93, "minecraft:chicken_spawn_egg"),
+ Map.entry(94, "minecraft:squid_spawn_egg"),
+ Map.entry(95, "minecraft:wolf_spawn_egg"),
+ Map.entry(96, "minecraft:mooshroom_spawn_egg"),
+ Map.entry(98, "minecraft:ocelot_spawn_egg"),
+ Map.entry(100, "minecraft:horse_spawn_egg"),
+ Map.entry(101, "minecraft:rabbit_spawn_egg"),
+ Map.entry(120, "minecraft:villager_spawn_egg")
+ );
+
+ private final static String[] SANDSTONE_VARIANTS = {
+ ":",
+ ":chiseled_",
+ ":cut_"
+ };
+
+ private final static String[] COLOR_VARIANTS = {
+ ":white_",
+ ":orange_",
+ ":magenta_",
+ ":light_blue_",
+ ":yellow_",
+ ":lime_",
+ ":pink_",
+ ":gray_",
+ ":light_gray_",
+ ":cyan_",
+ ":purple_",
+ ":blue_",
+ ":brown_",
+ ":green_",
+ ":red_",
+ ":black_"
+ };
+
+ private final static String[] WOOD_VARIANTS = {
+ ":oak_",
+ ":spruce_",
+ ":birch_",
+ ":jungle_",
+ ":acacia_",
+ ":dark_oak_"
+ };
+
+ //this is the map of all renames
+ private final static Map<String, String> RENAMED = Map.ofEntries(
+ Map.entry("minecraft:bed", "minecraft:red_bed"),
+ Map.entry("minecraft:boat", "minecraft:oak_boat"),
+ Map.entry("minecraft:brick_block", "minecraft:bricks"),
+ Map.entry("minecraft:deadbush", "minecraft:dead_bush"),
+ Map.entry("minecraft:fence_gate", "minecraft:oak_fence_gate"),
+ Map.entry("minecraft:fence", "minecraft:oak_fence"),
+ Map.entry("minecraft:firework_charge", "minecraft:firework_star"),
+ Map.entry("minecraft:fireworks", "minecraft:firework_rocket"),
+ Map.entry("minecraft:golden_rail", "minecraft:powered_rail"),
+ Map.entry("minecraft:grass", "minecraft:grass_block"),
+ Map.entry("minecraft:hardened_clay", "minecraft:terracotta"),
+ Map.entry("minecraft:lit_pumpkin", "minecraft:jack_o_lantern"),
+ Map.entry("minecraft:melon_block", "minecraft:melon"),
+ Map.entry("minecraft:melon", "minecraft:melon_slice"),
+ Map.entry("minecraft:mob_spawner", "minecraft:spawner"),
+ Map.entry("minecraft:nether_brick", "minecraft:nether_bricks"),
+ Map.entry("minecraft:netherbrick", "minecraft:nether_brick"),
+ Map.entry("minecraft:noteblock", "minecraft:note_block"),
+ Map.entry("minecraft:piston_extension", "minecraft:moving_piston"),
+ Map.entry("minecraft:portal", "minecraft:nether_portal"),
+ Map.entry("minecraft:pumpkin", "minecraft:carved_pumpkin"),
+ Map.entry("minecraft:quartz_ore", "minecraft:nether_quartz_ore"),
+ Map.entry("minecraft:record_11", "minecraft:music_disc_11"),
+ Map.entry("minecraft:record_13", "minecraft:music_disc_13"),
+ Map.entry("minecraft:record_blocks", "minecraft:music_disc_blocks"),
+ Map.entry("minecraft:record_cat", "minecraft:music_disc_cat"),
+ Map.entry("minecraft:record_chirp", "minecraft:music_disc_chirp"),
+ Map.entry("minecraft:record_far", "minecraft:music_disc_far"),
+ Map.entry("minecraft:record_mall", "minecraft:music_disc_mall"),
+ Map.entry("minecraft:record_mellohi", "minecraft:music_disc_mellohi"),
+ Map.entry("minecraft:record_stal", "minecraft:music_disc_stal"),
+ Map.entry("minecraft:record_strad", "minecraft:music_disc_strad"),
+ Map.entry("minecraft:record_wait", "minecraft:music_disc_wait"),
+ Map.entry("minecraft:record_ward", "minecraft:music_disc_ward"),
+ Map.entry("minecraft:red_nether_brick", "minecraft:red_nether_bricks"),
+ Map.entry("minecraft:reeds", "minecraft:sugar_cane"),
+ Map.entry("minecraft:sign", "minecraft:oak_sign"),
+ Map.entry("minecraft:slime", "minecraft:slime_block"),
+ Map.entry("minecraft:snow_layer", "minecraft:snow"),
+ Map.entry("minecraft:snow", "minecraft:snow_block"),
+ Map.entry("minecraft:speckled_melon", "minecraft:glistering_melon_slice"),
+ Map.entry("minecraft:stone_slab2", "minecraft:red_sandstone_slab"),
+ Map.entry("minecraft:stone_stairs", "minecraft:cobblestone_stairs"),
+ Map.entry("minecraft:trapdoor", "minecraft:oak_trapdoor"),
+ Map.entry("minecraft:waterlily", "minecraft:lily_pad"),
+ Map.entry("minecraft:web", "minecraft:cobweb"),
+ Map.entry("minecraft:wooden_button", "minecraft:oak_button"),
+ Map.entry("minecraft:wooden_door", "minecraft:oak_door"),
+ Map.entry("minecraft:wooden_pressure_plate", "minecraft:oak_pressure_plate"),
+ Map.entry("minecraft:yellow_flower", "minecraft:dandelion")
+ );
+
+ //TODO : Add mushroom block variants
+ //i'll do it later because it isn't used and unlike the other, it's not just a rename or a separate, it's a separate and a merge
+
+ public static String convertItemId(String id, int damage) {
+ return switch (id) {
+ //all the case are simple separate
+ case "minecraft:anvil" -> ANVIL_VARIANTS[damage];
+ case "minecraft:coal" -> COAL_VARIANTS[damage];
+ case "minecraft:cobblestone_wall" -> COBBLESTONE_WALL_VARIANTS[damage];
+ case "minecraft:cooked_fish" -> COOKED_FISH_VARIANTS[damage];
+ case "minecraft:dirt" -> DIRT_VARIANTS[damage];
+ case "minecraft:double_plant" -> DOUBLE_PLANT_VARIANTS[damage];
+ case "minecraft:dye" -> DYE_VARIANTS[damage];
+ case "minecraft:fish" -> FISH_VARIANTS[damage];
+ case "minecraft:golden_apple" -> GOLDEN_APPLE_VARIANTS[damage];
+ case "minecraft:log" -> LOG_VARIANTS[damage];
+ case "minecraft:log2" -> LOG2_VARIANTS[damage];
+ case "minecraft:monster_egg" -> MONSTER_EGG_VARIANTS[damage];
+ case "minecraft:prismarine" -> PRISMARINE_VARIANTS[damage];
+ case "minecraft:quartz_block" -> QUARTZ_BLOCK_VARIANTS[damage];
+ case "minecraft:red_flower" -> RED_FLOWER_VARIANTS[damage];
+ case "minecraft:sand" -> SAND_VARIANTS[damage];
+ case "minecraft:skull" -> SKULL_VARIANTS[damage];
+ case "minecraft:sponge" -> SPONGE_VARIANTS[damage];
+ case "minecraft:stone" -> STONE_VARIANTS[damage];
+ case "minecraft:stone_slab" -> STONE_SLAB_VARIANTS[damage];
+ case "minecraft:stonebrick" -> STONEBRICK_VARIANTS[damage];
+ case "minecraft:tallgrass" -> TALLGRASS_VARIANTS[damage];
+ //we use a Map from int to str instead of an array because numbers are not consecutive
+ case "minecraft:spawn_egg" -> SPAWN_EGG_VARIANTS.get(damage);
+ //when we use the generalized variant we need to replaceFirst
+ case "minecraft:sandstone", "minecraft:red_sandstone" -> id.replaceFirst(":", SANDSTONE_VARIANTS[damage]);
+ //to use the general color variants we need to reverse the order because Minecraft decided so for some reason
+ case "minecraft:banner" -> id.replaceFirst(":", COLOR_VARIANTS[15 - damage]);
+ case "minecraft:carpet", "minecraft:stained_glass", "minecraft:stained_glass_pane", "minecraft:wool" -> id.replaceFirst(":", COLOR_VARIANTS[damage]);
+ //for the terracotta we replace the whole name by the color and append "terracotta" at the end
+ case "minecraft:stained_hardened_clay" -> id.replaceFirst(":stained_hardened_clay", COLOR_VARIANTS[damage]) + "terracotta";
+ //for the wooden slab we need to remove the "wooden_" prefix, but otherwise it's the same, so I just combined them anyway
+ case "minecraft:leaves", "minecraft:planks", "minecraft:sapling", "minecraft:wooden_slab" -> id.replaceFirst(":(?:wooden_)?", WOOD_VARIANTS[damage]);
+ //here we replace the 2 by nothing to remove it as it's not needed anymore
+ case "minecraft:leaves2" -> id.replaceFirst(":", WOOD_VARIANTS[damage + 4]).replaceFirst("2", "");
+ //the default case is just a rename or no change
+ default -> RENAMED.getOrDefault(id, id);
+ };
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java
new file mode 100644
index 00000000..afdcaca8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java
@@ -0,0 +1,102 @@
+package de.hysky.skyblocker.skyblock.itemlist;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import de.hysky.skyblocker.mixin.accessor.RecipeBookWidgetAccessor;
+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.widget.TextFieldWidget;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.screen.AbstractRecipeScreenHandler;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+@Environment(value = EnvType.CLIENT)
+public class ItemListWidget extends RecipeBookWidget {
+ private int parentWidth;
+ private int parentHeight;
+ private int leftOffset;
+ private TextFieldWidget searchField;
+ private SearchResultsWidget results;
+
+ 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) {
+ 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 (ItemRegistry.filesImported) {
+ this.results = new SearchResultsWidget(this.client, x, y);
+ this.updateSearchResult();
+ }
+ }
+
+ @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;
+ context.drawTexture(TEXTURE, i, j, 1, 1, 147, 166);
+ this.searchField = ((RecipeBookWidgetAccessor) this).getSearchField();
+
+ if (!ItemRegistry.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 (ItemRegistry.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);
+ }
+ matrices.pop();
+ }
+ }
+
+ @Override
+ public void drawTooltip(DrawContext context, int x, int y, int mouseX, int mouseY) {
+ if (this.isOpen() && ItemRegistry.filesImported && results != null) {
+ this.results.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() && ItemRegistry.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);
+ }
+ } else return false;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRegistry.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRegistry.java
new file mode 100644
index 00000000..edfeccc0
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRegistry.java
@@ -0,0 +1,137 @@
+package de.hysky.skyblocker.skyblock.itemlist;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import de.hysky.skyblocker.utils.NEURepo;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.text.Text;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+public class ItemRegistry {
+ protected static final Logger LOGGER = LoggerFactory.getLogger(ItemRegistry.class);
+ protected static final Path ITEM_LIST_DIR = NEURepo.LOCAL_REPO_DIR.resolve("items");
+
+ protected static final List<ItemStack> items = new ArrayList<>();
+ protected static final Map<String, ItemStack> itemsMap = new HashMap<>();
+ protected static final List<SkyblockCraftingRecipe> recipes = new ArrayList<>();
+ public static final MinecraftClient client = MinecraftClient.getInstance();
+ public static boolean filesImported = false;
+
+ public static void init() {
+ NEURepo.runAsyncAfterLoad(ItemStackBuilder::loadPetNums);
+ NEURepo.runAsyncAfterLoad(ItemRegistry::importItemFiles);
+ }
+
+ private static void importItemFiles() {
+ List<JsonObject> jsonObjs = new ArrayList<>();
+
+ File dir = ITEM_LIST_DIR.toFile();
+ File[] files = dir.listFiles();
+ if (files == null) {
+ return;
+ }
+ for (File file : files) {
+ Path path = ITEM_LIST_DIR.resolve(file.getName());
+ try {
+ String fileContent = Files.readString(path);
+ jsonObjs.add(JsonParser.parseString(fileContent).getAsJsonObject());
+ } catch (Exception e) {
+ LOGGER.error("Failed to read file " + path, e);
+ }
+ }
+
+ for (JsonObject jsonObj : jsonObjs) {
+ String internalName = jsonObj.get("internalname").getAsString();
+ ItemStack itemStack = ItemStackBuilder.parseJsonObj(jsonObj);
+ items.add(itemStack);
+ itemsMap.put(internalName, itemStack);
+ }
+ for (JsonObject jsonObj : jsonObjs)
+ if (jsonObj.has("recipe")) {
+ recipes.add(SkyblockCraftingRecipe.fromJsonObject(jsonObj));
+ }
+
+ items.sort((lhs, rhs) -> {
+ String lhsInternalName = getInternalName(lhs);
+ String lhsFamilyName = lhsInternalName.replaceAll(".\\d+$", "");
+ String rhsInternalName = getInternalName(rhs);
+ String rhsFamilyName = rhsInternalName.replaceAll(".\\d+$", "");
+ if (lhsFamilyName.equals(rhsFamilyName)) {
+ if (lhsInternalName.length() != rhsInternalName.length())
+ return lhsInternalName.length() - rhsInternalName.length();
+ else return lhsInternalName.compareTo(rhsInternalName);
+ }
+ return lhsFamilyName.compareTo(rhsFamilyName);
+ });
+ filesImported = true;
+ }
+
+ public static String getWikiLink(String internalName) {
+ try {
+ String fileContent = Files.readString(ITEM_LIST_DIR.resolve(internalName + ".json"));
+ JsonObject fileJson = JsonParser.parseString(fileContent).getAsJsonObject();
+ //TODO optional official or unofficial wiki link
+ try {
+ return fileJson.get("info").getAsJsonArray().get(1).getAsString();
+ } catch (IndexOutOfBoundsException e) {
+ return fileJson.get("info").getAsJsonArray().get(0).getAsString();
+ }
+ } catch (IOException | NullPointerException e) {
+ LOGGER.error("Failed to read item file " + internalName + ".json", e);
+ if (client.player != null) {
+ client.player.sendMessage(Text.of("Can't locate a wiki article for this item..."), false);
+ }
+ return null;
+ }
+ }
+
+ public static List<SkyblockCraftingRecipe> getRecipes(String internalName) {
+ List<SkyblockCraftingRecipe> result = new ArrayList<>();
+ for (SkyblockCraftingRecipe recipe : recipes)
+ if (getInternalName(recipe.result).equals(internalName)) result.add(recipe);
+ for (SkyblockCraftingRecipe recipe : recipes)
+ for (ItemStack ingredient : recipe.grid)
+ if (!ingredient.getItem().equals(Items.AIR) && getInternalName(ingredient).equals(internalName)) {
+ result.add(recipe);
+ break;
+ }
+ return result;
+ }
+
+ public static Stream<SkyblockCraftingRecipe> getRecipesStream() {
+ return recipes.stream();
+ }
+
+ public static Stream<ItemStack> getItemsStream() {
+ return items.stream();
+ }
+
+ /**
+ * Get Internal name of an ItemStack
+ *
+ * @param itemStack ItemStack to get internal name from
+ * @return internal name of the given ItemStack
+ */
+ public static String getInternalName(ItemStack itemStack) {
+ if (itemStack.getNbt() == null) return "";
+ return itemStack.getNbt().getCompound("ExtraAttributes").getString("id");
+ }
+
+ public static ItemStack getItemStack(String internalName) {
+ return itemsMap.get(internalName);
+ }
+}
+
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java
new file mode 100644
index 00000000..24146c64
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java
@@ -0,0 +1,154 @@
+package de.hysky.skyblocker.skyblock.itemlist;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import de.hysky.skyblocker.utils.NEURepo;
+import net.minecraft.item.FireworkRocketItem;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.*;
+import net.minecraft.text.Text;
+import net.minecraft.util.Pair;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ItemStackBuilder {
+ private final static Path PETNUMS_PATH = NEURepo.LOCAL_REPO_DIR.resolve("constants/petnums.json");
+ private static JsonObject petNums;
+
+ public static void loadPetNums() {
+ try {
+ petNums = JsonParser.parseString(Files.readString(PETNUMS_PATH)).getAsJsonObject();
+ } catch (Exception e) {
+ ItemRegistry.LOGGER.error("Failed to load petnums.json");
+ }
+ }
+
+ public static ItemStack parseJsonObj(JsonObject obj) {
+ String internalName = obj.get("internalname").getAsString();
+
+ List<Pair<String, String>> injectors = new ArrayList<>(petData(internalName));
+
+ NbtCompound root = new NbtCompound();
+ root.put("Count", NbtByte.of((byte)1));
+
+ String id = obj.get("itemid").getAsString();
+ int damage = obj.get("damage").getAsInt();
+ root.put("id", NbtString.of(ItemFixerUpper.convertItemId(id, damage)));
+
+ NbtCompound tag = new NbtCompound();
+ root.put("tag", tag);
+
+ NbtCompound extra = new NbtCompound();
+ tag.put("ExtraAttributes", extra);
+ extra.put("id", NbtString.of(internalName));
+
+ NbtCompound display = new NbtCompound();
+ tag.put("display", display);
+
+ String name = injectData(obj.get("displayname").getAsString(), injectors);
+ display.put("Name", NbtString.of(Text.Serializer.toJson(Text.of(name))));
+
+ NbtList lore = new NbtList();
+ display.put("Lore", lore);
+ obj.get("lore").getAsJsonArray().forEach(el ->
+ lore.add(NbtString.of(Text.Serializer.toJson(Text.of(injectData(el.getAsString(), injectors)))))
+ );
+
+ String nbttag = obj.get("nbttag").getAsString();
+ // add skull texture
+ Matcher skullUuid = Pattern.compile("(?<=SkullOwner:\\{)Id:\"(.{36})\"").matcher(nbttag);
+ Matcher skullTexture = Pattern.compile("(?<=Properties:\\{textures:\\[0:\\{Value:)\"(.+?)\"").matcher(nbttag);
+ if (skullUuid.find() && skullTexture.find()) {
+ NbtCompound skullOwner = new NbtCompound();
+ tag.put("SkullOwner", skullOwner);
+ UUID uuid = UUID.fromString(skullUuid.group(1));
+ skullOwner.put("Id", NbtHelper.fromUuid(uuid));
+ skullOwner.put("Name", NbtString.of(internalName));
+
+ NbtCompound properties = new NbtCompound();
+ skullOwner.put("Properties", properties);
+ NbtList textures = new NbtList();
+ properties.put("textures", textures);
+ NbtCompound texture = new NbtCompound();
+ textures.add(texture);
+ texture.put("Value", NbtString.of(skullTexture.group(1)));
+ }
+ // add leather armor dye color
+ Matcher colorMatcher = Pattern.compile("color:(\\d+)").matcher(nbttag);
+ if (colorMatcher.find()) {
+ NbtInt color = NbtInt.of(Integer.parseInt(colorMatcher.group(1)));
+ display.put("color", color);
+ }
+ // add enchantment glint
+ if (nbttag.contains("ench:")) {
+ NbtList enchantments = new NbtList();
+ enchantments.add(new NbtCompound());
+ tag.put("Enchantments", enchantments);
+ }
+
+ // Add firework star color
+ Matcher explosionColorMatcher = Pattern.compile("\\{Explosion:\\{(?:Type:[0-9a-z]+,)?Colors:\\[(?<color>[0-9]+)\\]\\}").matcher(nbttag);
+ if (explosionColorMatcher.find()) {
+ NbtCompound explosion = new NbtCompound();
+
+ explosion.putInt("Type", FireworkRocketItem.Type.SMALL_BALL.getId()); //Forget about the actual ball type because it probably doesn't matter
+ explosion.putIntArray("Colors", new int[] { Integer.parseInt(explosionColorMatcher.group("color")) });
+ tag.put("Explosion", explosion);
+ }
+
+ return ItemStack.fromNbt(root);
+ }
+
+ // TODO: fix stats for GOLDEN_DRAGON (lv1 -> lv200)
+ private static List<Pair<String, String>> petData(String internalName) {
+ List<Pair<String, String>> list = new ArrayList<>();
+
+ String petName = internalName.split(";")[0];
+ if (!internalName.contains(";") || !petNums.has(petName)) return list;
+
+ list.add(new Pair<>("\\{LVL\\}", "1 ➡ 100"));
+
+ final String[] rarities = {
+ "COMMON",
+ "UNCOMMON",
+ "RARE",
+ "EPIC",
+ "LEGENDARY",
+ "MYTHIC"
+ };
+ String rarity = rarities[Integer.parseInt(internalName.split(";")[1])];
+ JsonObject data = petNums.get(petName).getAsJsonObject().get(rarity).getAsJsonObject();
+
+ JsonObject statNumsMin = data.get("1").getAsJsonObject().get("statNums").getAsJsonObject();
+ JsonObject statNumsMax = data.get("100").getAsJsonObject().get("statNums").getAsJsonObject();
+ Set<Map.Entry<String, JsonElement>> entrySet = statNumsMin.entrySet();
+ for (Map.Entry<String, JsonElement> entry : entrySet) {
+ String key = entry.getKey();
+ String left = "\\{" + key+ "\\}";
+ String right = statNumsMin.get(key).getAsString() + " ➡ " + statNumsMax.get(key).getAsString();
+ list.add(new Pair<>(left, right));
+ }
+
+ JsonArray otherNumsMin = data.get("1").getAsJsonObject().get("otherNums").getAsJsonArray();
+ JsonArray otherNumsMax = data.get("100").getAsJsonObject().get("otherNums").getAsJsonArray();
+ for (int i = 0; i < otherNumsMin.size(); ++i) {
+ String left = "\\{" + i + "\\}";
+ String right = otherNumsMin.get(i).getAsString() + " ➡ " + otherNumsMax.get(i).getAsString();
+ list.add(new Pair<>(left, right));
+ }
+
+ return list;
+ }
+
+ private static String injectData(String string, List<Pair<String, String>> injectors) {
+ for (Pair<String, String> injector : injectors)
+ string = string.replaceAll(injector.getLeft(), injector.getRight());
+ return string;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ResultButtonWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ResultButtonWidget.java
new file mode 100644
index 00000000..814611e5
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ResultButtonWidget.java
@@ -0,0 +1,65 @@
+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;
+
+public class ResultButtonWidget extends ClickableWidget {
+ private static final Identifier BACKGROUND_TEXTURE = new Identifier("recipe_book/slot_craftable");
+
+ protected ItemStack itemStack = null;
+
+ public ResultButtonWidget(int x, int y) {
+ super(x, y, 25, 25, Text.of(""));
+ }
+
+ protected void setItemStack(ItemStack itemStack) {
+ this.active = !itemStack.getItem().equals(Items.AIR);
+ this.visible = true;
+ this.itemStack = itemStack;
+ }
+
+ protected void clearItemStack() {
+ this.visible = false;
+ this.itemStack = null;
+ }
+
+ @Override
+ public void renderButton(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();
+ 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);
+ }
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {
+ // TODO Auto-generated method stub
+
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java
new file mode 100644
index 00000000..eedf695e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java
@@ -0,0 +1,228 @@
+package de.hysky.skyblocker.skyblock.itemlist;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+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.screen.ButtonTextures;
+import net.minecraft.client.gui.widget.ToggleButtonWidget;
+import net.minecraft.item.ItemStack;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.Identifier;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.jetbrains.annotations.Nullable;
+
+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"));
+ private static final int COLS = 5;
+ private static final int MAX_TEXT_WIDTH = 124;
+ private static final String ELLIPSIS = "...";
+ private static final Pattern FORMATTING_CODE_PATTERN = Pattern.compile("(?i)§[0-9A-FK-OR]");
+
+ private final MinecraftClient client;
+ private final int parentX;
+ private final int parentY;
+
+ private final List<ItemStack> searchResults = new ArrayList<>();
+ private List<SkyblockCraftingRecipe> recipeResults = new ArrayList<>();
+ private String searchText = null;
+ private final List<ResultButtonWidget> resultButtons = new ArrayList<>();
+ private final ToggleButtonWidget nextPageButton;
+ private final ToggleButtonWidget prevPageButton;
+ private int currentPage = 0;
+ private int pageCount = 0;
+ private boolean displayRecipes = false;
+
+ public SearchResultsWidget(MinecraftClient client, int parentX, int parentY) {
+ this.client = client;
+ this.parentX = parentX;
+ this.parentY = parentY;
+ int gridX = parentX + 11;
+ int gridY = parentY + 31;
+ int rows = 4;
+ for (int i = 0; i < rows; ++i)
+ for (int j = 0; j < COLS; ++j) {
+ int x = gridX + j * 25;
+ int y = gridY + i * 25;
+ resultButtons.add(new ResultButtonWidget(x, y));
+ }
+ this.nextPageButton = new ToggleButtonWidget(parentX + 93, parentY + 137, 12, 17, false);
+ this.nextPageButton.setTextures(PAGE_FORWARD_TEXTURES);
+ this.prevPageButton = new ToggleButtonWidget(parentX + 38, parentY + 137, 12, 17, true);
+ this.prevPageButton.setTextures(PAGE_BACKWARD_TEXTURES);
+ }
+
+ public void closeRecipeView() {
+ this.currentPage = 0;
+ this.pageCount = (this.searchResults.size() - 1) / resultButtons.size() + 1;
+ this.displayRecipes = false;
+ this.updateButtons();
+ }
+
+ protected void updateSearchResult(String searchText) {
+ if (!searchText.equals(this.searchText)) {
+ this.searchText = searchText;
+ this.searchResults.clear();
+ for (ItemStack entry : ItemRegistry.items) {
+ String name = entry.getName().toString().toLowerCase(Locale.ENGLISH);
+ if (entry.getNbt() == null) {
+ continue;
+ }
+ String disp = entry.getNbt().getCompound("display").toString().toLowerCase(Locale.ENGLISH);
+ if (name.contains(this.searchText) || disp.contains(this.searchText))
+ this.searchResults.add(entry);
+ }
+ this.currentPage = 0;
+ this.pageCount = (this.searchResults.size() - 1) / resultButtons.size() + 1;
+ this.displayRecipes = false;
+ this.updateButtons();
+ }
+ }
+
+ private void updateButtons() {
+ if (this.displayRecipes) {
+ SkyblockCraftingRecipe recipe = this.recipeResults.get(this.currentPage);
+ for (ResultButtonWidget button : resultButtons)
+ button.clearItemStack();
+ resultButtons.get(5).setItemStack(recipe.grid.get(0));
+ resultButtons.get(6).setItemStack(recipe.grid.get(1));
+ resultButtons.get(7).setItemStack(recipe.grid.get(2));
+ resultButtons.get(10).setItemStack(recipe.grid.get(3));
+ resultButtons.get(11).setItemStack(recipe.grid.get(4));
+ resultButtons.get(12).setItemStack(recipe.grid.get(5));
+ resultButtons.get(15).setItemStack(recipe.grid.get(6));
+ resultButtons.get(16).setItemStack(recipe.grid.get(7));
+ resultButtons.get(17).setItemStack(recipe.grid.get(8));
+ resultButtons.get(14).setItemStack(recipe.result);
+ } else {
+ for (int i = 0; i < resultButtons.size(); ++i) {
+ int index = this.currentPage * resultButtons.size() + i;
+ if (index < this.searchResults.size()) {
+ resultButtons.get(i).setItemStack(this.searchResults.get(index));
+ } else {
+ resultButtons.get(i).clearItemStack();
+ }
+ }
+ }
+ this.prevPageButton.active = this.currentPage > 0;
+ this.nextPageButton.active = this.currentPage < this.pageCount - 1;
+ }
+
+ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
+ TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+ RenderSystem.disableDepthTest();
+ if (this.displayRecipes) {
+ //Craft text - usually a requirement for the recipe
+ String craftText = this.recipeResults.get(this.currentPage).craftText;
+ if (textRenderer.getWidth(craftText) > MAX_TEXT_WIDTH) {
+ drawTooltip(textRenderer, context, craftText, this.parentX + 11, this.parentY + 31, mouseX, mouseY);
+ craftText = textRenderer.trimToWidth(craftText, MAX_TEXT_WIDTH) + ELLIPSIS;
+ }
+ context.drawTextWithShadow(textRenderer, craftText, this.parentX + 11, this.parentY + 31, 0xffffffff);
+
+ //Item name
+ Text resultText = this.recipeResults.get(this.currentPage).result.getName();
+ if (textRenderer.getWidth(Formatting.strip(resultText.getString())) > MAX_TEXT_WIDTH) {
+ drawTooltip(textRenderer, context, resultText, this.parentX + 11, this.parentY + 43, mouseX, mouseY);
+ resultText = Text.literal(getLegacyFormatting(resultText.getString()) + textRenderer.trimToWidth(Formatting.strip(resultText.getString()), MAX_TEXT_WIDTH) + ELLIPSIS).setStyle(resultText.getStyle());
+ }
+ context.drawTextWithShadow(textRenderer, resultText, this.parentX + 11, this.parentY + 43, 0xffffffff);
+
+ //Arrow pointing to result item from the recipe
+ context.drawTextWithShadow(textRenderer, "▶", this.parentX + 96, this.parentY + 90, 0xaaffffff);
+ }
+ for (ResultButtonWidget button : resultButtons)
+ button.render(context, mouseX, mouseY, delta);
+ if (this.pageCount > 1) {
+ String string = (this.currentPage + 1) + "/" + this.pageCount;
+ int dx = this.client.textRenderer.getWidth(string) / 2;
+ context.drawText(textRenderer, string, this.parentX - dx + 73, this.parentY + 141, -1, false);
+ }
+ if (this.prevPageButton.active) this.prevPageButton.render(context, mouseX, mouseY, delta);
+ if (this.nextPageButton.active) this.nextPageButton.render(context, mouseX, mouseY, delta);
+ RenderSystem.enableDepthTest();
+ }
+
+ /**
+ * Used for drawing tooltips over truncated text
+ */
+ private void drawTooltip(TextRenderer textRenderer, DrawContext context, Text text, int textX, int textY, int mouseX, int mouseY){
+ RenderSystem.disableDepthTest();
+ if (mouseX >= textX && mouseX <= textX + MAX_TEXT_WIDTH + 4 && mouseY >= textY && mouseY <= textY + 9) {
+ context.drawTooltip(textRenderer, text, mouseX, mouseY);
+ }
+ RenderSystem.enableDepthTest();
+ }
+
+ /**
+ * @see #drawTooltip(TextRenderer, DrawContext, Text, int, int, int, int)
+ */
+ private void drawTooltip(TextRenderer textRenderer, DrawContext context, String text, int textX, int textY, int mouseX, int mouseY){
+ drawTooltip(textRenderer, context, Text.of(text), textX, textY, mouseX, mouseY);
+ }
+
+ /**
+ * Retrieves the first occurrence of section symbol formatting in a string
+ *
+ * @param string The string to fetch section symbol formatting from
+ * @return The section symbol and its formatting code or {@code null} if a match isn't found or if the {@code string} is null
+ */
+ private static String getLegacyFormatting(@Nullable String string) {
+ if (string == null) {
+ return null;
+ }
+ Matcher matcher = FORMATTING_CODE_PATTERN.matcher(string);
+ if (matcher.find()) {
+ return matcher.group(0);
+ }
+ return null;
+ }
+
+ public void drawTooltip(DrawContext context, int mouseX, int mouseY) {
+ RenderSystem.disableDepthTest();
+ for (ResultButtonWidget button : resultButtons)
+ if (button.isMouseOver(mouseX, mouseY))
+ button.renderTooltip(context, mouseX, mouseY);
+ RenderSystem.enableDepthTest();
+ }
+
+ public boolean mouseClicked(double mouseX, double mouseY, int mouseButton) {
+ for (ResultButtonWidget button : resultButtons)
+ if (button.mouseClicked(mouseX, mouseY, mouseButton)) {
+ if (button.itemStack.getNbt() == null) {
+ continue;
+ }
+ String internalName = button.itemStack.getNbt().getCompound("ExtraAttributes").getString("id");
+ List<SkyblockCraftingRecipe> recipes = ItemRegistry.getRecipes(internalName);
+ if (!recipes.isEmpty()) {
+ this.recipeResults = recipes;
+ this.currentPage = 0;
+ this.pageCount = recipes.size();
+ this.displayRecipes = true;
+ this.updateButtons();
+ }
+ return true;
+ }
+ if (this.prevPageButton.mouseClicked(mouseX, mouseY, mouseButton)) {
+ --this.currentPage;
+ this.updateButtons();
+ return true;
+ }
+ if (this.nextPageButton.mouseClicked(mouseX, mouseY, mouseButton)) {
+ ++this.currentPage;
+ this.updateButtons();
+ return true;
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SkyblockCraftingRecipe.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SkyblockCraftingRecipe.java
new file mode 100644
index 00000000..b738dfef
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SkyblockCraftingRecipe.java
@@ -0,0 +1,60 @@
+package de.hysky.skyblocker.skyblock.itemlist;
+
+import com.google.gson.JsonObject;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SkyblockCraftingRecipe {
+ private static final Logger LOGGER = LoggerFactory.getLogger(SkyblockCraftingRecipe.class);
+ String craftText = "";
+ final List<ItemStack> grid = new ArrayList<>(9);
+ ItemStack result;
+
+ public static SkyblockCraftingRecipe fromJsonObject(JsonObject jsonObj) {
+ SkyblockCraftingRecipe recipe = new SkyblockCraftingRecipe();
+ if (jsonObj.has("crafttext")) recipe.craftText = jsonObj.get("crafttext").getAsString();
+ recipe.grid.add(getItemStack(jsonObj.getAsJsonObject("recipe").get("A1").getAsString()));
+ recipe.grid.add(getItemStack(jsonObj.getAsJsonObject("recipe").get("A2").getAsString()));
+ recipe.grid.add(getItemStack(jsonObj.getAsJsonObject("recipe").get("A3").getAsString()));
+ recipe.grid.add(getItemStack(jsonObj.getAsJsonObject("recipe").get("B1").getAsString()));
+ recipe.grid.add(getItemStack(jsonObj.getAsJsonObject("recipe").get("B2").getAsString()));
+ recipe.grid.add(getItemStack(jsonObj.getAsJsonObject("recipe").get("B3").getAsString()));
+ recipe.grid.add(getItemStack(jsonObj.getAsJsonObject("recipe").get("C1").getAsString()));
+ recipe.grid.add(getItemStack(jsonObj.getAsJsonObject("recipe").get("C2").getAsString()));
+ recipe.grid.add(getItemStack(jsonObj.getAsJsonObject("recipe").get("C3").getAsString()));
+ recipe.result = ItemRegistry.itemsMap.get(jsonObj.get("internalname").getAsString());
+ return recipe;
+ }
+
+ private static ItemStack getItemStack(String internalName) {
+ try {
+ if (internalName.length() > 0) {
+ int count = internalName.split(":").length == 1 ? 1 : Integer.parseInt(internalName.split(":")[1]);
+ internalName = internalName.split(":")[0];
+ ItemStack itemStack = ItemRegistry.itemsMap.get(internalName).copy();
+ itemStack.setCount(count);
+ return itemStack;
+ }
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker-Recipe] " + internalName, e);
+ }
+ return Items.AIR.getDefaultStack();
+ }
+
+ public List<ItemStack> getGrid() {
+ return grid;
+ }
+
+ public ItemStack getResult() {
+ return result;
+ }
+
+ public String getCraftText() {
+ return craftText;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNav.java b/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNav.java
new file mode 100644
index 00000000..51a3d409
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNav.java
@@ -0,0 +1,80 @@
+package de.hysky.skyblocker.skyblock.quicknav;
+
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+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.HandledScreen;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.StringNbtReader;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.PatternSyntaxException;
+
+public class QuickNav {
+ private static final String skyblockHubIconNbt = "{id:\"minecraft:player_head\",Count:1,tag:{SkullOwner:{Id:[I;-300151517,-631415889,-1193921967,-1821784279],Properties:{textures:[{Value:\"e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDdjYzY2ODc0MjNkMDU3MGQ1NTZhYzUzZTA2NzZjYjU2M2JiZGQ5NzE3Y2Q4MjY5YmRlYmVkNmY2ZDRlN2JmOCJ9fX0=\"}]}}}}";
+ private static final String dungeonHubIconNbt = "{id:\"minecraft:player_head\",Count:1,tag:{SkullOwner:{Id:[I;1605800870,415127827,-1236127084,15358548],Properties:{textures:[{Value:\"e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzg5MWQ1YjI3M2ZmMGJjNTBjOTYwYjJjZDg2ZWVmMWM0MGExYjk0MDMyYWU3MWU3NTQ3NWE1NjhhODI1NzQyMSJ9fX0=\"}]}}}}";
+
+ public static void init() {
+ ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
+ if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().quickNav.enableQuickNav && screen instanceof HandledScreen<?> && client.player != null && !client.player.isCreative()) {
+ String screenTitle = screen.getTitle().getString().trim();
+ List<QuickNavButton> buttons = QuickNav.init(screenTitle);
+ for (QuickNavButton button : buttons) Screens.getButtons(screen).add(button);
+ }
+ });
+ }
+
+ public static List<QuickNavButton> init(String screenTitle) {
+ List<QuickNavButton> buttons = new ArrayList<>();
+ SkyblockerConfig.QuickNav data = SkyblockerConfigManager.get().quickNav;
+ try {
+ if (data.button1.render) buttons.add(parseButton(data.button1, screenTitle, 0));
+ if (data.button2.render) buttons.add(parseButton(data.button2, screenTitle, 1));
+ if (data.button3.render) buttons.add(parseButton(data.button3, screenTitle, 2));
+ if (data.button4.render) buttons.add(parseButton(data.button4, screenTitle, 3));
+ if (data.button5.render) buttons.add(parseButton(data.button5, screenTitle, 4));
+ if (data.button6.render) buttons.add(parseButton(data.button6, screenTitle, 5));
+ if (data.button7.render) buttons.add(parseButton(data.button7, screenTitle, 6));
+ if (data.button8.render) buttons.add(parseButton(data.button8, screenTitle, 7));
+ if (data.button9.render) buttons.add(parseButton(data.button9, screenTitle, 8));
+ if (data.button10.render) buttons.add(parseButton(data.button10, screenTitle, 9));
+ if (data.button11.render) buttons.add(parseButton(data.button11, screenTitle, 10));
+ if (data.button12.render) buttons.add(parseButton(data.button12, screenTitle, 11));
+ } catch (CommandSyntaxException e) {
+ e.printStackTrace();
+ }
+ return buttons;
+ }
+
+ private static QuickNavButton parseButton(SkyblockerConfig.QuickNavItem buttonInfo, String screenTitle, int id) throws CommandSyntaxException {
+ SkyblockerConfig.ItemData itemData = buttonInfo.item;
+ String nbtString = "{id:\"minecraft:" + itemData.itemName.toLowerCase(Locale.ROOT) + "\",Count:1";
+ if (itemData.nbt.length() > 2) nbtString += "," + itemData.nbt;
+ nbtString += "}";
+ boolean uiTitleMatches = false;
+ try {
+ uiTitleMatches = screenTitle.matches(buttonInfo.uiTitle);
+ } catch (PatternSyntaxException e) {
+ e.printStackTrace();
+ ClientPlayerEntity player = MinecraftClient.getInstance().player;
+ if (player != null) {
+ player.sendMessage(Text.of(Formatting.RED + "[Skyblocker] Invalid regex in quicknav button " + (id + 1) + "!"), false);
+ }
+ }
+ return new QuickNavButton(id,
+ uiTitleMatches,
+ buttonInfo.clickEvent,
+ ItemStack.fromNbt(StringNbtReader.parse(nbtString))
+ );
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNavButton.java b/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNavButton.java
new file mode 100644
index 00000000..5e76427a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/quicknav/QuickNavButton.java
@@ -0,0 +1,107 @@
+package de.hysky.skyblocker.skyblock.quicknav;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+
+import de.hysky.skyblocker.mixin.accessor.HandledScreenAccessor;
+import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
+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.Screen;
+import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.widget.ClickableWidget;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.item.ItemStack;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+
+@Environment(value=EnvType.CLIENT)
+public class QuickNavButton extends ClickableWidget {
+ private static final Identifier BUTTON_TEXTURE = new Identifier("textures/gui/container/creative_inventory/tabs.png");
+
+ private final int index;
+ private boolean toggled;
+ private int u;
+ private int v;
+ private final String command;
+ private final ItemStack icon;
+
+ public QuickNavButton(int index, boolean toggled, String command, ItemStack icon) {
+ super(0, 0, 26, 32, Text.empty());
+ this.index = index;
+ this.toggled = toggled;
+ this.command = command;
+ this.icon = icon;
+ }
+
+ private void updateCoordinates() {
+ Screen screen = MinecraftClient.getInstance().currentScreen;
+ if (screen instanceof HandledScreen<?> handledScreen) {
+ int x = ((HandledScreenAccessor)handledScreen).getX();
+ int y = ((HandledScreenAccessor)handledScreen).getY();
+ int h = ((HandledScreenAccessor)handledScreen).getBackgroundHeight();
+ if (h > 166) --h; // why is this even a thing
+ this.setX(x + this.index % 6 * 26 + 4);
+ this.setY(this.index < 6 ? y - 26 : y + h - 4);
+ this.u = 26;
+ this.v = (index < 6 ? 0 : 64) + (toggled ? 32 : 0);
+ }
+ }
+
+ @Override
+ public void onClick(double mouseX, double mouseY) {
+ if (!this.toggled) {
+ this.toggled = true;
+ MessageScheduler.INSTANCE.sendMessageAfterCooldown(command);
+ // TODO : add null check with log error
+ }
+ }
+
+ @Override
+ public void renderButton(DrawContext context, int mouseX, int mouseY, float delta) {
+ this.updateCoordinates();
+ MatrixStack matrices = context.getMatrices();
+ RenderSystem.disableDepthTest();
+ // render button background
+ if (!this.toggled) {
+ if (this.index >= 6)
+ // this.drawTexture(matrices, this.x, this.y + 4, this.u, this.v + 4, this.width, this.height - 4);
+ context.drawTexture(BUTTON_TEXTURE, this.getX(), this.getY() + 4, this.u, this.v + 4, this.width, this.height - 4);
+ else
+ // this.drawTexture(matrices, this.x, this.y, this.u, this.v, this.width, this.height - 4);
+ context.drawTexture(BUTTON_TEXTURE, this.getX(), this.getY() - 2, this.u, this.v, this.width, this.height - 4);
+ // } else this.drawTexture(matrices, this.x, this.y, this.u, this.v, this.width, this.height);
+ } else {
+ matrices.push();
+ //Move the top buttons 2 pixels up if they're selected
+ if (this.index < 6) matrices.translate(0f, -2f, 0f);
+ context.drawTexture(BUTTON_TEXTURE, this.getX(), this.getY(), this.u, this.v, this.width, this.height);
+ matrices.pop();
+ }
+ // render button icon
+ if (!this.toggled) {
+ if (this.index >= 6)
+ // CLIENT.getItemRenderer().renderInGui(this.icon,this.x + 6, this.y + 6);
+ context.drawItem(this.icon,this.getX() + 5, this.getY() + 6);
+ else
+ // CLIENT.getItemRenderer().renderInGui(this.icon,this.x + 6, this.y + 9);
+ context.drawItem(this.icon,this.getX() + 5, this.getY() + 7);
+ } else {
+ if (this.index >= 6)
+ // CLIENT.getItemRenderer().renderInGui(this.icon,this.x + 6, this.y + 9);
+ context.drawItem(this.icon,this.getX() + 5, this.getY() + 9);
+ else
+ // CLIENT.getItemRenderer().renderInGui(this.icon,this.x + 6, this.y + 6);
+ context.drawItem(this.icon,this.getX() + 5, this.getY() + 6);
+ }
+ RenderSystem.enableDepthTest();
+ }
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {
+ // TODO Auto-generated method stub
+
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/rift/EffigyWaypoints.java b/src/main/java/de/hysky/skyblocker/skyblock/rift/EffigyWaypoints.java
new file mode 100644
index 00000000..4cc20ca5
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/rift/EffigyWaypoints.java
@@ -0,0 +1,71 @@
+package de.hysky.skyblocker.skyblock.rift;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.text.Text;
+import net.minecraft.text.TextColor;
+import net.minecraft.util.DyeColor;
+import net.minecraft.util.math.BlockPos;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class EffigyWaypoints {
+ private static final Logger LOGGER = LoggerFactory.getLogger(EffigyWaypoints.class);
+ private static final List<BlockPos> EFFIGIES = List.of(
+ new BlockPos(150, 79, 95), //Effigy 1
+ new BlockPos(193, 93, 119), //Effigy 2
+ new BlockPos(235, 110, 147), //Effigy 3
+ new BlockPos(293, 96, 134), //Effigy 4
+ new BlockPos(262, 99, 94), //Effigy 5
+ new BlockPos(240, 129, 118) //Effigy 6
+ );
+ private static final List<BlockPos> UNBROKEN_EFFIGIES = new ArrayList<>();
+
+ protected static void updateEffigies() {
+ if (!SkyblockerConfigManager.get().slayer.vampireSlayer.enableEffigyWaypoints || !Utils.isOnSkyblock() || !Utils.isInTheRift() || !Utils.getLocation().contains("Stillgore Château")) return;
+
+ UNBROKEN_EFFIGIES.clear();
+
+ try {
+ for (int i = 0; i < Utils.STRING_SCOREBOARD.size(); i++) {
+ String line = Utils.STRING_SCOREBOARD.get(i);
+
+ if (line.contains("Effigies")) {
+ List<Text> effigiesText = new ArrayList<>();
+ List<Text> prefixAndSuffix = Utils.TEXT_SCOREBOARD.get(i).getSiblings();
+
+ //Add contents of prefix and suffix to list
+ effigiesText.addAll(prefixAndSuffix.get(0).getSiblings());
+ effigiesText.addAll(prefixAndSuffix.get(1).getSiblings());
+
+ for (int i2 = 1; i2 < effigiesText.size(); i2++) {
+ if (effigiesText.get(i2).getStyle().getColor() == TextColor.parse("gray")) UNBROKEN_EFFIGIES.add(EFFIGIES.get(i2 - 1));
+ }
+ }
+ }
+ } catch (NullPointerException e) {
+ LOGGER.error("[Skyblocker] Error while updating effigies.", e);
+ }
+ }
+
+ protected static void render(WorldRenderContext context) {
+ if (SkyblockerConfigManager.get().slayer.vampireSlayer.enableEffigyWaypoints && Utils.getLocation().contains("Stillgore Château")) {
+ for (BlockPos effigy : UNBROKEN_EFFIGIES) {
+ float[] colorComponents = DyeColor.RED.getColorComponents();
+ if (SkyblockerConfigManager.get().slayer.vampireSlayer.compactEffigyWaypoints) {
+ RenderHelper.renderFilledThroughWallsWithBeaconBeam(context, effigy.down(6), colorComponents, 0.5F);
+ } else {
+ RenderHelper.renderFilledThroughWallsWithBeaconBeam(context, effigy, colorComponents, 0.5F);
+ for (int i = 1; i < 6; i++) {
+ RenderHelper.renderFilledThroughWalls(context, effigy.down(i), colorComponents, 0.5F - (0.075F * i));
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/rift/HealingMelonIndicator.java b/src/main/java/de/hysky/skyblocker/skyblock/rift/HealingMelonIndicator.java
new file mode 100644
index 00000000..333a4aa1
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/rift/HealingMelonIndicator.java
@@ -0,0 +1,27 @@
+package de.hysky.skyblocker.skyblock.rift;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import de.hysky.skyblocker.utils.render.title.Title;
+import de.hysky.skyblocker.utils.render.title.TitleContainer;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.util.Formatting;
+
+public class HealingMelonIndicator {
+ private static final Title title = new Title("skyblocker.rift.healNow", Formatting.DARK_RED);
+
+ public static void updateHealth() {
+ if (!SkyblockerConfigManager.get().slayer.vampireSlayer.enableHealingMelonIndicator || !Utils.isOnSkyblock() || !Utils.isInTheRift() || !Utils.getLocation().contains("Stillgore Château")) {
+ TitleContainer.removeTitle(title);
+ return;
+ }
+ ClientPlayerEntity player = MinecraftClient.getInstance().player;
+ if (player != null && player.getHealth() <= SkyblockerConfigManager.get().slayer.vampireSlayer.healingMelonHealthThreshold * 2F) {
+ RenderHelper.displayInTitleContainerAndPlaySound(title);
+ } else {
+ TitleContainer.removeTitle(title);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/rift/ManiaIndicator.java b/src/main/java/de/hysky/skyblocker/skyblock/rift/ManiaIndicator.java
new file mode 100644
index 00000000..ab252ff0
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/rift/ManiaIndicator.java
@@ -0,0 +1,42 @@
+package de.hysky.skyblocker.skyblock.rift;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.SlayerUtils;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import de.hysky.skyblocker.utils.render.title.Title;
+import de.hysky.skyblocker.utils.render.title.TitleContainer;
+import net.minecraft.block.Blocks;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.entity.Entity;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.math.BlockPos;
+
+public class ManiaIndicator {
+ private static final Title title = new Title("skyblocker.rift.mania", Formatting.RED);
+
+ protected static void updateMania() {
+ if (!SkyblockerConfigManager.get().slayer.vampireSlayer.enableManiaIndicator || !Utils.isOnSkyblock() || !Utils.isInTheRift() || !(Utils.getLocation().contains("Stillgore Château")) || !SlayerUtils.isInSlayer()) {
+ TitleContainer.removeTitle(title);
+ return;
+ }
+
+ Entity slayerEntity = SlayerUtils.getSlayerEntity();
+ if (slayerEntity == null) return;
+
+ boolean anyMania = false;
+ for (Entity entity : SlayerUtils.getEntityArmorStands(slayerEntity)) {
+ if (entity.getDisplayName().toString().contains("MANIA")) {
+ anyMania = true;
+ BlockPos pos = MinecraftClient.getInstance().player.getBlockPos().down();
+ boolean isGreen = MinecraftClient.getInstance().world.getBlockState(pos).getBlock() == Blocks.GREEN_TERRACOTTA;
+ title.setText(Text.translatable("skyblocker.rift.mania").formatted(isGreen ? Formatting.GREEN : Formatting.RED));
+ RenderHelper.displayInTitleContainerAndPlaySound(title);
+ }
+ }
+ if (!anyMania) {
+ TitleContainer.removeTitle(title);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/rift/MirrorverseWaypoints.java b/src/main/java/de/hysky/skyblocker/skyblock/rift/MirrorverseWaypoints.java
new file mode 100644
index 00000000..06181349
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/rift/MirrorverseWaypoints.java
@@ -0,0 +1,88 @@
+package de.hysky.skyblocker.skyblock.rift;
+
+import com.google.gson.JsonArray;
+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.Utils;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.util.DyeColor;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.BlockPos;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+
+public class MirrorverseWaypoints {
+ private static final Logger LOGGER = LoggerFactory.getLogger("skyblocker");
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+ private static final Identifier WAYPOINTS_JSON = new Identifier(SkyblockerMod.NAMESPACE, "mirrorverse_waypoints.json");
+ private static final BlockPos[] LAVA_PATH_WAYPOINTS = new BlockPos[107];
+ private static final BlockPos[] UPSIDE_DOWN_WAYPOINTS = new BlockPos[66];
+ private static final BlockPos[] TURBULATOR_WAYPOINTS = new BlockPos[27];
+ private static final float[] COLOR_COMPONENTS = DyeColor.RED.getColorComponents();
+
+ static {
+ loadWaypoints();
+ }
+
+ /**
+ * Loads the waypoint locations into memory
+ */
+ private static void loadWaypoints() {
+ try (BufferedReader reader = CLIENT.getResourceManager().openAsReader(WAYPOINTS_JSON)) {
+ JsonObject file = JsonParser.parseReader(reader).getAsJsonObject();
+ JsonArray sections = file.get("sections").getAsJsonArray();
+
+ /// Lava Path
+ JsonArray lavaPathWaypoints = sections.get(0).getAsJsonObject().get("waypoints").getAsJsonArray();
+
+ for (int i = 0; i < lavaPathWaypoints.size(); i++) {
+ JsonObject point = lavaPathWaypoints.get(i).getAsJsonObject();
+ LAVA_PATH_WAYPOINTS[i] = new BlockPos(point.get("x").getAsInt(), point.get("y").getAsInt(), point.get("z").getAsInt());
+ }
+
+ /// Upside Down Parkour
+ JsonArray upsideDownParkourWaypoints = sections.get(1).getAsJsonObject().get("waypoints").getAsJsonArray();
+
+ for (int i = 0; i < upsideDownParkourWaypoints.size(); i++) {
+ JsonObject point = upsideDownParkourWaypoints.get(i).getAsJsonObject();
+ UPSIDE_DOWN_WAYPOINTS[i] = new BlockPos(point.get("x").getAsInt(), point.get("y").getAsInt(), point.get("z").getAsInt());
+ }
+
+ /// Turbulator Parkour
+ JsonArray turbulatorParkourWaypoints = sections.get(2).getAsJsonObject().get("waypoints").getAsJsonArray();
+
+ for (int i = 0; i < turbulatorParkourWaypoints.size(); i++) {
+ JsonObject point = turbulatorParkourWaypoints.get(i).getAsJsonObject();
+ TURBULATOR_WAYPOINTS[i] = new BlockPos(point.get("x").getAsInt(), point.get("y").getAsInt(), point.get("z").getAsInt());
+ }
+
+ } catch (IOException e) {
+ LOGGER.info("[Skyblocker] Mirrorverse Waypoints failed to load ;(");
+ e.printStackTrace();
+ }
+ }
+
+ protected static void render(WorldRenderContext wrc) {
+ //I would also check for the mirrorverse location but the scoreboard stuff is not performant at all...
+ if (Utils.isInTheRift() && SkyblockerConfigManager.get().locations.rift.mirrorverseWaypoints) {
+ for (BlockPos pos : LAVA_PATH_WAYPOINTS) {
+ RenderHelper.renderFilledIfVisible(wrc, pos, COLOR_COMPONENTS, 0.5f);
+ }
+
+ for (BlockPos pos : UPSIDE_DOWN_WAYPOINTS) {
+ RenderHelper.renderFilledIfVisible(wrc, pos, COLOR_COMPONENTS, 0.5f);
+ }
+
+ for (BlockPos pos : TURBULATOR_WAYPOINTS) {
+ RenderHelper.renderFilledIfVisible(wrc, pos, COLOR_COMPONENTS, 0.5f);
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/rift/StakeIndicator.java b/src/main/java/de/hysky/skyblocker/skyblock/rift/StakeIndicator.java
new file mode 100644
index 00000000..be502143
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/rift/StakeIndicator.java
@@ -0,0 +1,27 @@
+package de.hysky.skyblocker.skyblock.rift;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.SlayerUtils;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import de.hysky.skyblocker.utils.render.title.Title;
+import de.hysky.skyblocker.utils.render.title.TitleContainer;
+import net.minecraft.entity.Entity;
+import net.minecraft.util.Formatting;
+
+public class StakeIndicator {
+ private static final Title title = new Title("skyblocker.rift.stakeNow", Formatting.RED);
+
+ protected static void updateStake() {
+ if (!SkyblockerConfigManager.get().slayer.vampireSlayer.enableSteakStakeIndicator || !Utils.isOnSkyblock() || !Utils.isInTheRift() || !Utils.getLocation().contains("Stillgore Château") || !SlayerUtils.isInSlayer()) {
+ TitleContainer.removeTitle(title);
+ return;
+ }
+ Entity slayerEntity = SlayerUtils.getSlayerEntity();
+ if (slayerEntity != null && slayerEntity.getDisplayName().toString().contains("҉")) {
+ RenderHelper.displayInTitleContainerAndPlaySound(title);
+ } else {
+ TitleContainer.removeTitle(title);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/rift/TheRift.java b/src/main/java/de/hysky/skyblocker/skyblock/rift/TheRift.java
new file mode 100644
index 00000000..10b593bd
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/rift/TheRift.java
@@ -0,0 +1,22 @@
+package de.hysky.skyblocker.skyblock.rift;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+
+public class TheRift {
+ /**
+ * @see Utils#isInTheRift() Utils#isInTheRift().
+ */
+ public static final String LOCATION = "rift";
+
+ public static void init() {
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(MirrorverseWaypoints::render);
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(EffigyWaypoints::render);
+ Scheduler.INSTANCE.scheduleCyclic(EffigyWaypoints::updateEffigies, SkyblockerConfigManager.get().slayer.vampireSlayer.effigyUpdateFrequency);
+ Scheduler.INSTANCE.scheduleCyclic(TwinClawsIndicator::updateIce, SkyblockerConfigManager.get().slayer.vampireSlayer.holyIceUpdateFrequency);
+ Scheduler.INSTANCE.scheduleCyclic(ManiaIndicator::updateMania, SkyblockerConfigManager.get().slayer.vampireSlayer.maniaUpdateFrequency);
+ Scheduler.INSTANCE.scheduleCyclic(StakeIndicator::updateStake, SkyblockerConfigManager.get().slayer.vampireSlayer.steakStakeUpdateFrequency);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/rift/TwinClawsIndicator.java b/src/main/java/de/hysky/skyblocker/skyblock/rift/TwinClawsIndicator.java
new file mode 100644
index 00000000..1622bf4a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/rift/TwinClawsIndicator.java
@@ -0,0 +1,43 @@
+package de.hysky.skyblocker.skyblock.rift;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.SlayerUtils;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import de.hysky.skyblocker.utils.render.title.Title;
+import de.hysky.skyblocker.utils.render.title.TitleContainer;
+import net.minecraft.entity.Entity;
+import net.minecraft.util.Formatting;
+
+public class TwinClawsIndicator {
+ private static final Title title = new Title("skyblocker.rift.iceNow", Formatting.AQUA);
+ private static boolean scheduled = false;
+
+ protected static void updateIce() {
+ if (!SkyblockerConfigManager.get().slayer.vampireSlayer.enableHolyIceIndicator || !Utils.isOnSkyblock() || !Utils.isInTheRift() || !(Utils.getLocation().contains("Stillgore Château")) || !SlayerUtils.isInSlayer()) {
+ TitleContainer.removeTitle(title);
+ return;
+ }
+
+ Entity slayerEntity = SlayerUtils.getSlayerEntity();
+ if (slayerEntity == null) return;
+
+ boolean anyClaws = false;
+ for (Entity entity : SlayerUtils.getEntityArmorStands(slayerEntity)) {
+ if (entity.getDisplayName().toString().contains("TWINCLAWS")) {
+ anyClaws = true;
+ if (!TitleContainer.containsTitle(title) && !scheduled) {
+ scheduled = true;
+ Scheduler.INSTANCE.schedule(() -> {
+ RenderHelper.displayInTitleContainerAndPlaySound(title);
+ scheduled = false;
+ }, SkyblockerConfigManager.get().slayer.vampireSlayer.holyIceIndicatorTickDelay);
+ }
+ }
+ }
+ if (!anyClaws) {
+ TitleContainer.removeTitle(title);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java
new file mode 100644
index 00000000..9c058a4f
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java
@@ -0,0 +1,208 @@
+package de.hysky.skyblocker.skyblock.shortcut;
+
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+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.fabricmc.fabric.api.client.message.v1.ClientSendMessageEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.command.CommandRegistryAccess;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument;
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
+
+public class Shortcuts {
+ private static final Logger LOGGER = LoggerFactory.getLogger(Shortcuts.class);
+ private static final File SHORTCUTS_FILE = SkyblockerMod.CONFIG_DIR.resolve("shortcuts.json").toFile();
+ @Nullable
+ private static CompletableFuture<Void> shortcutsLoaded;
+ public static final Map<String, String> commands = new HashMap<>();
+ public static final Map<String, String> commandArgs = new HashMap<>();
+
+ public static boolean isShortcutsLoaded() {
+ return shortcutsLoaded != null && shortcutsLoaded.isDone();
+ }
+
+ public static void init() {
+ loadShortcuts();
+ ClientLifecycleEvents.CLIENT_STOPPING.register(Shortcuts::saveShortcuts);
+ ClientCommandRegistrationCallback.EVENT.register(Shortcuts::registerCommands);
+ ClientSendMessageEvents.MODIFY_COMMAND.register(Shortcuts::modifyCommand);
+ }
+
+ protected static void loadShortcuts() {
+ if (shortcutsLoaded != null && !isShortcutsLoaded()) {
+ return;
+ }
+ shortcutsLoaded = CompletableFuture.runAsync(() -> {
+ try (BufferedReader reader = new BufferedReader(new FileReader(SHORTCUTS_FILE))) {
+ Type shortcutsType = new TypeToken<Map<String, Map<String, String>>>() {
+ }.getType();
+ Map<String, Map<String, String>> shortcuts = SkyblockerMod.GSON.fromJson(reader, shortcutsType);
+ commands.clear();
+ commandArgs.clear();
+ commands.putAll(shortcuts.get("commands"));
+ commandArgs.putAll(shortcuts.get("commandArgs"));
+ LOGGER.info("[Skyblocker] Loaded {} command shortcuts and {} command argument shortcuts", commands.size(), commandArgs.size());
+ } catch (FileNotFoundException e) {
+ registerDefaultShortcuts();
+ LOGGER.warn("[Skyblocker] Shortcuts file not found, using default shortcuts. This is normal when using for the first time.");
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to load shortcuts file", e);
+ }
+ });
+ }
+
+ private static void registerDefaultShortcuts() {
+ commands.clear();
+ commandArgs.clear();
+
+ // Skyblock
+ commands.put("/s", "/skyblock");
+ commands.put("/i", "/is");
+ commands.put("/h", "/hub");
+
+ // Dungeon
+ commands.put("/d", "/warp dungeon_hub");
+
+ // Chat channels
+ commands.put("/ca", "/chat all");
+ commands.put("/cp", "/chat party");
+ commands.put("/cg", "/chat guild");
+ commands.put("/co", "/chat officer");
+ commands.put("/cc", "/chat coop");
+
+ // Message
+ commandArgs.put("/m", "/msg");
+
+ // Party
+ commandArgs.put("/pa", "/p accept");
+ commands.put("/pv", "/p leave");
+ commands.put("/pd", "/p disband");
+ commands.put("/rp", "/reparty");
+
+ // Visit
+ commandArgs.put("/v", "/visit");
+ commands.put("/vp", "/visit portalhub");
+ }
+
+ @SuppressWarnings("unused")
+ private static void registerMoreDefaultShortcuts() {
+ // Combat
+ commands.put("/spider", "/warp spider");
+ commands.put("/crimson", "/warp nether");
+ commands.put("/end", "/warp end");
+
+ // Mining
+ commands.put("/gold", "/warp gold");
+ commands.put("/cavern", "/warp deep");
+ commands.put("/dwarven", "/warp mines");
+ commands.put("/fo", "/warp forge");
+ commands.put("/ch", "/warp crystals");
+
+ // Foraging & Farming
+ commands.put("/park", "/warp park");
+ commands.put("/barn", "/warp barn");
+ commands.put("/desert", "/warp desert");
+ commands.put("/ga", "/warp garden");
+
+ // Other warps
+ commands.put("/castle", "/warp castle");
+ commands.put("/museum", "/warp museum");
+ commands.put("/da", "/warp da");
+ commands.put("/crypt", "/warp crypt");
+ commands.put("/nest", "/warp nest");
+ commands.put("/magma", "/warp magma");
+ commands.put("/void", "/warp void");
+ commands.put("/drag", "/warp drag");
+ commands.put("/jungle", "/warp jungle");
+ commands.put("/howl", "/warp howl");
+ }
+
+ protected static void saveShortcuts(MinecraftClient client) {
+ JsonObject shortcutsJson = new JsonObject();
+ shortcutsJson.add("commands", SkyblockerMod.GSON.toJsonTree(commands));
+ shortcutsJson.add("commandArgs", SkyblockerMod.GSON.toJsonTree(commandArgs));
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(SHORTCUTS_FILE))) {
+ SkyblockerMod.GSON.toJson(shortcutsJson, writer);
+ LOGGER.info("[Skyblocker] Saved {} command shortcuts and {} command argument shortcuts", commands.size(), commandArgs.size());
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to save shortcuts file", e);
+ }
+ }
+
+ private static void registerCommands(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+ for (String key : commands.keySet()) {
+ if (key.startsWith("/")) {
+ dispatcher.register(literal(key.substring(1)));
+ }
+ }
+ for (String key : commandArgs.keySet()) {
+ if (key.startsWith("/")) {
+ dispatcher.register(literal(key.substring(1)).then(argument("args", StringArgumentType.greedyString())));
+ }
+ }
+ dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("help").executes(context -> {
+ FabricClientCommandSource source = context.getSource();
+ String status = SkyblockerConfigManager.get().general.shortcuts.enableShortcuts && SkyblockerConfigManager.get().general.shortcuts.enableCommandShortcuts ? "§a§l (Enabled)" : "§c§l (Disabled)";
+ source.sendFeedback(Text.of("§e§lSkyblocker §fCommand Shortcuts" + status));
+ if (!isShortcutsLoaded()) {
+ source.sendFeedback(Text.translatable("skyblocker.shortcuts.notLoaded"));
+ } else for (Map.Entry<String, String> command : commands.entrySet()) {
+ source.sendFeedback(Text.of("§7" + command.getKey() + " §f→ §7" + command.getValue()));
+ }
+ status = SkyblockerConfigManager.get().general.shortcuts.enableShortcuts && SkyblockerConfigManager.get().general.shortcuts.enableCommandArgShortcuts ? "§a§l (Enabled)" : "§c§l (Disabled)";
+ source.sendFeedback(Text.of("§e§lSkyblocker §fCommand Argument Shortcuts" + status));
+ if (!isShortcutsLoaded()) {
+ source.sendFeedback(Text.translatable("skyblocker.shortcuts.notLoaded"));
+ } else for (Map.Entry<String, String> commandArg : commandArgs.entrySet()) {
+ source.sendFeedback(Text.of("§7" + commandArg.getKey() + " §f→ §7" + commandArg.getValue()));
+ }
+ source.sendFeedback(Text.of("§e§lSkyblocker §fCommands"));
+ for (String command : dispatcher.getSmartUsage(dispatcher.getRoot().getChild(SkyblockerMod.NAMESPACE), source).values()) {
+ source.sendFeedback(Text.of("§7/" + SkyblockerMod.NAMESPACE + " " + command));
+ }
+ return Command.SINGLE_SUCCESS;
+ // Queue the screen or else the screen will be immediately closed after executing this command
+ })).then(literal("shortcuts").executes(Scheduler.queueOpenScreenCommand(ShortcutsConfigScreen::new))));
+ }
+
+ private static String modifyCommand(String command) {
+ if (SkyblockerConfigManager.get().general.shortcuts.enableShortcuts) {
+ if (!isShortcutsLoaded()) {
+ LOGGER.warn("[Skyblocker] Shortcuts not loaded yet, skipping shortcut for command: {}", command);
+ return command;
+ }
+ command = '/' + command;
+ if (SkyblockerConfigManager.get().general.shortcuts.enableCommandShortcuts) {
+ command = commands.getOrDefault(command, command);
+ }
+ if (SkyblockerConfigManager.get().general.shortcuts.enableCommandArgShortcuts) {
+ String[] messageArgs = command.split(" ");
+ for (int i = 0; i < messageArgs.length; i++) {
+ messageArgs[i] = commandArgs.getOrDefault(messageArgs[i], messageArgs[i]);
+ }
+ command = String.join(" ", messageArgs);
+ }
+ return command.substring(1);
+ }
+ return command;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigListWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigListWidget.java
new file mode 100644
index 00000000..5ebe4c1a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigListWidget.java
@@ -0,0 +1,232 @@
+package de.hysky.skyblocker.skyblock.shortcut;
+
+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.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.screen.narration.NarrationPart;
+import net.minecraft.client.gui.widget.ElementListWidget;
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.stream.Stream;
+
+public class ShortcutsConfigListWidget extends ElementListWidget<ShortcutsConfigListWidget.AbstractShortcutEntry> {
+ private final ShortcutsConfigScreen screen;
+ private final List<Map<String, String>> shortcutMaps = new ArrayList<>();
+
+ public ShortcutsConfigListWidget(MinecraftClient minecraftClient, ShortcutsConfigScreen screen, int width, int height, int top, int bottom, int itemHeight) {
+ super(minecraftClient, width, height, top, bottom, itemHeight);
+ this.screen = screen;
+ ShortcutCategoryEntry commandCategory = new ShortcutCategoryEntry(Shortcuts.commands, "skyblocker.shortcuts.command.target", "skyblocker.shortcuts.command.replacement");
+ if (Shortcuts.isShortcutsLoaded()) {
+ commandCategory.shortcutsMap.keySet().stream().sorted().forEach(commandTarget -> addEntry(new ShortcutEntry(commandCategory, commandTarget)));
+ } else {
+ addEntry(new ShortcutLoadingEntry());
+ }
+ ShortcutCategoryEntry commandArgCategory = new ShortcutCategoryEntry(Shortcuts.commandArgs, "skyblocker.shortcuts.commandArg.target", "skyblocker.shortcuts.commandArg.replacement", "skyblocker.shortcuts.commandArg.tooltip");
+ if (Shortcuts.isShortcutsLoaded()) {
+ commandArgCategory.shortcutsMap.keySet().stream().sorted().forEach(commandArgTarget -> addEntry(new ShortcutEntry(commandArgCategory, commandArgTarget)));
+ } else {
+ addEntry(new ShortcutLoadingEntry());
+ }
+ }
+
+ @Override
+ public int getRowWidth() {
+ return super.getRowWidth() + 100;
+ }
+
+ @Override
+ protected int getScrollbarPositionX() {
+ return super.getScrollbarPositionX() + 50;
+ }
+
+ protected Optional<ShortcutCategoryEntry> getCategory() {
+ if (getSelectedOrNull() instanceof ShortcutCategoryEntry category) {
+ return Optional.of(category);
+ } else if (getSelectedOrNull() instanceof ShortcutEntry shortcutEntry) {
+ return Optional.of(shortcutEntry.category);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public void setSelected(@Nullable ShortcutsConfigListWidget.AbstractShortcutEntry entry) {
+ super.setSelected(entry);
+ screen.updateButtons();
+ }
+
+ protected void addShortcutAfterSelected() {
+ getCategory().ifPresent(category -> children().add(children().indexOf(getSelectedOrNull()) + 1, new ShortcutEntry(category)));
+ }
+
+ @Override
+ protected boolean removeEntry(AbstractShortcutEntry entry) {
+ return super.removeEntry(entry);
+ }
+
+ protected boolean hasChanges() {
+ ShortcutEntry[] notEmptyShortcuts = getNotEmptyShortcuts().toArray(ShortcutEntry[]::new);
+ return notEmptyShortcuts.length != shortcutMaps.stream().mapToInt(Map::size).sum() || Arrays.stream(notEmptyShortcuts).anyMatch(ShortcutEntry::isChanged);
+ }
+
+ protected void saveShortcuts() {
+ shortcutMaps.forEach(Map::clear);
+ getNotEmptyShortcuts().forEach(ShortcutEntry::save);
+ Shortcuts.saveShortcuts(MinecraftClient.getInstance()); // Save shortcuts to disk
+ }
+
+ private Stream<ShortcutEntry> getNotEmptyShortcuts() {
+ return children().stream().filter(ShortcutEntry.class::isInstance).map(ShortcutEntry.class::cast).filter(ShortcutEntry::isNotEmpty);
+ }
+
+ protected static abstract class AbstractShortcutEntry extends ElementListWidget.Entry<AbstractShortcutEntry> {
+ }
+
+ private class ShortcutCategoryEntry extends AbstractShortcutEntry {
+ private final Map<String, String> shortcutsMap;
+ private final Text targetName;
+ private final Text replacementName;
+ @Nullable
+ private final Text tooltip;
+
+ private ShortcutCategoryEntry(Map<String, String> shortcutsMap, String targetName, String replacementName) {
+ this(shortcutsMap, targetName, replacementName, (Text) null);
+ }
+
+ private ShortcutCategoryEntry(Map<String, String> shortcutsMap, String targetName, String replacementName, String tooltip) {
+ this(shortcutsMap, targetName, replacementName, Text.translatable(tooltip));
+ }
+
+ private ShortcutCategoryEntry(Map<String, String> shortcutsMap, String targetName, String replacementName, @Nullable Text tooltip) {
+ this.shortcutsMap = shortcutsMap;
+ this.targetName = Text.translatable(targetName);
+ this.replacementName = Text.translatable(replacementName);
+ this.tooltip = tooltip;
+ shortcutMaps.add(shortcutsMap);
+ addEntry(this);
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return List.of();
+ }
+
+ @Override
+ public List<? extends Selectable> selectableChildren() {
+ return List.of(new Selectable() {
+ @Override
+ public SelectionType getType() {
+ return SelectionType.HOVERED;
+ }
+
+ @Override
+ public void appendNarrations(NarrationMessageBuilder builder) {
+ builder.put(NarrationPart.TITLE, targetName, replacementName);
+ }
+ });
+ }
+
+ @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.drawCenteredTextWithShadow(client.textRenderer, targetName, width / 2 - 85, y + 5, 0xFFFFFF);
+ context.drawCenteredTextWithShadow(client.textRenderer, replacementName, width / 2 + 85, y + 5, 0xFFFFFF);
+ if (tooltip != null && isMouseOver(mouseX, mouseY)) {
+ screen.setTooltip(tooltip);
+ }
+ }
+ }
+
+ private class ShortcutLoadingEntry extends AbstractShortcutEntry {
+ private final Text text;
+
+ private ShortcutLoadingEntry() {
+ this.text = Text.translatable("skyblocker.shortcuts.notLoaded");
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return List.of();
+ }
+
+ @Override
+ public List<? extends Selectable> selectableChildren() {
+ return List.of(new Selectable() {
+ @Override
+ public SelectionType getType() {
+ return SelectionType.HOVERED;
+ }
+
+ @Override
+ public void appendNarrations(NarrationMessageBuilder builder) {
+ builder.put(NarrationPart.TITLE, text);
+ }
+ });
+ }
+
+ @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.drawCenteredTextWithShadow(client.textRenderer, text, width / 2, y + 5, 0xFFFFFF);
+ }
+ }
+
+ protected class ShortcutEntry extends AbstractShortcutEntry {
+ private final List<TextFieldWidget> children;
+ private final ShortcutCategoryEntry category;
+ private final TextFieldWidget target;
+ private final TextFieldWidget replacement;
+
+ private ShortcutEntry(ShortcutCategoryEntry category) {
+ this(category, "");
+ }
+
+ private ShortcutEntry(ShortcutCategoryEntry category, String targetString) {
+ this.category = category;
+ target = new TextFieldWidget(MinecraftClient.getInstance().textRenderer, width / 2 - 160, 5, 150, 20, category.targetName);
+ replacement = new TextFieldWidget(MinecraftClient.getInstance().textRenderer, width / 2 + 10, 5, 150, 20, category.replacementName);
+ target.setText(targetString);
+ replacement.setText(category.shortcutsMap.getOrDefault(targetString, ""));
+ children = List.of(target, replacement);
+ }
+
+ @Override
+ public String toString() {
+ return target.getText() + " → " + replacement.getText();
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return children;
+ }
+
+ @Override
+ public List<? extends Selectable> selectableChildren() {
+ return children;
+ }
+
+ private boolean isNotEmpty() {
+ return !target.getText().isEmpty() && !replacement.getText().isEmpty();
+ }
+
+ private boolean isChanged() {
+ return !category.shortcutsMap.containsKey(target.getText()) || !category.shortcutsMap.get(target.getText()).equals(replacement.getText());
+ }
+
+ private void save() {
+ category.shortcutsMap.put(target.getText(), replacement.getText());
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) {
+ target.setY(y);
+ replacement.setY(y);
+ target.render(context, mouseX, mouseY, tickDelta);
+ replacement.render(context, mouseX, mouseY, tickDelta);
+ context.drawCenteredTextWithShadow(client.textRenderer, "→", width / 2, y + 5, 0xFFFFFF);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigScreen.java
new file mode 100644
index 00000000..196ad0d6
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigScreen.java
@@ -0,0 +1,113 @@
+package de.hysky.skyblocker.skyblock.shortcut;
+
+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.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.screen.ScreenTexts;
+import net.minecraft.text.Text;
+
+public class ShortcutsConfigScreen extends Screen {
+
+ private ShortcutsConfigListWidget shortcutsConfigListWidget;
+ private ButtonWidget buttonDelete;
+ private ButtonWidget buttonNew;
+ private ButtonWidget buttonDone;
+ private boolean initialized;
+ private double scrollAmount;
+ private final Screen parent;
+
+ public ShortcutsConfigScreen() {
+ this(null);
+ }
+
+ public ShortcutsConfigScreen(Screen parent) {
+ super(Text.translatable("skyblocker.shortcuts.config"));
+ this.parent = parent;
+ }
+
+ @Override
+ public void setTooltip(Text tooltip) {
+ super.setTooltip(tooltip);
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ if (initialized) {
+ shortcutsConfigListWidget.updateSize(width, height, 32, height - 64);
+ } else {
+ shortcutsConfigListWidget = new ShortcutsConfigListWidget(client, this, width, height, 32, height - 64, 25);
+ 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 -> {
+ if (client != null && shortcutsConfigListWidget.getSelectedOrNull() instanceof ShortcutsConfigListWidget.ShortcutEntry shortcutEntry) {
+ scrollAmount = shortcutsConfigListWidget.getScrollAmount();
+ client.setScreen(new ConfirmScreen(this::deleteEntry, Text.translatable("skyblocker.shortcuts.deleteQuestion"), Text.translatable("skyblocker.shortcuts.deleteWarning", shortcutEntry), Text.translatable("selectServer.deleteButton"), ScreenTexts.CANCEL));
+ }
+ }).build();
+ 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());
+ buttonDone = ButtonWidget.builder(ScreenTexts.DONE, button -> {
+ shortcutsConfigListWidget.saveShortcuts();
+ if (client != null) {
+ close();
+ }
+ }).tooltip(Tooltip.of(Text.translatable("skyblocker.shortcuts.commandSuggestionTooltip"))).build();
+ adder.add(buttonDone);
+ gridWidget.refreshPositions();
+ SimplePositioningWidget.setPos(gridWidget, 0, this.height - 64, this.width, 64);
+ gridWidget.forEachChild(this::addDrawableChild);
+ updateButtons();
+ }
+
+ private void deleteEntry(boolean confirmedAction) {
+ if (client != null) {
+ if (confirmedAction && shortcutsConfigListWidget.getSelectedOrNull() instanceof ShortcutsConfigListWidget.ShortcutEntry shortcutEntry) {
+ shortcutsConfigListWidget.removeEntry(shortcutEntry);
+ }
+ client.setScreen(this); // Re-inits the screen and keeps the old instance of ShortcutsConfigListWidget
+ shortcutsConfigListWidget.setScrollAmount(scrollAmount);
+ }
+ }
+
+ @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
+ public void close() {
+ if (client != null && shortcutsConfigListWidget.hasChanges()) {
+ client.setScreen(new ConfirmScreen(confirmedAction -> {
+ if (confirmedAction) {
+ this.client.setScreen(parent);
+ } else {
+ client.setScreen(this);
+ }
+ }, Text.translatable("text.skyblocker.quit_config"), Text.translatable("text.skyblocker.quit_config_sure"), Text.translatable("text.skyblocker.quit_discard"), ScreenTexts.CANCEL));
+ } else {
+ this.client.setScreen(parent);
+ }
+ }
+
+ protected void updateButtons() {
+ buttonDelete.active = Shortcuts.isShortcutsLoaded() && shortcutsConfigListWidget.getSelectedOrNull() instanceof ShortcutsConfigListWidget.ShortcutEntry;
+ buttonNew.active = Shortcuts.isShortcutsLoaded() && shortcutsConfigListWidget.getCategory().isPresent();
+ buttonDone.active = Shortcuts.isShortcutsLoaded();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/special/SpecialEffects.java b/src/main/java/de/hysky/skyblocker/skyblock/special/SpecialEffects.java
new file mode 100644
index 00000000..fba447ea
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/special/SpecialEffects.java
@@ -0,0 +1,96 @@
+package de.hysky.skyblocker.skyblock.special;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Utils;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.enchantment.Enchantments;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.nbt.StringNbtReader;
+import net.minecraft.particle.ParticleTypes;
+import net.minecraft.text.Text;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class SpecialEffects {
+ private static final Logger LOGGER = LoggerFactory.getLogger(SpecialEffects.class);
+ private static final Pattern DROP_PATTERN = Pattern.compile("(?:\\[[A-Z+]+] )?(?<player>[A-Za-z0-9_]+) unlocked (?<item>.+)!");
+ private static final ItemStack NECRON_HANDLE = new ItemStack(Items.STICK);
+ private static final ItemStack SCROLL = new ItemStack(Items.WRITABLE_BOOK);
+ private static ItemStack TIER_5_SKULL;
+ private static ItemStack FIFTH_STAR;
+
+ static {
+ NECRON_HANDLE.addEnchantment(Enchantments.PROTECTION, 1);
+ SCROLL.addEnchantment(Enchantments.PROTECTION, 1);
+ try {
+ TIER_5_SKULL = ItemStack.fromNbt(StringNbtReader.parse("{id:\"minecraft:player_head\",Count:1,tag:{SkullOwner:{Id:[I;-1613868903,-527154034,-1445577520,748807544],Properties:{textures:[{Value:\"eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTEwZjlmMTA4NWQ0MDcxNDFlYjc3NjE3YTRhYmRhYWEwOGQ4YWYzM2I5NjAyMDBmZThjMTI2YzFkMTQ0NTY4MiJ9fX0=\"}]}}}}"));
+ FIFTH_STAR = ItemStack.fromNbt(StringNbtReader.parse("{id:\"minecraft:player_head\",Count:1,tag:{SkullOwner:{Id:[I;1904417095,756174249,-1302927470,1407004198],Properties:{textures:[{Value:\"eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzFjODA0MjUyN2Y4MWM4ZTI5M2UyODEwMTEzNDg5ZjQzOTRjYzZlZmUxNWQxYWZhYzQzMTU3MWM3M2I2MmRjNCJ9fX0=\"}]}}}}"));
+ } catch (Exception e) {
+ TIER_5_SKULL = ItemStack.EMPTY;
+ FIFTH_STAR = ItemStack.EMPTY;
+ LOGGER.error("[Skyblocker Special Effects] Failed to parse NBT for a player head!", e);
+ }
+ }
+
+ public static void init() {
+ ClientReceiveMessageEvents.GAME.register(SpecialEffects::displayRareDropEffect);
+ }
+
+ private static void displayRareDropEffect(Text message, boolean overlay) {
+ //We don't check if we're in dungeons because that check doesn't work in m7 which defeats the point of this
+ //It might also allow it to work with Croesus
+ if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().general.specialEffects.rareDungeonDropEffects) {
+ try {
+ String stringForm = message.getString();
+ Matcher matcher = DROP_PATTERN.matcher(stringForm);
+
+ if (matcher.matches()) {
+ MinecraftClient client = MinecraftClient.getInstance();
+ String player = matcher.group("player");
+
+ if (player.equals(client.getSession().getUsername())) {
+ ItemStack stack = getStackFromName(matcher.group("item"));
+
+ if (!stack.isEmpty()) {
+ if (RenderSystem.isOnRenderThread()) {
+ client.particleManager.addEmitter(client.player, ParticleTypes.PORTAL, 30);
+ client.gameRenderer.showFloatingItem(stack);
+ } else {
+ RenderSystem.recordRenderCall(() -> {
+ client.particleManager.addEmitter(client.player, ParticleTypes.PORTAL, 30);
+ client.gameRenderer.showFloatingItem(stack);
+ });
+ }
+ }
+ }
+ }
+ } catch (Exception e) { //In case there's a regex failure or something else bad happens
+ LOGGER.error("[Skyblocker Special Effects] An unexpected exception was encountered: ", e);
+ }
+ }
+ }
+
+ private static ItemStack getStackFromName(String itemName) {
+ return switch (itemName) {
+ //M7
+ case "Necron Dye" -> new ItemStack(Items.ORANGE_DYE);
+ case "Dark Claymore" -> new ItemStack(Items.STONE_SWORD);
+ case "Necron's Handle", "Shiny Necron's Handle" -> NECRON_HANDLE;
+ case "Enchanted Book (Thunderlord VII)" -> new ItemStack(Items.ENCHANTED_BOOK);
+ case "Master Skull - Tier 5" -> TIER_5_SKULL;
+ case "Shadow Warp", "Wither Shield", "Implosion" -> SCROLL;
+ case "Fifth Master Star" -> FIFTH_STAR;
+
+ //M6
+ case "Giant's Sword" -> new ItemStack(Items.IRON_SWORD);
+
+ default -> ItemStack.EMPTY;
+ };
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/spidersden/Relics.java b/src/main/java/de/hysky/skyblocker/skyblock/spidersden/Relics.java
new file mode 100644
index 00000000..e5223874
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/spidersden/Relics.java
@@ -0,0 +1,171 @@
+package de.hysky.skyblocker.skyblock.spidersden;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+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.utils.PosUtils;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+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.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.command.CommandRegistryAccess;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.text.Text;
+import net.minecraft.util.DyeColor;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.BlockPos;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
+
+public class Relics {
+ private static final Logger LOGGER = LoggerFactory.getLogger(Relics.class);
+ private static CompletableFuture<Void> relicsLoaded;
+ @SuppressWarnings({"unused", "FieldCanBeLocal"})
+ private static int totalRelics = 0;
+ private static final List<BlockPos> relics = new ArrayList<>();
+ private static final Map<String, Set<BlockPos>> foundRelics = new HashMap<>();
+
+ public static void init() {
+ ClientLifecycleEvents.CLIENT_STARTED.register(Relics::loadRelics);
+ ClientLifecycleEvents.CLIENT_STOPPING.register(Relics::saveFoundRelics);
+ ClientCommandRegistrationCallback.EVENT.register(Relics::registerCommands);
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(Relics::render);
+ ClientReceiveMessageEvents.GAME.register(Relics::onChatMessage);
+ }
+
+ private static void loadRelics(MinecraftClient client) {
+ relicsLoaded = CompletableFuture.runAsync(() -> {
+ try (BufferedReader reader = client.getResourceManager().openAsReader(new Identifier(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();
+ } else if (json.getKey().equals("locations")) {
+ for (JsonElement locationJson : json.getValue().getAsJsonArray().asList()) {
+ JsonObject posData = locationJson.getAsJsonObject();
+ relics.add(new BlockPos(posData.get("x").getAsInt(), posData.get("y").getAsInt(), posData.get("z").getAsInt()));
+ }
+ }
+ }
+ LOGGER.info("[Skyblocker] Loaded relics locations");
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to load relics locations", e);
+ }
+
+ try (BufferedReader reader = new BufferedReader(new FileReader(SkyblockerMod.CONFIG_DIR.resolve("found_relics.json").toFile()))) {
+ for (Map.Entry<String, JsonElement> profileJson : JsonParser.parseReader(reader).getAsJsonObject().asMap().entrySet()) {
+ Set<BlockPos> foundRelicsForProfile = new HashSet<>();
+ for (JsonElement foundRelicsJson : profileJson.getValue().getAsJsonArray().asList()) {
+ foundRelicsForProfile.add(PosUtils.parsePosString(foundRelicsJson.getAsString()));
+ }
+ foundRelics.put(profileJson.getKey(), foundRelicsForProfile);
+ }
+ LOGGER.debug("[Skyblocker] Loaded found relics");
+ } catch (FileNotFoundException ignored) {
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to load found relics", e);
+ }
+ });
+ }
+
+ private static void saveFoundRelics(MinecraftClient client) {
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(SkyblockerMod.CONFIG_DIR.resolve("found_relics.json").toFile()))) {
+ JsonObject json = new JsonObject();
+ for (Map.Entry<String, Set<BlockPos>> foundRelicsForProfile : foundRelics.entrySet()) {
+ JsonArray foundRelicsJson = new JsonArray();
+ for (BlockPos foundRelic : foundRelicsForProfile.getValue()) {
+ foundRelicsJson.add(PosUtils.getPosString(foundRelic));
+ }
+ json.add(foundRelicsForProfile.getKey(), foundRelicsJson);
+ }
+ SkyblockerMod.GSON.toJson(json, writer);
+ LOGGER.debug("[Skyblocker] Saved found relics");
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to write found relics to file", e);
+ }
+ }
+
+ private static void registerCommands(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+ dispatcher.register(literal(SkyblockerMod.NAMESPACE)
+ .then(literal("relics")
+ .then(literal("markAllFound").executes(context -> {
+ Relics.markAllFound();
+ context.getSource().sendFeedback(Text.translatable("skyblocker.relics.markAllFound"));
+ return 1;
+ }))
+ .then(literal("markAllMissing").executes(context -> {
+ Relics.markAllMissing();
+ context.getSource().sendFeedback(Text.translatable("skyblocker.relics.markAllMissing"));
+ return 1;
+ }))));
+ }
+
+ private static void render(WorldRenderContext context) {
+ SkyblockerConfig.Relics config = SkyblockerConfigManager.get().locations.spidersDen.relics;
+
+ if (config.enableRelicsHelper && relicsLoaded.isDone() && Utils.getLocationRaw().equals("combat_1")) {
+ for (BlockPos fairySoulPos : relics) {
+ boolean isRelicMissing = isRelicMissing(fairySoulPos);
+ if (!isRelicMissing && !config.highlightFoundRelics) continue;
+ float[] colorComponents = isRelicMissing ? DyeColor.YELLOW.getColorComponents() : DyeColor.BROWN.getColorComponents();
+ RenderHelper.renderFilledThroughWallsWithBeaconBeam(context, fairySoulPos, colorComponents, 0.5F);
+ }
+ }
+ }
+
+ private static void onChatMessage(Text text, boolean overlay) {
+ String message = text.getString();
+ if (message.equals("You've already found this relic!") || message.startsWith("+10,000 Coins! (") && message.endsWith("/28 Relics)")) {
+ markClosestRelicFound();
+ }
+ }
+
+ private static void markClosestRelicFound() {
+ if (!relicsLoaded.isDone()) return;
+ PlayerEntity player = MinecraftClient.getInstance().player;
+ if (player == null) {
+ LOGGER.warn("[Skyblocker] Failed to mark closest relic as found because player is null");
+ return;
+ }
+ relics.stream()
+ .filter(Relics::isRelicMissing)
+ .min(Comparator.comparingDouble(relicPos -> relicPos.getSquaredDistance(player.getPos())))
+ .filter(relicPos -> relicPos.getSquaredDistance(player.getPos()) <= 16)
+ .ifPresent(relicPos -> {
+ foundRelics.computeIfAbsent(Utils.getProfile(), profileKey -> new HashSet<>());
+ foundRelics.get(Utils.getProfile()).add(relicPos);
+ });
+ }
+
+ private static boolean isRelicMissing(BlockPos relicPos) {
+ Set<BlockPos> foundRelicsForProfile = foundRelics.get(Utils.getProfile());
+ return foundRelicsForProfile == null || !foundRelicsForProfile.contains(relicPos);
+ }
+
+ private static void markAllFound() {
+ foundRelics.computeIfAbsent(Utils.getProfile(), profileKey -> new HashSet<>());
+ foundRelics.get(Utils.getProfile()).addAll(relics);
+ }
+
+ private static void markAllMissing() {
+ Set<BlockPos> foundRelicsForProfile = foundRelics.get(Utils.getProfile());
+ if (foundRelicsForProfile != null) {
+ foundRelicsForProfile.clear();
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/TabHud.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/TabHud.java
new file mode 100644
index 00000000..f226f371
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/TabHud.java
@@ -0,0 +1,39 @@
+package de.hysky.skyblocker.skyblock.tabhud;
+
+import org.lwjgl.glfw.GLFW;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
+import net.minecraft.client.option.KeyBinding;
+import net.minecraft.client.util.InputUtil;
+
+public class TabHud {
+
+ public static KeyBinding toggleB;
+ public static KeyBinding toggleA;
+ // public static KeyBinding mapTgl;
+ public static KeyBinding defaultTgl;
+
+ public static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Tab HUD");
+
+ public static void init() {
+
+ toggleB = KeyBindingHelper.registerKeyBinding(
+ new KeyBinding("key.skyblocker.toggleB",
+ InputUtil.Type.KEYSYM,
+ GLFW.GLFW_KEY_B,
+ "key.categories.skyblocker"));
+ toggleA = KeyBindingHelper.registerKeyBinding(
+ new KeyBinding("key.skyblocker.toggleA",
+ InputUtil.Type.KEYSYM,
+ GLFW.GLFW_KEY_N,
+ "key.categories.skyblocker"));
+ defaultTgl = KeyBindingHelper.registerKeyBinding(
+ new KeyBinding("key.skyblocker.defaultTgl",
+ InputUtil.Type.KEYSYM,
+ GLFW.GLFW_KEY_M,
+ "key.categories.skyblocker"));
+
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/ScreenBuilder.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/ScreenBuilder.java
new file mode 100644
index 00000000..ceeaa365
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/ScreenBuilder.java
@@ -0,0 +1,179 @@
+package de.hysky.skyblocker.skyblock.tabhud.screenbuilder;
+
+import java.io.BufferedReader;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.NoSuchElementException;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
+import de.hysky.skyblocker.skyblock.tabhud.screenbuilder.pipeline.AlignStage;
+import de.hysky.skyblocker.skyblock.tabhud.screenbuilder.pipeline.CollideStage;
+import de.hysky.skyblocker.skyblock.tabhud.screenbuilder.pipeline.PipelineStage;
+import de.hysky.skyblocker.skyblock.tabhud.screenbuilder.pipeline.PlaceStage;
+import de.hysky.skyblocker.skyblock.tabhud.screenbuilder.pipeline.StackStage;
+import de.hysky.skyblocker.skyblock.tabhud.widget.DungeonPlayerWidget;
+import de.hysky.skyblocker.skyblock.tabhud.widget.ErrorWidget;
+import de.hysky.skyblocker.skyblock.tabhud.widget.EventWidget;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.util.Identifier;
+
+public class ScreenBuilder {
+
+ // layout pipeline
+ private final ArrayList<PipelineStage> layoutPipeline = new ArrayList<>();
+
+ // all widget instances this builder knows
+ private final ArrayList<Widget> instances = new ArrayList<>();
+ // maps alias -> widget instance
+ private final HashMap<String, Widget> objectMap = new HashMap<>();
+
+ private final String builderName;
+
+ /**
+ * Create a ScreenBuilder from a json.
+ */
+ public ScreenBuilder(Identifier ident) {
+
+ try (BufferedReader reader = MinecraftClient.getInstance().getResourceManager().openAsReader(ident)) {
+ this.builderName = ident.getPath();
+
+ JsonObject json = JsonParser.parseReader(reader).getAsJsonObject();
+
+ JsonArray widgets = json.getAsJsonArray("widgets");
+ JsonArray layout = json.getAsJsonArray("layout");
+
+ for (JsonElement w : widgets) {
+ JsonObject widget = w.getAsJsonObject();
+ String name = widget.get("name").getAsString();
+ String alias = widget.get("alias").getAsString();
+
+ Widget wid = instanceFrom(name, widget);
+ objectMap.put(alias, wid);
+ instances.add(wid);
+ }
+
+ for (JsonElement l : layout) {
+ PipelineStage ps = createStage(l.getAsJsonObject());
+ layoutPipeline.add(ps);
+ }
+ } catch (Exception ex) {
+ // rethrow as unchecked exception so that I don't have to catch anything in the ScreenMaster
+ throw new IllegalStateException("Failed to load file " + ident + ". Reason: " + ex.getMessage());
+ }
+ }
+
+ /**
+ * Try to find a class in the widget package that has the supplied name and
+ * call it's constructor. Manual work is required if the class has arguments.
+ */
+ public Widget instanceFrom(String name, JsonObject widget) {
+
+ // do widgets that require args the normal way
+ JsonElement arg;
+ switch (name) {
+ case "EventWidget" -> {
+ return new EventWidget(widget.get("inGarden").getAsBoolean());
+ }
+ case "DungeonPlayerWidget" -> {
+ return new DungeonPlayerWidget(widget.get("player").getAsInt());
+ }
+ case "ErrorWidget" -> {
+ arg = widget.get("text");
+ if (arg == null) {
+ return new ErrorWidget();
+ } else {
+ return new ErrorWidget(arg.getAsString());
+ }
+ }
+ case "Widget" ->
+ // clown case sanity check. don't instantiate the superclass >:|
+ throw new NoSuchElementException(builderName + "[ERROR]: No such Widget type \"Widget\"!");
+ }
+
+ // reflect something together for the "normal" ones.
+
+ // list all packages that might contain widget classes
+ // using Package isn't reliable, as some classes might not be loaded yet,
+ // causing the packages not to show.
+ String packbase = "de.hysky.skyblocker.skyblock.tabhud.widget";
+ String[] packnames = {
+ packbase,
+ packbase + ".rift"
+ };
+
+ // construct the full class name and try to load.
+ Class<?> clazz = null;
+ for (String pn : packnames) {
+ try {
+ clazz = Class.forName(pn + "." + name);
+ } catch (LinkageError | ClassNotFoundException ex) {
+ continue;
+ }
+ }
+
+ // load failed.
+ if (clazz == null) {
+ throw new NoSuchElementException(builderName + "/[ERROR]: No such Widget type \"" + name + "\"!");
+ }
+
+ // return instance of that class.
+ try {
+ Constructor<?> ctor = clazz.getConstructor();
+ return (Widget) ctor.newInstance();
+ } catch (NoSuchMethodException | InstantiationException | IllegalAccessException
+ | IllegalArgumentException | InvocationTargetException | SecurityException ex) {
+ throw new IllegalStateException(builderName + "/" + name + ": Internal error...");
+ }
+ }
+
+ /**
+ * Create a PipelineStage from a json object.
+ */
+ public PipelineStage createStage(JsonObject descr) throws NoSuchElementException {
+
+ String op = descr.get("op").getAsString();
+
+ return switch (op) {
+ case "place" -> new PlaceStage(this, descr);
+ case "stack" -> new StackStage(this, descr);
+ case "align" -> new AlignStage(this, descr);
+ case "collideAgainst" -> new CollideStage(this, descr);
+ default -> throw new NoSuchElementException("No such op " + op + " as requested by " + this.builderName);
+ };
+ }
+
+ /**
+ * Lookup Widget instance from alias name
+ */
+ public Widget getInstance(String name) {
+ if (!this.objectMap.containsKey(name)) {
+ throw new NoSuchElementException("No widget with alias " + name + " in screen " + builderName);
+ }
+ return this.objectMap.get(name);
+ }
+
+ /**
+ * Run the pipeline to build a Screen
+ */
+ public void run(DrawContext context, int screenW, int screenH) {
+
+ for (Widget w : instances) {
+ w.update();
+ }
+ for (PipelineStage ps : layoutPipeline) {
+ ps.run(screenW, screenH);
+ }
+ for (Widget w : instances) {
+ w.render(context);
+ }
+ }
+
+}
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
new file mode 100644
index 00000000..210d8001
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/ScreenMaster.java
@@ -0,0 +1,144 @@
+package de.hysky.skyblocker.skyblock.tabhud.screenbuilder;
+
+import java.io.BufferedReader;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import de.hysky.skyblocker.skyblock.tabhud.TabHud;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerLocator;
+import net.fabricmc.fabric.api.resource.ResourceManagerHelper;
+import net.fabricmc.fabric.api.resource.ResourcePackActivationType;
+import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener;
+import net.fabricmc.loader.api.FabricLoader;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.resource.Resource;
+import net.minecraft.resource.ResourceManager;
+import net.minecraft.resource.ResourceType;
+import net.minecraft.util.Identifier;
+
+public class ScreenMaster {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger("skyblocker");
+
+ private static final int VERSION = 1;
+
+ private static final HashMap<String, ScreenBuilder> standardMap = new HashMap<>();
+ private static final HashMap<String, ScreenBuilder> screenAMap = new HashMap<>();
+ private static final HashMap<String, ScreenBuilder> screenBMap = new HashMap<>();
+
+ /**
+ * Load a screen mapping from an identifier
+ */
+ public static void load(Identifier ident) {
+
+ String path = ident.getPath();
+ String[] parts = path.split("/");
+ String screenType = parts[parts.length - 2];
+ String location = parts[parts.length - 1];
+ location = location.replace(".json", "");
+
+ ScreenBuilder sb = new ScreenBuilder(ident);
+ switch (screenType) {
+ case "standard" -> standardMap.put(location, sb);
+ case "screen_a" -> screenAMap.put(location, sb);
+ case "screen_b" -> screenBMap.put(location, sb);
+ }
+ }
+
+ /**
+ * Top level render method.
+ * Calls the appropriate ScreenBuilder with the screen's dimensions
+ */
+ public static void render(DrawContext context, int w, int h) {
+ String location = PlayerLocator.getPlayerLocation().internal;
+ HashMap<String, ScreenBuilder> lookup;
+ if (TabHud.toggleA.isPressed()) {
+ lookup = screenAMap;
+ } else if (TabHud.toggleB.isPressed()) {
+ lookup = screenBMap;
+ } else {
+ lookup = standardMap;
+ }
+
+ ScreenBuilder sb = lookup.get(location);
+ // seems suboptimal, maybe load the default first into all possible values
+ // and then override?
+ if (sb == null) {
+ sb = lookup.get("default");
+ }
+
+ sb.run(context, w, h);
+
+ }
+
+ public static void init() {
+
+ // WHY MUST IT ALWAYS BE SUCH NESTED GARBAGE MINECRAFT KEEP THAT IN DFU FFS
+
+ FabricLoader.getInstance()
+ .getModContainer("skyblocker")
+ .ifPresent(container -> ResourceManagerHelper.registerBuiltinResourcePack(
+ new Identifier("skyblocker", "top_aligned"),
+ container,
+ ResourcePackActivationType.NORMAL));
+
+ ResourceManagerHelper.get(ResourceType.CLIENT_RESOURCES).registerReloadListener(
+ // ...why are we instantiating an interface again?
+ new SimpleSynchronousResourceReloadListener() {
+ @Override
+ public Identifier getFabricId() {
+ return new Identifier("skyblocker", "tabhud");
+ }
+
+ @Override
+ public void reload(ResourceManager manager) {
+
+ standardMap.clear();
+ screenAMap.clear();
+ screenBMap.clear();
+
+ int excnt = 0;
+
+ for (Map.Entry<Identifier, Resource> entry : manager
+ .findResources("tabhud", path -> path.getPath().endsWith("version.json"))
+ .entrySet()) {
+
+ try (BufferedReader reader = MinecraftClient.getInstance().getResourceManager()
+ .openAsReader(entry.getKey())) {
+ JsonObject json = JsonParser.parseReader(reader).getAsJsonObject();
+ if (json.get("format_version").getAsInt() != VERSION) {
+ throw new IllegalStateException(String.format("Resource pack isn't compatible! Expected version %d, got %d", VERSION, json.get("format_version").getAsInt()));
+ }
+
+ } catch (Exception ex) {
+ throw new IllegalStateException(
+ "Rejected this resource pack. Reason: " + ex.getMessage());
+ }
+ }
+
+ for (Map.Entry<Identifier, Resource> entry : manager
+ .findResources("tabhud", path -> path.getPath().endsWith(".json") && !path.getPath().endsWith("version.json"))
+ .entrySet()) {
+ try {
+
+ load(entry.getKey());
+ } catch (Exception e) {
+ LOGGER.error(e.getMessage());
+ excnt++;
+ }
+ }
+ if (excnt > 0) {
+ throw new IllegalStateException("This screen definition isn't valid, see above");
+ }
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/AlignStage.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/AlignStage.java
new file mode 100644
index 00000000..7c01a6db
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/AlignStage.java
@@ -0,0 +1,83 @@
+package de.hysky.skyblocker.skyblock.tabhud.screenbuilder.pipeline;
+
+import java.util.ArrayList;
+import java.util.NoSuchElementException;
+
+import com.google.gson.JsonObject;
+
+import de.hysky.skyblocker.skyblock.tabhud.screenbuilder.ScreenBuilder;
+import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
+import de.hysky.skyblocker.skyblock.tabhud.util.ScreenConst;
+
+public class AlignStage extends PipelineStage {
+
+ private enum AlignReference {
+ HORICENT("horizontalCenter"),
+ VERTCENT("verticalCenter"),
+ LEFTCENT("leftOfCenter"),
+ RIGHTCENT("rightOfCenter"),
+ TOPCENT("topOfCenter"),
+ BOTCENT("botOfCenter"),
+ TOP("top"),
+ BOT("bot"),
+ LEFT("left"),
+ RIGHT("right");
+
+ private final String str;
+
+ AlignReference(String d) {
+ this.str = d;
+ }
+
+ public static AlignReference parse(String s) throws NoSuchElementException {
+ for (AlignReference d : AlignReference.values()) {
+ if (d.str.equals(s)) {
+ return d;
+ }
+ }
+ throw new NoSuchElementException("\"" + s + "\" is not a valid reference for an align op!");
+ }
+ }
+
+ private final AlignReference reference;
+
+ public AlignStage(ScreenBuilder builder, JsonObject descr) {
+ this.reference = AlignReference.parse(descr.get("reference").getAsString());
+ this.primary = new ArrayList<>(descr.getAsJsonArray("apply_to")
+ .asList()
+ .stream()
+ .map(x -> builder.getInstance(x.getAsString()))
+ .toList());
+ }
+
+ public void run(int screenW, int screenH) {
+ int wHalf, hHalf;
+ for (Widget wid : primary) {
+ switch (this.reference) {
+ case HORICENT -> wid.setX((screenW - wid.getWidth()) / 2);
+ case VERTCENT -> wid.setY((screenH - wid.getHeight()) / 2);
+ case LEFTCENT -> {
+ wHalf = screenW / 2;
+ wid.setX(wHalf - ScreenConst.WIDGET_PAD_HALF - wid.getWidth());
+ }
+ case RIGHTCENT -> {
+ wHalf = screenW / 2;
+ wid.setX(wHalf + ScreenConst.WIDGET_PAD_HALF);
+ }
+ case TOPCENT -> {
+ hHalf = screenH / 2;
+ wid.setY(hHalf - ScreenConst.WIDGET_PAD_HALF - wid.getHeight());
+ }
+ case BOTCENT -> {
+ hHalf = screenH / 2;
+ wid.setY(hHalf + ScreenConst.WIDGET_PAD_HALF);
+ }
+ case TOP -> wid.setY(ScreenConst.getScreenPad());
+ case BOT -> wid.setY(screenH - wid.getHeight() - ScreenConst.getScreenPad());
+ case LEFT -> wid.setX(ScreenConst.getScreenPad());
+ case RIGHT -> wid.setX(screenW - wid.getWidth() - ScreenConst.getScreenPad());
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/CollideStage.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/CollideStage.java
new file mode 100644
index 00000000..d100a52e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/CollideStage.java
@@ -0,0 +1,153 @@
+package de.hysky.skyblocker.skyblock.tabhud.screenbuilder.pipeline;
+
+import java.util.ArrayList;
+import java.util.NoSuchElementException;
+
+import com.google.gson.JsonObject;
+
+import de.hysky.skyblocker.skyblock.tabhud.screenbuilder.ScreenBuilder;
+import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
+import de.hysky.skyblocker.skyblock.tabhud.util.ScreenConst;
+
+public class CollideStage extends PipelineStage {
+
+ private enum CollideDirection {
+ LEFT("left"),
+ RIGHT("right"),
+ TOP("top"),
+ BOT("bot");
+
+ private final String str;
+
+ CollideDirection(String d) {
+ this.str = d;
+ }
+
+ public static CollideDirection parse(String s) throws NoSuchElementException {
+ for (CollideDirection d : CollideDirection.values()) {
+ if (d.str.equals(s)) {
+ return d;
+ }
+ }
+ throw new NoSuchElementException("\"" + s + "\" is not a valid direction for a collide op!");
+ }
+ }
+
+ private final CollideDirection direction;
+
+ public CollideStage(ScreenBuilder builder, JsonObject descr) {
+ this.direction = CollideDirection.parse(descr.get("direction").getAsString());
+ this.primary = new ArrayList<>(descr.getAsJsonArray("widgets")
+ .asList()
+ .stream()
+ .map(x -> builder.getInstance(x.getAsString()))
+ .toList());
+ this.secondary = new ArrayList<>(descr.getAsJsonArray("colliders")
+ .asList()
+ .stream()
+ .map(x -> builder.getInstance(x.getAsString()))
+ .toList());
+ }
+
+ public void run(int screenW, int screenH) {
+ switch (this.direction) {
+ case LEFT -> primary.forEach(w -> collideAgainstL(screenW, w));
+ case RIGHT -> primary.forEach(w -> collideAgainstR(screenW, w));
+ case TOP -> primary.forEach(w -> collideAgainstT(screenH, w));
+ case BOT -> primary.forEach(w -> collideAgainstB(screenH, w));
+ }
+ }
+
+ public void collideAgainstL(int screenW, Widget w) {
+ int yMin = w.getY();
+ int yMax = w.getY() + w.getHeight();
+
+ int xCor = screenW;
+
+ for (Widget other : secondary) {
+ if (other.getY() + other.getHeight() + ScreenConst.WIDGET_PAD < yMin) {
+ // too high, next one
+ continue;
+ }
+
+ if (other.getY() - ScreenConst.WIDGET_PAD > yMax) {
+ // too low, next
+ continue;
+ }
+
+ int xPos = other.getX() - ScreenConst.WIDGET_PAD - w.getWidth();
+ xCor = Math.min(xCor, xPos);
+ }
+ w.setX(xCor);
+ }
+
+ public void collideAgainstR(int screenW, Widget w) {
+ int yMin = w.getY();
+ int yMax = w.getY() + w.getHeight();
+
+ int xCor = 0;
+
+ for (Widget other : secondary) {
+ if (other.getY() + other.getHeight() + ScreenConst.WIDGET_PAD < yMin) {
+ // too high, next one
+ continue;
+ }
+
+ if (other.getY() - ScreenConst.WIDGET_PAD > yMax) {
+ // too low, next
+ continue;
+ }
+
+ int xPos = other.getX() + other.getWidth() + ScreenConst.WIDGET_PAD;
+ xCor = Math.max(xCor, xPos);
+ }
+ w.setX(xCor);
+ }
+
+ public void collideAgainstT(int screenH, Widget w) {
+ int xMin = w.getX();
+ int xMax = w.getX() + w.getWidth();
+
+ int yCor = screenH;
+
+ for (Widget other : secondary) {
+ if (other.getX() + other.getWidth() + ScreenConst.WIDGET_PAD < xMin) {
+ // too far left, next one
+ continue;
+ }
+
+ if (other.getX() - ScreenConst.WIDGET_PAD > xMax) {
+ // too far right, next
+ continue;
+ }
+
+ int yPos = other.getY() - ScreenConst.WIDGET_PAD - w.getHeight();
+ yCor = Math.min(yCor, yPos);
+ }
+ w.setY(yCor);
+ }
+
+ public void collideAgainstB(int screenH, Widget w) {
+ int xMin = w.getX();
+ int xMax = w.getX() + w.getWidth();
+
+ int yCor = 0;
+
+ for (Widget other : secondary) {
+ if (other.getX() + other.getWidth() + ScreenConst.WIDGET_PAD < xMin) {
+ // too far left, next one
+ continue;
+ }
+
+ if (other.getX() - ScreenConst.WIDGET_PAD > xMax) {
+ // too far right, next
+ continue;
+ }
+
+ int yPos = other.getY() + other.getHeight() + ScreenConst.WIDGET_PAD;
+ yCor = Math.max(yCor, yPos);
+ }
+ w.setY(yCor);
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/PipelineStage.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/PipelineStage.java
new file mode 100644
index 00000000..20e4859e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/PipelineStage.java
@@ -0,0 +1,14 @@
+package de.hysky.skyblocker.skyblock.tabhud.screenbuilder.pipeline;
+
+import java.util.ArrayList;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
+
+public abstract class PipelineStage {
+
+ protected ArrayList<Widget> primary = null;
+ protected ArrayList<Widget> secondary = null;
+
+ public abstract void run(int screenW, int screenH);
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/PlaceStage.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/PlaceStage.java
new file mode 100644
index 00000000..7d57305b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/PlaceStage.java
@@ -0,0 +1,94 @@
+package de.hysky.skyblocker.skyblock.tabhud.screenbuilder.pipeline;
+
+import java.util.ArrayList;
+import java.util.NoSuchElementException;
+
+import com.google.gson.JsonObject;
+
+import de.hysky.skyblocker.skyblock.tabhud.screenbuilder.ScreenBuilder;
+import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
+import de.hysky.skyblocker.skyblock.tabhud.util.ScreenConst;
+
+public class PlaceStage extends PipelineStage {
+
+ private enum PlaceLocation {
+ CENTER("center"),
+ TOPCENT("centerTop"),
+ BOTCENT("centerBot"),
+ LEFTCENT("centerLeft"),
+ RIGHTCENT("centerRight"),
+ TRCORNER("cornerTopRight"),
+ TLCORNER("cornerTopLeft"),
+ BRCORNER("cornerBotRight"),
+ BLCORNER("cornerBotLeft");
+
+ private final String str;
+
+ PlaceLocation(String d) {
+ this.str = d;
+ }
+
+ public static PlaceLocation parse(String s) throws NoSuchElementException {
+ for (PlaceLocation d : PlaceLocation.values()) {
+ if (d.str.equals(s)) {
+ return d;
+ }
+ }
+ throw new NoSuchElementException("\"" + s + "\" is not a valid location for a place op!");
+ }
+ }
+
+ private final PlaceLocation where;
+
+ public PlaceStage(ScreenBuilder builder, JsonObject descr) {
+ this.where = PlaceLocation.parse(descr.get("where").getAsString());
+ this.primary = new ArrayList<>(descr.getAsJsonArray("apply_to")
+ .asList()
+ .stream()
+ .map(x -> builder.getInstance(x.getAsString()))
+ .limit(1)
+ .toList());
+ }
+
+ public void run(int screenW, int screenH) {
+ Widget wid = primary.get(0);
+ switch (where) {
+ case CENTER -> {
+ wid.setX((screenW - wid.getWidth()) / 2);
+ wid.setY((screenH - wid.getHeight()) / 2);
+ }
+ case TOPCENT -> {
+ wid.setX((screenW - wid.getWidth()) / 2);
+ wid.setY(ScreenConst.getScreenPad());
+ }
+ case BOTCENT -> {
+ wid.setX((screenW - wid.getWidth()) / 2);
+ wid.setY((screenH - wid.getHeight()) - ScreenConst.getScreenPad());
+ }
+ case LEFTCENT -> {
+ wid.setX(ScreenConst.getScreenPad());
+ wid.setY((screenH - wid.getHeight()) / 2);
+ }
+ case RIGHTCENT -> {
+ wid.setX((screenW - wid.getWidth()) - ScreenConst.getScreenPad());
+ wid.setY((screenH - wid.getHeight()) / 2);
+ }
+ case TLCORNER -> {
+ wid.setX(ScreenConst.getScreenPad());
+ wid.setY(ScreenConst.getScreenPad());
+ }
+ case TRCORNER -> {
+ wid.setX((screenW - wid.getWidth()) - ScreenConst.getScreenPad());
+ wid.setY(ScreenConst.getScreenPad());
+ }
+ case BLCORNER -> {
+ wid.setX(ScreenConst.getScreenPad());
+ wid.setY((screenH - wid.getHeight()) - ScreenConst.getScreenPad());
+ }
+ case BRCORNER -> {
+ wid.setX((screenW - wid.getWidth()) - ScreenConst.getScreenPad());
+ wid.setY((screenH - wid.getHeight()) - ScreenConst.getScreenPad());
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/StackStage.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/StackStage.java
new file mode 100644
index 00000000..f4fe07e5
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/screenbuilder/pipeline/StackStage.java
@@ -0,0 +1,114 @@
+package de.hysky.skyblocker.skyblock.tabhud.screenbuilder.pipeline;
+
+import java.util.ArrayList;
+import java.util.NoSuchElementException;
+
+import com.google.gson.JsonObject;
+
+import de.hysky.skyblocker.skyblock.tabhud.screenbuilder.ScreenBuilder;
+import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
+import de.hysky.skyblocker.skyblock.tabhud.util.ScreenConst;
+
+public class StackStage extends PipelineStage {
+
+ private enum StackDirection {
+ HORIZONTAL("horizontal"),
+ VERTICAL("vertical");
+
+ private final String str;
+
+ StackDirection(String d) {
+ this.str = d;
+ }
+
+ public static StackDirection parse(String s) throws NoSuchElementException {
+ for (StackDirection d : StackDirection.values()) {
+ if (d.str.equals(s)) {
+ return d;
+ }
+ }
+ throw new NoSuchElementException("\"" + s + "\" is not a valid direction for a stack op!");
+ }
+ }
+
+ private enum StackAlign {
+ TOP("top"),
+ BOT("bot"),
+ LEFT("left"),
+ RIGHT("right"),
+ CENTER("center");
+
+ private final String str;
+
+ StackAlign(String d) {
+ this.str = d;
+ }
+
+ public static StackAlign parse(String s) throws NoSuchElementException {
+ for (StackAlign d : StackAlign.values()) {
+ if (d.str.equals(s)) {
+ return d;
+ }
+ }
+ throw new NoSuchElementException("\"" + s + "\" is not a valid alignment for a stack op!");
+ }
+ }
+
+ private final StackDirection direction;
+ private final StackAlign align;
+
+ public StackStage(ScreenBuilder builder, JsonObject descr) {
+ this.direction = StackDirection.parse(descr.get("direction").getAsString());
+ this.align = StackAlign.parse(descr.get("align").getAsString());
+ this.primary = new ArrayList<>(descr.getAsJsonArray("apply_to")
+ .asList()
+ .stream()
+ .map(x -> builder.getInstance(x.getAsString()))
+ .toList());
+ }
+
+ public void run(int screenW, int screenH) {
+ switch (this.direction) {
+ case HORIZONTAL -> stackWidgetsHoriz(screenW);
+ case VERTICAL -> stackWidgetsVert(screenH);
+ }
+ }
+
+ public void stackWidgetsVert(int screenH) {
+ int compHeight = -ScreenConst.WIDGET_PAD;
+ for (Widget wid : primary) {
+ compHeight += wid.getHeight() + 5;
+ }
+
+ int y = switch (this.align) {
+
+ case TOP -> ScreenConst.getScreenPad();
+ case BOT -> (screenH - compHeight) - ScreenConst.getScreenPad();
+ default -> (screenH - compHeight) / 2;
+ };
+
+ for (Widget wid : primary) {
+ wid.setY(y);
+ y += wid.getHeight() + ScreenConst.WIDGET_PAD;
+ }
+ }
+
+ public void stackWidgetsHoriz(int screenW) {
+ int compWidth = -ScreenConst.WIDGET_PAD;
+ for (Widget wid : primary) {
+ compWidth += wid.getWidth() + ScreenConst.WIDGET_PAD;
+ }
+
+ int x = switch (this.align) {
+
+ case LEFT -> ScreenConst.getScreenPad();
+ case RIGHT -> (screenW - compWidth) - ScreenConst.getScreenPad();
+ default -> (screenW - compWidth) / 2;
+ };
+
+ for (Widget wid : primary) {
+ wid.setX(x);
+ x += wid.getWidth() + ScreenConst.WIDGET_PAD;
+ }
+ }
+} \ No newline at end of file
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
new file mode 100644
index 00000000..24883d77
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/Ico.java
@@ -0,0 +1,60 @@
+package de.hysky.skyblocker.skyblock.tabhud.util;
+
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+
+/**
+ * Stores convenient shorthands for common ItemStack definitions
+ */
+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 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 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 GOLD = new ItemStack(Items.GOLD_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 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 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);
+ public static final ItemStack LEATHER = new ItemStack(Items.LEATHER);
+ public static final ItemStack MITHRIL = new ItemStack(Items.PRISMARINE_CRYSTALS);
+ public static final ItemStack REDSTONE = new ItemStack(Items.REDSTONE);
+ public static final ItemStack FIRE = new ItemStack(Items.CAMPFIRE);
+ 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 DRAGON = new ItemStack(Items.DRAGON_HEAD);
+ public static final ItemStack DIAMOND = new ItemStack(Items.DIAMOND);
+ public static final ItemStack ICE = new ItemStack(Items.ICE);
+ public static final ItemStack CHEST = new ItemStack(Items.CHEST);
+ public static final ItemStack COMMAND = new ItemStack(Items.COMMAND_BLOCK);
+ public static final ItemStack SKULL = new ItemStack(Items.SKELETON_SKULL);
+ 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 B_ROD = new ItemStack(Items.BLAZE_ROD);
+ public static final ItemStack BOW = new ItemStack(Items.BOW);
+ public static final ItemStack COPPER = new ItemStack(Items.COPPER_INGOT);
+ public static final ItemStack COMPOSTER = new ItemStack(Items.COMPOSTER);
+ public static final ItemStack SAPLING = new ItemStack(Items.OAK_SAPLING);
+ public static final ItemStack MILESTONE = new ItemStack(Items.LODESTONE);
+ public static final ItemStack 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);
+ public static final ItemStack PINK_DYE = new ItemStack(Items.PINK_DYE);
+ public static final ItemStack ENCHANTED_BOOK = new ItemStack(Items.ENCHANTED_BOOK);
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/PlayerListMgr.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/PlayerListMgr.java
new file mode 100644
index 00000000..f577f2d3
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/PlayerListMgr.java
@@ -0,0 +1,171 @@
+package de.hysky.skyblocker.skyblock.tabhud.util;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.mixin.accessor.PlayerListHudAccessor;
+import de.hysky.skyblocker.utils.Utils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.client.network.PlayerListEntry;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+
+/**
+ * This class may be used to get data from the player list. It doesn't get its
+ * data every frame, instead, a scheduler is used to update the data this class
+ * is holding periodically. The list is sorted like in the vanilla game.
+ */
+public class PlayerListMgr {
+
+ public static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Regex");
+
+ private static List<PlayerListEntry> playerList;
+ private static String footer;
+
+ public static void updateList() {
+
+ if (!Utils.isOnSkyblock()) {
+ return;
+ }
+
+ ClientPlayNetworkHandler cpnwh = MinecraftClient.getInstance().getNetworkHandler();
+
+ // check is needed, else game crash on server leave
+ if (cpnwh != null) {
+ playerList = cpnwh.getPlayerList().stream().sorted(PlayerListHudAccessor.getOrdering()).toList();
+ }
+ }
+
+ public static void updateFooter(Text f) {
+ if (f == null) {
+ footer = null;
+ } else {
+ footer = f.getString();
+ }
+ }
+
+ public static String getFooter() {
+ return footer;
+ }
+
+ /**
+ * Get the display name at some index of the player list and apply a pattern to
+ * it
+ *
+ * @return the matcher if p fully matches, else null
+ */
+ public static Matcher regexAt(int idx, Pattern p) {
+
+ String str = PlayerListMgr.strAt(idx);
+
+ if (str == null) {
+ return null;
+ }
+
+ Matcher m = p.matcher(str);
+ if (!m.matches()) {
+ LOGGER.error("no match: \"{}\" against \"{}\"", str, p);
+ return null;
+ } else {
+ return m;
+ }
+ }
+
+ /**
+ * Get the display name at some index of the player list as string
+ *
+ * @return the string or null, if the display name is null, empty or whitespace
+ * only
+ */
+ public static String strAt(int idx) {
+
+ if (playerList == null) {
+ return null;
+ }
+
+ if (playerList.size() <= idx) {
+ return null;
+ }
+
+ Text txt = playerList.get(idx).getDisplayName();
+ if (txt == null) {
+ return null;
+ }
+ String str = txt.getString().trim();
+ if (str.isEmpty()) {
+ return null;
+ }
+ return str;
+ }
+
+ /**
+ * Gets the display name at some index of the player list
+ *
+ * @return the text or null, if the display name is null
+ *
+ * @implNote currently designed specifically for crimson isles faction quests
+ * widget and the rift widgets, might not work correctly without
+ * modification for other stuff. you've been warned!
+ */
+ public static Text textAt(int idx) {
+
+ if (playerList == null) {
+ return null;
+ }
+
+ if (playerList.size() <= idx) {
+ return null;
+ }
+
+ Text txt = playerList.get(idx).getDisplayName();
+ if (txt == null) {
+ return null;
+ }
+
+ // Rebuild the text object to remove leading space thats in all faction quest
+ // stuff (also removes trailing space just in case)
+ MutableText newText = Text.empty();
+ int size = txt.getSiblings().size();
+
+ for (int i = 0; i < size; i++) {
+ Text current = txt.getSiblings().get(i);
+ String textToAppend = current.getString();
+
+ // Trim leading & trailing space - this can only be done at the start and end
+ // otherwise it'll produce malformed results
+ if (i == 0)
+ textToAppend = textToAppend.stripLeading();
+ if (i == size - 1)
+ textToAppend = textToAppend.stripTrailing();
+
+ newText.append(Text.literal(textToAppend).setStyle(current.getStyle()));
+ }
+
+ // Avoid returning an empty component - Rift advertisements needed this
+ if (newText.getString().isEmpty()) {
+ return null;
+ }
+
+ return newText;
+ }
+
+ /**
+ * Get the display name at some index of the player list as Text as seen in the
+ * game
+ *
+ * @return the PlayerListEntry at that index
+ */
+ public static PlayerListEntry getRaw(int idx) {
+ return playerList.get(idx);
+ }
+
+ public static int getSize() {
+ return playerList.size();
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/PlayerLocator.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/PlayerLocator.java
new file mode 100644
index 00000000..e5f5bfc8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/PlayerLocator.java
@@ -0,0 +1,87 @@
+package de.hysky.skyblocker.skyblock.tabhud.util;
+
+import de.hysky.skyblocker.utils.Utils;
+
+/**
+ * Uses data from the player list to determine the area the player is in.
+ */
+public class PlayerLocator {
+
+ public enum Location {
+ DUNGEON("dungeon"),
+ GUEST_ISLAND("guest_island"),
+ HOME_ISLAND("home_island"),
+ CRIMSON_ISLE("crimson_isle"),
+ DUNGEON_HUB("dungeon_hub"),
+ FARMING_ISLAND("farming_island"),
+ PARK("park"),
+ DWARVEN_MINES("dwarven_mines"),
+ CRYSTAL_HOLLOWS("crystal_hollows"),
+ END("end"),
+ GOLD_MINE("gold_mine"),
+ DEEP_CAVERNS("deep_caverns"),
+ HUB("hub"),
+ SPIDER_DEN("spider_den"),
+ JERRY("jerry_workshop"),
+ GARDEN("garden"),
+ INSTANCED("kuudra"),
+ THE_RIFT("rift"),
+ DARK_AUCTION("dark_auction"),
+ UNKNOWN("unknown");
+
+ public final String internal;
+
+ Location(String i) {
+ // as used internally by the mod, e.g. in the json
+ this.internal = i;
+ }
+
+ }
+
+ public static Location getPlayerLocation() {
+
+ if (!Utils.isOnSkyblock()) {
+ return Location.UNKNOWN;
+ }
+
+ String areaDescriptor = PlayerListMgr.strAt(41);
+
+ if (areaDescriptor == null || areaDescriptor.length() < 6) {
+ return Location.UNKNOWN;
+ }
+
+ if (areaDescriptor.startsWith("Dungeon")) {
+ return Location.DUNGEON;
+ }
+
+ return switch (areaDescriptor.substring(6)) {
+ case "Private Island" -> {
+ String islandType = PlayerListMgr.strAt(44);
+ if (islandType == null) {
+ yield Location.UNKNOWN;
+ } else if (islandType.endsWith("Guest")) {
+ yield Location.GUEST_ISLAND;
+ } else {
+ yield Location.HOME_ISLAND;
+ }
+ }
+ case "Crimson Isle" -> Location.CRIMSON_ISLE;
+ case "Dungeon Hub" -> Location.DUNGEON_HUB;
+ case "The Farming Islands" -> Location.FARMING_ISLAND;
+ case "The Park" -> Location.PARK;
+ case "Dwarven Mines" -> Location.DWARVEN_MINES;
+ case "Crystal Hollows" -> Location.CRYSTAL_HOLLOWS;
+ case "The End" -> Location.END;
+ case "Gold Mine" -> Location.GOLD_MINE;
+ case "Deep Caverns" -> Location.DEEP_CAVERNS;
+ case "Hub" -> Location.HUB;
+ case "Spider's Den" -> Location.SPIDER_DEN;
+ case "Jerry's Workshop" -> Location.JERRY;
+ case "Garden" -> Location.GARDEN;
+ case "Instanced" -> Location.INSTANCED;
+ case "The Rift" -> Location.THE_RIFT;
+ case "Dark Auction" -> Location.DARK_AUCTION;
+ default -> Location.UNKNOWN;
+ };
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/ScreenConst.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/ScreenConst.java
new file mode 100644
index 00000000..6a4d96d3
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/ScreenConst.java
@@ -0,0 +1,13 @@
+package de.hysky.skyblocker.skyblock.tabhud.util;
+
+import me.xmrvizzy.skyblocker.config.SkyblockerConfigManager;
+
+public class ScreenConst {
+ public static final int WIDGET_PAD = 5;
+ public static final int WIDGET_PAD_HALF = 3;
+ private static final int SCREEN_PAD_BASE = 20;
+
+ public static int getScreenPad() {
+ return (int) ((1f/((float)SkyblockerConfigManager.get().general.tabHud.tabHudScale/100f) * SCREEN_PAD_BASE));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/CameraPositionWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/CameraPositionWidget.java
new file mode 100644
index 00000000..9cff3d32
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/CameraPositionWidget.java
@@ -0,0 +1,37 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.math.MathHelper;
+
+public class CameraPositionWidget extends Widget {
+ private static final MutableText TITLE = Text.literal("Camera Pos").formatted(Formatting.DARK_PURPLE,
+ Formatting.BOLD);
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+
+ public CameraPositionWidget() {
+ super(TITLE, Formatting.DARK_PURPLE.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ double yaw = CLIENT.getCameraEntity().getYaw();
+ double pitch = CLIENT.getCameraEntity().getPitch();
+
+ this.addComponent(
+ new PlainTextComponent(Text.literal("Yaw: " + roundToDecimalPlaces(MathHelper.wrapDegrees(yaw), 3))));
+ this.addComponent(new PlainTextComponent(
+ Text.literal("Pitch: " + roundToDecimalPlaces(MathHelper.wrapDegrees(pitch), 3))));
+
+ }
+
+ // https://stackoverflow.com/a/33889423
+ private static double roundToDecimalPlaces(double value, int decimalPlaces) {
+ double shift = Math.pow(10, decimalPlaces);
+
+ return Math.round(value * shift) / shift;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/CommsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/CommsWidget.java
new file mode 100644
index 00000000..e8bf91ab
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/CommsWidget.java
@@ -0,0 +1,63 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.ProgressComponent;
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.math.MathHelper;
+
+// this widget shows the status of the king's commissions.
+// (dwarven mines and crystal hollows)
+
+public class CommsWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Commissions").formatted(Formatting.DARK_AQUA,
+ Formatting.BOLD);
+
+ // match a comm
+ // group 1: comm name
+ // group 2: comm progress (without "%" for comms that show a percentage)
+ private static final Pattern COMM_PATTERN = Pattern.compile("(?<name>.*): (?<progress>.*)%?");
+
+ public CommsWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ for (int i = 50; i <= 53; i++) {
+ Matcher m = PlayerListMgr.regexAt(i, COMM_PATTERN);
+ // end of comms found?
+ if (m == null) {
+ if (i == 50) {
+ this.addComponent(new IcoTextComponent());
+ }
+ break;
+ }
+
+ ProgressComponent pc;
+
+ String name = m.group("name");
+ String progress = m.group("progress");
+
+ if (progress.equals("DONE")) {
+ pc = new ProgressComponent(Ico.BOOK, Text.of(name), Text.of(progress), 100f, pcntToCol(100));
+ } else {
+ float pcnt = Float.parseFloat(progress.substring(0, progress.length() - 1));
+ pc = new ProgressComponent(Ico.BOOK, Text.of(name), pcnt, pcntToCol(pcnt));
+ }
+ this.addComponent(pc);
+ }
+ }
+
+ private int pcntToCol(float pcnt) {
+ return MathHelper.hsvToRgb(pcnt / 300f, 0.9f, 0.9f);
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ComposterWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ComposterWidget.java
new file mode 100644
index 00000000..fbeb5ae5
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ComposterWidget.java
@@ -0,0 +1,30 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about the garden's composter
+
+public class ComposterWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Composter").formatted(Formatting.GREEN,
+ Formatting.BOLD);
+
+ public ComposterWidget() {
+ super(TITLE, Formatting.GREEN.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ this.addSimpleIcoText(Ico.SAPLING, "Organic Matter:", Formatting.YELLOW, 48);
+ this.addSimpleIcoText(Ico.FURNACE, "Fuel:", Formatting.BLUE, 49);
+ this.addSimpleIcoText(Ico.CLOCK, "Time Left:", Formatting.RED, 50);
+ this.addSimpleIcoText(Ico.COMPOSTER, "Stored Compost:", Formatting.DARK_GREEN, 51);
+
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/CookieWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/CookieWidget.java
new file mode 100644
index 00000000..a5883e7e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/CookieWidget.java
@@ -0,0 +1,50 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about active super cookies
+// or not, if you're unwilling to buy one
+
+public class CookieWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Cookie Info").formatted(Formatting.DARK_PURPLE,
+ Formatting.BOLD);
+
+ private static final Pattern COOKIE_PATTERN = Pattern.compile(".*\\nCookie Buff\\n(?<buff>.*)\\n");
+
+ public CookieWidget() {
+ super(TITLE, Formatting.DARK_PURPLE.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ String footertext = PlayerListMgr.getFooter();
+ if (footertext == null || !footertext.contains("Cookie Buff")) {
+ this.addComponent(new IcoTextComponent());
+ return;
+ }
+
+ Matcher m = COOKIE_PATTERN.matcher(footertext);
+ if (!m.find() || m.group("buff") == null) {
+ this.addComponent(new IcoTextComponent());
+ return;
+ }
+
+ String buff = m.group("buff");
+ if (buff.startsWith("Not")) {
+ this.addComponent(new IcoTextComponent(Ico.COOKIE, Text.of("Not active")));
+ } else {
+ Text cookie = Text.literal("Time Left: ").append(buff);
+ this.addComponent(new IcoTextComponent(Ico.COOKIE, cookie));
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonBuffWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonBuffWidget.java
new file mode 100644
index 00000000..fd896796
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonBuffWidget.java
@@ -0,0 +1,68 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import java.util.Arrays;
+import java.util.Comparator;
+
+// this widget shows a list of obtained dungeon buffs
+
+public class DungeonBuffWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Dungeon Buffs").formatted(Formatting.DARK_PURPLE,
+ Formatting.BOLD);
+
+ public DungeonBuffWidget() {
+ super(TITLE, Formatting.DARK_PURPLE.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+
+ String footertext = PlayerListMgr.getFooter();
+
+ if (footertext == null || !footertext.contains("Dungeon Buffs")) {
+ this.addComponent(new PlainTextComponent(Text.literal("No data").formatted(Formatting.GRAY)));
+ return;
+ }
+
+ String interesting = footertext.split("Dungeon Buffs")[1];
+ String[] lines = interesting.split("\n");
+
+ if (!lines[1].startsWith("Blessing")) {
+ this.addComponent(new PlainTextComponent(Text.literal("No buffs found!").formatted(Formatting.GRAY)));
+ return;
+ }
+
+ //Filter out text unrelated to blessings
+ lines = Arrays.stream(lines).filter(s -> s.contains("Blessing")).toArray(String[]::new);
+
+ //Alphabetically sort the blessings
+ Arrays.sort(lines, Comparator.comparing(String::toLowerCase));
+
+ for (String line : lines) {
+ if (line.length() < 3) { // empty line is §s
+ break;
+ }
+ int color = getBlessingColor(line);
+ this.addComponent(new PlainTextComponent(Text.literal(line).styled(style -> style.withColor(color))));
+ }
+
+ }
+
+ @SuppressWarnings("DataFlowIssue")
+ public int getBlessingColor(String blessing) {
+ if (blessing.contains("Life")) return Formatting.LIGHT_PURPLE.getColorValue();
+ if (blessing.contains("Power")) return Formatting.RED.getColorValue();
+ if (blessing.contains("Stone")) return Formatting.GREEN.getColorValue();
+ if (blessing.contains("Time")) return 0xafb8c1;
+ if (blessing.contains("Wisdom")) return Formatting.AQUA.getColorValue();
+
+ return 0xffffff;
+ }
+
+} \ No newline at end of file
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
new file mode 100644
index 00000000..9c299210
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDeathWidget.java
@@ -0,0 +1,47 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows various dungeon info
+// deaths, healing, dmg taken, milestones
+
+public class DungeonDeathWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Death").formatted(Formatting.DARK_PURPLE,
+ Formatting.BOLD);
+
+ // match the deaths entry
+ // group 1: amount of deaths
+ private static final Pattern DEATH_PATTERN = Pattern.compile("Team Deaths: (?<deathnum>\\d+).*");
+
+ public DungeonDeathWidget() {
+ super(TITLE, Formatting.DARK_PURPLE.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ Matcher m = PlayerListMgr.regexAt(25, DEATH_PATTERN);
+ if (m == null) {
+ this.addComponent(new IcoTextComponent());
+ } else {
+ Formatting f = (m.group("deathnum").equals("0")) ? Formatting.GREEN : Formatting.RED;
+ Text d = Widget.simpleEntryText(m.group("deathnum"), "Deaths: ", f);
+ IcoTextComponent deaths = new IcoTextComponent(Ico.SKULL, d);
+ this.addComponent(deaths);
+ }
+
+ this.addSimpleIcoText(Ico.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/DungeonDownedWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDownedWidget.java
new file mode 100644
index 00000000..9a8de0eb
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDownedWidget.java
@@ -0,0 +1,44 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about... something?
+// related to downed people in dungeons, not sure what this is supposed to show
+
+public class DungeonDownedWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Downed").formatted(Formatting.DARK_PURPLE,
+ Formatting.BOLD);
+
+ public DungeonDownedWidget() {
+ super(TITLE, Formatting.DARK_PURPLE.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ String down = PlayerListMgr.strAt(21);
+ if (down == null) {
+ this.addComponent(new IcoTextComponent());
+ } else {
+
+ Formatting format = Formatting.RED;
+ if (down.endsWith("NONE")) {
+ format = Formatting.GRAY;
+ }
+ int idx = down.indexOf(": ");
+ Text downed = (idx == -1) ? null
+ : Widget.simpleEntryText(down.substring(idx + 2), "Downed: ", format);
+ IcoTextComponent d = new IcoTextComponent(Ico.SKULL, downed);
+ this.addComponent(d);
+ }
+
+ this.addSimpleIcoText(Ico.CLOCK, "Time:", Formatting.GRAY, 22);
+ this.addSimpleIcoText(Ico.POTION, "Revive:", Formatting.GRAY, 23);
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonPlayerWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonPlayerWidget.java
new file mode 100644
index 00000000..be1a3c6e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonPlayerWidget.java
@@ -0,0 +1,103 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import net.minecraft.item.ItemStack;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about a player in the current dungeon group
+
+public class DungeonPlayerWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Player").formatted(Formatting.DARK_PURPLE,
+ Formatting.BOLD);
+
+ // match a player entry
+ // group 1: name
+ // group 2: class (or literal "EMPTY" pre dungeon start)
+ // group 3: level (or nothing, if pre dungeon start)
+ // this regex filters out the ironman icon as well as rank prefixes and emblems
+ // \[\d*\] (?:\[[A-Za-z]+\] )?(?<name>[A-Za-z0-9_]*) (?:.* )?\((?<class>\S*) ?(?<level>[LXVI]*)\)
+ private static final Pattern PLAYER_PATTERN = Pattern
+ .compile("\\[\\d*\\] (?:\\[[A-Za-z]+\\] )?(?<name>[A-Za-z0-9_]*) (?:.* )?\\((?<class>\\S*) ?(?<level>[LXVI]*)\\)");
+
+ private static final HashMap<String, ItemStack> ICOS = new HashMap<>();
+ private static final ArrayList<String> MSGS = new ArrayList<>();
+ static {
+ ICOS.put("Tank", Ico.CHESTPLATE);
+ ICOS.put("Mage", Ico.B_ROD);
+ ICOS.put("Berserk", Ico.DIASWORD);
+ ICOS.put("Archer", Ico.BOW);
+ ICOS.put("Healer", Ico.POTION);
+
+ MSGS.add("PRESS A TO JOIN");
+ MSGS.add("Invite a friend!");
+ MSGS.add("But nobody came.");
+ MSGS.add("More is better!");
+ }
+
+ private final int player;
+
+ // title needs to be changeable here
+ public DungeonPlayerWidget(int player) {
+ super(TITLE, Formatting.DARK_PURPLE.getColorValue());
+ this.player = player;
+ }
+
+ @Override
+ public void updateContent() {
+ int start = 1 + (player - 1) * 4;
+
+ if (PlayerListMgr.strAt(start) == null) {
+ int idx = player - 2;
+ IcoTextComponent noplayer = new IcoTextComponent(Ico.SIGN,
+ Text.literal(MSGS.get(idx)).formatted(Formatting.GRAY));
+ this.addComponent(noplayer);
+ return;
+ }
+ Matcher m = PlayerListMgr.regexAt(start, PLAYER_PATTERN);
+ if (m == null) {
+ this.addComponent(new IcoTextComponent());
+ this.addComponent(new IcoTextComponent());
+ } else {
+
+ Text name = Text.literal("Name: ").append(Text.literal(m.group("name")).formatted(Formatting.YELLOW));
+ this.addComponent(new IcoTextComponent(Ico.PLAYER, name));
+
+ String cl = m.group("class");
+ String level = m.group("level");
+
+ if (level == null) {
+ PlainTextComponent ptc = new PlainTextComponent(
+ Text.literal("Player is dead").formatted(Formatting.RED));
+ this.addComponent(ptc);
+ } else {
+
+ Formatting clf = Formatting.GRAY;
+ ItemStack cli = Ico.BARRIER;
+ if (!cl.equals("EMPTY")) {
+ cli = ICOS.get(cl);
+ clf = Formatting.LIGHT_PURPLE;
+ cl += " " + m.group("level");
+ }
+
+ Text clazz = Text.literal("Class: ").append(Text.literal(cl).formatted(clf));
+ IcoTextComponent itclass = new IcoTextComponent(cli, clazz);
+ this.addComponent(itclass);
+ }
+ }
+
+ this.addSimpleIcoText(Ico.CLOCK, "Ult Cooldown:", Formatting.GOLD, start + 1);
+ this.addSimpleIcoText(Ico.POTION, "Revives:", Formatting.DARK_PURPLE, start + 2);
+
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonPuzzleWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonPuzzleWidget.java
new file mode 100644
index 00000000..1b3b8644
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonPuzzleWidget.java
@@ -0,0 +1,57 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about all puzzeles in the dungeon (name and status)
+
+public class DungeonPuzzleWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Puzzles").formatted(Formatting.DARK_PURPLE,
+ Formatting.BOLD);
+
+ // match a puzzle entry
+ // group 1: name
+ // group 2: status
+ // " ?.*" to diescard the solver's name if present
+ // the teleport maze has a trailing whitespace that messes with the regex
+ private static final Pattern PUZZLE_PATTERN = Pattern.compile("(?<name>.*): \\[(?<status>.*)\\] ?.*");
+
+ public DungeonPuzzleWidget() {
+ super(TITLE, Formatting.DARK_PURPLE.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ int pos = 48;
+
+ while (pos < 60) {
+ Matcher m = PlayerListMgr.regexAt(pos, PUZZLE_PATTERN);
+ if (m == null) {
+ break;
+ }
+ Text t = Text.literal(m.group("name") + ": ")
+ .append(Text.literal("[").formatted(Formatting.GRAY))
+ .append(m.group("status"))
+ .append(Text.literal("]").formatted(Formatting.GRAY));
+ IcoTextComponent itc = new IcoTextComponent(Ico.SIGN, t);
+ this.addComponent(itc);
+ pos++;
+ // code points for puzzle status chars unsolved and solved: 10022, 10004
+ // not sure which one is which
+ // still need to find out codepoint for the puzzle failed char
+ }
+ if (pos == 48) {
+ this.addComponent(
+ new IcoTextComponent(Ico.BARRIER, Text.literal("No puzzles!").formatted(Formatting.GRAY)));
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonSecretWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonSecretWidget.java
new file mode 100644
index 00000000..6f40f5a8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonSecretWidget.java
@@ -0,0 +1,26 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about the secrets of the dungeon
+
+public class DungeonSecretWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Discoveries").formatted(Formatting.DARK_PURPLE,
+ Formatting.BOLD);
+
+ public DungeonSecretWidget() {
+ super(TITLE, Formatting.DARK_PURPLE.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ this.addSimpleIcoText(Ico.CHEST, "Secrets:", Formatting.YELLOW, 31);
+ this.addSimpleIcoText(Ico.SKULL, "Crypts:", Formatting.YELLOW, 32);
+
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonServerWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonServerWidget.java
new file mode 100644
index 00000000..569987e8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonServerWidget.java
@@ -0,0 +1,48 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.ProgressComponent;
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows broad info about the current dungeon
+// opened/completed rooms, % of secrets found and time taken
+
+public class DungeonServerWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Dungeon Info").formatted(Formatting.DARK_PURPLE,
+ Formatting.BOLD);
+
+ // match the secrets text
+ // group 1: % of secrets found (without "%")
+ private static final Pattern SECRET_PATTERN = Pattern.compile("Secrets Found: (?<secnum>.*)%");
+
+ public DungeonServerWidget() {
+ super(TITLE, Formatting.DARK_PURPLE.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ this.addSimpleIcoText(Ico.NTAG, "Name:", Formatting.AQUA, 41);
+ this.addSimpleIcoText(Ico.SIGN, "Rooms Visited:", Formatting.DARK_PURPLE, 42);
+ this.addSimpleIcoText(Ico.SIGN, "Rooms Completed:", Formatting.LIGHT_PURPLE, 43);
+
+ Matcher m = PlayerListMgr.regexAt(44, SECRET_PATTERN);
+ if (m == null) {
+ this.addComponent(new ProgressComponent());
+ } else {
+ ProgressComponent scp = new ProgressComponent(Ico.CHEST, Text.of("Secrets found:"),
+ Float.parseFloat(m.group("secnum")),
+ Formatting.DARK_PURPLE.getColorValue());
+ this.addComponent(scp);
+ }
+
+ this.addSimpleIcoText(Ico.CLOCK, "Time:", Formatting.GOLD, 45);
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/EffectWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/EffectWidget.java
new file mode 100644
index 00000000..5ec3faf1
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/EffectWidget.java
@@ -0,0 +1,67 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoFatTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widgte shows, how many active effects you have.
+// it also shows one of those in detail.
+// the parsing is super suspect and should be replaced by some regexes sometime later
+
+public class EffectWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Effect Info").formatted(Formatting.DARK_PURPLE,
+ Formatting.BOLD);
+
+ public EffectWidget() {
+ super(TITLE, Formatting.DARK_PURPLE.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+
+ String footertext = PlayerListMgr.getFooter();
+
+ if (footertext == null || !footertext.contains("Active Effects")) {
+ this.addComponent(new IcoTextComponent());
+ return;
+
+ }
+
+ String[] lines = footertext.split("Active Effects")[1].split("\n");
+ if (lines.length < 2) {
+ this.addComponent(new IcoTextComponent());
+ return;
+ }
+
+ if (lines[1].startsWith("No")) {
+ Text txt = Text.literal("No effects active").formatted(Formatting.GRAY);
+ this.addComponent(new IcoTextComponent(Ico.POTION, txt));
+ } else if (lines[1].contains("God")) {
+ String timeleft = lines[1].split("! ")[1];
+ Text godpot = Text.literal("God potion!").formatted(Formatting.RED);
+ Text txttleft = Text.literal(timeleft).formatted(Formatting.LIGHT_PURPLE);
+ IcoFatTextComponent iftc = new IcoFatTextComponent(Ico.POTION, godpot, txttleft);
+ this.addComponent(iftc);
+ } else {
+ String number = lines[1].substring("You have ".length());
+ int idx = number.indexOf(' ');
+ if (idx == -1 || lines.length < 4) {
+ this.addComponent(new IcoFatTextComponent());
+ return;
+ }
+ number = number.substring(0, idx);
+ Text active = Text.literal("Active Effects: ")
+ .append(Text.literal(number).formatted(Formatting.YELLOW));
+
+ IcoFatTextComponent iftc = new IcoFatTextComponent(Ico.POTION, active,
+ Text.literal(lines[3]).formatted(Formatting.AQUA));
+ this.addComponent(iftc);
+ }
+ }
+
+}
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
new file mode 100644
index 00000000..ec935faf
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ElectionWidget.java
@@ -0,0 +1,104 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.ProgressComponent;
+import net.minecraft.item.ItemStack;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows the status or results of the current election
+
+public class ElectionWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Election Info").formatted(Formatting.YELLOW,
+ Formatting.BOLD);
+
+ private static final HashMap<String, ItemStack> MAYOR_DATA = new HashMap<>();
+
+ private static final Text EL_OVER = Text.literal("Election ")
+ .append(Text.literal("over!").formatted(Formatting.RED));
+
+ // pattern matching a candidate while people are voting
+ // group 1: name
+ // group 2: % of votes
+ private static final Pattern VOTE_PATTERN = Pattern.compile("(?<mayor>\\S*): \\|+ \\((?<pcnt>\\d*)%\\)");
+
+ static {
+ MAYOR_DATA.put("Aatrox", Ico.DIASWORD);
+ MAYOR_DATA.put("Cole", Ico.PICKAXE);
+ MAYOR_DATA.put("Diana", Ico.BONE);
+ MAYOR_DATA.put("Diaz", Ico.GOLD);
+ MAYOR_DATA.put("Finnegan", Ico.HOE);
+ MAYOR_DATA.put("Foxy", Ico.SUGAR);
+ MAYOR_DATA.put("Paul", Ico.COMPASS);
+ MAYOR_DATA.put("Scorpius", Ico.MOREGOLD);
+ MAYOR_DATA.put("Jerry", Ico.VILLAGER);
+ MAYOR_DATA.put("Derpy", Ico.DBUSH);
+ MAYOR_DATA.put("Marina", Ico.FISH_ROD);
+ }
+
+ private static final Formatting[] COLS = { Formatting.GOLD, Formatting.RED, Formatting.LIGHT_PURPLE };
+
+ public ElectionWidget() {
+ super(TITLE, Formatting.YELLOW.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ String status = PlayerListMgr.strAt(76);
+ if (status == null) {
+ this.addComponent(new IcoTextComponent());
+ this.addComponent(new IcoTextComponent());
+ this.addComponent(new IcoTextComponent());
+ this.addComponent(new IcoTextComponent());
+ return;
+ }
+
+ if (status.contains("Over!")) {
+ // election is over
+ IcoTextComponent over = new IcoTextComponent(Ico.BARRIER, EL_OVER);
+ this.addComponent(over);
+
+ String win = PlayerListMgr.strAt(77);
+ if (win == null || !win.contains(": ")) {
+ this.addComponent(new IcoTextComponent());
+ } else {
+ String winnername = win.split(": ")[1];
+ Text winnertext = Widget.simpleEntryText(winnername, "Winner: ", Formatting.GREEN);
+ IcoTextComponent winner = new IcoTextComponent(MAYOR_DATA.get(winnername), winnertext);
+ this.addComponent(winner);
+ }
+
+ this.addSimpleIcoText(Ico.PLAYER, "Participants:", Formatting.AQUA, 78);
+ this.addSimpleIcoText(Ico.SIGN, "Year:", Formatting.LIGHT_PURPLE, 79);
+
+ } else {
+ // election is going on
+ this.addSimpleIcoText(Ico.CLOCK, "End in:", Formatting.GOLD, 76);
+
+ for (int i = 77; i <= 79; i++) {
+ Matcher m = PlayerListMgr.regexAt(i, VOTE_PATTERN);
+ if (m == null) {
+ this.addComponent(new ProgressComponent());
+ } else {
+
+ String mayorname = m.group("mayor");
+ String pcntstr = m.group("pcnt");
+ float pcnt = Float.parseFloat(pcntstr);
+ Text candidate = Text.literal(mayorname).formatted(COLS[i - 77]);
+ ProgressComponent pc = new ProgressComponent(MAYOR_DATA.get(mayorname), candidate, pcnt,
+ COLS[i - 77].getColorValue());
+ this.addComponent(pc);
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ErrorWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ErrorWidget.java
new file mode 100644
index 00000000..85019dbf
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ErrorWidget.java
@@ -0,0 +1,32 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// empty widget for when nothing can be shown
+
+public class ErrorWidget extends Widget {
+ private static final MutableText TITLE = Text.literal("Error").formatted(Formatting.RED,
+ Formatting.BOLD);
+
+ Text error = Text.of("No info available!");
+
+ public ErrorWidget() {
+ super(TITLE, Formatting.RED.getColorValue());
+ }
+
+ public ErrorWidget(String error) {
+ super(TITLE, Formatting.RED.getColorValue());
+ this.error = Text.of(error);
+ }
+
+ @Override
+ public void updateContent() {
+ PlainTextComponent inf = new PlainTextComponent(this.error);
+ this.addComponent(inf);
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/EssenceWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/EssenceWidget.java
new file mode 100644
index 00000000..d171b753
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/EssenceWidget.java
@@ -0,0 +1,47 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.TableComponent;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows your dungeon essences (dungeon hub only)
+
+public class EssenceWidget extends Widget {
+
+ private Text undead, wither, diamond, gold, dragon, spider, ice, crimson;
+
+ private static final MutableText TITLE = Text.literal("Essences").formatted(Formatting.DARK_AQUA,
+ Formatting.BOLD);
+
+ public EssenceWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ wither = Widget.simpleEntryText(46, "Wither:", Formatting.DARK_PURPLE);
+ spider = Widget.simpleEntryText(47, "Spider:", Formatting.DARK_PURPLE);
+ undead = Widget.simpleEntryText(48, "Undead:", Formatting.DARK_PURPLE);
+ dragon = Widget.simpleEntryText(49, "Dragon:", Formatting.DARK_PURPLE);
+ gold = Widget.simpleEntryText(50, "Gold:", Formatting.DARK_PURPLE);
+ diamond = Widget.simpleEntryText(51, "Diamond:", Formatting.DARK_PURPLE);
+ ice = Widget.simpleEntryText(52, "Ice:", Formatting.DARK_PURPLE);
+ crimson = Widget.simpleEntryText(53, "Crimson:", Formatting.DARK_PURPLE);
+
+ TableComponent tc = new TableComponent(2, 4, Formatting.DARK_AQUA.getColorValue());
+
+ tc.addToCell(0, 0, new IcoTextComponent(Ico.WITHER, wither));
+ tc.addToCell(0, 1, new IcoTextComponent(Ico.STRING, spider));
+ tc.addToCell(0, 2, new IcoTextComponent(Ico.FLESH, undead));
+ tc.addToCell(0, 3, new IcoTextComponent(Ico.DRAGON, dragon));
+ tc.addToCell(1, 0, new IcoTextComponent(Ico.GOLD, gold));
+ tc.addToCell(1, 1, new IcoTextComponent(Ico.DIAMOND, diamond));
+ tc.addToCell(1, 2, new IcoTextComponent(Ico.ICE, ice));
+ tc.addToCell(1, 3, new IcoTextComponent(Ico.REDSTONE, crimson));
+ this.addComponent(tc);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/EventWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/EventWidget.java
new file mode 100644
index 00000000..5a1e4239
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/EventWidget.java
@@ -0,0 +1,35 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about ongoing events (e.g. election)
+
+public class EventWidget extends Widget {
+ private static final MutableText TITLE = Text.literal("Event Info").formatted(Formatting.YELLOW, Formatting.BOLD);
+
+ private final boolean isInGarden;
+
+ public EventWidget(boolean isInGarden) {
+ super(TITLE, Formatting.YELLOW.getColorValue());
+ this.isInGarden = isInGarden;
+ }
+
+ @Override
+ public void updateContent() {
+ // hypixel devs carefully inserting the most random edge cases #317:
+ // the event info is placed a bit differently when in the garden.
+ int offset = (isInGarden) ? -1 : 0;
+
+ this.addSimpleIcoText(Ico.NTAG, "Name:", Formatting.YELLOW, 73 + offset);
+
+ // this could look better
+ Text time = Widget.plainEntryText(74 + offset);
+ IcoTextComponent t = new IcoTextComponent(Ico.CLOCK, time);
+ this.addComponent(t);
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/FireSaleWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/FireSaleWidget.java
new file mode 100644
index 00000000..0211cbd6
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/FireSaleWidget.java
@@ -0,0 +1,68 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.ProgressComponent;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about fire sales when in the hub.
+// or not, if there isn't one going on
+
+public class FireSaleWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Fire Sale").formatted(Formatting.DARK_AQUA,
+ Formatting.BOLD);
+
+ // matches a fire sale item
+ // group 1: item name
+ // group 2: # items available
+ // group 3: # items available in total (1 digit + "k")
+ private static final Pattern FIRE_PATTERN = Pattern.compile("(?<item>.*): (?<avail>\\d*)/(?<total>[0-9.]*)k");
+
+ public FireSaleWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ String event = PlayerListMgr.strAt(46);
+
+ if (event == null) {
+ this.addComponent(new PlainTextComponent(Text.literal("No Fire Sale!").formatted(Formatting.GRAY)));
+ return;
+ }
+
+ if (event.contains("Starts In")) {
+ this.addSimpleIcoText(Ico.CLOCK, "Starts in:", Formatting.DARK_AQUA, 46);
+ return;
+ }
+
+ for (int i = 46;; i++) {
+ Matcher m = PlayerListMgr.regexAt( i, FIRE_PATTERN);
+ if (m == null) {
+ break;
+ }
+ String avail = m.group("avail");
+ Text itemTxt = Text.literal(m.group("item"));
+ float total = Float.parseFloat(m.group("total")) * 1000;
+ Text prgressTxt = Text.literal(String.format("%s/%.0f", avail, total));
+ float pcnt = (Float.parseFloat(avail) / (total)) * 100f;
+ ProgressComponent pc = new ProgressComponent(Ico.GOLD, itemTxt, prgressTxt, pcnt, pcntToCol(pcnt));
+ this.addComponent(pc);
+ }
+
+ }
+
+ private int pcntToCol(float pcnt) {
+ return MathHelper.hsvToRgb( pcnt / 300f, 0.9f, 0.9f);
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ForgeWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ForgeWidget.java
new file mode 100644
index 00000000..1a4683f5
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ForgeWidget.java
@@ -0,0 +1,81 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.Component;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoFatTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows what you're forging right now.
+// for locked slots, the unlock requirement is shown
+
+public class ForgeWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Forge Status").formatted(Formatting.DARK_AQUA,
+ Formatting.BOLD);
+
+ public ForgeWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ int forgestart = 54;
+ // why is it forges and not looms >:(
+ String pos = PlayerListMgr.strAt(53);
+ if (pos == null) {
+ this.addComponent(new IcoTextComponent());
+ return;
+ }
+
+ if (!pos.startsWith("Forges")) {
+ forgestart += 2;
+ }
+
+ for (int i = forgestart, slot = 1; i < forgestart + 5 && i < 60; i++, slot++) {
+ String fstr = PlayerListMgr.strAt(i);
+ if (fstr == null || fstr.length() < 3) {
+ if (i == forgestart) {
+ this.addComponent(new IcoTextComponent());
+ }
+ break;
+ }
+ Component c;
+ Text l1, l2;
+
+ switch (fstr.substring(3)) {
+ case "LOCKED" -> {
+ l1 = Text.literal("Locked").formatted(Formatting.RED);
+ l2 = switch (slot) {
+ case 3 -> Text.literal("Needs HotM 3").formatted(Formatting.GRAY);
+ case 4 -> Text.literal("Needs HotM 4").formatted(Formatting.GRAY);
+ case 5 -> Text.literal("Needs PotM 2").formatted(Formatting.GRAY);
+ default ->
+ Text.literal("This message should not appear").formatted(Formatting.RED, Formatting.BOLD);
+ };
+ c = new IcoFatTextComponent(Ico.BARRIER, l1, l2);
+ }
+ case "EMPTY" -> {
+ l1 = Text.literal("Empty").formatted(Formatting.GRAY);
+ c = new IcoTextComponent(Ico.FURNACE, l1);
+ }
+ default -> {
+ String[] parts = fstr.split(": ");
+ if (parts.length != 2) {
+ c = new IcoFatTextComponent();
+ } else {
+ l1 = Text.literal(parts[0].substring(3)).formatted(Formatting.YELLOW);
+ l2 = Text.literal("Done in: ").formatted(Formatting.GRAY).append(Text.literal(parts[1]).formatted(Formatting.WHITE));
+ c = new IcoFatTextComponent(Ico.FIRE, l1, l2);
+ }
+ }
+ }
+ this.addComponent(c);
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenServerWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenServerWidget.java
new file mode 100644
index 00000000..221f8b08
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenServerWidget.java
@@ -0,0 +1,54 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about the garden server
+
+public class GardenServerWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Server Info").formatted(Formatting.DARK_AQUA,
+ Formatting.BOLD);
+
+ // match the next visitor in the garden
+ // group 1: visitor name
+ private static final Pattern VISITOR_PATTERN = Pattern.compile("Next Visitor: (?<vis>.*)");
+
+ public GardenServerWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ this.addSimpleIcoText(Ico.MAP, "Area:", Formatting.DARK_AQUA, 41);
+ this.addSimpleIcoText(Ico.NTAG, "Server ID:", Formatting.GRAY, 42);
+ this.addSimpleIcoText(Ico.EMERALD, "Gems:", Formatting.GREEN, 43);
+ this.addSimpleIcoText(Ico.COPPER, "Copper:", Formatting.GOLD, 44);
+
+ Matcher m = PlayerListMgr.regexAt(45, VISITOR_PATTERN);
+ if (m == null ) {
+ this.addComponent(new IcoTextComponent());
+ return;
+ }
+
+ String vis = m.group("vis");
+ Formatting col;
+ if (vis.equals("Not Unlocked!")) {
+ col = Formatting.RED;
+ } else {
+ col = Formatting.GREEN;
+ }
+ Text visitor = Widget.simpleEntryText(vis, "Next Visitor: ", col);
+ IcoTextComponent v = new IcoTextComponent(Ico.PLAYER, visitor);
+ this.addComponent(v);
+ }
+
+}
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
new file mode 100644
index 00000000..e7058fd6
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenSkillsWidget.java
@@ -0,0 +1,80 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.ProgressComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.TableComponent;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about your skills while in the garden
+
+public class GardenSkillsWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Skill Info").formatted(Formatting.YELLOW,
+ Formatting.BOLD);
+
+ // match the skill entry
+ // group 1: skill name and level
+ // group 2: progress to next level (without "%")
+ private static final Pattern SKILL_PATTERN = Pattern
+ .compile("\\S*: (?<skill>[A-Za-z]* [0-9]*): (?<progress>\\S*)%");
+ // same, but with leading space
+ private static final Pattern MS_PATTERN = Pattern.compile("\\S*: (?<skill>[A-Za-z]* [0-9]*): (?<progress>\\S*)%");
+
+ public GardenSkillsWidget() {
+ super(TITLE, Formatting.YELLOW.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ ProgressComponent pc;
+ Matcher m = PlayerListMgr.regexAt(66, SKILL_PATTERN);
+ if (m == null) {
+ pc = new ProgressComponent();
+ } else {
+
+ String strpcnt = m.group("progress");
+ String skill = m.group("skill");
+
+ float pcnt = Float.parseFloat(strpcnt);
+ pc = new ProgressComponent(Ico.LANTERN, Text.of(skill), pcnt,
+ Formatting.GOLD.getColorValue());
+ }
+
+ this.addComponent(pc);
+
+ 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);
+
+ TableComponent tc = new TableComponent(2, 1, Formatting.YELLOW.getColorValue());
+ tc.addToCell(0, 0, spd);
+ tc.addToCell(1, 0, ffo);
+ this.addComponent(tc);
+
+ ProgressComponent pc2;
+ m = PlayerListMgr.regexAt(69, MS_PATTERN);
+ if (m == null) {
+ pc2 = new ProgressComponent();
+ } else {
+ String strpcnt = m.group("progress");
+ String skill = m.group("skill");
+
+ float pcnt = Float.parseFloat(strpcnt);
+ pc2 = new ProgressComponent(Ico.MILESTONE, Text.of(skill), pcnt,
+ Formatting.GREEN.getColorValue());
+
+ }
+ this.addComponent(pc2);
+
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenVisitorsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenVisitorsWidget.java
new file mode 100644
index 00000000..cfbd6cd0
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenVisitorsWidget.java
@@ -0,0 +1,30 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+public class GardenVisitorsWidget extends Widget {
+ private static final MutableText TITLE = Text.literal("Visitors").formatted(Formatting.DARK_GREEN, Formatting.BOLD);
+
+ public GardenVisitorsWidget() {
+ super(TITLE, Formatting.DARK_GREEN.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ if (PlayerListMgr.textAt(54) == null) {
+ this.addComponent(new PlainTextComponent(Text.literal("No visitors!").formatted(Formatting.GRAY)));
+ return;
+ }
+
+ for (int i = 54; i < 59; i++) {
+ String text = PlayerListMgr.strAt(i);
+ if (text != null)
+ this.addComponent(new PlainTextComponent(Text.literal(text)));
+ }
+
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GuestServerWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GuestServerWidget.java
new file mode 100644
index 00000000..bbd97fb5
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GuestServerWidget.java
@@ -0,0 +1,30 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about the private island you're visiting
+
+public class GuestServerWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Island Info").formatted(Formatting.DARK_AQUA,
+ Formatting.BOLD);
+
+ public GuestServerWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ this.addSimpleIcoText(Ico.MAP, "Area:", Formatting.DARK_AQUA, 41);
+ this.addSimpleIcoText(Ico.NTAG, "Server ID:", Formatting.GRAY, 42);
+ this.addSimpleIcoText(Ico.SIGN, "Owner:", Formatting.GREEN, 43);
+ this.addSimpleIcoText(Ico.SIGN, "Status:", Formatting.BLUE, 44);
+
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandGuestsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandGuestsWidget.java
new file mode 100644
index 00000000..b527dc78
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandGuestsWidget.java
@@ -0,0 +1,47 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows a list of all people visiting the same private island as you
+
+public class IslandGuestsWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Guests").formatted(Formatting.AQUA,
+ Formatting.BOLD);
+
+ // matches a player entry, removing their level and the hand icon
+ // group 1: player name
+ private static final Pattern GUEST_PATTERN = Pattern.compile("\\[\\d*\\] (.*) \\[.\\]");
+
+ public IslandGuestsWidget() {
+ super(TITLE, Formatting.AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ for (int i = 21; i < 40; i++) {
+ String str = PlayerListMgr.strAt(i);
+ if (str == null) {
+ if (i == 21) {
+ this.addComponent(new PlainTextComponent(Text.literal("No Visitors!").formatted(Formatting.GRAY)));
+ }
+ break;
+ }
+ Matcher m = PlayerListMgr.regexAt( i, GUEST_PATTERN);
+ if (m == null) {
+ this.addComponent(new PlainTextComponent(Text.of("???")));
+ } else {
+ this.addComponent(new PlainTextComponent(Text.of(m.group(1))));
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandOwnersWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandOwnersWidget.java
new file mode 100644
index 00000000..cde1fa38
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandOwnersWidget.java
@@ -0,0 +1,66 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows a list of the owners of a home island while guesting
+
+public class IslandOwnersWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Owners").formatted(Formatting.DARK_PURPLE,
+ Formatting.BOLD);
+
+ // matches an owner
+ // group 1: player name
+ // group 2: last seen, if owner not online
+ // ^(?<nameA>.*) \((?<lastseen>.*)\)$|^\[\d*\] (?:\[[A-Za-z]+\] )?(?<nameB>[A-Za-z0-9_]*)(?: .*)?$|^(?<nameC>.*)$
+ private static final Pattern OWNER_PATTERN = Pattern
+ .compile("^(?<nameA>.*) \\((?<lastseen>.*)\\)$|^\\[\\d*\\] (?:\\[[A-Za-z]+\\] )?(?<nameB>[A-Za-z0-9_]*)(?: .*)?$|^(?<nameC>.*)$");
+
+ public IslandOwnersWidget() {
+ super(TITLE, Formatting.DARK_PURPLE.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+
+ for (int i = 1; i < 20; i++) {
+ Matcher m = PlayerListMgr.regexAt(i, OWNER_PATTERN);
+ if (m == null) {
+ break;
+ }
+
+ String name, lastseen;
+ Formatting format;
+ if (m.group("nameA") != null) {
+ name = m.group("nameA");
+ lastseen = m.group("lastseen");
+ format = Formatting.GRAY;
+ } else if (m.group("nameB")!=null){
+ name = m.group("nameB");
+ lastseen = "Online";
+ format = Formatting.WHITE;
+ } else {
+ name = m.group("nameC");
+ lastseen = "Online";
+ format = Formatting.WHITE;
+ }
+
+ Text entry = Text.literal(name)
+ .append(
+ Text.literal(" (" + lastseen + ")")
+ .formatted(format));
+ PlainTextComponent ptc = new PlainTextComponent(entry);
+ this.addComponent(ptc);
+ }
+
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandSelfWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandSelfWidget.java
new file mode 100644
index 00000000..31ad66f7
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandSelfWidget.java
@@ -0,0 +1,43 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows a list of the owners while on your home island
+
+public class IslandSelfWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Owners").formatted(Formatting.DARK_PURPLE,
+ Formatting.BOLD);
+
+ // matches an owner
+ // group 1: player name, optionally offline time
+ // ^\[\d*\] (?:\[[A-Za-z]+\] )?([A-Za-z0-9_() ]*)(?: .*)?$|^(.*)$
+ private static final Pattern OWNER_PATTERN = Pattern
+ .compile("^\\[\\d*\\] (?:\\[[A-Za-z]+\\] )?([A-Za-z0-9_() ]*)(?: .*)?$|^(.*)$");
+
+ public IslandSelfWidget() {
+ super(TITLE, Formatting.DARK_PURPLE.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ for (int i = 1; i < 20; i++) {
+ Matcher m = PlayerListMgr.regexAt(i, OWNER_PATTERN);
+ if (m == null) {
+ break;
+ }
+
+ Text entry = (m.group(1) != null) ? Text.of(m.group(1)) : Text.of(m.group(2));
+ this.addComponent(new PlainTextComponent(entry));
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandServerWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandServerWidget.java
new file mode 100644
index 00000000..53dc11a6
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/IslandServerWidget.java
@@ -0,0 +1,32 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about your home island
+
+public class IslandServerWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Island Info").formatted(Formatting.DARK_AQUA,
+ Formatting.BOLD);
+
+ public IslandServerWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ this.addSimpleIcoText(Ico.MAP, "Area:", Formatting.DARK_AQUA, 41);
+ this.addSimpleIcoText(Ico.NTAG, "Server ID:", Formatting.GRAY, 42);
+ this.addSimpleIcoText(Ico.EMERALD, "Crystals:", Formatting.DARK_PURPLE, 43);
+ this.addSimpleIcoText(Ico.CHEST, "Stash:", Formatting.GREEN, 44);
+ this.addSimpleIcoText(Ico.COMMAND, "Minions:", Formatting.BLUE, 45);
+
+
+ }
+
+}
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
new file mode 100644
index 00000000..5ae0bd3d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/JacobsContestWidget.java
@@ -0,0 +1,62 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.HashMap;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.TableComponent;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about the current jacob's contest (garden only)
+
+public class JacobsContestWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Jacob's Contest").formatted(Formatting.YELLOW,
+ Formatting.BOLD);
+
+ private static final HashMap<String, ItemStack> FARM_DATA = new HashMap<>();
+
+ // again, there HAS to be a better way to do this
+ static {
+ FARM_DATA.put("Wheat", new ItemStack(Items.WHEAT));
+ FARM_DATA.put("Sugar Cane", new ItemStack(Items.SUGAR_CANE));
+ FARM_DATA.put("Carrot", new ItemStack(Items.CARROT));
+ FARM_DATA.put("Potato", new ItemStack(Items.POTATO));
+ FARM_DATA.put("Melon", new ItemStack(Items.MELON_SLICE));
+ FARM_DATA.put("Pumpkin", new ItemStack(Items.PUMPKIN));
+ FARM_DATA.put("Cocoa Beans", new ItemStack(Items.COCOA_BEANS));
+ FARM_DATA.put("Nether Wart", new ItemStack(Items.NETHER_WART));
+ FARM_DATA.put("Cactus", new ItemStack(Items.CACTUS));
+ FARM_DATA.put("Mushroom", new ItemStack(Items.RED_MUSHROOM));
+ }
+
+ public JacobsContestWidget() {
+ super(TITLE, Formatting.YELLOW.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ this.addSimpleIcoText(Ico.CLOCK, "Starts in:", Formatting.GOLD, 76);
+
+ TableComponent tc = new TableComponent(1, 3, Formatting.YELLOW .getColorValue());
+
+ for (int i = 77; i < 80; i++) {
+ String item = PlayerListMgr.strAt(i);
+ IcoTextComponent itc;
+ if (item == null) {
+ itc = new IcoTextComponent();
+ } else {
+ itc = new IcoTextComponent(FARM_DATA.get(item), Text.of(item));
+ }
+ tc.addToCell(0, i - 77, itc);
+ }
+ this.addComponent(tc);
+
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/MinionWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/MinionWidget.java
new file mode 100644
index 00000000..35b9a0c6
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/MinionWidget.java
@@ -0,0 +1,151 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about minions placed on the home island
+
+public class MinionWidget extends Widget {
+ private static final MutableText TITLE = Text.literal("Minions").formatted(Formatting.DARK_AQUA,
+ Formatting.BOLD);
+
+ private static final HashMap<String, ItemStack> MIN_ICOS = new HashMap<>();
+
+ // hmm...
+ static {
+ MIN_ICOS.put("Blaze", new ItemStack(Items.BLAZE_ROD));
+ MIN_ICOS.put("Cave Spider", new ItemStack(Items.SPIDER_EYE));
+ MIN_ICOS.put("Creeper", new ItemStack(Items.GUNPOWDER));
+ MIN_ICOS.put("Enderman", new ItemStack(Items.ENDER_PEARL));
+ MIN_ICOS.put("Ghast", new ItemStack(Items.GHAST_TEAR));
+ MIN_ICOS.put("Magma Cube", new ItemStack(Items.MAGMA_CREAM));
+ MIN_ICOS.put("Skeleton", new ItemStack(Items.BONE));
+ MIN_ICOS.put("Slime", new ItemStack(Items.SLIME_BALL));
+ MIN_ICOS.put("Spider", new ItemStack(Items.STRING));
+ MIN_ICOS.put("Zombie", new ItemStack(Items.ROTTEN_FLESH));
+ MIN_ICOS.put("Cactus", new ItemStack(Items.CACTUS));
+ MIN_ICOS.put("Carrot", new ItemStack(Items.CARROT));
+ MIN_ICOS.put("Chicken", new ItemStack(Items.CHICKEN));
+ MIN_ICOS.put("Cocoa Beans", new ItemStack(Items.COCOA_BEANS));
+ MIN_ICOS.put("Cow", new ItemStack(Items.BEEF));
+ MIN_ICOS.put("Melon", new ItemStack(Items.MELON_SLICE));
+ MIN_ICOS.put("Mushroom", new ItemStack(Items.RED_MUSHROOM));
+ MIN_ICOS.put("Nether Wart", new ItemStack(Items.NETHER_WART));
+ MIN_ICOS.put("Pig", new ItemStack(Items.PORKCHOP));
+ MIN_ICOS.put("Potato", new ItemStack(Items.POTATO));
+ MIN_ICOS.put("Pumpkin", new ItemStack(Items.PUMPKIN));
+ MIN_ICOS.put("Rabbit", new ItemStack(Items.RABBIT));
+ MIN_ICOS.put("Sheep", new ItemStack(Items.WHITE_WOOL));
+ MIN_ICOS.put("Sugar Cane", new ItemStack(Items.SUGAR_CANE));
+ MIN_ICOS.put("Wheat", new ItemStack(Items.WHEAT));
+ MIN_ICOS.put("Clay", new ItemStack(Items.CLAY));
+ MIN_ICOS.put("Fishing", new ItemStack(Items.FISHING_ROD));
+ MIN_ICOS.put("Coal", new ItemStack(Items.COAL));
+ MIN_ICOS.put("Cobblestone", new ItemStack(Items.COBBLESTONE));
+ MIN_ICOS.put("Diamond", new ItemStack(Items.DIAMOND));
+ MIN_ICOS.put("Emerald", new ItemStack(Items.EMERALD));
+ MIN_ICOS.put("End Stone", new ItemStack(Items.END_STONE));
+ MIN_ICOS.put("Glowstone", new ItemStack(Items.GLOWSTONE_DUST));
+ MIN_ICOS.put("Gold", new ItemStack(Items.GOLD_INGOT));
+ MIN_ICOS.put("Gravel", new ItemStack(Items.GRAVEL));
+ MIN_ICOS.put("Hard Stone", new ItemStack(Items.STONE));
+ MIN_ICOS.put("Ice", new ItemStack(Items.ICE));
+ MIN_ICOS.put("Iron", new ItemStack(Items.IRON_INGOT));
+ MIN_ICOS.put("Lapis", new ItemStack(Items.LAPIS_LAZULI));
+ MIN_ICOS.put("Mithril", new ItemStack(Items.PRISMARINE_CRYSTALS));
+ MIN_ICOS.put("Mycelium", new ItemStack(Items.MYCELIUM));
+ MIN_ICOS.put("Obsidian", new ItemStack(Items.OBSIDIAN));
+ MIN_ICOS.put("Quartz", new ItemStack(Items.QUARTZ));
+ MIN_ICOS.put("Red Sand", new ItemStack(Items.RED_SAND));
+ MIN_ICOS.put("Redstone", new ItemStack(Items.REDSTONE));
+ MIN_ICOS.put("Sand", new ItemStack(Items.SAND));
+ MIN_ICOS.put("Snow", new ItemStack(Items.SNOWBALL));
+ MIN_ICOS.put("Inferno", new ItemStack(Items.BLAZE_SPAWN_EGG));
+ MIN_ICOS.put("Revenant", new ItemStack(Items.ZOMBIE_SPAWN_EGG));
+ MIN_ICOS.put("Tarantula", new ItemStack(Items.SPIDER_SPAWN_EGG));
+ MIN_ICOS.put("Vampire", new ItemStack(Items.REDSTONE));
+ MIN_ICOS.put("Voidling", new ItemStack(Items.ENDERMAN_SPAWN_EGG));
+ MIN_ICOS.put("Acacia", new ItemStack(Items.ACACIA_LOG));
+ MIN_ICOS.put("Birch", new ItemStack(Items.BIRCH_LOG));
+ MIN_ICOS.put("Dark Oak", new ItemStack(Items.DARK_OAK_LOG));
+ MIN_ICOS.put("Flower", new ItemStack(Items.POPPY));
+ MIN_ICOS.put("Jungle", new ItemStack(Items.JUNGLE_LOG));
+ MIN_ICOS.put("Oak", new ItemStack(Items.OAK_LOG));
+ MIN_ICOS.put("Spruce", new ItemStack(Items.SPRUCE_LOG));
+ }
+
+ // matches a minion entry
+ // group 1: name
+ // group 2: level
+ // group 3: status
+ public static final Pattern MINION_PATTERN = Pattern.compile("(?<name>.*) (?<level>[XVI]*) \\[(?<status>.*)\\]");
+
+ public MinionWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+
+ // this looks a bit weird because if we used regex mismatch as a stop condition,
+ // it'd spam the chat.
+ // not sure if not having that debug output is worth the cleaner solution here...
+
+ for (int i = 48; i < 59; i++) {
+ if (!this.addMinionComponent(i)) {
+ break;
+ }
+ }
+
+ // if more minions are placed than the tab menu can display,
+ // a "And X more..." text is shown
+ // look for that and add it to the widget
+ String more = PlayerListMgr.strAt(59);
+ if (more == null) {
+ return;
+ } else if (more.startsWith("And ")) {
+ this.addComponent(new PlainTextComponent(Text.of(more)));
+ } else {
+ this.addMinionComponent(59);
+ }
+ }
+
+ public boolean addMinionComponent(int i) {
+ Matcher m = PlayerListMgr.regexAt(i, MINION_PATTERN);
+ if (m != null) {
+
+ String min = m.group("name");
+ String lvl = m.group("level");
+ String stat = m.group("status");
+
+ MutableText mt = Text.literal(min + " " + lvl).append(Text.literal(": "));
+
+ Formatting format = Formatting.RED;
+ if (stat.equals("ACTIVE")) {
+ format = Formatting.GREEN;
+ } else if (stat.equals("SLOW")) {
+ format = Formatting.YELLOW;
+ }
+ // makes "BLOCKED" also red. in reality, it's some kind of crimson
+ mt.append(Text.literal(stat).formatted(format));
+
+ IcoTextComponent itc = new IcoTextComponent(MIN_ICOS.get(min), mt);
+ this.addComponent(itc);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ParkServerWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ParkServerWidget.java
new file mode 100644
index 00000000..c781a1bc
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ParkServerWidget.java
@@ -0,0 +1,30 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about the park server
+
+public class ParkServerWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Server Info").formatted(Formatting.DARK_AQUA,
+ Formatting.BOLD);
+
+ public ParkServerWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ this.addSimpleIcoText(Ico.MAP, "Area:", Formatting.DARK_AQUA, 41);
+ this.addSimpleIcoText(Ico.NTAG, "Server ID:", Formatting.GRAY, 42);
+ this.addSimpleIcoText(Ico.EMERALD, "Gems:", Formatting.GREEN, 43);
+ this.addSimpleIcoText(Ico.WATER, "Rain:", Formatting.BLUE, 44);
+
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/PlayerListWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/PlayerListWidget.java
new file mode 100644
index 00000000..ba178a5e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/PlayerListWidget.java
@@ -0,0 +1,71 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlayerComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.TableComponent;
+import net.minecraft.client.network.PlayerListEntry;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+
+// this widget shows a list of players with their skins.
+// responsible for non-private-island areas
+
+public class PlayerListWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Players").formatted(Formatting.GREEN,
+ Formatting.BOLD);
+
+ public PlayerListWidget() {
+ super(TITLE, Formatting.GREEN.getColorValue());
+
+ }
+
+ @Override
+ public void updateContent() {
+ ArrayList<PlayerListEntry> list = new ArrayList<>();
+
+ // hard cap to 4x20 entries.
+ // 5x20 is too wide (and not possible in theory. in reality however...)
+ int listlen = Math.min(PlayerListMgr.getSize(), 160);
+
+ // list isn't fully loaded, so our hack won't work...
+ if (listlen < 80) {
+ this.addComponent(new PlainTextComponent(Text.literal("List loading...").formatted(Formatting.GRAY)));
+ return;
+ }
+
+ // unintuitive int ceil division stolen from
+ // https://stackoverflow.com/questions/7139382/java-rounding-up-to-an-int-using-math-ceil#21830188
+ int tblW = ((listlen - 80) - 1) / 20 + 1;
+
+ TableComponent tc = new TableComponent(tblW, Math.min(listlen - 80, 20), Formatting.GREEN.getColorValue());
+
+ for (int i = 80; i < listlen; i++) {
+ list.add(PlayerListMgr.getRaw(i));
+ }
+
+ if (SkyblockerConfigManager.get().general.tabHud.nameSorting == SkyblockerConfig.NameSorting.ALPHABETICAL) {
+ list.sort(Comparator.comparing(o -> o.getProfile().getName().toLowerCase()));
+ }
+
+ int x = 0, y = 0;
+
+ for (PlayerListEntry ple : list) {
+ tc.addToCell(x, y, new PlayerComponent(ple));
+ y++;
+ if (y >= 20) {
+ y = 0;
+ x++;
+ }
+ }
+
+ this.addComponent(tc);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/PowderWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/PowderWidget.java
new file mode 100644
index 00000000..44635fbe
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/PowderWidget.java
@@ -0,0 +1,29 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows how much mithril and gemstone powder you have
+// (dwarven mines and crystal hollows)
+
+public class PowderWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Powders").formatted(Formatting.DARK_AQUA,
+ Formatting.BOLD);
+
+ public PowderWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ this.addSimpleIcoText(Ico.MITHRIL, "Mithril:", Formatting.AQUA, 46);
+ this.addSimpleIcoText(Ico.EMERALD, "Gemstone:", Formatting.DARK_PURPLE, 47);
+
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ProfileWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ProfileWidget.java
new file mode 100644
index 00000000..de2ea0c6
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ProfileWidget.java
@@ -0,0 +1,28 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about your profile and bank
+
+public class ProfileWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Profile").formatted(Formatting.YELLOW, Formatting.BOLD);
+
+ public ProfileWidget() {
+ super(TITLE, Formatting.YELLOW.getColorValue());
+
+ }
+
+ @Override
+ public void updateContent() {
+ this.addSimpleIcoText(Ico.SIGN, "Profile:", Formatting.GREEN, 61);
+ this.addSimpleIcoText(Ico.BONE, "Pet Sitter:", Formatting.AQUA, 62);
+ this.addSimpleIcoText(Ico.EMERALD, "Balance:", Formatting.GOLD, 63);
+ this.addSimpleIcoText(Ico.CLOCK, "Interest in:", Formatting.GOLD, 64);
+
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/QuestWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/QuestWidget.java
new file mode 100644
index 00000000..3c3d3c92
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/QuestWidget.java
@@ -0,0 +1,33 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows your crimson isle faction quests
+
+public class QuestWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Faction Quests").formatted(Formatting.AQUA,
+ Formatting.BOLD);
+
+ public QuestWidget() {
+ super(TITLE, Formatting.AQUA.getColorValue());
+
+ }
+
+ @Override
+ public void updateContent() {
+ for (int i = 51; i < 56; i++) {
+ Text q = PlayerListMgr.textAt(i);
+ IcoTextComponent itc = new IcoTextComponent(Ico.BOOK, q);
+ this.addComponent(itc);
+ }
+
+ }
+
+}
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
new file mode 100644
index 00000000..3c218fb1
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ReputationWidget.java
@@ -0,0 +1,69 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.ProgressComponent;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows your faction status (crimson isle)
+
+public class ReputationWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Faction Status").formatted(Formatting.AQUA,
+ Formatting.BOLD);
+
+ // matches your faction alignment progress
+ // group 1: percentage to next alignment level
+ private static final Pattern PROGRESS_PATTERN = Pattern.compile("\\|+ \\((?<prog>[0-9.]*)%\\)");
+
+ // matches alignment level names
+ // group 1: left level name
+ // group 2: right level name
+ private static final Pattern STATE_PATTERN = Pattern.compile("(?<from>\\S*) *(?<to>\\S*)");
+
+ public ReputationWidget() {
+ super(TITLE, Formatting.AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ String fracstr = PlayerListMgr.strAt(45);
+
+ int spaceidx;
+ IcoTextComponent faction;
+ if (fracstr == null || (spaceidx = fracstr.indexOf(' ')) == -1) {
+ faction = new IcoTextComponent();
+ } else {
+ String fname = fracstr.substring(0, spaceidx);
+ 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));
+ }
+ }
+ this.addComponent(faction);
+
+ Text rep = Widget.plainEntryText(46);
+ Matcher prog = PlayerListMgr.regexAt(47, PROGRESS_PATTERN);
+ Matcher state = PlayerListMgr.regexAt(48, STATE_PATTERN);
+
+ if (prog == null || state == null) {
+ this.addComponent(new ProgressComponent());
+ } else {
+ float pcnt = Float.parseFloat(prog.group("prog"));
+ Text reputationText = state.group("from").equals("Max") ? Text.literal("Max Reputation") : Text.literal(state.group("from") + " -> " + state.group("to"));
+ ProgressComponent pc = new ProgressComponent(Ico.LANTERN,
+ reputationText, rep, pcnt,
+ Formatting.AQUA.getColorValue());
+ this.addComponent(pc);
+ }
+
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ServerWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ServerWidget.java
new file mode 100644
index 00000000..475cb038
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ServerWidget.java
@@ -0,0 +1,30 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about "generic" servers.
+// a server is "generic", when only name, server ID and gems are shown
+// in the third column of the tab HUD
+
+public class ServerWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Server Info").formatted(Formatting.DARK_AQUA,
+ Formatting.BOLD);
+
+ public ServerWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ this.addSimpleIcoText(Ico.MAP, "Area:", Formatting.DARK_AQUA, 41);
+ this.addSimpleIcoText(Ico.NTAG, "Server ID:", Formatting.GRAY, 42);
+ this.addSimpleIcoText(Ico.EMERALD, "Gems:", Formatting.GREEN, 43);
+ }
+
+}
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
new file mode 100644
index 00000000..379fbb62
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/SkillsWidget.java
@@ -0,0 +1,78 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.Component;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoFatTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.ProgressComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.TableComponent;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about a skill and some stats,
+// as seen in the rightmost column of the default HUD
+
+public class SkillsWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Skill Info").formatted(Formatting.YELLOW,
+ Formatting.BOLD);
+
+ // match the skill entry
+ // group 1: skill name and level
+ // group 2: progress to next level (without "%")
+ private static final Pattern SKILL_PATTERN = Pattern.compile("\\S*: ([A-Za-z]* [0-9]*): ([0-9.MAX]*)%?");
+
+ public SkillsWidget() {
+ super(TITLE, Formatting.YELLOW.getColorValue());
+
+ }
+
+ @Override
+ public void updateContent() {
+ Matcher m = PlayerListMgr.regexAt(66, SKILL_PATTERN);
+ Component progress;
+ if (m == null) {
+ progress = new ProgressComponent();
+ } else {
+
+ String skill = m.group(1);
+ String pcntStr = m.group(2);
+
+ if (!pcntStr.equals("MAX")) {
+ float pcnt = Float.parseFloat(pcntStr);
+ progress = new ProgressComponent(Ico.LANTERN, Text.of(skill),
+ Text.of(pcntStr + "%"), pcnt, Formatting.GOLD.getColorValue());
+ } else {
+ progress = new IcoFatTextComponent(Ico.LANTERN, Text.of(skill),
+ Text.literal(pcntStr).formatted(Formatting.RED));
+ }
+ }
+
+ this.addComponent(progress);
+
+ 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);
+ Text critDmg = Widget.simpleEntryText(69, "CCH", Formatting.BLUE);
+ IcoTextComponent cdg = new IcoTextComponent(Ico.SWORD, critDmg);
+ Text critCh = Widget.simpleEntryText(70, "CDG", Formatting.BLUE);
+ IcoTextComponent cch = new IcoTextComponent(Ico.SWORD, critCh);
+ Text aSpeed = Widget.simpleEntryText(71, "ASP", Formatting.YELLOW);
+ IcoTextComponent asp = new IcoTextComponent(Ico.HOE, aSpeed);
+
+ TableComponent tc = new TableComponent(2, 3, Formatting.YELLOW.getColorValue());
+ tc.addToCell(0, 0, spd);
+ tc.addToCell(0, 1, str);
+ tc.addToCell(0, 2, asp);
+ tc.addToCell(1, 0, cdg);
+ tc.addToCell(1, 1, cch);
+ this.addComponent(tc);
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/TrapperWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/TrapperWidget.java
new file mode 100644
index 00000000..74bba632
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/TrapperWidget.java
@@ -0,0 +1,25 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows how meny pelts you have (farming island)
+
+public class TrapperWidget extends Widget {
+ private static final MutableText TITLE = Text.literal("Trapper").formatted(Formatting.DARK_AQUA,
+ Formatting.BOLD);
+
+ public TrapperWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+
+ }
+
+ @Override
+ public void updateContent() {
+ this.addSimpleIcoText(Ico.LEATHER, "Pelts:", Formatting.AQUA, 46);
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/UpgradeWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/UpgradeWidget.java
new file mode 100644
index 00000000..a245cbe9
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/UpgradeWidget.java
@@ -0,0 +1,51 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+// this widget shows info about ongoing profile/account upgrades
+// or not, if there aren't any
+// TODO: not very pretty atm
+
+public class UpgradeWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Upgrade Info").formatted(Formatting.GOLD,
+ Formatting.BOLD);
+
+ public UpgradeWidget() {
+ super(TITLE, Formatting.GOLD.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ String footertext = PlayerListMgr.getFooter();
+
+ if (footertext == null) {
+ this.addComponent(new PlainTextComponent(Text.literal("No data").formatted(Formatting.GRAY)));
+ return;
+ }
+
+ if (!footertext.contains("Upgrades")) {
+ this.addComponent(new PlainTextComponent(Text.of("Currently no upgrades...")));
+ return;
+ }
+
+ String interesting = footertext.split("Upgrades")[1];
+ String[] lines = interesting.split("\n");
+
+ for (int i = 1; i < lines.length; i++) {
+ if (lines[i].trim().length() < 3) { // empty line is §s
+ break;
+ }
+ IcoTextComponent itc = new IcoTextComponent(Ico.SIGN, Text.of(lines[i]));
+ this.addComponent(itc);
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/VolcanoWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/VolcanoWidget.java
new file mode 100644
index 00000000..8dacfb3a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/VolcanoWidget.java
@@ -0,0 +1,59 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.HashMap;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.Pair;
+
+// shows the volcano status (crimson isle)
+
+public class VolcanoWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Volcano Status").formatted(Formatting.AQUA,
+ Formatting.BOLD);
+
+ private static final HashMap<String, Pair<ItemStack, Formatting>> BOOM_TYPE = new HashMap<>();
+
+ static {
+ BOOM_TYPE.put("INACTIVE",
+ new Pair<>(new ItemStack(Items.BARRIER), Formatting.DARK_GRAY));
+ BOOM_TYPE.put("CHILL",
+ new Pair<>(new ItemStack(Items.ICE), Formatting.AQUA));
+ BOOM_TYPE.put("LOW",
+ new Pair<>(new ItemStack(Items.FLINT_AND_STEEL), Formatting.GRAY));
+ BOOM_TYPE.put("DISRUPTIVE",
+ new Pair<>(new ItemStack(Items.CAMPFIRE), Formatting.WHITE));
+ BOOM_TYPE.put("MEDIUM",
+ new Pair<>(new ItemStack(Items.LAVA_BUCKET), Formatting.YELLOW));
+ BOOM_TYPE.put("HIGH",
+ new Pair<>(new ItemStack(Items.FIRE_CHARGE), Formatting.GOLD));
+ BOOM_TYPE.put("EXPLOSIVE",
+ new Pair<>(new ItemStack(Items.TNT), Formatting.RED));
+ BOOM_TYPE.put("CATACLYSMIC",
+ new Pair<>(new ItemStack(Items.SKELETON_SKULL), Formatting.DARK_RED));
+ }
+
+ public VolcanoWidget() {
+ super(TITLE, Formatting.AQUA.getColorValue());
+
+ }
+
+ @Override
+ public void updateContent() {
+ String s = PlayerListMgr.strAt(58);
+ if (s == null) {
+ this.addComponent(new IcoTextComponent());
+ } else {
+ Pair<ItemStack, Formatting> p = BOOM_TYPE.get(s);
+ this.addComponent(new IcoTextComponent(p.getLeft(), Text.literal(s).formatted(p.getRight())));
+ }
+
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/Widget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/Widget.java
new file mode 100644
index 00000000..5f0d2c3c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/Widget.java
@@ -0,0 +1,216 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget;
+
+import java.util.ArrayList;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.Component;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import net.minecraft.client.MinecraftClient;
+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.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+/**
+ * Abstract base class for a Widget.
+ * Widgets are containers for components with a border and a title.
+ * Their size is dependent on the components inside,
+ * the position may be changed after construction.
+ */
+public abstract class Widget {
+
+ private final ArrayList<Component> components = new ArrayList<>();
+ private int w = 0, h = 0;
+ private int x = 0, y = 0;
+ private final int color;
+ private final Text title;
+
+ private static final TextRenderer txtRend = MinecraftClient.getInstance().textRenderer;
+
+ static final int BORDER_SZE_N = txtRend.fontHeight + 4;
+ static final int BORDER_SZE_S = 4;
+ static final int BORDER_SZE_W = 4;
+ static final int BORDER_SZE_E = 4;
+ static final int COL_BG_BOX = 0xc00c0c0c;
+
+ public Widget(MutableText title, Integer colorValue) {
+ this.title = title;
+ this.color = 0xff000000 | colorValue;
+ }
+
+ public final void addComponent(Component c) {
+ this.components.add(c);
+ }
+
+ public final void update() {
+ this.components.clear();
+ this.updateContent();
+ this.pack();
+ }
+
+ public abstract void updateContent();
+
+ /**
+ * Shorthand function for simple components.
+ * If the entry at idx has the format "<textA>: <textB>", an IcoTextComponent is
+ * added as such:
+ * [ico] [string] [textB.formatted(fmt)]
+ */
+ public final void addSimpleIcoText(ItemStack ico, String string, Formatting fmt, int idx) {
+ Text txt = Widget.simpleEntryText(idx, string, fmt);
+ this.addComponent(new IcoTextComponent(ico, txt));
+ }
+
+ /**
+ * Calculate the size of this widget.
+ * <b>Must be called before returning from the widget constructor and after all
+ * components are added!</b>
+ */
+ private void pack() {
+ h = 0;
+ w = 0;
+ for (Component c : components) {
+ h += c.getHeight() + Component.PAD_L;
+ w = Math.max(w, c.getWidth() + Component.PAD_S);
+ }
+
+ h -= Component.PAD_L / 2; // less padding after lowest/last component
+ h += BORDER_SZE_N + BORDER_SZE_S - 2;
+ w += BORDER_SZE_E + BORDER_SZE_W;
+
+ // min width is dependent on title
+ w = Math.max(w, BORDER_SZE_W + BORDER_SZE_E + Widget.txtRend.getWidth(title) + 4 + 4 + 1);
+ }
+
+ public final void setX(int x) {
+ this.x = x;
+ }
+
+ public final int getY() {
+ return this.y;
+ }
+
+ public final int getX() {
+ return this.x;
+ }
+
+ public final void setY(int y) {
+ this.y = y;
+ }
+
+ public final int getWidth() {
+ return this.w;
+ }
+
+ public final int getHeight() {
+ return this.h;
+ }
+
+ /**
+ * Draw this widget with a background
+ */
+ public final void render(DrawContext context) {
+ this.render(context, true);
+ }
+
+ /**
+ * Draw this widget, possibly with a background
+ */
+ public final void render(DrawContext context, boolean hasBG) {
+ MatrixStack ms = context.getMatrices();
+
+ // not sure if this is the way to go, but it fixes Z-layer issues
+ // like blocks being rendered behind the BG and the hotbar clipping into things
+ RenderSystem.enableDepthTest();
+ ms.push();
+
+ float scale = SkyblockerConfigManager.get().general.tabHud.tabHudScale / 100f;
+ ms.scale(scale, scale, 1);
+
+ // move above other UI elements
+ ms.translate(0, 0, 200);
+ if (hasBG) {
+ context.fill(x + 1, y, x + w - 1, y + h, COL_BG_BOX);
+ context.fill(x, y + 1, x + 1, y + h - 1, COL_BG_BOX);
+ context.fill(x + w - 1, y + 1, x + w, y + h - 1, COL_BG_BOX);
+ }
+ // move above background (if exists)
+ ms.translate(0, 0, 100);
+
+ int strHeightHalf = Widget.txtRend.fontHeight / 2;
+ int strAreaWidth = Widget.txtRend.getWidth(title) + 4;
+
+ context.drawText(txtRend, title, x + 8, y + 2, this.color, false);
+
+ this.drawHLine(context, x + 2, y + 1 + strHeightHalf, 4);
+ this.drawHLine(context, x + 2 + strAreaWidth + 4, y + 1 + strHeightHalf, w - 4 - 4 - strAreaWidth);
+ this.drawHLine(context, x + 2, y + h - 2, w - 4);
+
+ this.drawVLine(context, x + 1, y + 2 + strHeightHalf, h - 4 - strHeightHalf);
+ this.drawVLine(context, x + w - 2, y + 2 + strHeightHalf, h - 4 - strHeightHalf);
+
+ int yOffs = y + BORDER_SZE_N;
+
+ for (Component c : components) {
+ c.render(context, x + BORDER_SZE_W, yOffs);
+ yOffs += c.getHeight() + Component.PAD_L;
+ }
+ // pop manipulations above
+ ms.pop();
+ RenderSystem.disableDepthTest();
+ }
+
+ private void drawHLine(DrawContext context, int xpos, int ypos, int width) {
+ context.fill(xpos, ypos, xpos + width, ypos + 1, this.color);
+ }
+
+ private void drawVLine(DrawContext context, int xpos, int ypos, int height) {
+ context.fill(xpos, ypos, xpos + 1, ypos + height, this.color);
+ }
+
+ /**
+ * If the entry at idx has the format "[textA]: [textB]", the following is
+ * returned:
+ * [entryName] [textB.formatted(contentFmt)]
+ */
+ public static Text simpleEntryText(int idx, String entryName, Formatting contentFmt) {
+
+ String src = PlayerListMgr.strAt(idx);
+
+ if (src == null) {
+ return null;
+ }
+
+ int cidx = src.indexOf(':');
+ if (cidx == -1) {
+ return null;
+ }
+
+ src = src.substring(src.indexOf(':') + 1);
+ return Widget.simpleEntryText(src, entryName, contentFmt);
+ }
+
+ /**
+ * @return [entryName] [entryContent.formatted(contentFmt)]
+ */
+ public static Text simpleEntryText(String entryContent, String entryName, Formatting contentFmt) {
+ return Text.literal(entryName).append(Text.literal(entryContent).formatted(contentFmt));
+ }
+
+ /**
+ * @return the entry at idx as unformatted Text
+ */
+ public static Text plainEntryText(int idx) {
+ String str = PlayerListMgr.strAt(idx);
+ if (str == null) {
+ return null;
+ }
+ return Text.of(str);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/Component.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/Component.java
new file mode 100644
index 00000000..3c987068
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/Component.java
@@ -0,0 +1,31 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget.component;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+
+/**
+ * Abstract base class for a component that may be added to a Widget.
+ */
+public abstract class Component {
+
+ static final int ICO_DIM = 16;
+ public static final int PAD_S = 2;
+ public static final int PAD_L = 4;
+
+ static final TextRenderer txtRend = MinecraftClient.getInstance().textRenderer;
+
+ // these should always be the content dimensions without any padding.
+ int width, height;
+
+ public abstract void render(DrawContext context, int x, int y);
+
+ public int getWidth() {
+ return this.width;
+ }
+
+ public int getHeight() {
+ return this.height;
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/IcoFatTextComponent.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/IcoFatTextComponent.java
new file mode 100644
index 00000000..def60d4d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/IcoFatTextComponent.java
@@ -0,0 +1,45 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget.component;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.item.ItemStack;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+/**
+ * Component that consists of an icon and two lines of text
+ */
+public class IcoFatTextComponent extends Component {
+
+ private static final int ICO_OFFS = 1;
+
+ private ItemStack ico;
+ private Text line1, line2;
+
+ public IcoFatTextComponent(ItemStack ico, Text l1, Text l2) {
+ this.ico = (ico == null) ? Ico.BARRIER : ico;
+ this.line1 = l1;
+ this.line2 = l2;
+
+ if (l1 == null || l2 == null) {
+ this.ico = Ico.BARRIER;
+ this.line1 = Text.literal("No data").formatted(Formatting.GRAY);
+ this.line2 = Text.literal("No data").formatted(Formatting.GRAY);
+ }
+
+ this.width = ICO_DIM + PAD_L + Math.max(txtRend.getWidth(this.line1), txtRend.getWidth(this.line2));
+ this.height = txtRend.fontHeight + PAD_S + txtRend.fontHeight;
+ }
+
+ public IcoFatTextComponent() {
+ this(null, null, null);
+ }
+
+ @Override
+ public void render(DrawContext context, int x, int y) {
+ context.drawItem(ico, x, y + ICO_OFFS);
+ context.drawText(txtRend, line1, x + ICO_DIM + PAD_L, y, 0xffffffff, false);
+ context.drawText(txtRend, line2, x + ICO_DIM + PAD_L, y + txtRend.fontHeight + PAD_S, 0xffffffff, false);
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/IcoTextComponent.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/IcoTextComponent.java
new file mode 100644
index 00000000..903a1fa3
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/IcoTextComponent.java
@@ -0,0 +1,40 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget.component;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.item.ItemStack;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+/**
+ * Component that consists of an icon and a line of text.
+ */
+public class IcoTextComponent extends Component {
+
+ private ItemStack ico;
+ private Text text;
+
+ public IcoTextComponent(ItemStack ico, Text txt) {
+ this.ico = (ico == null) ? Ico.BARRIER : ico;
+ this.text = txt;
+
+ if (txt == null) {
+ this.ico = Ico.BARRIER;
+ this.text = Text.literal("No data").formatted(Formatting.GRAY);
+ }
+
+ this.width = ICO_DIM + PAD_L + txtRend.getWidth(this.text);
+ this.height = ICO_DIM;
+ }
+
+ public IcoTextComponent() {
+ this(null, null);
+ }
+
+ @Override
+ public void render(DrawContext context, int x, int y) {
+ context.drawItem(ico, x, y);
+ context.drawText(txtRend, text, x + ICO_DIM + PAD_L, y + 5, 0xffffffff, false);
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/PlainTextComponent.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/PlainTextComponent.java
new file mode 100644
index 00000000..59e82e4e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/PlainTextComponent.java
@@ -0,0 +1,30 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget.component;
+
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+/**
+ * Component that consists of a line of text.
+ */
+public class PlainTextComponent extends Component {
+
+ private Text text;
+
+ public PlainTextComponent(Text txt) {
+ this.text = txt;
+
+ if (txt == null) {
+ this.text = Text.literal("No data").formatted(Formatting.GRAY);
+ }
+
+ this.width = PAD_S + txtRend.getWidth(this.text); // looks off without padding
+ this.height = txtRend.fontHeight;
+ }
+
+ @Override
+ public void render(DrawContext context, int x, int y) {
+ context.drawText(txtRend, text, x + PAD_S, y, 0xffffffff, false);
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/PlayerComponent.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/PlayerComponent.java
new file mode 100644
index 00000000..ab79bb31
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/PlayerComponent.java
@@ -0,0 +1,39 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget.component;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.PlayerSkinDrawer;
+import net.minecraft.client.network.PlayerListEntry;
+import net.minecraft.scoreboard.Team;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+
+/**
+ * Component that consists of a player's skin icon and their name
+ */
+public class PlayerComponent extends Component {
+
+ private static final int SKIN_ICO_DIM = 8;
+
+ private final Text name;
+ private final Identifier tex;
+
+ public PlayerComponent(PlayerListEntry ple) {
+
+ boolean plainNames = SkyblockerConfigManager.get().general.tabHud.plainPlayerNames;
+ Team team = ple.getScoreboardTeam();
+ String username = ple.getProfile().getName();
+ name = (team != null && !plainNames) ? Text.empty().append(team.getPrefix()).append(Text.literal(username).formatted(team.getColor())).append(team.getSuffix()) : Text.of(username);
+ tex = ple.getSkinTextures().texture();
+
+ this.width = SKIN_ICO_DIM + PAD_S + txtRend.getWidth(name);
+ this.height = txtRend.fontHeight;
+ }
+
+ @Override
+ public void render(DrawContext context, int x, int y) {
+ PlayerSkinDrawer.draw(context, tex, x, y, SKIN_ICO_DIM);
+ context.drawText(txtRend, name, x + SKIN_ICO_DIM + PAD_S, y, 0xffffffff, false);
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/ProgressComponent.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/ProgressComponent.java
new file mode 100644
index 00000000..fa839dbe
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/ProgressComponent.java
@@ -0,0 +1,69 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget.component;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.item.ItemStack;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+
+/**
+ * Component that consists of an icon, some text and a progress bar.
+ * The progress bar either shows the fill percentage or custom text.
+ * NOTICE: pcnt is 0-100, not 0-1!
+ */
+public class ProgressComponent extends Component {
+
+ private static final int BAR_WIDTH = 100;
+ private static final int BAR_HEIGHT = txtRend.fontHeight + 3;
+ private static final int ICO_OFFS = 4;
+ private static final int COL_BG_BAR = 0xf0101010;
+
+ private final ItemStack ico;
+ private final Text desc, bar;
+ private final float pcnt;
+ private final int color;
+ private final int barW;
+
+ public ProgressComponent(ItemStack ico, Text d, Text b, float pcnt, int color) {
+ if (d == null || b == null) {
+ this.ico = Ico.BARRIER;
+ this.desc = Text.literal("No data").formatted(Formatting.GRAY);
+ this.bar = Text.literal("---").formatted(Formatting.GRAY);
+ this.pcnt = 100f;
+ this.color = 0xff000000 | Formatting.DARK_GRAY.getColorValue();
+ } else {
+ this.ico = (ico == null) ? Ico.BARRIER : ico;
+ this.desc = d;
+ this.bar = b;
+ this.pcnt = pcnt;
+ this.color = 0xff000000 | color;
+ }
+
+ this.barW = BAR_WIDTH;
+ this.width = ICO_DIM + PAD_L + Math.max(this.barW, txtRend.getWidth(this.desc));
+ this.height = txtRend.fontHeight + PAD_S + 2 + txtRend.fontHeight + 2;
+ }
+
+ public ProgressComponent(ItemStack ico, Text text, float pcnt, int color) {
+ this(ico, text, Text.of(pcnt + "%"), pcnt, color);
+ }
+
+ public ProgressComponent() {
+ this(null, null, null, 100, 0);
+ }
+
+ @Override
+ public void render(DrawContext context, int x, int y) {
+ context.drawItem(ico, x, y + ICO_OFFS);
+ context.drawText(txtRend, desc, x + ICO_DIM + PAD_L, y, 0xffffffff, false);
+
+ int barX = x + ICO_DIM + PAD_L;
+ int barY = y + txtRend.fontHeight + PAD_S;
+ int endOffsX = ((int) (this.barW * (this.pcnt / 100f)));
+ context.fill(barX + endOffsX, barY, barX + this.barW, barY + BAR_HEIGHT, COL_BG_BAR);
+ context.fill(barX, barY, barX + endOffsX, barY + BAR_HEIGHT,
+ this.color);
+ context.drawTextWithShadow(txtRend, bar, barX + 3, barY + 2, 0xffffffff);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/TableComponent.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/TableComponent.java
new file mode 100644
index 00000000..dbc0bf55
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/component/TableComponent.java
@@ -0,0 +1,58 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget.component;
+
+import net.minecraft.client.gui.DrawContext;
+
+/**
+ * Meta-Component that consists of a grid of other components
+ * Grid cols are separated by lines.
+ */
+public class TableComponent extends Component {
+
+ private final Component[][] comps;
+ private final int color;
+ private final int cols, rows;
+ private int cellW, cellH;
+
+ public TableComponent(int w, int h, int col) {
+ comps = new Component[w][h];
+ color = 0xff000000 | col;
+ cols = w;
+ rows = h;
+ }
+
+ public void addToCell(int x, int y, Component c) {
+ this.comps[x][y] = c;
+
+ // pad extra to add a vertical line later
+ this.cellW = Math.max(this.cellW, c.width + PAD_S + PAD_L);
+
+ // assume all rows are equally high so overwriting doesn't matter
+ // if this wasn't the case, drawing would need more math
+ // not doing any of that if it's not needed
+ this.cellH = c.height + PAD_S;
+
+ this.width = this.cellW * this.cols;
+ this.height = (this.cellH * this.rows) - PAD_S / 2;
+
+ }
+
+ @Override
+ public void render(DrawContext context, int xpos, int ypos) {
+ for (int x = 0; x < cols; x++) {
+ for (int y = 0; y < rows; y++) {
+ if (comps[x][y] != null) {
+ comps[x][y].render(context, xpos + (x * cellW), ypos + y * cellH);
+ }
+ }
+ // add a line before the col if we're not drawing the first one
+ if (x != 0) {
+ int lineX1 = xpos + (x * cellW) - PAD_S - 1;
+ int lineX2 = xpos + (x * cellW) - PAD_S;
+ int lineY1 = ypos + 1;
+ int lineY2 = ypos + this.height - PAD_S - 1; // not sure why but it looks correct
+ context.fill(lineX1, lineY1, lineX2, lineY2, this.color);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/hud/HudCommsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/hud/HudCommsWidget.java
new file mode 100644
index 00000000..6aa363c9
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/hud/HudCommsWidget.java
@@ -0,0 +1,73 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget.hud;
+
+import java.util.List;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
+import de.hysky.skyblocker.skyblock.dwarven.DwarvenHud.Commission;
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.Component;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.ProgressComponent;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.math.MathHelper;
+
+// this widget shows the status of the king's commissions.
+// (dwarven mines and crystal hollows)
+// USE ONLY WITH THE DWARVEN HUD!
+
+public class HudCommsWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Commissions").formatted(Formatting.DARK_AQUA,
+ Formatting.BOLD);
+
+ private List<Commission> commissions;
+ private boolean isFancy;
+
+ // disgusting hack to get around text renderer issues.
+ // the ctor eventually tries to get the font's height, which doesn't work
+ // when called before the client window is created (roughly).
+ // the rebdering god 2 from the fabricord explained that detail, thanks!
+ public static final HudCommsWidget INSTANCE = new HudCommsWidget();
+ public static final HudCommsWidget INSTANCE_CFG = new HudCommsWidget();
+
+ // another repulsive hack to make this widget-like hud element work with the new widget class
+ // DON'T USE WITH THE WIDGET SYSTEM, ONLY USE FOR DWARVENHUD!
+ public HudCommsWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ public void updateData(List<Commission> commissions, boolean isFancy) {
+ this.commissions = commissions;
+ this.isFancy = isFancy;
+ }
+
+ @Override
+ public void updateContent() {
+ for (Commission comm : commissions) {
+
+ Text c = Text.literal(comm.commission());
+
+ float p = 100f;
+ if (!comm.progression().contains("DONE")) {
+ p = Float.parseFloat(comm.progression().substring(0, comm.progression().length() - 1));
+ }
+
+ Component comp;
+ if (isFancy) {
+ comp = new ProgressComponent(Ico.BOOK, c, p, pcntToCol(p));
+ } else {
+ comp = new PlainTextComponent(
+ Text.literal(comm.commission() + ": ")
+ .append(Text.literal(comm.progression()).formatted(Formatting.GREEN)));
+ }
+ this.addComponent(comp);
+ }
+ }
+
+ private int pcntToCol(float pcnt) {
+ return MathHelper.hsvToRgb(pcnt / 300f, 0.9f, 0.9f);
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/AdvertisementWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/AdvertisementWidget.java
new file mode 100644
index 00000000..3499ce39
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/AdvertisementWidget.java
@@ -0,0 +1,35 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget.rift;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+public class AdvertisementWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Advertisement").formatted(Formatting.DARK_AQUA,
+ Formatting.BOLD);
+
+ public AdvertisementWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ boolean added = false;
+ for (int i = 73; i < 80; i++) {
+ Text text = PlayerListMgr.textAt(i);
+ if (text != null) {
+ this.addComponent(new PlainTextComponent(text));
+ added = true;
+ }
+ }
+
+ if (!added) {
+ this.addComponent(new PlainTextComponent(Text.literal("No Advertisements").formatted(Formatting.GRAY)));
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/GoodToKnowWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/GoodToKnowWidget.java
new file mode 100644
index 00000000..3a5da142
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/GoodToKnowWidget.java
@@ -0,0 +1,69 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget.rift;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+public class GoodToKnowWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Good To Know").formatted(Formatting.BLUE, Formatting.BOLD);
+
+ public GoodToKnowWidget() {
+ super(TITLE, Formatting.BLUE.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ // After you progress further the tab adds more info so we need to be careful of
+ // that
+ // In beginning it only shows montezuma, then timecharms and enigma souls are
+ // added
+
+ int headerPos = 0;
+ // this seems suboptimal, but I'm not sure if there's a way to do it better.
+ // search for the GTK header and offset the rest accordingly.
+ for (int i = 45; i <= 49; i++) {
+ String str = PlayerListMgr.strAt(i);
+ if (str != null && str.startsWith("Good to")) {
+ headerPos = i;
+ break;
+ }
+ }
+
+ Text posA = PlayerListMgr.textAt(headerPos + 2); // Can be times visited rift
+ Text posB = PlayerListMgr.textAt(headerPos + 4); // Can be lifetime motes or visited rift
+ Text posC = PlayerListMgr.textAt(headerPos + 6); // Can be lifetime motes
+
+ int visitedRiftPos = 0;
+ int lifetimeMotesPos = 0;
+
+ // Check each position to see what is or isn't there so we don't try adding
+ // invalid components
+ if (posA != null && posA.getString().contains("times"))
+ visitedRiftPos = headerPos + 2;
+ if (posB != null && posB.getString().contains("Motes"))
+ lifetimeMotesPos = headerPos + 4;
+ if (posB != null && posB.getString().contains("times"))
+ visitedRiftPos = headerPos + 4;
+ if (posC != null && posC.getString().contains("Motes"))
+ lifetimeMotesPos = headerPos + 6;
+
+ Text timesVisitedRift = (visitedRiftPos == headerPos + 4) ? posB : (visitedRiftPos == headerPos + 2) ? posA : Text.literal("No Data").formatted(Formatting.GRAY);
+ Text lifetimeMotesEarned = (lifetimeMotesPos == headerPos + 6) ? posC : (lifetimeMotesPos == headerPos + 4) ? posB : Text.literal("No Data").formatted(Formatting.GRAY);
+
+ if (visitedRiftPos != 0) {
+ this.addComponent(new IcoTextComponent(Ico.EXPERIENCE_BOTTLE,
+ Text.literal("Visited Rift: ").append(timesVisitedRift)));
+ }
+
+ if (lifetimeMotesPos != 0) {
+ this.addComponent(
+ new IcoTextComponent(Ico.PINK_DYE, Text.literal("Lifetime Earned: ").append(lifetimeMotesEarned)));
+ }
+
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftProfileWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftProfileWidget.java
new file mode 100644
index 00000000..178bf142
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftProfileWidget.java
@@ -0,0 +1,21 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget.rift;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+public class RiftProfileWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Profile").formatted(Formatting.DARK_AQUA, Formatting.BOLD);
+
+ public RiftProfileWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ this.addSimpleIcoText(Ico.SIGN, "Profile:", Formatting.GREEN, 61);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftProgressWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftProgressWidget.java
new file mode 100644
index 00000000..93ade5cb
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftProgressWidget.java
@@ -0,0 +1,123 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget.rift;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.ProgressComponent;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.math.MathHelper;
+
+public class RiftProgressWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Rift Progress").formatted(Formatting.BLUE, Formatting.BOLD);
+
+ private static final Pattern TIMECHARMS_PATTERN = Pattern.compile("Timecharms: (?<current>[0-9]+)\\/(?<total>[0-9]+)");
+ private static final Pattern ENIGMA_SOULS_PATTERN = Pattern.compile("Enigma Souls: (?<current>[0-9]+)\\/(?<total>[0-9]+)");
+ private static final Pattern MONTEZUMA_PATTERN = Pattern.compile("Montezuma: (?<current>[0-9]+)\\/(?<total>[0-9]+)");
+
+ public RiftProgressWidget() {
+ super(TITLE, Formatting.BLUE.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ // After you progress further, the tab adds more info so we need to be careful
+ // of that.
+ // In beginning it only shows montezuma, then timecharms and enigma souls are
+ // added.
+
+ String pos44 = PlayerListMgr.strAt(44);
+
+ // LHS short-circuits, so the RHS won't be evaluated on pos44 == null
+ if (pos44 == null || !pos44.contains("Rift Progress")) {
+ this.addComponent(new PlainTextComponent(Text.literal("No Progress").formatted(Formatting.GRAY)));
+ return;
+ }
+
+ // let's try to be clever by assuming what progress item may appear where and
+ // when to skip testing every slot for every thing.
+
+ // always non-null, as this holds the topmost item.
+ // if there is none, there shouldn't be a header.
+ String pos45 = PlayerListMgr.strAt(45);
+
+ // Can be Montezuma, Enigma Souls or Timecharms.
+ // assume timecharms can only appear here and that they're the last thing to
+ // appear, so if this exists, we know the rest.
+ if (pos45.contains("Timecharms")) {
+ addTimecharmsComponent(45);
+ addEnigmaSoulsComponent(46);
+ addMontezumaComponent(47);
+ return;
+ }
+
+ // timecharms didn't appear at the top, so there's two or one entries.
+ // assume that if there's two, souls is always top.
+ String pos46 = PlayerListMgr.strAt(46);
+
+ if (pos45.contains("Enigma Souls")) {
+ addEnigmaSoulsComponent(45);
+ if (pos46 != null) {
+ // souls might appear alone.
+ // if there's a second entry, it has to be montezuma
+ addMontezumaComponent(46);
+ }
+ } else {
+ // first entry isn't souls, so it's just montezuma and nothing else.
+ addMontezumaComponent(45);
+ }
+
+ }
+
+ private static int pcntToCol(float pcnt) {
+ return MathHelper.hsvToRgb(pcnt / 300f, 0.9f, 0.9f);
+ }
+
+ private void addTimecharmsComponent(int pos) {
+ Matcher m = PlayerListMgr.regexAt(pos, TIMECHARMS_PATTERN);
+
+ int current = Integer.parseInt(m.group("current"));
+ int total = Integer.parseInt(m.group("total"));
+ float pcnt = ((float) current / (float) total) * 100f;
+ Text progressText = Text.literal(current + "/" + total);
+
+ ProgressComponent pc = new ProgressComponent(Ico.NETHER_STAR, Text.literal("Timecharms"), progressText,
+ pcnt, pcntToCol(pcnt));
+
+ this.addComponent(pc);
+ }
+
+ private void addEnigmaSoulsComponent(int pos) {
+ Matcher m = PlayerListMgr.regexAt(pos, ENIGMA_SOULS_PATTERN);
+
+ int current = Integer.parseInt(m.group("current"));
+ int total = Integer.parseInt(m.group("total"));
+ float pcnt = ((float) current / (float) total) * 100f;
+ Text progressText = Text.literal(current + "/" + total);
+
+ ProgressComponent pc = new ProgressComponent(Ico.HEART_OF_THE_SEA, Text.literal("Enigma Souls"),
+ progressText, pcnt, pcntToCol(pcnt));
+
+ this.addComponent(pc);
+ }
+
+ private void addMontezumaComponent(int pos) {
+ Matcher m = PlayerListMgr.regexAt(pos, MONTEZUMA_PATTERN);
+
+ int current = Integer.parseInt(m.group("current"));
+ int total = Integer.parseInt(m.group("total"));
+ float pcnt = ((float) current / (float) total) * 100f;
+ Text progressText = Text.literal(current + "/" + total);
+
+ ProgressComponent pc = new ProgressComponent(Ico.BONE, Text.literal("Montezuma"), progressText, pcnt,
+ pcntToCol(pcnt));
+
+ this.addComponent(pc);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftServerInfoWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftServerInfoWidget.java
new file mode 100644
index 00000000..89530e2f
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftServerInfoWidget.java
@@ -0,0 +1,27 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget.rift;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+/**
+ * Special version of the server info widget for the rift!
+ *
+ */
+public class RiftServerInfoWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Server Info").formatted(Formatting.LIGHT_PURPLE, Formatting.BOLD);
+
+ public RiftServerInfoWidget() {
+ super(TITLE, Formatting.LIGHT_PURPLE.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ this.addSimpleIcoText(Ico.MAP, "Area:", Formatting.LIGHT_PURPLE, 41);
+ this.addSimpleIcoText(Ico.NTAG, "Server ID:", Formatting.GRAY, 42);
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftStatsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftStatsWidget.java
new file mode 100644
index 00000000..0e2f323d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/RiftStatsWidget.java
@@ -0,0 +1,43 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget.rift;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
+import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.TableComponent;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+public class RiftStatsWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Stats").formatted(Formatting.DARK_AQUA, Formatting.BOLD);
+
+ public RiftStatsWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ Text riftDamage = Widget.simpleEntryText(64, "RDG", Formatting.DARK_PURPLE);
+ IcoTextComponent rdg = new IcoTextComponent(Ico.DIASWORD, riftDamage);
+
+ Text speed = Widget.simpleEntryText(65, "SPD", Formatting.WHITE);
+ IcoTextComponent spd = new IcoTextComponent(Ico.SUGAR, speed);
+
+ Text intelligence = Widget.simpleEntryText(66, "INT", Formatting.AQUA);
+ IcoTextComponent intel = new IcoTextComponent(Ico.ENCHANTED_BOOK, intelligence);
+
+ Text manaRegen = Widget.simpleEntryText(67, "MRG", Formatting.AQUA);
+ IcoTextComponent mrg = new IcoTextComponent(Ico.DIAMOND, manaRegen);
+
+ TableComponent tc = new TableComponent(2, 2, Formatting.AQUA.getColorValue());
+ tc.addToCell(0, 0, rdg);
+ tc.addToCell(0, 1, spd);
+ tc.addToCell(1, 0, intel);
+ tc.addToCell(1, 1, mrg);
+
+ this.addComponent(tc);
+ }
+
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/ShenWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/ShenWidget.java
new file mode 100644
index 00000000..2827400e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/rift/ShenWidget.java
@@ -0,0 +1,22 @@
+package de.hysky.skyblocker.skyblock.tabhud.widget.rift;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
+import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+public class ShenWidget extends Widget {
+
+ private static final MutableText TITLE = Text.literal("Shen's Countdown").formatted(Formatting.DARK_AQUA, Formatting.BOLD);
+
+ public ShenWidget() {
+ super(TITLE, Formatting.DARK_AQUA.getColorValue());
+ }
+
+ @Override
+ public void updateContent() {
+ this.addComponent(new PlainTextComponent(Text.literal(PlayerListMgr.strAt(70))));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/Boxes.java b/src/main/java/de/hysky/skyblocker/utils/Boxes.java
new file mode 100644
index 00000000..cd944a46
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/Boxes.java
@@ -0,0 +1,50 @@
+package de.hysky.skyblocker.utils;
+
+import net.minecraft.util.math.Box;
+import net.minecraft.util.math.Direction.Axis;
+import net.minecraft.util.math.Vec3d;
+
+public class Boxes {
+ /** Returns the vector of the min pos of this box. **/
+ public static Vec3d getMinVec(Box box) {
+ return new Vec3d(box.minX, box.minY, box.minZ);
+ }
+
+ /** Returns the vector of the max pos of this box. **/
+ public static Vec3d getMaxVec(Box box) {
+ return new Vec3d(box.maxX, box.maxY, box.maxZ);
+ }
+
+ /** Returns the vector of the side lengths of this box. **/
+ public static Vec3d getLengthVec(Box box) {
+ return new Vec3d(box.getLengthX(), box.getLengthY(), box.getLengthZ());
+ }
+
+ /** Offsets this box so that minX, minY and minZ are all zero. **/
+ public static Box moveToZero(Box box) {
+ return box.offset(getMinVec(box).negate());
+ }
+
+ /** Returns the distance between to oppisite corners of the box. **/
+ public static double getCornerLength(Box box) {
+ return getMinVec(box).distanceTo(getMaxVec(box));
+ }
+
+ /** Returns the length of an axis in the box. **/
+ public static double getAxisLength(Box box, Axis axis) {
+ return box.getMax(axis) - box.getMin(axis);
+ }
+
+ /** Returns a box with each axis multiplied by the amount specified. **/
+ public static Box multiply(Box box, double amount) {
+ return multiply(box, amount, amount, amount);
+ }
+
+ /** Returns a box with each axis multiplied by the amount specified. **/
+ public static Box multiply(Box box, double x, double y, double z) {
+ return box.expand(
+ getAxisLength(box, Axis.X) * (x - 1) / 2d,
+ getAxisLength(box, Axis.Y) * (y - 1) / 2d,
+ getAxisLength(box, Axis.Z) * (z - 1) / 2d);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/Constants.java b/src/main/java/de/hysky/skyblocker/utils/Constants.java
new file mode 100644
index 00000000..fbeb448c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/Constants.java
@@ -0,0 +1,8 @@
+package de.hysky.skyblocker.utils;
+
+/**
+ * Holds generic static constants
+ */
+public interface Constants {
+ String LEVEL_EMBLEMS = "\u2E15\u273F\u2741\u2E19\u03B1\u270E\u2615\u2616\u2663\u213B\u2694\u27B6\u26A1\u2604\u269A\u2693\u2620\u269B\u2666\u2660\u2764\u2727\u238A\u1360\u262C\u269D\u29C9\uA214\u32D6\u2E0E\u26A0\uA541\u3020\u30C4\u2948\u2622\u2623\u273E\u269C\u0BD0\u0A6D\u2742\u16C3\u3023\u10F6\u0444\u266A\u266B\u04C3\u26C1\u26C3\u16DD\uA03E\u1C6A\u03A3\u09EB\u2603\u2654\u26C2\u12DE";
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/Http.java b/src/main/java/de/hysky/skyblocker/utils/Http.java
new file mode 100644
index 00000000..ee500b5a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/Http.java
@@ -0,0 +1,89 @@
+package de.hysky.skyblocker.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpClient.Version;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.time.Duration;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.InflaterInputStream;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import net.minecraft.SharedConstants;
+
+/**
+ * @implNote All http requests are sent using HTTP 2
+ */
+public class Http {
+ private static final String USER_AGENT = "Skyblocker/" + SkyblockerMod.VERSION + " (" + SharedConstants.getGameVersion().getName() + ")";
+ private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(10))
+ .build();
+
+ public static String sendGetRequest(String url) throws IOException, InterruptedException {
+ HttpRequest request = 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();
+
+ HttpResponse<InputStream> response = HTTP_CLIENT.send(request, BodyHandlers.ofInputStream());
+ InputStream decodedInputStream = getDecodedInputStream(response);
+ String body = new String(decodedInputStream.readAllBytes());
+
+ return body;
+ }
+
+ public static HttpHeaders sendHeadRequest(String url) throws IOException, InterruptedException {
+ HttpRequest request = HttpRequest.newBuilder()
+ .method("HEAD", BodyPublishers.noBody())
+ .header("User-Agent", USER_AGENT)
+ .version(Version.HTTP_2)
+ .uri(URI.create(url))
+ .build();
+
+ HttpResponse<Void> response = HTTP_CLIENT.send(request, BodyHandlers.discarding());
+ return response.headers();
+ }
+
+ private static InputStream getDecodedInputStream(HttpResponse<InputStream> response) {
+ String encoding = getContentEncoding(response);
+
+ try {
+ switch (encoding) {
+ case "":
+ return response.body();
+ case "gzip":
+ return new GZIPInputStream(response.body());
+ case "deflate":
+ return new InflaterInputStream(response.body());
+ default:
+ throw new UnsupportedOperationException("The server sent content in an unexpected encoding: " + encoding);
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private static String getContentEncoding(HttpResponse<InputStream> response) {
+ return response.headers().firstValue("Content-Encoding").orElse("");
+ }
+
+ public static String getEtag(HttpHeaders headers) {
+ return headers.firstValue("Etag").orElse("");
+ }
+
+ public static String getLastModified(HttpHeaders headers) {
+ return headers.firstValue("Last-Modified").orElse("");
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java
new file mode 100644
index 00000000..6ae1b4d0
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java
@@ -0,0 +1,111 @@
+package de.hysky.skyblocker.utils;
+
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.item.TooltipContext;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.StringNbtReader;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class ItemUtils {
+ private final static Pattern WHITESPACES = Pattern.compile("^\\s*$");
+
+ public static List<Text> getTooltip(ItemStack item) {
+ MinecraftClient client = MinecraftClient.getInstance();
+ return client.player == null || item == null ? Collections.emptyList() : item.getTooltip(client.player, TooltipContext.Default.BASIC);
+ }
+
+ public static List<String> getTooltipStrings(ItemStack item) {
+ List<Text> lines = getTooltip(item);
+ List<String> list = new ArrayList<>();
+
+ for (Text line : lines) {
+ String string = line.getString();
+ if (!WHITESPACES.matcher(string).matches())
+ list.add(string);
+ }
+
+ return list;
+ }
+
+ @Nullable
+ public static Durability getDurability(ItemStack stack) {
+ if (!Utils.isOnSkyblock() || !SkyblockerConfigManager.get().locations.dwarvenMines.enableDrillFuel || stack.isEmpty()) {
+ return null;
+ }
+
+ NbtCompound tag = stack.getNbt();
+ if (tag == null || !tag.contains("ExtraAttributes")) {
+ return null;
+ }
+
+ NbtCompound extraAttributes = tag.getCompound("ExtraAttributes");
+ if (!extraAttributes.contains("drill_fuel") && !extraAttributes.getString("id").equals("PICKONIMBUS")) {
+ return null;
+ }
+
+ int current = 0;
+ int max = 0;
+ String clearFormatting;
+
+ for (String line : ItemUtils.getTooltipStrings(stack)) {
+ clearFormatting = Formatting.strip(line);
+ if (line.contains("Fuel: ")) {
+ if (clearFormatting != null) {
+ String clear = Pattern.compile("[^0-9 /]").matcher(clearFormatting).replaceAll("").trim();
+ String[] split = clear.split("/");
+ current = Integer.parseInt(split[0]);
+ max = Integer.parseInt(split[1]) * 1000;
+ return new Durability(current, max);
+ }
+ } else if (line.contains("uses.")) {
+ if (clearFormatting != null) {
+ int startIndex = clearFormatting.lastIndexOf("after") + 6;
+ int endIndex = clearFormatting.indexOf("uses", startIndex);
+ if (startIndex >= 0 && endIndex > startIndex) {
+ String usesString = clearFormatting.substring(startIndex, endIndex).trim();
+ current = Integer.parseInt(usesString);
+ max = 5000;
+ }
+ return new Durability(current, max);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static ItemStack getSkyblockerStack() {
+ try {
+ return ItemStack.fromNbt(StringNbtReader.parse("{id:\"minecraft:player_head\",Count:1,tag:{SkullOwner:{Id:[I;-300151517,-631415889,-1193921967,-1821784279],Properties:{textures:[{Value:\"e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDdjYzY2ODc0MjNkMDU3MGQ1NTZhYzUzZTA2NzZjYjU2M2JiZGQ5NzE3Y2Q4MjY5YmRlYmVkNmY2ZDRlN2JmOCJ9fX0=\"}]}}}}"));
+ } catch (CommandSyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String getItemId(ItemStack itemStack) {
+ if (itemStack == null) return null;
+
+ NbtCompound nbt = itemStack.getNbt();
+ if (nbt != null && nbt.contains("ExtraAttributes")) {
+ NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes");
+ if (extraAttributes.contains("id")) {
+ return extraAttributes.getString("id");
+ }
+ }
+
+ return null;
+ }
+
+ public record Durability(int current, int max) {
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/NEURepo.java b/src/main/java/de/hysky/skyblocker/utils/NEURepo.java
new file mode 100644
index 00000000..9bc6b245
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/NEURepo.java
@@ -0,0 +1,101 @@
+package de.hysky.skyblocker.utils;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.skyblock.itemlist.ItemRegistry;
+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.text.Text;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.TransportException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Initializes the NEU repo, which contains item metadata and fairy souls location data. Clones the repo if it does not exist and checks for updates. Use {@link #runAsyncAfterLoad(Runnable)} to run code after the repo is initialized.
+ */
+public class NEURepo {
+ private static final Logger LOGGER = LoggerFactory.getLogger(NEURepo.class);
+ public static final String REMOTE_REPO_URL = "https://github.com/NotEnoughUpdates/NotEnoughUpdates-REPO.git";
+ public static final Path LOCAL_REPO_DIR = SkyblockerMod.CONFIG_DIR.resolve("item-repo");
+ private static final CompletableFuture<Void> REPO_INITIALIZED = initRepository();
+
+ /**
+ * Adds command to update repository manually from ingame.
+ * <p></p>
+ * TODO A button could be added to the settings menu that will trigger this command.
+ */
+ public static void init() {
+ ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) ->
+ dispatcher.register(ClientCommandManager.literal(SkyblockerMod.NAMESPACE)
+ .then(ClientCommandManager.literal("updaterepository").executes(context -> {
+ deleteAndDownloadRepository();
+ return 1;
+ }))));
+ }
+
+ private static CompletableFuture<Void> initRepository() {
+ return CompletableFuture.runAsync(() -> {
+ try {
+ if (Files.isDirectory(NEURepo.LOCAL_REPO_DIR)) {
+ try (Git localRepo = Git.open(NEURepo.LOCAL_REPO_DIR.toFile())) {
+ localRepo.pull().setRebase(true).call();
+ LOGGER.info("[Skyblocker] NEU Repository Updated");
+ }
+ } else {
+ Git.cloneRepository().setURI(REMOTE_REPO_URL).setDirectory(NEURepo.LOCAL_REPO_DIR.toFile()).setBranchesToClone(List.of("refs/heads/master")).setBranch("refs/heads/master").call().close();
+ LOGGER.info("[Skyblocker] NEU Repository Downloaded");
+ }
+ } catch (TransportException e){
+ LOGGER.error("[Skyblocker] Transport operation failed. Most likely unable to connect to the remote NEU repo on github", e);
+ } catch (RepositoryNotFoundException e) {
+ LOGGER.warn("[Skyblocker] Local NEU Repository not found or corrupted, downloading new one", e);
+ deleteAndDownloadRepository();
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Encountered unknown exception while initializing NEU Repository", e);
+ }
+ });
+ }
+
+ private static void deleteAndDownloadRepository() {
+ CompletableFuture.runAsync(() -> {
+ try {
+ ItemRegistry.filesImported = false;
+ File dir = NEURepo.LOCAL_REPO_DIR.toFile();
+ recursiveDelete(dir);
+ } catch (Exception ex) {
+ if (MinecraftClient.getInstance().player != null)
+ MinecraftClient.getInstance().player.sendMessage(Text.translatable("skyblocker.updaterepository.failed"), false);
+ return;
+ }
+ initRepository();
+ });
+ }
+
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ private static void recursiveDelete(File dir) {
+ File[] children;
+ if (dir.isDirectory() && !Files.isSymbolicLink(dir.toPath()) && (children = dir.listFiles()) != null) {
+ for (File child : children) {
+ recursiveDelete(child);
+ }
+ }
+ dir.delete();
+ }
+
+ /**
+ * Runs the given runnable after the NEU repo is initialized.
+ * @param runnable the runnable to run
+ * @return a completable future of the given runnable
+ */
+ public static CompletableFuture<Void> runAsyncAfterLoad(Runnable runnable) {
+ return REPO_INITIALIZED.thenRunAsync(runnable);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/PosUtils.java b/src/main/java/de/hysky/skyblocker/utils/PosUtils.java
new file mode 100644
index 00000000..6a34b060
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/PosUtils.java
@@ -0,0 +1,14 @@
+package de.hysky.skyblocker.utils;
+
+import net.minecraft.util.math.BlockPos;
+
+public final class PosUtils {
+ public static BlockPos parsePosString(String posData) {
+ String[] posArray = posData.split(",");
+ return new BlockPos(Integer.parseInt(posArray[0]), Integer.parseInt(posArray[1]), Integer.parseInt(posArray[2]));
+ }
+
+ public static String getPosString(BlockPos blockPos) {
+ return blockPos.getX() + "," + blockPos.getY() + "," + blockPos.getZ();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/SlayerUtils.java b/src/main/java/de/hysky/skyblocker/utils/SlayerUtils.java
new file mode 100644
index 00000000..0a42c6ae
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/SlayerUtils.java
@@ -0,0 +1,54 @@
+package de.hysky.skyblocker.utils;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.decoration.ArmorStandEntity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+//TODO Slayer Packet system that can provide information about the current slayer boss, abstract so that different bosses can have different info
+public class SlayerUtils {
+ private static final Logger LOGGER = LoggerFactory.getLogger(SlayerUtils.class);
+
+ //TODO: Cache this, probably included in Packet system
+ public static List<Entity> getEntityArmorStands(Entity entity) {
+ return entity.getEntityWorld().getOtherEntities(entity, entity.getBoundingBox().expand(1F, 2.5F, 1F), x -> x instanceof ArmorStandEntity && x.hasCustomName());
+ }
+
+ //Eventually this should be modified so that if you hit a slayer boss all slayer features will work on that boss.
+ public static Entity getSlayerEntity() {
+ if (MinecraftClient.getInstance().world != null) {
+ for (Entity entity : MinecraftClient.getInstance().world.getEntities()) {
+ //Check if entity is Bloodfiend
+ if (entity.hasCustomName() && entity.getCustomName().getString().contains("Bloodfiend")) {
+ //Grab the players username
+ String username = MinecraftClient.getInstance().getSession().getUsername();
+ //Check all armor stands around the boss
+ for (Entity armorStand : getEntityArmorStands(entity)) {
+ //Check if the display name contains the players username
+ if (armorStand.getDisplayName().getString().contains(username)) {
+ return entity;
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ public static boolean isInSlayer() {
+ try {
+ for (int i = 0; i < Utils.STRING_SCOREBOARD.size(); i++) {
+ String line = Utils.STRING_SCOREBOARD.get(i);
+
+ if (line.contains("Slay the boss!")) return true;
+ }
+ } catch (NullPointerException e) {
+ LOGGER.error("[Skyblocker] Error while checking if player is in slayer", e);
+ }
+
+ return false;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java
new file mode 100644
index 00000000..f046bffb
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java
@@ -0,0 +1,370 @@
+package de.hysky.skyblocker.utils;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import de.hysky.skyblocker.events.SkyblockEvents;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import de.hysky.skyblocker.skyblock.item.PriceInfoTooltip;
+import de.hysky.skyblocker.skyblock.rift.TheRift;
+import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
+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;
+import net.fabricmc.loader.api.FabricLoader;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.client.network.PlayerListEntry;
+import net.minecraft.scoreboard.Scoreboard;
+import net.minecraft.scoreboard.ScoreboardDisplaySlot;
+import net.minecraft.scoreboard.ScoreboardObjective;
+import net.minecraft.scoreboard.ScoreboardPlayerScore;
+import net.minecraft.scoreboard.Team;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility variables and methods for retrieving Skyblock related information.
+ */
+public class Utils {
+ private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);
+ private static final String ALTERNATE_HYPIXEL_ADDRESS = System.getProperty("skyblocker.alternateHypixelAddress", "");
+ private static final String PROFILE_PREFIX = "Profile: ";
+ private static boolean isOnHypixel = false;
+ private static boolean isOnSkyblock = false;
+ private static boolean isInDungeons = false;
+ private static boolean isInjected = false;
+ /**
+ * The following fields store data returned from /locraw: {@link #profile}, {@link #server}, {@link #gameType}, {@link #locationRaw}, and {@link #map}.
+ */
+ @SuppressWarnings("JavadocDeclaration")
+ private static String profile = "";
+ private static String server = "";
+ private static String gameType = "";
+ private static String locationRaw = "";
+ private static String map = "";
+ private static long clientWorldJoinTime = 0;
+ private static boolean sentLocRaw = false;
+ private static boolean canSendLocRaw = false;
+
+ /**
+ * @implNote The parent text will always be empty, the actual text content is inside the text's siblings.
+ */
+ public static final ObjectArrayList<Text> TEXT_SCOREBOARD = new ObjectArrayList<>();
+ public static final ObjectArrayList<String> STRING_SCOREBOARD = new ObjectArrayList<>();
+
+ public static boolean isOnHypixel() {
+ return isOnHypixel;
+ }
+
+ public static boolean isOnSkyblock() {
+ return isOnSkyblock;
+ }
+
+ public static boolean isInDungeons() {
+ return isInDungeons;
+ }
+
+ public static boolean isInTheRift() {
+ return getLocationRaw().equals(TheRift.LOCATION);
+ }
+
+ public static boolean isInjected() {
+ return isInjected;
+ }
+
+ /**
+ * @return the profile parsed from the player list.
+ */
+ public static String getProfile() {
+ return profile;
+ }
+
+ /**
+ * @return the server parsed from /locraw.
+ */
+ public static String getServer() {
+ return server;
+ }
+
+ /**
+ * @return the game type parsed from /locraw.
+ */
+ public static String getGameType() {
+ return gameType;
+ }
+
+ /**
+ * @return the location raw parsed from /locraw.
+ */
+ public static String getLocationRaw() {
+ return locationRaw;
+ }
+
+ /**
+ * @return the map parsed from /locraw.
+ */
+ public static String getMap() {
+ return map;
+ }
+
+ public static void init() {
+ ClientPlayConnectionEvents.JOIN.register(Utils::onClientWorldJoin);
+ ClientReceiveMessageEvents.ALLOW_GAME.register(Utils::onChatMessage);
+ ClientReceiveMessageEvents.GAME_CANCELED.register(Utils::onChatMessage); // Somehow this works even though onChatMessage returns a boolean
+ }
+
+ /**
+ * Updates all the fields stored in this class from the sidebar, player list, and /locraw.
+ */
+ public static void update() {
+ MinecraftClient client = MinecraftClient.getInstance();
+ updateScoreboard(client);
+ updatePlayerPresenceFromScoreboard(client);
+ updateFromPlayerList(client);
+ updateLocRaw();
+ }
+
+ /**
+ * Updates {@link #isOnSkyblock}, {@link #isInDungeons}, and {@link #isInjected} from the scoreboard.
+ */
+ public static void updatePlayerPresenceFromScoreboard(MinecraftClient client) {
+ List<String> sidebar = STRING_SCOREBOARD;
+
+ FabricLoader fabricLoader = FabricLoader.getInstance();
+ if ((client.world == null || client.isInSingleplayer() || sidebar == null || sidebar.isEmpty())) {
+ if (fabricLoader.isDevelopmentEnvironment()) {
+ sidebar = Collections.emptyList();
+ } else {
+ isOnSkyblock = false;
+ isInDungeons = false;
+ return;
+ }
+ }
+
+ if (sidebar.isEmpty() && !fabricLoader.isDevelopmentEnvironment()) return;
+ String string = sidebar.toString();
+
+ if (fabricLoader.isDevelopmentEnvironment() || isConnectedToHypixel(client)) {
+ if (!isOnHypixel) {
+ isOnHypixel = true;
+ }
+ if (fabricLoader.isDevelopmentEnvironment() || sidebar.get(0).contains("SKYBLOCK") || sidebar.get(0).contains("SKIBLOCK")) {
+ if (!isOnSkyblock) {
+ if (!isInjected) {
+ isInjected = true;
+ ItemTooltipCallback.EVENT.register(PriceInfoTooltip::onInjectTooltip);
+ }
+ isOnSkyblock = true;
+ SkyblockEvents.JOIN.invoker().onSkyblockJoin();
+ }
+ } else {
+ onLeaveSkyblock();
+ }
+ isInDungeons = fabricLoader.isDevelopmentEnvironment() || isOnSkyblock && string.contains("The Catacombs");
+ } else if (isOnHypixel) {
+ isOnHypixel = false;
+ onLeaveSkyblock();
+ }
+ }
+
+ private static boolean isConnectedToHypixel(MinecraftClient client) {
+ String serverAddress = (client.getCurrentServerEntry() != null) ? client.getCurrentServerEntry().address.toLowerCase() : "";
+ String serverBrand = (client.player != null && client.player.networkHandler != null && client.player.networkHandler.getBrand() != null) ? client.player.networkHandler.getBrand() : "";
+
+ return serverAddress.equalsIgnoreCase(ALTERNATE_HYPIXEL_ADDRESS) || serverAddress.contains("hypixel.net") || serverAddress.contains("hypixel.io") || serverBrand.contains("Hypixel BungeeCord");
+ }
+
+ private static void onLeaveSkyblock() {
+ if (isOnSkyblock) {
+ isOnSkyblock = false;
+ isInDungeons = false;
+ SkyblockEvents.LEAVE.invoker().onSkyblockLeave();
+ }
+ }
+
+ public static String getLocation() {
+ String location = null;
+ List<String> sidebarLines = STRING_SCOREBOARD;
+ try {
+ if (sidebarLines != null) {
+ for (String sidebarLine : sidebarLines) {
+ if (sidebarLine.contains("⏣")) location = sidebarLine;
+ if (sidebarLine.contains("ф")) location = sidebarLine; //Rift
+ }
+ if (location == null) location = "Unknown";
+ location = location.strip();
+ }
+ } catch (IndexOutOfBoundsException e) {
+ LOGGER.error("[Skyblocker] Failed to get location from sidebar", e);
+ }
+ return location;
+ }
+
+ public static double getPurse() {
+ String purseString = null;
+ double purse = 0;
+
+ List<String> sidebarLines = STRING_SCOREBOARD;
+ try {
+
+ if (sidebarLines != null) {
+ for (String sidebarLine : sidebarLines) {
+ if (sidebarLine.contains("Piggy:")) purseString = sidebarLine;
+ if (sidebarLine.contains("Purse:")) purseString = sidebarLine;
+ }
+ }
+ if (purseString != null) purse = Double.parseDouble(purseString.replaceAll("[^0-9.]", "").strip());
+ else purse = 0;
+
+ } catch (IndexOutOfBoundsException e) {
+ LOGGER.error("[Skyblocker] Failed to get purse from sidebar", e);
+ }
+ return purse;
+ }
+
+ public static int getBits() {
+ int bits = 0;
+ String bitsString = null;
+ List<String> sidebarLines = STRING_SCOREBOARD;
+ try {
+ if (sidebarLines != null) {
+ for (String sidebarLine : sidebarLines) {
+ if (sidebarLine.contains("Bits")) bitsString = sidebarLine;
+ }
+ }
+ if (bitsString != null) {
+ bits = Integer.parseInt(bitsString.replaceAll("[^0-9.]", "").strip());
+ }
+ } catch (IndexOutOfBoundsException e) {
+ LOGGER.error("[Skyblocker] Failed to get bits from sidebar", e);
+ }
+ return bits;
+ }
+
+ private static void updateScoreboard(MinecraftClient client) {
+ try {
+ TEXT_SCOREBOARD.clear();
+ STRING_SCOREBOARD.clear();
+
+ ClientPlayerEntity player = client.player;
+ if (player == null) return;
+
+ Scoreboard scoreboard = player.getScoreboard();
+ ScoreboardObjective objective = scoreboard.getObjectiveForSlot(ScoreboardDisplaySlot.FROM_ID.apply(1));
+ ObjectArrayList<Text> textLines = new ObjectArrayList<>();
+ ObjectArrayList<String> stringLines = new ObjectArrayList<>();
+
+ for (ScoreboardPlayerScore score : scoreboard.getAllPlayerScores(objective)) {
+ Team team = scoreboard.getPlayerTeam(score.getPlayerName());
+
+ if (team != null) {
+ Text textLine = Text.empty().append(team.getPrefix().copy()).append(team.getSuffix().copy());
+ String strLine = team.getPrefix().getString() + team.getSuffix().getString();
+
+ if (!strLine.trim().isEmpty()) {
+ String formatted = Formatting.strip(strLine);
+
+ textLines.add(textLine);
+ stringLines.add(formatted);
+ }
+ }
+ }
+
+ if (objective != null) {
+ stringLines.add(objective.getDisplayName().getString());
+ textLines.add(Text.empty().append(objective.getDisplayName().copy()));
+
+ Collections.reverse(stringLines);
+ Collections.reverse(textLines);
+ }
+
+ TEXT_SCOREBOARD.addAll(textLines);
+ STRING_SCOREBOARD.addAll(stringLines);
+ } catch (NullPointerException e) {
+ //Do nothing
+ }
+ }
+
+ private static void updateFromPlayerList(MinecraftClient client) {
+ if (client.getNetworkHandler() == null) {
+ return;
+ }
+ for (PlayerListEntry playerListEntry : client.getNetworkHandler().getPlayerList()) {
+ if (playerListEntry.getDisplayName() == null) {
+ continue;
+ }
+ String name = playerListEntry.getDisplayName().getString();
+ if (name.startsWith(PROFILE_PREFIX)) {
+ profile = name.substring(PROFILE_PREFIX.length());
+ }
+ }
+ }
+
+ public static void onClientWorldJoin(ClientPlayNetworkHandler handler, PacketSender sender, MinecraftClient client) {
+ clientWorldJoinTime = System.currentTimeMillis();
+ resetLocRawInfo();
+ }
+
+ /**
+ * Sends /locraw to the server if the player is on skyblock and on a new island.
+ */
+ private static void updateLocRaw() {
+ if (isOnSkyblock) {
+ long currentTime = System.currentTimeMillis();
+ if (!sentLocRaw && canSendLocRaw && currentTime > clientWorldJoinTime + 1000) {
+ MessageScheduler.INSTANCE.sendMessageAfterCooldown("/locraw");
+ sentLocRaw = true;
+ canSendLocRaw = false;
+ }
+ } else {
+ resetLocRawInfo();
+ }
+ }
+
+ /**
+ * Parses the /locraw reply from the server
+ *
+ * @return not display the message in chat is the command is sent by the mod
+ */
+ public static boolean onChatMessage(Text text, boolean overlay) {
+ String message = text.getString();
+ if (message.startsWith("{\"server\":") && message.endsWith("}")) {
+ JsonObject locRaw = JsonParser.parseString(message).getAsJsonObject();
+ if (locRaw.has("server")) {
+ server = locRaw.get("server").getAsString();
+ if (locRaw.has("gameType")) {
+ gameType = locRaw.get("gameType").getAsString();
+ }
+ if (locRaw.has("mode")) {
+ locationRaw = locRaw.get("mode").getAsString();
+ }
+ if (locRaw.has("map")) {
+ map = locRaw.get("map").getAsString();
+ }
+
+ boolean shouldFilter = !sentLocRaw;
+ sentLocRaw = false;
+
+ return shouldFilter;
+ }
+ }
+ return true;
+ }
+
+ private static void resetLocRawInfo() {
+ sentLocRaw = false;
+ canSendLocRaw = true;
+ server = "";
+ gameType = "";
+ locationRaw = "";
+ map = "";
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/chat/ChatFilterResult.java b/src/main/java/de/hysky/skyblocker/utils/chat/ChatFilterResult.java
new file mode 100644
index 00000000..5a94682a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/chat/ChatFilterResult.java
@@ -0,0 +1,18 @@
+package de.hysky.skyblocker.utils.chat;
+
+import net.minecraft.client.resource.language.I18n;
+public enum ChatFilterResult {
+ // Skip this one / no action
+ PASS,
+ // Filter
+ FILTER,
+ // Move to action bar
+ ACTION_BAR;
+ // Skip remaining checks, don't filter
+ // null
+
+ @Override
+ public String toString() {
+ return I18n.translate("text.autoconfig.skyblocker.option.messages.chatFilterResult." + name());
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java b/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java
new file mode 100644
index 00000000..7892445e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java
@@ -0,0 +1,89 @@
+package de.hysky.skyblocker.utils.chat;
+
+import de.hysky.skyblocker.skyblock.filters.*;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.skyblock.barn.HungryHiker;
+import de.hysky.skyblocker.skyblock.barn.TreasureHunter;
+import de.hysky.skyblocker.skyblock.dungeon.Reparty;
+import de.hysky.skyblocker.skyblock.dungeon.ThreeWeirdos;
+import de.hysky.skyblocker.skyblock.dungeon.Trivia;
+import de.hysky.skyblocker.skyblock.dwarven.Fetchur;
+import de.hysky.skyblocker.skyblock.dwarven.Puzzler;
+import de.hysky.skyblocker.skyblock.filters.*;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.api.event.EventFactory;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.text.Text;
+
+@FunctionalInterface
+public interface ChatMessageListener {
+ /**
+ * An event called when a game message is received. Register your listeners in {@link ChatMessageListener#init()}.
+ */
+ Event<ChatMessageListener> EVENT = EventFactory.createArrayBacked(ChatMessageListener.class,
+ (listeners) -> (message, asString) -> {
+ for (ChatMessageListener listener : listeners) {
+ ChatFilterResult result = listener.onMessage(message, asString);
+ if (result != ChatFilterResult.PASS) return result;
+ }
+ return ChatFilterResult.PASS;
+ });
+
+ /**
+ * Registers {@link ChatMessageListener}s to {@link ChatMessageListener#EVENT} and registers {@link ChatMessageListener#EVENT} to {@link ClientReceiveMessageEvents#ALLOW_GAME}
+ */
+ static void init() {
+ ChatMessageListener[] listeners = new ChatMessageListener[]{
+ // Features
+ new Fetchur(),
+ new Puzzler(),
+ new Reparty(),
+ new ThreeWeirdos(),
+ new Trivia(),
+ new TreasureHunter(),
+ new HungryHiker(),
+ // Filters
+ new AbilityFilter(),
+ new AdFilter(),
+ new AoteFilter(),
+ new ComboFilter(),
+ new HealFilter(),
+ new ImplosionFilter(),
+ new MoltenWaveFilter(),
+ new TeleportPadFilter(),
+ new AutopetFilter(),
+ new ShowOffFilter()
+ };
+ // Register all listeners to EVENT
+ for (ChatMessageListener listener : listeners) {
+ EVENT.register(listener);
+ }
+ // Register EVENT to ClientReceiveMessageEvents.ALLOW_GAME from fabric api
+ ClientReceiveMessageEvents.ALLOW_GAME.register((message, overlay) -> {
+ if (!Utils.isOnSkyblock()) {
+ return true;
+ }
+ ChatFilterResult result = EVENT.invoker().onMessage(message, message.getString());
+ switch (result) {
+ case ACTION_BAR -> {
+ if (overlay) {
+ return true;
+ }
+ ClientPlayerEntity player = MinecraftClient.getInstance().player;
+ if (player != null) {
+ player.sendMessage(message, true);
+ return false;
+ }
+ }
+ case FILTER -> {
+ return false;
+ }
+ }
+ return true;
+ });
+ }
+
+ ChatFilterResult onMessage(Text message, String asString);
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/chat/ChatPatternListener.java b/src/main/java/de/hysky/skyblocker/utils/chat/ChatPatternListener.java
new file mode 100644
index 00000000..708af280
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/chat/ChatPatternListener.java
@@ -0,0 +1,30 @@
+package de.hysky.skyblocker.utils.chat;
+
+import net.minecraft.text.Text;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public abstract class ChatPatternListener implements ChatMessageListener {
+ protected static final String NUMBER = "-?[0-9]{1,3}(?>,[0-9]{3})*(?:\\.[1-9])?";
+ public final Pattern pattern;
+
+ public ChatPatternListener(String pattern) {
+ this.pattern = Pattern.compile(pattern);
+ }
+
+ @Override
+ public final ChatFilterResult onMessage(Text message, String asString) {
+ ChatFilterResult state = state();
+ if (state == ChatFilterResult.PASS) return ChatFilterResult.PASS;
+ Matcher m = pattern.matcher(asString);
+ if (m.matches() && onMatch(message, m)) {
+ return state;
+ }
+ return ChatFilterResult.PASS;
+ }
+
+ protected abstract ChatFilterResult state();
+
+ protected abstract boolean onMatch(Text message, Matcher matcher);
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/discord/DiscordRPCManager.java b/src/main/java/de/hysky/skyblocker/utils/discord/DiscordRPCManager.java
new file mode 100644
index 00000000..f0589a0b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/discord/DiscordRPCManager.java
@@ -0,0 +1,122 @@
+package de.hysky.skyblocker.utils.discord;
+
+
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.events.SkyblockEvents;
+import de.hysky.skyblocker.utils.Utils;
+import meteordevelopment.discordipc.DiscordIPC;
+import meteordevelopment.discordipc.RichPresence;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.text.DecimalFormat;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Manages the discord rich presence. Automatically connects to discord and displays a customizable activity when playing Skyblock.
+ */
+public class DiscordRPCManager {
+ public static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("###,###.##");
+ public static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Discord RPC");
+ /**
+ * The update task used to avoid multiple update tasks running simultaneously.
+ */
+ public static CompletableFuture<Void> updateTask;
+ public static long startTimeStamp;
+ public static int cycleCount;
+
+ public static void init() {
+ SkyblockEvents.LEAVE.register(DiscordRPCManager::initAndUpdatePresence);
+ SkyblockEvents.JOIN.register(() -> {
+ startTimeStamp = System.currentTimeMillis();
+ initAndUpdatePresence(true);
+ });
+ }
+
+ /**
+ * Checks the {@link SkyblockerConfig.RichPresence#customMessage custom message}, updates {@link #cycleCount} if enabled, and updates rich presence.
+ */
+ public static void updateDataAndPresence() {
+ // If the custom message is empty, discord will keep the last message, this is can serve as a default if the user doesn't want a custom message
+ if (SkyblockerConfigManager.get().richPresence.customMessage.isEmpty()) {
+ SkyblockerConfigManager.get().richPresence.customMessage = "Playing Skyblock";
+ SkyblockerConfigManager.save();
+ }
+ if (SkyblockerConfigManager.get().richPresence.cycleMode) cycleCount = (cycleCount + 1) % 3;
+ initAndUpdatePresence();
+ }
+
+ /**
+ * @see #initAndUpdatePresence(boolean)
+ */
+ private static void initAndUpdatePresence() {
+ initAndUpdatePresence(false);
+ }
+
+ /**
+ * Updates discord presence asynchronously.
+ * <p>
+ * When the {@link #updateTask previous update} does not exist or {@link CompletableFuture#isDone() has completed}:
+ * <p>
+ * Connects to discord if {@link SkyblockerConfig.RichPresence#enableRichPresence rich presence is enabled},
+ * the player {@link Utils#isOnSkyblock() is on Skyblock}, and {@link DiscordIPC#isConnected() discord is not already connected}.
+ * Updates the presence if {@link SkyblockerConfig.RichPresence#enableRichPresence rich presence is enabled}
+ * and the player {@link Utils#isOnSkyblock() is on Skyblock}.
+ * Stops the connection if {@link SkyblockerConfig.RichPresence#enableRichPresence rich presence is disabled}
+ * or the player {@link Utils#isOnSkyblock() is not on Skyblock} and {@link DiscordIPC#isConnected() discord is connected}.
+ * Saves the update task in {@link #updateTask}
+ *
+ * @param initialization whether this is the first time the presence is being updates. If {@code true}, a message will be logged
+ * if {@link SkyblockerConfig.RichPresence#enableRichPresence rich presence is disabled}.
+ */
+ private static void initAndUpdatePresence(boolean initialization) {
+ if (updateTask == null || updateTask.isDone()) {
+ updateTask = CompletableFuture.runAsync(() -> {
+ if (SkyblockerConfigManager.get().richPresence.enableRichPresence && Utils.isOnSkyblock()) {
+ if (!DiscordIPC.isConnected()) {
+ if (DiscordIPC.start(934607927837356052L, null)) {
+ LOGGER.info("Discord RPC started successfully");
+ } else {
+ LOGGER.error("Discord RPC failed to start");
+ return;
+ }
+ }
+ DiscordIPC.setActivity(buildPresence());
+ } else if (DiscordIPC.isConnected()) {
+ DiscordIPC.stop();
+ LOGGER.info("Discord RPC stopped");
+ } else if (initialization) {
+ LOGGER.info("Discord RPC is currently disabled");
+ }
+ });
+ }
+ }
+
+ public static RichPresence buildPresence() {
+ RichPresence presence = new RichPresence();
+ presence.setLargeImage("skyblocker-default", null);
+ presence.setStart(startTimeStamp);
+ presence.setDetails(SkyblockerConfigManager.get().richPresence.customMessage);
+ presence.setState(getInfo());
+ return presence;
+ }
+
+ public static String getInfo() {
+ String info = null;
+ if (!SkyblockerConfigManager.get().richPresence.cycleMode) {
+ switch (SkyblockerConfigManager.get().richPresence.info) {
+ case BITS -> info = "Bits: " + DECIMAL_FORMAT.format(Utils.getBits());
+ case PURSE -> info = "Purse: " + DECIMAL_FORMAT.format(Utils.getPurse());
+ case LOCATION -> info = Utils.getLocation();
+ }
+ } else if (SkyblockerConfigManager.get().richPresence.cycleMode) {
+ switch (cycleCount) {
+ case 0 -> info = "Bits: " + DECIMAL_FORMAT.format(Utils.getBits());
+ case 1 -> info = "Purse: " + DECIMAL_FORMAT.format(Utils.getPurse());
+ case 2 -> info = Utils.getLocation();
+ }
+ }
+ return info;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/FrustumUtils.java b/src/main/java/de/hysky/skyblocker/utils/render/FrustumUtils.java
new file mode 100644
index 00000000..3fe79e43
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/render/FrustumUtils.java
@@ -0,0 +1,21 @@
+package de.hysky.skyblocker.utils.render;
+
+import de.hysky.skyblocker.mixin.accessor.FrustumInvoker;
+import de.hysky.skyblocker.mixin.accessor.WorldRendererAccessor;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.Frustum;
+import net.minecraft.util.math.Box;
+
+public class FrustumUtils {
+ public static Frustum getFrustum() {
+ return ((WorldRendererAccessor) MinecraftClient.getInstance().worldRenderer).getFrustum();
+ }
+
+ public static boolean isVisible(Box box) {
+ return getFrustum().isVisible(box);
+ }
+
+ public static boolean isVisible(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) {
+ return ((FrustumInvoker) getFrustum()).invokeIsVisible(minX, minY, minZ, maxX, maxY, maxZ);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java
new file mode 100644
index 00000000..4630149c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java
@@ -0,0 +1,247 @@
+package de.hysky.skyblocker.utils.render;
+
+import com.mojang.blaze3d.platform.GlStateManager.DstFactor;
+import com.mojang.blaze3d.platform.GlStateManager.SrcFactor;
+import com.mojang.blaze3d.systems.RenderSystem;
+import de.hysky.skyblocker.mixin.accessor.BeaconBlockEntityRendererInvoker;
+import me.x150.renderer.render.Renderer3d;
+import de.hysky.skyblocker.utils.render.culling.OcclusionCulling;
+import de.hysky.skyblocker.utils.render.title.Title;
+import de.hysky.skyblocker.utils.render.title.TitleContainer;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.render.*;
+import net.minecraft.client.render.VertexFormat.DrawMode;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.sound.SoundEvents;
+import net.minecraft.text.OrderedText;
+import net.minecraft.text.Text;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Box;
+import net.minecraft.util.math.Vec3d;
+import org.joml.Matrix3f;
+import org.joml.Matrix4f;
+import org.joml.Vector3f;
+import org.lwjgl.opengl.GL11;
+
+import java.awt.*;
+
+public class RenderHelper {
+ 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();
+
+ public static void renderFilledThroughWallsWithBeaconBeam(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha) {
+ renderFilledThroughWalls(context, pos, colorComponents, alpha);
+ renderBeaconBeam(context, pos, colorComponents);
+ }
+
+ public static void renderFilledThroughWalls(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha) {
+ if (FrustumUtils.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1)) {
+ Renderer3d.renderThroughWalls();
+ renderFilled(context, pos, colorComponents, alpha);
+ Renderer3d.stopRenderThroughWalls();
+ }
+ }
+
+ public static void renderFilledIfVisible(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha) {
+ if (OcclusionCulling.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1)) {
+ renderFilled(context, pos, colorComponents, alpha);
+ }
+ }
+
+ private static void renderFilled(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha) {
+ Renderer3d.renderFilled(context.matrixStack(), new Color(colorComponents[0], colorComponents[1], colorComponents[2], alpha), Vec3d.of(pos), ONE);
+ }
+
+ private static void renderBeaconBeam(WorldRenderContext context, BlockPos pos, float[] colorComponents) {
+ if (FrustumUtils.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, MAX_OVERWORLD_BUILD_HEIGHT, pos.getZ() + 1)) {
+ MatrixStack matrices = context.matrixStack();
+ Vec3d camera = context.camera().getPos();
+
+ matrices.push();
+ matrices.translate(pos.getX() - camera.getX(), pos.getY() - camera.getY(), pos.getZ() - camera.getZ());
+
+ Tessellator tessellator = RenderSystem.renderThreadTesselator();
+ BufferBuilder buffer = tessellator.getBuffer();
+ VertexConsumerProvider.Immediate consumer = VertexConsumerProvider.immediate(buffer);
+
+ BeaconBlockEntityRendererInvoker.renderBeam(matrices, consumer, context.tickDelta(), context.world().getTime(), 0, MAX_OVERWORLD_BUILD_HEIGHT, colorComponents);
+
+ consumer.draw();
+ matrices.pop();
+ }
+ }
+
+ /**
+ * Renders the outline of a box with the specified color components and line width.
+ * This does not use renderer since renderer draws outline using debug lines with a fixed width.
+ */
+ public static void renderOutline(WorldRenderContext context, Box box, float[] colorComponents, float lineWidth) {
+ if (FrustumUtils.isVisible(box)) {
+ 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);
+ RenderSystem.lineWidth(lineWidth);
+ RenderSystem.disableCull();
+ RenderSystem.enableDepthTest();
+
+ matrices.push();
+ matrices.translate(-camera.getX(), -camera.getY(), -camera.getZ());
+
+ buffer.begin(DrawMode.LINES, VertexFormats.LINES);
+ WorldRenderer.drawBox(matrices, buffer, box, colorComponents[0], colorComponents[1], colorComponents[2], 1f);
+ tessellator.draw();
+
+ matrices.pop();
+ RenderSystem.lineWidth(1f);
+ RenderSystem.enableCull();
+ RenderSystem.disableDepthTest();
+ }
+ }
+
+ /**
+ * Draws lines from point to point.<br><br>
+ * <p>
+ * Tip: To draw lines from the center of a block, offset the X, Y and Z each by 0.5
+ *
+ * @param context The WorldRenderContext which supplies the matrices and tick delta
+ * @param points The points from which to draw lines between
+ * @param colorComponents An array of R, G and B color components
+ * @param alpha The alpha of the lines
+ * @param lineWidth The width of the lines
+ */
+ public static void renderLinesFromPoints(WorldRenderContext context, Vec3d[] points, float[] colorComponents, float alpha, float lineWidth) {
+ Vec3d camera = context.camera().getPos();
+ MatrixStack matrices = context.matrixStack();
+
+ matrices.push();
+ matrices.translate(-camera.x, -camera.y, -camera.z);
+
+ Tessellator tessellator = RenderSystem.renderThreadTesselator();
+ BufferBuilder buffer = tessellator.getBuffer();
+ Matrix4f projectionMatrix = matrices.peek().getPositionMatrix();
+ Matrix3f normalMatrix = matrices.peek().getNormalMatrix();
+
+ GL11.glEnable(GL11.GL_LINE_SMOOTH);
+ GL11.glHint(GL11.GL_LINE_SMOOTH_HINT, GL11.GL_NICEST);
+
+ RenderSystem.setShader(GameRenderer::getRenderTypeLinesProgram);
+ RenderSystem.setShaderColor(1f, 1f, 1f, 1f);
+ RenderSystem.lineWidth(lineWidth);
+ RenderSystem.enableBlend();
+ RenderSystem.blendFunc(SrcFactor.SRC_ALPHA, DstFactor.ONE_MINUS_SRC_ALPHA);
+ RenderSystem.disableCull();
+ RenderSystem.enableDepthTest();
+
+ buffer.begin(DrawMode.LINE_STRIP, VertexFormats.LINES);
+
+ for (int i = 0; i < points.length; i++) {
+ Vec3d point = points[i];
+ Vec3d nextPoint = (i + 1 == points.length) ? points[i - 1] : points[i + 1];
+ Vector3f normalVec = new Vector3f((float) nextPoint.getX(), (float) nextPoint.getY(), (float) nextPoint.getZ()).sub((float) point.getX(), (float) point.getY(), (float) point.getZ()).normalize();
+
+ buffer
+ .vertex(projectionMatrix, (float) point.getX(), (float) point.getY(), (float) point.getZ())
+ .color(colorComponents[0], colorComponents[1], colorComponents[2], alpha)
+ .normal(normalMatrix, normalVec.x, normalVec.y, normalVec.z)
+ .next();
+ }
+
+ tessellator.draw();
+
+ matrices.pop();
+ GL11.glDisable(GL11.GL_LINE_SMOOTH);
+ RenderSystem.lineWidth(1f);
+ RenderSystem.disableBlend();
+ RenderSystem.defaultBlendFunc();
+ RenderSystem.enableCull();
+ RenderSystem.disableDepthTest();
+ }
+
+ public static void renderText(WorldRenderContext context, Text text, Vec3d pos, boolean seeThrough) {
+ renderText(context, text, pos, 1, seeThrough);
+ }
+
+ public static void renderText(WorldRenderContext context, Text text, Vec3d pos, float scale, boolean seeThrough) {
+ renderText(context, text, pos, scale, 0, seeThrough);
+ }
+
+ public static void renderText(WorldRenderContext context, Text text, Vec3d pos, float scale, float yOffset, boolean seeThrough) {
+ renderText(context, text.asOrderedText(), pos, scale, yOffset, seeThrough);
+ }
+
+ /**
+ * Renders text in the world space.
+ *
+ * @param seeThrough Whether the text should be able to be seen through walls or not.
+ */
+ public static void renderText(WorldRenderContext context, OrderedText text, Vec3d pos, float scale, float yOffset, boolean seeThrough) {
+ MatrixStack matrices = context.matrixStack();
+ Vec3d camera = context.camera().getPos();
+ TextRenderer textRenderer = client.textRenderer;
+
+ scale *= 0.025f;
+
+ matrices.push();
+ matrices.translate(pos.getX() - camera.getX(), pos.getY() - camera.getY(), pos.getZ() - camera.getZ());
+ matrices.peek().getPositionMatrix().mul(RenderSystem.getModelViewMatrix());
+ matrices.multiply(context.camera().getRotation());
+ matrices.scale(-scale, -scale, scale);
+
+ Matrix4f positionMatrix = matrices.peek().getPositionMatrix();
+ float xOffset = -textRenderer.getWidth(text) / 2f;
+
+ Tessellator tessellator = RenderSystem.renderThreadTesselator();
+ BufferBuilder buffer = tessellator.getBuffer();
+ VertexConsumerProvider.Immediate consumers = VertexConsumerProvider.immediate(buffer);
+
+ RenderSystem.depthFunc(seeThrough ? GL11.GL_ALWAYS : GL11.GL_LEQUAL);
+
+ textRenderer.draw(text, xOffset, yOffset, 0xFFFFFFFF, false, positionMatrix, consumers, TextRenderer.TextLayerType.SEE_THROUGH, 0, LightmapTextureManager.MAX_LIGHT_COORDINATE);
+ consumers.draw();
+
+ RenderSystem.depthFunc(GL11.GL_LEQUAL);
+ matrices.pop();
+ }
+
+ /**
+ * Adds the title to {@link TitleContainer} and {@link #playNotificationSound() plays the notification sound} if the title is not in the {@link TitleContainer} already.
+ * No checking needs to be done on whether the title is in the {@link TitleContainer} already by the caller.
+ *
+ * @param title the title
+ */
+ public static void displayInTitleContainerAndPlaySound(Title title) {
+ if (TitleContainer.addTitle(title)) {
+ playNotificationSound();
+ }
+ }
+
+ /**
+ * Adds the title to {@link TitleContainer} for a set number of ticks and {@link #playNotificationSound() plays the notification sound} if the title is not in the {@link TitleContainer} already.
+ * No checking needs to be done on whether the title is in the {@link TitleContainer} already by the caller.
+ *
+ * @param title the title
+ * @param ticks the number of ticks the title will remain
+ */
+ public static void displayInTitleContainerAndPlaySound(Title title, int ticks) {
+ if (TitleContainer.addTitle(title, ticks)) {
+ playNotificationSound();
+ }
+ }
+
+ private static void playNotificationSound() {
+ if (MinecraftClient.getInstance().player != null) {
+ MinecraftClient.getInstance().player.playSound(SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP, 100f, 0.1f);
+ }
+ }
+
+ public static boolean pointIsInArea(double x, double y, double x1, double y1, double x2, double y2) {
+ return x >= x1 && x <= x2 && y >= y1 && y <= y2;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCulling.java b/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCulling.java
new file mode 100644
index 00000000..5f8d1592
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCulling.java
@@ -0,0 +1,47 @@
+package de.hysky.skyblocker.utils.render.culling;
+
+import com.logisticscraft.occlusionculling.OcclusionCullingInstance;
+import com.logisticscraft.occlusionculling.cache.ArrayOcclusionCache;
+import com.logisticscraft.occlusionculling.util.Vec3d;
+import de.hysky.skyblocker.utils.render.FrustumUtils;
+import net.minecraft.client.MinecraftClient;
+
+public class OcclusionCulling {
+ private static final int TRACING_DISTANCE = 128;
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+ private static OcclusionCullingInstance instance = null;
+
+ // Reused objects to reduce allocation overhead
+ private static final Vec3d cameraPos = new Vec3d(0, 0, 0);
+ private static final Vec3d min = new Vec3d(0, 0, 0);
+ private static final Vec3d max = new Vec3d(0, 0, 0);
+
+ /**
+ * Initializes the occlusion culling instance
+ */
+ public static void init() {
+ instance = new OcclusionCullingInstance(TRACING_DISTANCE, new WorldProvider(), new ArrayOcclusionCache(TRACING_DISTANCE), 2);
+ }
+
+ private static void updateCameraPos() {
+ var camera = CLIENT.gameRenderer.getCamera().getPos();
+ cameraPos.set(camera.x, camera.y, camera.z);
+ }
+
+ /**
+ * This first checks checks if the bounding box is within the camera's FOV, if
+ * it is then it checks for whether it's occluded or not.
+ *
+ * @return A boolean representing whether the bounding box is fully visible or
+ * not.
+ */
+ public static boolean isVisible(double x1, double y1, double z1, double x2, double y2, double z2) {
+ if (!FrustumUtils.isVisible(x1, y1, z1, x2, y2, z2)) return false;
+
+ updateCameraPos();
+ min.set(x1, y1, z1);
+ max.set(x2, y2, z2);
+
+ return instance.isAABBVisible(min, max, cameraPos);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/culling/WorldProvider.java b/src/main/java/de/hysky/skyblocker/utils/render/culling/WorldProvider.java
new file mode 100644
index 00000000..7ee0f0ed
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/render/culling/WorldProvider.java
@@ -0,0 +1,28 @@
+package de.hysky.skyblocker.utils.render.culling;
+
+import com.logisticscraft.occlusionculling.DataProvider;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.util.math.BlockPos;
+
+public class WorldProvider implements DataProvider {
+ private final static MinecraftClient CLIENT = MinecraftClient.getInstance();
+ private ClientWorld world = null;
+
+ @Override
+ public boolean prepareChunk(int chunkX, int chunkZ) {
+ this.world = CLIENT.world;
+ return this.world != null;
+ }
+
+ @Override
+ public boolean isOpaqueFullCube(int x, int y, int z) {
+ BlockPos pos = new BlockPos(x, y, z);
+ return this.world.getBlockState(pos).isOpaqueFullCube(this.world, pos);
+ }
+
+ @Override
+ public void cleanup() {
+ this.world = null;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/culling/package-info.java b/src/main/java/de/hysky/skyblocker/utils/render/culling/package-info.java
new file mode 100644
index 00000000..1d5cdf98
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/render/culling/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Package dedicated to occlusion culling utilities
+ */
+package de.hysky.skyblocker.utils.render.culling; \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ColorHighlight.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ColorHighlight.java
new file mode 100644
index 00000000..5451e1a8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ColorHighlight.java
@@ -0,0 +1,24 @@
+package de.hysky.skyblocker.utils.render.gui;
+
+public record ColorHighlight(int slot, int color) {
+ private static final int RED_HIGHLIGHT = 64 << 24 | 255 << 16;
+ private static final int YELLOW_HIGHLIGHT = 128 << 24 | 255 << 16 | 255 << 8;
+ private static final int GREEN_HIGHLIGHT = 128 << 24 | 64 << 16 | 196 << 8 | 64;
+ private static final int GRAY_HIGHLIGHT = 128 << 24 | 64 << 16 | 64 << 8 | 64;
+
+ public static ColorHighlight red(int slot) {
+ return new ColorHighlight(slot, RED_HIGHLIGHT);
+ }
+
+ public static ColorHighlight yellow(int slot) {
+ return new ColorHighlight(slot, YELLOW_HIGHLIGHT);
+ }
+
+ public static ColorHighlight green(int slot) {
+ return new ColorHighlight(slot, GREEN_HIGHLIGHT);
+ }
+
+ public static ColorHighlight gray(int slot) {
+ return new ColorHighlight(slot, GRAY_HIGHLIGHT);
+ }
+} \ No newline at end of file
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
new file mode 100644
index 00000000..cf67e84c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java
@@ -0,0 +1,44 @@
+package de.hysky.skyblocker.utils.render.gui;
+
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
+import net.minecraft.item.ItemStack;
+
+import java.util.List;
+import java.util.Map;
+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);
+ }
+
+ protected abstract boolean isEnabled();
+
+ public Pattern getName() {
+ return containerName;
+ }
+
+ protected void start(GenericContainerScreen screen) {
+ }
+
+ protected void reset() {
+ }
+
+ protected abstract List<ColorHighlight> getColors(String[] groups, Map<Integer, ItemStack> slots);
+
+ protected void trimEdges(Map<Integer, ItemStack> slots, int rows) {
+ for (int i = 0; i < rows; i++) {
+ slots.remove(9 * i);
+ slots.remove(9 * i + 8);
+ }
+ for (int i = 1; i < 8; i++) {
+ slots.remove(i);
+ slots.remove((rows - 1) * 9 + i);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java
new file mode 100644
index 00000000..60e2b4e4
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java
@@ -0,0 +1,125 @@
+package de.hysky.skyblocker.utils.render.gui;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import de.hysky.skyblocker.mixin.accessor.HandledScreenAccessor;
+import de.hysky.skyblocker.skyblock.dungeon.CroesusHelper;
+import de.hysky.skyblocker.skyblock.dungeon.terminal.ColorTerminal;
+import de.hysky.skyblocker.skyblock.dungeon.terminal.OrderTerminal;
+import de.hysky.skyblocker.skyblock.dungeon.terminal.StartsWithTerminal;
+import de.hysky.skyblocker.skyblock.experiment.ChronomatronSolver;
+import de.hysky.skyblocker.skyblock.experiment.SuperpairsSolver;
+import de.hysky.skyblocker.skyblock.experiment.UltrasequencerSolver;
+import de.hysky.skyblocker.utils.Utils;
+import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Manager class for {@link ContainerSolver}s like terminal solvers and experiment solvers. To add a new gui solver, extend {@link ContainerSolver} and register it in {@link #ContainerSolverManager()}.
+ */
+public class ContainerSolverManager {
+ private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("");
+ private final ContainerSolver[] solvers;
+ private ContainerSolver currentSolver = null;
+ private String[] groups;
+ private List<ColorHighlight> highlights;
+
+ public ContainerSolverManager() {
+ solvers = new ContainerSolver[]{
+ new ColorTerminal(),
+ new OrderTerminal(),
+ new StartsWithTerminal(),
+ new CroesusHelper(),
+ new ChronomatronSolver(),
+ new SuperpairsSolver(),
+ new UltrasequencerSolver()
+ };
+ }
+
+ public ContainerSolver getCurrentSolver() {
+ return currentSolver;
+ }
+
+ public void init() {
+ ScreenEvents.BEFORE_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
+ if (Utils.isOnSkyblock() && screen instanceof GenericContainerScreen genericContainerScreen) {
+ ScreenEvents.afterRender(screen).register((screen1, context, mouseX, mouseY, delta) -> {
+ MatrixStack matrices = context.getMatrices();
+ matrices.push();
+ matrices.translate(((HandledScreenAccessor) genericContainerScreen).getX(), ((HandledScreenAccessor) genericContainerScreen).getY(), 300);
+ onDraw(context, genericContainerScreen.getScreenHandler().slots.subList(0, genericContainerScreen.getScreenHandler().getRows() * 9));
+ matrices.pop();
+ });
+ ScreenEvents.remove(screen).register(screen1 -> clearScreen());
+ onSetScreen(genericContainerScreen);
+ } else {
+ clearScreen();
+ }
+ });
+ }
+
+ public void onSetScreen(@NotNull GenericContainerScreen screen) {
+ String screenName = screen.getTitle().getString();
+ Matcher matcher = PLACEHOLDER_PATTERN.matcher(screenName);
+ for (ContainerSolver solver : solvers) {
+ if (solver.isEnabled()) {
+ matcher.usePattern(solver.getName());
+ matcher.reset();
+ if (matcher.matches()) {
+ currentSolver = solver;
+ groups = new String[matcher.groupCount()];
+ for (int i = 0; i < groups.length; i++) {
+ groups[i] = matcher.group(i + 1);
+ }
+ currentSolver.start(screen);
+ return;
+ }
+ }
+ }
+ clearScreen();
+ }
+
+ public void clearScreen() {
+ if (currentSolver != null) {
+ currentSolver.reset();
+ currentSolver = null;
+ }
+ }
+
+ public void markDirty() {
+ highlights = null;
+ }
+
+ public void onDraw(DrawContext context, List<Slot> slots) {
+ if (currentSolver == null)
+ return;
+ if (highlights == null)
+ highlights = currentSolver.getColors(groups, slotMap(slots));
+ RenderSystem.enableDepthTest();
+ RenderSystem.colorMask(true, true, true, false);
+ for (ColorHighlight highlight : highlights) {
+ Slot slot = slots.get(highlight.slot());
+ int color = highlight.color();
+ context.fillGradient(slot.x, slot.y, slot.x + 16, slot.y + 16, color, color);
+ }
+ RenderSystem.colorMask(true, true, true, true);
+ }
+
+ private Map<Integer, ItemStack> slotMap(List<Slot> slots) {
+ Map<Integer, ItemStack> slotMap = new TreeMap<>();
+ for (int i = 0; i < slots.size(); i++) {
+ slotMap.put(i, slots.get(i).getStack());
+ }
+ return slotMap;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/title/Title.java b/src/main/java/de/hysky/skyblocker/utils/render/title/Title.java
new file mode 100644
index 00000000..1e167afa
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/render/title/Title.java
@@ -0,0 +1,53 @@
+package de.hysky.skyblocker.utils.render.title;
+
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+/**
+ * Represents a title used for {@link TitleContainer}.
+ *
+ * @see TitleContainer
+ */
+public class Title {
+ private MutableText text;
+ protected float x = -1;
+ protected float y = -1;
+
+ /**
+ * Constructs a new title with the given translation key and formatting to be applied.
+ *
+ * @param textKey the translation key
+ * @param formatting the formatting to be applied to the text
+ */
+ public Title(String textKey, Formatting formatting) {
+ this(Text.translatable(textKey).formatted(formatting));
+ }
+
+ /**
+ * Constructs a new title with the given {@link MutableText}.
+ * Use {@link Text#literal(String)} or {@link Text#translatable(String)} to create a {@link MutableText}
+ *
+ * @param text the mutable text
+ */
+ public Title(MutableText text) {
+ this.text = text;
+ }
+
+ public MutableText getText() {
+ return text;
+ }
+
+ public void setText(MutableText text) {
+ this.text = text;
+ }
+
+ protected boolean isDefaultPos() {
+ return x == -1 && y == -1;
+ }
+
+ protected void resetPos() {
+ this.x = -1;
+ this.y = -1;
+ }
+}
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
new file mode 100644
index 00000000..487e3d8b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainer.java
@@ -0,0 +1,175 @@
+package de.hysky.skyblocker.utils.render.title;
+
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.util.math.MathHelper;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+public class TitleContainer {
+ /**
+ * The set of titles which will be rendered.
+ *
+ * @see #containsTitle(Title)
+ * @see #addTitle(Title)
+ * @see #addTitle(Title, int)
+ * @see #removeTitle(Title)
+ */
+ private static final Set<Title> titles = new LinkedHashSet<>();
+
+ public static void init() {
+ HudRenderCallback.EVENT.register(TitleContainer::render);
+ ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal("skyblocker")
+ .then(ClientCommandManager.literal("hud")
+ .then(ClientCommandManager.literal("titleContainer")
+ .executes(Scheduler.queueOpenScreenCommand(TitleContainerConfigScreen::new))))));
+ }
+
+ /**
+ * Returns {@code true} if the title is currently shown.
+ *
+ * @param title the title to check
+ * @return whether the title in currently shown
+ */
+ public static boolean containsTitle(Title title) {
+ return titles.contains(title);
+ }
+
+ /**
+ * Adds a title to be shown
+ *
+ * @param title the title to be shown
+ * @return whether the title is already currently being shown
+ */
+ public static boolean addTitle(Title title) {
+ if (titles.add(title)) {
+ title.resetPos();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Adds a title to be shown for a set number of ticks
+ *
+ * @param title the title to be shown
+ * @param ticks the number of ticks to show the title
+ * @return whether the title is already currently being shown
+ */
+ public static boolean addTitle(Title title, int ticks) {
+ if (addTitle(title)) {
+ Scheduler.INSTANCE.schedule(() -> TitleContainer.removeTitle(title), ticks);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Stops showing a title
+ *
+ * @param title the title to stop showing
+ */
+ public static void removeTitle(Title title) {
+ titles.remove(title);
+ }
+
+ private static void render(DrawContext context, float tickDelta) {
+ render(context, titles, SkyblockerConfigManager.get().general.titleContainer.x, SkyblockerConfigManager.get().general.titleContainer.y, tickDelta);
+ }
+
+ protected static void render(DrawContext context, Set<Title> titles, int xPos, int yPos, float tickDelta) {
+ var client = MinecraftClient.getInstance();
+ TextRenderer textRenderer = client.textRenderer;
+
+ // Calculate Scale to use
+ float scale = 3F * (SkyblockerConfigManager.get().general.titleContainer.titleContainerScale / 100F);
+
+ // Grab direction and alignment values
+ SkyblockerConfig.Direction direction = SkyblockerConfigManager.get().general.titleContainer.direction;
+ SkyblockerConfig.Alignment alignment = SkyblockerConfigManager.get().general.titleContainer.alignment;
+ // x/y refer to the starting position for the text
+ // y always starts at yPos
+ float x = 0;
+ float y = yPos;
+
+ //Calculate the width of combined text
+ float width = 0;
+ for (Title title : titles) {
+ width += textRenderer.getWidth(title.getText()) * scale + 10;
+ }
+
+ if (alignment == SkyblockerConfig.Alignment.MIDDLE) {
+ if (direction == SkyblockerConfig.Direction.HORIZONTAL) {
+ //If middle aligned horizontally, start the xPosition at half of the width to the left.
+ x = xPos - (width / 2);
+ } else {
+ //If middle aligned vertically, start at xPos, we will shift each text to the left later
+ x = xPos;
+ }
+ }
+ if (alignment == SkyblockerConfig.Alignment.LEFT || alignment == SkyblockerConfig.Alignment.RIGHT) {
+ //If left or right aligned, start at xPos, we will shift each text later
+ x = xPos;
+ }
+
+ for (Title title : titles) {
+
+ //Calculate which x the text should use
+ float xToUse;
+ if (direction == SkyblockerConfig.Direction.HORIZONTAL) {
+ xToUse = alignment == SkyblockerConfig.Alignment.RIGHT ?
+ x - (textRenderer.getWidth(title.getText()) * scale) : //if right aligned we need the text position to be aligned on the right side.
+ x;
+ } else {
+ xToUse = alignment == SkyblockerConfig.Alignment.MIDDLE ?
+ x - (textRenderer.getWidth(title.getText()) * scale) / 2 : //if middle aligned we need the text position to be aligned in the middle.
+ alignment == SkyblockerConfig.Alignment.RIGHT ?
+ x - (textRenderer.getWidth(title.getText()) * scale) : //if right aligned we need the text position to be aligned on the right side.
+ x;
+ }
+
+ //Start displaying the title at the correct position, not at the default position
+ if (title.isDefaultPos()) {
+ title.x = xToUse;
+ title.y = y;
+ }
+
+ //Lerp the texts x and y variables
+ title.x = MathHelper.lerp(tickDelta * 0.5F, title.x, xToUse);
+ title.y = MathHelper.lerp(tickDelta * 0.5F, title.y, y);
+
+ //Translate the matrix to the texts position and scale
+ context.getMatrices().push();
+ context.getMatrices().translate(title.x, title.y, 200);
+ context.getMatrices().scale(scale, scale, scale);
+
+ //Draw text
+ context.drawTextWithShadow(textRenderer, title.getText(), 0, 0, 0xFFFFFF);
+ context.getMatrices().pop();
+
+ //Calculate the x and y positions for the next title
+ if (direction == SkyblockerConfig.Direction.HORIZONTAL) {
+ if (alignment == SkyblockerConfig.Alignment.MIDDLE || alignment == SkyblockerConfig.Alignment.LEFT) {
+ //Move to the right if middle or left aligned
+ x += textRenderer.getWidth(title.getText()) * scale + 10;
+ }
+
+ if (alignment == SkyblockerConfig.Alignment.RIGHT) {
+ //Move to the left if right aligned
+ x -= textRenderer.getWidth(title.getText()) * scale + 10;
+ }
+ } else {
+ //Y always moves by the same amount if vertical
+ y += textRenderer.fontHeight * scale + 10;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainerConfigScreen.java b/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainerConfigScreen.java
new file mode 100644
index 00000000..5a42eeb4
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainerConfigScreen.java
@@ -0,0 +1,170 @@
+package de.hysky.skyblocker.utils.render.title;
+
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.util.math.Vector2f;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.Pair;
+import org.lwjgl.glfw.GLFW;
+
+import java.awt.*;
+import java.util.Set;
+
+public class TitleContainerConfigScreen extends Screen {
+ private final Title example1 = new Title(Text.literal("Test1").formatted(Formatting.RED));
+ private final Title example2 = new Title(Text.literal("Test23").formatted(Formatting.AQUA));
+ private final Title example3 = new Title(Text.literal("Testing1234").formatted(Formatting.DARK_GREEN));
+ private float hudX = SkyblockerConfigManager.get().general.titleContainer.x;
+ private float hudY = SkyblockerConfigManager.get().general.titleContainer.y;
+ private final Screen parent;
+
+ protected TitleContainerConfigScreen() {
+ this(null);
+ }
+
+ public TitleContainerConfigScreen(Screen parent) {
+ super(Text.of("Title Container HUD Config"));
+ this.parent = parent;
+ }
+
+ @Override
+ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
+ super.render(context, mouseX, mouseY, delta);
+ renderBackground(context, mouseX, mouseY, delta);
+ TitleContainer.render(context, Set.of(example1, example2, example3), (int) hudX, (int) hudY, delta);
+ SkyblockerConfig.Direction direction = SkyblockerConfigManager.get().general.titleContainer.direction;
+ SkyblockerConfig.Alignment alignment = SkyblockerConfigManager.get().general.titleContainer.alignment;
+ context.drawCenteredTextWithShadow(textRenderer, "Press Q/E to change Alignment: " + alignment, width / 2, textRenderer.fontHeight * 2, Color.WHITE.getRGB());
+ context.drawCenteredTextWithShadow(textRenderer, "Press R to change Direction: " + direction, width / 2, textRenderer.fontHeight * 3 + 5, Color.WHITE.getRGB());
+ context.drawCenteredTextWithShadow(textRenderer, "Press +/- to change Scale", width / 2, textRenderer.fontHeight * 4 + 10, Color.WHITE.getRGB());
+ context.drawCenteredTextWithShadow(textRenderer, "Right Click To Reset Position", width / 2, textRenderer.fontHeight * 5 + 15, Color.GRAY.getRGB());
+
+ Pair<Vector2f, Vector2f> boundingBox = getSelectionBoundingBox();
+ int x1 = (int) boundingBox.getLeft().getX();
+ int y1 = (int) boundingBox.getLeft().getY();
+ int x2 = (int) boundingBox.getRight().getX();
+ int y2 = (int) boundingBox.getRight().getY();
+
+ context.drawHorizontalLine(x1, x2, y1, Color.RED.getRGB());
+ context.drawHorizontalLine(x1, x2, y2, Color.RED.getRGB());
+ context.drawVerticalLine(x1, y1, y2, Color.RED.getRGB());
+ context.drawVerticalLine(x2, y1, y2, Color.RED.getRGB());
+ }
+
+ private Pair<Vector2f, Vector2f> getSelectionBoundingBox() {
+ SkyblockerConfig.Alignment alignment = SkyblockerConfigManager.get().general.titleContainer.alignment;
+
+ float midWidth = getSelectionWidth() / 2F;
+ float x1 = 0;
+ float x2 = 0;
+ float y1 = hudY;
+ float y2 = hudY + getSelectionHeight();
+ switch (alignment) {
+ case RIGHT -> {
+ x1 = hudX - midWidth * 2;
+ x2 = hudX;
+ }
+ case MIDDLE -> {
+ x1 = hudX - midWidth;
+ x2 = hudX + midWidth;
+ }
+ case LEFT -> {
+ x1 = hudX;
+ x2 = hudX + midWidth * 2;
+ }
+ }
+ return new Pair<>(new Vector2f(x1, y1), new Vector2f(x2, y2));
+ }
+
+ private float getSelectionHeight() {
+ float scale = (3F * (SkyblockerConfigManager.get().general.titleContainer.titleContainerScale / 100F));
+ return SkyblockerConfigManager.get().general.titleContainer.direction == SkyblockerConfig.Direction.HORIZONTAL ?
+ (textRenderer.fontHeight * scale) :
+ (textRenderer.fontHeight + 10F) * 3F * scale;
+ }
+
+ private float getSelectionWidth() {
+ float scale = (3F * (SkyblockerConfigManager.get().general.titleContainer.titleContainerScale / 100F));
+ return SkyblockerConfigManager.get().general.titleContainer.direction == SkyblockerConfig.Direction.HORIZONTAL ?
+ (textRenderer.getWidth("Test1") + 10 + textRenderer.getWidth("Test23") + 10 + textRenderer.getWidth("Testing1234")) * scale :
+ textRenderer.getWidth("Testing1234") * scale;
+ }
+
+ @Override
+ public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
+ float midWidth = getSelectionWidth() / 2;
+ float midHeight = getSelectionHeight() / 2;
+ var alignment = SkyblockerConfigManager.get().general.titleContainer.alignment;
+
+ Pair<Vector2f, Vector2f> boundingBox = getSelectionBoundingBox();
+ float x1 = boundingBox.getLeft().getX();
+ float y1 = boundingBox.getLeft().getY();
+ float x2 = boundingBox.getRight().getX();
+ float y2 = boundingBox.getRight().getY();
+
+ if (RenderHelper.pointIsInArea(mouseX, mouseY, x1, y1, x2, y2) && button == 0) {
+ hudX = switch (alignment) {
+ case LEFT -> (int) mouseX - midWidth;
+ case MIDDLE -> (int) mouseX;
+ case RIGHT -> (int) mouseX + midWidth;
+ };
+ hudY = (int) (mouseY - midHeight);
+ }
+ return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (button == 1) {
+ hudX = (float) this.width / 2;
+ hudY = this.height * 0.6F;
+ }
+ return super.mouseClicked(mouseX, mouseY, button);
+ }
+
+ @Override
+ public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
+ if (keyCode == GLFW.GLFW_KEY_Q) {
+ SkyblockerConfig.Alignment current = SkyblockerConfigManager.get().general.titleContainer.alignment;
+ SkyblockerConfigManager.get().general.titleContainer.alignment = switch (current) {
+ case LEFT -> SkyblockerConfig.Alignment.MIDDLE;
+ case MIDDLE -> SkyblockerConfig.Alignment.RIGHT;
+ case RIGHT -> SkyblockerConfig.Alignment.LEFT;
+ };
+ }
+ if (keyCode == GLFW.GLFW_KEY_E) {
+ SkyblockerConfig.Alignment current = SkyblockerConfigManager.get().general.titleContainer.alignment;
+ SkyblockerConfigManager.get().general.titleContainer.alignment = switch (current) {
+ case LEFT -> SkyblockerConfig.Alignment.RIGHT;
+ case MIDDLE -> SkyblockerConfig.Alignment.LEFT;
+ case RIGHT -> SkyblockerConfig.Alignment.MIDDLE;
+ };
+ }
+ if (keyCode == GLFW.GLFW_KEY_R) {
+ SkyblockerConfig.Direction current = SkyblockerConfigManager.get().general.titleContainer.direction;
+ SkyblockerConfigManager.get().general.titleContainer.direction = switch (current) {
+ case HORIZONTAL -> SkyblockerConfig.Direction.VERTICAL;
+ case VERTICAL -> SkyblockerConfig.Direction.HORIZONTAL;
+ };
+ }
+ if (keyCode == GLFW.GLFW_KEY_EQUAL) {
+ SkyblockerConfigManager.get().general.titleContainer.titleContainerScale += 10;
+ }
+ if (keyCode == GLFW.GLFW_KEY_MINUS) {
+ SkyblockerConfigManager.get().general.titleContainer.titleContainerScale -= 10;
+ }
+ return super.keyPressed(keyCode, scanCode, modifiers);
+ }
+
+ @Override
+ public void close() {
+ SkyblockerConfigManager.get().general.titleContainer.x = (int) hudX;
+ SkyblockerConfigManager.get().general.titleContainer.y = (int) hudY;
+ SkyblockerConfigManager.save();
+ this.client.setScreen(parent);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/scheduler/MessageScheduler.java b/src/main/java/de/hysky/skyblocker/utils/scheduler/MessageScheduler.java
new file mode 100644
index 00000000..15636965
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/scheduler/MessageScheduler.java
@@ -0,0 +1,66 @@
+package de.hysky.skyblocker.utils.scheduler;
+
+import net.minecraft.client.MinecraftClient;
+
+/**
+ * A scheduler for sending chat messages or commands. Use the instance in {@link #INSTANCE}. Do not instantiate this class.
+ */
+public class MessageScheduler extends Scheduler {
+ /**
+ * The minimum delay that the server will accept between chat messages.
+ */
+ private static final int MIN_DELAY = 200;
+ public static final MessageScheduler INSTANCE = new MessageScheduler();
+ /**
+ * The timestamp of the last message send,
+ */
+ private long lastMessage = 0;
+
+ protected MessageScheduler() {
+ }
+
+ /**
+ * Sends a chat message or command after the minimum cooldown. Prefer this method to send messages or commands to the server.
+ *
+ * @param message the message to send
+ */
+ public void sendMessageAfterCooldown(String message) {
+ if (lastMessage + MIN_DELAY < System.currentTimeMillis()) {
+ sendMessage(message);
+ lastMessage = System.currentTimeMillis();
+ } else {
+ queueMessage(message, 0);
+ }
+ }
+
+ private void sendMessage(String message) {
+ if (MinecraftClient.getInstance().player != null) {
+ if (message.startsWith("/")) {
+ MinecraftClient.getInstance().player.networkHandler.sendCommand(message.substring(1));
+ } else {
+ MinecraftClient.getInstance().inGameHud.getChatHud().addToMessageHistory(message);
+ MinecraftClient.getInstance().player.networkHandler.sendChatMessage(message);
+ }
+ }
+ }
+
+ /**
+ * Queues a chat message or command to send in {@code delay} ticks. Use this method to send messages or commands a set time in the future. The minimum cooldown is still respected.
+ *
+ * @param message the message to send
+ * @param delay the delay before sending the message in ticks
+ */
+ public void queueMessage(String message, int delay) {
+ schedule(() -> sendMessage(message), delay);
+ }
+
+ @Override
+ protected boolean runTask(Runnable task) {
+ if (lastMessage + MIN_DELAY < System.currentTimeMillis()) {
+ task.run();
+ lastMessage = System.currentTimeMillis();
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java b/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java
new file mode 100644
index 00000000..0f44cf93
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java
@@ -0,0 +1,140 @@
+package de.hysky.skyblocker.utils.scheduler;
+
+import com.mojang.brigadier.Command;
+import it.unimi.dsi.fastutil.ints.AbstractInt2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.Screen;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * A scheduler for running tasks at a later time. Tasks will be run synchronously on the main client thread. Use the instance stored in {@link #INSTANCE}. Do not instantiate this class.
+ */
+public class Scheduler {
+ private static final Logger LOGGER = LoggerFactory.getLogger(Scheduler.class);
+ public static final Scheduler INSTANCE = new Scheduler();
+ private int currentTick = 0;
+ private final AbstractInt2ObjectMap<List<ScheduledTask>> tasks = new Int2ObjectOpenHashMap<>();
+
+ protected Scheduler() {
+ }
+
+ /**
+ * Schedules a task to run after a delay.
+ *
+ * @param task the task to run
+ * @param delay the delay in ticks
+ */
+ public void schedule(Runnable task, int delay) {
+ if (delay >= 0) {
+ addTask(new ScheduledTask(task), currentTick + delay);
+ } else {
+ LOGGER.warn("Scheduled a task with negative delay");
+ }
+ }
+
+ /**
+ * Schedules a task to run every period ticks.
+ *
+ * @param task the task to run
+ * @param period the period in ticks
+ */
+ public void scheduleCyclic(Runnable task, int period) {
+ if (period > 0) {
+ addTask(new CyclicTask(task, period), currentTick);
+ } else {
+ LOGGER.error("Attempted to schedule a cyclic task with period lower than 1");
+ }
+ }
+
+ public static Command<FabricClientCommandSource> queueOpenScreenCommand(Supplier<Screen> screenSupplier) {
+ return context -> INSTANCE.queueOpenScreen(screenSupplier);
+ }
+
+ /**
+ * Schedules 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
+ * @see #queueOpenScreenCommand(Supplier)
+ */
+ public int queueOpenScreen(Supplier<Screen> screenSupplier) {
+ MinecraftClient.getInstance().send(() -> MinecraftClient.getInstance().setScreen(screenSupplier.get()));
+ return Command.SINGLE_SUCCESS;
+ }
+
+ public void tick() {
+ if (tasks.containsKey(currentTick)) {
+ List<ScheduledTask> currentTickTasks = tasks.get(currentTick);
+ //noinspection ForLoopReplaceableByForEach (or else we get a ConcurrentModificationException)
+ for (int i = 0; i < currentTickTasks.size(); i++) {
+ ScheduledTask task = currentTickTasks.get(i);
+ if (!runTask(task)) {
+ tasks.computeIfAbsent(currentTick + 1, key -> new ArrayList<>()).add(task);
+ }
+ }
+ tasks.remove(currentTick);
+ }
+ currentTick += 1;
+ }
+
+ /**
+ * Runs the task if able.
+ *
+ * @param task the task to run
+ * @return {@code true} if the task is run, and {@link false} if task is not run.
+ */
+ protected boolean runTask(Runnable task) {
+ task.run();
+ return true;
+ }
+
+ private void addTask(ScheduledTask scheduledTask, int schedule) {
+ if (tasks.containsKey(schedule)) {
+ tasks.get(schedule).add(scheduledTask);
+ } else {
+ List<ScheduledTask> list = new ArrayList<>();
+ list.add(scheduledTask);
+ tasks.put(schedule, list);
+ }
+ }
+
+ /**
+ * A task that runs every period ticks. More specifically, this task reschedules itself to run again after period ticks every time it runs.
+ */
+ protected class CyclicTask extends ScheduledTask {
+ private final int period;
+
+ CyclicTask(Runnable inner, int period) {
+ super(inner);
+ this.period = period;
+ }
+
+ @Override
+ public void run() {
+ super.run();
+ addTask(this, currentTick + period);
+ }
+ }
+
+ /**
+ * A task that runs at a specific tick, relative to {@link #currentTick}.
+ */
+ protected static class ScheduledTask implements Runnable {
+ private final Runnable inner;
+
+ public ScheduledTask(Runnable inner) {
+ this.inner = inner;
+ }
+
+ @Override
+ public void run() {
+ inner.run();
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/tictactoe/TicTacToeUtils.java b/src/main/java/de/hysky/skyblocker/utils/tictactoe/TicTacToeUtils.java
new file mode 100644
index 00000000..908ba46d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/tictactoe/TicTacToeUtils.java
@@ -0,0 +1,104 @@
+package de.hysky.skyblocker.utils.tictactoe;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class TicTacToeUtils {
+
+ public static int getBestMove(char[][] board) {
+ HashMap<Integer, Integer> moves = new HashMap<>();
+ for (int row = 0; row < board.length; row++) {
+ for (int col = 0; col < board[row].length; col++) {
+ if (board[row][col] != '\0') continue;
+ board[row][col] = 'O';
+ int score = alphabeta(board, Integer.MIN_VALUE, Integer.MAX_VALUE, false, 0);
+ board[row][col] = '\0';
+ moves.put(row * 3 + col + 1, score);
+ }
+ }
+ return Collections.max(moves.entrySet(), Map.Entry.comparingByValue()).getKey();
+ }
+
+ public static boolean hasMovesLeft(char[][] board) {
+ for (char[] rows : board) {
+ for (char col : rows) {
+ if (col == '\0') return true;
+ }
+ }
+ return false;
+ }
+
+ public static int getBoardRanking(char[][] board) {
+ for (int row = 0; row < 3; row++) {
+ if (board[row][0] == board[row][1] && board[row][0] == board[row][2]) {
+ if (board[row][0] == 'X') {
+ return -10;
+ } else if (board[row][0] == 'O') {
+ return 10;
+ }
+ }
+ }
+
+ for (int col = 0; col < 3; col++) {
+ if (board[0][col] == board[1][col] && board[0][col] == board[2][col]) {
+ if (board[0][col] == 'X') {
+ return -10;
+ } else if (board[0][col] == 'O') {
+ return 10;
+ }
+ }
+ }
+
+ if (board[0][0] == board[1][1] && board[0][0] == board[2][2]) {
+ if (board[0][0] == 'X') {
+ return -10;
+ } else if (board[0][0] == 'O') {
+ return 10;
+ }
+ } else if (board[0][2] == board[1][1] && board[0][2] == board[2][0]) {
+ if (board[0][2] == 'X') {
+ return -10;
+ } else if (board[0][2] == 'O') {
+ return 10;
+ }
+ }
+
+ return 0;
+ }
+ public static int alphabeta(char[][] board, int alpha, int beta, boolean max, int depth) {
+ int score = getBoardRanking(board);
+ if (score == 10 || score == -10) return score;
+ if (!hasMovesLeft(board)) return 0;
+
+ if (max) {
+ int bestScore = Integer.MIN_VALUE;
+ for (int row = 0; row < 3; row++) {
+ for (int col = 0; col < 3; col++) {
+ if (board[row][col] == '\0') {
+ board[row][col] = 'O';
+ bestScore = Math.max(bestScore, alphabeta(board, alpha, beta, false, depth + 1));
+ board[row][col] = '\0';
+ alpha = Math.max(alpha, bestScore);
+ if (beta <= alpha) break; // Pruning
+ }
+ }
+ }
+ return bestScore - depth;
+ } else {
+ int bestScore = Integer.MAX_VALUE;
+ for (int row = 0; row < 3; row++) {
+ for (int col = 0; col < 3; col++) {
+ if (board[row][col] == '\0') {
+ board[row][col] = 'X';
+ bestScore = Math.min(bestScore, alphabeta(board, alpha, beta, true, depth + 1));
+ board[row][col] = '\0';
+ beta = Math.min(beta, bestScore);
+ if (beta <= alpha) break; // Pruning
+ }
+ }
+ }
+ return bestScore + depth;
+ }
+ }
+} \ No newline at end of file