diff options
Diffstat (limited to 'src/main')
137 files changed, 5777 insertions, 448 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index d793e73d..8dd1419d 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -11,8 +11,11 @@ import de.hysky.skyblocker.skyblock.chat.ChatRuleAnnouncementScreen; import de.hysky.skyblocker.skyblock.chat.ChatRulesHandler; import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder; import de.hysky.skyblocker.skyblock.chocolatefactory.TimeTowerReminder; +import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; import de.hysky.skyblocker.skyblock.crimson.kuudra.Kuudra; import de.hysky.skyblocker.skyblock.dungeon.*; +import de.hysky.skyblocker.skyblock.dungeon.device.LightsOn; +import de.hysky.skyblocker.skyblock.dungeon.device.SimonSays; import de.hysky.skyblocker.skyblock.dungeon.partyfinder.PartyFinderScreen; import de.hysky.skyblocker.skyblock.dungeon.puzzle.*; import de.hysky.skyblocker.skyblock.dungeon.puzzle.boulder.Boulder; @@ -36,6 +39,8 @@ import de.hysky.skyblocker.skyblock.item.tooltip.BackpackPreview; import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; import de.hysky.skyblocker.skyblock.item.tooltip.TooltipManager; import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; +import de.hysky.skyblocker.skyblock.mayors.JerryTimer; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; import de.hysky.skyblocker.skyblock.rift.TheRift; import de.hysky.skyblocker.skyblock.searchoverlay.SearchOverManager; import de.hysky.skyblocker.skyblock.shortcut.Shortcuts; @@ -105,6 +110,7 @@ public class SkyblockerMod implements ClientModInitializer { Utils.init(); SkyblockerConfigManager.init(); SkyblockerScreen.initClass(); + ProfileViewerScreen.initClass(); Tips.init(); NEURepoManager.init(); //ImageRepoLoader.init(); @@ -147,6 +153,8 @@ public class SkyblockerMod implements ClientModInitializer { Silverfish.init(); IceFill.init(); DungeonScore.init(); + SimonSays.init(); + LightsOn.init(); PartyFinderScreen.initClass(); ChestValue.init(); FireFreezeStaffTimer.init(); @@ -179,8 +187,10 @@ public class SkyblockerMod implements ClientModInitializer { ApiUtils.init(); Debug.init(); Kuudra.init(); + DojoManager.init(); RenderHelper.init(); FancyStatusBars.init(); + SkyblockInventoryScreen.initEquipment(); EventNotifications.init(); containerSolverManager.init(); statusBarTracker.init(); @@ -190,6 +200,7 @@ public class SkyblockerMod implements ClientModInitializer { EggFinder.init(); TimeTowerReminder.init(); SkyblockTime.init(); + JerryTimer.init(); TooltipManager.init(); SlotTextManager.init(); diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java b/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java index 0196a37a..9686c6d8 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java @@ -1,7 +1,5 @@ package de.hysky.skyblocker; -import java.time.LocalDate; - import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.skyblock.Tips; import de.hysky.skyblocker.utils.scheduler.Scheduler; @@ -11,11 +9,7 @@ import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.ConfirmLinkScreen; import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.gui.widget.GridWidget; -import net.minecraft.client.gui.widget.MultilineTextWidget; -import net.minecraft.client.gui.widget.TextWidget; -import net.minecraft.client.gui.widget.ThreePartsLayoutWidget; +import net.minecraft.client.gui.widget.*; import net.minecraft.screen.ScreenTexts; import net.minecraft.text.OrderedText; import net.minecraft.text.StringVisitable; @@ -23,6 +17,8 @@ import net.minecraft.text.Text; import net.minecraft.util.Identifier; import net.minecraft.util.Language; +import java.time.LocalDate; + public class SkyblockerScreen extends Screen { private static final int SPACING = 8; private static final int BUTTON_WIDTH = 210; @@ -36,7 +32,8 @@ public class SkyblockerScreen extends Screen { private static final Text TRANSLATE_TEXT = Text.translatable("text.skyblocker.translate"); private static final Text MODRINTH_TEXT = Text.translatable("text.skyblocker.modrinth"); private static final Text DISCORD_TEXT = Text.translatable("text.skyblocker.discord"); - private final ThreePartsLayoutWidget layout = new ThreePartsLayoutWidget(this); + private ThreePartsLayoutWidget layout; + private MultilineTextWidget tip; static { LocalDate date = LocalDate.now(); @@ -44,7 +41,7 @@ public class SkyblockerScreen extends Screen { ICON = date.getMonthValue() == 4 && date.getDayOfMonth() == 1 ? Identifier.of(SkyblockerMod.NAMESPACE, "icons.png") : Identifier.of(SkyblockerMod.NAMESPACE, "icon.png"); } - private SkyblockerScreen() { + public SkyblockerScreen() { super(TITLE); } @@ -57,6 +54,7 @@ public class SkyblockerScreen extends Screen { @Override protected void init() { + this.layout = new ThreePartsLayoutWidget(this, 50, 100); this.layout.addHeader(new IconTextWidget(this.getTitle(), this.textRenderer, ICON)); GridWidget gridWidget = this.layout.addBody(new GridWidget()).setSpacing(SPACING); @@ -72,17 +70,26 @@ public class SkyblockerScreen extends Screen { adder.add(ButtonWidget.builder(DISCORD_TEXT, ConfirmLinkScreen.opening(this, "https://discord.gg/aNNJHQykck")).width(HALF_BUTTON_WIDTH).build()); adder.add(ButtonWidget.builder(ScreenTexts.DONE, button -> this.close()).width(BUTTON_WIDTH).build(), 2); - MultilineTextWidget tip = new MultilineTextWidget(Text.translatable("skyblocker.tips.tip", Tips.nextTipInternal()), this.textRenderer) - .setCentered(true) - .setMaxWidth((int) (this.width * 0.7)); + GridWidget footerGridWidget = this.layout.addFooter(new GridWidget()).setSpacing(SPACING).setRowSpacing(0); + footerGridWidget.getMainPositioner().alignHorizontalCenter(); + GridWidget.Adder footerAdder = footerGridWidget.createAdder(2); + footerAdder.add(tip = new MultilineTextWidget(Tips.nextTip(), this.textRenderer).setCentered(true).setMaxWidth((int) (this.width * 0.7)), 2); + footerAdder.add(ButtonWidget.builder(Text.translatable("skyblocker.tips.previous"), button -> { + tip.setMessage(Tips.previousTip()); + layout.refreshPositions(); + }).build()); + footerAdder.add(ButtonWidget.builder(Text.translatable("skyblocker.tips.next"), button -> { + tip.setMessage(Tips.nextTip()); + layout.refreshPositions(); + }).build()); - this.layout.addFooter(tip); this.layout.refreshPositions(); this.layout.forEachChild(this::addDrawableChild); } @Override protected void initTabNavigation() { + super.initTabNavigation(); this.layout.refreshPositions(); } diff --git a/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java index fed8fa96..e8127a0e 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java @@ -81,6 +81,68 @@ public class CrimsonIsleCategory { .controller(IntegerFieldControllerBuilder::create) .build()) .build()) - .build(); + //dojo + .group(OptionGroup.createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo")) + .collapsed(false) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.forceHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.forceHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableForceHelper, + () -> config.crimsonIsle.dojo.enableForceHelper, + newValue -> config.crimsonIsle.dojo.enableForceHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.staminaHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.staminaHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableStaminaHelper, + () -> config.crimsonIsle.dojo.enableStaminaHelper, + newValue -> config.crimsonIsle.dojo.enableStaminaHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.masteryHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.masteryHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableMasteryHelper, + () -> config.crimsonIsle.dojo.enableMasteryHelper, + newValue -> config.crimsonIsle.dojo.enableMasteryHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.disciplineHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.disciplineHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableDisciplineHelper, + () -> config.crimsonIsle.dojo.enableDisciplineHelper, + newValue -> config.crimsonIsle.dojo.enableDisciplineHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.swiftnessHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.swiftnessHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableSwiftnessHelper, + () -> config.crimsonIsle.dojo.enableSwiftnessHelper, + newValue -> config.crimsonIsle.dojo.enableSwiftnessHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.controlHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.controlHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableControlHelper, + () -> config.crimsonIsle.dojo.enableControlHelper, + newValue -> config.crimsonIsle.dojo.enableControlHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.tenacityHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.tenacityHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableTenacityHelper, + () -> config.crimsonIsle.dojo.enableTenacityHelper, + newValue -> config.crimsonIsle.dojo.enableTenacityHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .build()) + + .build(); } } diff --git a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java index 28ace441..017e9186 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java @@ -269,6 +269,28 @@ public class DungeonsCategory { .build()) .build()) + // Devices (F7/M7) + .group(OptionGroup.createBuilder() + .name(Text.translatable("skyblocker.config.dungeons.devices")) + .collapsed(true) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.dungeons.devices.solveSimonSays")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.dungeons.devices.solveSimonSays.@Tooltip"))) + .binding(defaults.dungeons.devices.solveSimonSays, + () -> config.dungeons.devices.solveSimonSays, + newValue -> config.dungeons.devices.solveSimonSays = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.dungeons.devices.solveLightsOn")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.dungeons.devices.solveLightsOn.@Tooltip"))) + .binding(defaults.dungeons.devices.solveLightsOn, + () -> config.dungeons.devices.solveLightsOn, + newValue -> config.dungeons.devices.solveLightsOn = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .build()) + // Dungeon Secret Waypoints .group(OptionGroup.createBuilder() .name(Text.translatable("skyblocker.config.dungeons.secretWaypoints")) diff --git a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java index fa87be3d..96bb226d 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java @@ -1,8 +1,10 @@ package de.hysky.skyblocker.config.categories; +import de.hysky.skyblocker.SkyblockerScreen; import de.hysky.skyblocker.config.ConfigUtils; import de.hysky.skyblocker.config.SkyblockerConfig; import de.hysky.skyblocker.config.configs.GeneralConfig; +import de.hysky.skyblocker.skyblock.item.tooltip.adders.CraftPriceTooltip; import de.hysky.skyblocker.skyblock.shortcut.ShortcutsConfigScreen; import dev.isxander.yacl3.api.*; import dev.isxander.yacl3.api.controller.FloatSliderControllerBuilder; @@ -16,6 +18,13 @@ public class GeneralCategory { return ConfigCategory.createBuilder() .name(Text.translatable("skyblocker.config.general")) + //Skyblocker Screen + .option(ButtonOption.createBuilder() + .name(Text.translatable("skyblocker.skyblockerScreen")) + .text(Text.translatable("text.skyblocker.open")) + .action((screen, opt) -> MinecraftClient.getInstance().setScreen(new SkyblockerScreen())) + .build()) + //Ungrouped Options .option(Option.<Boolean>createBuilder() .name(Text.translatable("skyblocker.config.general.enableTips")) @@ -155,6 +164,14 @@ public class GeneralCategory { newValue -> config.general.itemTooltip.enableBazaarPrice = newValue) .controller(ConfigUtils::createBooleanController) .build()) + .option(Option.<GeneralConfig.Craft>createBuilder() + .name(Text.translatable("skyblocker.config.general.itemTooltip.craft")) + .binding(defaults.general.itemTooltip.enableCraftingCost, + () -> config.general.itemTooltip.enableCraftingCost, + newValue -> config.general.itemTooltip.enableCraftingCost = newValue) + .listener((Option<GeneralConfig.Craft> ignored, GeneralConfig.Craft ignored2) -> CraftPriceTooltip.clearCache()) + .controller(ConfigUtils::createEnumCyclingListController) + .build()) .option(Option.<Boolean>createBuilder() .name(Text.translatable("skyblocker.config.general.itemTooltip.enableObtainedDate")) .binding(defaults.general.itemTooltip.enableObtainedDate, diff --git a/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java index 9e4935cb..ec2c561c 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java @@ -2,6 +2,7 @@ package de.hysky.skyblocker.config.categories; import de.hysky.skyblocker.config.ConfigUtils; import de.hysky.skyblocker.config.SkyblockerConfig; +import de.hysky.skyblocker.skyblock.bazaar.BazaarHelper; import de.hysky.skyblocker.utils.waypoint.Waypoint; import dev.isxander.yacl3.api.ConfigCategory; import dev.isxander.yacl3.api.Option; @@ -38,6 +39,20 @@ public class HelperCategory { .build()) .build()) + //Jerry Timer + .group(OptionGroup.createBuilder() + .name(Text.translatable("skyblocker.config.helpers.jerry")) + .collapsed(true) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.helpers.jerry.enableJerryTimer")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.jerry.enableJerryTimer.@Tooltip"))) + .binding(defaults.helpers.jerry.enableJerryTimer, + () -> config.helpers.jerry.enableJerryTimer, + newValue -> config.helpers.jerry.enableJerryTimer = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .build()) + //Experiments Solver .group(OptionGroup.createBuilder() .name(Text.translatable("skyblocker.config.helpers.experiments")) @@ -182,8 +197,30 @@ public class HelperCategory { newValue -> config.helpers.chocolateFactory.enableTimeTowerReminder = newValue) .controller(ConfigUtils::createBooleanController) .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.helpers.chocolateFactory.straySound")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.chocolateFactory.straySound.@Tooltip"))) + .binding(defaults.helpers.chocolateFactory.straySound, + () -> config.helpers.chocolateFactory.straySound, + newValue -> config.helpers.chocolateFactory.straySound = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) .build()) + //Bazaar + .group(OptionGroup.createBuilder() + .name(Text.translatable("skyblocker.config.helpers.bazaar")) + .collapsed(true) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.helpers.bazaar.enableBazaarHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.bazaar.enableBazaarHelper.@Tooltip", BazaarHelper.getExpiringIcon(), BazaarHelper.getExpiredIcon(), BazaarHelper.getFilledIcon(69), BazaarHelper.getFilledIcon(100)))) + .binding(defaults.helpers.bazaar.enableBazaarHelper, + () -> config.helpers.bazaar.enableBazaarHelper, + newValue -> config.helpers.bazaar.enableBazaarHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .build()) + .build(); } } diff --git a/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java index e77e9f4b..4e11d869 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java @@ -30,6 +30,14 @@ public class MiningCategory { .controller(ConfigUtils::createBooleanController) .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.mining.commissionHighlight")) + .binding(defaults.mining.commissionHighlight, + () -> config.mining.commissionHighlight, + newValue -> config.mining.commissionHighlight = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + //Dwarven Mines .group(OptionGroup.createBuilder() .name(Text.translatable("skyblocker.config.mining.dwarvenMines")) @@ -95,7 +103,15 @@ public class MiningCategory { newValue -> config.mining.crystalHollows.metalDetectorHelper = newValue) .controller(ConfigUtils::createBooleanController) .build()) - .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.mining.crystalHollows.nucleusWaypoints")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.mining.crystalHollows.nucleusWaypoints.@Tooltip"))) + .binding(defaults.mining.crystalHollows.nucleusWaypoints, + () -> config.mining.crystalHollows.nucleusWaypoints, + newValue -> config.mining.crystalHollows.nucleusWaypoints = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .build()) //Crystal Hollows Map .group(OptionGroup.createBuilder() diff --git a/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java index a2a0f815..889b253a 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java @@ -68,6 +68,13 @@ public class UIAndVisualsCategory { newValue -> config.uiAndVisuals.hideStatusEffectOverlay = newValue) .controller(ConfigUtils::createBooleanController) .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.uiAndVisuals.showEquipmentInInventory")) + .binding(defaults.uiAndVisuals.showEquipmentInInventory, + () -> config.uiAndVisuals.showEquipmentInInventory, + newValue -> config.uiAndVisuals.showEquipmentInInventory = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) //Chest Value FIXME change dropdown to color controller .group(OptionGroup.createBuilder() diff --git a/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java index 8dd93aee..451d1983 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java @@ -7,6 +7,10 @@ public class CrimsonIsleConfig { @SerialEntry public Kuudra kuudra = new Kuudra(); + @SerialEntry + public Dojo dojo = new Dojo(); + + public static class Kuudra { @SerialEntry public boolean supplyWaypoints = true; @@ -32,4 +36,27 @@ public class CrimsonIsleConfig { @SerialEntry public int arrowPoisonThreshold = 32; } + + public static class Dojo { + @SerialEntry + public boolean enableForceHelper = true; + + @SerialEntry + public boolean enableStaminaHelper = true; + + @SerialEntry + public boolean enableMasteryHelper = true; + + @SerialEntry + public boolean enableDisciplineHelper = true; + + @SerialEntry + public boolean enableSwiftnessHelper = true; + + @SerialEntry + public boolean enableControlHelper = true; + + @SerialEntry + public boolean enableTenacityHelper = true; + } } diff --git a/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java index 7b394b53..1a0cad9d 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java @@ -42,6 +42,9 @@ public class DungeonsConfig { public Terminals terminals = new Terminals(); @SerialEntry + public Devices devices = new Devices(); + + @SerialEntry public SecretWaypoints secretWaypoints = new SecretWaypoints(); @SerialEntry @@ -135,6 +138,14 @@ public class DungeonsConfig { public boolean blockIncorrectClicks = false; } + public static class Devices { + @SerialEntry + public boolean solveSimonSays = true; + + @SerialEntry + public boolean solveLightsOn = true; + } + public static class SecretWaypoints { @SerialEntry public boolean enableRoomMatching = true; diff --git a/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java index 9f612028..754e15f1 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java @@ -115,6 +115,9 @@ public class GeneralConfig { public boolean enableBazaarPrice = true; @SerialEntry + public Craft enableCraftingCost = Craft.OFF; + + @SerialEntry public boolean enableObtainedDate = true; @SerialEntry @@ -139,6 +142,23 @@ public class GeneralConfig { } } + public enum Craft { + SELL_ORDER, BUY_ORDER, OFF; + + @Override + public String toString() { + return I18n.translate("skyblocker.config.general.itemTooltip.craft." + name()); + } + + public String getOrder() { + return switch (this) { + case SELL_ORDER -> "sellPrice"; + case BUY_ORDER -> "buyPrice"; + case OFF -> null; + }; + } + } + public static class ItemInfoDisplay { @SerialEntry public boolean slotText = true; diff --git a/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java index c0314924..e009f680 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java @@ -12,6 +12,9 @@ public class HelperConfig { public MythologicalRitual mythologicalRitual = new MythologicalRitual(); @SerialEntry + public Jerry jerry = new Jerry(); + + @SerialEntry public Experiments experiments = new Experiments(); @SerialEntry @@ -23,11 +26,19 @@ public class HelperConfig { @SerialEntry public ChocolateFactory chocolateFactory = new ChocolateFactory(); + @SerialEntry + public Bazaar bazaar = new Bazaar(); + public static class MythologicalRitual { @SerialEntry public boolean enableMythologicalRitualHelper = true; } + public static class Jerry { + @SerialEntry + public boolean enableJerryTimer = false; + } + public static class Experiments { @SerialEntry public boolean enableChronomatronSolver = true; @@ -82,5 +93,13 @@ public class HelperConfig { @SerialEntry public boolean enableTimeTowerReminder = true; + + @SerialEntry + public boolean straySound = true; + } + + public static class Bazaar { + @SerialEntry + public boolean enableBazaarHelper = true; } } diff --git a/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java index a2a9bcf7..d71f57b6 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java @@ -27,6 +27,9 @@ public class MiningConfig { @SerialEntry public Glacite glacite = new Glacite(); + @SerialEntry + public boolean commissionHighlight = true; + public static class DwarvenMines { @SerialEntry public boolean solveFetchur = true; @@ -61,6 +64,9 @@ public class MiningConfig { public static class CrystalHollows { @SerialEntry public boolean metalDetectorHelper = true; + + @SerialEntry + public boolean nucleusWaypoints = false; } public static class CrystalsHud { diff --git a/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java index e016988b..80bdb1c9 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java @@ -28,6 +28,9 @@ public class UIAndVisualsConfig { public boolean hideStatusEffectOverlay = false; @SerialEntry + public boolean showEquipmentInInventory = true; + + @SerialEntry public ChestValue chestValue = new ChestValue(); @SerialEntry diff --git a/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java b/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java index c268103d..93426143 100644 --- a/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java +++ b/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java @@ -26,6 +26,16 @@ public final class SkyblockEvents { } }); + /** + * Called when the player's Skyblock profile changes. + * @implNote This is called upon receiving the chat message for the profile change rather than the exact moment of profile change, so it may be delayed by a few seconds. + */ + public static final Event<ProfileChange> PROFILE_CHANGE = EventFactory.createArrayBacked(ProfileChange.class, callbacks -> (prev, profile) -> { + for (ProfileChange callback : callbacks) { + callback.onSkyblockProfileChange(prev, profile); + } + }); + @Environment(EnvType.CLIENT) @FunctionalInterface public interface SkyblockJoin { @@ -43,4 +53,10 @@ public final class SkyblockEvents { public interface SkyblockLocationChange { void onSkyblockLocationChange(Location location); } + + @Environment(EnvType.CLIENT) + @FunctionalInterface + public interface ProfileChange { + void onSkyblockProfileChange(String prevProfileId, String profileId); + } } diff --git a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java index 48389d40..7bbbac81 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java @@ -6,6 +6,7 @@ import com.llamalad7.mixinextras.sugar.Local; import de.hysky.skyblocker.skyblock.CompactDamage; import de.hysky.skyblocker.skyblock.FishingHelper; import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder; +import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; import de.hysky.skyblocker.skyblock.dungeon.DungeonScore; import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import de.hysky.skyblocker.skyblock.end.BeaconHighlighter; @@ -97,6 +98,7 @@ public abstract class ClientPlayNetworkHandlerMixin { @Inject(method = "onParticle", at = @At("RETURN")) private void skyblocker$onParticle(ParticleS2CPacket packet, CallbackInfo ci) { MythologicalRitual.onParticle(packet); + DojoManager.onParticle(packet); EnderNodes.onParticle(packet); } diff --git a/src/main/java/de/hysky/skyblocker/mixins/ClientWorldMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ClientWorldMixin.java new file mode 100644 index 00000000..a2d7887b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/ClientWorldMixin.java @@ -0,0 +1,31 @@ +package de.hysky.skyblocker.mixins; + +import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; +import de.hysky.skyblocker.skyblock.dungeon.device.SimonSays; +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.block.BlockState; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.llamalad7.mixinextras.sugar.Local; + +@Mixin(ClientWorld.class) +public class ClientWorldMixin { + + /** + * @implNote The {@code pos} can be mutable when this is called by chunk delta updates, so if you want to copy it into memory + * (e.g. store it in a field/list/map) make sure to duplicate it via {@link BlockPos#toImmutable()}. + */ + @Inject(method = "handleBlockUpdate", at = @At("RETURN")) + private void skyblocker$handleBlockUpdate(CallbackInfo ci, @Local(argsOnly = true) BlockPos pos, @Local(argsOnly = true) BlockState state) { + if (Utils.isInCrimson()) { + DojoManager.onBlockUpdate(pos.toImmutable(), state); + } + + SimonSays.onBlockUpdate(pos, state); + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java index f75af09a..3c3dbd52 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java @@ -2,7 +2,11 @@ package de.hysky.skyblocker.mixins; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.skyblock.dungeon.partyfinder.PartyFinderScreen; +import de.hysky.skyblocker.skyblock.item.SkyblockInventoryScreen; +import de.hysky.skyblocker.utils.Utils; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; import net.minecraft.item.ItemStack; import net.minecraft.screen.GenericContainerScreenHandler; import net.minecraft.screen.ScreenHandler; @@ -22,8 +26,21 @@ public abstract class GenericContainerScreenHandlerMixin extends ScreenHandler { public void setStackInSlot(int slot, int revision, ItemStack stack) { super.setStackInSlot(slot, revision, stack); SkyblockerMod.getInstance().containerSolverManager.markDirty(); - if (MinecraftClient.getInstance().currentScreen instanceof PartyFinderScreen screen) { - screen.markDirty(); + + Screen currentScreen = MinecraftClient.getInstance().currentScreen; + switch (currentScreen) { + case PartyFinderScreen screen -> screen.markDirty(); + case GenericContainerScreen screen when screen.getTitle().getString().toLowerCase().contains("equipment") -> { + int line = slot/9; + if (line > 0 && line < 5 && slot % 9 == 1) { + boolean empty = stack.getName().getString().trim().toLowerCase().startsWith("empty"); + if (Utils.isInTheRift()) + SkyblockInventoryScreen.equipment_rift[line - 1] = empty ? ItemStack.EMPTY : stack; + else + SkyblockInventoryScreen.equipment[line - 1] = empty ? ItemStack.EMPTY : stack; + } + } + case null, default -> {} } } diff --git a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java index f2e3e907..709b8697 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java @@ -247,10 +247,21 @@ public abstract class HandledScreenMixin<T extends ScreenHandler> extends Screen return; } // Prevent salvaging + // TODO in future maybe also block clicking the salvage button if a protected item manages to get into the menu if (title.equals("Salvage Items") && ItemProtection.isItemProtected(stack)) { ci.cancel(); return; } + // Prevent Trading + if (title.startsWith("You ") && ItemProtection.isItemProtected(stack)) { //Terrible way to detect the trade menu lol + ci.cancel(); + return; + } + // Prevent Auctioning + if ((title.equals("Create BIN Auction") || title.equals("Create Auction")) && ItemProtection.isItemProtected(stack)) { + ci.cancel(); + return; + } switch (this.handler) { case GenericContainerScreenHandler genericContainerScreenHandler when genericContainerScreenHandler.getRows() == 6 -> { diff --git a/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java index 0d833c22..2194e7a8 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java @@ -1,11 +1,16 @@ package de.hysky.skyblocker.mixins; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.skyblock.itemlist.ItemListWidget; import de.hysky.skyblocker.utils.Utils; +import net.minecraft.client.gui.screen.ButtonTextures; import net.minecraft.client.gui.screen.ingame.InventoryScreen; import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TexturedButtonWidget; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -15,4 +20,16 @@ public abstract class InventoryScreenMixin { private RecipeBookWidget skyblocker$replaceRecipeBook(RecipeBookWidget original) { return SkyblockerConfigManager.get().general.itemList.enableItemList && Utils.isOnSkyblock() ? new ItemListWidget() : original; } + + @WrapOperation(method = "init", at = @At(value = "NEW", target = "(IIIILnet/minecraft/client/gui/screen/ButtonTextures;Lnet/minecraft/client/gui/widget/ButtonWidget$PressAction;)Lnet/minecraft/client/gui/widget/TexturedButtonWidget;")) + private TexturedButtonWidget skyblocker$moveButton(int x, int y, int width, int height, ButtonTextures textures, ButtonWidget.PressAction pressAction, Operation<TexturedButtonWidget> original) { + if (!Utils.isOnSkyblock() || !SkyblockerConfigManager.get().uiAndVisuals.showEquipmentInInventory) return original.call(x, y, width, height, textures, pressAction); + return new TexturedButtonWidget(x + 21, y, width, height, textures, pressAction); + } + + @WrapOperation(method = "method_19891", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/widget/ButtonWidget;setPosition(II)V")) + private void skyblocker$moveButtonWhenPressed(ButtonWidget instance, int i, int j, Operation<Void> original) { + if (!Utils.isOnSkyblock() || !SkyblockerConfigManager.get().uiAndVisuals.showEquipmentInInventory) original.call(instance, i, j); + else instance.setPosition(i + 21, j); + } } diff --git a/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java index dfee0d24..1493cf26 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java @@ -8,9 +8,11 @@ import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.injected.SkyblockerStack; import de.hysky.skyblocker.skyblock.PetCache.PetInfo; import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.client.MinecraftClient; import net.minecraft.component.ComponentHolder; import net.minecraft.component.type.ItemEnchantmentsComponent; import net.minecraft.item.ItemStack; @@ -108,8 +110,8 @@ public abstract class ItemStackMixin implements ComponentHolder, SkyblockerStack } @Unique - private boolean skyblocker$shouldProcess() { - return Utils.isOnSkyblock() && SkyblockerConfigManager.get().mining.enableDrillFuel && ItemUtils.hasCustomDurability((ItemStack) (Object) this); + private boolean skyblocker$shouldProcess() { // Durability bar renders atop of tooltips in ProfileViewer so disable on this screen + return !(MinecraftClient.getInstance().currentScreen instanceof ProfileViewerScreen) && Utils.isOnSkyblock() && SkyblockerConfigManager.get().mining.enableDrillFuel && ItemUtils.hasCustomDurability((ItemStack) (Object) this); } @Unique diff --git a/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java b/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java index b04f958f..f91ddc86 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java @@ -1,7 +1,11 @@ package de.hysky.skyblocker.mixins; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.skyblock.item.HotbarSlotLock; import de.hysky.skyblocker.skyblock.item.ItemProtection; +import de.hysky.skyblocker.skyblock.item.SkyblockInventoryScreen; import de.hysky.skyblocker.utils.JoinWorldPlaceholderScreen; import de.hysky.skyblocker.utils.ReconfiguringPlaceholderScreen; import de.hysky.skyblocker.utils.Utils; @@ -9,8 +13,10 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.DownloadingTerrainScreen; import net.minecraft.client.gui.screen.ReconfiguringScreen; import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.InventoryScreen; import net.minecraft.client.network.ClientPlayNetworkHandler; import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.entity.player.PlayerEntity; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -54,4 +60,10 @@ public abstract class MinecraftClientMixin { private Screen modifyJoinWorld(Screen screen) { return Utils.isOnSkyblock() ? new JoinWorldPlaceholderScreen() : screen; } + + @WrapOperation(method = "handleInputEvents", at = @At(value = "NEW", target = "(Lnet/minecraft/entity/player/PlayerEntity;)Lnet/minecraft/client/gui/screen/ingame/InventoryScreen;")) + private InventoryScreen skyblocker$skyblockInventoryScreen(PlayerEntity player, Operation<InventoryScreen> original) { + if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().uiAndVisuals.showEquipmentInInventory) return new SkyblockInventoryScreen(player); + else return original.call(player); + } }
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java new file mode 100644 index 00000000..a9fae752 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java @@ -0,0 +1,22 @@ +package de.hysky.skyblocker.mixins; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.client.network.PingMeasurer; +import net.minecraft.util.profiler.MultiValueDebugSampleLogImpl; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(PingMeasurer.class) +public class PingMeasurerMixin { + + @WrapOperation(method = "onPingResult", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/profiler/MultiValueDebugSampleLogImpl;push(J)V")) + private void skyblocker$onPingResult(MultiValueDebugSampleLogImpl log, long ping, Operation<Void> operation) { + if (Utils.isInCrimson()) { + DojoManager.onPingResult(ping); + } + operation.call(log, ping); + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixins/PlayerSkinTextureMixin.java b/src/main/java/de/hysky/skyblocker/mixins/PlayerSkinTextureMixin.java index 828d32e3..9b9691c5 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/PlayerSkinTextureMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/PlayerSkinTextureMixin.java @@ -1,5 +1,8 @@ package de.hysky.skyblocker.mixins; +import java.awt.Color; +import java.util.Set; + import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -9,32 +12,62 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.skyblock.item.PlayerHeadHashCache; import de.hysky.skyblocker.utils.Utils; import net.minecraft.client.texture.NativeImage; import net.minecraft.client.texture.PlayerSkinTexture; +import net.minecraft.util.math.ColorHelper; @Mixin(PlayerSkinTexture.class) public class PlayerSkinTextureMixin { + @Unique + private static final Set<String> STRIP_DE_FACTO_TRANSPARENT_PIXELS = Set.of( + "4f3b91b6aa7124f30ed4ad1b2bb012a82985a33640555e18e792f96af8f58ec6", /*Titanium Necklace*/ + "49821410631186c6f3fbbae5f0ef5b947f475eb32027a8aad0a456512547c209", /*Titanium Cloak*/ + "4162303bcdd770aebe7fd19fa26371390a7515140358548084361b5056cdc4e6" /*Titanium Belt*/); + @Unique + private static final float BRIGHTNESS_THRESHOLD = 0.1f; + @Shadow @Final private String url; - @Unique - private boolean isSkyblockSkinTexture; - @Inject(method = "remapTexture", at = @At("HEAD")) - private void skyblocker$determineSkinSource(CallbackInfoReturnable<NativeImage> cir) { - if (Utils.isOnSkyblock()) { - int skinHash = PlayerHeadHashCache.getSkinHash(this.url).hashCode(); - this.isSkyblockSkinTexture = PlayerHeadHashCache.contains(skinHash); + private void skyblocker$determineSkinSource(NativeImage image, CallbackInfoReturnable<NativeImage> cir, @Share("isSkyblockSkinTexture") LocalBooleanRef isSkyblockSkinTexture) { + if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().uiAndVisuals.dontStripSkinAlphaValues) { + String skinTextureHash = PlayerHeadHashCache.getSkinHash(this.url); + int skinHash = skinTextureHash.hashCode(); + isSkyblockSkinTexture.set(PlayerHeadHashCache.contains(skinHash)); + + //Hypixel had the grand idea of using black pixels in place of actual transparent pixels on the titanium equipment so here we go! + if (STRIP_DE_FACTO_TRANSPARENT_PIXELS.contains(skinTextureHash)) { + stripDeFactoTransparentPixels(image); + } } } @WrapWithCondition(method = "remapTexture", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/texture/PlayerSkinTexture;stripAlpha(Lnet/minecraft/client/texture/NativeImage;IIII)V")) - private boolean skyblocker$dontStripAlphaValues(NativeImage image, int x1, int y1, int x2, int y2) { - return !(SkyblockerConfigManager.get().uiAndVisuals.dontStripSkinAlphaValues && this.isSkyblockSkinTexture); + private boolean skyblocker$dontStripAlphaValues(NativeImage image, int x1, int y1, int x2, int y2, @Share("isSkyblockSkinTexture") LocalBooleanRef isSkyblockSkinTexture) { + return !isSkyblockSkinTexture.get(); + } + + @Unique + private static void stripDeFactoTransparentPixels(NativeImage image) { + int height = image.getHeight(); + int width = image.getWidth(); + + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + int color = image.getColor(x, y); + float[] hsb = Color.RGBtoHSB(ColorHelper.Abgr.getRed(color), ColorHelper.Abgr.getGreen(color), ColorHelper.Abgr.getBlue(color), null); + + //Work around "fake" transparent pixels - Thanks Hypixel I totally appreciate this! + if (hsb[2] <= BRIGHTNESS_THRESHOLD) image.setColor(x, y, ColorHelper.Abgr.withAlpha(0x00, color & 0x00FFFFFF)); + } + } } } diff --git a/src/main/java/de/hysky/skyblocker/mixins/accessors/MinecraftClientAccessor.java b/src/main/java/de/hysky/skyblocker/mixins/accessors/MinecraftClientAccessor.java new file mode 100644 index 00000000..a750ded2 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/accessors/MinecraftClientAccessor.java @@ -0,0 +1,12 @@ +package de.hysky.skyblocker.mixins.accessors; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.session.ProfileKeys; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(MinecraftClient.class) +public interface MinecraftClientAccessor { + @Accessor + ProfileKeys getProfileKeys(); +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java b/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java index d8cd6e48..8ddcd60e 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java @@ -1,22 +1,10 @@ package de.hysky.skyblocker.skyblock; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.util.concurrent.CompletableFuture; -import java.util.Optional; - -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; - import com.google.gson.JsonParser; import com.mojang.logging.LogUtils; import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; import com.mojang.serialization.codecs.RecordCodecBuilder; - import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; @@ -26,6 +14,16 @@ import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtCompound; import net.minecraft.screen.slot.Slot; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; /** * Doesn't work with auto pet right now because thats complicated. @@ -135,12 +133,14 @@ public class PetCache { return CACHED_PETS.containsKey(uuid) && CACHED_PETS.get(uuid).containsKey(profileId) ? CACHED_PETS.get(uuid).get(profileId) : null; } - public record PetInfo(String type, double exp, String tier, Optional<String> uuid) { + public record PetInfo(String type, double exp, String tier, Optional<String> uuid, Optional<String> item, Optional<String> skin) { public static final Codec<PetInfo> CODEC = RecordCodecBuilder.create(instance -> instance.group( Codec.STRING.fieldOf("type").forGetter(PetInfo::type), Codec.DOUBLE.fieldOf("exp").forGetter(PetInfo::exp), Codec.STRING.fieldOf("tier").forGetter(PetInfo::tier), - Codec.STRING.optionalFieldOf("uuid").forGetter(PetInfo::uuid)) + Codec.STRING.optionalFieldOf("uuid").forGetter(PetInfo::uuid), + Codec.STRING.optionalFieldOf("heldItem").forGetter(PetInfo::item), + Codec.STRING.optionalFieldOf("skin").forGetter(PetInfo::skin)) .apply(instance, PetInfo::new)); private static final Codec<Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, PetInfo>>> SERIALIZATION_CODEC = Codec.unboundedMap(Codec.STRING, Codec.unboundedMap(Codec.STRING, CODEC).xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/Tips.java b/src/main/java/de/hysky/skyblocker/skyblock/Tips.java index 513dc4b7..5d983e20 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/Tips.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/Tips.java @@ -1,5 +1,6 @@ package de.hysky.skyblocker.skyblock; +import com.demonwav.mcdev.annotations.Translatable; import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.context.CommandContext; @@ -14,16 +15,16 @@ import net.minecraft.command.CommandRegistryAccess; import net.minecraft.text.ClickEvent; import net.minecraft.text.Text; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Random; import java.util.function.Supplier; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; public class Tips { - private static final Random RANDOM = new Random(); - private static int previousTipIndex = -1; - private static final List<Supplier<Text>> TIPS = List.of( + private static int currentTipIndex = 0; + private static final List<Supplier<Text>> TIPS = new ArrayList<>(List.of( getTipFactory("skyblocker.tips.customItemNames", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker custom renameItem"), getTipFactory("skyblocker.tips.customArmorDyeColors", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker custom dyeColor"), getTipFactory("skyblocker.tips.customArmorTrims", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker custom armorTrim"), @@ -36,47 +37,60 @@ public class Tips { getTipFactory("skyblocker.tips.gallery", ClickEvent.Action.OPEN_URL, "https://hysky.de/skyblocker/gallery"), getTipFactory("skyblocker.tips.itemRarityBackground", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), getTipFactory("skyblocker.tips.modMenuUpdate"), - getTipFactory("skyblocker.tips.issues", ClickEvent.Action.OPEN_URL, "https://github.com/SkyblockerMod/Skyblocker"), + getTipFactory("skyblocker.tips.issues", ClickEvent.Action.OPEN_URL, "https://github.com/SkyblockerMod/Skyblocker/issues"), getTipFactory("skyblocker.tips.beta", ClickEvent.Action.OPEN_URL, "https://github.com/SkyblockerMod/Skyblocker/actions"), + getTipFactory("skyblocker.tips.contribute", ClickEvent.Action.OPEN_URL, "https://github.com/SkyblockerMod/Skyblocker/wiki/contribute"), getTipFactory("skyblocker.tips.discord", ClickEvent.Action.OPEN_URL, "https://discord.gg/aNNJHQykck"), getTipFactory("skyblocker.tips.flameOverlay", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), getTipFactory("skyblocker.tips.wikiLookup", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), getTipFactory("skyblocker.tips.protectItem", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker protectItem"), getTipFactory("skyblocker.tips.fairySoulsEnigmaSoulsRelics", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker fairySouls"), - getTipFactory("skyblocker.tips.quickNav", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config") - ); + getTipFactory("skyblocker.tips.quickNav", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), + getTipFactory("skyblocker.tips.waypoints", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker waypoint"), + getTipFactory("skyblocker.tips.orderedWaypoints", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker waypoint ordered"), + getTipFactory("skyblocker.tips.visitorHelper"), + getTipFactory("skyblocker.tips.slotText"), + getTipFactory("skyblocker.tips.profileViewer", ClickEvent.Action.SUGGEST_COMMAND, "/pv"), + getTipFactory("skyblocker.tips.configSearch", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), + getTipFactory("skyblocker.tips.compactDamage", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), + getTipFactory("skyblocker.tips.skyblockerScreen", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker"), + getTipFactory("skyblocker.tips.tipsClick", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker tips next"), + getTipFactory("skyblocker.tips.eventNotifications", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), + getTipFactory("skyblocker.tips.signCalculator"), + getTipFactory("skyblocker.tips.calculateCommand", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker calculate"), + getTipFactory("skyblocker.tips.fancierBars", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker bars"), + getTipFactory("skyblocker.tips.crystalWaypointsShare", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker crystalWaypoints share"), + getTipFactory("skyblocker.tips.gardenMouseLock", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), + getTipFactory("skyblocker.tips.newYearCakesHelper"), + getTipFactory("skyblocker.tips.accessoryHelper"), + getTipFactory("skyblocker.tips.fancyAuctionHouseCheapHighlight") + )); private static boolean sentTip = false; - private static Supplier<Text> getTipFactory(String key) { + private static Supplier<Text> getTipFactory(@Translatable String key) { return () -> Text.translatable(key); } - private static Supplier<Text> getTipFactory(String key, ClickEvent.Action clickAction, String value) { + private static Supplier<Text> getTipFactory(@Translatable String key, ClickEvent.Action clickAction, String value) { return () -> Text.translatable(key).styled(style -> style.withClickEvent(new ClickEvent(clickAction, value))); } public static void init() { ClientCommandRegistrationCallback.EVENT.register(Tips::registerTipsCommand); SkyblockEvents.JOIN.register(Tips::sendNextTip); + Collections.shuffle(TIPS); } private static void registerTipsCommand(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) { dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("tips") .then(literal("enable").executes(Tips::enableTips)) .then(literal("disable").executes(Tips::disableTips)) - .then(literal("next").executes(Tips::nextTip)) + .then(literal("previous").executes(Tips::sendPreviousTipCommand)) + .then(literal("next").executes(Tips::sendNextTipCommand)) )); } - private static void sendNextTip() { - MinecraftClient client = MinecraftClient.getInstance(); - if (client.player != null && SkyblockerConfigManager.get().general.enableTips && !sentTip) { - client.player.sendMessage(nextTip(), false); - sentTip = true; - } - } - private static int enableTips(CommandContext<FabricClientCommandSource> context) { SkyblockerConfigManager.get().general.enableTips = true; SkyblockerConfigManager.save(); @@ -91,22 +105,52 @@ public class Tips { return Command.SINGLE_SUCCESS; } - private static int nextTip(CommandContext<FabricClientCommandSource> context) { - context.getSource().sendFeedback(nextTip()); + private static void sendNextTip() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player != null && SkyblockerConfigManager.get().general.enableTips && !sentTip) { + client.player.sendMessage(tipMessage(nextTip()), false); + sentTip = true; + } + } + + private static int sendNextTipCommand(CommandContext<FabricClientCommandSource> context) { + context.getSource().sendFeedback(tipMessage(nextTip())); return Command.SINGLE_SUCCESS; } - private static Text nextTip() { - return Constants.PREFIX.get().append(Text.translatable("skyblocker.tips.tip", nextTipInternal())) + public static Text nextTip() { + return Text.translatable("skyblocker.tips.tip", nextTipInternal()); + } + + private static Text nextTipInternal() { + currentTipIndex++; + currentTipIndex %= TIPS.size(); + return TIPS.get(currentTipIndex).get(); + } + + private static int sendPreviousTipCommand(CommandContext<FabricClientCommandSource> context) { + context.getSource().sendFeedback(tipMessage(previousTip())); + return Command.SINGLE_SUCCESS; + } + + public static Text previousTip() { + return Text.translatable("skyblocker.tips.tip", previousTipInternal()); + } + + private static Text previousTipInternal() { + currentTipIndex--; + currentTipIndex += TIPS.size(); + currentTipIndex %= TIPS.size(); + return TIPS.get(currentTipIndex).get(); + } + + private static Text tipMessage(Text tip) { + return Constants.PREFIX.get().append(tip) + .append(" ") + .append(Text.translatable("skyblocker.tips.clickPreviousTip").styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker tips previous")))) + .append(" ") .append(Text.translatable("skyblocker.tips.clickNextTip").styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker tips next")))) .append(" ") .append(Text.translatable("skyblocker.tips.clickDisable").styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker tips disable")))); } - - public static Text nextTipInternal() { - int randomInt = RANDOM.nextInt(TIPS.size()); - while (randomInt == previousTipIndex) randomInt = RANDOM.nextInt(TIPS.size()); - previousTipIndex = randomInt; - return TIPS.get(randomInt).get(); - } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/bazaar/BazaarHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/bazaar/BazaarHelper.java new file mode 100644 index 00000000..8b83b06b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/bazaar/BazaarHelper.java @@ -0,0 +1,73 @@ +package de.hysky.skyblocker.skyblock.bazaar; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.item.slottext.SlotText; +import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder; +import de.hysky.skyblocker.utils.ItemUtils; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.apache.commons.lang3.math.NumberUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class BazaarHelper extends SlotTextAdder { + private static final Pattern FILLED_PATTERN = Pattern.compile("Filled: \\S+ \\(?([\\d.]+)%\\)?!?"); + private static final int RED = 0xe60b1e; + private static final int YELLOW = 0xe6ba0b; + private static final int GREEN = 0x1ee60b; + + public BazaarHelper() { + super("(?:Co-op|Your) Bazaar Orders"); + } + + @Override + public @NotNull List<SlotText> getText(Slot slot) { + if (!SkyblockerConfigManager.get().helpers.bazaar.enableBazaarHelper) return List.of(); + // Skip the first row as it's always glass panes. + if (slot.id < 10) return List.of(); + // Skip the last 10 items. 11 is subtracted because size is 1-based so the last slot is size - 1. + if (slot.id > slot.inventory.size() - 11) return List.of(); //Note that this also skips the slots in player's inventory (anything above 36/45/54 depending on the order count) + + int column = slot.id % 9; + if (column == 0 || column == 8) return List.of(); // Skip the first and last column as those are always glass panes as well. + + ItemStack item = slot.getStack(); + if (item.isEmpty()) return List.of(); //We've skipped all invalid slots, so we can just check if it's not air here. + + Matcher matcher = ItemUtils.getLoreLineIfMatch(item, FILLED_PATTERN); + if (matcher != null) { + List<Text> lore = ItemUtils.getLore(item); + if (!lore.isEmpty() && lore.getLast().getString().equals("Click to claim!")) { //Only show the filled icon when there are items to claim + int filled = NumberUtils.toInt(matcher.group(1)); + return SlotText.topLeftList(getFilledIcon(filled)); + } + } + + if (ItemUtils.getLoreLineIf(item, str -> str.equals("Expired!")) != null) { + return SlotText.topLeftList(getExpiredIcon()); + } else if (ItemUtils.getLoreLineIf(item, str -> str.startsWith("Expires in")) != null) { + return SlotText.topLeftList(getExpiringIcon()); + } + + return List.of(); + } + + public static @NotNull MutableText getExpiredIcon() { + return Text.literal("⏰").withColor(RED).formatted(Formatting.BOLD); + } + + public static @NotNull MutableText getExpiringIcon() { + return Text.literal("⏰").withColor(YELLOW).formatted(Formatting.BOLD); + } + + public static @NotNull MutableText getFilledIcon(int filled) { + if (filled < 100) return Text.literal("%").withColor(YELLOW).formatted(Formatting.BOLD); + return Text.literal("✅").withColor(GREEN).formatted(Formatting.BOLD); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/bazaar/ReorderHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/bazaar/ReorderHelper.java new file mode 100644 index 00000000..c2b11926 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/bazaar/ReorderHelper.java @@ -0,0 +1,73 @@ +package de.hysky.skyblocker.skyblock.bazaar; + +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.render.gui.ColorHighlight; +import de.hysky.skyblocker.utils.render.gui.ContainerSolver; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.util.InputUtil; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ReorderHelper extends ContainerSolver { + private static final Pattern BUY_PATTERN = Pattern.compile("([\\d,]+)x missing items\\."); + private static final Pattern SELL_PATTERN = Pattern.compile("([\\d,]+)x items\\."); + + public ReorderHelper() { + super("^Order options"); + } + + @Override + protected boolean isEnabled() { + return true; + } + + @Override + protected boolean onClickSlot(int slot, ItemStack stack, int screenId, String[] groups) { + // V This part is so that it short-circuits if not necessary + if ((slot == 11 || slot == 13) && stack.isOf(Items.GREEN_TERRACOTTA) && InputUtil.isKeyPressed(MinecraftClient.getInstance().getWindow().getHandle(), GLFW.GLFW_KEY_LEFT_CONTROL)) { + Matcher matcher; + // The terracotta is at slot 13 on sell orders and at slot 11 on buy orders + if (slot == 13) matcher = ItemUtils.getLoreLineIfContainsMatch(stack, SELL_PATTERN); + else matcher = ItemUtils.getLoreLineIfContainsMatch(stack, BUY_PATTERN); + if (matcher != null) { + MinecraftClient.getInstance().keyboard.setClipboard(matcher.group(1).replace(",", "")); + return false; + } + } + return false; + } + + @Override + protected List<ColorHighlight> getColors(String[] groups, Int2ObjectMap<ItemStack> slots) { + return List.of(); + } + + public static class Tooltip extends TooltipAdder { + public Tooltip() { + super("^Order options", Integer.MIN_VALUE); + } + + @Override + public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { + if (focusedSlot == null || !stack.isOf(Items.GREEN_TERRACOTTA)) return; + switch (focusedSlot.id) { + case 11, 13 -> { + lines.add(Text.empty()); + lines.add(Text.empty().append(Text.translatable("skyblocker.reorderHelper.tooltip.line1")).formatted(Formatting.DARK_GRAY, Formatting.ITALIC)); + lines.add(Text.empty().append(Text.translatable("skyblocker.reorderHelper.tooltip.line2")).formatted(Formatting.DARK_GRAY, Formatting.ITALIC)); + } + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java index f9c1f7ae..db81382c 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java @@ -9,9 +9,13 @@ import de.hysky.skyblocker.utils.render.gui.ColorHighlight; import de.hysky.skyblocker.utils.render.gui.ContainerSolver; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.sound.PositionedSoundInstance; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; import net.minecraft.screen.slot.Slot; +import net.minecraft.sound.SoundEvents; import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.util.Formatting; @@ -49,6 +53,7 @@ public class ChocolateFactorySolver extends ContainerSolver { private static boolean isTimeTowerActive = false; private static int bestUpgrade = -1; private static int bestAffordableUpgrade = -1; + private static StraySound ding = StraySound.NONE; private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); @Override @@ -65,6 +70,7 @@ public class ChocolateFactorySolver extends ContainerSolver { isTimeTowerActive = false; bestUpgrade = -1; bestAffordableUpgrade = -1; + ding = StraySound.NONE; } //Slots, for ease of maintenance rather than using magic numbers everywhere. @@ -78,8 +84,20 @@ public class ChocolateFactorySolver extends ContainerSolver { private static final byte STRAY_RABBIT_START = 0; private static final byte STRAY_RABBIT_END = 26; + private static int dingTick = 0; + public ChocolateFactorySolver() { super("^Chocolate Factory$"); //There are multiple screens that fit the pattern `^Chocolate Factory`, so the $ is required + ClientTickEvents.START_CLIENT_TICK.register(ChocolateFactorySolver::onTick); + } + + private static void onTick(MinecraftClient client) { + if (ding != StraySound.NONE) { + dingTick = (++dingTick) % (ding == StraySound.NORMAL ? 5 : 3); + if (dingTick == 0) { + client.getSoundManager().play(PositionedSoundInstance.master(ding == StraySound.NORMAL ? SoundEvents.BLOCK_NOTE_BLOCK_PLING.value() : SoundEvents.BLOCK_NOTE_BLOCK_HARP.value(), 1.f, 1.f)); + } + } } @Override @@ -209,10 +227,10 @@ public class ChocolateFactorySolver extends ContainerSolver { } Matcher costMatcher = COST_PATTERN.matcher(coachLore); - OptionalInt cost = RegexUtils.getIntFromMatcher(costMatcher, multiplierIncreaseMatcher.hasMatch() ? multiplierIncreaseMatcher.end() : 0); //Cost comes after the multiplier line + OptionalLong cost = RegexUtils.getLongFromMatcher(costMatcher, multiplierIncreaseMatcher.hasMatch() ? multiplierIncreaseMatcher.end() : 0); //Cost comes after the multiplier line if (cost.isEmpty()) return Optional.empty(); - return Optional.of(new Rabbit(totalCps / totalCpsMultiplier * (nextCpsMultiplier.getAsDouble() - currentCpsMultiplier.getAsDouble()), cost.getAsInt(), COACH_SLOT)); + return Optional.of(new Rabbit(totalCps / totalCpsMultiplier * (nextCpsMultiplier.getAsDouble() - currentCpsMultiplier.getAsDouble()), cost.getAsLong(), COACH_SLOT)); } private static Optional<Rabbit> getRabbit(ItemStack item, int slot) { @@ -227,9 +245,9 @@ public class ChocolateFactorySolver extends ContainerSolver { } Matcher costMatcher = COST_PATTERN.matcher(lore); - OptionalInt cost = RegexUtils.getIntFromMatcher(costMatcher, cpsMatcher.hasMatch() ? cpsMatcher.end() : 0); //Cost comes after the cps line + OptionalLong cost = RegexUtils.getLongFromMatcher(costMatcher, cpsMatcher.hasMatch() ? cpsMatcher.end() : 0); //Cost comes after the cps line if (cost.isEmpty()) return Optional.empty(); - return Optional.of(new Rabbit((nextCps.getAsInt() - currentCps.getAsInt())*(totalCpsMultiplier < 0 ? 1 : totalCpsMultiplier), cost.getAsInt(), slot)); + return Optional.of(new Rabbit((nextCps.getAsInt() - currentCps.getAsInt())*(totalCpsMultiplier < 0 ? 1 : totalCpsMultiplier), cost.getAsLong(), slot)); } private static Optional<ColorHighlight> getPrestigeHighlight() { @@ -239,19 +257,27 @@ public class ChocolateFactorySolver extends ContainerSolver { } private static List<ColorHighlight> getStrayRabbitHighlight(Int2ObjectMap<ItemStack> slots) { + ding = StraySound.NONE; final List<ColorHighlight> highlights = new ArrayList<>(); for (byte i = STRAY_RABBIT_START; i <= STRAY_RABBIT_END; i++) { ItemStack item = slots.get(i); if (!item.isOf(Items.PLAYER_HEAD)) continue; String name = item.getName().getString(); if (name.equals("CLICK ME!") || name.startsWith("Golden Rabbit - ")) { + if (SkyblockerConfigManager.get().helpers.chocolateFactory.straySound) ding = name.startsWith("Golden") ? StraySound.GOLDEN : StraySound.NORMAL; highlights.add(ColorHighlight.green(i)); } } return highlights; } - private record Rabbit(double cpsIncrease, int cost, int slot) { } + private record Rabbit(double cpsIncrease, long cost, int slot) { } + + private enum StraySound { + NONE, + NORMAL, + GOLDEN + } public static final class Tooltip extends TooltipAdder { public Tooltip() { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java index 42883c4f..620da37c 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java @@ -24,7 +24,6 @@ import net.minecraft.text.ClickEvent; import net.minecraft.text.HoverEvent; import net.minecraft.text.Text; import net.minecraft.util.Formatting; -import org.apache.commons.lang3.mutable.MutableObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,6 +33,7 @@ import java.util.regex.Pattern; public class EggFinder { private static final Pattern eggFoundPattern = Pattern.compile("^(?:HOPPITY'S HUNT You found a Chocolate|You have already collected this Chocolate) (Breakfast|Lunch|Dinner)"); + private static final Pattern newEggPattern = Pattern.compile("^HOPPITY'S HUNT A Chocolate (Breakfast|Lunch|Dinner) Egg has appeared!$"); private static final Logger logger = LoggerFactory.getLogger("Skyblocker Egg Finder"); private static final LinkedList<ArmorStandEntity> armorStandQueue = new LinkedList<>(); private static final Location[] possibleLocations = {Location.CRIMSON_ISLE, Location.CRYSTAL_HOLLOWS, Location.DUNGEON_HUB, Location.DWARVEN_MINES, Location.HUB, Location.THE_END, Location.THE_PARK, Location.GOLD_MINE, Location.DEEP_CAVERNS, Location.SPIDERS_DEN, Location.THE_FARMING_ISLAND}; @@ -96,7 +96,7 @@ public class EggFinder { for (ItemStack itemStack : armorStand.getArmorItems()) { ItemUtils.getHeadTextureOptional(itemStack).ifPresent(texture -> { for (EggType type : EggType.entries) { //Compare blockPos rather than entity to avoid incorrect matches when the entity just moves rather than a new one being spawned elsewhere - if (texture.equals(type.texture) && (type.egg.getValue() == null || !type.egg.getValue().entity.getBlockPos().equals(armorStand.getBlockPos()))) { + if (texture.equals(type.texture) && (type.egg == null || !type.egg.entity.getBlockPos().equals(armorStand.getBlockPos()))) { handleFoundEgg(armorStand, type); return; } @@ -109,27 +109,29 @@ public class EggFinder { if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableEggFinder) return; isLocationCorrect = false; for (EggType type : EggType.entries) { - type.egg.setValue(null); + type.egg = null; } } private static void handleFoundEgg(ArmorStandEntity entity, EggType eggType) { - eggType.egg.setValue(new Egg(entity, new Waypoint(entity.getBlockPos().up(2), SkyblockerConfigManager.get().helpers.chocolateFactory.waypointType, ColorUtils.getFloatComponents(eggType.color)))); - - if (!SkyblockerConfigManager.get().helpers.chocolateFactory.sendEggFoundMessages) return; - MinecraftClient.getInstance().player.sendMessage(Constants.PREFIX.get() - .append("Found a ") - .append(Text.literal("Chocolate " + eggType + " Egg") - .withColor(eggType.color)) - .append(" at " + entity.getBlockPos().up(2).toShortString() + "!") - .styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker eggFinder shareLocation " + entity.getBlockX() + " " + (entity.getBlockY() + 2) + " " + entity.getBlockZ() + " " + eggType)) - .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal("Click to share the location in chat!").formatted(Formatting.GREEN))))); + eggType.egg = new Egg(entity, new Waypoint(entity.getBlockPos().up(2), SkyblockerConfigManager.get().helpers.chocolateFactory.waypointType, ColorUtils.getFloatComponents(eggType.color))); + + if (!SkyblockerConfigManager.get().helpers.chocolateFactory.sendEggFoundMessages || System.currentTimeMillis() - eggType.messageLastSent < 1000) return; + eggType.messageLastSent = System.currentTimeMillis(); + MinecraftClient.getInstance().player.sendMessage( + Constants.PREFIX.get() + .append("Found a ") + .append(Text.literal("Chocolate " + eggType + " Egg") + .withColor(eggType.color)) + .append(" at " + entity.getBlockPos().up(2).toShortString() + "!") + .styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker eggFinder shareLocation " + entity.getBlockX() + " " + (entity.getBlockY() + 2) + " " + entity.getBlockZ() + " " + eggType)) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal("Click to share the location in chat!").formatted(Formatting.GREEN))))); } private static void renderWaypoints(WorldRenderContext context) { if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableEggFinder) return; for (EggType type : EggType.entries) { - Egg egg = type.egg.getValue(); + Egg egg = type.egg; if (egg != null && egg.waypoint.shouldRender()) egg.waypoint.render(context); } } @@ -139,30 +141,47 @@ public class EggFinder { Matcher matcher = eggFoundPattern.matcher(text.getString()); if (matcher.find()) { try { - Egg egg = EggType.valueOf(matcher.group(1).toUpperCase()).egg.getValue(); + Egg egg = EggType.valueOf(matcher.group(1).toUpperCase()).egg; if (egg != null) egg.waypoint.setFound(); } catch (IllegalArgumentException e) { logger.error("[Skyblocker Egg Finder] Failed to find egg type for egg found message. Tried to match against: " + matcher.group(0), e); } } + + matcher.usePattern(newEggPattern); + if (matcher.find()) { + try { + EggType.valueOf(matcher.group(1).toUpperCase()).egg = null; + } catch (IllegalArgumentException e) { + logger.error("[Skyblocker Egg Finder] Failed to find egg type for egg spawn message. Tried to match against: " + matcher.group(0), e); + } + } } - record Egg(ArmorStandEntity entity, Waypoint waypoint) { } + record Egg(ArmorStandEntity entity, Waypoint waypoint) {} + @SuppressWarnings("DataFlowIssue") //Removes that pesky "unboxing of Integer might cause NPE" warning when we already know it's not null enum EggType { - LUNCH(new MutableObject<>(), Formatting.BLUE.getColorValue(), "ewogICJ0aW1lc3RhbXAiIDogMTcxMTQ2MjU2ODExMiwKICAicHJvZmlsZUlkIiA6ICI3NzUwYzFhNTM5M2Q0ZWQ0Yjc2NmQ4ZGUwOWY4MjU0NiIsCiAgInByb2ZpbGVOYW1lIiA6ICJSZWVkcmVsIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzdhZTZkMmQzMWQ4MTY3YmNhZjk1MjkzYjY4YTRhY2Q4NzJkNjZlNzUxZGI1YTM0ZjJjYmM2NzY2YTAzNTZkMGEiCiAgICB9CiAgfQp9"), - DINNER(new MutableObject<>(), Formatting.GREEN.getColorValue(), "ewogICJ0aW1lc3RhbXAiIDogMTcxMTQ2MjY0OTcwMSwKICAicHJvZmlsZUlkIiA6ICI3NGEwMzQxNWY1OTI0ZTA4YjMyMGM2MmU1NGE3ZjJhYiIsCiAgInByb2ZpbGVOYW1lIiA6ICJNZXp6aXIiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZTVlMzYxNjU4MTlmZDI4NTBmOTg1NTJlZGNkNzYzZmY5ODYzMTMxMTkyODNjMTI2YWNlMGM0Y2M0OTVlNzZhOCIKICAgIH0KICB9Cn0"), - BREAKFAST(new MutableObject<>(), Formatting.GOLD.getColorValue(), "ewogICJ0aW1lc3RhbXAiIDogMTcxMTQ2MjY3MzE0OSwKICAicHJvZmlsZUlkIiA6ICJiN2I4ZTlhZjEwZGE0NjFmOTY2YTQxM2RmOWJiM2U4OCIsCiAgInByb2ZpbGVOYW1lIiA6ICJBbmFiYW5hbmFZZzciLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTQ5MzMzZDg1YjhhMzE1ZDAzMzZlYjJkZjM3ZDhhNzE0Y2EyNGM1MWI4YzYwNzRmMWI1YjkyN2RlYjUxNmMyNCIKICAgIH0KICB9Cn0"); + LUNCH(Formatting.BLUE.getColorValue(), "ewogICJ0aW1lc3RhbXAiIDogMTcxMTQ2MjU2ODExMiwKICAicHJvZmlsZUlkIiA6ICI3NzUwYzFhNTM5M2Q0ZWQ0Yjc2NmQ4ZGUwOWY4MjU0NiIsCiAgInByb2ZpbGVOYW1lIiA6ICJSZWVkcmVsIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzdhZTZkMmQzMWQ4MTY3YmNhZjk1MjkzYjY4YTRhY2Q4NzJkNjZlNzUxZGI1YTM0ZjJjYmM2NzY2YTAzNTZkMGEiCiAgICB9CiAgfQp9"), + DINNER(Formatting.GREEN.getColorValue(), "ewogICJ0aW1lc3RhbXAiIDogMTcxMTQ2MjY0OTcwMSwKICAicHJvZmlsZUlkIiA6ICI3NGEwMzQxNWY1OTI0ZTA4YjMyMGM2MmU1NGE3ZjJhYiIsCiAgInByb2ZpbGVOYW1lIiA6ICJNZXp6aXIiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZTVlMzYxNjU4MTlmZDI4NTBmOTg1NTJlZGNkNzYzZmY5ODYzMTMxMTkyODNjMTI2YWNlMGM0Y2M0OTVlNzZhOCIKICAgIH0KICB9Cn0"), + BREAKFAST(Formatting.GOLD.getColorValue(), "ewogICJ0aW1lc3RhbXAiIDogMTcxMTQ2MjY3MzE0OSwKICAicHJvZmlsZUlkIiA6ICJiN2I4ZTlhZjEwZGE0NjFmOTY2YTQxM2RmOWJiM2U4OCIsCiAgInByb2ZpbGVOYW1lIiA6ICJBbmFiYW5hbmFZZzciLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTQ5MzMzZDg1YjhhMzE1ZDAzMzZlYjJkZjM3ZDhhNzE0Y2EyNGM1MWI4YzYwNzRmMWI1YjkyN2RlYjUxNmMyNCIKICAgIH0KICB9Cn0"); - public final MutableObject<Egg> egg; + private Egg egg = null; public final int color; public final String texture; + /* + When a new egg spawns in the player's range, the order of packets/messages goes like this: + set_equipment -> new egg message -> set_entity_data + We have to set the egg to null to prevent the highlight from staying where it was before the new egg spawned, + and doing so causes the found message to get sent twice. This is the reason for the existence of this field, so that we can not send the 2nd message. + This doesn't fix the field being set twice, but that's not an issue anyway. It'd be much harder to fix the highlight issue mentioned above if it wasn't being set twice. + */ + private long messageLastSent = 0; //This is to not create an array each time we iterate over the values public static final ObjectImmutableList<EggType> entries = ObjectImmutableList.of(BREAKFAST, LUNCH, DINNER); - EggType(MutableObject<Egg> egg, int color, String texture) { - this.egg = egg; + EggType(int color, String texture) { this.color = color; this.texture = texture; } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ControlTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ControlTestHelper.java new file mode 100644 index 00000000..f63d2fa2 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ControlTestHelper.java @@ -0,0 +1,77 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import net.minecraft.entity.mob.WitherSkeletonEntity; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; + +import java.awt.*; + +public class ControlTestHelper { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + + private static WitherSkeletonEntity correctWitherSkeleton; + private static Vec3d lastPos; + private static long lastUpdate; + private static Vec3d pingOffset; + private static Vec3d lastPingOffset; + + protected static void reset() { + correctWitherSkeleton = null; + lastPos = null; + lastUpdate = -1; + pingOffset = null; + lastPingOffset = null; + } + + /** + * Find the correct WitherSkeleton entity when it spawns to start tracking it + * + * @param entity spawned entity + */ + protected static void onEntitySpawn(Entity entity) { + if (entity instanceof WitherSkeletonEntity witherSkeleton && correctWitherSkeleton == null) { + correctWitherSkeleton = witherSkeleton; + } + } + + /** + * Finds where to look in 3 ticks effected by ping + */ + protected static void update() { + if (correctWitherSkeleton != null) { + //smoothly adjust the ping throughout the test + if (lastPos != null) { + lastPingOffset = pingOffset; + double ping = DojoManager.ping / 1000d; + //find distance between last position and current position of skeleton + Vec3d movementVector = correctWitherSkeleton.getPos().subtract(lastPos).multiply(1, 0.1, 1); + //adjust the vector to current ping (multiply by 1 + time in second until the next update offset by the players ping) + pingOffset = movementVector.multiply(1 + 3 / 20d + ping); + } + lastPos = correctWitherSkeleton.getPos(); + lastUpdate = System.currentTimeMillis(); + } + } + + /** + * Renders an outline around where the player should aim (assumes values are updated every 3 ticks) + * + * @param context render context + */ + protected static void render(WorldRenderContext context) { + if (CLIENT.player != null && correctWitherSkeleton != null && pingOffset != null && lastPingOffset != null) { + float tickDelta = context.tickCounter().getTickDelta(false); + //how long until net update + double updatePercent = (double) (System.currentTimeMillis() - lastUpdate) / 150; + Vec3d aimPos = correctWitherSkeleton.getCameraPosVec(tickDelta).add(pingOffset.multiply(updatePercent)).add(lastPingOffset.multiply(1 - updatePercent)); + Box targetBox = new Box(aimPos.add(-0.5, -0.5, -0.5), aimPos.add(0.5, 0.5, 0.5)); + boolean playerLookingAtBox = targetBox.raycast(CLIENT.player.getCameraPosVec(tickDelta), CLIENT.player.getCameraPosVec(tickDelta).add(CLIENT.player.getRotationVec(tickDelta).multiply(30))).isPresent(); + float[] boxColor = playerLookingAtBox ? Color.GREEN.getColorComponents(new float[]{0, 0, 0}) : Color.LIGHT_GRAY.getColorComponents(new float[]{0, 0, 0}); + RenderHelper.renderOutline(context, targetBox, boxColor, 3, true); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DisciplineTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DisciplineTestHelper.java new file mode 100644 index 00000000..ab0a0781 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DisciplineTestHelper.java @@ -0,0 +1,66 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntMaps; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.client.MinecraftClient; + +import java.util.Map; +import java.util.Objects; + +public class DisciplineTestHelper { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + + /** + * Stores what sword is needed for the name of a zombie + */ + private static final Map<String, String> SWORD_TO_NAME_LOOKUP = Map.of( + "WOOD_SWORD", "Wood", + "IRON_SWORD", "Iron", + "GOLD_SWORD", "Gold", + "DIAMOND_SWORD", "Diamond" + ); + + /** + * Stores a color related to the color of the sword: wood = brown, iron = silver, gold = gold, diamond = cyan + */ + private static final Object2IntMap<String> SWORD_TO_COLOR_LOOKUP = Object2IntMaps.unmodifiable(new Object2IntOpenHashMap<>(Map.of( + "WOOD_SWORD", 0xa52a2a, + "IRON_SWORD", 0xc0c0c0, + "GOLD_SWORD", 0xffd700, + "DIAMOND_SWORD", 0x00ffff + ))); + + /** + * Works out if a zombie should glow based on its name and the currently held item by the player + * + * @param name name of the zombie to see if it should glow + * @return if the zombie should glow + */ + protected static boolean shouldGlow(String name) { + if (CLIENT == null || CLIENT.player == null) { + return false; + } + String heldId = CLIENT.player.getMainHandStack().getSkyblockId(); + if (heldId == null) { + return false; + } + return Objects.equals(SWORD_TO_NAME_LOOKUP.get(heldId), name); + } + + /** + * gets the color linked to the currently held sword for zombies to glow + * + * @return color linked to sword + */ + protected static int getColor() { + if (DojoManager.currentChallenge != DojoManager.DojoChallenges.DISCIPLINE || CLIENT == null || CLIENT.player == null) { + return 0; + } + String heldId = CLIENT.player.getMainHandStack().getSkyblockId(); + if (heldId == null) { + return 0; + } + return SWORD_TO_COLOR_LOOKUP.getOrDefault(heldId, 0); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DojoManager.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DojoManager.java new file mode 100644 index 00000000..323c985c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DojoManager.java @@ -0,0 +1,256 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientEntityEvents; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.fabricmc.fabric.api.event.player.AttackEntityCallback; +import net.minecraft.block.BlockState; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.packet.c2s.query.QueryPingC2SPacket; +import net.minecraft.network.packet.s2c.play.ParticleS2CPacket; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Formatting; +import net.minecraft.util.Hand; +import net.minecraft.util.Util; +import net.minecraft.util.hit.EntityHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DojoManager { + + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + private static final String START_MESSAGE = "[NPC] Master Tao: Ahhh, here we go! Let's get you into the Arena."; + private static final Pattern TEST_OF_PATTERN = Pattern.compile("\\s+Test of (\\w+) OBJECTIVES"); + private static final String CHALLENGE_FINISHED_REGEX = "\\s+CHALLENGE ((COMPLETED)|(FAILED))"; + + + protected enum DojoChallenges { + NONE("none", enabled -> false), + FORCE("Force", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableForceHelper), + STAMINA("Stamina", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableStaminaHelper), + MASTERY("Mastery", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableMasteryHelper), + DISCIPLINE("Discipline", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableDisciplineHelper), + SWIFTNESS("Swiftness", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableSwiftnessHelper), + CONTROL("Control", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableControlHelper), + TENACITY("Tenacity", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableTenacityHelper); + + private final String name; + private final Predicate<Boolean> enabled; + + DojoChallenges(String name, Predicate<Boolean> enabled) { + this.name = name; + this.enabled = enabled; + } + + public static DojoChallenges from(String name) { + return Arrays.stream(DojoChallenges.values()).filter(n -> name.equals(n.name)).findFirst().orElse(NONE); + } + } + + protected static DojoChallenges currentChallenge = DojoChallenges.NONE; + public static boolean inArena = false; + protected static long ping = -1; + + public static void init() { + ClientReceiveMessageEvents.GAME.register(DojoManager::onMessage); + WorldRenderEvents.AFTER_TRANSLUCENT.register(DojoManager::render); + ClientPlayConnectionEvents.JOIN.register((_handler, _sender, _client) -> reset()); + ClientEntityEvents.ENTITY_LOAD.register(DojoManager::onEntitySpawn); + ClientEntityEvents.ENTITY_UNLOAD.register(DojoManager::onEntityDespawn); + AttackEntityCallback.EVENT.register(DojoManager::onEntityAttacked); + Scheduler.INSTANCE.scheduleCyclic(DojoManager::update, 3); + } + + private static void reset() { + inArena = false; + currentChallenge = DojoChallenges.NONE; + ForceTestHelper.reset(); + StaminaTestHelper.reset(); + MasteryTestHelper.reset(); + SwiftnessTestHelper.reset(); + ControlTestHelper.reset(); + TenacityTestHelper.reset(); + } + + /** + * works out if the player is in dojo and if so what challenge based on chat messages + * + * @param text message + * @param overlay is overlay + */ + private static void onMessage(Text text, Boolean overlay) { + if (!Utils.isInCrimson() || overlay) { + return; + } + if (Objects.equals(Formatting.strip(text.getString()), START_MESSAGE)) { + inArena = true; + //update the players ping + getPing(); + return; + } + if (!inArena) { + return; + } + if (text.getString().matches(CHALLENGE_FINISHED_REGEX)) { + reset(); + return; + } + + //look for a message saying what challenge is starting if one has not already been found + if (currentChallenge != DojoChallenges.NONE) { + return; + } + Matcher nextChallenge = TEST_OF_PATTERN.matcher(text.getString()); + if (nextChallenge.matches()) { + currentChallenge = DojoChallenges.from(nextChallenge.group(1)); + if (!currentChallenge.enabled.test(true)) { + currentChallenge = DojoChallenges.NONE; + } + } + } + + private static void getPing() { + ClientPlayNetworkHandler networkHandler = CLIENT.getNetworkHandler(); + if (networkHandler != null) { + networkHandler.sendPacket(new QueryPingC2SPacket(Util.getMeasuringTimeMs())); + } + } + + public static void onPingResult(long ping) { + DojoManager.ping = ping; + } + + private static void update() { + if (!Utils.isInCrimson() || !inArena) { + return; + } + switch (currentChallenge) { + case STAMINA -> StaminaTestHelper.update(); + case CONTROL -> ControlTestHelper.update(); + } + } + + /** + * called from the {@link de.hysky.skyblocker.skyblock.entity.MobGlow} class and checks the current challenge to see if zombies should be glowing + * + * @param name name of the zombie + * @return if the zombie should glow + */ + public static boolean shouldGlow(String name) { + if (!Utils.isInCrimson() || !inArena) { + return false; + } + return switch (currentChallenge) { + case FORCE -> ForceTestHelper.shouldGlow(name); + case DISCIPLINE -> DisciplineTestHelper.shouldGlow(name); + default -> false; + }; + } + + /** + * called from the {@link de.hysky.skyblocker.skyblock.entity.MobGlow} class and checks the current challenge to see zombie outline color + * + * @return if the zombie should glow + */ + public static int getColor() { + if (!Utils.isInCrimson() || !inArena) { + return 0xf57738; + } + return switch (currentChallenge) { + case FORCE -> ForceTestHelper.getColor(); + case DISCIPLINE -> DisciplineTestHelper.getColor(); + default -> 0xf57738; + }; + } + + /** + * when a block is updated check the current challenge and send the packet to correct helper + * + * @param pos the location of the updated block + * @param state the state of the new block + */ + public static void onBlockUpdate(BlockPos pos, BlockState state) { + if (!Utils.isInCrimson() || !inArena) { + return; + } + switch (currentChallenge) { + case MASTERY -> MasteryTestHelper.onBlockUpdate(pos, state); + case SWIFTNESS -> SwiftnessTestHelper.onBlockUpdate(pos, state); + } + } + + private static void onEntitySpawn(Entity entity, ClientWorld clientWorld) { + if (!Utils.isInCrimson() || !inArena || CLIENT == null || CLIENT.player == null) { + return; + } + // Check if within 50 blocks and 5 blocks vertically + if (entity.squaredDistanceTo(CLIENT.player) > 2500 || Math.abs(entity.getBlockY() - CLIENT.player.getBlockY()) > 5) { + return; + } + switch (currentChallenge) { + case FORCE -> ForceTestHelper.onEntitySpawn(entity); + case CONTROL -> ControlTestHelper.onEntitySpawn(entity); + case TENACITY -> TenacityTestHelper.onEntitySpawn(entity); + } + } + + private static void onEntityDespawn(Entity entity, ClientWorld clientWorld) { + if (!Utils.isInCrimson() || !inArena) { + return; + } + switch (currentChallenge) { + case FORCE -> ForceTestHelper.onEntityDespawn(entity); + case TENACITY -> TenacityTestHelper.onEntityDespawn(entity); + } + } + + private static ActionResult onEntityAttacked(PlayerEntity playerEntity, World world, Hand hand, Entity entity, EntityHitResult entityHitResult) { + if (!Utils.isInCrimson() || !inArena) { + return ActionResult.PASS; + } + if (currentChallenge == DojoChallenges.FORCE) { + ForceTestHelper.onEntityAttacked(entity); + } + return ActionResult.PASS; + } + + public static void onParticle(ParticleS2CPacket packet) { + if (!Utils.isInCrimson() || !inArena) { + return; + } + if (currentChallenge == DojoChallenges.TENACITY) { + TenacityTestHelper.onParticle(packet); + } + } + + private static void render(WorldRenderContext context) { + if (!Utils.isInCrimson() || !inArena) { + return; + } + switch (currentChallenge) { + case FORCE -> ForceTestHelper.render(context); + case STAMINA -> StaminaTestHelper.render(context); + case MASTERY -> MasteryTestHelper.render(context); + case SWIFTNESS -> SwiftnessTestHelper.render(context); + case CONTROL -> ControlTestHelper.render(context); + case TENACITY -> TenacityTestHelper.render(context); + } + } + +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ForceTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ForceTestHelper.java new file mode 100644 index 00000000..70d6a401 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ForceTestHelper.java @@ -0,0 +1,81 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import de.hysky.skyblocker.utils.render.RenderHelper; +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.entity.Entity; +import net.minecraft.entity.mob.ZombieEntity; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.math.Vec3d; + +import java.awt.*; +import java.text.DecimalFormat; +import java.util.Map; + +public class ForceTestHelper { + + private static final DecimalFormat FORMATTER = new DecimalFormat("0.0"); + private static final int ZOMBIE_LIFE_TIME = 10100; + + private static final Object2LongOpenHashMap<ZombieEntity> zombies = new Object2LongOpenHashMap<>(); + + protected static void reset() { + zombies.clear(); + } + + /** + * If a zombie value is negative make it glow + * + * @param name zombies value + * @return if the zombie should glow + */ + protected static boolean shouldGlow(String name) { + return name.contains("-"); + } + + protected static int getColor() { + return Color.RED.getRGB(); + } + + protected static void onEntitySpawn(Entity entity) { + if (entity instanceof ZombieEntity zombie) { + zombies.put(zombie, System.currentTimeMillis() + ZOMBIE_LIFE_TIME); + } + } + + protected static void onEntityAttacked(Entity entity) { + if (entity instanceof ZombieEntity zombie) { + if (zombies.containsKey(zombie)) { + zombies.put(zombie, System.currentTimeMillis() + ZOMBIE_LIFE_TIME); //timer is reset when they are hit + } + } + } + + protected static void onEntityDespawn(Entity entity) { + if (entity instanceof ZombieEntity zombie) { + zombies.removeLong(zombie); + } + } + + protected static void render(WorldRenderContext context) { + //render times + long currentTime = System.currentTimeMillis(); + for (Map.Entry<ZombieEntity, Long> zombie : zombies.object2LongEntrySet()) { + float secondsTime = Math.max((zombie.getValue() - currentTime) / 1000f, 0); + + MutableText text = Text.literal(FORMATTER.format(secondsTime)); + if (secondsTime > 1) { + text = text.formatted(Formatting.GREEN); + } else if (secondsTime > 0) { + text = text.formatted(Formatting.YELLOW); + } else { + text = text.formatted(Formatting.RED); + } + + Vec3d labelPos = zombie.getKey().getCameraPosVec(context.tickCounter().getTickDelta(false)); + RenderHelper.renderText(context, text, labelPos, 1.5f, true); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/MasteryTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/MasteryTestHelper.java new file mode 100644 index 00000000..625b91eb --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/MasteryTestHelper.java @@ -0,0 +1,68 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import de.hysky.skyblocker.utils.render.RenderHelper; +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; + +import java.awt.*; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; + +public class MasteryTestHelper { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + private static final DecimalFormat FORMATTER = new DecimalFormat("0.00"); + /** + * How long it takes for a block to turn red + */ + private static final int BLOCK_LIFE_TIME = 6550; + + private static final List<BlockPos> blockOrder = new ArrayList<>(); + private static final Object2LongOpenHashMap<BlockPos> endTimes = new Object2LongOpenHashMap<>(); + + protected static void reset() { + blockOrder.clear(); + endTimes.clear(); + } + + protected static void onBlockUpdate(BlockPos pos, BlockState state) { + if (CLIENT == null || CLIENT.player == null) { + return; + } + if (state.isOf(Blocks.LIME_WOOL)) { + blockOrder.add(pos); + //add lifetime of a block to the time to get time when block expires + // work out how long it will take between the player firing and arrow hitting the block and to subtract from time + long travelTime = (long) (CLIENT.player.getPos().distanceTo(pos.toCenterPos()) * 1000 / 60); //an arrow speed is about 60 blocks a second from a full draw + endTimes.put(pos, System.currentTimeMillis() + BLOCK_LIFE_TIME - DojoManager.ping - travelTime); + } + if (state.isAir()) { + blockOrder.remove(pos); + endTimes.removeLong(pos); + } + } + + protected static void render(WorldRenderContext context) { + //render connecting lines + if (!blockOrder.isEmpty()) { + RenderHelper.renderLineFromCursor(context, blockOrder.getFirst().toCenterPos(), Color.LIGHT_GRAY.getColorComponents(new float[]{0, 0, 0}), 1f, 2); + } + if (blockOrder.size() >= 2) { + RenderHelper.renderLinesFromPoints(context, new Vec3d[]{blockOrder.get(0).toCenterPos(), blockOrder.get(1).toCenterPos()}, Color.LIGHT_GRAY.getColorComponents(new float[]{0, 0, 0}), 1, 2, false); + } + + //render times + long currentTime = System.currentTimeMillis(); + for (BlockPos pos : blockOrder) { + long blockEndTime = endTimes.getLong(pos); + float secondsTime = Math.max((blockEndTime - currentTime) / 1000f, 0); + RenderHelper.renderText(context, Text.literal(FORMATTER.format(secondsTime)), pos.add(0, 1, 0).toCenterPos(), 3, true); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/StaminaTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/StaminaTestHelper.java new file mode 100644 index 00000000..3f7dfe56 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/StaminaTestHelper.java @@ -0,0 +1,279 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import de.hysky.skyblocker.utils.render.RenderHelper; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.client.MinecraftClient; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.math.Vec3i; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class StaminaTestHelper { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + private static final int WALL_THRESHOLD_VALUE = 13; + private static final int WALL_HEIGHT = 5; + private static final float[] INCOMING_COLOR = new float[]{0f, 1f, 0f, 0f}; + private static final float[] OUTGOING_COLOR = new float[]{1f, 0.64f, 0f, 0f}; + + private static final List<Box> wallHoles = new ArrayList<>(); + private static final List<Box> lastHoles = new ArrayList<>(); + private static final Map<Box, HoleDirection> holeDirections = new HashMap<>(); + private static BlockPos middleBase; + + private enum HoleDirection { + POSITIVE_X, + POSITIVE_Z, + NEGATIVE_X, + NEGATIVE_Z, + NEW, + UNCHANGED + } + + protected static void reset() { + wallHoles.clear(); + lastHoles.clear(); + holeDirections.clear(); + middleBase = null; + } + + protected static void update() { + + //search the world around the player for walls 30 x 10 x 30 area centered on player + + List<BlockPos> currentBottomWallLocations = findWallBlocks(); + if (currentBottomWallLocations == null) { //stop here if the center pos has not been found + return; + } + //find walls + List<Box> walls = findWalls(currentBottomWallLocations); + + //find air then holes and add whole to list + lastHoles.clear(); + lastHoles.addAll(wallHoles); + wallHoles.clear(); + for (Box wall : walls) { + wallHoles.addAll(findHolesInBox(wall)); + } + // get direction for the holes + Map<Box, HoleDirection> lastHoleDirections = new HashMap<>(holeDirections); + holeDirections.clear(); + for (Box hole : wallHoles) { + HoleDirection holeDirection = getWholeDirection(hole); + if (holeDirection == HoleDirection.UNCHANGED) { + holeDirections.put(hole, lastHoleDirections.get(hole)); + continue; + } + holeDirections.put(hole, holeDirection); + } + } + + /** + * Locates the center of the game and once this is found scans the bottom of room for blocks that make up the walls + * + * @return list of blocks that make up the bottom of the walls + */ + private static List<BlockPos> findWallBlocks() { + if (CLIENT == null || CLIENT.player == null || CLIENT.world == null) { + return null; + } + BlockPos playerPos = CLIENT.player.getBlockPos(); + //find the center first before starting to look for walls + if (middleBase == null) { + for (int x = playerPos.getX() - 10; x < playerPos.getX() + 10; x++) { + for (int y = playerPos.getY() - 5; y < playerPos.getY(); y++) { + for (int z = playerPos.getZ() - 10; z < playerPos.getZ() + 10; z++) { + BlockPos pos = new BlockPos(x, y, z); + BlockState state = CLIENT.world.getBlockState(pos); + if (state.isOf(Blocks.CHISELED_STONE_BRICKS)) { + middleBase = pos; + return null; + } + } + } + } + return null; + } + List<BlockPos> currentBottomWallLocations = new ArrayList<>(); + for (int x = middleBase.getX() - 15; x < middleBase.getX() + 15; x++) { + for (int z = middleBase.getZ() - 15; z < middleBase.getZ() + 15; z++) { + BlockPos pos = new BlockPos(x, middleBase.getY() + 1, z); + BlockState state = CLIENT.world.getBlockState(pos); + //find the bottom of walls + if (!state.isAir()) { + currentBottomWallLocations.add(pos); + } + } + } + return currentBottomWallLocations; + } + + private static List<Box> findWalls(List<BlockPos> currentBottomWallLocations) { + Int2ObjectOpenHashMap<List<BlockPos>> possibleWallsX = new Int2ObjectOpenHashMap<>(); + Int2ObjectOpenHashMap<List<BlockPos>> possibleWallsZ = new Int2ObjectOpenHashMap<>(); + for (BlockPos block : currentBottomWallLocations) { + //add to the x walls + int x = block.getX(); + if (!possibleWallsX.containsKey(x)) { + possibleWallsX.put(x, new ArrayList<>()); + + } + possibleWallsX.get(x).add(block); + //add to the z walls + int z = block.getZ(); + if (!possibleWallsZ.containsKey(z)) { + possibleWallsZ.put(z, new ArrayList<>()); + } + possibleWallsZ.get(z).add(block); + } + + //extract only the lines that are long enough to be a wall and not from walls overlapping + List<List<BlockPos>> walls = new ArrayList<>(); + for (List<BlockPos> line : possibleWallsX.values()) { + if (line.size() >= WALL_THRESHOLD_VALUE) { + walls.add(line); + } + } + for (List<BlockPos> line : possibleWallsZ.values()) { + if (line.size() >= WALL_THRESHOLD_VALUE) { + walls.add(line); + } + } + + //final find the maximum values for each wall to output a box for them + List<Box> wallBoxes = new ArrayList<>(); + for (List<BlockPos> wall : walls) { + BlockPos minPos = wall.getFirst(); + BlockPos maxPos = wall.getFirst(); + for (BlockPos pos : wall) { + if (pos.getX() < minPos.getX()) { + minPos = new BlockPos(pos.getX(), minPos.getY(), minPos.getZ()); + } + if (pos.getZ() < minPos.getZ()) { + minPos = new BlockPos(minPos.getX(), minPos.getY(), pos.getZ()); + } + + if (pos.getX() > maxPos.getX()) { + maxPos = new BlockPos(pos.getX(), maxPos.getY(), maxPos.getZ()); + } + if (pos.getZ() > maxPos.getZ()) { + maxPos = new BlockPos(maxPos.getX(), maxPos.getY(), pos.getZ()); + } + } + //expand wall to top + maxPos = new BlockPos(maxPos.getX(), maxPos.getY() + WALL_HEIGHT, maxPos.getZ()); + + wallBoxes.add(Box.enclosing(minPos, maxPos)); + } + + return wallBoxes; + } + + private static List<Box> findHolesInBox(Box box) { + List<Box> holes = new ArrayList<>(); + if (CLIENT == null || CLIENT.player == null || CLIENT.world == null) { + return holes; + } + //get the direction vector + Vec3i wallDirection = box.getLengthX() == 1 ? new Vec3i(0, 0, 1) : new Vec3i(1, 0, 0); + //find the corners of boxes (only need 3) + List<BlockPos> topLeft = new ArrayList<>(); + List<BlockPos> topRight = new ArrayList<>(); + List<BlockPos> bottomLeft = new ArrayList<>(); + for (int z = (int) box.minZ; z < box.maxZ; z++) { + for (int x = (int) box.minX; x < box.maxX; x++) { + for (int y = (int) box.minY; y < box.maxY; y++) { + BlockPos pos = new BlockPos(x, y, z); + BlockState state = CLIENT.world.getBlockState(pos); + if (!state.isAir()) { + //do not check non-air + continue; + } + boolean top = y == box.maxY - 1 || !CLIENT.world.getBlockState(pos.add(0, 1, 0)).isAir(); + boolean bottom = !CLIENT.world.getBlockState(pos.add(0, -1, 0)).isAir(); + boolean left = !CLIENT.world.getBlockState(pos.add(wallDirection)).isAir(); + boolean right = !CLIENT.world.getBlockState(pos.subtract(wallDirection)).isAir(); + if (top) { + if (left) { + topLeft.add(pos); + } + if (right) { + topRight.add(pos); + } + } + if (bottom && left) { + bottomLeft.add(pos); + } + + } + } + } + // gets box around top of hole then expands to the bottom of hole + for (int i = 0; i < topLeft.size(); i++) { + if (topRight.size() <= i || bottomLeft.size() <= i) { + //if corners can not be found end looking + break; + } + Box hole = Box.enclosing(topLeft.get(i), topRight.get(i)); + hole = hole.stretch(0, bottomLeft.get(i).getY() - topLeft.get(i).getY(), 0); + holes.add(hole); + } + return holes; + } + + private static HoleDirection getWholeDirection(Box hole) { + //the value has not changed since last time + if (lastHoles.contains(hole)) { + return HoleDirection.UNCHANGED; + } + //check each direction to work out which way the whole is going + Box posX = hole.offset(1, 0, 0); + if (lastHoles.contains(posX)) { + return HoleDirection.POSITIVE_X; + } + Box negX = hole.offset(-1, 0, 0); + if (lastHoles.contains(negX)) { + return HoleDirection.NEGATIVE_X; + } + Box posZ = hole.offset(0, 0, 1); + if (lastHoles.contains(posZ)) { + return HoleDirection.POSITIVE_Z; + } + Box negZ = hole.offset(0, 0, -1); + if (lastHoles.contains(negZ)) { + return HoleDirection.NEGATIVE_Z; + } + // if pos can not be found mark as new + return HoleDirection.NEW; + + } + + protected static void render(WorldRenderContext context) { + if (wallHoles.isEmpty() || CLIENT == null || CLIENT.player == null) { + return; + } + BlockPos playerPos = CLIENT.player.getBlockPos(); + for (Box hole : wallHoles) { + float[] color = isHoleIncoming(hole, holeDirections.get(hole), playerPos) ? INCOMING_COLOR : OUTGOING_COLOR; + RenderHelper.renderFilled(context, new BlockPos((int) hole.minX, (int) hole.minY, (int) hole.minZ), new Vec3d(hole.getLengthX(), hole.getLengthY(), hole.getLengthZ()), color, 0.3f, false); + } + } + + private static boolean isHoleIncoming(Box holePos, HoleDirection holeDirection, BlockPos playerPos) { + return switch (holeDirection) { + case POSITIVE_X -> playerPos.getX() < holePos.minX; + case POSITIVE_Z -> playerPos.getZ() < holePos.minZ; + case NEGATIVE_X -> playerPos.getX() > holePos.maxX; + case NEGATIVE_Z -> playerPos.getZ() > holePos.maxZ; + + default -> true; + }; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/SwiftnessTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/SwiftnessTestHelper.java new file mode 100644 index 00000000..678005c4 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/SwiftnessTestHelper.java @@ -0,0 +1,34 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.util.math.BlockPos; + +public class SwiftnessTestHelper { + + private static BlockPos lastBlock; + + protected static void reset() { + lastBlock = null; + } + + protected static void onBlockUpdate(BlockPos pos, BlockState state) { + if (state.isOf(Blocks.LIME_WOOL)) { + lastBlock = pos.toImmutable(); + } + } + + /** + * Renders a green block around the newest block + * + * @param context render context + */ + protected static void render(WorldRenderContext context) { + if (lastBlock == null) { + return; + } + RenderHelper.renderFilled(context, lastBlock, new float[]{0f, 1f, 0f}, 0.5f, true); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/TenacityTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/TenacityTestHelper.java new file mode 100644 index 00000000..51e99fbd --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/TenacityTestHelper.java @@ -0,0 +1,100 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import de.hysky.skyblocker.utils.render.RenderHelper; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import net.minecraft.entity.decoration.ArmorStandEntity; +import net.minecraft.network.packet.s2c.play.ParticleS2CPacket; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.RaycastContext; + +public class TenacityTestHelper { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + + private static final Object2ObjectOpenHashMap<ArmorStandEntity, Vec3d> fireBallsWithStartPos = new Object2ObjectOpenHashMap<>(); + private static final Object2ObjectOpenHashMap<ArmorStandEntity, Vec3d> particleOffsets = new Object2ObjectOpenHashMap<>(); + + protected static void reset() { + fireBallsWithStartPos.clear(); + particleOffsets.clear(); + } + + protected static void render(WorldRenderContext context) { + for (ArmorStandEntity fireball : fireBallsWithStartPos.keySet()) { + Vec3d lineStart = fireBallsWithStartPos.get(fireball).add(particleOffsets.getOrDefault(fireball, Vec3d.ZERO)); + Vec3d fireballPos = fireball.getPos().add(particleOffsets.getOrDefault(fireball, Vec3d.ZERO)); + + Vec3d distance = fireballPos.subtract(lineStart); + if (distance.length() > 0.02) { //if big enough gap try from start calculate and show trajectory + distance = distance.multiply(100); + Vec3d lineEnd = lineStart.add(distance); + + RenderHelper.renderLinesFromPoints(context, new Vec3d[]{lineStart, lineEnd}, new float[]{1f, 0f, 0f}, 1, 3, false); + + //get highlighted block + HitResult hitResult = raycast(lineStart, lineEnd, fireball); + if (hitResult != null && hitResult.getType() == HitResult.Type.BLOCK && hitResult instanceof BlockHitResult blockHitResult) { + RenderHelper.renderFilled(context, blockHitResult.getBlockPos(), new float[]{1f, 0f, 0f}, 0.5f, false); + } + } + } + } + + protected static HitResult raycast(Vec3d start, Vec3d end, ArmorStandEntity fireball) { + if (CLIENT == null || CLIENT.world == null) { + return null; + } + return CLIENT.world.raycast(new RaycastContext(start, end, RaycastContext.ShapeType.OUTLINE, RaycastContext.FluidHandling.ANY, fireball)); + } + + /** + * If a spawned entity is an armour stand add it to the fireballs map (assuming all armour stands are fireballs) + * + * @param entity spawned entity + */ + protected static void onEntitySpawn(Entity entity) { + if (entity instanceof ArmorStandEntity armorStand) { + fireBallsWithStartPos.put(armorStand, armorStand.getPos()); + } + } + + protected static void onEntityDespawn(Entity entity) { + if (entity instanceof ArmorStandEntity armorStand) { + fireBallsWithStartPos.remove(armorStand); + } + } + + /** + * Uses the particles spawned with the fireballs to offset from the armour stand position to get a more accurate guess of where it's going + * + * @param packet particle packet + */ + protected static void onParticle(ParticleS2CPacket packet) { + if (!ParticleTypes.FLAME.equals(packet.getParameters().getType())) { + return; + } + //get nearest fireball to particle + Vec3d particlePos = new Vec3d(packet.getX(), packet.getY(), packet.getZ()); + ArmorStandEntity neareastFireball = null; + double clostestDistance = 50; + for (ArmorStandEntity fireball : fireBallsWithStartPos.keySet()) { + double distance = fireball.getPos().distanceTo(particlePos); + if (distance < clostestDistance) { + neareastFireball = fireball; + clostestDistance = distance; + } + } + if (neareastFireball == null) { //can not find fireball near particle + return; + } + //adjust fireball offset with particle pos + Vec3d delta = particlePos.subtract(neareastFireball.getPos()); + //update values + particleOffsets.put(neareastFireball, delta); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java index 7a5abed1..d1fc08ec 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java @@ -304,7 +304,7 @@ public class DungeonScore { if (s.equals("You")) return MinecraftClient.getInstance().getSession().getUsername(); //This will be wrong if the dead player is called 'You' but that's unlikely else return s; }); - ProfileUtils.updateProfile(whoDied).thenAccept(player -> firstDeathHasSpiritPet = hasSpiritPet(player, whoDied)); + ProfileUtils.updateProfileByName(whoDied).thenAccept(player -> firstDeathHasSpiritPet = hasSpiritPet(player, whoDied)); } private static void checkMessageForWatcher(String message) { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Reparty.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Reparty.java index 6165ac6a..80753c1d 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Reparty.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Reparty.java @@ -1,94 +1,112 @@ package de.hysky.skyblocker.skyblock.dungeon; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Constants; 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.azureaaron.hmapi.data.party.PartyRole; +import net.azureaaron.hmapi.events.HypixelPacketEvents; +import net.azureaaron.hmapi.network.HypixelNetworking; +import net.azureaaron.hmapi.network.packet.s2c.ErrorS2CPacket; +import net.azureaaron.hmapi.network.packet.s2c.HypixelS2CPacket; +import net.azureaaron.hmapi.network.packet.v2.s2c.PartyInfoS2CPacket; 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.Map; +import java.util.Objects; +import java.util.UUID; import java.util.regex.Matcher; -import java.util.regex.Pattern; + +import org.slf4j.Logger; + +import com.mojang.brigadier.Command; +import com.mojang.logging.LogUtils; 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); - } + private static final Logger LOGGER = LogUtils.getLogger(); + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + private static final int BASE_DELAY = 10; + + private boolean repartying; + private String partyLeader; + + public Reparty() { + super("^(?:([\\[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; + HypixelPacketEvents.PARTY_INFO.register(this::onPacket); + 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; + HypixelNetworking.sendPartyInfoC2SPacket(2); + + return Command.SINGLE_SUCCESS; + }))); + } + + private void onPacket(HypixelS2CPacket packet) { + switch (packet) { + case PartyInfoS2CPacket(var inParty, var members) when this.repartying -> { + UUID ourUuid = Objects.requireNonNull(CLIENT.getSession().getUuidOrNull()); + + if (inParty && members.get(ourUuid) == PartyRole.LEADER) { + sendCommand("/p disband", 1); + int count = 0; + + for (Map.Entry<UUID, PartyRole> entry : members.entrySet()) { + UUID uuid = entry.getKey(); + PartyRole role = entry.getValue(); + + //Don't invite ourself + if (role != PartyRole.LEADER) sendCommand("/p " + uuid.toString(), ++count + 2); + } + + Scheduler.INSTANCE.schedule(() -> this.repartying = false, count * BASE_DELAY); + } else { + CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.reparty.notInPartyOrNotLeader"))); + this.repartying = false; + } + } + + case ErrorS2CPacket(var id, var error) when id.equals(PartyInfoS2CPacket.ID) && this.repartying -> { + CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.reparty.error"))); + LOGGER.error("[Skyblocker Reparty] The party info packet returned an unexpected error! {}", error); + + this.repartying = false; + } + + default -> {} //Do nothing + } + } + + @Override + public ChatFilterResult state() { + return SkyblockerConfigManager.get().general.acceptReparty ? ChatFilterResult.FILTER : ChatFilterResult.PASS; + } + + @Override + public boolean onMatch(Text message, Matcher matcher) { + if (matcher.group("disband") != null && !matcher.group("disband").equals(CLIENT.getSession().getUsername())) { + partyLeader = matcher.group("disband"); + Scheduler.INSTANCE.schedule(() -> partyLeader = null, 61); + } else if (matcher.group("invite") != null && matcher.group("invite").equals(partyLeader)) { + String command = "/party accept " + partyLeader; + sendCommand(command, 0); + } + + return false; + } + + 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/device/LightsOn.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/device/LightsOn.java new file mode 100644 index 00000000..555a8e4b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/device/LightsOn.java @@ -0,0 +1,46 @@ +package de.hysky.skyblocker.skyblock.dungeon.device; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.dungeon.DungeonBoss; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; +import de.hysky.skyblocker.utils.ColorUtils; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.state.property.Properties; +import net.minecraft.util.DyeColor; +import net.minecraft.util.math.BlockPos; + +public class LightsOn { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + private static final BlockPos TOP_LEFT = new BlockPos(62, 136, 142); + private static final BlockPos TOP_RIGHT = new BlockPos(58, 136, 142); + private static final BlockPos MIDDLE_TOP = new BlockPos(60, 135, 142); + private static final BlockPos MIDDLE_BOTTOM = new BlockPos(60, 134, 142); + private static final BlockPos BOTTOM_LEFT = new BlockPos(62, 133, 142); + private static final BlockPos BOTTOM_RIGHT = new BlockPos(58, 133, 142); + private static final BlockPos[] LEVERS = { TOP_LEFT, TOP_RIGHT, MIDDLE_TOP, MIDDLE_BOTTOM, BOTTOM_LEFT, BOTTOM_RIGHT }; + private static final float[] RED = ColorUtils.getFloatComponents(DyeColor.RED); + + public static void init() { + WorldRenderEvents.AFTER_TRANSLUCENT.register(LightsOn::render); + } + + private static void render(WorldRenderContext context) { + if (SkyblockerConfigManager.get().dungeons.devices.solveLightsOn && Utils.isInDungeons() && DungeonManager.isInBoss() && DungeonManager.getBoss() == DungeonBoss.MAXOR) { + for (BlockPos lever : LEVERS) { + ClientWorld world = CLIENT.world; + BlockState state = world.getBlockState(lever); + + if (state.getBlock().equals(Blocks.LEVER) && state.contains(Properties.POWERED) && !state.get(Properties.POWERED)) { + RenderHelper.renderFilled(context, lever, RED, 0.5f, false); + } + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/device/SimonSays.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/device/SimonSays.java new file mode 100644 index 00000000..5aa97dd9 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/device/SimonSays.java @@ -0,0 +1,122 @@ +package de.hysky.skyblocker.skyblock.dungeon.device; + +import java.util.Objects; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.dungeon.DungeonBoss; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; +import de.hysky.skyblocker.utils.Boxes; +import de.hysky.skyblocker.utils.ColorUtils; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.RenderHelper; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectList; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import it.unimi.dsi.fastutil.objects.ObjectSet; +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.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.DyeColor; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +public class SimonSays { + private static final Box BOARD_AREA = Box.enclosing(new BlockPos(111, 123, 92), new BlockPos(111, 120, 95)); + private static final Box BUTTONS_AREA = Box.enclosing(new BlockPos(110, 123, 92), new BlockPos(110, 120, 95)); + private static final BlockPos START_BUTTON = new BlockPos(110, 121, 91); + private static final float[] GREEN = ColorUtils.getFloatComponents(DyeColor.LIME); + private static final float[] YELLOW = ColorUtils.getFloatComponents(DyeColor.YELLOW); + private static final ObjectSet<BlockPos> CLICKED_BUTTONS = new ObjectOpenHashSet<>(); + private static final ObjectList<BlockPos> SIMON_PATTERN = new ObjectArrayList<>(); + + public static void init() { + UseBlockCallback.EVENT.register(SimonSays::onBlockInteract); + ClientPlayConnectionEvents.JOIN.register((_handler, _sender, _client) -> reset()); + WorldRenderEvents.AFTER_TRANSLUCENT.register(SimonSays::render); + } + + //When another player is pressing the buttons hypixel doesnt send block or block state updates + //so you can't see it which means the solver can only count the buttons you press yourself + private static ActionResult onBlockInteract(PlayerEntity player, World world, Hand hand, BlockHitResult hitResult) { + if (shouldProcess()) { + BlockPos pos = hitResult.getBlockPos(); + Block block = world.getBlockState(pos).getBlock(); + + if (block.equals(Blocks.STONE_BUTTON)) { + if (BUTTONS_AREA.contains(Vec3d.of(pos))) { + CLICKED_BUTTONS.add(new BlockPos(pos)); //Copy just in case it becomes mutable in the future + } else if (pos.equals(START_BUTTON)) { + reset(); + } + } + } + + //This could also be used to cancel incorrect clicks in the future + return ActionResult.PASS; + } + + //If the player goes out of the range required to receive block/chunk updates then their solver won't detect stuff but that + //doesn't matter because if they're doing pre-4 or something they won't be doing the ss, and if they end up needing to they can + //just reset it or have the other person finish the current sequence first then let them do it. + public static void onBlockUpdate(BlockPos pos, BlockState state) { + if (shouldProcess()) { + Vec3d posVec = Vec3d.of(pos); + Block block = state.getBlock(); + + if (BOARD_AREA.contains(posVec) && block.equals(Blocks.SEA_LANTERN)) { + SIMON_PATTERN.add(pos.toImmutable()); //Convert to immutable because chunk delta updates use the mutable variant + } else if (BUTTONS_AREA.contains(posVec) && block.equals(Blocks.AIR)) { + //Upon reaching the showing of the next sequence we need to reset the state so that we don't show old data + //Otherwise, the nextIndex will go beyond 5 and that can cause bugs, it also helps with the other case noted above + reset(); + } + } + } + + private static void render(WorldRenderContext context) { + if (shouldProcess()) { + int buttonsRendered = 0; + + for (BlockPos pos : SIMON_PATTERN) { + //Offset to west (x - 1) to get the position of the button from the sea lantern block + BlockPos buttonPos = pos.west(); + ClientWorld world = Objects.requireNonNull(MinecraftClient.getInstance().world); //Should never be null here + BlockState state = world.getBlockState(buttonPos); + + //If the button hasn't been clicked yet + //Also don't do anything if the button isn't there which means the device is showing the sequence + if (!CLICKED_BUTTONS.contains(buttonPos) && state.getBlock().equals(Blocks.STONE_BUTTON)) { + Box outline = RenderHelper.getBlockBoundingBox(world, state, buttonPos); + float[] colour = buttonsRendered == 0 ? GREEN : YELLOW; + + RenderHelper.renderFilled(context, Boxes.getMinVec(outline), Boxes.getLengthVec(outline), colour, 0.5f, true); + RenderHelper.renderOutline(context, outline, colour, 5f, true); + + if (++buttonsRendered == 2) return; + } + } + } + } + + private static boolean shouldProcess() { + return SkyblockerConfigManager.get().dungeons.devices.solveSimonSays && + Utils.isInDungeons() && DungeonManager.isInBoss() && DungeonManager.getBoss() == DungeonBoss.MAXOR; + } + + private static void reset() { + CLICKED_BUTTONS.clear(); + SIMON_PATTERN.clear(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CommissionHighlight.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CommissionHighlight.java new file mode 100644 index 00000000..de26809c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CommissionHighlight.java @@ -0,0 +1,38 @@ +package de.hysky.skyblocker.skyblock.dwarven; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.render.gui.ColorHighlight; +import de.hysky.skyblocker.utils.render.gui.ContainerSolver; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +public class CommissionHighlight extends ContainerSolver { + + public CommissionHighlight() { + super("^Commissions$"); + } + + @Override + protected boolean isEnabled() { + return SkyblockerConfigManager.get().mining.commissionHighlight; + } + + @Override + protected List<ColorHighlight> getColors(String[] groups, Int2ObjectMap<ItemStack> slots) { + List<ColorHighlight> highlights = new ArrayList<>(); + for (Int2ObjectMap.Entry<ItemStack> entry : slots.int2ObjectEntrySet()) { + ItemStack stack = entry.getValue(); + if (stack != null && stack.contains(DataComponentTypes.LORE)) { + if (ItemUtils.getLoreLineIf(stack, s -> s.contains("COMPLETED")) != null) { + highlights.add(ColorHighlight.green(entry.getIntKey())); + } + } + } + return highlights; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java index 6f4c86a7..d709181f 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java @@ -42,6 +42,12 @@ import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.arg import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; import static net.minecraft.command.CommandSource.suggestMatching; +/** + * Manager for Crystal Hollows waypoints that handles {@link #update() location detection}, + * {@link #extractLocationFromMessage(Text, Boolean) waypoints receiving}, {@link #shareWaypoint(String) sharing}, + * {@link #registerWaypointLocationCommands(CommandDispatcher, CommandRegistryAccess) commands}, and + * {@link #render(WorldRenderContext) rendering}. + */ public class CrystalsLocationsManager { private static final Logger LOGGER = LogUtils.getLogger(); private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); @@ -55,11 +61,15 @@ public class CrystalsLocationsManager { protected static Map<String, CrystalsWaypoint> activeWaypoints = new HashMap<>(); public static void init() { + // Crystal Hollows Waypoints Scheduler.INSTANCE.scheduleCyclic(CrystalsLocationsManager::update, 40); WorldRenderEvents.AFTER_TRANSLUCENT.register(CrystalsLocationsManager::render); ClientReceiveMessageEvents.GAME.register(CrystalsLocationsManager::extractLocationFromMessage); ClientCommandRegistrationCallback.EVENT.register(CrystalsLocationsManager::registerWaypointLocationCommands); ClientPlayConnectionEvents.JOIN.register((_handler, _sender, _client) -> reset()); + + // Nucleus Waypoints + WorldRenderEvents.AFTER_TRANSLUCENT.register(NucleusWaypoints::render); } private static void extractLocationFromMessage(Text message, Boolean overlay) { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/NucleusWaypoints.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/NucleusWaypoints.java new file mode 100644 index 00000000..8046ed19 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/NucleusWaypoints.java @@ -0,0 +1,61 @@ +package de.hysky.skyblocker.skyblock.dwarven; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.text.TextColor; +import net.minecraft.text.Style; +import net.minecraft.util.DyeColor; +import net.minecraft.util.math.BlockPos; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +public class NucleusWaypoints { + private static final Logger LOGGER = LoggerFactory.getLogger(NucleusWaypoints.class); + + private static class Waypoint { + BlockPos position; + String name; + DyeColor color; + + Waypoint(BlockPos position, String name, DyeColor color) { + this.position = position; + this.name = name; + this.color = color; + } + } + + private static final List<Waypoint> WAYPOINTS = List.of( + new Waypoint(new BlockPos(551, 116, 551), "Precursor Remnants", DyeColor.LIGHT_BLUE), + new Waypoint(new BlockPos(551, 116, 475), "Mithril Deposits", DyeColor.LIME), + new Waypoint(new BlockPos(475, 116, 551), "Goblin Holdout", DyeColor.ORANGE), + new Waypoint(new BlockPos(475, 116, 475), "Jungle", DyeColor.PURPLE), + new Waypoint(new BlockPos(513, 106, 524), "Nucleus", DyeColor.RED) + ); + + public static void render(WorldRenderContext context) { + try { + boolean enabled = SkyblockerConfigManager.get().mining.crystalHollows.nucleusWaypoints; + boolean inCrystalHollows = Utils.isInCrystalHollows(); + + if (enabled && inCrystalHollows) { + for (Waypoint waypoint : WAYPOINTS) { + + int rgb = waypoint.color.getFireworkColor(); + TextColor textColor = TextColor.fromRgb(rgb); + + MutableText text = Text.literal(waypoint.name).setStyle(Style.EMPTY.withColor(textColor)); + + RenderHelper.renderText(context, text, waypoint.position.toCenterPos().add(0, 5, 0), 8, true); + } + } + } catch (Exception e) { + LOGGER.error("[{}] Error occurred while rendering Nucleus waypoints. {}", LOGGER.getName(), e); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java index d6f9410b..81e328ca 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java @@ -1,6 +1,7 @@ package de.hysky.skyblocker.skyblock.entity; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; import de.hysky.skyblocker.skyblock.dungeon.LividColor; import de.hysky.skyblocker.skyblock.end.TheEnd; import de.hysky.skyblocker.utils.ItemUtils; @@ -10,6 +11,7 @@ import de.hysky.skyblocker.utils.render.culling.OcclusionCulling; import net.minecraft.entity.Entity; import net.minecraft.entity.decoration.ArmorStandEntity; import net.minecraft.entity.mob.EndermanEntity; +import net.minecraft.entity.mob.ZombieEntity; import net.minecraft.entity.passive.BatEntity; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.ItemStack; @@ -28,6 +30,7 @@ public class MobGlow { if (OcclusionCulling.getReducedCuller().isVisible(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) { String name = entity.getName().getString(); + // Dungeons if (Utils.isInDungeons() && !entity.isInvisible()) { return switch (entity) { @@ -46,6 +49,7 @@ public class MobGlow { }; } + return switch (entity) { // Rift case PlayerEntity p when Utils.isInTheRift() && !entity.isInvisible() && name.equals("Blobbercyst ") -> SkyblockerConfigManager.get().otherLocations.rift.blobbercystGlow; @@ -57,6 +61,9 @@ public class MobGlow { // Special Zelot case EndermanEntity enderman when Utils.isInTheEnd() && !entity.isInvisible() -> TheEnd.isSpecialZealot(enderman); + //dojo + case ZombieEntity zombie when Utils.isInCrimson() && DojoManager.inArena -> DojoManager.shouldGlow(getArmorStandName(zombie)); + default -> false; }; } @@ -66,6 +73,7 @@ public class MobGlow { /** * Checks if an entity is starred by checking if its armor stand contains a star in its name. + * * @param entity the entity to check. * @return true if the entity is starred, false otherwise */ @@ -74,6 +82,20 @@ public class MobGlow { return !armorStands.isEmpty() && armorStands.getFirst().getName().getString().contains("✯"); } + /** + * Returns name of entity by finding closed armor stand and getting name of that + * + * @param entity the entity to check + * @return the name string of the entities label + */ + public static String getArmorStandName(Entity entity) { + List<ArmorStandEntity> armorStands = getArmorStands(entity); + if (armorStands.isEmpty()) { + return ""; + } + return armorStands.getFirst().getName().getString(); + } + public static List<ArmorStandEntity> getArmorStands(Entity entity) { return getArmorStands(entity.getWorld(), entity.getBoundingBox()); } @@ -94,6 +116,7 @@ public class MobGlow { case EndermanEntity enderman when TheEnd.isSpecialZealot(enderman) -> Formatting.RED.getColorValue(); case ArmorStandEntity armorStand when isNukekubiHead(armorStand) -> 0x990099; + case ZombieEntity zombie when Utils.isInCrimson() && DojoManager.inArena -> DojoManager.getColor(); default -> 0xf57738; }; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java b/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java index da2a0c2f..e846a88a 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java @@ -13,7 +13,6 @@ import de.hysky.skyblocker.utils.Http; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.scheduler.Scheduler; import it.unimi.dsi.fastutil.ints.IntList; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.minecraft.client.MinecraftClient; @@ -29,6 +28,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; public class EventNotifications { private static final Logger LOGGER = LogUtils.getLogger(); @@ -39,21 +39,19 @@ public class EventNotifications { public static final IntList DEFAULT_REMINDERS = IntList.of(60, 60 * 5); - public static final Map<String, ItemStack> eventIcons = new Object2ObjectOpenHashMap<>(); - - static { - eventIcons.put("Dark Auction", new ItemStack(Items.NETHER_BRICK)); - eventIcons.put("Bonus Fishing Festival", new ItemStack(Items.FISHING_ROD)); - eventIcons.put("Bonus Mining Fiesta", new ItemStack(Items.IRON_PICKAXE)); - eventIcons.put(JACOBS, new ItemStack(Items.IRON_HOE)); - eventIcons.put("New Year Celebration", new ItemStack(Items.CAKE)); - eventIcons.put("Election Over!", new ItemStack(Items.JUKEBOX)); - eventIcons.put("Election Booth Opens", new ItemStack(Items.JUKEBOX)); - eventIcons.put("Spooky Festival", new ItemStack(Items.JACK_O_LANTERN)); - eventIcons.put("Season of Jerry", new ItemStack(Items.SNOWBALL)); - eventIcons.put("Jerry's Workshop Opens", new ItemStack(Items.SNOW_BLOCK)); - eventIcons.put("Traveling Zoo", new ItemStack(Items.HAY_BLOCK)); // change to the custom head one day - } + public static final Map<String, ItemStack> eventIcons = Map.ofEntries( + Map.entry("Dark Auction", new ItemStack(Items.NETHER_BRICK)), + Map.entry("Bonus Fishing Festival", new ItemStack(Items.FISHING_ROD)), + Map.entry("Bonus Mining Fiesta", new ItemStack(Items.IRON_PICKAXE)), + Map.entry(JACOBS, new ItemStack(Items.IRON_HOE)), + Map.entry("New Year Celebration", new ItemStack(Items.CAKE)), + Map.entry("Election Over!", new ItemStack(Items.JUKEBOX)), + Map.entry("Election Booth Opens", new ItemStack(Items.JUKEBOX)), + Map.entry("Spooky Festival", new ItemStack(Items.JACK_O_LANTERN)), + Map.entry("Season of Jerry", new ItemStack(Items.SNOWBALL)), + Map.entry("Jerry's Workshop Opens", new ItemStack(Items.SNOW_BLOCK)), + Map.entry("Traveling Zoo", new ItemStack(Items.HAY_BLOCK)) // change to the custom head one day + ); public static void init() { Scheduler.INSTANCE.scheduleCyclic(EventNotifications::timeUpdate, 20); @@ -85,7 +83,7 @@ public class EventNotifications { )); } - private static final Map<String, LinkedList<SkyblockEvent>> events = new Object2ObjectOpenHashMap<>(); + private static final Map<String, LinkedList<SkyblockEvent>> events = new ConcurrentHashMap<>(); public static Map<String, LinkedList<SkyblockEvent>> getEvents() { return events; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/SimpleChatFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/SimpleChatFilter.java index 025b3dce..2521b3a9 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/filters/SimpleChatFilter.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/SimpleChatFilter.java @@ -2,11 +2,12 @@ package de.hysky.skyblocker.skyblock.filters; import de.hysky.skyblocker.utils.chat.ChatPatternListener; import net.minecraft.text.Text; +import org.intellij.lang.annotations.Language; import java.util.regex.Matcher; public abstract class SimpleChatFilter extends ChatPatternListener { - public SimpleChatFilter(String pattern) { + protected SimpleChatFilter(@Language("RegExp") String pattern) { super(pattern); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java b/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java index 50982d29..214ecc84 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java @@ -10,7 +10,6 @@ import com.mojang.brigadier.CommandDispatcher; import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; import com.mojang.serialization.codecs.RecordCodecBuilder; -import com.mojang.util.UndashedUuid; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.Http; @@ -44,6 +43,7 @@ import java.nio.file.Path; import java.util.Base64; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; public class MuseumItemCache { private static final Logger LOGGER = LoggerFactory.getLogger(MuseumItemCache.class); @@ -113,7 +113,7 @@ public class MuseumItemCache { String uuid = Utils.getUndashedUuid(); //Be safe about access to avoid NPEs Map<String, ProfileMuseumData> playerData = MUSEUM_ITEM_CACHE.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()); - playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY); + playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY.get()); playerData.get(profileId).collectedItemIds().add(itemId); save(); @@ -224,7 +224,7 @@ public class MuseumItemCache { if (loaded.isDone() && (!MUSEUM_ITEM_CACHE.containsKey(uuid) || !MUSEUM_ITEM_CACHE.getOrDefault(uuid, new Object2ObjectOpenHashMap<>()).containsKey(profileId))) { Map<String, ProfileMuseumData> playerData = MUSEUM_ITEM_CACHE.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()); - playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY); + playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY.get()); updateData4ProfileMember(uuid, profileId); } @@ -238,7 +238,7 @@ public class MuseumItemCache { } private record ProfileMuseumData(long lastResync, ObjectOpenHashSet<String> collectedItemIds) { - private static final ProfileMuseumData EMPTY = new ProfileMuseumData(0L, null); + private static final Supplier<ProfileMuseumData> EMPTY = () -> new ProfileMuseumData(0L, new ObjectOpenHashSet<>()); private static final long TIME_BETWEEN_RESYNCING_ALLOWED = 600_000L; private static final Codec<ProfileMuseumData> CODEC = RecordCodecBuilder.create(instance -> instance.group( Codec.LONG.fieldOf("lastResync").forGetter(ProfileMuseumData::lastResync), @@ -256,4 +256,4 @@ public class MuseumItemCache { return this.lastResync + TIME_BETWEEN_RESYNCING_ALLOWED < System.currentTimeMillis(); } } -}
\ No newline at end of file +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java new file mode 100644 index 00000000..42a52a85 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java @@ -0,0 +1,194 @@ +package de.hysky.skyblocker.skyblock.item; + +import com.mojang.serialization.Codec; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.events.SkyblockEvents; +import de.hysky.skyblocker.mixins.accessors.SlotAccessor; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.MessageScheduler; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.InventoryScreen; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.inventory.Inventory; +import net.minecraft.inventory.SimpleInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.StringNbtReader; +import net.minecraft.nbt.visitor.StringNbtWriter; +import net.minecraft.screen.slot.Slot; +import net.minecraft.util.Identifier; +import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Opened here {@code de.hysky.skyblocker.mixins.MinecraftClientMixin#skyblocker$skyblockInventoryScreen} + * <br> + * Book button is moved here {@code de.hysky.skyblocker.mixins.InventoryScreenMixin#skyblocker} + */ +public class SkyblockInventoryScreen extends InventoryScreen { + private static final Logger LOGGER = LoggerFactory.getLogger("Equipment"); + private static final Supplier<ItemStack[]> EMPTY_EQUIPMENT = () -> new ItemStack[]{ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY}; + public static final ItemStack[] equipment = EMPTY_EQUIPMENT.get(); + public static final ItemStack[] equipment_rift = EMPTY_EQUIPMENT.get(); + private static final Codec<ItemStack[]> CODEC = ItemUtils.EMPTY_ALLOWING_ITEMSTACK_CODEC.listOf(4, 8) // min size at 4 for backwards compat + .xmap(itemStacks -> itemStacks.toArray(ItemStack[]::new), List::of).fieldOf("items").codec(); + + private static final Identifier SLOT_TEXTURE = Identifier.ofVanilla("container/slot"); + private static final Identifier EMPTY_SLOT = Identifier.of(SkyblockerMod.NAMESPACE, "equipment/empty_icon"); + private static final Path FOLDER = SkyblockerMod.CONFIG_DIR.resolve("equipment"); + + private final Slot[] equipmentSlots = new Slot[4]; + + private static void save(String profileId) { + try { + Files.createDirectories(FOLDER); + } catch (IOException e) { + LOGGER.error("[Skyblocker] Failed to create folder for equipment!", e); + } + Path resolve = FOLDER.resolve(profileId + ".nbt"); + + try (BufferedWriter writer = Files.newBufferedWriter(resolve)) { + + writer.write(new StringNbtWriter().apply(CODEC.encodeStart(NbtOps.INSTANCE, ArrayUtils.addAll(equipment, equipment_rift)).getOrThrow())); + } catch (Exception e) { + LOGGER.error("[Skyblocker] Failed to save Equipment data", e); + } + } + + private static void load(String profileId) { + Path resolve = FOLDER.resolve(profileId + ".nbt"); + CompletableFuture.supplyAsync(() -> { + try (BufferedReader reader = Files.newBufferedReader(resolve)) { + return CODEC.parse(NbtOps.INSTANCE, StringNbtReader.parse(reader.lines().collect(Collectors.joining()))).getOrThrow(); + } catch (NoSuchFileException ignored) { + } catch (Exception e) { + LOGGER.error("[Skyblocker] Failed to load Equipment data", e); + } + return EMPTY_EQUIPMENT.get(); + // Schedule on main thread to avoid any async weirdness + }).thenAccept(itemStacks -> MinecraftClient.getInstance().execute(() -> { + System.arraycopy(itemStacks, 0, equipment, 0, Math.min(itemStacks.length, 4)); + if (itemStacks.length <= 4) return; + System.arraycopy(itemStacks, 4, equipment_rift, 0, Math.clamp(itemStacks.length - 4, 0, 4)); + })); + } + + public static void initEquipment() { + + SkyblockEvents.PROFILE_CHANGE.register(((prevProfileId, profileId) -> { + if (!prevProfileId.isEmpty()) CompletableFuture.runAsync(() -> save(prevProfileId)).thenRun(() -> load(profileId)); + else load(profileId); + })); + + ClientLifecycleEvents.CLIENT_STOPPING.register(client1 -> { + String profileId = Utils.getProfileId(); + if (!profileId.isBlank()) { + CompletableFuture.runAsync(() -> save(profileId)); + } + }); + } + + public SkyblockInventoryScreen(PlayerEntity player) { + super(player); + SimpleInventory inventory = new SimpleInventory(Utils.isInTheRift() ? equipment_rift: equipment); + + Slot slot = handler.slots.get(45); + ((SlotAccessor) slot).setX(slot.x + 21); + for (int i = 0; i < 4; i++) { + equipmentSlots[i] = new EquipmentSlot(inventory, i, 77, 8 + i * 18); + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + for (Slot equipmentSlot : equipmentSlots) { + if (isPointWithinBounds(equipmentSlot.x, equipmentSlot.y, 16, 16, mouseX, mouseY)) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown("/equipment"); + return true; + } + } + return super.mouseClicked(mouseX, mouseY, button); + } + + /** + * Draws the equipment slots in the foreground layer after vanilla slots are drawn + * in {@link net.minecraft.client.gui.screen.ingame.HandledScreen#render(DrawContext, int, int, float) HandledScreen#render(DrawContext, int, int, float)}. + */ + @Override + protected void drawForeground(DrawContext context, int mouseX, int mouseY) { + for (Slot equipmentSlot : equipmentSlots) { + drawSlot(context, equipmentSlot); + if (isPointWithinBounds(equipmentSlot.x, equipmentSlot.y, 16, 16, mouseX, mouseY)) drawSlotHighlight(context, equipmentSlot.x, equipmentSlot.y, 0); + } + + super.drawForeground(context, mouseX, mouseY); + } + + @Override + protected void drawMouseoverTooltip(DrawContext context, int x, int y) { + super.drawMouseoverTooltip(context, x, y); + if (!handler.getCursorStack().isEmpty()) return; + for (Slot equipmentSlot : equipmentSlots) { + if (isPointWithinBounds(equipmentSlot.x, equipmentSlot.y, 16, 16, x, y) && equipmentSlot.hasStack()) { + ItemStack itemStack = equipmentSlot.getStack(); + context.drawTooltip(this.textRenderer, this.getTooltipFromItem(itemStack), itemStack.getTooltipData(), x, y); + } + } + } + + @Override + public void removed() { + super.removed(); + // put the handler back how it was, the handler is the same while the player is alive/in the same world + Slot slot = handler.slots.get(45); + ((SlotAccessor) slot).setX(slot.x - 21); + } + + @Override + protected void drawBackground(DrawContext context, float delta, int mouseX, int mouseY) { + super.drawBackground(context, delta, mouseX, mouseY); + for (int i = 0; i < 4; i++) { + context.drawGuiTexture(SLOT_TEXTURE, x + 76 + (i == 3 ? 21 : 0), y + 7 + i * 18, 18, 18); + } + } + + @Override + protected void drawSlot(DrawContext context, Slot slot) { + super.drawSlot(context, slot); + if (slot instanceof EquipmentSlot && !slot.hasStack()) { + context.drawGuiTexture(EMPTY_SLOT, slot.x, slot.y, 16, 16); + } + } + + private static class EquipmentSlot extends Slot { + + public EquipmentSlot(Inventory inventory, int index, int x, int y) { + super(inventory, index, x, y); + } + + @Override + public boolean canTakeItems(PlayerEntity playerEntity) { + return false; + } + + @Override + public boolean canInsert(ItemStack stack) { + return false; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotText.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotText.java index 66c02ca1..73224509 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotText.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotText.java @@ -1,7 +1,10 @@ package de.hysky.skyblocker.skyblock.item.slottext; +import it.unimi.dsi.fastutil.objects.ObjectLists; import net.minecraft.text.Text; +import java.util.List; + public record SlotText(Text text, TextPosition position) { public static SlotText bottomLeft(Text text) { return new SlotText(text, TextPosition.BOTTOM_LEFT); @@ -18,4 +21,20 @@ public record SlotText(Text text, TextPosition position) { public static SlotText topRight(Text text) { return new SlotText(text, TextPosition.TOP_RIGHT); } + + public static List<SlotText> topLeftList(Text text) { + return ObjectLists.singleton(topLeft(text)); + } + + public static List<SlotText> topRightList(Text text) { + return ObjectLists.singleton(topRight(text)); + } + + public static List<SlotText> bottomLeftList(Text text) { + return ObjectLists.singleton(bottomLeft(text)); + } + + public static List<SlotText> bottomRightList(Text text) { + return ObjectLists.singleton(bottomRight(text)); + } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java index d3941d77..aa9bf939 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java @@ -1,5 +1,6 @@ package de.hysky.skyblocker.skyblock.item.slottext; +import de.hysky.skyblocker.skyblock.bazaar.BazaarHelper; import de.hysky.skyblocker.skyblock.item.slottext.adders.*; import de.hysky.skyblocker.utils.Utils; import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; @@ -30,6 +31,7 @@ public class SlotTextManager { new CommunityShopAdder(), new YourEssenceAdder(), new PowerStonesGuideAdder(), + new BazaarHelper(), new StatsTuningAdder() }; private static final ArrayList<SlotTextAdder> currentScreenAdders = new ArrayList<>(); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java index c7ea17dc..d94d6405 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java @@ -48,7 +48,7 @@ public class CommunityShopAdder extends SlotTextAdder { String lastLine = lore.getLast().getString(); return List.of(SlotText.bottomLeft(switch (lastLine) { case "Maxed out!" -> Text.literal("Max").withColor(0xfab387); - case "Currently upgrading!" -> Text.literal("⏰").withColor(0xf9e2af).formatted(Formatting.BOLD); + case "Currently upgrading!", "Click to instantly upgrade!" -> Text.literal("⏰").withColor(0xf9e2af).formatted(Formatting.BOLD); case "Click to claim!" -> Text.literal("✅").withColor(0xa6e3a1).formatted(Formatting.BOLD); default -> Text.literal(String.valueOf(RomanNumerals.romanToDecimal(roman))).withColor(0xcba6f7); })); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PetLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PetLevelAdder.java index 88d48fbf..3049cd3f 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PetLevelAdder.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PetLevelAdder.java @@ -2,11 +2,11 @@ package de.hysky.skyblocker.skyblock.item.slottext.adders; import de.hysky.skyblocker.skyblock.item.slottext.SlotText; import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder; +import de.hysky.skyblocker.utils.ItemUtils; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; import net.minecraft.screen.slot.Slot; import net.minecraft.text.Text; -import net.minecraft.util.Formatting; import org.apache.commons.lang3.math.NumberUtils; import org.jetbrains.annotations.NotNull; @@ -22,8 +22,8 @@ public class PetLevelAdder extends SlotTextAdder { ItemStack itemStack = slot.getStack(); if (!itemStack.isOf(Items.PLAYER_HEAD)) return List.of(); String level = CatacombsLevelAdder.getBracketedLevelFromName(itemStack); - if (!NumberUtils.isDigits(level)) return List.of(); - if ("100".equals(level) || "200".equals(level)) return List.of(); + if (!NumberUtils.isDigits(level) || "100".equals(level) || "200".equals(level)) return List.of(); + if (!ItemUtils.getItemId(itemStack).equals("PET")) return List.of(); return List.of(SlotText.topLeft(Text.literal(level).withColor(0xFFDDC1))); } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java index 49d170b9..955ebc87 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java @@ -2,6 +2,7 @@ package de.hysky.skyblocker.skyblock.item.tooltip; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.config.configs.GeneralConfig; +import de.hysky.skyblocker.skyblock.item.tooltip.adders.CraftPriceTooltip; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.scheduler.Scheduler; @@ -129,6 +130,8 @@ public class ItemTooltip { LOGGER.error("Encountered unknown error while downloading tooltip data", e); return null; }); + + CraftPriceTooltip.clearCache(); }, 1200, true); } }
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipAdder.java index 9bd63adc..f3395def 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipAdder.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipAdder.java @@ -4,6 +4,7 @@ import de.hysky.skyblocker.utils.render.gui.AbstractContainerMatcher; import net.minecraft.item.ItemStack; import net.minecraft.screen.slot.Slot; import net.minecraft.text.Text; +import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.Nullable; import java.util.List; @@ -19,7 +20,7 @@ public abstract class TooltipAdder extends AbstractContainerMatcher { */ public final int priority; - protected TooltipAdder(String titlePattern, int priority) { + protected TooltipAdder(@Language("RegExp") String titlePattern, int priority) { super(titlePattern); this.priority = priority; } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java index d82b2682..92adf49d 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java @@ -1,29 +1,23 @@ package de.hysky.skyblocker.skyblock.item.tooltip; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.google.gson.stream.JsonReader; import de.hysky.skyblocker.SkyblockerMod; -import de.hysky.skyblocker.config.SkyblockerConfig; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.config.configs.GeneralConfig; import de.hysky.skyblocker.utils.Http; import de.hysky.skyblocker.utils.Utils; +import org.jetbrains.annotations.Nullable; -import java.io.StringReader; import java.net.http.HttpHeaders; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.function.Predicate; -import org.jetbrains.annotations.Nullable; - public enum TooltipInfoType implements Runnable { NPC("https://hysky.de/api/npcprice", itemTooltip -> itemTooltip.enableNPCPrice, true), - BAZAAR("https://hysky.de/api/bazaar", itemTooltip -> itemTooltip.enableBazaarPrice || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.enableProfitCalculator || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.croesusProfit || SkyblockerConfigManager.get().uiAndVisuals.chestValue.enableChestValue, itemTooltip -> itemTooltip.enableBazaarPrice, false), - LOWEST_BINS("https://hysky.de/api/auctions/lowestbins", itemTooltip -> itemTooltip.enableLowestBIN || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.enableProfitCalculator || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.croesusProfit || SkyblockerConfigManager.get().uiAndVisuals.chestValue.enableChestValue, itemTooltip -> itemTooltip.enableLowestBIN, false), + BAZAAR("https://hysky.de/api/bazaar", itemTooltip -> itemTooltip.enableBazaarPrice || itemTooltip.enableCraftingCost.getOrder() != null || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.enableProfitCalculator || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.croesusProfit || SkyblockerConfigManager.get().uiAndVisuals.chestValue.enableChestValue, itemTooltip -> itemTooltip.enableBazaarPrice, false), + LOWEST_BINS("https://hysky.de/api/auctions/lowestbins", itemTooltip -> itemTooltip.enableLowestBIN || itemTooltip.enableCraftingCost.getOrder() != null || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.enableProfitCalculator || SkyblockerConfigManager.get().dungeons.dungeonChestProfit.croesusProfit || SkyblockerConfigManager.get().uiAndVisuals.chestValue.enableChestValue, itemTooltip -> itemTooltip.enableLowestBIN, false), ONE_DAY_AVERAGE("https://hysky.de/api/auctions/lowestbins/average/1day.json", itemTooltip -> itemTooltip.enableAvgBIN, false), THREE_DAY_AVERAGE("https://hysky.de/api/auctions/lowestbins/average/3day.json", itemTooltip -> itemTooltip.enableAvgBIN || SkyblockerConfigManager.get().uiAndVisuals.searchOverlay.enableAuctionHouse, itemTooltip -> itemTooltip.enableAvgBIN, false), MOTES("https://hysky.de/api/motesprice", itemTooltip -> itemTooltip.enableMotesPrice, itemTooltip -> itemTooltip.enableMotesPrice && Utils.isInTheRift(), true), diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java index e3a2ef04..bd06acba 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java @@ -1,8 +1,10 @@ package de.hysky.skyblocker.skyblock.item.tooltip; import de.hysky.skyblocker.mixins.accessors.HandledScreenAccessor; +import de.hysky.skyblocker.skyblock.bazaar.ReorderHelper; import de.hysky.skyblocker.skyblock.chocolatefactory.ChocolateFactorySolver; import de.hysky.skyblocker.skyblock.item.tooltip.adders.*; +import de.hysky.skyblocker.skyblock.item.tooltip.adders.CraftPriceTooltip; import de.hysky.skyblocker.utils.Utils; import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; @@ -23,16 +25,18 @@ public class TooltipManager { new LineSmoothener(), // Applies before anything else new SupercraftReminder(), new ChocolateFactorySolver.Tooltip(), + new ReorderHelper.Tooltip(), new NpcPriceTooltip(1), new BazaarPriceTooltip(2), new LBinTooltip(3), new AvgBinTooltip(4), - new DungeonQualityTooltip(5), - new MotesTooltip(6), - new ObtainedDateTooltip(7), - new MuseumTooltip(8), - new ColorTooltip(9), - new AccessoryTooltip(10), + new CraftPriceTooltip(5), + new DungeonQualityTooltip(6), + new MotesTooltip(7), + new ObtainedDateTooltip(8), + new MuseumTooltip(9), + new ColorTooltip(10), + new AccessoryTooltip(11), }; private static final ArrayList<TooltipAdder> currentScreenAdders = new ArrayList<>(); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/CraftPriceTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/CraftPriceTooltip.java new file mode 100644 index 00000000..f7af446e --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/CraftPriceTooltip.java @@ -0,0 +1,115 @@ +package de.hysky.skyblocker.skyblock.item.tooltip.adders; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.config.configs.GeneralConfig; +import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder; +import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType; +import de.hysky.skyblocker.utils.NEURepoManager; +import io.github.moulberry.repo.data.NEUIngredient; +import io.github.moulberry.repo.data.NEUItem; +import io.github.moulberry.repo.data.NEURecipe; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.apache.commons.lang3.math.NumberUtils; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class CraftPriceTooltip extends TooltipAdder { + protected static final Logger LOGGER = LoggerFactory.getLogger(CraftPriceTooltip.class.getName()); + private static final Map<String, Double> cachedCraftCosts = new ConcurrentHashMap<>(); + private static final int MAX_RECURSION_DEPTH = 15; + + public CraftPriceTooltip(int priority) { + super(priority); + } + + @Override + public void addToTooltip(@Nullable Slot focusedSloFt, ItemStack stack, List<Text> lines) { + if (SkyblockerConfigManager.get().general.itemTooltip.enableCraftingCost == GeneralConfig.Craft.OFF) return; + + String internalID = stack.getSkyblockId(); + if (stack.getNeuName() == null || internalID == null) return; + + if (TooltipInfoType.LOWEST_BINS.getData() == null || TooltipInfoType.BAZAAR.getData() == null) { + ItemTooltip.nullWarning(); + return; + } + + NEUItem neuItem = NEURepoManager.NEU_REPO.getItems().getItemBySkyblockId(internalID); + if (neuItem == null) return; + + List<NEURecipe> neuRecipes = neuItem.getRecipes(); + if (neuRecipes.isEmpty() || neuRecipes.getFirst() instanceof io.github.moulberry.repo.data.NEUKatUpgradeRecipe) return; + + try { + double totalCraftCost = getItemCost(neuRecipes.getFirst(), 0); + + if (totalCraftCost == 0) return; + + int amountInStack; + if (lines.get(1).getString().endsWith("Sack")) { + String line = lines.get(3).getSiblings().get(1).getString().replace(",", ""); + amountInStack = NumberUtils.isParsable(line) && !line.equals("0") ? Integer.parseInt(line) : stack.getCount(); + } else amountInStack = stack.getCount(); + + neuRecipes.getFirst().getAllOutputs().stream().findFirst().ifPresent(outputIngredient -> + lines.add(Text.literal(String.format("%-20s", "Crafting Price:")).formatted(Formatting.GOLD) + .append(ItemTooltip.getCoinsMessage(totalCraftCost / outputIngredient.getAmount(), amountInStack)))); + + } catch (Exception e) { + LOGGER.error("[Skyblocker Craft Price] Error calculating craftprice tooltip for: " + internalID, e); + } + } + + private double getItemCost(NEURecipe recipe, int depth) { + if (depth >= MAX_RECURSION_DEPTH) return -1; + + double totalCraftCost = 0; + for (NEUIngredient input : recipe.getAllInputs()) { + String inputItemName = input.getItemId(); + double inputItemCount = input.getAmount(); + if (cachedCraftCosts.containsKey(inputItemName)) { + totalCraftCost += cachedCraftCosts.get(inputItemName) * inputItemCount; + continue; + } + + double itemCost = 0; + + if (TooltipInfoType.BAZAAR.getData().has(inputItemName)) { + itemCost = TooltipInfoType.BAZAAR.getData().getAsJsonObject(inputItemName).get(SkyblockerConfigManager.get().general.itemTooltip.enableCraftingCost.getOrder()).getAsDouble(); + } else if (TooltipInfoType.LOWEST_BINS.getData().has(inputItemName)) { + itemCost = TooltipInfoType.LOWEST_BINS.getData().get(inputItemName).getAsDouble(); + } + + if (itemCost > 0) { + cachedCraftCosts.put(inputItemName, itemCost); + } + + NEUItem neuItem = NEURepoManager.NEU_REPO.getItems().getItemBySkyblockId(inputItemName); + if (neuItem != null) { + List<NEURecipe> neuRecipes = neuItem.getRecipes(); + if (!neuRecipes.isEmpty()) { + double craftCost = getItemCost(neuRecipes.getFirst(), depth + 1); + if (craftCost != -1) itemCost = Math.min(itemCost, craftCost); + cachedCraftCosts.put(inputItemName, itemCost); + } + } + + totalCraftCost += itemCost * inputItemCount; + } + return totalCraftCost; + } + + public static void clearCache() { + cachedCraftCosts.clear(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java index 672201d5..d556c9b2 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java @@ -7,6 +7,7 @@ import net.minecraft.item.ItemStack; import net.minecraft.screen.slot.Slot; import net.minecraft.text.Text; import net.minecraft.util.Formatting; +import org.apache.commons.lang3.math.NumberUtils; import org.jetbrains.annotations.Nullable; import java.util.List; @@ -20,9 +21,18 @@ public class NpcPriceTooltip extends TooltipAdder { public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) { final String internalID = stack.getSkyblockId(); if (internalID != null && TooltipInfoType.NPC.isTooltipEnabledAndHasOrNullWarning(internalID)) { + int amount; + if (lines.get(1).getString().endsWith("Sack")) { + //The amount is in the 2nd sibling of the 3rd line of the lore. here V + //Example line: empty[style={color=dark_purple,!italic}, siblings=[literal{Stored: }[style={color=gray}], literal{0}[style={color=dark_gray}], literal{/20k}[style={color=gray}]] + String line = lines.get(3).getSiblings().get(1).getString().replace(",", ""); + amount = NumberUtils.isParsable(line) && !line.equals("0") ? Integer.parseInt(line) : stack.getCount(); + } else { + amount = stack.getCount(); + } lines.add(Text.literal(String.format("%-21s", "NPC Sell Price:")) .formatted(Formatting.YELLOW) - .append(ItemTooltip.getCoinsMessage(TooltipInfoType.NPC.getData().get(internalID).getAsDouble(), stack.getCount()))); + .append(ItemTooltip.getCoinsMessage(TooltipInfoType.NPC.getData().get(internalID).getAsDouble(), amount))); } } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java index 7eda7646..ab61b684 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java @@ -2,6 +2,7 @@ package de.hysky.skyblocker.skyblock.itemlist; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.NEURepoManager; +import de.hysky.skyblocker.utils.TextTransformer; import io.github.moulberry.repo.constants.PetNumbers; import io.github.moulberry.repo.data.NEUItem; import io.github.moulberry.repo.data.Rarity; @@ -21,8 +22,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; public class ItemStackBuilder { - private static final Pattern SKULL_UUID_PATTERN = Pattern.compile("(?<=SkullOwner:\\{)Id:\"(.{36})\""); - private static final Pattern SKULL_TEXTURE_PATTERN = Pattern.compile("(?<=Properties:\\{textures:\\[0:\\{Value:)\"(.+?)\""); + public static final Pattern SKULL_UUID_PATTERN = Pattern.compile("(?<=SkullOwner:\\{)Id:\"(.{36})\""); + public static final Pattern SKULL_TEXTURE_PATTERN = Pattern.compile("(?<=Properties:\\{textures:\\[0:\\{Value:)\"(.+?)\""); private static final Pattern COLOR_PATTERN = Pattern.compile("color:(\\d+)"); private static final Pattern EXPLOSION_COLOR_PATTERN = Pattern.compile("\\{Explosion:\\{(?:Type:[0-9a-z]+,)?Colors:\\[(?<color>[0-9]+)]\\}"); private static Map<String, Map<Rarity, PetNumbers>> petNums; @@ -53,10 +54,10 @@ public class ItemStackBuilder { // Item Name String name = injectData(item.getDisplayName(), injectors); - stack.set(DataComponentTypes.CUSTOM_NAME, Text.of(name)); + stack.set(DataComponentTypes.CUSTOM_NAME, TextTransformer.fromLegacy(name)); // Lore - stack.set(DataComponentTypes.LORE, new LoreComponent(item.getLore().stream().map(line -> Text.of(injectData(line, injectors))).toList())); + stack.set(DataComponentTypes.LORE, new LoreComponent(item.getLore().stream().map(line -> TextTransformer.fromLegacy(injectData(line, injectors))).map(Text.class::cast).toList())); String nbttag = item.getNbttag(); // add skull texture @@ -138,7 +139,7 @@ public class ItemStackBuilder { return list; } - private static String injectData(String string, List<Pair<String, String>> injectors) { + public static String injectData(String string, List<Pair<String, String>> injectors) { for (Pair<String, String> injector : injectors) { string = string.replaceAll(injector.getLeft(), injector.getRight()); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java index dfb53628..bb236526 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java @@ -73,6 +73,8 @@ public class SearchResultsWidget implements Drawable, Element { } protected void updateSearchResult(String searchText) { + searchText = searchText.toLowerCase(Locale.ENGLISH); + if (!searchText.equals(this.searchText)) { this.searchText = searchText; this.searchResults.clear(); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java b/src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java new file mode 100644 index 00000000..7131a567 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java @@ -0,0 +1,37 @@ +package de.hysky.skyblocker.skyblock.mayors; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvents; +import net.minecraft.text.HoverEvent; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +public final class JerryTimer { + private JerryTimer() { + } + public static void init() { + //Example message: "§b ☺ §eThere is a §aGreen Jerry§e!" + //There are various formats, all of which start with the "§b ☺ " prefix and contain the word "<color> Jerry" + ClientReceiveMessageEvents.GAME.register((message, overlay) -> { + if (overlay || !Utils.getMayor().equals("Jerry") || !SkyblockerConfigManager.get().helpers.jerry.enableJerryTimer) return; + String text = message.getString(); + //This part of hypixel still uses legacy text formatting, so we can't directly check for the actual text + if (!text.startsWith("§b ☺ ") || !text.contains("Jerry")) return; + HoverEvent hoverEvent = message.getStyle().getHoverEvent(); + if (hoverEvent == null || hoverEvent.getAction() != HoverEvent.Action.SHOW_TEXT) return; + ClientPlayerEntity player = MinecraftClient.getInstance().player; + Scheduler.INSTANCE.schedule(() -> { + if (player == null || !Utils.isOnSkyblock()) return; + player.sendMessage(Constants.PREFIX.get().append(Text.literal("Jerry cooldown is over!")).formatted(Formatting.GREEN), false); + player.playSoundToPlayer(SoundEvents.ENTITY_VILLAGER_TRADE, SoundCategory.NEUTRAL, 100f, 1.0f); + }, 20*60*6); // 6 minutes + }); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerNavButton.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerNavButton.java new file mode 100644 index 00000000..16f7eb28 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerNavButton.java @@ -0,0 +1,65 @@ +package de.hysky.skyblocker.skyblock.profileviewer; + +import com.mojang.blaze3d.systems.RenderSystem; +import de.hysky.skyblocker.skyblock.profileviewer.utils.ProfileViewerUtils; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.item.ItemStack; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.util.Map; + +public class ProfileViewerNavButton extends ClickableWidget { + private final static Identifier BUTTON_TEXTURES_TOGGLED = Identifier.of("container/creative_inventory/tab_top_selected_2"); + private final static Identifier BUTTON_TEXTURES = Identifier.of("container/creative_inventory/tab_top_unselected_2"); + private boolean toggled; + private final int index; + private final ProfileViewerScreen screen; + private final ItemStack icon; + + private static final Map<String, ItemStack> HEAD_ICON = Map.ofEntries( + Map.entry("Skills", Ico.IRON_SWORD), + Map.entry("Slayers", ProfileViewerUtils.createSkull("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHBzOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzkzMzZkN2NjOTVjYmY2Njg5ZjVlOGM5NTQyOTRlYzhkMWVmYzQ5NGE0MDMxMzI1YmI0MjdiYzgxZDU2YTQ4NGQifX19")), + Map.entry("Pets", Ico.BONE), + Map.entry("Dungeons", ProfileViewerUtils.createSkull("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHBzOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzliNTY4OTViOTY1OTg5NmFkNjQ3ZjU4NTk5MjM4YWY1MzJkNDZkYjljMWIwMzg5YjhiYmViNzA5OTlkYWIzM2QifX19")), + Map.entry("Inventories", Ico.E_CHEST), + Map.entry("Collections", Ico.PAINTING) + ); + + public ProfileViewerNavButton(ProfileViewerScreen screen, String tabName, int index, boolean toggled) { + super(-100, -100, 28, 32, Text.empty()); + this.screen = screen; + this.toggled = toggled; + this.index = index; + this.icon = HEAD_ICON.getOrDefault(tabName, Ico.BARRIER); + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + RenderSystem.disableDepthTest(); + + context.drawGuiTexture(toggled ? BUTTON_TEXTURES_TOGGLED : BUTTON_TEXTURES, this.getX(), this.getY(), this.width, this.height - ((this.toggled) ? 0 : 4)); + context.drawItem(this.icon, this.getX() + 6, this.getY() + (this.toggled ? 7 : 9)); + + RenderSystem.enableDepthTest(); + } + + @Override + public void onClick(double mouseX, double mouseY) { + screen.onNavButtonClick(this); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} + + public void setToggled(boolean toggled) { + this.toggled = toggled; + } + + public int getIndex() { + return index; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerPage.java new file mode 100644 index 00000000..f5a5ec40 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerPage.java @@ -0,0 +1,19 @@ +package de.hysky.skyblocker.skyblock.profileviewer; + +import de.hysky.skyblocker.skyblock.profileviewer.utils.SubPageSelectButton; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.ClickableWidget; + +import java.util.List; + +public interface ProfileViewerPage { + void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY); + default List<ClickableWidget> getButtons() { + return null; + } + default void onNavButtonClick(SubPageSelectButton selectButton) {} + default void markWidgetsAsVisible() {} + default void markWidgetsAsInvisible() {} + default void nextPage() {} + default void previousPage() {} +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java new file mode 100644 index 00000000..f74526a4 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java @@ -0,0 +1,251 @@ +package de.hysky.skyblocker.skyblock.profileviewer; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.mixins.accessors.SkullBlockEntityAccessor; +import de.hysky.skyblocker.skyblock.profileviewer.collections.CollectionsPage; +import de.hysky.skyblocker.skyblock.profileviewer.dungeons.DungeonsPage; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.InventoryPage; +import de.hysky.skyblocker.skyblock.profileviewer.skills.SkillsPage; +import de.hysky.skyblocker.skyblock.profileviewer.slayers.SlayersPage; +import de.hysky.skyblocker.utils.ApiUtils; +import de.hysky.skyblocker.utils.Http; +import de.hysky.skyblocker.utils.ProfileUtils; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.network.OtherClientPlayerEntity; +import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.client.util.SkinTextures; +import net.minecraft.command.CommandSource; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerModelPart; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.*; +import java.io.IOException; +import java.util.List; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import static net.minecraft.client.gui.screen.ingame.InventoryScreen.drawEntity; + +public class ProfileViewerScreen extends Screen { + public static final Logger LOGGER = LoggerFactory.getLogger(ProfileViewerScreen.class); + private static final Text TITLE = Text.of("Skyblocker Profile Viewer"); + private static final String HYPIXEL_COLLECTIONS = "https://api.hypixel.net/v2/resources/skyblock/collections"; + private static final Object2ObjectOpenHashMap<String, Map<String, ?>> COLLECTIONS_CACHE = new Object2ObjectOpenHashMap<>(); + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/base_plate.png"); + private static final int GUI_WIDTH = 322; + private static final int GUI_HEIGHT = 180; + + private String playerName; + private JsonObject hypixelProfile; + private JsonObject playerProfile; + private boolean profileNotFound = false; + + private int activePage = 0; + private static final String[] PAGE_NAMES = {"Skills", "Slayers", "Dungeons", "Inventories", "Collections"}; + private final ProfileViewerPage[] profileViewerPages = new ProfileViewerPage[PAGE_NAMES.length]; + private final List<ProfileViewerNavButton> profileViewerNavButtons = new ArrayList<>(); + private OtherClientPlayerEntity entity; + private ProfileViewerTextWidget textWidget; + + public ProfileViewerScreen(String username) { + super(TITLE); + fetchPlayerData(username).thenRun(this::initialisePagesAndWidgets); + + for (int i = 0; i < PAGE_NAMES.length; i++) { + profileViewerNavButtons.add(new ProfileViewerNavButton(this, PAGE_NAMES[i], i, i == 0)); + } + } + + private void initialisePagesAndWidgets() { + if (profileNotFound) return; + + textWidget = new ProfileViewerTextWidget(hypixelProfile, playerProfile); + + CompletableFuture<Void> skillsFuture = CompletableFuture.runAsync(() -> profileViewerPages[0] = new SkillsPage(hypixelProfile, playerProfile)); + CompletableFuture<Void> slayersFuture = CompletableFuture.runAsync(() -> profileViewerPages[1] = new SlayersPage(playerProfile)); + CompletableFuture<Void> dungeonsFuture = CompletableFuture.runAsync(() -> profileViewerPages[2] = new DungeonsPage(playerProfile)); + CompletableFuture<Void> inventoriesFuture = CompletableFuture.runAsync(() -> profileViewerPages[3] = new InventoryPage(playerProfile)); + CompletableFuture<Void> collectionsFuture = CompletableFuture.runAsync(() -> profileViewerPages[4] = new CollectionsPage(hypixelProfile, playerProfile)); + + CompletableFuture.allOf(skillsFuture, slayersFuture, dungeonsFuture, inventoriesFuture, collectionsFuture) + .thenRun(() -> { + synchronized (this) { + clearAndInit(); + } + }); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + synchronized (this) { + super.render(context, mouseX, mouseY, delta); + } + + int rootX = width / 2 - GUI_WIDTH / 2; + int rootY = height / 2 - GUI_HEIGHT / 2 + 5; + + context.drawTexture(TEXTURE, rootX, rootY, 0, 0, GUI_WIDTH, GUI_HEIGHT, GUI_WIDTH, GUI_HEIGHT); + for (ProfileViewerNavButton button : profileViewerNavButtons) { + button.setX(rootX + button.getIndex() * 28 + 4); + button.setY(rootY - 28); + button.render(context, mouseX, mouseY, delta); + } + + + if (textWidget != null) textWidget.render(context, textRenderer, rootX + 8, rootY + 120); + drawPlayerEntity(context, playerName != null ? playerName : "Loading...", rootX, rootY, mouseX, mouseY); + + if (profileViewerPages[activePage] != null) { + profileViewerPages[activePage].markWidgetsAsVisible(); + profileViewerPages[activePage].render(context, mouseX, mouseY, delta, rootX + 93, rootY + 7); + } else { + context.drawText(textRenderer, profileNotFound ? "No Profile" : "Loading...", rootX + 180, rootY + 80, Color.WHITE.getRGB(), true); + } + } + + private void drawPlayerEntity(DrawContext context, String username, int rootX, int rootY, int mouseX, int mouseY) { + if (entity != null) + drawEntity(context, rootX + 9, rootY + 16, rootX + 89, rootY + 124, 42, 0.0625F, mouseX, mouseY, entity); + context.drawCenteredTextWithShadow(textRenderer, username.length() > 15 ? username.substring(0, 15) : username, rootX + 47, rootY + 14, Color.WHITE.getRGB()); + } + + private CompletableFuture<Void> fetchPlayerData(String username) { + CompletableFuture<Void> profileFuture = ProfileUtils.fetchFullProfile(username).thenAccept(profiles -> { + try { + Optional<JsonObject> selectedProfile = profiles.getAsJsonArray("profiles").asList().stream() + .map(JsonElement::getAsJsonObject) + .filter(profile -> profile.getAsJsonPrimitive("selected").getAsBoolean()) + .findFirst(); + + if (selectedProfile.isPresent()) { + this.hypixelProfile = selectedProfile.get(); + this.playerProfile = hypixelProfile.getAsJsonObject("members").get(ApiUtils.name2Uuid(username)).getAsJsonObject(); + } + } catch (Exception e) { + this.profileNotFound = true; + LOGGER.warn("[Skyblocker Profile Viewer] Error while looking for profile", e); + } + }); + + CompletableFuture<Void> minecraftProfileFuture = SkullBlockEntityAccessor.invokeFetchProfileByName(username).thenAccept(profile -> { + this.playerName = profile.get().getName(); + entity = new OtherClientPlayerEntity(MinecraftClient.getInstance().world, profile.get()) { + @Override + public SkinTextures getSkinTextures() { + PlayerListEntry playerListEntry = new PlayerListEntry(profile.get(), false); + return playerListEntry.getSkinTextures(); + } + + @Override + public boolean isPartVisible(PlayerModelPart modelPart) { + return !(modelPart.getName().equals(PlayerModelPart.CAPE.getName())); + } + + @Override + public boolean isInvisibleTo(PlayerEntity player) { + return true; + } + }; + entity.setCustomNameVisible(false); + }).exceptionally(ex -> { + this.playerName = "User not found"; + this.profileNotFound = true; + return null; + }); + + return CompletableFuture.allOf(profileFuture, minecraftProfileFuture); + } + + + public void onNavButtonClick(ProfileViewerNavButton clickedButton) { + if (profileViewerPages[activePage] != null) profileViewerPages[activePage].markWidgetsAsInvisible(); + for (ProfileViewerNavButton button : profileViewerNavButtons) { + button.setToggled(false); + } + activePage = clickedButton.getIndex(); + clickedButton.setToggled(true); + } + + @Override + public void init() { + profileViewerNavButtons.forEach(this::addDrawableChild); + for (ProfileViewerPage profileViewerPage : profileViewerPages) { + if (profileViewerPage != null && profileViewerPage.getButtons() != null) { + for (ClickableWidget button : profileViewerPage.getButtons()) { + if (button != null) addDrawableChild(button); + } + } + } + } + + public static void initClass() { + fetchCollectionsData(); // caching on launch + + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { + LiteralArgumentBuilder<FabricClientCommandSource> literalArgumentBuilder = ClientCommandManager.literal("pv") + .then(ClientCommandManager.argument("username", StringArgumentType.string()) + .suggests((source, builder) -> CommandSource.suggestMatching(getPlayerSuggestions(source.getSource()), builder)) + .executes(Scheduler.queueOpenScreenFactoryCommand(context -> new ProfileViewerScreen(StringArgumentType.getString(context, "username")))) + ) + .executes(Scheduler.queueOpenScreenCommand(() -> new ProfileViewerScreen(MinecraftClient.getInstance().getSession().getUsername()))); + dispatcher.register(literalArgumentBuilder); + dispatcher.register(ClientCommandManager.literal(SkyblockerMod.NAMESPACE).then(literalArgumentBuilder)); + }); + } + + @NotNull + public static Map<String, Map<String, ?>> fetchCollectionsData() { + if (!COLLECTIONS_CACHE.isEmpty()) return COLLECTIONS_CACHE; + try { + JsonObject jsonObject = JsonParser.parseString(Http.sendGetRequest(HYPIXEL_COLLECTIONS)).getAsJsonObject(); + if (jsonObject.get("success").getAsBoolean()) { + Map<String, String[]> collectionsMap = new HashMap<>(); + Map<String, List<Integer>> tierRequirementsMap = new HashMap<>(); + JsonObject collections = jsonObject.getAsJsonObject("collections"); + collections.entrySet().forEach(entry -> { + String category = entry.getKey(); + JsonObject itemsObject = entry.getValue().getAsJsonObject().getAsJsonObject("items"); + String[] items = itemsObject.keySet().toArray(new String[0]); + collectionsMap.put(category, items); + itemsObject.entrySet().forEach(itemEntry -> { + List<Integer> tierReqs = new ArrayList<>(); + itemEntry.getValue().getAsJsonObject().getAsJsonArray("tiers").forEach(req -> + tierReqs.add(req.getAsJsonObject().get("amountRequired").getAsInt())); + tierRequirementsMap.put(itemEntry.getKey(), tierReqs); + }); + }); + COLLECTIONS_CACHE.put("COLLECTIONS", collectionsMap); + COLLECTIONS_CACHE.put("TIER_REQS", tierRequirementsMap); + return COLLECTIONS_CACHE; + } + } catch (IOException | InterruptedException e) { + LOGGER.error("[Skyblocker Profile Viewer] Failed to fetch collections data", e); + } + return Collections.emptyMap(); + } + + /** + * Ensures that "dummy" players aren't included in command suggestions + */ + private static String[] getPlayerSuggestions(FabricClientCommandSource source) { + return source.getPlayerNames().stream().filter(playerName -> playerName.matches("[A-Za-z0-9_]+")).toArray(String[]::new); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerTextWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerTextWidget.java new file mode 100644 index 00000000..58c238f8 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerTextWidget.java @@ -0,0 +1,44 @@ +package de.hysky.skyblocker.skyblock.profileviewer; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.utils.ProfileViewerUtils; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Colors; + +public class ProfileViewerTextWidget { + private static final int ROW_GAP = 9; + + private String PROFILE_NAME = "UNKNOWN"; + private int SKYBLOCK_LEVEL = 0; + private double PURSE = 0; + private double BANK = 0; + + public ProfileViewerTextWidget(JsonObject hypixelProfile, JsonObject playerProfile){ + try { + this.PROFILE_NAME = hypixelProfile.get("cute_name").getAsString(); + this.SKYBLOCK_LEVEL = playerProfile.getAsJsonObject("leveling").get("experience").getAsInt() / 100; + this.PURSE = playerProfile.getAsJsonObject("currencies").get("coin_purse").getAsDouble(); + this.BANK = hypixelProfile.getAsJsonObject("banking").get("balance").getAsDouble(); + } catch (Exception ignored) {} + } + + public void render(DrawContext context, TextRenderer textRenderer, int root_x, int root_y){ + // Profile Icon + MatrixStack matrices = context.getMatrices(); + matrices.push(); + matrices.scale(0.75f, 0.75f, 1); + int rootAdjustedX = (int) ((root_x) / 0.75f); + int rootAdjustedY = (int) ((root_y) / 0.75f); + context.drawItem(Ico.PAINTING, rootAdjustedX, rootAdjustedY); + matrices.pop(); + + context.drawText(textRenderer, "§n"+PROFILE_NAME, root_x + 14, root_y + 3, Colors.WHITE, true); + context.drawText(textRenderer, "§aLevel:§r " + SKYBLOCK_LEVEL, root_x + 2, root_y + 6 + ROW_GAP, Colors.WHITE, true); + context.drawText(textRenderer, "§6Purse:§r " + ProfileViewerUtils.numLetterFormat(PURSE), root_x + 2, root_y + 6 + ROW_GAP * 2, Colors.WHITE, true); + context.drawText(textRenderer, "§6Bank:§r " + ProfileViewerUtils.numLetterFormat(BANK), root_x + 2, root_y + 6 + ROW_GAP * 3, Colors.WHITE, true); + context.drawText(textRenderer, "§6NW:§r " + "Soon™", root_x + 2, root_y + 6 + ROW_GAP * 4, Colors.WHITE, true ); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/CollectionsPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/CollectionsPage.java new file mode 100644 index 00000000..b77c3e7a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/CollectionsPage.java @@ -0,0 +1,100 @@ +package de.hysky.skyblocker.skyblock.profileviewer.collections; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import de.hysky.skyblocker.skyblock.profileviewer.utils.SubPageSelectButton; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.item.ItemStack; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class CollectionsPage implements ProfileViewerPage { + private static final String[] COLLECTION_CATEGORIES = {"MINING", "FARMING", "COMBAT", "FISHING", "FORAGING", "RIFT"}; + private static final int TOTAL_HEIGHT = 165; + private static final Map<String, ItemStack> ICON_MAP = Map.ofEntries( + Map.entry("MINING", Ico.STONE_PICKAXE), + Map.entry("FARMING", Ico.GOLDEN_HOE), + Map.entry("COMBAT", Ico.STONE_SWORD), + Map.entry("FISHING", Ico.FISH_ROD), + Map.entry("FORAGING", Ico.JUNGLE_SAPLING), + // Map.entry("BOSS", Ico.WITHER), Not currently part of Collections API so skipping for now + Map.entry("RIFT", Ico.MYCELIUM) + ); + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + + private final GenericCategory[] collections = new GenericCategory[COLLECTION_CATEGORIES.length]; + private final List<SubPageSelectButton> collectionSelectButtons = new ArrayList<>(); + private int activePage = 0; + + + public CollectionsPage(JsonObject hProfile, JsonObject pProfile) { + for (int i = 0; i < COLLECTION_CATEGORIES.length; i++) { + try { + collectionSelectButtons.add(new SubPageSelectButton(this, -100, 0, i, ICON_MAP.getOrDefault(COLLECTION_CATEGORIES[i], Ico.BARRIER))); + collections[i] = new GenericCategory(hProfile, pProfile, COLLECTION_CATEGORIES[i]); + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Error creating Collections Page", e); + } + } + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + int startingY = rootY + (TOTAL_HEIGHT - collectionSelectButtons.size() * 21) / 2; + for (int i = 0; i < collectionSelectButtons.size(); i++) { + collectionSelectButtons.get(i).setX(rootX); + collectionSelectButtons.get(i).setY(startingY + i * 21); + collectionSelectButtons.get(i).render(context, mouseX, mouseY, delta); + } + + if (collections[activePage] == null) { + context.drawText(textRenderer, "No data...", rootX + 92, rootY + 72, Color.DARK_GRAY.getRGB(), false); + return; + } + + collections[activePage].markWidgetsAsVisible(); + collections[activePage].render(context, mouseX, mouseY, delta, rootX + 35, rootY + 6); + } + + public void onNavButtonClick(SubPageSelectButton selectButton) { + if (collections[activePage] != null) collections[activePage].markWidgetsAsInvisible(); + for (SubPageSelectButton button : collectionSelectButtons) { + button.setToggled(false); + } + activePage = selectButton.getIndex(); + selectButton.setToggled(true); + } + + @Override + public List<ClickableWidget> getButtons() { + List<ClickableWidget> clickableWidgets = new ArrayList<>(collectionSelectButtons); + for (ProfileViewerPage page : collections) { + if (page != null && page.getButtons() != null) clickableWidgets.addAll(page.getButtons()); + } + return clickableWidgets; + } + + @Override + public void markWidgetsAsVisible() { + for (SubPageSelectButton button : collectionSelectButtons) { + button.visible = true; + button.active = true; + } + } + + @Override + public void markWidgetsAsInvisible() { + for (SubPageSelectButton button : collectionSelectButtons) { + button.visible = false; + button.active = false; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/GenericCategory.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/GenericCategory.java new file mode 100644 index 00000000..a9f83ca8 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/GenericCategory.java @@ -0,0 +1,153 @@ +package de.hysky.skyblocker.skyblock.profileviewer.collections; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.LoreComponent; +import net.minecraft.component.type.NbtComponent; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.tooltip.TooltipType; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import static de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen.fetchCollectionsData; +import static de.hysky.skyblocker.skyblock.profileviewer.utils.ProfileViewerUtils.COMMA_FORMATTER; + +public class GenericCategory implements ProfileViewerPage { + private final String category; + private final LinkedList<ItemStack> collections = new LinkedList<>(); + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final Identifier BUTTON_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/button_icon_toggled.png"); + private static final int COLUMN_GAP = 26; + private static final int ROW_GAP = 34; + private static final int COLUMNS = 7; + + private final Map<String, String[]> collectionsMap; + private final Map<String, List<Integer>> tierRequirementsMap; + private final Map<String, String> ICON_TRANSLATION = Map.ofEntries( + Map.entry("MUSHROOM_COLLECTION", "RED_MUSHROOM")); + private final String[] ROMAN_NUMERALS = {"-", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII", "XIII", "XIV", "XV", "XVI", "XVII", "XVIII", "XIX", "XX"}; + + public GenericCategory(JsonObject hProfile, JsonObject pProfile, String collection) { + Map<String, Map<String, ?>> fetchedData = fetchCollectionsData(); + //noinspection unchecked + collectionsMap = (Map<String, String[]>) fetchedData.get("COLLECTIONS"); + //noinspection unchecked + tierRequirementsMap = (Map<String, List<Integer>>) fetchedData.get("TIER_REQS"); + this.category = collection; + setupItemStacks(hProfile, pProfile); + } + + private int calculateTier(int achieved, List<Integer> requirements) { + return (int) requirements.stream().filter(req -> achieved >= req).count(); + } + + private void setupItemStacks(JsonObject hProfile, JsonObject pProfile) { + JsonObject playerCollection = pProfile.getAsJsonObject("collection"); + + for (String collection : collectionsMap.get(this.category)) { + ItemStack itemStack = ItemRepository.getItemStack(ICON_TRANSLATION.getOrDefault(collection, collection).replace(':', '-')); + itemStack = itemStack == null ? Ico.BARRIER.copy() : itemStack.copy(); + + if (itemStack.getItem().getName().getString().equals("Barrier")) { + itemStack.set(DataComponentTypes.CUSTOM_NAME, Text.of(collection)); + System.out.println(collection); + System.out.println(this.category); + } + + Style style = Style.EMPTY.withColor(Formatting.WHITE).withItalic(false); + itemStack.set(DataComponentTypes.CUSTOM_NAME, Text.literal(Formatting.strip(itemStack.getComponents().get(DataComponentTypes.CUSTOM_NAME).getString())).setStyle(style)); + + + int personalColl = playerCollection != null && playerCollection.has(collection) ? playerCollection.get(collection).getAsInt() : 0; + + int totalCollection = 0; + for (String member : hProfile.get("members").getAsJsonObject().keySet()) { + if (!hProfile.getAsJsonObject("members").getAsJsonObject(member).has("collection")) continue; + JsonObject memberColl = hProfile.getAsJsonObject("members").getAsJsonObject(member).getAsJsonObject("collection"); + totalCollection += memberColl.has(collection) ? memberColl.get(collection).getAsInt() : 0; + } + + int collectionTier = calculateTier(totalCollection, tierRequirementsMap.get(collection)); + List<Integer> tierRequirements = tierRequirementsMap.get(collection); + + List<Text> lore = new ArrayList<>(); + lore.add(Text.literal("Collection Item").setStyle(style).formatted(Formatting.DARK_GRAY)); + lore.add(Text.empty()); + + if (hProfile.get("members").getAsJsonObject().keySet().size() > 1) { + lore.add(Text.literal("Personal: " + COMMA_FORMATTER.format(personalColl)).setStyle(style).formatted(Formatting.GOLD)); + lore.add(Text.literal("Co-op Collection: " + COMMA_FORMATTER.format(totalCollection-personalColl)).setStyle(style).formatted(Formatting.AQUA)); + } + lore.add(Text.literal("Collection: " + COMMA_FORMATTER.format(totalCollection)).setStyle(style).formatted(Formatting.YELLOW)); + + lore.add(Text.empty()); + lore.add(Text.literal("Collection Tier: " + collectionTier + "/" + tierRequirements.size()).setStyle(style).formatted(Formatting.LIGHT_PURPLE)); + + if (collectionTier == tierRequirements.size()) itemStack.set(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, true); + + itemStack.set(DataComponentTypes.LORE, new LoreComponent(lore)); + + itemStack.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT); + + collections.add(itemStack); + } + } + + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + Text categoryTitle = Text.literal(category.charAt(0) + category.substring(1).toLowerCase() + " Collections").formatted(Formatting.BOLD); + context.drawText(textRenderer, categoryTitle, rootX + 88 - (textRenderer.getWidth(categoryTitle) / 2), rootY, Color.DARK_GRAY.getRGB(), false); + + for (int i = 0; i < collections.size(); i++) { + int x = rootX + 2 + (i % COLUMNS) * COLUMN_GAP; + int y = rootY + 19 + (i / COLUMNS) * ROW_GAP; + + context.fill(x - 3, y - 3, x + 19, y + 19, Color.BLACK.getRGB()); + context.drawTexture(BUTTON_TEXTURE, x - 2, y - 2, 0, 0, 20, 20, 20, 20); + context.drawItem(collections.get(i), x, y); + + ItemStack itemStack = collections.get(i); + List<Text> lore = itemStack.getOrDefault(DataComponentTypes.LORE, LoreComponent.DEFAULT).lines(); + for (Text text : lore) { + if (text.getString().startsWith("Collection Tier: ")) { + String tierText = text.getString().substring("Collection Tier: ".length()); + if (tierText.contains("/")) { + String[] parts = tierText.split("/"); + int cTier = Integer.parseInt(parts[0].trim()); + Color colour = itemStack.hasGlint() ? Color.MAGENTA : Color.darkGray; + //DO NOT CHANGE THIS METHOD CALL! Aaron's Mod mixes in here to provide chroma text for max collections + //and changing the method called here will break that! Consult Aaron before making any changes :) + context.drawText(textRenderer, Text.literal(toRomanNumerals(cTier)), x + 9 - (textRenderer.getWidth(toRomanNumerals(cTier)) / 2), y + 21, colour.getRGB(), false); + } + break; + } + } + + if (mouseX > x && mouseX < x + 16 && mouseY > y && mouseY < y + 16) { + List<Text> tooltip = collections.get(i).getTooltip(Item.TooltipContext.DEFAULT, MinecraftClient.getInstance().player, TooltipType.BASIC); + context.drawTooltip(textRenderer, tooltip, mouseX, mouseY); + } + } + } + + private String toRomanNumerals(int number) { + return number <= ROMAN_NUMERALS.length ? ROMAN_NUMERALS[number] : "Err"; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonClassWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonClassWidget.java new file mode 100644 index 00000000..37953a2b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonClassWidget.java @@ -0,0 +1,73 @@ +package de.hysky.skyblocker.skyblock.profileviewer.dungeons; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; +import de.hysky.skyblocker.skyblock.profileviewer.utils.ProfileViewerUtils; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.item.ItemStack; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class DungeonClassWidget { + private final String className; + private LevelFinder.LevelInfo classLevel; + private static final int CLASS_CAP = 50; + private JsonObject classData; + private final ItemStack stack; + private boolean active = false; + + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/icon_data_widget.png"); + private static final Identifier ACTIVE_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/item_protection.png"); + private static final Identifier BAR_FILL = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_fill"); + private static final Identifier BAR_BACK = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_back"); + + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final Map<String, ItemStack> CLASS_ICON = Map.ofEntries( + Map.entry("Healer", Ico.S_POTION), + Map.entry("Mage", Ico.B_ROD), + Map.entry("Berserk", Ico.IRON_SWORD), + Map.entry("Archer", Ico.BOW), + Map.entry("Tank", Ico.CHESTPLATE) + ); + + public DungeonClassWidget(String className, JsonObject playerProfile) { + this.className = className; + stack = CLASS_ICON.getOrDefault(className, Ico.BARRIER); + try { + classData = playerProfile.getAsJsonObject("dungeons").getAsJsonObject("player_classes").getAsJsonObject(this.className.toLowerCase()); + classLevel = LevelFinder.getLevelInfo("Catacombs", classData.get("experience").getAsLong()); + active = playerProfile.getAsJsonObject("dungeons").get("selected_dungeon_class").getAsString().equals(className.toLowerCase()); + } catch (Exception ignored) { + classLevel = LevelFinder.getLevelInfo("", 0); + } + } + + public void render(DrawContext context, int mouseX, int mouseY, int x, int y) { + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 26, 109, 26); + context.drawItem(stack, x + 3, y + 5); + if (active) context.drawTexture(ACTIVE_TEXTURE, x + 3, y + 5, 0, 0, 16, 16, 16, 16); + + context.drawText(textRenderer, className + " " + classLevel.level, x + 31, y + 5, Color.WHITE.getRGB(), false); + Color fillColor = classLevel.level >= CLASS_CAP ? Color.MAGENTA : Color.GREEN; + context.drawGuiTexture(BAR_BACK, x + 30, y + 15, 75, 6); + RenderHelper.renderNineSliceColored(context, BAR_FILL, x + 30, y + 15, (int) (75 * classLevel.fill), 6, fillColor); + + if (mouseX > x + 30 && mouseX < x + 105 && mouseY > y + 12 && mouseY < y + 22){ + List<Text> tooltipText = new ArrayList<>(); + tooltipText.add(Text.literal(this.className).formatted(Formatting.GREEN)); + tooltipText.add(Text.literal("XP: " + ProfileViewerUtils.COMMA_FORMATTER.format(this.classLevel.xp)).formatted(Formatting.GOLD)); + context.drawTooltip(textRenderer, tooltipText, mouseX, mouseY); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonFloorRunsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonFloorRunsWidget.java new file mode 100644 index 00000000..b592266a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonFloorRunsWidget.java @@ -0,0 +1,84 @@ +package de.hysky.skyblocker.skyblock.profileviewer.dungeons; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class DungeonFloorRunsWidget { + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/dungeons_body.png"); + + private static final String[] DUNGEONS = {"catacombs", "master_catacombs"}; + private JsonObject dungeonsStats; + + public DungeonFloorRunsWidget(JsonObject pProfile) { + try { + dungeonsStats = pProfile.getAsJsonObject("dungeons").getAsJsonObject("dungeon_types"); + } catch (Exception ignored) {} + } + + public void render(DrawContext context, int mouseX ,int mouseY, int x, int y) { + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 110, 109, 110); + context.drawText(textRenderer, Text.literal("Floor Runs").formatted(Formatting.BOLD), x + 6, y + 4, Color.WHITE.getRGB(), true); + + int columnX = x + 4; + int elementY = y + 15; + for (String dungeon : DUNGEONS) { + JsonObject dungeonData; + try { + dungeonData = dungeonsStats.getAsJsonObject(dungeon).getAsJsonObject("tier_completions"); + for (Map.Entry<String, JsonElement> entry : dungeonData.entrySet()) { + if (entry.getKey().equals("total")) continue; + + String textToRender = String.format((dungeon.equals("catacombs") ? "§aF" : "§cM") + "%s§r %s", entry.getKey(), entry.getValue().getAsInt()); + context.drawText(textRenderer, textToRender, columnX + 2, elementY + 2, Color.WHITE.getRGB(), true); + if (!entry.getKey().equals("0") && mouseX >= columnX && mouseX <= columnX + 40 && mouseY >= elementY && mouseY <= elementY + 9) { + List<Text> tooltipText = new ArrayList<>(); + tooltipText.add(Text.literal("Personal Bests").formatted(Formatting.BOLD, Formatting.LIGHT_PURPLE)); + + JsonObject fastestTimes = dungeonsStats.getAsJsonObject(dungeon).getAsJsonObject("fastest_time_s"); + if (fastestTimes != null && fastestTimes.has(entry.getKey())) { + tooltipText.add(Text.literal("S Run: " + formatTime(fastestTimes.get(entry.getKey()).getAsLong())).formatted(Formatting.GOLD)); + } + + fastestTimes = dungeonsStats.getAsJsonObject(dungeon).getAsJsonObject("fastest_time_s_plus"); + if (fastestTimes != null && fastestTimes.has(entry.getKey())) { + tooltipText.add(Text.literal("S+ Run: " + formatTime(fastestTimes.get(entry.getKey()).getAsLong())).formatted(Formatting.GOLD)); + } + + fastestTimes = dungeonsStats.getAsJsonObject(dungeon).getAsJsonObject("fastest_time"); + if (fastestTimes != null && fastestTimes.has(entry.getKey()) && tooltipText.size() == 1) { + tooltipText.add(Text.literal("Completion: " + formatTime(fastestTimes.get(entry.getKey()).getAsLong())).formatted(Formatting.GOLD)); + } + + context.drawTooltip(textRenderer, tooltipText, mouseX, mouseY); + } + + elementY += 11; + } + columnX += 52; + elementY = y + 26; + } catch (Exception e) { + return; + } + } + } + + private String formatTime(long milliseconds) { + long seconds = milliseconds / 1000; + long minutes = seconds / 60; + seconds %= 60; + return String.format("%2d:%02d", minutes, seconds); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonHeaderWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonHeaderWidget.java new file mode 100644 index 00000000..1a62a47b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonHeaderWidget.java @@ -0,0 +1,46 @@ +package de.hysky.skyblocker.skyblock.profileviewer.dungeons; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.text.DecimalFormat; + +public class DungeonHeaderWidget { + private LevelFinder.LevelInfo classLevel; + private float classAvg; + + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final DecimalFormat DF = new DecimalFormat("#.##"); + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/dungeons_header.png"); + + public DungeonHeaderWidget(JsonObject playerProfile, String[] classes) { + try { + JsonObject DUNGEONS_PROFILE = playerProfile.getAsJsonObject("dungeons").getAsJsonObject("dungeon_types").getAsJsonObject("catacombs"); + this.classLevel = LevelFinder.getLevelInfo("Catacombs", DUNGEONS_PROFILE.get("experience").getAsLong()); + + float avg = 0; + JsonObject CLASS_DATA = playerProfile.getAsJsonObject("dungeons").getAsJsonObject("player_classes"); + for (String element : classes) { + avg += LevelFinder.getLevelInfo("Catacombs", CLASS_DATA.getAsJsonObject(element.toLowerCase()).get("experience").getAsLong()).level; + } + classAvg = avg/classes.length; + } catch (Exception ignored) { + this.classLevel = LevelFinder.getLevelInfo("", 0); + classAvg = 0; + } + } + + public void render(DrawContext context, int x, int y) { + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 26, 109, 26); + + context.drawText(textRenderer, "§i§6§lCatacombs §r" + this.classLevel.level, x + 3, y + 4, Color.WHITE.getRGB(), true); + + context.drawText(textRenderer, "§eClass Average §r" + DF.format(this.classAvg), x + 3, y + 14, Color.WHITE.getRGB(), true); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonMiscStatsWidgets.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonMiscStatsWidgets.java new file mode 100644 index 00000000..780eec24 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonMiscStatsWidgets.java @@ -0,0 +1,61 @@ +package de.hysky.skyblocker.skyblock.profileviewer.dungeons; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.text.DecimalFormat; +import java.util.HashMap; +import java.util.Map; + +public class DungeonMiscStatsWidgets { + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/icon_data_widget.png"); + private static final Identifier RUN_ICON = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/run_icon.png"); + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final DecimalFormat DF = new DecimalFormat("#.##"); + private static final String[] DUNGEONS = {"catacombs", "master_catacombs"}; + + private final Map<String, Integer> dungeonRuns = new HashMap<>(); + private int secrets = 0; + private int totalRuns = 0; + + public DungeonMiscStatsWidgets(JsonObject pProfile) { + JsonObject DUNGEONS_DATA = pProfile.getAsJsonObject("dungeons"); + try { + secrets = DUNGEONS_DATA.get("secrets").getAsInt(); + + for (String dungeon : DUNGEONS) { + JsonObject dungeonData = DUNGEONS_DATA.getAsJsonObject("dungeon_types").getAsJsonObject(dungeon).getAsJsonObject("tier_completions"); + int runs = 0; + for (Map.Entry<String, JsonElement> entry : dungeonData.entrySet()) { + String key = entry.getKey(); + if (key.equals("total")) continue; + runs += entry.getValue().getAsInt(); + } + dungeonRuns.put(dungeon, runs); + totalRuns += runs; + } + + } catch (Exception ignored) {} + } + + public void render(DrawContext context, int x, int y) { + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 26, 109, 26); + context.drawItem(Ico.FEATHER, x + 2, y + 4); + + context.drawText(textRenderer, "Secrets " + secrets, x + 30, y + 4, Color.WHITE.getRGB(), true); + context.drawText(textRenderer, "Avg " + (totalRuns > 0 ? DF.format(secrets / (float) totalRuns) : 0) + "/Run", x + 30, y + 14, Color.WHITE.getRGB(), true); + + context.drawTexture(TEXTURE, x, y + 28, 0, 0, 109, 26, 109, 26); + context.drawTexture(RUN_ICON, x + 4, y + 33, 0, 0, 14, 16, 14, 16); + + context.drawText(textRenderer, "§aNormal §r" + dungeonRuns.getOrDefault("catacombs", 0), x + 30, y + 32, Color.WHITE.getRGB(), true); + context.drawText(textRenderer, "§cMaster §r" + dungeonRuns.getOrDefault("master_catacombs", 0), x + 30, y + 42, Color.WHITE.getRGB(), true); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonsPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonsPage.java new file mode 100644 index 00000000..e0051c88 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonsPage.java @@ -0,0 +1,39 @@ +package de.hysky.skyblocker.skyblock.profileviewer.dungeons; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.utils.ProfileUtils; +import net.minecraft.client.gui.DrawContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class DungeonsPage implements ProfileViewerPage { + public static final Logger LOGGER = LoggerFactory.getLogger(ProfileUtils.class); + private static final String[] CLASSES = {"Healer", "Mage", "Berserk", "Archer", "Tank"}; + + private final DungeonHeaderWidget dungeonHeaderWidget; + private final List<DungeonClassWidget> dungeonClassWidgetsList = new ArrayList<>(); + private final DungeonFloorRunsWidget dungeonFloorRunsWidget; + private final DungeonMiscStatsWidgets dungeonMiscStatsWidgets; + + public DungeonsPage(JsonObject pProfile) { + dungeonHeaderWidget = new DungeonHeaderWidget(pProfile, CLASSES); + dungeonFloorRunsWidget = new DungeonFloorRunsWidget(pProfile); + dungeonMiscStatsWidgets = new DungeonMiscStatsWidgets(pProfile); + for (String element : CLASSES) { + dungeonClassWidgetsList.add(new DungeonClassWidget(element, pProfile)); + } + } + + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + dungeonHeaderWidget.render(context, rootX, rootY); + dungeonFloorRunsWidget.render(context, mouseX, mouseY, rootX + 113, rootY + 56); + dungeonMiscStatsWidgets.render(context, rootX + 113, rootY); + for (int i = 0; i < dungeonClassWidgetsList.size(); i++) { + dungeonClassWidgetsList.get(i).render(context, mouseX, mouseY, rootX, rootY + 28 + i * 28); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Inventory.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Inventory.java new file mode 100644 index 00000000..126c55ec --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Inventory.java @@ -0,0 +1,131 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.item.ItemRarityBackgrounds; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders.ItemLoader; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.resource.language.I18n; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.tooltip.TooltipType; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Inventory implements ProfileViewerPage { + private static final Identifier TEXTURE = Identifier.of("textures/gui/container/generic_54.png"); + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private final IntIntPair dimensions; + private final int itemsPerPage; + private final List<ItemStack> containerList; + private final String containerName; + private int activePage = 0; + private int totalPages = 1; + private final PaginationButton previousPage = new PaginationButton(this, -1000, 0, false); + private final PaginationButton nextPage = new PaginationButton(this, -1000, 0, true); + + public Inventory(String name, IntIntPair dimensions, JsonObject inventory) { + this(name, dimensions, inventory, new ItemLoader()); + } + + public Inventory(String name, IntIntPair dimensions, JsonObject inventory, ItemLoader itemLoader) { + containerName = name; + this.dimensions = dimensions; + itemsPerPage = dimensions.rightInt() * dimensions.leftInt(); + this.containerList = itemLoader.loadItems(inventory); + this.totalPages = (int) Math.ceil((double) containerList.size() / itemsPerPage); + } + + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + int rootYAdjusted = rootY + (26 - dimensions.leftInt() * 3); + context.drawTexture(TEXTURE, rootX, rootYAdjusted, 0, 0, dimensions.rightInt() * 18 + 7, dimensions.leftInt() * 18 + 17); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootYAdjusted, 169, 0, 7, dimensions.leftInt() * 18 + 17); + context.drawTexture(TEXTURE, rootX, rootYAdjusted + dimensions.leftInt() * 18 + 17, 0, 215, dimensions.rightInt() * 18 + 7, 7); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootYAdjusted + dimensions.leftInt() * 18 + 17, 169, 215, 7, 7); + + context.drawText(textRenderer, I18n.translate("skyblocker.profileviewer.inventory." + containerName), rootX + 7, rootYAdjusted + 7, Color.DARK_GRAY.getRGB(), false); + + if (containerList.size() > itemsPerPage) { + previousPage.setX(rootX + 44); + previousPage.setY(rootY + 136); + previousPage.render(context, mouseX, mouseY, delta); + + context.drawCenteredTextWithShadow(textRenderer, "Page: " + (activePage + 1) + "/" + totalPages, rootX + 88, rootY + 140, Color.WHITE.getRGB()); + + nextPage.setX(rootX + 121); + nextPage.setY(rootY + 136); + nextPage.render(context, mouseX, mouseY, delta); + } + + int startIndex = activePage * itemsPerPage; + int endIndex = Math.min(startIndex + itemsPerPage, containerList.size()); + List<Text> tooltip = Collections.emptyList(); + for (int i = 0; i < endIndex - startIndex; i++) { + if (containerList.get(startIndex + i) == ItemStack.EMPTY) continue; + int column = i % dimensions.rightInt(); + int row = i / dimensions.rightInt(); + + int x = rootX + 8 + column * 18; + int y = rootYAdjusted + 18 + row * 18; + + if (SkyblockerConfigManager.get().general.itemInfoDisplay.itemRarityBackgrounds) { + ItemRarityBackgrounds.tryDraw(containerList.get(startIndex + i), context, x, y); + } + + context.drawItem(containerList.get(startIndex + i), x, y); + context.drawItemInSlot(textRenderer, containerList.get(startIndex + i), x, y); + + if (mouseX > x -1 && mouseX < x + 16 && mouseY > y - 1 && mouseY < y + 16) { + tooltip = containerList.get(startIndex + i).getTooltip(Item.TooltipContext.DEFAULT, MinecraftClient.getInstance().player, TooltipType.BASIC); + } + } + + if (!tooltip.isEmpty()) context.drawTooltip(textRenderer, tooltip, mouseX, mouseY); + } + + public void nextPage() { + if (activePage < totalPages - 1) { + activePage++; + } + } + + public void previousPage() { + if (activePage > 0) { + activePage--; + } + } + + @Override + public void markWidgetsAsVisible() { + nextPage.visible = true; + previousPage.visible = true; + nextPage.active = true; + previousPage.active = true; + } + + @Override + public void markWidgetsAsInvisible() { + nextPage.visible = false; + previousPage.visible = false; + nextPage.active = false; + previousPage.active = false; + } + + @Override + public List<ClickableWidget> getButtons() { + List<ClickableWidget> buttons = new ArrayList<>(); + buttons.add(nextPage); + buttons.add(previousPage); + return buttons; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/InventoryPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/InventoryPage.java new file mode 100644 index 00000000..6aa92ef6 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/InventoryPage.java @@ -0,0 +1,113 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders.BackpackItemLoader; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders.PetsInventoryItemLoader; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders.WardrobeInventoryItemLoader; +import de.hysky.skyblocker.skyblock.profileviewer.utils.ProfileViewerUtils; +import de.hysky.skyblocker.skyblock.profileviewer.utils.SubPageSelectButton; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.item.ItemStack; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class InventoryPage implements ProfileViewerPage { + private static final String[] INVENTORY_PAGES = {"inventory", "enderchest", "backpack", "wardrobe", "pets", "accessoryBag"}; + private static final int TOTAL_HEIGHT = 165; + private static final Map<String, ItemStack> ICON_MAP = Map.ofEntries( + Map.entry("wardrobe", Ico.L_CHESTPLATE), + Map.entry("inventory", Ico.CHEST), + Map.entry("backpack", ProfileViewerUtils.createSkull("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHBzOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzYyZjNiM2EwNTQ4MWNkZTc3MjQwMDA1YzBkZGNlZTFjMDY5ZTU1MDRhNjJjZTA5Nzc4NzlmNTVhMzkzOTYxNDYifX19")), + Map.entry("pets", Ico.BONE), + Map.entry("enderchest", Ico.E_CHEST), + Map.entry("accessoryBag", ProfileViewerUtils.createSkull("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTYxYTkxOGMwYzQ5YmE4ZDA1M2U1MjJjYjkxYWJjNzQ2ODkzNjdiNGQ4YWEwNmJmYzFiYTkxNTQ3MzA5ODVmZiJ9fX0=")) + ); + + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private final ProfileViewerPage[] inventorySubPages = new ProfileViewerPage[6]; + private final List<SubPageSelectButton> inventorySelectButtons = new ArrayList<>(); + private int activePage = 0; + + public InventoryPage(JsonObject pProfile) { + for (int i = 0; i < INVENTORY_PAGES.length; i++) { + inventorySelectButtons.add(new SubPageSelectButton(this, -100, 0, i, ICON_MAP.getOrDefault(INVENTORY_PAGES[i], Ico.BARRIER))); + } + + try { + JsonObject inventoryData = pProfile.getAsJsonObject("inventory"); + if (inventoryData == null) return; + inventorySubPages[0] = new PlayerInventory(inventoryData); + inventorySubPages[1] = new Inventory(INVENTORY_PAGES[1], IntIntPair.of(5, 9), inventoryData.getAsJsonObject("ender_chest_contents")); + inventorySubPages[2] = new Inventory(INVENTORY_PAGES[2], IntIntPair.of(5, 9), inventoryData.getAsJsonObject("backpack_contents"), new BackpackItemLoader()); + inventorySubPages[3] = new Inventory(INVENTORY_PAGES[3], IntIntPair.of(4, 9), inventoryData.getAsJsonObject("wardrobe_contents"), new WardrobeInventoryItemLoader(inventoryData)); + inventorySubPages[4] = new Inventory(INVENTORY_PAGES[4], IntIntPair.of(4, 9), pProfile, new PetsInventoryItemLoader()); + inventorySubPages[5] = new Inventory(INVENTORY_PAGES[5], IntIntPair.of(5, 9), inventoryData.getAsJsonObject("bag_contents").getAsJsonObject("talisman_bag")); + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Error while loading inventory data: ", e); + } + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + int startingY = rootY + (TOTAL_HEIGHT - inventorySelectButtons.size() * 21) / 2; + for (int i = 0; i < inventorySelectButtons.size(); i++) { + inventorySelectButtons.get(i).setX(rootX); + inventorySelectButtons.get(i).setY(startingY + i * 21); + inventorySelectButtons.get(i).render(context, mouseX, mouseY, delta); + } + + if (inventorySubPages[activePage] == null) { + context.drawText(textRenderer, "No data...", rootX + 92, rootY + 72, Color.DARK_GRAY.getRGB(), false); + return; + } + + inventorySubPages[activePage].markWidgetsAsVisible(); + inventorySubPages[activePage].render(context, mouseX, mouseY, delta, rootX + 35, rootY + 6); + } + + public void onNavButtonClick(SubPageSelectButton clickedButton) { + if (inventorySubPages[activePage] != null) inventorySubPages[activePage].markWidgetsAsInvisible(); + for (SubPageSelectButton button : inventorySelectButtons) { + button.setToggled(false); + } + activePage = clickedButton.getIndex(); + clickedButton.setToggled(true); + } + + @Override + public List<ClickableWidget> getButtons() { + List<ClickableWidget> clickableWidgets = new ArrayList<>(inventorySelectButtons); + for (ProfileViewerPage page : inventorySubPages) { + if (page != null && page.getButtons() != null) clickableWidgets.addAll(page.getButtons()); + } + return clickableWidgets; + } + + @Override + public void markWidgetsAsVisible() { + if (inventorySubPages[activePage] != null) inventorySubPages[activePage].markWidgetsAsVisible(); + for (SubPageSelectButton button : inventorySelectButtons) { + button.visible = true; + button.active = true; + } + } + + @Override + public void markWidgetsAsInvisible() { + if (inventorySubPages[activePage] != null) inventorySubPages[activePage].markWidgetsAsInvisible(); + for (SubPageSelectButton button : inventorySelectButtons) { + button.visible = false; + button.active = false; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PaginationButton.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PaginationButton.java new file mode 100644 index 00000000..1a725e1a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PaginationButton.java @@ -0,0 +1,46 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory; + +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +public class PaginationButton extends ClickableWidget { + private final ProfileViewerPage screen; + private final boolean isNextButton; + private final Identifier TEXTURE; + private final Identifier HIGHLIGHT; + + public PaginationButton(ProfileViewerPage screen, int x, int y, boolean isNextButton) { + super(x, y, 12, 17, Text.empty()); + this.screen = screen; + this.isNextButton = isNextButton; + if (isNextButton) { + TEXTURE = Identifier.of("minecraft", "textures/gui/sprites/recipe_book/page_forward.png"); + HIGHLIGHT = Identifier.of("minecraft", "textures/gui/sprites/recipe_book/page_forward_highlighted.png"); + } else { + TEXTURE = Identifier.of("minecraft", "textures/gui/sprites/recipe_book/page_backward.png"); + HIGHLIGHT = Identifier.of("minecraft", "textures/gui/sprites/recipe_book/page_backward_highlighted.png"); + } + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawTexture(TEXTURE, this.getX(), this.getY(), 0, 0, 12, 17, 12, 17); + if (isMouseOver(mouseX, mouseY)) context.drawTexture(HIGHLIGHT, this.getX(), this.getY(), 0, 0, 12, 17, 12, 17); + } + + @Override + public void onClick(double mouseX, double mouseY) { + if (isNextButton) { + screen.nextPage(); + } else { + screen.previousPage(); + } + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Pet.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Pet.java new file mode 100644 index 00000000..857198e1 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Pet.java @@ -0,0 +1,265 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory; + +import de.hysky.skyblocker.skyblock.PetCache; +import de.hysky.skyblocker.skyblock.itemlist.ItemFixerUpper; +import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; +import de.hysky.skyblocker.skyblock.profileviewer.utils.ProfileViewerUtils; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.NEURepoManager; +import io.github.moulberry.repo.constants.PetNumbers; +import io.github.moulberry.repo.data.NEUItem; +import io.github.moulberry.repo.data.Rarity; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.LoreComponent; +import net.minecraft.component.type.ProfileComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.Registries; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static de.hysky.skyblocker.skyblock.itemlist.ItemStackBuilder.SKULL_TEXTURE_PATTERN; +import static de.hysky.skyblocker.skyblock.profileviewer.utils.ProfileViewerUtils.numLetterFormat; + +public class Pet { + private final String name; + private final double xp; + private final String tier; + private final Optional<String> heldItem; + private final Optional<String> skin; + private final Optional<String> skinTexture; + private final int level; + private final double perecentageToLevel; + private final long levelXP; + private final long nextLevelXP; + private final ItemStack icon; + + private final Pattern statsMatcher = Pattern.compile("\\{[A-Za-z_]+}"); + private final Pattern numberMatcher = Pattern.compile("\\{\\d+}"); + + + + private static final Map<String, Integer> TIER_MAP = Map.of( + "COMMON", 0, "UNCOMMON", 1, "RARE", 2, "EPIC", 3, "LEGENDARY", 4, "MYTHIC", 5 + ); + + private static final Int2ObjectMap<Formatting> RARITY_COLOR_MAP = Int2ObjectMaps.unmodifiable(new Int2ObjectOpenHashMap<>(Map.of( + 0, Formatting.WHITE, // COMMON + 1, Formatting.GREEN, // UNCOMMON + 2, Formatting.BLUE, // RARE + 3, Formatting.DARK_PURPLE, // EPIC + 4, Formatting.GOLD, // LEGENDARY + 5, Formatting.LIGHT_PURPLE, // MYTHIC + 6, Formatting.AQUA // DIVINE (future proofing, because why not) + ))); + + public Pet(PetCache.PetInfo petData) { + LevelFinder.LevelInfo info = LevelFinder.getLevelInfo(petData.type().equals("GOLDEN_DRAGON") ? "PET_GREG" : "PET_" + petData.tier(), (long) petData.exp()); + this.name = petData.type(); + this.xp = petData.exp(); + this.heldItem = petData.item(); + this.skin = petData.skin(); + this.skinTexture = calculateSkinTexture(); + this.tier = petData.tier(); + this.level = info.level; + this.perecentageToLevel = info.fill; + this.levelXP = info.levelXP; + this.nextLevelXP = info.nextLevelXP; + this.icon = createIcon(); + } + + private String getName() { + return name; + } + + public long getXP() { + return (long) xp; + } + + private int getTier() { + return TIER_MAP.getOrDefault(tier, 0); + } + + public String getTierAsString() { + return tier; + } + + private Optional<String> calculateSkinTexture() { + if (this.skin.isPresent()) { + NEUItem item = NEURepoManager.NEU_REPO.getItems().getItemBySkyblockId("PET_SKIN_" + this.skin.get()); + if (item == null) return Optional.empty(); + Matcher skullTexture = SKULL_TEXTURE_PATTERN.matcher(item.getNbttag()); + if (skullTexture.find()) return Optional.of(skullTexture.group(1)); + } + return Optional.empty(); + } + public int getLevel() { return level; } + public ItemStack getIcon() { return icon; } + + + private ItemStack createIcon() { + if (NEURepoManager.isLoading() || !ItemRepository.filesImported()) return Ico.BARRIER; + Map<String, NEUItem> items = NEURepoManager.NEU_REPO.getItems().getItems(); + if (items == null) return Ico.BARRIER; + + String targetItemId = this.getName() + ";" + (this.getTier() + (heldItem.isPresent() && heldItem.get().equals("PET_ITEM_TIER_BOOST") ? 1 : 0)); + NEUItem item = NEURepoManager.NEU_REPO.getItems().getItems().get(targetItemId); + + // For cases life RIFT_FERRET Where it can be tier boosted into a pet that otherwise can't exist + if (item == null && heldItem.isPresent() && heldItem.get().equals("PET_ITEM_TIER_BOOST")) { + item = NEURepoManager.NEU_REPO.getItems().getItems().get(getName() + ";" + getTier()); + } + + return fromNEUItem(item, this.heldItem.map(ItemRepository::getItemStack).orElse(null)); + } + + /** + * Converts NEU item data into an ItemStack. + * <p> This method converts NEU item data into a Pet by using the placeholder + * information from NEU-REPO and injecting the player's calculated pet stats into the lore and transforming + * the NBT Data into modern DataComponentTypes before returning the final ItemStack </p + * + * @param item The NEUItem representing the pet. + * @param heldItem The ItemStack of the pet's held item, if any. + * @return The ItemStack representing the pet with all its properties set. + */ + private ItemStack fromNEUItem(NEUItem item, ItemStack heldItem) { + if (item == null) { + ItemStack errIcon = Ico.BARRIER.copy(); + errIcon.set(DataComponentTypes.CUSTOM_NAME, Text.of(this.getName())); + return errIcon; + } + + Identifier itemId = Identifier.of(ItemFixerUpper.convertItemId(item.getMinecraftItemId(), item.getDamage())); + ItemStack petStack = new ItemStack(Registries.ITEM.get(itemId)).copy(); + + List<Text> formattedLore = !(name.equals("GOLDEN_DRAGON") && level < 101) ? processLore(item.getLore(), heldItem) : buildGoldenDragonEggLore(item.getLore()); + + // Calculate and display XP for level + Style style = Style.EMPTY.withItalic(false); + if (level != 100 && level != 200) { + String progress = "Progress to Level " + this.level + ": §e" + fixDecimals(this.perecentageToLevel * 100, true) + "%"; + formattedLore.add(formattedLore.size() - 1, Text.literal(progress).setStyle(style).formatted(Formatting.GRAY)); + String string = "§2§m ".repeat((int) Math.round(perecentageToLevel * 30)) + "§f§m ".repeat(30 - (int) Math.round(perecentageToLevel * 30)); + formattedLore.add(formattedLore.size() - 1, Text.literal(string + "§r§e " + numLetterFormat(levelXP) + "§6/§e" + numLetterFormat(nextLevelXP)).setStyle(style)); + formattedLore.add(formattedLore.size() - 1, Text.empty()); + } else { + formattedLore.add(formattedLore.size() - 1, Text.literal("MAX LEVEL").setStyle(style).formatted(Formatting.AQUA, Formatting.BOLD)); + formattedLore.add(formattedLore.size() - 1, Text.literal("▸ " + ProfileViewerUtils.COMMA_FORMATTER.format((long) xp) + " XP").setStyle(style).formatted(Formatting.DARK_GRAY)); + formattedLore.add(formattedLore.size() - 1, Text.empty()); + } + + // Skin Head Texture + if (skinTexture.isPresent()) { + formattedLore.set(0, Text.of(formattedLore.getFirst().getString() + ", " + Formatting.strip(NEURepoManager.NEU_REPO.getItems().getItems().get("PET_SKIN_" + skin.get()).getDisplayName()))); + petStack.set(DataComponentTypes.PROFILE, new ProfileComponent( + Optional.of(item.getSkyblockItemId()), Optional.of(UUID.randomUUID()), + ItemUtils.propertyMapWithTexture(this.skinTexture.get()))); + } else { + Matcher skullTexture = SKULL_TEXTURE_PATTERN.matcher(item.getNbttag()); + if (skullTexture.find()) { + petStack.set(DataComponentTypes.PROFILE, new ProfileComponent( + Optional.of(item.getSkyblockItemId()), Optional.of(UUID.randomUUID()), + ItemUtils.propertyMapWithTexture(skullTexture.group(1)))); + } + } + + if ((boosted())) formattedLore.set(formattedLore.size() - 1, Text.literal(Rarity.values()[getTier() + 1].toString()).setStyle(style).formatted(Formatting.BOLD, RARITY_COLOR_MAP.get(getTier() + 1))); + + // Update the lore and name + petStack.set(DataComponentTypes.LORE, new LoreComponent(formattedLore)); + String displayName = Formatting.strip(item.getDisplayName()).replace("[Lvl {LVL}]", "§7[Lvl " + this.level + "]§r"); + petStack.set(DataComponentTypes.CUSTOM_NAME, Text.literal(displayName).setStyle(style).formatted(RARITY_COLOR_MAP.get(this.getTier() + (boosted() ? 1 : 0)))); + return petStack; + } + + /** + * Iterates through a Pet's lore injecting interpolated stat numbers based on pet level + * + * @param lore the raw lore data stored in NEU Repo + * @param heldItem the pet's held item, if any + * @return Formatted lore with injected stats inserted into the tooltip + */ + private List<Text> processLore(List<String> lore, ItemStack heldItem) { + Map<String, Map<Rarity, PetNumbers>> petNums = NEURepoManager.NEU_REPO.getConstants().getPetNumbers(); + Rarity rarity = Rarity.values()[getTier()]; + PetNumbers data = petNums.get(getName()).get(rarity); + List<Text> formattedLore = new ArrayList<>(); + + for (String line : lore) { + if (line.contains("Right-click to add this") || line.contains("pet menu!")) continue; + + String formattedLine = line; + + Matcher stats = statsMatcher.matcher(formattedLine); + Matcher other = numberMatcher.matcher(formattedLine); + + while (stats.find()) { + String placeholder = stats.group(); + String statKey = placeholder.substring(1, placeholder.length() - 1); + String statValue = String.valueOf(fixDecimals(data.interpolatedStatsAtLevel(this.level).getStatNumbers().get(statKey), true)); + formattedLine = formattedLine.replace(placeholder, statValue); + } + + while (other.find()) { + String placeholder = other.group(); + int numberKey = Integer.parseInt(placeholder.substring(1, placeholder.length() - 1)); + String statValue = String.valueOf(fixDecimals(data.interpolatedStatsAtLevel(this.level).getOtherNumbers().get(numberKey), false)); + formattedLine = formattedLine.replace(placeholder, statValue); + } + + formattedLore.add(Text.of(formattedLine)); + } + + + if (heldItem != null) { + formattedLore.set(formattedLore.size() - 2, Text.of("§r§6Held Item: " + heldItem.getName().getString())); + formattedLore.add(formattedLore.size() - 1, Text.empty()); + } + + return formattedLore; + } + + /** + * NEU Repo doesn't distinguish between the Egg and the hatched GoldenDragon pet so hardcoded lore :eues: + * @param lore the existing lore + * @return Fully formatted GoldenDragonEgg Lore + */ + private List<Text> buildGoldenDragonEggLore(List<String> lore) { + List<Text> formattedLore = new ArrayList<>(); + Style style = Style.EMPTY.withItalic(false); + + formattedLore.add(Text.of(lore.getFirst())); + formattedLore.add(Text.empty()); + formattedLore.add(Text.literal("Perks:").setStyle(style).formatted(Formatting.GRAY)); + formattedLore.add(Text.literal("???").setStyle(style).formatted(Formatting.RED, Formatting.BOLD)); + formattedLore.add(Text.empty()); + formattedLore.add(Text.literal("Hatches at level §b100").setStyle(style).formatted(Formatting.GRAY)); + formattedLore.add(Text.empty()); + formattedLore.add(Text.of(lore.getLast())); + + return formattedLore; + } + + private String fixDecimals(double num, boolean truncate) { + if (num % 1 == 0) return String.valueOf((int) (num)); + BigDecimal roundedNum = new BigDecimal(num).setScale(truncate ? 1 : 3, RoundingMode.HALF_UP); + return roundedNum.stripTrailingZeros().toPlainString(); + } + + private boolean boosted() { + return this.heldItem.isPresent() && this.heldItem.get().equals("PET_ITEM_TIER_BOOST"); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PlayerInventory.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PlayerInventory.java new file mode 100644 index 00000000..e210ca9a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PlayerInventory.java @@ -0,0 +1,82 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.item.ItemRarityBackgrounds; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders.InventoryItemLoader; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.resource.language.I18n; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.tooltip.TooltipType; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.Collections; +import java.util.List; + +public class PlayerInventory implements ProfileViewerPage { + private static final Identifier TEXTURE = Identifier.of("textures/gui/container/generic_54.png"); + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private final List<ItemStack> containerList; + private List<Text> tooltip = Collections.emptyList(); + + public PlayerInventory(JsonObject inventory) { + this.containerList = new InventoryItemLoader().loadItems(inventory); + } + + // Z-STACKING forces this nonsense of separating the Background texture and Item Drawing :( + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + drawContainerTextures(context, "armor", rootX, rootY + 108, IntIntPair.of(1, 4)); + drawContainerTextures(context, "inventory", rootX, rootY + 2, IntIntPair.of(4, 9)); + drawContainerTextures(context, "equipment", rootX + 90, rootY + 108, IntIntPair.of(1, 4)); + + tooltip.clear(); + drawContainerItems(context, rootX, rootY + 108, IntIntPair.of(1, 4), 36, 40, mouseX, mouseY); + drawContainerItems(context, rootX, rootY + 2, IntIntPair.of(4, 9), 0, 36, mouseX, mouseY); + drawContainerItems(context, rootX + 90, rootY + 108, IntIntPair.of(1, 4), 40, containerList.size(), mouseX, mouseY); + if (!tooltip.isEmpty()) context.drawTooltip(textRenderer, tooltip, mouseX, mouseY); + } + + private void drawContainerTextures(DrawContext context, String containerName, int rootX, int rootY, IntIntPair dimensions) { + if (containerName.equals("inventory")) { + context.drawTexture(TEXTURE, rootX, rootY + dimensions.leftInt() + 10, 0, 136, dimensions.rightInt() * 18 + 7, dimensions.leftInt() * 18 + 17); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootY, 169, 0, 7, dimensions.leftInt() * 18 + 21); + context.drawTexture(TEXTURE, rootX, rootY, 0, 0, dimensions.rightInt() * 18 + 7, 14); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootY + dimensions.leftInt() * 18 + 21, 169, 215, 7, 7); + } else { + context.drawTexture(TEXTURE, rootX, rootY, 0, 0, dimensions.rightInt() * 18 + 7, dimensions.leftInt() * 18 + 17); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootY, 169, 0, 7, dimensions.leftInt() * 18 + 17); + context.drawTexture(TEXTURE, rootX, rootY + dimensions.leftInt() * 18 + 17, 0, 215, dimensions.rightInt() * 18 + 7, 7); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootY + dimensions.leftInt() * 18 + 17, 169, 215, 7, 7); + } + context.drawText(textRenderer, I18n.translate("skyblocker.profileviewer.inventory." + containerName), rootX + 7, rootY + 7, Color.DARK_GRAY.getRGB(), false); + } + + private void drawContainerItems(DrawContext context, int rootX, int rootY, IntIntPair dimensions, int startIndex, int endIndex, int mouseX, int mouseY) { + for (int i = 0; i < endIndex - startIndex; i++) { + if (containerList.get(startIndex + i) == ItemStack.EMPTY) continue; + int column = i % dimensions.rightInt(); + int row = i / dimensions.rightInt(); + + int x = rootX + 8 + column * 18; + int y = (rootY + 18 + row * 18) + (dimensions.leftInt() > 1 && row + 1 == dimensions.leftInt() ? 4 : 0); + + if (SkyblockerConfigManager.get().general.itemInfoDisplay.itemRarityBackgrounds) { + ItemRarityBackgrounds.tryDraw(containerList.get(startIndex + i), context, x, y); + } + + context.drawItem(containerList.get(startIndex + i), x, y); + context.drawItemInSlot(textRenderer, containerList.get(startIndex + i), x, y); + + if (mouseX > x -1 && mouseX < x + 16 && mouseY > y - 1 && mouseY < y + 16) { + tooltip = containerList.get(startIndex + i).getTooltip(Item.TooltipContext.DEFAULT, MinecraftClient.getInstance().player, TooltipType.BASIC); + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/BackpackItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/BackpackItemLoader.java new file mode 100644 index 00000000..dee2bfaf --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/BackpackItemLoader.java @@ -0,0 +1,41 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.LoreComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.text.Text; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class BackpackItemLoader extends ItemLoader { + @Override + public List<ItemStack> loadItems(JsonObject data) { + List<ItemStack> backpackItems = new ArrayList<>(); + + // Sort the data by keys numerically + List<Map.Entry<String, JsonElement>> sortedEntries = data.entrySet().stream() + .sorted((e1, e2) -> { + int key1 = Integer.parseInt(e1.getKey()); + int key2 = Integer.parseInt(e2.getKey()); + return Integer.compare(key1, key2); + }).toList(); + + for (int i = 0; i < sortedEntries.size(); i++) { + backpackItems.addAll(super.loadItems(sortedEntries.get(i).getValue().getAsJsonObject())); + int paddingNeeded = (45 - (backpackItems.size() % 45)) % 45; + for (int j = 0; j < paddingNeeded; j++) { + ItemStack paddingItem = Ico.GRAY_DYE.copy(); + paddingItem.set(DataComponentTypes.CUSTOM_NAME, Text.translatable("skyblocker.profileviewer.inventory.inactive")); + paddingItem.set(DataComponentTypes.LORE, new LoreComponent(List.of(Text.translatable("skyblocker.profileviewer.inventory.inactive.description.backpack"),Text.translatable("skyblocker.profileviewer.inventory.inactive.description.general")))); + backpackItems.add(paddingItem); + } + } + + return backpackItems; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java new file mode 100644 index 00000000..f73661a1 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java @@ -0,0 +1,29 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders; + +import com.google.gson.JsonObject; +import net.minecraft.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +public class InventoryItemLoader extends ItemLoader { + + private static final String[] INVENTORIES = {"inv_contents", "inv_armor", "equipment_contents"}; + + @Override + public List<ItemStack> loadItems(JsonObject data) { + List<ItemStack> inventoryItems = new ArrayList<>(); + for (String inventory : INVENTORIES) { + List<ItemStack> inv = super.loadItems(data.getAsJsonObject(inventory)); + switch (inventory) { + case "inv_armor" -> inventoryItems.addAll(inv.reversed()); + case "inv_contents" -> { + inventoryItems.addAll(inv.subList(9,inv.size())); + inventoryItems.addAll(inv.subList(0, 9)); + } + default -> inventoryItems.addAll(inv); + } + } + return inventoryItems; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/ItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/ItemLoader.java new file mode 100644 index 00000000..11280af1 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/ItemLoader.java @@ -0,0 +1,144 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; + +import de.hysky.skyblocker.skyblock.PetCache; +import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.Pet; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.NEURepoManager; +import de.hysky.skyblocker.utils.TextTransformer; +import io.github.moulberry.repo.data.NEUItem; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.*; +import net.minecraft.datafixer.fix.ItemIdFix; +import net.minecraft.datafixer.fix.ItemInstanceTheFlatteningFix; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.*; +import net.minecraft.registry.Registries; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; + +import java.io.ByteArrayInputStream; +import java.util.*; +import java.util.stream.Collectors; + +import static de.hysky.skyblocker.skyblock.itemlist.ItemRepository.getItemStack; + +public class ItemLoader { + + public List<ItemStack> loadItems(JsonObject data) { + NbtList containerContent = decompress(data); + List<ItemStack> itemList = new ArrayList<>(); + + for (int i = 0; i < containerContent.size(); i++) { + if (containerContent.getCompound(i).getInt("id") == 0) { + itemList.add(ItemStack.EMPTY); + continue; + } + + NbtCompound nbttag = containerContent.getCompound(i).getCompound("tag"); + NbtCompound extraAttributes = nbttag.getCompound("ExtraAttributes"); + String internalName = extraAttributes.getString("id"); + if (internalName.equals("PET")) { + PetCache.PetInfo petInfo = PetCache.PetInfo.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(extraAttributes.getString("petInfo"))).getOrThrow(); + Pet pet = new Pet(petInfo); + itemList.add(pet.getIcon()); + continue; + } + + Identifier itemId = identifierFromOldId(containerContent.getCompound(i).getInt("id"), containerContent.getCompound(i).getInt("Damage")); + ItemStack stack = itemId.toString().equals("minecraft:air") ? getItemStack(internalName) : new ItemStack(Registries.ITEM.get(itemId)); + + if (stack == null || stack.isEmpty() || stack.getItem().equals(Ico.BARRIER.getItem())) { + // Last ditch effort to find item in NEU REPO + Map<String, NEUItem> items = NEURepoManager.NEU_REPO.getItems().getItems(); + stack = items.values().stream() + .filter(j -> Formatting.strip(j.getSkyblockItemId()).equals(Formatting.strip(internalName).replace(":", "-"))) + .findFirst() + .map(NEUItem::getSkyblockItemId) + .map(ItemRepository::getItemStack) + .orElse(Ico.BARRIER.copy()); + + + if (stack.getName().getString().contains("barrier")) { + stack.set(DataComponentTypes.CUSTOM_NAME, Text.literal("Err: " + internalName)); + itemList.add(stack); + continue; + } + } + + // Custom Data + NbtCompound customData = new NbtCompound(); + + // Add Skyblock Item Id + customData.put(ItemUtils.ID, NbtString.of(internalName)); + + + // Item Name + stack.set(DataComponentTypes.CUSTOM_NAME, TextTransformer.fromLegacy(nbttag.getCompound("display").getString("Name"))); + + // Lore + NbtList loreList = nbttag.getCompound("display").getList("Lore", 8); + stack.set(DataComponentTypes.LORE, new LoreComponent(loreList.stream() + .map(NbtElement::asString) + .map(TextTransformer::fromLegacy) + .collect(Collectors.toList()))); + + // add skull texture + NbtList texture = nbttag.getCompound("SkullOwner").getCompound("Properties").getList("textures", 10); + if (!texture.isEmpty()) { + stack.set(DataComponentTypes.PROFILE, new ProfileComponent(Optional.of(internalName), Optional.of(UUID.fromString(nbttag.getCompound("SkullOwner").get("Id").asString())), ItemUtils.propertyMapWithTexture(texture.getCompound(0).getString("Value")))); + } + + // Colour + if (nbttag.getCompound("display").contains("color")) { + int color = nbttag.getCompound("display").getInt("color"); + stack.set(DataComponentTypes.DYED_COLOR, new DyedColorComponent(color, false)); + } + + // add enchantment glint + if (nbttag.getKeys().contains("ench")) { + stack.set(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, true); + } + + // Hide weapon damage and other useless info + stack.set(DataComponentTypes.ATTRIBUTE_MODIFIERS, new AttributeModifiersComponent(List.of(), false)); + + // Set Count + stack.setCount(containerContent.getCompound(i).getInt("Count")); + + // Attach an override for Aaron's Mod so that these ItemStacks will work with the mod's features even when not in Skyblock + extraAttributes.put("aaron-mod", Util.make(new NbtCompound(), comp -> comp.putBoolean("alwaysDisplaySkyblockInfo", true))); + + stack.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(extraAttributes)); + + itemList.add(stack); + } + + return itemList; + } + + private static Identifier identifierFromOldId(int id, int damage) { + try { + return damage != 0 ? Identifier.of(ItemInstanceTheFlatteningFix.getItem(ItemIdFix.fromId(id), damage)) : Identifier.of(ItemIdFix.fromId(id)); + } catch (Exception e) { + return Identifier.of("air"); + } + } + + private static NbtList decompress(JsonObject data) { + try { + return NbtIo.readCompressed(new ByteArrayInputStream(Base64.getDecoder().decode(data.get("data").getAsString())), NbtSizeTracker.ofUnlimitedBytes()).getList("i", NbtElement.COMPOUND_TYPE); + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Failed to decompress item data", e); + } + return null; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/PetsInventoryItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/PetsInventoryItemLoader.java new file mode 100644 index 00000000..cd3b7a26 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/PetsInventoryItemLoader.java @@ -0,0 +1,42 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; +import de.hysky.skyblocker.skyblock.PetCache; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.Pet; +import net.minecraft.item.ItemStack; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class PetsInventoryItemLoader extends ItemLoader { + private static final List<String> TIER_ORDER = List.of("MYTHIC", "LEGENDARY", "EPIC", "RARE", "UNCOMMON", "COMMON"); + + @Override + public List<ItemStack> loadItems(JsonObject data) { + List<Pet> petList = new ArrayList<>(); + try { + JsonObject petsData = data.getAsJsonObject("pets_data"); + if (petsData != null && petsData.has("pets")) { + for (var petElement : petsData.get("pets").getAsJsonArray()) { + PetCache.PetInfo petInfo = PetCache.PetInfo.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(petElement.toString())).getOrThrow(); + petList.add(new Pet(petInfo)); + } + } + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Failed to load pets", e); + } + + // Sort pets by tier (in reverse order) and level (in reverse order) + petList.sort(Comparator.comparingInt((Pet pet) -> TIER_ORDER.indexOf(pet.getTierAsString())).reversed().thenComparingInt(Pet::getLevel).reversed()); + + List<ItemStack> itemList = new ArrayList<>(); + for (Pet pet : petList) { + itemList.add(pet.getIcon()); + } + return itemList; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java new file mode 100644 index 00000000..9d434726 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java @@ -0,0 +1,40 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import net.minecraft.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +public class WardrobeInventoryItemLoader extends ItemLoader { + private final int activeSlot; + private final JsonObject activeArmorSet; + + public WardrobeInventoryItemLoader(JsonObject inventory) { + this.activeSlot = inventory.get("wardrobe_equipped_slot").getAsInt(); + this.activeArmorSet = inventory.get("inv_armor").getAsJsonObject(); + } + + @Override + public List<ItemStack> loadItems(JsonObject data) { + List<ItemStack> itemList = new ArrayList<>(); + + try { + itemList.addAll(super.loadItems(data)); + if (activeSlot != -1) { + List<ItemStack> activeArmour = super.loadItems(activeArmorSet).reversed(); + for (int i = 0; i < 4; i++) { + int baseIndex = activeSlot % 9; + int page = activeSlot / 9; + int slotIndex = (page * 36) + (i * 9) + baseIndex - 1; + itemList.set(slotIndex, activeArmour.get(i)); + } + } + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Failed to load wardrobe items", e); + } + + return itemList; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillWidget.java new file mode 100644 index 00000000..51f5e5f4 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillWidget.java @@ -0,0 +1,106 @@ +package de.hysky.skyblocker.skyblock.profileviewer.skills; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; +import de.hysky.skyblocker.skyblock.profileviewer.utils.ProfileViewerUtils; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.item.ItemStack; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class SkillWidget { + private final String SKILL_NAME; + private final LevelFinder.LevelInfo SKILL_LEVEL; + + private static final Identifier BAR_FILL = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_fill"); + private static final Identifier BAR_BACK = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_back"); + + private final ItemStack stack; + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final Map<String, ItemStack> SKILL_LOGO = Map.ofEntries( + Map.entry("Combat", Ico.STONE_SWORD), + Map.entry("Farming", Ico.GOLDEN_HOE), + Map.entry("Mining", Ico.STONE_PICKAXE), + Map.entry("Foraging", Ico.JUNGLE_SAPLING), + Map.entry("Fishing", Ico.FISH_ROD), + Map.entry("Enchanting", Ico.ENCHANTING_TABLE), + Map.entry("Alchemy", Ico.BREWING_STAND), + Map.entry("Taming", Ico.SPAWN_EGG), + Map.entry("Carpentry", Ico.CRAFTING_TABLE), + Map.entry("Catacombs", ProfileViewerUtils.createSkull("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHBzOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzliNTY4OTViOTY1OTg5NmFkNjQ3ZjU4NTk5MjM4YWY1MzJkNDZkYjljMWIwMzg5YjhiYmViNzA5OTlkYWIzM2QifX19")), + Map.entry("Runecraft", Ico.MAGMA_CREAM), + Map.entry("Social", Ico.EMERALD) + ); + private static final Map<String, Integer> SKILL_CAP = Map.ofEntries( + Map.entry("Combat", 60), + Map.entry("Farming", 60), + Map.entry("Mining", 60), + Map.entry("Foraging", 50), + Map.entry("Fishing", 50), + Map.entry("Enchanting", 60), + Map.entry("Alchemy", 50), + Map.entry("Taming", 60), + Map.entry("Carpentry", 50), + Map.entry("Catacombs", 50), + Map.entry("Runecraft", 25), + Map.entry("Social", 25) + ); + private static final Map<String, Integer> SOFT_SKILL_CAP = Map.of( + "Taming", 50, + "Farming", 50 + ); + + private static final Map<String, Integer> INFINITE = Map.of( + "Catacombs", 0 + ); + + public SkillWidget(String skill, long xp, int playerCap) { + this.SKILL_NAME = skill; + this.SKILL_LEVEL = LevelFinder.getLevelInfo(skill, xp); + if (SKILL_LEVEL.level >= SKILL_CAP.get(skill) && !INFINITE.containsKey(skill)) { + SKILL_LEVEL.fill = 1; + SKILL_LEVEL.level = SKILL_CAP.get(skill); + } + + this.stack = SKILL_LOGO.getOrDefault(skill, Ico.BARRIER); + if (playerCap != -1) { + this.SKILL_LEVEL.level = Math.min(SKILL_LEVEL.level, (SOFT_SKILL_CAP.get(this.SKILL_NAME) + playerCap)); + } + + } + + public void render(DrawContext context, int mouseX, int mouseY, int x, int y) { + context.drawItem(this.stack, x + 3, y + 2); + context.drawText(textRenderer, SKILL_NAME + " " + SKILL_LEVEL.level, x + 31, y + 2, Color.white.hashCode(), false); + + Color fillColor = Color.green; + if (SKILL_LEVEL.level >= SKILL_CAP.get(SKILL_NAME)) { + fillColor = Color.MAGENTA; + } + + if ((SOFT_SKILL_CAP.containsKey(SKILL_NAME) && SKILL_LEVEL.level > SOFT_SKILL_CAP.get(SKILL_NAME)) && SKILL_LEVEL.level < SKILL_CAP.get(SKILL_NAME) && SKILL_LEVEL.fill == 1 || + (SKILL_NAME.equals("Taming") && SKILL_LEVEL.level >= SOFT_SKILL_CAP.get(SKILL_NAME))) { + fillColor = Color.YELLOW; + } + + context.drawGuiTexture(BAR_BACK, x + 30, y + 12, 75, 6); + RenderHelper.renderNineSliceColored(context, BAR_FILL, x + 30, y + 12, (int) (75 * SKILL_LEVEL.fill), 6, fillColor); + + if (mouseX > x + 30 && mouseX < x + 105 && mouseY > y + 10 && mouseY < y + 19){ + List<Text> tooltipText = new ArrayList<>(); + tooltipText.add(Text.literal(this.SKILL_NAME).formatted(Formatting.GREEN)); + tooltipText.add(Text.literal("XP: " + ProfileViewerUtils.COMMA_FORMATTER.format(this.SKILL_LEVEL.xp)).formatted(Formatting.GOLD)); + context.drawTooltip(textRenderer, tooltipText, mouseX, mouseY); + } + } +}
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillsPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillsPage.java new file mode 100644 index 00000000..952e5620 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillsPage.java @@ -0,0 +1,93 @@ +package de.hysky.skyblocker.skyblock.profileviewer.skills; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.util.Identifier; + +import java.util.ArrayList; +import java.util.List; + +public class SkillsPage implements ProfileViewerPage { + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/icon_data_widget.png"); + private static final String[] SKILLS = {"Combat", "Mining", "Farming", "Foraging", "Fishing", "Enchanting", "Alchemy", "Taming", "Carpentry", "Catacombs", "Runecraft", "Social"}; + private static final int ROW_GAP = 28; + + private final JsonObject HYPIXEL_PROFILE; + private final JsonObject PLAYER_PROFILE; + + private final List<SkillWidget> skillWidgets = new ArrayList<>(); + private JsonObject skills; + + public SkillsPage(JsonObject hProfile, JsonObject pProfile) { + this.HYPIXEL_PROFILE = hProfile; + this.PLAYER_PROFILE = pProfile; + + try { + this.skills = this.PLAYER_PROFILE.getAsJsonObject("player_data").getAsJsonObject("experience"); + for (String skill : SKILLS) { + skillWidgets.add(new SkillWidget(skill, getSkillXP("SKILL_" + skill.toUpperCase()), getSkillCap(skill))); + } + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Error creating widgets.", e); + } + } + + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + int column2 = rootX + 113; + for (int i = 0; i < skillWidgets.size(); i++) { + int x = (i < 6) ? rootX : column2; + int y = rootY + (i % 6) * ROW_GAP; + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 26, 109, 26); + skillWidgets.get(i).render(context, mouseX, mouseY, x, y + 3); + } + } + + private int getSkillCap(String skill) { + try { + return switch (skill) { + case "Farming" -> this.PLAYER_PROFILE.getAsJsonObject("jacobs_contest").getAsJsonObject("perks").get("farming_level_cap").getAsInt(); + default -> -1; + }; + } catch (Exception e) { + return 0; + } + } + + private long getSkillXP(String skill) { + try { + return switch (skill) { + case "SKILL_CATACOMBS" -> getCatacombsXP(); + case "SKILL_SOCIAL" -> getCoopSocialXP(); + case "SKILL_RUNECRAFT" -> this.skills.get("SKILL_RUNECRAFTING").getAsLong(); + default -> this.skills.get(skill).getAsLong(); + }; + } catch (Exception e) { + return 0; + } + } + + private long getCatacombsXP() { + try { + JsonObject dungeonSkills = this.PLAYER_PROFILE.getAsJsonObject("dungeons").getAsJsonObject("dungeon_types"); + return dungeonSkills.getAsJsonObject("catacombs").get("experience").getAsLong(); + } catch (Exception e) { + return 0; + } + } + + private long getCoopSocialXP() { + long socialXP = 0; + JsonObject members = HYPIXEL_PROFILE.getAsJsonObject("members"); + for (String memberId : members.keySet()) { + try { + socialXP += members.getAsJsonObject(memberId).getAsJsonObject("player_data").getAsJsonObject("experience").get("SKILL_SOCIAL").getAsLong(); + } catch (Exception e) { + ProfileViewerScreen.LOGGER.warn("[Skyblocker Profile Viewer] Error calculating co-op social xp", e); + } + } + return socialXP; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayerWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayerWidget.java new file mode 100644 index 00000000..978c58c4 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayerWidget.java @@ -0,0 +1,105 @@ +package de.hysky.skyblocker.skyblock.profileviewer.slayers; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; +import de.hysky.skyblocker.skyblock.profileviewer.utils.ProfileViewerUtils; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.item.ItemStack; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class SlayerWidget { + private final String slayerName; + private final LevelFinder.LevelInfo slayerLevel; + private JsonObject slayerData = null; + + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/icon_data_widget.png"); + private static final Identifier BAR_FILL = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_fill"); + private static final Identifier BAR_BACK = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_back"); + private final Identifier item; + private final ItemStack drop; + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final Map<String, Identifier> HEAD_ICON = Map.ofEntries( + Map.entry("Zombie", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/zombie.png")), + Map.entry("Spider", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/spider.png")), + Map.entry("Wolf", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/wolf.png")), + Map.entry("Enderman", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/enderman.png")), + Map.entry("Vampire", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/vampire.png")), + Map.entry("Blaze", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/blaze.png")) + ); + + private static final Map<String, ItemStack> DROP_ICON = Map.ofEntries( + Map.entry("Zombie", Ico.FLESH), + Map.entry("Spider", Ico.STRING), + Map.entry("Wolf", Ico.MUTTON), + Map.entry("Enderman", Ico.E_PEARL), + Map.entry("Vampire", Ico.REDSTONE), + Map.entry("Blaze", Ico.B_POWDER) + ); + + public SlayerWidget(String slayer, long xp, JsonObject playerProfile) { + this.slayerName = slayer; + this.slayerLevel = LevelFinder.getLevelInfo(slayer, xp); + this.item = HEAD_ICON.get(slayer); + this.drop = DROP_ICON.getOrDefault(slayer, Ico.BARRIER); + try { + this.slayerData = playerProfile.getAsJsonObject("slayer").getAsJsonObject("slayer_bosses").getAsJsonObject(this.slayerName.toLowerCase()); + } catch (Exception ignored) {} + } + + public void render(DrawContext context, int mouseX, int mouseY, int x, int y) { + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 26, 109, 26); + context.drawTexture(this.item, x + 1, y + 3, 0, 0, 20, 20, 20, 20); + context.drawText(textRenderer, slayerName + " " + slayerLevel.level, x + 31, y + 5, Color.white.hashCode(), false); + + int col2 = x + 113; + context.drawTexture(TEXTURE, col2, y, 0, 0, 109, 26, 109, 26); + context.drawItem(this.drop, col2 + 3, y + 5); + context.drawText(textRenderer, "§aKills: §r" + findTotalKills(), col2 + 30, y + 4, Color.white.hashCode(), true); + context.drawText(textRenderer, findTopTierKills(), findTopTierKills().equals("No Data") ? col2 + 30 : col2 + 29, y + 15, Color.white.hashCode(), true); + + context.drawGuiTexture(BAR_BACK, x + 30, y + 15, 75, 6); + Color fillColor = slayerLevel.fill == 1 ? Color.MAGENTA : Color.green; + RenderHelper.renderNineSliceColored(context, BAR_FILL, x + 30, y + 15, (int) (75 * slayerLevel.fill), 6, fillColor); + + if (mouseX > x + 30 && mouseX < x + 105 && mouseY > y + 12 && mouseY < y + 22){ + List<Text> tooltipText = new ArrayList<>(); + tooltipText.add(Text.literal(this.slayerName).formatted(Formatting.GREEN)); + tooltipText.add(Text.literal("XP: " + ProfileViewerUtils.COMMA_FORMATTER.format(this.slayerLevel.xp)).formatted(Formatting.GOLD)); + context.drawTooltip(textRenderer, tooltipText, mouseX, mouseY); + } + } + + private int findTotalKills() { + try { + int totalKills = 0; + for (String key : this.slayerData.keySet()) { + if (key.startsWith("boss_kills_tier_")) totalKills += this.slayerData.get(key).getAsInt(); + } + return totalKills; + } catch (Exception e) { + return 0; + } + } + + private String findTopTierKills() { + try { + for (int tier = 4; tier >= 0; tier--) { + String key = "boss_kills_tier_" + tier; + if (this.slayerData.has(key)) return "§cT" + (tier + 1) + " Kills: §r" + this.slayerData.get(key).getAsInt(); + } + } catch (Exception ignored) {} + return "No Data"; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayersPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayersPage.java new file mode 100644 index 00000000..528bd3ac --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayersPage.java @@ -0,0 +1,41 @@ +package de.hysky.skyblocker.skyblock.profileviewer.slayers; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import net.minecraft.client.gui.DrawContext; + +import java.util.ArrayList; +import java.util.List; + +public class SlayersPage implements ProfileViewerPage { + private static final String[] SLAYERS = {"Zombie", "Spider", "Wolf", "Enderman", "Vampire", "Blaze"}; + private static final int ROW_GAP = 28; + + private final List<SlayerWidget> slayerWidgets = new ArrayList<>(); + + public SlayersPage(JsonObject pProfile) { + try { + for (String slayer : SLAYERS) { + slayerWidgets.add(new SlayerWidget(slayer, getSlayerXP(slayer.toLowerCase(), pProfile), pProfile)); + } + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Error creating slayer widgets", e); + } + } + + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + for (int i = 0; i < slayerWidgets.size(); i++) { + slayerWidgets.get(i).render(context, mouseX, mouseY, rootX, rootY + i * ROW_GAP); + } + } + + private long getSlayerXP(String slayer, JsonObject pProfile) { + try { + return pProfile.getAsJsonObject("slayer").getAsJsonObject("slayer_bosses") + .getAsJsonObject(slayer).get("xp").getAsLong(); + } catch (Exception e) { + return 0; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/LevelFinder.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/LevelFinder.java new file mode 100644 index 00000000..f53df0f5 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/LevelFinder.java @@ -0,0 +1,316 @@ +package de.hysky.skyblocker.skyblock.profileviewer.utils; + +import java.util.ArrayList; +import java.util.List; + +public class LevelFinder { + public static class LevelInfo { + public long xp; + public int level; + public double fill; + public long levelXP; + public long nextLevelXP; + + public LevelInfo(long xp, int level) { + this.xp = xp; + this.level = level; + } + + public LevelInfo(long xp, int level, double fill, double levelXP, double nextLevelXP) { + this.xp = xp; + this.level = level; + this.fill = fill; + this.levelXP = (long) levelXP; + this.nextLevelXP = (long) nextLevelXP; + } + } + + private static final long CATA_XP_PER_LEVEL = 200_000_000; + private static final List<LevelInfo> GENERIC_SKILL_BOUNDARIES = createGenericSkillBoundaries(); + private static final List<LevelInfo> CATACOMBS_SKILL_BOUNDARIES = createCatacombsSkillBoundaries(); + private static final List<LevelInfo> RUNECRAFT_SKILL_BOUNDARIES = createRunecraftSkillBoundaries(); + private static final List<LevelInfo> SOCIAL_SKILL_BOUNDARIES = createSocialSkillBoundaries(); + + private static final List<LevelInfo> COMMON_PET_BOUNDARIES = createCommonPetBoundaries(); + private static final List<LevelInfo> UNCOMMON_PET_BOUNDARIES = createUncommonPetBoundaries(); + private static final List<LevelInfo> RARE_PET_BOUNDARIES = createRarePetBoundaries(); + private static final List<LevelInfo> EPIC_PET_BOUNDARIES = createEpicPetBoundaries(); + private static final List<LevelInfo> LEGENDARY_PET_BOUNDARIES = createLegendaryPetBoundaries(); + + + private static final List<LevelInfo> GENERIC_SLAYER_BOUNDARIES = createGenericSlayerBoundaries(); + private static final List<LevelInfo> VAMPIRE_SLAYER_BOUNDARIES = createVampireSlayerBoundaries(); + + private static List<LevelInfo> createGenericSkillBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 50L, 175L, 375L, 675L, 1175L, 1925L, 2925L, 4425L, 6425L, + 9925L, 14925L, 22425L, 32425L, 47425L, 67425L, 97425L, 147425L, + 222425L, 322425L, 522425L, 822425L, 1222425L, 1722425L, 2322425L, + 3022425L, 3822425L, 4722425L, 5722425L, 6822425L, 8022425L, + 9322425L, 10722425L, 12222425L, 13822425L, 15522425L, 17322425L, + 19222425L, 21222425L, 23322425L, 25522425L, 27822425L, 30222425L, + 32722425L, 35322425L, 38072425L, 40972425L, 44072425L, 47472425L, + 51172425L, 55172425L, 59472425L, 64072425L, 68972425L, 74172425L, + 79672425L, 85472425L, 91572425L, 97972425L, 104672425L, 111672425L + }; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + return boundaries; + } + + private static List<LevelInfo> createCatacombsSkillBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 50L, 125L, 235L, 395L, 625L, 955L, 1425L, 2095L, 3045L, + 4385L, 6275L, 8940L, 12700L, 17960L, 25340L, 35640L, 50040L, + 70040L, 97640L, 135640L, 188140L, 259640L, 356640L, 488640L, + 668640L, 911640L, 1239640L, 1684640L, 2284640L, 3084640L, + 4149640L, 5559640L, 7459640L, 9959640L, 13259640L, 17559640L, + 23159640L, 30359640L, 39359640L, 51359640L, 66359640L, 85359640L, + 109559640L, 139559640L, 177559640L, 225559640L, 295559640L, + 360559640L, 453559640L, 569809640L + }; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + return boundaries; + } + + private static List<LevelInfo> createRunecraftSkillBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 50L, 150L, 275L, 435L, 635L, 885L, 1200L, 1600L, 2100L, + 2725L, 3150L, 4510L, 5760L, 7325L, 9325L, 11825L, 14950L, + 18950L, 23950L, 30200L, 38050L, 47850L, 60100L, 75400L, 94500L + }; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + return boundaries; + } + + private static List<LevelInfo> createSocialSkillBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 50L, 150L, 300L, 550L, 1050L, 1800L, 2800L, 4050L, 5550L, + 7550L, 10050L, 13050L, 16800L, 21300L, 27300L, 35300L, 45300L, + 57800L, 72800L, 92800L, 117800L, 147800L, 182800L, 222800L, + 272800L + }; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + return boundaries; + } + + private static List<LevelInfo> createGenericSlayerBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = {0L, 5L, 15L, 200L, 1000L, 5000L,20000L,100000L,400000L,1000000L}; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + return boundaries; + } + + private static List<LevelInfo> createVampireSlayerBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = {0L, 20L, 75L, 240L, 840L, 2400L}; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + private static List<LevelInfo> createCommonPetBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 0L, 100L, 210L, 330L, 460L, 605L, 765L, 940L, 1130L, 1340L, 1570L, 1820L, 2095L, + 2395L, 2725L, 3085L, 3485L, 3925L, 4415L, 4955L, 5555L, 6215L, 6945L, 7745L, + 8625L, 9585L, 10635L, 11785L, 13045L, 14425L, 15935L, 17585L, 19385L, 21345L, + 23475L, 25785L, 28285L, 30985L, 33905L, 37065L, 40485L, 44185L, 48185L, 52535L, + 57285L, 62485L, 68185L, 74485L, 81485L, 89285L, 97985L, 107685L, 118485L, 130485L, + 143785L, 158485L, 174685L, 192485L, 211985L, 233285L, 256485L, 281685L, 309085L, + 338885L, 371285L, 406485L, 444685L, 486085L, 530885L, 579285L, 631485L, 687685L, + 748085L, 812885L, 882285L, 956485L, 1035685L, 1120385L, 1211085L, 1308285L, + 1412485L, 1524185L, 1643885L, 1772085L, 1909285L, 2055985L, 2212685L, 2380385L, + 2560085L, 2752785L, 2959485L, 3181185L, 3418885L, 3673585L, 3946285L, 4237985L, + 4549685L, 4883385L, 5241085L, 5624785L + }; + + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + private static List<LevelInfo> createUncommonPetBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 0L, 175L, 365L, 575L, 805L, 1055L, 1330L, 1630L, 1960L, 2320L, 2720L, 3160L, + 3650L, 4190L, 4790L, 5450L, 6180L, 6980L, 7860L, 8820L, 9870L, 11020L, 12280L, + 13660L, 15170L, 16820L, 18620L, 20580L, 22710L, 25020L, 27520L, 30220L, 33140L, + 36300L, 39720L, 43420L, 47420L, 51770L, 56520L, 61720L, 67420L, 73720L, 80720L, + 88520L, 97220L, 106920L, 117720L, 129720L, 143020L, 157720L, 173920L, 191720L, + 211220L, 232520L, 255720L, 280920L, 308320L, 338120L, 370520L, 405720L, 443920L, + 485320L, 530120L, 578520L, 630720L, 686920L, 747320L, 812120L, 881520L, 955720L, + 1034920L, 1119620L, 1210320L, 1307520L, 1411720L, 1523420L, 1643120L, 1771320L, + 1908520L, 2055220L, 2211920L, 2379620L, 2559320L, 2752020L, 2958720L, 3180420L, + 3418120L, 3672820L, 3945520L, 4237220L, 4548920L, 4882620L, 5240320L, 5624020L, + 6035720L, 6477420L, 6954120L, 7470820L, 8032520L, 8644220L + }; + + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + private static List<LevelInfo> createRarePetBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 0L, 275L, 575L, 905L, 1265L, 1665L, 2105L, 2595L, 3135L, 3735L, 4395L, 5125L, + 5925L, 6805L, 7765L, 8815L, 9965L, 11225L, 12605L, 14115L, 15765L, 17565L, 19525L, + 21655L, 23965L, 26465L, 29165L, 32085L, 35245L, 38665L, 42365L, 46365L, 50715L, + 55465L, 60665L, 66365L, 72665L, 79665L, 87465L, 96165L, 105865L, 116665L, 128665L, + 141965L, 156665L, 172865L, 190665L, 210165L, 231465L, 254665L, 279865L, 307265L, + 337065L, 369465L, 404665L, 442865L, 484265L, 529065L, 577465L, 629665L, 685865L, + 746265L, 811065L, 880465L, 954665L, 1033865L, 1118565L, 1209265L, 1306465L, + 1410665L, 1522365L, 1642065L, 1770265L, 1907465L, 2054165L, 2210865L, 2378565L, + 2558265L, 2750965L, 2957665L, 3179365L, 3417065L, 3671765L, 3944465L, 4236165L, + 4547865L, 4881565L, 5239265L, 5622965L, 6034665L, 6476365L, 6953065L, 7469765L, + 8031465L, 8643165L, 9309865L, 10036565L, 10828265L, 11689965L, 12626665L + }; + + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + private static List<LevelInfo> createEpicPetBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 0L, 440L, 930L, 1470L, 2070L, 2730L, 3460L, 4260L, 5140L, 6100L, 7150L, 8300L, + 9560L, 10940L, 12450L, 14100L, 15900L, 17860L, 19990L, 22300L, 24800L, 27500L, 30420L, + 33580L, 37000L, 40700L, 44700L, 49050L, 53800L, 59000L, 64700L, 71000L, 78000L, 85800L, + 94500L, 104200L, 115000L, 127000L, 140300L, 155000L, 171200L, 189000L, 208500L, 229800L, + 253000L, 278200L, 305600L, 335400L, 367800L, 403000L, 441200L, 482600L, 527400L, 575800L, + 628000L, 684200L, 744600L, 809400L, 878800L, 953000L, 1032200L, 1116900L, 1207600L, 1304800L, + 1409000L, 1520700L, 1640400L, 1768600L, 1905800L, 2052500L, 2209200L, 2376900L, 2556600L, + 2749300L, 2956000L, 3177700L, 3415400L, 3670100L, 3942800L, 4234500L, 4546200L, 4879900L, + 5237600L, 5621300L, 6033000L, 6474700L, 6951400L, 7468100L, 8029800L, 8641500L, 9308200L, + 10034900L, 10826600L, 11688300L, 12625000L, 13641700L, 14743400L, 15935100L, 17221800L, + 18608500L + }; + + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + private static List<LevelInfo> createLegendaryPetBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + Long[] cumulativeXp = { + 0L, 0L, 660L, 1390L, 2190L, 3070L, 4030L, 5080L, 6230L, 7490L, + 8870L, 10380L, 12030L, 13830L, 15790L, 17920L, 20230L, 22730L, + 25430L, 28350L, 31510L, 34930L, 38630L, 42630L, 46980L, 51730L, + 56930L, 62630L, 68930L, 75930L, 83730L, 92430L, 102130L, 112930L, + 124930L, 138230L, 152930L, 169130L, 186930L, 206430L, 227730L, + 250930L, 276130L, 303530L, 333330L, 365730L, 400930L, 439130L, + 480530L, 525330L, 573730L, 625930L, 682130L, 742530L, 807330L, + 876730L, 950930L, 1030130L, 1114830L, 1205530L, 1302730L, 1406930L, + 1518630L, 1638330L, 1766530L, 1903730L, 2050430L, 2207130L, 2374830L, + 2554530L, 2747230L, 2953930L, 3175630L, 3413330L, 3668030L, 3940730L, + 4232430L, 4544130L, 4877830L, 5235530L, 5619230L, 6030930L, 6472630L, + 6949330L, 7466030L, 8027730L, 8639430L, 9306130L, 10032830L, 10824530L, + 11686230L, 12622930L, 13639630L, 14741330L, 15933030L, 17219730L, 18606430L, + 20103130L, 21719830L, 23466530L, 25353230L, 25353230L, 25358785L, 27245485L, + 29132185L, 31018885L, 32905585L, 34792285L, 36678985L, 38565685L, 40452385L, + 42339085L, 44225785L, 46112485L, 47999185L, 49885885L, 51772585L, 53659285L, + 55545985L, 57432685L, 59319385L, 61206085L, 63092785L, 64979485L, 66866185L, + 68752885L, 70639585L, 72526285L, 74412985L, 76299685L, 78186385L, 80073085L, + 81959785L, 83846485L, 85733185L, 87619885L, 89506585L, 91393285L, 93279985L, + 95166685L, 97053385L, 98940085L, 100826785L, 102713485L, 104600185L, 106486885L, + 108373585L, 110260285L, 112146985L, 114033685L, 115920385L, 117807085L, 119693785L, + 121580485L, 123467185L, 125353885L, 127240585L, 129127285L, 131013985L, 132900685L, + 134787385L, 136674085L, 138560785L, 140447485L, 142334185L, 144220885L, 146107585L, + 147994285L, 149880985L, 151767685L, 153654385L, 155541085L, 157427785L, 159314485L, + 161201185L, 163087885L, 164974585L, 166861285L, 168747985L, 170634685L, 172521385L, + 174408085L, 176294785L, 178181485L, 180068185L, 181954885L, 183841585L, 185728285L, + 187614985L, 189501685L, 191388385L, 193275085L, 195161785L, 197048485L, 198935185L, + 200821885L, 202708585L, 204595285L, 206481985L, 208368685L, 210255385L + }; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + public static LevelInfo getLevelInfo(String name, long xp) { + List<LevelInfo> boundaries = getLevelBoundaries(name, xp); + for (int i = boundaries.size() - 1; i >= 0 ; i--) { + if (xp >= boundaries.get(i).xp) { + double fill; + double xpInCurrentLevel; + double levelXPRange; + if (i < boundaries.getLast().level) { + double currentLevelXP = boundaries.get(i).xp; + double nextLevelXP = boundaries.get(i + 1).xp; + levelXPRange = nextLevelXP - currentLevelXP; + xpInCurrentLevel = xp - currentLevelXP; + fill = xpInCurrentLevel / levelXPRange; + } else { + fill = 1.0; + xpInCurrentLevel = xp - boundaries.getLast().xp; + levelXPRange = boundaries.getLast().xp - boundaries.get(boundaries.size()-2).xp; + } + return new LevelInfo(xp, boundaries.get(i).level, fill, xpInCurrentLevel, levelXPRange); + } + } + return new LevelInfo(0L, 0); + } + + + private static List<LevelInfo> getLevelBoundaries(String levelName, long xp) { + return switch (levelName) { + case "Vampire" -> VAMPIRE_SLAYER_BOUNDARIES; + case "Zombie", "Spider", "Wolf", "Enderman", "Blaze" -> GENERIC_SLAYER_BOUNDARIES; + case "PET_COMMON" -> COMMON_PET_BOUNDARIES; + case "PET_UNCOMMON" -> UNCOMMON_PET_BOUNDARIES; + case "PET_RARE" -> RARE_PET_BOUNDARIES; + case "PET_EPIC" -> EPIC_PET_BOUNDARIES; + case "PET_LEGENDARY", "PET_MYTHIC" -> LEGENDARY_PET_BOUNDARIES.subList(0,101); + case "PET_GREG" -> LEGENDARY_PET_BOUNDARIES; + case "Social" -> SOCIAL_SKILL_BOUNDARIES; + case "Runecraft" -> RUNECRAFT_SKILL_BOUNDARIES; + case "Catacombs" -> calculateCatacombsSkillBoundaries(xp); + default -> GENERIC_SKILL_BOUNDARIES; + }; + } + + private static List<LevelInfo> calculateCatacombsSkillBoundaries(long xp) { + if (xp >= CATACOMBS_SKILL_BOUNDARIES.getLast().xp) { + int additionalLevels = (int) ((xp - CATACOMBS_SKILL_BOUNDARIES.getLast().xp) / CATA_XP_PER_LEVEL) ; + + List<LevelInfo> updatedBoundaries = new ArrayList<>(CATACOMBS_SKILL_BOUNDARIES); + for (int i = 0; i <= additionalLevels; i++) { + int level = CATACOMBS_SKILL_BOUNDARIES.getLast().level + i + 1; + long nextLevelXP = updatedBoundaries.getLast().xp + CATA_XP_PER_LEVEL; + updatedBoundaries.add(new LevelInfo(nextLevelXP, level)); + } + + return updatedBoundaries; + } + + return CATACOMBS_SKILL_BOUNDARIES; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/ProfileViewerUtils.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/ProfileViewerUtils.java new file mode 100644 index 00000000..8dadedaf --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/ProfileViewerUtils.java @@ -0,0 +1,43 @@ +package de.hysky.skyblocker.skyblock.profileviewer.utils; + +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.ProfileComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; + +import java.text.NumberFormat; +import java.util.Locale; +import java.util.Optional; +import java.util.UUID; + +public class ProfileViewerUtils { + public static final NumberFormat COMMA_FORMATTER = NumberFormat.getNumberInstance(Locale.US); + + public static ItemStack createSkull(String textureB64) { + ItemStack skull = new ItemStack(Items.PLAYER_HEAD); + try { + PropertyMap map = new PropertyMap(); + map.put("textures", new Property("textures", textureB64)); + ProfileComponent profile = new ProfileComponent(Optional.of("skull"), Optional.of(UUID.randomUUID()), map); + skull.set(DataComponentTypes.PROFILE, profile); + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Failed to create skull", e); + } + return skull; + } + + public static String numLetterFormat(double amount) { + if (amount >= 1_000_000_000) { + return String.format("%.4gB", amount / 1_000_000_000); + } else if (amount >= 1_000_000) { + return String.format("%.4gM", amount / 1_000_000); + } else if (amount >= 1_000) { + return String.format("%.4gK", amount / 1_000); + } else { + return String.valueOf((int)(amount)); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SubPageSelectButton.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SubPageSelectButton.java new file mode 100644 index 00000000..8398747d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SubPageSelectButton.java @@ -0,0 +1,65 @@ +package de.hysky.skyblocker.skyblock.profileviewer.utils; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ButtonTextures; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.LoreComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; + +import java.awt.*; + +public class SubPageSelectButton extends ClickableWidget { + private final ProfileViewerPage page; + private final int index; + private boolean toggled; + + private static final ButtonTextures TEXTURES = new ButtonTextures(Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/button_icon_toggled.png"), Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/button_icon.png"), Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/button_icon_toggled_highlighted.png"), Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/button_icon_highlighted.png")); + private final ItemStack ICON; + + public SubPageSelectButton(ProfileViewerPage page, int x, int y, int index, ItemStack item) { + super(x, y, 22, 22, item.getName()); + this.ICON = item; + this.toggled = index == 0; + this.index = index; + this.page = page; + visible = false; + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + context.fill(this.getX(), this.getY(), this.getX() + 20, this.getY() + 20, Color.BLACK.getRGB()); + context.drawTexture(TEXTURES.get(toggled, (mouseX > getX() && mouseX < getX() + 19 && mouseY > getY() && mouseY < getY() + 19)), this.getX() + 1, this.getY() + 1,0, 0, 18, 18, 18, 18); + context.drawItem(ICON, this.getX() + 2, this.getY() + 2); + if ((mouseX > getX() + 1 && mouseX < getX() + 19 && mouseY > getY() + 1 && mouseY < getY() + 19)) { + LoreComponent lore = ICON.get(DataComponentTypes.LORE); + if (lore != null) context.drawTooltip(MinecraftClient.getInstance().textRenderer, lore.lines(), mouseX, mouseY + 10); + } + } + + @Override + protected boolean clicked(double mouseX, double mouseY) { + return this.active && this.visible &&(mouseX > getX() + 1 && mouseX < getX() + 19 && mouseY > getY() + 1 && mouseY < getY() + 19); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} + + public void setToggled(boolean toggled) { + this.toggled = toggled; + } + + @Override + public void onClick(double mouseX, double mouseY) { + page.onNavButtonClick(this); + } + + public int getIndex() { + return index; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java index c2c952cf..dfde789e 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java @@ -5,6 +5,7 @@ import com.google.gson.reflect.TypeToken; import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.tree.CommandNode; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.utils.scheduler.Scheduler; @@ -97,7 +98,6 @@ public class Shortcuts { // Party commandArgs.put("/pa", "/p accept"); - commands.put("/pv", "/p leave"); commands.put("/pd", "/p disband"); commands.put("/rp", "/reparty"); @@ -157,9 +157,21 @@ public class Shortcuts { 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()))); + for (Map.Entry<String, String> set : commandArgs.entrySet()) { + if (set.getKey().startsWith("/")) { + CommandNode<FabricClientCommandSource> redirectLocation = dispatcher.getRoot(); + for (String word : set.getValue().substring(1).split(" ")) { + redirectLocation = redirectLocation.getChild(word); + if (redirectLocation == null) { + break; + } + } + if (redirectLocation == null) { + dispatcher.register(literal(set.getKey().substring(1)).then(argument("args", StringArgumentType.greedyString()))); + } + else { + dispatcher.register(literal(set.getKey().substring(1)).redirect(redirectLocation)); + } } } dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("help").executes(context -> { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/Ico.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/Ico.java index 818056f0..f182a949 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/Ico.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/Ico.java @@ -1,8 +1,11 @@ package de.hysky.skyblocker.skyblock.tabhud.util; +import net.minecraft.enchantment.Enchantment; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; +import static net.minecraft.enchantment.Enchantments.PROTECTION; + /** * Stores convenient shorthands for common ItemStack definitions */ @@ -10,23 +13,29 @@ public class Ico { public static final ItemStack MAP = new ItemStack(Items.FILLED_MAP); public static final ItemStack NTAG = new ItemStack(Items.NAME_TAG); public static final ItemStack EMERALD = new ItemStack(Items.EMERALD); + public static final ItemStack MAGMA_CREAM = new ItemStack(Items.MAGMA_CREAM); public static final ItemStack AMETHYST_SHARD = new ItemStack(Items.AMETHYST_SHARD); public static final ItemStack CLOCK = new ItemStack(Items.CLOCK); public static final ItemStack DIASWORD = new ItemStack(Items.DIAMOND_SWORD); public static final ItemStack DBUSH = new ItemStack(Items.DEAD_BUSH); public static final ItemStack VILLAGER = new ItemStack(Items.VILLAGER_SPAWN_EGG); + public static final ItemStack SPAWN_EGG = new ItemStack(Items.GHAST_SPAWN_EGG); public static final ItemStack MOREGOLD = new ItemStack(Items.GOLDEN_APPLE); public static final ItemStack COMPASS = new ItemStack(Items.COMPASS); public static final ItemStack SUGAR = new ItemStack(Items.SUGAR); - public static final ItemStack HOE = new ItemStack(Items.IRON_HOE); + public static final ItemStack IRON_HOE = new ItemStack(Items.IRON_HOE); + public static final ItemStack GOLDEN_HOE = new ItemStack(Items.GOLDEN_HOE); public static final ItemStack GOLD = new ItemStack(Items.GOLD_INGOT); + public static final ItemStack IRON = new ItemStack(Items.IRON_INGOT); public static final ItemStack BONE = new ItemStack(Items.BONE); public static final ItemStack SIGN = new ItemStack(Items.OAK_SIGN); public static final ItemStack FISH_ROD = new ItemStack(Items.FISHING_ROD); - public static final ItemStack SWORD = new ItemStack(Items.IRON_SWORD); + public static final ItemStack STONE_SWORD = new ItemStack(Items.STONE_SWORD); + public static final ItemStack IRON_SWORD = new ItemStack(Items.IRON_SWORD); public static final ItemStack LANTERN = new ItemStack(Items.LANTERN); public static final ItemStack COOKIE = new ItemStack(Items.COOKIE); public static final ItemStack POTION = new ItemStack(Items.POTION); + public static final ItemStack S_POTION = new ItemStack(Items.SPLASH_POTION); public static final ItemStack BARRIER = new ItemStack(Items.BARRIER); public static final ItemStack PLAYER = new ItemStack(Items.PLAYER_HEAD); public static final ItemStack WATER = new ItemStack(Items.WATER_BUCKET); @@ -37,6 +46,7 @@ public class Ico { public static final ItemStack STRING = new ItemStack(Items.STRING); public static final ItemStack WITHER = new ItemStack(Items.WITHER_SKELETON_SKULL); public static final ItemStack FLESH = new ItemStack(Items.ROTTEN_FLESH); + public static final ItemStack MUTTON = new ItemStack(Items.MUTTON); public static final ItemStack DRAGON = new ItemStack(Items.DRAGON_HEAD); public static final ItemStack DIAMOND = new ItemStack(Items.DIAMOND); public static final ItemStack ICE = new ItemStack(Items.ICE); @@ -46,31 +56,34 @@ public class Ico { public static final ItemStack BOOK = new ItemStack(Items.WRITABLE_BOOK); public static final ItemStack FURNACE = new ItemStack(Items.FURNACE); public static final ItemStack CHESTPLATE = new ItemStack(Items.IRON_CHESTPLATE); + public static final ItemStack L_CHESTPLATE = new ItemStack(Items.LEATHER_CHESTPLATE); public static final ItemStack B_ROD = new ItemStack(Items.BLAZE_ROD); + public static final ItemStack B_POWDER = new ItemStack(Items.BLAZE_POWDER); public static final ItemStack BOW = new ItemStack(Items.BOW); public static final ItemStack COPPER = new ItemStack(Items.COPPER_INGOT); public static final ItemStack NETHERITE_UPGRADE_ST = new ItemStack(Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE); public static final ItemStack COMPOSTER = new ItemStack(Items.COMPOSTER); public static final ItemStack SAPLING = new ItemStack(Items.OAK_SAPLING); public static final ItemStack SEEDS = new ItemStack(Items.WHEAT_SEEDS); - public static final ItemStack WHEAT = new ItemStack(Items.WHEAT); - public static final ItemStack CARROT = new ItemStack(Items.CARROT); - public static final ItemStack POTATO = new ItemStack(Items.POTATO); - public static final ItemStack SUGAR_CANE = new ItemStack(Items.SUGAR_CANE); - public static final ItemStack NETHER_WART = new ItemStack(Items.NETHER_WART); - public static final ItemStack MUSHROOM = new ItemStack(Items.RED_MUSHROOM); - public static final ItemStack CACTUS = new ItemStack(Items.CACTUS); - public static final ItemStack MELON = new ItemStack(Items.MELON); - public static final ItemStack PUMPKIN = new ItemStack(Items.PUMPKIN); - public static final ItemStack COCOA_BEANS = new ItemStack(Items.COCOA_BEANS); public static final ItemStack MILESTONE = new ItemStack(Items.LODESTONE); - public static final ItemStack PICKAXE = new ItemStack(Items.IRON_PICKAXE); + public static final ItemStack STONE_PICKAXE = new ItemStack(Items.STONE_PICKAXE); + public static final ItemStack IRON_PICKAXE = new ItemStack(Items.IRON_PICKAXE); public static final ItemStack NETHER_STAR = new ItemStack(Items.NETHER_STAR); public static final ItemStack HEART_OF_THE_SEA = new ItemStack(Items.HEART_OF_THE_SEA); public static final ItemStack EXPERIENCE_BOTTLE = new ItemStack(Items.EXPERIENCE_BOTTLE); public static final ItemStack PINK_DYE = new ItemStack(Items.PINK_DYE); public static final ItemStack LIME_DYE = new ItemStack(Items.LIME_DYE); + public static final ItemStack GRAY_DYE = new ItemStack(Items.GRAY_DYE); public static final ItemStack ENCHANTED_BOOK = new ItemStack(Items.ENCHANTED_BOOK); public static final ItemStack SPIDER_EYE = new ItemStack(Items.SPIDER_EYE); public static final ItemStack BLUE_ICE = new ItemStack(Items.BLUE_ICE); + public static final ItemStack JUNGLE_SAPLING = new ItemStack(Items.JUNGLE_SAPLING); + public static final ItemStack ENCHANTING_TABLE = new ItemStack(Items.ENCHANTING_TABLE); + public static final ItemStack BREWING_STAND = new ItemStack(Items.BREWING_STAND); + public static final ItemStack CRAFTING_TABLE = new ItemStack(Items.CRAFTING_TABLE); + public static final ItemStack PAINTING = new ItemStack(Items.PAINTING); + public static final ItemStack E_PEARL = new ItemStack(Items.ENDER_PEARL); + public static final ItemStack FEATHER = new ItemStack(Items.FEATHER); + public static final ItemStack E_CHEST = new ItemStack(Items.ENDER_CHEST); + public static final ItemStack MYCELIUM = new ItemStack(Items.MYCELIUM); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDeathWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDeathWidget.java index 9c299210..79193b51 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDeathWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDeathWidget.java @@ -38,7 +38,7 @@ public class DungeonDeathWidget extends Widget { this.addComponent(deaths); } - this.addSimpleIcoText(Ico.SWORD, "Damage Dealt:", Formatting.RED, 26); + this.addSimpleIcoText(Ico.IRON_SWORD, "Damage Dealt:", Formatting.RED, 26); this.addSimpleIcoText(Ico.POTION, "Healing Done:", Formatting.RED, 27); this.addSimpleIcoText(Ico.NTAG, "Milestone:", Formatting.YELLOW, 28); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ElectionWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ElectionWidget.java index ec935faf..8f50f9ff 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ElectionWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ElectionWidget.java @@ -32,10 +32,10 @@ public class ElectionWidget extends Widget { static { MAYOR_DATA.put("Aatrox", Ico.DIASWORD); - MAYOR_DATA.put("Cole", Ico.PICKAXE); + MAYOR_DATA.put("Cole", Ico.IRON_PICKAXE); MAYOR_DATA.put("Diana", Ico.BONE); MAYOR_DATA.put("Diaz", Ico.GOLD); - MAYOR_DATA.put("Finnegan", Ico.HOE); + MAYOR_DATA.put("Finnegan", Ico.IRON_HOE); MAYOR_DATA.put("Foxy", Ico.SUGAR); MAYOR_DATA.put("Paul", Ico.COMPASS); MAYOR_DATA.put("Scorpius", Ico.MOREGOLD); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenSkillsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenSkillsWidget.java index 75652b33..9e1f3989 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenSkillsWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenSkillsWidget.java @@ -82,14 +82,14 @@ public class GardenSkillsWidget extends Widget { Text speed = Widget.simpleEntryText(67, "SPD", Formatting.WHITE); IcoTextComponent spd = new IcoTextComponent(Ico.SUGAR, speed); Text farmfort = Widget.simpleEntryText(68, "FFO", Formatting.GOLD); - IcoTextComponent ffo = new IcoTextComponent(Ico.HOE, farmfort); + IcoTextComponent ffo = new IcoTextComponent(Ico.IRON_HOE, farmfort); TableComponent tc = new TableComponent(2, 1, Formatting.YELLOW.getColorValue()); tc.addToCell(0, 0, spd); tc.addToCell(1, 0, ffo); this.addComponent(tc); - this.addComponent(new IcoTextComponent(Ico.HOE, PlayerListMgr.textAt(70))); + this.addComponent(new IcoTextComponent(Ico.IRON_HOE, PlayerListMgr.textAt(70))); ProgressComponent pc2; Matcher milestoneMatcher = PlayerListMgr.regexAt(69, MS_PATTERN); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ReputationWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ReputationWidget.java index 3c218fb1..34d15a28 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ReputationWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ReputationWidget.java @@ -44,7 +44,7 @@ public class ReputationWidget extends Widget { if (fname.equals("Mage")) { faction = new IcoTextComponent(Ico.POTION, Text.literal(fname).formatted(Formatting.DARK_AQUA)); } else { - faction = new IcoTextComponent(Ico.SWORD, Text.literal(fname).formatted(Formatting.RED)); + faction = new IcoTextComponent(Ico.IRON_SWORD, Text.literal(fname).formatted(Formatting.RED)); } } this.addComponent(faction); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/SkillsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/SkillsWidget.java index 379fbb62..c9cf61aa 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/SkillsWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/SkillsWidget.java @@ -58,13 +58,13 @@ public class SkillsWidget extends Widget { Text speed = Widget.simpleEntryText(67, "SPD", Formatting.WHITE); IcoTextComponent spd = new IcoTextComponent(Ico.SUGAR, speed); Text strength = Widget.simpleEntryText(68, "STR", Formatting.RED); - IcoTextComponent str = new IcoTextComponent(Ico.SWORD, strength); + IcoTextComponent str = new IcoTextComponent(Ico.IRON_SWORD, strength); Text critDmg = Widget.simpleEntryText(69, "CCH", Formatting.BLUE); - IcoTextComponent cdg = new IcoTextComponent(Ico.SWORD, critDmg); + IcoTextComponent cdg = new IcoTextComponent(Ico.IRON_SWORD, critDmg); Text critCh = Widget.simpleEntryText(70, "CDG", Formatting.BLUE); - IcoTextComponent cch = new IcoTextComponent(Ico.SWORD, critCh); + IcoTextComponent cch = new IcoTextComponent(Ico.IRON_SWORD, critCh); Text aSpeed = Widget.simpleEntryText(71, "ASP", Formatting.YELLOW); - IcoTextComponent asp = new IcoTextComponent(Ico.HOE, aSpeed); + IcoTextComponent asp = new IcoTextComponent(Ico.IRON_HOE, aSpeed); TableComponent tc = new TableComponent(2, 3, Formatting.YELLOW.getColorValue()); tc.addToCell(0, 0, spd); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java index f8930882..11ec1b8d 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java @@ -1,28 +1,5 @@ package de.hysky.skyblocker.skyblock.waypoint; -import static com.mojang.brigadier.arguments.StringArgumentType.getString; -import static com.mojang.brigadier.arguments.StringArgumentType.word; -import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; -import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.util.Base64; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Semaphore; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; - -import org.slf4j.Logger; - import com.google.common.primitives.Floats; import com.google.gson.Gson; import com.google.gson.JsonParser; @@ -33,8 +10,8 @@ import com.mojang.logging.LogUtils; import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; import com.mojang.serialization.codecs.RecordCodecBuilder; - import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.skyblock.item.CustomArmorDyeColors; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.Utils; @@ -59,6 +36,28 @@ import net.minecraft.server.command.ServerCommandSource; import net.minecraft.text.Text; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Vec3d; +import org.slf4j.Logger; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Semaphore; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import static com.mojang.brigadier.arguments.StringArgumentType.getString; +import static com.mojang.brigadier.arguments.StringArgumentType.word; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; public class OrderedWaypoints { private static final Logger LOGGER = LogUtils.getLogger(); @@ -400,7 +399,7 @@ public class OrderedWaypoints { private int waypointIndex; OrderedWaypoint(BlockPos pos, float[] colorComponents) { - super(pos, Type.WAYPOINT, colorComponents); + super(pos, () -> SkyblockerConfigManager.get().uiAndVisuals.waypoints.waypointType, colorComponents); } private BlockPos getPos() { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Waypoints.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Waypoints.java index 18096117..6866aba3 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Waypoints.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Waypoints.java @@ -11,6 +11,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Location; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.scheduler.Scheduler; import de.hysky.skyblocker.utils.waypoint.WaypointCategory; @@ -21,11 +22,14 @@ import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.MinecraftClient; import net.minecraft.client.toast.SystemToast; +import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Base64; @@ -33,6 +37,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.function.Function; +import java.util.zip.GZIPInputStream; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; @@ -62,37 +67,42 @@ public class Waypoints { } } - public static List<WaypointCategory> fromSkytilsBase64(String base64, String defaultIsland) { + public static List<WaypointCategory> fromSkytils(String waypointsString, String defaultIsland) { try { - if (base64.startsWith("<Skytils-Waypoint-Data>(V")) { - int version = Integer.parseInt(base64.substring(26, base64.indexOf(')'))); + if (waypointsString.startsWith("<Skytils-Waypoint-Data>(V")) { + int version = Integer.parseInt(waypointsString.substring(25, waypointsString.indexOf(')'))); + waypointsString = waypointsString.substring(waypointsString.indexOf(':') + 1); if (version == 1) { - return fromSkytilsJson(new String(Base64.getDecoder().decode(base64.substring(base64.indexOf(':') + 1))), defaultIsland); + try (GZIPInputStream reader = new GZIPInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(waypointsString)))) { + return fromSkytilsJson(IOUtils.toString(reader, StandardCharsets.UTF_8), defaultIsland); + } } else { - LOGGER.error("[Skyblocker Waypoints] Unknown Skytils waypoint data version: " + version); + LOGGER.error("[Skyblocker Waypoints] Unknown Skytils waypoint data version: {}", version); } - } else return fromSkytilsJson(new String(Base64.getDecoder().decode(base64)), defaultIsland); + } else return fromSkytilsJson(new String(Base64.getDecoder().decode(waypointsString)), defaultIsland); } catch (NumberFormatException e) { LOGGER.error("[Skyblocker Waypoints] Encountered exception while parsing Skytils waypoint data version", e); - } catch (IllegalArgumentException e) { + } catch (Exception e) { LOGGER.error("[Skyblocker Waypoints] Encountered exception while decoding Skytils waypoint data", e); } return Collections.emptyList(); } - public static List<WaypointCategory> fromSkytilsJson(String waypointCategories, String defaultIsland) { + public static List<WaypointCategory> fromSkytilsJson(String waypointCategoriesString, String defaultIsland) { JsonArray waypointCategoriesJson; try { - waypointCategoriesJson = SkyblockerMod.GSON.fromJson(waypointCategories, JsonObject.class).getAsJsonArray("categories"); + waypointCategoriesJson = SkyblockerMod.GSON.fromJson(waypointCategoriesString, JsonObject.class).getAsJsonArray("categories"); } catch (JsonSyntaxException e) { + // Handle the case where there is only a single json list of waypoints and no category data. JsonObject waypointCategoryJson = new JsonObject(); waypointCategoryJson.addProperty("name", "New Category"); waypointCategoryJson.addProperty("island", defaultIsland); - waypointCategoryJson.add("waypoints", SkyblockerMod.GSON.fromJson(waypointCategories, JsonArray.class)); + waypointCategoryJson.add("waypoints", SkyblockerMod.GSON.fromJson(waypointCategoriesString, JsonArray.class)); waypointCategoriesJson = new JsonArray(); waypointCategoriesJson.add(waypointCategoryJson); } - return SKYTILS_CODEC.parse(JsonOps.INSTANCE, waypointCategoriesJson).resultOrPartial(LOGGER::error).orElseThrow(); + List<WaypointCategory> waypointCategories = SKYTILS_CODEC.parse(JsonOps.INSTANCE, waypointCategoriesJson).resultOrPartial(LOGGER::error).orElseThrow(); + return waypointCategories.stream().map(waypointCategory -> Location.from(waypointCategory.island()) == Location.UNKNOWN ? waypointCategory.withIsland(defaultIsland) : waypointCategory).toList(); } public static String toSkytilsBase64(List<WaypointCategory> waypointCategories) { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsShareScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsShareScreen.java index aee21ec8..eaca845a 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsShareScreen.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/WaypointsShareScreen.java @@ -31,7 +31,7 @@ public class WaypointsShareScreen extends AbstractWaypointsScreen<WaypointsScree GridWidget.Adder adder = gridWidget.createAdder(2); adder.add(ButtonWidget.builder(Text.translatable("skyblocker.waypoints.importWaypointsSkytils"), buttonImport -> { try { - List<WaypointCategory> waypointCategories = Waypoints.fromSkytilsBase64(client.keyboard.getClipboard(), island); + List<WaypointCategory> waypointCategories = Waypoints.fromSkytils(client.keyboard.getClipboard(), island); for (WaypointCategory waypointCategory : waypointCategories) { selectedWaypoints.addAll(waypointCategory.waypoints()); waypoints.put(waypointCategory.island(), waypointCategory); diff --git a/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java b/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java index fbf814ee..5f65c336 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java +++ b/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java @@ -7,6 +7,8 @@ import java.util.Base64; import java.util.Objects; import java.util.UUID; +import de.hysky.skyblocker.mixins.accessors.MinecraftClientAccessor; +import net.minecraft.client.session.ProfileKeys; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -56,8 +58,9 @@ public class ApiAuthentication { * was generated by Mojang and is tied to said player. For information about what the randomly signed data is used for and why see {@link #getRandomSignedData(PrivateKey)} */ private static void updateToken() { + ProfileKeys profileKeys = ((MinecraftClientAccessor) CLIENT).getProfileKeys(); //The fetching runs async in ProfileKeysImpl#getKeyPair - CLIENT.getProfileKeys().fetchKeyPair().thenAcceptAsync(playerKeypairOpt -> { + profileKeys.fetchKeyPair().thenAcceptAsync(playerKeypairOpt -> { if (playerKeypairOpt.isPresent()) { PlayerKeyPair playerKeyPair = playerKeypairOpt.get(); diff --git a/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java b/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java index c63af3ba..93e314a7 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java @@ -1,16 +1,14 @@ package de.hysky.skyblocker.utils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.google.gson.JsonParser; import com.mojang.util.UndashedUuid; - import de.hysky.skyblocker.utils.Http.ApiResponse; import de.hysky.skyblocker.utils.scheduler.Scheduler; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.minecraft.client.MinecraftClient; import net.minecraft.client.session.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /* * Contains only basic helpers for using Http APIs diff --git a/src/main/java/de/hysky/skyblocker/utils/Http.java b/src/main/java/de/hysky/skyblocker/utils/Http.java index 361bab14..051bd52e 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Http.java +++ b/src/main/java/de/hysky/skyblocker/utils/Http.java @@ -1,5 +1,10 @@ package de.hysky.skyblocker.utils; +import de.hysky.skyblocker.SkyblockerMod; +import net.minecraft.SharedConstants; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -16,12 +21,6 @@ import java.time.Duration; import java.util.zip.GZIPInputStream; import java.util.zip.InflaterInputStream; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import de.hysky.skyblocker.SkyblockerMod; -import net.minecraft.SharedConstants; - /** * @implNote All http requests are sent using HTTP 2 */ @@ -34,7 +33,7 @@ public class Http { .followRedirects(Redirect.NORMAL) .build(); - private static ApiResponse sendCacheableGetRequest(String url, @Nullable String token) throws IOException, InterruptedException { + public static ApiResponse sendCacheableGetRequest(String url, @Nullable String token) throws IOException, InterruptedException { HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() .GET() .header("Accept", "application/json") @@ -67,9 +66,8 @@ public class Http { .build(); HttpResponse<InputStream> response = HTTP_CLIENT.send(request, BodyHandlers.ofInputStream()); - InputStream decodedInputStream = getDecodedInputStream(response); - return decodedInputStream; + return getDecodedInputStream(response); } public static String sendGetRequest(String url) throws IOException, InterruptedException { @@ -126,16 +124,12 @@ public class Http { String encoding = getContentEncoding(response.headers()); try { - switch (encoding) { - case "": - return response.body(); - case "gzip": - return new GZIPInputStream(response.body()); - case "deflate": - return new InflaterInputStream(response.body()); - default: - throw new UnsupportedOperationException("The server sent content in an unexpected encoding: " + encoding); - } + return switch (encoding) { + case "" -> response.body(); + case "gzip" -> new GZIPInputStream(response.body()); + case "deflate" -> new InflaterInputStream(response.body()); + default -> throw new UnsupportedOperationException("The server sent content in an unexpected encoding: " + encoding); + }; } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java index de7a0f9e..65807886 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java @@ -10,9 +10,9 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; import com.mojang.serialization.codecs.RecordCodecBuilder; -import de.hysky.skyblocker.skyblock.item.tooltip.adders.ObtainedDateTooltip; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType; +import de.hysky.skyblocker.skyblock.item.tooltip.adders.ObtainedDateTooltip; import it.unimi.dsi.fastutil.doubles.DoubleBooleanPair; import it.unimi.dsi.fastutil.ints.IntIntPair; import it.unimi.dsi.fastutil.longs.LongBooleanPair; @@ -43,7 +43,7 @@ import java.util.regex.Pattern; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; -public class ItemUtils { +public final class ItemUtils { public static final String ID = "id"; public static final String UUID = "uuid"; public static final Pattern NOT_DURABILITY = Pattern.compile("[^0-9 /]"); @@ -55,6 +55,8 @@ public class ItemUtils { ComponentChanges.CODEC.optionalFieldOf("components", ComponentChanges.EMPTY).forGetter(ItemStack::getComponentChanges) ).apply(instance, ItemStack::new))); + private ItemUtils() {} + public static LiteralArgumentBuilder<FabricClientCommandSource> dumpHeldItemCommand() { return literal("dumpHeldItem").executes(context -> { context.getSource().sendFeedback(Text.literal("[Skyblocker Debug] Held Item: " + SkyblockerMod.GSON_COMPACT.toJson(ItemStack.CODEC.encodeStart(JsonOps.INSTANCE, context.getSource().getPlayer().getMainHandStack()).getOrThrow()))); @@ -62,8 +64,13 @@ public class ItemUtils { }); } + /** + * Gets the nbt in the custom data component of the item stack. + * @return The {@link DataComponentTypes#CUSTOM_DATA custom data} of the itemstack, + * or an empty {@link NbtCompound} if the itemstack is missing a custom data component + */ @SuppressWarnings("deprecation") - public static NbtCompound getCustomData(@NotNull ComponentHolder stack) { + public static @NotNull NbtCompound getCustomData(@NotNull ComponentHolder stack) { return stack.getOrDefault(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT).getNbt(); } @@ -73,7 +80,7 @@ public class ItemUtils { * @param stack the item stack to get the internal name from * @return an optional containing the internal name of the item stack */ - public static Optional<String> getItemIdOptional(@NotNull ItemStack stack) { + public static @NotNull Optional<String> getItemIdOptional(@NotNull ItemStack stack) { NbtCompound customData = getCustomData(stack); return customData.contains(ID) ? Optional.of(customData.getString(ID)) : Optional.empty(); } @@ -84,7 +91,7 @@ public class ItemUtils { * @param stack the item stack to get the internal name from * @return the internal name of the item stack, or an empty string if the item stack is null or does not have an internal name */ - public static String getItemId(@NotNull ItemStack stack) { + public static @NotNull String getItemId(@NotNull ItemStack stack) { return getCustomData(stack).getString(ID); } @@ -94,7 +101,7 @@ public class ItemUtils { * @param stack the item stack to get the UUID from * @return an optional containing the UUID of the item stack */ - public static Optional<String> getItemUuidOptional(@NotNull ItemStack stack) { + public static @NotNull Optional<String> getItemUuidOptional(@NotNull ItemStack stack) { NbtCompound customData = getCustomData(stack); return customData.contains(UUID) ? Optional.of(customData.getString(UUID)) : Optional.empty(); } @@ -105,7 +112,7 @@ public class ItemUtils { * @param stack the item stack to get the UUID from * @return the UUID of the item stack, or an empty string if the item stack is null or does not have a UUID */ - public static String getItemUuid(@NotNull ComponentHolder stack) { + public static @NotNull String getItemUuid(@NotNull ComponentHolder stack) { return getCustomData(stack).getString(UUID); } @@ -115,7 +122,7 @@ public class ItemUtils { * @return An {@link LongBooleanPair} with the {@code left long} representing the item's price, * and the {@code right boolean} indicating if the price was based on complete data. */ - public static DoubleBooleanPair getItemPrice(@NotNull ItemStack stack) { + public static @NotNull DoubleBooleanPair getItemPrice(@NotNull ItemStack stack) { return getItemPrice(getItemId(stack)); } @@ -125,7 +132,7 @@ public class ItemUtils { * @return An {@link LongBooleanPair} with the {@code left long} representing the item's price, * and the {@code right boolean} indicating if the price was based on complete data. */ - public static DoubleBooleanPair getItemPrice(@Nullable String id) { + public static @NotNull DoubleBooleanPair getItemPrice(@Nullable String id) { JsonObject bazaarPrices = TooltipInfoType.BAZAAR.getData(); JsonObject lowestBinPrices = TooltipInfoType.LOWEST_BINS.getData(); @@ -168,15 +175,15 @@ public class ItemUtils { public static boolean hasCustomDurability(@NotNull ItemStack stack) { NbtCompound customData = getCustomData(stack); - return customData != null && (customData.contains("drill_fuel") || customData.getString(ID).equals("PICKONIMBUS")); + return !customData.isEmpty() && (customData.contains("drill_fuel") || customData.getString(ID).equals("PICKONIMBUS")); } @Nullable public static IntIntPair getDurability(@NotNull ItemStack stack) { NbtCompound customData = getCustomData(stack); - if (customData == null) return null; + if (customData.isEmpty()) return null; - // TODO Calculate drill durability based on the drill_fuel flag, fuel_tank flag, and hotm level + // TODO Calculate drill durability based on the drill_fuel flag, fuel_tank flag, and hotm level // TODO Cache the max durability and only update the current durability on inventory tick int pickonimbusDurability = customData.getInt("pickonimbus_durability"); @@ -193,9 +200,13 @@ public class ItemUtils { return null; } + /** + * Gets the first line of the lore that matches the specified predicate. + * @return The first line of the lore that matches the predicate, or {@code null} if no line matches. + */ @Nullable - public static String getLoreLineIf(ItemStack item, Predicate<String> predicate) { - for (Text line : getLore(item)) { + public static String getLoreLineIf(ItemStack stack, Predicate<String> predicate) { + for (Text line : getLore(stack)) { String string = line.getString(); if (predicate.test(string)) { return string; @@ -205,28 +216,47 @@ public class ItemUtils { return null; } + /** + * Gets the first line of the lore that matches the specified pattern, using {@link Matcher#matches()}. + * @return A matcher that contains match results if the pattern was found in the lore, otherwise {@code null}. + */ @Nullable - public static Matcher getLoreLineIfMatch(ItemStack item, Pattern pattern) { - for (Text line : getLore(item)) { - String string = line.getString(); - Matcher matcher = pattern.matcher(string); - if (matcher.matches()) { + public static Matcher getLoreLineIfMatch(ItemStack stack, Pattern pattern) { + Matcher matcher = pattern.matcher(""); + for (Text line : getLore(stack)) { + if (matcher.reset(line.getString()).matches()) { return matcher; } } - return null; } - public static List<Text> getLore(ItemStack item) { - return item.getOrDefault(DataComponentTypes.LORE, LoreComponent.DEFAULT).styledLines(); + /** + * Gets the first line of the lore that matches the specified pattern, using {@link Matcher#find()}. + * @param pattern the pattern to search for + * @param stack the stack to search the lore of + * @return A {@link Matcher matcher} that contains match results if the pattern was found in the lore, otherwise {@code null}. + */ + @Nullable + public static Matcher getLoreLineIfContainsMatch(ItemStack stack, Pattern pattern) { + Matcher matcher = pattern.matcher(""); + for (Text line : getLore(stack)) { + if (matcher.reset(line.getString()).find()) { + return matcher; + } + } + return null; + } + + public static @NotNull List<Text> getLore(ItemStack stack) { + return stack.getOrDefault(DataComponentTypes.LORE, LoreComponent.DEFAULT).styledLines(); } - public static PropertyMap propertyMapWithTexture(String textureValue) { + public static @NotNull PropertyMap propertyMapWithTexture(String textureValue) { return Codecs.GAME_PROFILE_PROPERTY_MAP.parse(JsonOps.INSTANCE, JsonParser.parseString("[{\"name\":\"textures\",\"value\":\"" + textureValue + "\"}]")).getOrThrow(); } - public static String getHeadTexture(ItemStack stack) { + public static @NotNull String getHeadTexture(@NotNull ItemStack stack) { if (!stack.isOf(Items.PLAYER_HEAD) || !stack.contains(DataComponentTypes.PROFILE)) return ""; ProfileComponent profile = stack.get(DataComponentTypes.PROFILE); @@ -238,13 +268,13 @@ public class ItemUtils { .orElse(""); } - public static Optional<String> getHeadTextureOptional(ItemStack stack) { + public static @NotNull Optional<String> getHeadTextureOptional(ItemStack stack) { String texture = getHeadTexture(stack); if (texture.isBlank()) return Optional.empty(); return Optional.of(texture); } - public static ItemStack getSkyblockerStack() { + public static @NotNull ItemStack getSkyblockerStack() { try { ItemStack stack = new ItemStack(Items.PLAYER_HEAD); stack.set(DataComponentTypes.PROFILE, new ProfileComponent(Optional.of("SkyblockerStack"), Optional.of(java.util.UUID.randomUUID()), propertyMapWithTexture("e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDdjYzY2ODc0MjNkMDU3MGQ1NTZhYzUzZTA2NzZjYjU2M2JiZGQ5NzE3Y2Q4MjY5YmRlYmVkNmY2ZDRlN2JmOCJ9fX0="))); diff --git a/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java b/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java index a786e79f..aa7a0492 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java @@ -4,7 +4,6 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import de.hysky.skyblocker.SkyblockerMod; import it.unimi.dsi.fastutil.objects.ObjectLongPair; -import net.minecraft.client.MinecraftClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,13 +16,24 @@ public class ProfileUtils { private static final long HYPIXEL_API_COOLDOWN = 300000; // 5min = 300000 public static Map<String, ObjectLongPair<JsonObject>> players = new HashMap<>(); - - public static CompletableFuture<JsonObject> updateProfile() { - return updateProfile(MinecraftClient.getInstance().getSession().getUsername()); + public static Map<String, ObjectLongPair<JsonObject>> profiles = new HashMap<>(); + + public static CompletableFuture<JsonObject> updateProfileByName(String name) { + return fetchFullProfile(name).thenApply(profile -> { + JsonObject player = profile.getAsJsonArray("profiles").asList().stream() + .map(JsonElement::getAsJsonObject) + .filter(profileObj -> profileObj.getAsJsonPrimitive("selected").getAsBoolean()) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No selected profile found!?")) + .getAsJsonObject("members").get(name).getAsJsonObject(); + + players.put(name, ObjectLongPair.of(player, System.currentTimeMillis())); + return player; + }); } - public static CompletableFuture<JsonObject> updateProfile(String name) { - ObjectLongPair<JsonObject> playerCache = players.get(name); + public static CompletableFuture<JsonObject> fetchFullProfile(String name) { + ObjectLongPair<JsonObject> playerCache = profiles.get(name); if (playerCache != null && playerCache.rightLong() + HYPIXEL_API_COOLDOWN > System.currentTimeMillis()) { return CompletableFuture.completedFuture(playerCache.left()); } @@ -32,19 +42,12 @@ public class ProfileUtils { String uuid = ApiUtils.name2Uuid(name); try (Http.ApiResponse response = Http.sendHypixelRequest("skyblock/profiles", "?uuid=" + uuid)) { if (!response.ok()) { - throw new IllegalStateException("Failed to get profile uuid for players " + name + "! Response: " + response.content()); + throw new IllegalStateException("Failed to get profile uuid for player: " + name + "! Response: " + response.content()); } - JsonObject responseJson = SkyblockerMod.GSON.fromJson(response.content(), JsonObject.class); - - JsonObject player = responseJson.getAsJsonArray("profiles").asList().stream() - .map(JsonElement::getAsJsonObject) - .filter(profile -> profile.getAsJsonPrimitive("selected").getAsBoolean()) - .findFirst() - .orElseThrow(() -> new IllegalStateException("No selected profile found!?")) - .getAsJsonObject("members").get(uuid).getAsJsonObject(); + JsonObject profile = SkyblockerMod.GSON.fromJson(response.content(), JsonObject.class); + profiles.put(name, ObjectLongPair.of(profile, System.currentTimeMillis())); - players.put(name, ObjectLongPair.of(player, System.currentTimeMillis())); - return player; + return profile; } catch (Exception e) { LOGGER.error("[Skyblocker Profile Utils] Failed to get Player Profile Data for players {}, is the API Down/Limited?", name, e); } diff --git a/src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java b/src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java index 045ecc4e..ec3effaf 100644 --- a/src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java +++ b/src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java @@ -21,10 +21,10 @@ public class SkyblockTime { public static void init() { updateTime(); //ScheduleCyclic already runs the task upon scheduling, so there's no need to call updateTime() here - Scheduler.INSTANCE.schedule(() -> Scheduler.INSTANCE.scheduleCyclic(SkyblockTime::updateTime, 1200 * 24), (int) (1200000 - (getSkyblockMillis() % 1200000)) / 50); + Scheduler.INSTANCE.schedule(() -> Scheduler.INSTANCE.scheduleCyclic(SkyblockTime::updateTime, 1200 * 20), (int) (1200000 - (getSkyblockMillis() % 1200000)) / 50); } - private static long getSkyblockMillis() { + public static long getSkyblockMillis() { return System.currentTimeMillis() - SKYBLOCK_EPOCH; } diff --git a/src/main/java/de/hysky/skyblocker/utils/TextTransformer.java b/src/main/java/de/hysky/skyblocker/utils/TextTransformer.java new file mode 100644 index 00000000..b8fb5101 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/TextTransformer.java @@ -0,0 +1,92 @@ +package de.hysky.skyblocker.utils; + +import org.jetbrains.annotations.NotNull; + +import it.unimi.dsi.fastutil.chars.CharList; +import net.minecraft.text.MutableText; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +/** + * Contains utilities for transforming text. These methods are from Aaron's Mod. + * + * @author AzureAaron + */ +public class TextTransformer { + private static final CharList FORMAT_CODES = CharList.of('4', 'c', '6', 'e', '2', 'a','b', '3', '1', '9', 'd', '5', 'f', '7', '8', '0', 'r', 'k', 'l', 'm', 'n', 'o'); + + /** + * Converts strings with section symbol/legacy formatting to MutableText objects. + * + * @param legacy The string with legacy formatting to be transformed + * @return A {@link MutableText} object matching the exact formatting of the input + * + * @author AzureAaron + */ + public static MutableText fromLegacy(@NotNull String legacy) { + MutableText newText = Text.empty(); + StringBuilder builder = new StringBuilder(); + Formatting formatting = null; + boolean bold = false; + boolean italic = false; + boolean underline = false; + boolean strikethrough = false; + boolean obfuscated = false; + + for (int i = 0; i < legacy.length(); i++) { + //If we've encountered a new formatting code then append the text from the previous "sequence" and reset state + if (i != 0 && legacy.charAt(i - 1) == '§' && FORMAT_CODES.contains(Character.toLowerCase(legacy.charAt(i))) && !builder.isEmpty()) { + newText.append(Text.literal(builder.toString()).setStyle(Style.EMPTY + .withColor(formatting) + .withBold(bold) + .withItalic(italic) + .withUnderline(underline) + .withStrikethrough(strikethrough) + .withObfuscated(obfuscated))); + + //Erase all characters in the builder so we can reuse it, also clear formatting + builder.delete(0, builder.length()); + formatting = null; + bold = false; + italic = false; + underline = false; + strikethrough = false; + obfuscated = false; + } + + if (i != 0 && legacy.charAt(i - 1) == '§') { + Formatting fmt = Formatting.byCode(legacy.charAt(i)); + + switch (fmt) { + case BOLD -> bold = true; + case ITALIC -> italic = true; + case UNDERLINE -> underline = true; + case STRIKETHROUGH -> strikethrough = true; + case OBFUSCATED -> obfuscated = true; + + default -> formatting = fmt; + } + + continue; + } + + //This character isn't the start of a formatting sequence or this character isn't part of a formatting sequence + if (legacy.charAt(i) != '§' && (i == 0 || (i != 0 && legacy.charAt(i - 1) != '§'))) { + builder.append(legacy.charAt(i)); + } + + // We've read the last character so append the last text with all of the formatting + if (i == legacy.length() - 1) { + newText.append(Text.literal(builder.toString()).setStyle(Style.EMPTY + .withColor(formatting) + .withBold(bold) + .withItalic(italic) + .withUnderline(underline) + .withStrikethrough(strikethrough) + .withObfuscated(obfuscated))); + } + } + return newText; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java index 84b3cb9e..bbeb11b5 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Utils.java +++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java @@ -3,30 +3,35 @@ package de.hysky.skyblocker.utils; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.mojang.util.UndashedUuid; - import de.hysky.skyblocker.events.SkyblockEvents; import de.hysky.skyblocker.mixins.accessors.MessageHandlerAccessor; import de.hysky.skyblocker.skyblock.item.MuseumItemCache; -import de.hysky.skyblocker.utils.scheduler.MessageScheduler; import de.hysky.skyblocker.utils.scheduler.Scheduler; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.azureaaron.hmapi.data.server.Environment; +import net.azureaaron.hmapi.events.HypixelPacketEvents; +import net.azureaaron.hmapi.network.HypixelNetworking; +import net.azureaaron.hmapi.network.packet.s2c.ErrorS2CPacket; +import net.azureaaron.hmapi.network.packet.s2c.HelloS2CPacket; +import net.azureaaron.hmapi.network.packet.s2c.HypixelS2CPacket; +import net.azureaaron.hmapi.network.packet.v1.s2c.LocationUpdateS2CPacket; 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.*; import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.util.Formatting; +import net.minecraft.util.Util; + +import org.apache.http.client.HttpResponseException; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; import java.time.Instant; import java.util.Collections; import java.util.List; @@ -46,7 +51,7 @@ public class Utils { private static boolean isOnSkyblock = false; private static boolean isInjected = false; /** - * Current Skyblock location (from /locraw) + * Current Skyblock location (from the Mod API) */ @NotNull private static Location location = Location.UNKNOWN; @@ -61,10 +66,12 @@ public class Utils { @NotNull private static String profileId = ""; /** - * The following fields store data returned from /locraw: {@link #server}, {@link #gameType}, {@link #locationRaw}, and {@link #map}. + * The following fields store data returned from the Mod API: {@link #environment}, {@link #server}, {@link #gameType}, {@link #locationRaw}, and {@link #map}. */ @SuppressWarnings("JavadocDeclaration") @NotNull + private static Environment environment = Environment.PRODUCTION; + @NotNull private static String server = ""; @NotNull private static String gameType = ""; @@ -72,12 +79,8 @@ public class Utils { private static String locationRaw = ""; @NotNull private static String map = ""; - private static long clientWorldJoinTime = 0; - private static boolean sentLocRaw = false; - private static boolean canSendLocRaw = false; - //This is required to prevent the location change event from being fired twice. - private static boolean locationChanged = true; - + private static boolean mayorTickScheduled = false; + private static int mayorTickRetryAttempts = 0; private static String mayor = ""; /** @@ -95,15 +98,15 @@ public class Utils { } public static boolean isInDungeons() { - return location == Location.DUNGEON || FabricLoader.getInstance().isDevelopmentEnvironment(); + return location == Location.DUNGEON; } public static boolean isInCrystalHollows() { - return location == Location.CRYSTAL_HOLLOWS || FabricLoader.getInstance().isDevelopmentEnvironment(); + return location == Location.CRYSTAL_HOLLOWS; } public static boolean isInDwarvenMines() { - return location == Location.DWARVEN_MINES || location == Location.GLACITE_MINESHAFT || FabricLoader.getInstance().isDevelopmentEnvironment(); + return location == Location.DWARVEN_MINES || location == Location.GLACITE_MINESHAFT; } public static boolean isInTheRift() { @@ -120,6 +123,9 @@ public class Utils { public static boolean isInKuudra() { return location == Location.KUUDRAS_HOLLOW; } + public static boolean isInCrimson() { + return location == Location.CRIMSON_ISLE; + } public static boolean isInModernForagingIsland() { return location == Location.MODERN_FORAGING_ISLAND; @@ -143,7 +149,7 @@ public class Utils { } /** - * @return the location parsed from /locraw. + * @return the location parsed from the Mod API. */ @NotNull public static Location getLocation() { @@ -151,7 +157,17 @@ public class Utils { } /** - * @return the server parsed from /locraw. + * Can be used to restrict features to being active only on the Alpha network. + * + * @return the current environment parsed from the Mod API. + */ + @NotNull + public static Environment getEnvironment() { + return environment; + } + + /** + * @return the server parsed from the Mod API. */ @NotNull public static String getServer() { @@ -159,7 +175,7 @@ public class Utils { } /** - * @return the game type parsed from /locraw. + * @return the game type parsed from the Mod API. */ @NotNull public static String getGameType() { @@ -167,7 +183,7 @@ public class Utils { } /** - * @return the location raw parsed from /locraw. + * @return the location raw parsed from the the Mod API. */ @NotNull public static String getLocationRaw() { @@ -175,7 +191,7 @@ public class Utils { } /** - * @return the map parsed from /locraw. + * @return the map parsed from the Mod API. */ @NotNull public static String getMap() { @@ -191,22 +207,30 @@ public class Utils { } public static void init() { - SkyblockEvents.JOIN.register(() -> tickMayorCache(false)); - ClientPlayConnectionEvents.JOIN.register(Utils::onClientWorldJoin); + SkyblockEvents.JOIN.register(() -> { + if (!mayorTickScheduled) { + tickMayorCache(); + scheduleMayorTick(); + mayorTickScheduled = true; + } + }); ClientReceiveMessageEvents.ALLOW_GAME.register(Utils::onChatMessage); ClientReceiveMessageEvents.GAME_CANCELED.register(Utils::onChatMessage); // Somehow this works even though onChatMessage returns a boolean - Scheduler.INSTANCE.scheduleCyclic(() -> tickMayorCache(true), 24_000, true); // Update every 20 minutes + + //Register Mod API stuff + HypixelNetworking.registerToEvents(Util.make(new Object2IntOpenHashMap<>(), map -> map.put(LocationUpdateS2CPacket.ID, 1))); + HypixelPacketEvents.HELLO.register(Utils::onPacket); + HypixelPacketEvents.LOCATION_UPDATE.register(Utils::onPacket); } /** - * Updates all the fields stored in this class from the sidebar, player list, and /locraw. + * Updates all the fields stored in this class from the sidebar, and player list. */ public static void update() { MinecraftClient client = MinecraftClient.getInstance(); updateScoreboard(client); updatePlayerPresenceFromScoreboard(client); updateFromPlayerList(client); - updateLocRaw(); } /** @@ -236,7 +260,7 @@ public class Utils { if (!isInjected) { isInjected = true; } - isOnSkyblock = true; + isOnSkyblock = true; //TODO in the future we can probably replace these skyblock checks entirely with the Mod API SkyblockEvents.JOIN.invoker().onSkyblockJoin(); } } else { @@ -252,7 +276,7 @@ public class Utils { 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"); + return (!serverAddress.isEmpty() && serverAddress.equalsIgnoreCase(ALTERNATE_HYPIXEL_ADDRESS)) || serverAddress.contains("hypixel.net") || serverAddress.contains("hypixel.io") || serverBrand.contains("Hypixel BungeeCord"); } private static void onLeaveSkyblock() { @@ -358,8 +382,8 @@ public class Utils { // TODO: Combine with `ChocolateFactorySolver.formatTime` and move into `SkyblockTime`. public static Text getDurationText(int timeInSeconds) { int seconds = timeInSeconds % 60; - int minutes = (timeInSeconds/60) % 60; - int hours = (timeInSeconds/3600); + int minutes = (timeInSeconds / 60) % 60; + int hours = (timeInSeconds / 3600); MutableText time = Text.empty(); if (hours > 0) { @@ -387,25 +411,39 @@ public class Utils { } } - public static void onClientWorldJoin(ClientPlayNetworkHandler handler, PacketSender sender, MinecraftClient client) { - clientWorldJoinTime = System.currentTimeMillis(); - resetLocRawInfo(); - } + private static void onPacket(HypixelS2CPacket packet) { + switch (packet) { + case HelloS2CPacket(var environment) -> { + Utils.environment = environment; + } - /** - * 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; - locationChanged = true; + case LocationUpdateS2CPacket(var serverName, var serverType, var _lobbyName, var mode, var map) -> { + Utils.server = serverName; + Utils.gameType = serverType.orElse(""); + Utils.locationRaw = mode.orElse(""); + Utils.location = Location.from(locationRaw); + Utils.map = map.orElse(""); + + SkyblockEvents.LOCATION_CHANGE.invoker().onSkyblockLocationChange(location); } - } else { - resetLocRawInfo(); + + case ErrorS2CPacket(var id, var error) when id.equals(LocationUpdateS2CPacket.ID) -> { + server = ""; + gameType = ""; + locationRaw = ""; + location = Location.UNKNOWN; + map = ""; + + ClientPlayerEntity player = MinecraftClient.getInstance().player; + + if (player != null) { + player.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.utils.locationUpdateError").formatted(Formatting.RED))); + } + + LOGGER.error("[Skyblocker] Failed to update your current location! Some features of the mod may not work correctly :( - Error: {}", error); + } + + default -> {} //Do Nothing } } @@ -415,6 +453,7 @@ public class Utils { * * @param message json message from chat */ + @Deprecated private static void parseLocRaw(String message) { JsonObject locRaw = JsonParser.parseString(message).getAsJsonObject(); @@ -433,11 +472,6 @@ public class Utils { if (locRaw.has("map")) { map = locRaw.get("map").getAsString(); } - - if (locationChanged) { - SkyblockEvents.LOCATION_CHANGE.invoker().onSkyblockLocationChange(location); - locationChanged = false; - } } /** @@ -450,17 +484,17 @@ public class Utils { if (message.startsWith("{\"server\":") && message.endsWith("}")) { parseLocRaw(message); - boolean shouldFilter = !sentLocRaw; - sentLocRaw = false; - - return shouldFilter; } if (isOnSkyblock) { if (message.startsWith(PROFILE_MESSAGE_PREFIX)) { profile = message.substring(PROFILE_MESSAGE_PREFIX.length()).split("§b")[0]; } else if (message.startsWith(PROFILE_ID_PREFIX)) { + String prevProfileId = profileId; profileId = message.substring(PROFILE_ID_PREFIX.length()); + if (!prevProfileId.equals(profileId)) { + SkyblockEvents.PROFILE_CHANGE.invoker().onSkyblockProfileChange(prevProfileId, profileId); + } MuseumItemCache.tick(profileId); } @@ -469,37 +503,47 @@ public class Utils { return true; } - private static void resetLocRawInfo() { - sentLocRaw = false; - canSendLocRaw = true; - server = ""; - gameType = ""; - locationRaw = ""; - map = ""; - location = Location.UNKNOWN; + private static void scheduleMayorTick() { + long currentYearMillis = SkyblockTime.getSkyblockMillis() % 446400000L; //446400000ms is 1 year, 105600000ms is the amount of time from early spring 1st to late spring 27th + // If current time is past late spring 27th, the next mayor change is at next year's spring 27th, otherwise it's at this year's spring 27th + long millisUntilNextMayorChange = currentYearMillis > 105600000L ? 446400000L - currentYearMillis + 105600000L : 105600000L - currentYearMillis; + Scheduler.INSTANCE.schedule(Utils::tickMayorCache, (int) (millisUntilNextMayorChange / 50) + 5 * 60 * 20); // 5 extra minutes to allow the cache to expire. This is a simpler than checking age and subtracting from max age and rescheduling again. } - private static void tickMayorCache(boolean refresh) { - if (!mayor.isEmpty() && !refresh) return; - + private static void tickMayorCache() { CompletableFuture.supplyAsync(() -> { try { - JsonObject json = JsonParser.parseString(Http.sendGetRequest("https://api.hypixel.net/v2/resources/skyblock/election")).getAsJsonObject(); - if (json.get("success").getAsBoolean()) return json.get("mayor").getAsJsonObject().get("name").getAsString(); - throw new IOException(json.get("cause").getAsString()); + Http.ApiResponse response = Http.sendCacheableGetRequest("https://api.hypixel.net/v2/resources/skyblock/election", null); //Authentication is not required for this endpoint + if (!response.ok()) throw new HttpResponseException(response.statusCode(), response.content()); + JsonObject json = JsonParser.parseString(response.content()).getAsJsonObject(); + if (!json.get("success").getAsBoolean()) throw new RuntimeException("Request failed!"); //Can't find a more appropriate exception to throw here. + return json.get("mayor").getAsJsonObject().get("name").getAsString(); } catch (Exception e) { - LOGGER.error("[Skyblocker] Failed to get mayor status!", e); + throw new RuntimeException(e); //Wrap the exception to be handled by the exceptionally block + } + }).exceptionally(throwable -> { + LOGGER.error("[Skyblocker] Failed to get mayor status!", throwable.getCause()); + if (mayorTickRetryAttempts < 5) { + int minutes = 5 * 1 << mayorTickRetryAttempts; //5, 10, 20, 40, 80 minutes + mayorTickRetryAttempts++; + LOGGER.warn("[Skyblocker] Retrying in {} minutes.", minutes); + Scheduler.INSTANCE.schedule(Utils::tickMayorCache, minutes * 60 * 20); + } else { + LOGGER.warn("[Skyblocker] Failed to get mayor status after 5 retries! Stopping further retries until next reboot."); + } + return ""; //Have to return a value for the thenAccept block. + }).thenAccept(result -> { + if (!result.isEmpty()) { + mayor = result; + LOGGER.info("[Skyblocker] Mayor set to {}.", mayor); + scheduleMayorTick(); //Ends up as a cyclic task with finer control over scheduled time } - return ""; - }).thenAccept(s -> { - if (!s.isEmpty()) mayor = s; }); - } /** * Used to avoid triggering things like chat rules or chat listeners infinitely, do not use otherwise. - * + * <p> * Bypasses MessageHandler#onGameMessage */ public static void sendMessageToBypassEvents(Text message) { diff --git a/src/main/java/de/hysky/skyblocker/utils/chat/ChatPatternListener.java b/src/main/java/de/hysky/skyblocker/utils/chat/ChatPatternListener.java index 708af280..e701e24c 100644 --- a/src/main/java/de/hysky/skyblocker/utils/chat/ChatPatternListener.java +++ b/src/main/java/de/hysky/skyblocker/utils/chat/ChatPatternListener.java @@ -1,6 +1,7 @@ package de.hysky.skyblocker.utils.chat; import net.minecraft.text.Text; +import org.intellij.lang.annotations.Language; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -9,7 +10,7 @@ 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) { + protected ChatPatternListener(@Language("RegExp") String pattern) { this.pattern = Pattern.compile(pattern); } diff --git a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java index a5b9bf6b..1b16b138 100644 --- a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java +++ b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java @@ -11,6 +11,7 @@ import de.hysky.skyblocker.utils.render.title.TitleContainer; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; import net.fabricmc.fabric.api.event.Event; +import net.minecraft.block.BlockState; import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.DrawContext; @@ -20,6 +21,7 @@ import net.minecraft.client.texture.Scaling; import net.minecraft.client.texture.Sprite; import net.minecraft.client.util.BufferAllocator; import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.client.world.ClientWorld; import net.minecraft.sound.SoundEvents; import net.minecraft.text.OrderedText; import net.minecraft.text.Text; @@ -64,18 +66,22 @@ public class RenderHelper { } public static void renderFilled(WorldRenderContext context, BlockPos pos, Vec3d dimensions, float[] colorComponents, float alpha, boolean throughWalls) { + renderFilled(context, Vec3d.of(pos), dimensions, colorComponents, alpha, throughWalls); + } + + public static void renderFilled(WorldRenderContext context, Vec3d pos, Vec3d dimensions, float[] colorComponents, float alpha, boolean throughWalls) { if (throughWalls) { if (FrustumUtils.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + dimensions.x, pos.getY() + dimensions.y, pos.getZ() + dimensions.z)) { - renderFilled(context, Vec3d.of(pos), dimensions, colorComponents, alpha, true); + renderFilledInternal(context, pos, dimensions, colorComponents, alpha, true); } } else { if (OcclusionCulling.getRegularCuller().isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + dimensions.x, pos.getY() + dimensions.y, pos.getZ() + dimensions.z)) { - renderFilled(context, Vec3d.of(pos), dimensions, colorComponents, alpha, false); + renderFilledInternal(context, pos, dimensions, colorComponents, alpha, false); } } } - private static void renderFilled(WorldRenderContext context, Vec3d pos, Vec3d dimensions, float[] colorComponents, float alpha, boolean throughWalls) { + private static void renderFilledInternal(WorldRenderContext context, Vec3d pos, Vec3d dimensions, float[] colorComponents, float alpha, boolean throughWalls) { MatrixStack matrices = context.matrixStack(); Vec3d camera = context.camera().getPos(); @@ -330,6 +336,14 @@ public class RenderHelper { } } + public static Box getBlockBoundingBox(ClientWorld world, BlockPos pos) { + return getBlockBoundingBox(world, world.getBlockState(pos), pos); + } + + public static Box getBlockBoundingBox(ClientWorld world, BlockState state, BlockPos pos) { + return state.getOutlineShape(world, pos).asCuboid().getBoundingBox().offset(pos); + } + /** * 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. diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java index 81c9ebec..9a9d0907 100644 --- a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java @@ -4,6 +4,7 @@ import de.hysky.skyblocker.SkyblockerMod; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; import net.minecraft.item.ItemStack; +import org.intellij.lang.annotations.Language; import java.util.List; import java.util.regex.Pattern; @@ -12,7 +13,7 @@ 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 extends AbstractContainerMatcher { - protected ContainerSolver(String titlePattern) { + protected ContainerSolver(@Language("RegExp") String titlePattern) { super(titlePattern); } diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java index 8a5d32be..79cc78f5 100644 --- a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java @@ -1,10 +1,10 @@ package de.hysky.skyblocker.utils.render.gui; import com.mojang.blaze3d.systems.RenderSystem; - import de.hysky.skyblocker.mixins.accessors.HandledScreenAccessor; import de.hysky.skyblocker.skyblock.accessories.newyearcakes.NewYearCakeBagHelper; import de.hysky.skyblocker.skyblock.accessories.newyearcakes.NewYearCakesHelper; +import de.hysky.skyblocker.skyblock.bazaar.ReorderHelper; import de.hysky.skyblocker.skyblock.chocolatefactory.ChocolateFactorySolver; import de.hysky.skyblocker.skyblock.dungeon.CroesusHelper; import de.hysky.skyblocker.skyblock.dungeon.CroesusProfit; @@ -12,6 +12,7 @@ import de.hysky.skyblocker.skyblock.dungeon.terminal.ColorTerminal; import de.hysky.skyblocker.skyblock.dungeon.terminal.LightsOnTerminal; import de.hysky.skyblocker.skyblock.dungeon.terminal.OrderTerminal; import de.hysky.skyblocker.skyblock.dungeon.terminal.StartsWithTerminal; +import de.hysky.skyblocker.skyblock.dwarven.CommissionHighlight; import de.hysky.skyblocker.skyblock.experiment.ChronomatronSolver; import de.hysky.skyblocker.skyblock.experiment.SuperpairsSolver; import de.hysky.skyblocker.skyblock.experiment.UltrasequencerSolver; @@ -51,13 +52,15 @@ public class ContainerSolverManager { new StartsWithTerminal(), new LightsOnTerminal(), new CroesusHelper(), + new CommissionHighlight(), new CroesusProfit(), new ChronomatronSolver(), new SuperpairsSolver(), UltrasequencerSolver.INSTANCE, new NewYearCakeBagHelper(), NewYearCakesHelper.INSTANCE, - new ChocolateFactorySolver() + new ChocolateFactorySolver(), + new ReorderHelper() }; } diff --git a/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java b/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java index 2f5375fe..547adc5c 100644 --- a/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java +++ b/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java @@ -2,6 +2,7 @@ package de.hysky.skyblocker.utils.scheduler; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; import it.unimi.dsi.fastutil.ints.AbstractInt2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; @@ -14,6 +15,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Function; import java.util.function.Supplier; /** @@ -73,18 +75,56 @@ public class Scheduler { } } + /** + * Creates a command that queues a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. + * + * @param screenFactory the factory of the screen to open + * @return the command + */ + public static Command<FabricClientCommandSource> queueOpenScreenFactoryCommand(Function<CommandContext<FabricClientCommandSource>, Screen> screenFactory) { + return context -> queueOpenScreen(screenFactory.apply(context)); + } + + /** + * Creates a command that queues a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. + * + * @param screenSupplier the supplier of the screen to open + * @return the command + */ public static Command<FabricClientCommandSource> queueOpenScreenCommand(Supplier<Screen> screenSupplier) { - return context -> INSTANCE.queueOpenScreen(screenSupplier); + return context -> queueOpenScreen(screenSupplier.get()); + } + + /** + * Creates a command that queues a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. + * + * @param screen the screen to open + * @return the command + */ + public static Command<FabricClientCommandSource> queueOpenScreenCommand(Screen screen) { + return context -> queueOpenScreen(screen); } /** * Schedules a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. * + * @deprecated Use {@link #queueOpenScreen(Screen)} instead * @param screenSupplier the supplier of the screen to open * @see #queueOpenScreenCommand(Supplier) */ - public int queueOpenScreen(Supplier<Screen> screenSupplier) { - MinecraftClient.getInstance().send(() -> MinecraftClient.getInstance().setScreen(screenSupplier.get())); + @Deprecated(forRemoval = true) + public static int queueOpenScreen(Supplier<Screen> screenSupplier) { + return queueOpenScreen(screenSupplier.get()); + } + + /** + * Schedules a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. + * + * @param screen the screen to open + * @see #queueOpenScreenFactoryCommand(Function) + */ + public static int queueOpenScreen(Screen screen) { + MinecraftClient.getInstance().send(() -> MinecraftClient.getInstance().setScreen(screen)); return Command.SINGLE_SUCCESS; } diff --git a/src/main/java/de/hysky/skyblocker/utils/waypoint/NamedWaypoint.java b/src/main/java/de/hysky/skyblocker/utils/waypoint/NamedWaypoint.java index 2f02b51f..5ba920c0 100644 --- a/src/main/java/de/hysky/skyblocker/utils/waypoint/NamedWaypoint.java +++ b/src/main/java/de/hysky/skyblocker/utils/waypoint/NamedWaypoint.java @@ -6,11 +6,13 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.codecs.RecordCodecBuilder; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.ColorUtils; import de.hysky.skyblocker.utils.render.RenderHelper; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; import net.minecraft.text.Text; import net.minecraft.text.TextCodecs; import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ColorHelper; import net.minecraft.util.math.Vec3d; import java.util.Objects; @@ -32,7 +34,7 @@ public class NamedWaypoint extends Waypoint { Codec.INT.fieldOf("y").forGetter(waypoint -> waypoint.pos.getY()), Codec.INT.fieldOf("z").forGetter(waypoint -> waypoint.pos.getZ()), Codec.either(Codec.STRING, Codec.INT).xmap(either -> either.map(str -> str, Object::toString), Either::left).fieldOf("name").forGetter(waypoint -> waypoint.name.getString()), - Codec.INT.fieldOf("color").forGetter(waypoint -> (int) (waypoint.alpha * 255) << 24 | (int) (waypoint.colorComponents[0] * 255) << 16 | (int) (waypoint.colorComponents[1] * 255) << 8 | (int) (waypoint.colorComponents[2] * 255)), + Codec.INT.optionalFieldOf("color", ColorHelper.Argb.getArgb(128, 0, 255, 0)).forGetter(waypoint -> (int) (waypoint.alpha * 255) << 24 | (int) (waypoint.colorComponents[0] * 255) << 16 | (int) (waypoint.colorComponents[1] * 255) << 8 | (int) (waypoint.colorComponents[2] * 255)), Codec.BOOL.fieldOf("enabled").forGetter(Waypoint::shouldRender) ).apply(instance, NamedWaypoint::fromSkytils)); public final Text name; @@ -69,7 +71,7 @@ public class NamedWaypoint extends Waypoint { if (alpha == 0) { alpha = DEFAULT_HIGHLIGHT_ALPHA; } - return new NamedWaypoint(new BlockPos(x, y, z), name, new float[]{((color & 0x00FF0000) >> 16) / 255f, ((color & 0x0000FF00) >> 8) / 255f, (color & 0x000000FF) / 255f}, alpha, enabled); + return new NamedWaypoint(new BlockPos(x, y, z), name, ColorUtils.getFloatComponents(color), alpha, enabled); } public NamedWaypoint copy() { diff --git a/src/main/java/de/hysky/skyblocker/utils/waypoint/WaypointCategory.java b/src/main/java/de/hysky/skyblocker/utils/waypoint/WaypointCategory.java index db2a6d82..8bfef7f4 100644 --- a/src/main/java/de/hysky/skyblocker/utils/waypoint/WaypointCategory.java +++ b/src/main/java/de/hysky/skyblocker/utils/waypoint/WaypointCategory.java @@ -29,6 +29,10 @@ public record WaypointCategory(String name, String island, List<NamedWaypoint> w return new WaypointCategory(name, island(), waypoints()); } + public WaypointCategory withIsland(String island) { + return new WaypointCategory(name(), island, waypoints()); + } + public WaypointCategory deepCopy() { return new WaypointCategory(name(), island(), waypoints().stream().map(NamedWaypoint::copy).collect(Collectors.toList())); } diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index a814718e..08b29d4c 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -19,6 +19,8 @@ "text.skyblocker.modrinth": "Modrinth", "text.skyblocker.discord": "Discord", + "skyblocker.skyblockerScreen": "Skyblocker Main Screen", + "skyblocker.config.title": "Skyblocker Settings", "skyblocker.config.crimsonIsle": "Crimson Isle", @@ -44,6 +46,12 @@ "skyblocker.config.dungeons.croesusHelper": "Croesus Helper", "skyblocker.config.dungeons.croesusHelper.@Tooltip": "Gray out chests that have already been opened.", + "skyblocker.config.dungeons.devices": "Device Solvers (F7/M7)", + "skyblocker.config.dungeons.devices.solveLightsOn": "Solve Lights On", + "skyblocker.config.dungeons.devices.solveLightsOn.@Tooltip": "Highlights the correct levers to click in red", + "skyblocker.config.dungeons.devices.solveSimonSays": "Solve Simon Says", + "skyblocker.config.dungeons.devices.solveSimonSays.@Tooltip": "Highlights the correct button to click in green, and highlights the next one in yellow.", + "skyblocker.config.dungeons.doorHighlight": "Door Highlight", "skyblocker.config.dungeons.doorHighlight.doorHighlightType": "Door Highlight Type", "skyblocker.config.dungeons.doorHighlight.doorHighlightType.@Tooltip": "Highlight: Only displays a highlight.\n\nOutlined Highlight: Displays both a highlight and an outline.\n\nOutline: Only displays an outline.", @@ -237,6 +245,11 @@ "skyblocker.config.general.itemTooltip.enableAccessoriesHelper.@Tooltip[5]": "You don't own any accessory from this family.", "skyblocker.config.general.itemTooltip.enableAvgBIN": "Enable Avg. BIN Price", "skyblocker.config.general.itemTooltip.enableBazaarPrice": "Enable Bazaar buy/sell Price", + "skyblocker.config.general.itemTooltip.craft": "Crafting Cost", + "skyblocker.config.general.itemTooltip.craft.@Tooltip": "You can choose which Bazaar order type to use in crafting calculation", + "skyblocker.config.general.itemTooltip.craft.OFF": "Off", + "skyblocker.config.general.itemTooltip.craft.SELL_ORDER": "Sell Order", + "skyblocker.config.general.itemTooltip.craft.BUY_ORDER": "Buy Order", "skyblocker.config.general.itemTooltip.enableExoticTooltip": "Enable Exotic Tooltip", "skyblocker.config.general.itemTooltip.enableExoticTooltip.@Tooltip": "Displays the type of exotic below the item's name if an armor piece is exotic.", "skyblocker.config.general.itemTooltip.enableLowestBIN": "Enable Lowest BIN Price", @@ -278,6 +291,10 @@ "skyblocker.config.helpers": "Helpers", + "skyblocker.config.helpers.bazaar": "Bazaar", + "skyblocker.config.helpers.bazaar.enableBazaarHelper": "Enable Bazaar Helper", + "skyblocker.config.helpers.bazaar.enableBazaarHelper.@Tooltip": "Draws icons on top of orders to explain the current state of the order.\n\n%s: Order is going to expire soon\n%s: Order has expired\n%s: Order is filled to some degree and there are items/coins to claim\n%s: Order is filled", + "skyblocker.config.helpers.chocolateFactory": "Chocolate Factory", "skyblocker.config.helpers.chocolateFactory.enableChocolateFactoryHelper": "Enable Chocolate Factory Helper", "skyblocker.config.helpers.chocolateFactory.enableChocolateFactoryHelper.@Tooltip": "Highlights the best upgrade when enabled. \n\nThe best upgrade is marked as green, but if you can't afford it, it's marked as yellow while marking the next best upgrade that you can afford as green.", @@ -287,6 +304,8 @@ "skyblocker.config.helpers.chocolateFactory.enableTimeTowerReminder.@Tooltip": "Sends a message in chat when your Time Tower deactivates.", "skyblocker.config.helpers.chocolateFactory.sendEggFoundMessages": "Send Egg Found Messages", "skyblocker.config.helpers.chocolateFactory.sendEggFoundMessages.@Tooltip": "Sends a message in chat when an egg is found in the current island.", + "skyblocker.config.helpers.chocolateFactory.straySound": "Stray Rabbit Sound", + "skyblocker.config.helpers.chocolateFactory.straySound.@Tooltip": "Repeatedly plays a ding while a stray rabbit is present. If it is golden, the sound will be more frequent and different.", "skyblocker.config.helpers.chocolateFactory.waypointType": "Egg Waypoint Type", "skyblocker.config.helpers.chocolateFactory.waypointType.@Tooltip": "Waypoint: Displays a highlight and a beacon beam.\n\nOutlined Waypoint: Displays both a waypoint and an outline.\n\nHighlight: Only displays a highlight.\n\nOutlined Highlight: Displays both a highlight and an outline.\n\nOutline: Only displays an outline.", @@ -315,6 +334,11 @@ "skyblocker.config.helpers.fishing.hideOtherPlayers": "Hide Other Players Rods", "skyblocker.config.helpers.fishing.hideOtherPlayers.@Tooltip": "Hide other players fishing rods from showing for you", + "skyblocker.config.helpers.jerry": "Jerry", + "skyblocker.config.helpers.jerry.enableJerryTimer": "Enable Jerry Timer", + "skyblocker.config.helpers.jerry.enableJerryTimer.@Tooltip": "Sends a message in chat and plays a sound when the hidden jerry spawn cooldown is over.", + + "skyblocker.config.helpers.mythologicalRitual": "Mythological Ritual Helper", "skyblocker.config.helpers.mythologicalRitual.enableMythologicalRitualHelper": "Enable Mythological Ritual Helper", @@ -428,6 +452,8 @@ "skyblocker.config.mining.crystalHollows": "Crystal Hollows", "skyblocker.config.mining.crystalHollows.metalDetectorHelper": "Metal Detector Helper", "skyblocker.config.mining.crystalHollows.metalDetectorHelper.@Tooltip": "Helper for the metal detector puzzle in the Mines of Divan.", + "skyblocker.config.mining.crystalHollows.nucleusWaypoints": "Nucleus Waypoints", + "skyblocker.config.mining.crystalHollows.nucleusWaypoints.@Tooltip": "Show waypoints to the Nucleus in the Crystal Hollows.", "skyblocker.config.mining.crystalsHud": "Crystal Hollows Map", "skyblocker.config.mining.crystalsHud.enabled": "Enabled", @@ -460,6 +486,8 @@ "skyblocker.config.mining.enableDrillFuel": "Enable Drill Fuel", + "skyblocker.config.mining.commissionHighlight": "Highlights Completed Commissions", + "skyblocker.config.mining.glacite": "Glacite Tunnels", "skyblocker.config.mining.glacite.coldOverlay": "Cold Overlay", "skyblocker.config.mining.glacite.coldOverlay@Tooltip": "Shows a frosty overlay in the Glacite mines that gets stronger as you get colder.", @@ -604,6 +632,8 @@ "skyblocker.config.uiAndVisuals.searchOverlay.maxSuggestions": "Maximum Suggestions", "skyblocker.config.uiAndVisuals.searchOverlay.maxSuggestions.@Tooltip": "The maximum number of suggested items to show.", + "skyblocker.config.uiAndVisuals.showEquipmentInInventory": "Show Equipment in Inventory", + "skyblocker.config.uiAndVisuals.tabHud": "Fancy tab HUD (Temporarily disabled outside dungeons)", "skyblocker.config.uiAndVisuals.tabHud.enableHudBackground": "Enable HUD Background", "skyblocker.config.uiAndVisuals.tabHud.enableHudBackground.@Tooltip": "Enables the background of the non-tab HUD.", @@ -634,6 +664,11 @@ "skyblocker.config.uiAndVisuals.waypoints.waypointType.@Tooltip": "Waypoint: Displays a highlight and a beacon beam.\n\nOutlined Waypoint: Displays both a waypoint and an outline.\n\nHighlight: Only displays a highlight.\n\nOutlined Highlight: Displays both a highlight and an outline.\n\nOutline: Only displays an outline.", "skyblocker.config.uiAndVisuals.waypoints.waypointType.generalNote": "\n\n\nThis option does not apply to all waypoints. Some waypoints such as secret waypoints have their own waypoint type option.", + "skyblocker.utils.locationUpdateError": "Failed to update your location! Some features of the mod may not work properly :(", + + "skyblocker.reparty.notInPartyOrNotLeader": "You must be in a party and be the leader of it in order to reparty!", + "skyblocker.reparty.error": "Failed to reparty, try again in a moment!", + "skyblocker.itemTooltip.noData": "\u00a7cNo Data", "skyblocker.itemTooltip.nullMessage": "\u00a7cItem price information on tooltip will renew in max 60 seconds. If not, check latest.log", @@ -722,7 +757,6 @@ "skyblocker.events.tab.noMore": "No more this year!", "skyblocker.events.tab.startsIn": "Starts in %s", - "skyblocker.garden.hud.mouseLocked": "Mouse locked.", "skyblocker.fishing.reelNow": "Reel in now!", @@ -811,8 +845,11 @@ "skyblocker.tips.enabled": "§aEnabled Tips.", "skyblocker.tips.disabled": "§cDisabled Tips.", + "skyblocker.tips.previous": "Previous Tip", + "skyblocker.tips.next": "Next Tip", "skyblocker.tips.clickEnable": "§a[Click to Enable Tips]", "skyblocker.tips.clickDisable": "§c[Click to Disable Tips]", + "skyblocker.tips.clickPreviousTip": "§b[Click for Previous Tip]", "skyblocker.tips.clickNextTip": "§a[Click for Next Tip]", "skyblocker.tips.tip": "§aTip: %s\n", "skyblocker.tips.customItemNames": "Customize the names of your items with /skyblocker custom renameItem", @@ -829,12 +866,31 @@ "skyblocker.tips.modMenuUpdate": "ModMenu will let you know if there's an update available for Skyblocker for your game version.", "skyblocker.tips.issues": "Submit bug reports and feature requests to https://github.com/SkyblockerMod/Skyblocker.", "skyblocker.tips.beta": "We often have beta versions available from GitHub Actions that contain new and experimental features.", + "skyblocker.tips.contribute": "We welcome contributions to the mod! See https://github.com/SkyblockerMod/Skyblocker/wiki/contribute for more info.", "skyblocker.tips.discord": "Join our discord at https://discord.gg/aNNJHQykck to keep up with the latest news about Skyblocker!", "skyblocker.tips.flameOverlay": "Find that the flame overlay takes up too much screen space? Check out the config to make it smaller", "skyblocker.tips.wikiLookup": "Press F4 while hovering over an item to open its wiki page in your web browser.", "skyblocker.tips.protectItem": "Prevent accidentally dropping your items with /skyblocker protectItem.", "skyblocker.tips.fairySoulsEnigmaSoulsRelics": "Don't know where to find Fairy Souls, Enigma Souls, or Relics? Enable the helpers to aid your exploration, they'll remember which souls you've already found.", "skyblocker.tips.quickNav": "You can customize the QuickNav buttons in the config.", + "skyblocker.tips.waypoints": "You can import (including from skytils), add, remove, and toggle waypoints on any island with /skyblocker waypoints.", + "skyblocker.tips.orderedWaypoints": "You can import (including from coleweight), add, remove, and toggle ordered waypoints with /skyblocker waypoints ordered.", + "skyblocker.tips.visitorHelper": "Click on the item name in the visitor helper to buy from the bazaar or click on [Copy Amount] to copy the amount to your clipboard.", + "skyblocker.tips.slotText": "Slot text shows you the attribute shard info, catacombs level, collection level, enchantment book level, minion level, pet level, potion level, prehistoric egg blocks walked, rancher's boots speed cap, skill level, skyblock level in the slot.", + "skyblocker.tips.profileViewer": "You can view other player's profiles with /pv.", + "skyblocker.tips.configSearch": "You can search the entire config using the search bar on the bottom right of the config screen.", + "skyblocker.tips.compactDamage": "Customize your damage value with Compact Damage in the config.", + "skyblocker.tips.skyblockerScreen": "You can access the Skyblocker screen to open the config or checkout helpful links for the mod with /skyblocker.", + "skyblocker.tips.tipsClick": "Click on a tip chat message to run its suggestion!", + "skyblocker.tips.eventNotifications": "Check out the customizable event notifications in the config.", + "skyblocker.tips.signCalculator": "Type an math expression in a sign to have the mod calculate it for you.", + "skyblocker.tips.calculateCommand": "Enter an expression after /skyblocker calculate to have the mod calculate it for you.", + "skyblocker.tips.fancierBars": "Customize the look of your health, mana, defense, and experience bars with /skyblocker bars. You can snap and resize bars too!", + "skyblocker.tips.crystalWaypointsShare": "Share Crystal Hollows Waypoints with /skyblocker crystalWaypoints share.", + "skyblocker.tips.gardenMouseLock": "Lock your mouse while farming in the Skyblocker Garden config.", + "skyblocker.tips.newYearCakesHelper": "Open your New Year Cake Bag and Skyblocker will remember them and highlight duplicate cakes red and missing cakes green.", + "skyblocker.tips.accessoryHelper": "Open your accessory bag and Skyblocker will remember them. The accessory helper will tell you what accessories you have and what accessories are missing.", + "skyblocker.tips.fancyAuctionHouseCheapHighlight": "Cheap BINs are highlighted in green in the Fancy Auction House.", "skyblocker.partyFinder.tabs.partyFinder": "Party Finder", "skyblocker.partyFinder.tabs.searchSettings": "Search Filters", @@ -849,6 +905,9 @@ "skyblocker.partyFinder.deList": "Click to de-list", "skyblocker.partyFinder.join": "Click to join", + "skyblocker.reorderHelper.tooltip.line1": "[Skyblocker] You can copy the amount of items", + "skyblocker.reorderHelper.tooltip.line2": "by holding CTRL while clicking on the item!", + "skyblocker.fancyAuctionHouse.editBid": "Click to edit bid!", "skyblocker.fancyAuctionHouse.price": "Price:", "skyblocker.fancyAuctionHouse.newBid": "New Bid:", @@ -866,6 +925,22 @@ "skyblocker.fancyAuctionHouse.yourAuction": "This is your auction!", "skyblocker.fancyAuctionHouse.youPay": "You pay: %s", + "skyblocker.crimson.dojo": "Dojo", + "skyblocker.crimson.dojo.forceHelper": "Enable Force Helper", + "skyblocker.crimson.dojo.forceHelper.@Tooltip": "Shows timer showing how long until a zombie despawns and outlines negative zombies.", + "skyblocker.crimson.dojo.staminaHelper": "Enable Stamina Helper", + "skyblocker.crimson.dojo.staminaHelper.@Tooltip": "Highlights the holes in the walls turning orange once you have been through a wall.", + "skyblocker.crimson.dojo.masteryHelper": "Enable Mastery Helper", + "skyblocker.crimson.dojo.masteryHelper.@Tooltip": "Shows count down to when to relase the bow and a path to follow.", + "skyblocker.crimson.dojo.disciplineHelper": "Enable Discipline Helper", + "skyblocker.crimson.dojo.disciplineHelper.@Tooltip": "Outlines the zombies to attack with currently held sword.", + "skyblocker.crimson.dojo.swiftnessHelper": "Enable Swiftness Helper", + "skyblocker.crimson.dojo.swiftnessHelper.@Tooltip": "highlights the newest wool block to go to.", + "skyblocker.crimson.dojo.controlHelper": "Enable Control Helper", + "skyblocker.crimson.dojo.controlHelper.@Tooltip": "enders an outline around where to aim.", + "skyblocker.crimson.dojo.tenacityHelper": "Enable Tenacity Helper", + "skyblocker.crimson.dojo.tenacityHelper.@Tooltip": "Shows a path for each fireball and predicted block they are going to hit.", + "skyblocker.crimson.kuudra.noArrowPoison": "No Arrow Poison!", "skyblocker.crimson.kuudra.lowArrowPoison": "Low on Arrow Poison!", @@ -887,5 +962,17 @@ "skyblocker.waypoints.ordered.import.coleWeight.success": "Successfully imported waypoints from the Cole Weight format.", "skyblocker.waypoints.ordered.import.coleWeight.fail": "§cFailed to import waypoints from the Cole Weight format. Make sure to have the waypoint data copied to your clipboard!", + "skyblocker.profileviewer.inventory.inventory": "Inventory", + "skyblocker.profileviewer.inventory.armor": "Armor", + "skyblocker.profileviewer.inventory.equipment": "Equipment", + "skyblocker.profileviewer.inventory.enderchest": "Enderchest", + "skyblocker.profileviewer.inventory.backpack": "Backpack", + "skyblocker.profileviewer.inventory.wardrobe": "Wardrobe", + "skyblocker.profileviewer.inventory.pets": "Pets", + "skyblocker.profileviewer.inventory.accessoryBag": "Accessory Bag", + "skyblocker.profileviewer.inventory.inactive": "Locked Slot", + "skyblocker.profileviewer.inventory.inactive.description.backpack": "The selected backpack", + "skyblocker.profileviewer.inventory.inactive.description.general": "does not contain this slot", + "emi.category.skyblocker.skyblock": "Skyblock" } diff --git a/src/main/resources/assets/skyblocker/lang/lol_us.json b/src/main/resources/assets/skyblocker/lang/lol_us.json new file mode 100644 index 00000000..4cda6799 --- /dev/null +++ b/src/main/resources/assets/skyblocker/lang/lol_us.json @@ -0,0 +1,3 @@ +{ + "key.categories.skyblocker": "Skibidiblocker" +} diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/base_plate.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/base_plate.png Binary files differnew file mode 100644 index 00000000..0edf5705 --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/base_plate.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/blaze.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/blaze.png Binary files differnew file mode 100644 index 00000000..ed168ddf --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/blaze.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon.png Binary files differnew file mode 100644 index 00000000..d29969be --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_highlighted.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_highlighted.png Binary files differnew file mode 100644 index 00000000..11340a55 --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_highlighted.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled.png Binary files differnew file mode 100644 index 00000000..109cd859 --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled_highlighted.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled_highlighted.png Binary files differnew file mode 100644 index 00000000..581cdefe --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/button_icon_toggled_highlighted.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_body.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_body.png Binary files differnew file mode 100644 index 00000000..379557e4 --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_body.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_header.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_header.png Binary files differnew file mode 100644 index 00000000..384dc0e3 --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/dungeons_header.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/enderman.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/enderman.png Binary files differnew file mode 100644 index 00000000..84650c7f --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/enderman.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/icon_data_widget.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/icon_data_widget.png Binary files differnew file mode 100644 index 00000000..c28aeed4 --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/icon_data_widget.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/run_icon.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/run_icon.png Binary files differnew file mode 100644 index 00000000..d83fad70 --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/run_icon.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/spider.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/spider.png Binary files differnew file mode 100644 index 00000000..a5daa3d6 --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/spider.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/vampire.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/vampire.png Binary files differnew file mode 100644 index 00000000..efbe985d --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/vampire.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/wolf.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/wolf.png Binary files differnew file mode 100644 index 00000000..f7bec9a4 --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/wolf.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/zombie.png b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/zombie.png Binary files differnew file mode 100644 index 00000000..37ea069f --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/profile_viewer/zombie.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/equipment/empty_icon.png b/src/main/resources/assets/skyblocker/textures/gui/sprites/equipment/empty_icon.png Binary files differnew file mode 100644 index 00000000..be89af40 --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/sprites/equipment/empty_icon.png diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index e46085de..1361c00a 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -36,6 +36,7 @@ "fabricloader": ">=0.15.11", "fabric-api": ">=0.100.1+1.21", "yet_another_config_lib_v3": ">=3.5.0+1.21", + "hm-api": ">=1.0.0+1.21", "minecraft": "~1.21", "java": ">=21" }, diff --git a/src/main/resources/skyblocker.mixins.json b/src/main/resources/skyblocker.mixins.json index 9b96ba61..685758f7 100644 --- a/src/main/resources/skyblocker.mixins.json +++ b/src/main/resources/skyblocker.mixins.json @@ -8,6 +8,7 @@ "BatEntityMixin", "ClientPlayerEntityMixin", "ClientPlayNetworkHandlerMixin", + "ClientWorldMixin", "CommandTreeS2CPacketMixin", "ComponentHolderMixin", "DataTrackerMixin", @@ -26,6 +27,7 @@ "LivingEntityRendererMixin", "MinecraftClientMixin", "MouseMixin", + "PingMeasurerMixin", "PlayerInventoryMixin", "PlayerListHudMixin", "PlayerSkinProviderMixin", @@ -45,6 +47,7 @@ "accessors.FrustumInvoker", "accessors.HandledScreenAccessor", "accessors.MessageHandlerAccessor", + "accessors.MinecraftClientAccessor", "accessors.PlayerListHudAccessor", "accessors.RecipeBookWidgetAccessor", "accessors.ScreenAccessor", |