diff options
Diffstat (limited to 'src/main/java/de/hysky/skyblocker')
86 files changed, 4347 insertions, 186 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index d793e73d..f1eb4321 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -11,6 +11,7 @@ import de.hysky.skyblocker.skyblock.chat.ChatRuleAnnouncementScreen; import de.hysky.skyblocker.skyblock.chat.ChatRulesHandler; import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder; import de.hysky.skyblocker.skyblock.chocolatefactory.TimeTowerReminder; +import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; import de.hysky.skyblocker.skyblock.crimson.kuudra.Kuudra; import de.hysky.skyblocker.skyblock.dungeon.*; import de.hysky.skyblocker.skyblock.dungeon.partyfinder.PartyFinderScreen; @@ -36,6 +37,8 @@ import de.hysky.skyblocker.skyblock.item.tooltip.BackpackPreview; import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; import de.hysky.skyblocker.skyblock.item.tooltip.TooltipManager; import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; +import de.hysky.skyblocker.skyblock.mayors.JerryTimer; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; import de.hysky.skyblocker.skyblock.rift.TheRift; import de.hysky.skyblocker.skyblock.searchoverlay.SearchOverManager; import de.hysky.skyblocker.skyblock.shortcut.Shortcuts; @@ -105,6 +108,7 @@ public class SkyblockerMod implements ClientModInitializer { Utils.init(); SkyblockerConfigManager.init(); SkyblockerScreen.initClass(); + ProfileViewerScreen.initClass(); Tips.init(); NEURepoManager.init(); //ImageRepoLoader.init(); @@ -179,8 +183,10 @@ public class SkyblockerMod implements ClientModInitializer { ApiUtils.init(); Debug.init(); Kuudra.init(); + DojoManager.init(); RenderHelper.init(); FancyStatusBars.init(); + SkyblockInventoryScreen.initEquipment(); EventNotifications.init(); containerSolverManager.init(); statusBarTracker.init(); @@ -190,6 +196,7 @@ public class SkyblockerMod implements ClientModInitializer { EggFinder.init(); TimeTowerReminder.init(); SkyblockTime.init(); + JerryTimer.init(); TooltipManager.init(); SlotTextManager.init(); diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java b/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java index 0196a37a..9686c6d8 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerScreen.java @@ -1,7 +1,5 @@ package de.hysky.skyblocker; -import java.time.LocalDate; - import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.skyblock.Tips; import de.hysky.skyblocker.utils.scheduler.Scheduler; @@ -11,11 +9,7 @@ import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.ConfirmLinkScreen; import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.gui.widget.GridWidget; -import net.minecraft.client.gui.widget.MultilineTextWidget; -import net.minecraft.client.gui.widget.TextWidget; -import net.minecraft.client.gui.widget.ThreePartsLayoutWidget; +import net.minecraft.client.gui.widget.*; import net.minecraft.screen.ScreenTexts; import net.minecraft.text.OrderedText; import net.minecraft.text.StringVisitable; @@ -23,6 +17,8 @@ import net.minecraft.text.Text; import net.minecraft.util.Identifier; import net.minecraft.util.Language; +import java.time.LocalDate; + public class SkyblockerScreen extends Screen { private static final int SPACING = 8; private static final int BUTTON_WIDTH = 210; @@ -36,7 +32,8 @@ public class SkyblockerScreen extends Screen { private static final Text TRANSLATE_TEXT = Text.translatable("text.skyblocker.translate"); private static final Text MODRINTH_TEXT = Text.translatable("text.skyblocker.modrinth"); private static final Text DISCORD_TEXT = Text.translatable("text.skyblocker.discord"); - private final ThreePartsLayoutWidget layout = new ThreePartsLayoutWidget(this); + private ThreePartsLayoutWidget layout; + private MultilineTextWidget tip; static { LocalDate date = LocalDate.now(); @@ -44,7 +41,7 @@ public class SkyblockerScreen extends Screen { ICON = date.getMonthValue() == 4 && date.getDayOfMonth() == 1 ? Identifier.of(SkyblockerMod.NAMESPACE, "icons.png") : Identifier.of(SkyblockerMod.NAMESPACE, "icon.png"); } - private SkyblockerScreen() { + public SkyblockerScreen() { super(TITLE); } @@ -57,6 +54,7 @@ public class SkyblockerScreen extends Screen { @Override protected void init() { + this.layout = new ThreePartsLayoutWidget(this, 50, 100); this.layout.addHeader(new IconTextWidget(this.getTitle(), this.textRenderer, ICON)); GridWidget gridWidget = this.layout.addBody(new GridWidget()).setSpacing(SPACING); @@ -72,17 +70,26 @@ public class SkyblockerScreen extends Screen { adder.add(ButtonWidget.builder(DISCORD_TEXT, ConfirmLinkScreen.opening(this, "https://discord.gg/aNNJHQykck")).width(HALF_BUTTON_WIDTH).build()); adder.add(ButtonWidget.builder(ScreenTexts.DONE, button -> this.close()).width(BUTTON_WIDTH).build(), 2); - MultilineTextWidget tip = new MultilineTextWidget(Text.translatable("skyblocker.tips.tip", Tips.nextTipInternal()), this.textRenderer) - .setCentered(true) - .setMaxWidth((int) (this.width * 0.7)); + GridWidget footerGridWidget = this.layout.addFooter(new GridWidget()).setSpacing(SPACING).setRowSpacing(0); + footerGridWidget.getMainPositioner().alignHorizontalCenter(); + GridWidget.Adder footerAdder = footerGridWidget.createAdder(2); + footerAdder.add(tip = new MultilineTextWidget(Tips.nextTip(), this.textRenderer).setCentered(true).setMaxWidth((int) (this.width * 0.7)), 2); + footerAdder.add(ButtonWidget.builder(Text.translatable("skyblocker.tips.previous"), button -> { + tip.setMessage(Tips.previousTip()); + layout.refreshPositions(); + }).build()); + footerAdder.add(ButtonWidget.builder(Text.translatable("skyblocker.tips.next"), button -> { + tip.setMessage(Tips.nextTip()); + layout.refreshPositions(); + }).build()); - this.layout.addFooter(tip); this.layout.refreshPositions(); this.layout.forEachChild(this::addDrawableChild); } @Override protected void initTabNavigation() { + super.initTabNavigation(); this.layout.refreshPositions(); } diff --git a/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java index fed8fa96..e8127a0e 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/CrimsonIsleCategory.java @@ -81,6 +81,68 @@ public class CrimsonIsleCategory { .controller(IntegerFieldControllerBuilder::create) .build()) .build()) - .build(); + //dojo + .group(OptionGroup.createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo")) + .collapsed(false) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.forceHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.forceHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableForceHelper, + () -> config.crimsonIsle.dojo.enableForceHelper, + newValue -> config.crimsonIsle.dojo.enableForceHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.staminaHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.staminaHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableStaminaHelper, + () -> config.crimsonIsle.dojo.enableStaminaHelper, + newValue -> config.crimsonIsle.dojo.enableStaminaHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.masteryHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.masteryHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableMasteryHelper, + () -> config.crimsonIsle.dojo.enableMasteryHelper, + newValue -> config.crimsonIsle.dojo.enableMasteryHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.disciplineHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.disciplineHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableDisciplineHelper, + () -> config.crimsonIsle.dojo.enableDisciplineHelper, + newValue -> config.crimsonIsle.dojo.enableDisciplineHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.swiftnessHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.swiftnessHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableSwiftnessHelper, + () -> config.crimsonIsle.dojo.enableSwiftnessHelper, + newValue -> config.crimsonIsle.dojo.enableSwiftnessHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.controlHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.controlHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableControlHelper, + () -> config.crimsonIsle.dojo.enableControlHelper, + newValue -> config.crimsonIsle.dojo.enableControlHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.crimson.dojo.tenacityHelper")) + .description(OptionDescription.of(Text.translatable("skyblocker.crimson.dojo.tenacityHelper.@Tooltip"))) + .binding(config.crimsonIsle.dojo.enableTenacityHelper, + () -> config.crimsonIsle.dojo.enableTenacityHelper, + newValue -> config.crimsonIsle.dojo.enableTenacityHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .build()) + + .build(); } } diff --git a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java index 279b7f6a..96bb226d 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java @@ -1,5 +1,6 @@ package de.hysky.skyblocker.config.categories; +import de.hysky.skyblocker.SkyblockerScreen; import de.hysky.skyblocker.config.ConfigUtils; import de.hysky.skyblocker.config.SkyblockerConfig; import de.hysky.skyblocker.config.configs.GeneralConfig; @@ -17,6 +18,13 @@ public class GeneralCategory { return ConfigCategory.createBuilder() .name(Text.translatable("skyblocker.config.general")) + //Skyblocker Screen + .option(ButtonOption.createBuilder() + .name(Text.translatable("skyblocker.skyblockerScreen")) + .text(Text.translatable("text.skyblocker.open")) + .action((screen, opt) -> MinecraftClient.getInstance().setScreen(new SkyblockerScreen())) + .build()) + //Ungrouped Options .option(Option.<Boolean>createBuilder() .name(Text.translatable("skyblocker.config.general.enableTips")) diff --git a/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java index 9e4935cb..1518dfe7 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java @@ -38,6 +38,20 @@ public class HelperCategory { .build()) .build()) + //Jerry Timer + .group(OptionGroup.createBuilder() + .name(Text.translatable("skyblocker.config.helpers.jerry")) + .collapsed(true) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.helpers.jerry.enableJerryTimer")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.jerry.enableJerryTimer.@Tooltip"))) + .binding(defaults.helpers.jerry.enableJerryTimer, + () -> config.helpers.jerry.enableJerryTimer, + newValue -> config.helpers.jerry.enableJerryTimer = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .build()) + //Experiments Solver .group(OptionGroup.createBuilder() .name(Text.translatable("skyblocker.config.helpers.experiments")) diff --git a/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java index e77e9f4b..4e11d869 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/MiningCategory.java @@ -30,6 +30,14 @@ public class MiningCategory { .controller(ConfigUtils::createBooleanController) .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.mining.commissionHighlight")) + .binding(defaults.mining.commissionHighlight, + () -> config.mining.commissionHighlight, + newValue -> config.mining.commissionHighlight = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + //Dwarven Mines .group(OptionGroup.createBuilder() .name(Text.translatable("skyblocker.config.mining.dwarvenMines")) @@ -95,7 +103,15 @@ public class MiningCategory { newValue -> config.mining.crystalHollows.metalDetectorHelper = newValue) .controller(ConfigUtils::createBooleanController) .build()) - .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.mining.crystalHollows.nucleusWaypoints")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.mining.crystalHollows.nucleusWaypoints.@Tooltip"))) + .binding(defaults.mining.crystalHollows.nucleusWaypoints, + () -> config.mining.crystalHollows.nucleusWaypoints, + newValue -> config.mining.crystalHollows.nucleusWaypoints = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .build()) //Crystal Hollows Map .group(OptionGroup.createBuilder() diff --git a/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java index a2a0f815..889b253a 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java @@ -68,6 +68,13 @@ public class UIAndVisualsCategory { newValue -> config.uiAndVisuals.hideStatusEffectOverlay = newValue) .controller(ConfigUtils::createBooleanController) .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.uiAndVisuals.showEquipmentInInventory")) + .binding(defaults.uiAndVisuals.showEquipmentInInventory, + () -> config.uiAndVisuals.showEquipmentInInventory, + newValue -> config.uiAndVisuals.showEquipmentInInventory = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) //Chest Value FIXME change dropdown to color controller .group(OptionGroup.createBuilder() diff --git a/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java index 8dd93aee..451d1983 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/CrimsonIsleConfig.java @@ -7,6 +7,10 @@ public class CrimsonIsleConfig { @SerialEntry public Kuudra kuudra = new Kuudra(); + @SerialEntry + public Dojo dojo = new Dojo(); + + public static class Kuudra { @SerialEntry public boolean supplyWaypoints = true; @@ -32,4 +36,27 @@ public class CrimsonIsleConfig { @SerialEntry public int arrowPoisonThreshold = 32; } + + public static class Dojo { + @SerialEntry + public boolean enableForceHelper = true; + + @SerialEntry + public boolean enableStaminaHelper = true; + + @SerialEntry + public boolean enableMasteryHelper = true; + + @SerialEntry + public boolean enableDisciplineHelper = true; + + @SerialEntry + public boolean enableSwiftnessHelper = true; + + @SerialEntry + public boolean enableControlHelper = true; + + @SerialEntry + public boolean enableTenacityHelper = true; + } } diff --git a/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java index c0314924..33047f1c 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java @@ -12,6 +12,9 @@ public class HelperConfig { public MythologicalRitual mythologicalRitual = new MythologicalRitual(); @SerialEntry + public Jerry jerry = new Jerry(); + + @SerialEntry public Experiments experiments = new Experiments(); @SerialEntry @@ -28,6 +31,11 @@ public class HelperConfig { public boolean enableMythologicalRitualHelper = true; } + public static class Jerry { + @SerialEntry + public boolean enableJerryTimer = false; + } + public static class Experiments { @SerialEntry public boolean enableChronomatronSolver = true; diff --git a/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java index a2a9bcf7..d71f57b6 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/MiningConfig.java @@ -27,6 +27,9 @@ public class MiningConfig { @SerialEntry public Glacite glacite = new Glacite(); + @SerialEntry + public boolean commissionHighlight = true; + public static class DwarvenMines { @SerialEntry public boolean solveFetchur = true; @@ -61,6 +64,9 @@ public class MiningConfig { public static class CrystalHollows { @SerialEntry public boolean metalDetectorHelper = true; + + @SerialEntry + public boolean nucleusWaypoints = false; } public static class CrystalsHud { diff --git a/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java index e016988b..80bdb1c9 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java @@ -28,6 +28,9 @@ public class UIAndVisualsConfig { public boolean hideStatusEffectOverlay = false; @SerialEntry + public boolean showEquipmentInInventory = true; + + @SerialEntry public ChestValue chestValue = new ChestValue(); @SerialEntry diff --git a/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java b/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java index c268103d..93426143 100644 --- a/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java +++ b/src/main/java/de/hysky/skyblocker/events/SkyblockEvents.java @@ -26,6 +26,16 @@ public final class SkyblockEvents { } }); + /** + * Called when the player's Skyblock profile changes. + * @implNote This is called upon receiving the chat message for the profile change rather than the exact moment of profile change, so it may be delayed by a few seconds. + */ + public static final Event<ProfileChange> PROFILE_CHANGE = EventFactory.createArrayBacked(ProfileChange.class, callbacks -> (prev, profile) -> { + for (ProfileChange callback : callbacks) { + callback.onSkyblockProfileChange(prev, profile); + } + }); + @Environment(EnvType.CLIENT) @FunctionalInterface public interface SkyblockJoin { @@ -43,4 +53,10 @@ public final class SkyblockEvents { public interface SkyblockLocationChange { void onSkyblockLocationChange(Location location); } + + @Environment(EnvType.CLIENT) + @FunctionalInterface + public interface ProfileChange { + void onSkyblockProfileChange(String prevProfileId, String profileId); + } } diff --git a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java index 48389d40..7bbbac81 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java @@ -6,6 +6,7 @@ import com.llamalad7.mixinextras.sugar.Local; import de.hysky.skyblocker.skyblock.CompactDamage; import de.hysky.skyblocker.skyblock.FishingHelper; import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder; +import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; import de.hysky.skyblocker.skyblock.dungeon.DungeonScore; import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import de.hysky.skyblocker.skyblock.end.BeaconHighlighter; @@ -97,6 +98,7 @@ public abstract class ClientPlayNetworkHandlerMixin { @Inject(method = "onParticle", at = @At("RETURN")) private void skyblocker$onParticle(ParticleS2CPacket packet, CallbackInfo ci) { MythologicalRitual.onParticle(packet); + DojoManager.onParticle(packet); EnderNodes.onParticle(packet); } diff --git a/src/main/java/de/hysky/skyblocker/mixins/ClientWorldMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ClientWorldMixin.java new file mode 100644 index 00000000..6c10e5d2 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/ClientWorldMixin.java @@ -0,0 +1,23 @@ +package de.hysky.skyblocker.mixins; + + +import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.block.BlockState; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientWorld.class) +public class ClientWorldMixin { + + @Inject(method = "handleBlockUpdate", at = @At("RETURN")) + private void skyblocker$handleBlockUpdate(BlockPos pos, BlockState state, int flags, CallbackInfo ci) { + if (Utils.isInCrimson()) { + DojoManager.onBlockUpdate(pos.toImmutable(), state); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java index f75af09a..3c3dbd52 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/GenericContainerScreenHandlerMixin.java @@ -2,7 +2,11 @@ package de.hysky.skyblocker.mixins; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.skyblock.dungeon.partyfinder.PartyFinderScreen; +import de.hysky.skyblocker.skyblock.item.SkyblockInventoryScreen; +import de.hysky.skyblocker.utils.Utils; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; import net.minecraft.item.ItemStack; import net.minecraft.screen.GenericContainerScreenHandler; import net.minecraft.screen.ScreenHandler; @@ -22,8 +26,21 @@ public abstract class GenericContainerScreenHandlerMixin extends ScreenHandler { public void setStackInSlot(int slot, int revision, ItemStack stack) { super.setStackInSlot(slot, revision, stack); SkyblockerMod.getInstance().containerSolverManager.markDirty(); - if (MinecraftClient.getInstance().currentScreen instanceof PartyFinderScreen screen) { - screen.markDirty(); + + Screen currentScreen = MinecraftClient.getInstance().currentScreen; + switch (currentScreen) { + case PartyFinderScreen screen -> screen.markDirty(); + case GenericContainerScreen screen when screen.getTitle().getString().toLowerCase().contains("equipment") -> { + int line = slot/9; + if (line > 0 && line < 5 && slot % 9 == 1) { + boolean empty = stack.getName().getString().trim().toLowerCase().startsWith("empty"); + if (Utils.isInTheRift()) + SkyblockInventoryScreen.equipment_rift[line - 1] = empty ? ItemStack.EMPTY : stack; + else + SkyblockInventoryScreen.equipment[line - 1] = empty ? ItemStack.EMPTY : stack; + } + } + case null, default -> {} } } diff --git a/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java index 0d833c22..2194e7a8 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/InventoryScreenMixin.java @@ -1,11 +1,16 @@ package de.hysky.skyblocker.mixins; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.skyblock.itemlist.ItemListWidget; import de.hysky.skyblocker.utils.Utils; +import net.minecraft.client.gui.screen.ButtonTextures; import net.minecraft.client.gui.screen.ingame.InventoryScreen; import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TexturedButtonWidget; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -15,4 +20,16 @@ public abstract class InventoryScreenMixin { private RecipeBookWidget skyblocker$replaceRecipeBook(RecipeBookWidget original) { return SkyblockerConfigManager.get().general.itemList.enableItemList && Utils.isOnSkyblock() ? new ItemListWidget() : original; } + + @WrapOperation(method = "init", at = @At(value = "NEW", target = "(IIIILnet/minecraft/client/gui/screen/ButtonTextures;Lnet/minecraft/client/gui/widget/ButtonWidget$PressAction;)Lnet/minecraft/client/gui/widget/TexturedButtonWidget;")) + private TexturedButtonWidget skyblocker$moveButton(int x, int y, int width, int height, ButtonTextures textures, ButtonWidget.PressAction pressAction, Operation<TexturedButtonWidget> original) { + if (!Utils.isOnSkyblock() || !SkyblockerConfigManager.get().uiAndVisuals.showEquipmentInInventory) return original.call(x, y, width, height, textures, pressAction); + return new TexturedButtonWidget(x + 21, y, width, height, textures, pressAction); + } + + @WrapOperation(method = "method_19891", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/widget/ButtonWidget;setPosition(II)V")) + private void skyblocker$moveButtonWhenPressed(ButtonWidget instance, int i, int j, Operation<Void> original) { + if (!Utils.isOnSkyblock() || !SkyblockerConfigManager.get().uiAndVisuals.showEquipmentInInventory) original.call(instance, i, j); + else instance.setPosition(i + 21, j); + } } diff --git a/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java index 3abbfbcd..1493cf26 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java @@ -8,9 +8,11 @@ import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.injected.SkyblockerStack; import de.hysky.skyblocker.skyblock.PetCache.PetInfo; import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.client.MinecraftClient; import net.minecraft.component.ComponentHolder; import net.minecraft.component.type.ItemEnchantmentsComponent; import net.minecraft.item.ItemStack; @@ -108,8 +110,8 @@ public abstract class ItemStackMixin implements ComponentHolder, SkyblockerStack } @Unique - private boolean skyblocker$shouldProcess() { - return Utils.isOnSkyblock() && SkyblockerConfigManager.get().mining.enableDrillFuel && ItemUtils.hasCustomDurability((ItemStack) (Object) this); + private boolean skyblocker$shouldProcess() { // Durability bar renders atop of tooltips in ProfileViewer so disable on this screen + return !(MinecraftClient.getInstance().currentScreen instanceof ProfileViewerScreen) && Utils.isOnSkyblock() && SkyblockerConfigManager.get().mining.enableDrillFuel && ItemUtils.hasCustomDurability((ItemStack) (Object) this); } @Unique @@ -252,6 +254,18 @@ public abstract class ItemStackMixin implements ComponentHolder, SkyblockerStack return customDataString + "-LIFELINE-MANA_POOL"; } } + + case "MIDAS_SWORD" -> { + if (customData.getInt("winning_bid") >= 50000000) { + return customDataString + "_50M"; + } + } + + case "MIDAS_STAFF" -> { + if (customData.getInt("winning_bid") >= 100000000) { + return customDataString + "_100M"; + } + } } return customDataString; } diff --git a/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java b/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java index b04f958f..f91ddc86 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/MinecraftClientMixin.java @@ -1,7 +1,11 @@ package de.hysky.skyblocker.mixins; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.skyblock.item.HotbarSlotLock; import de.hysky.skyblocker.skyblock.item.ItemProtection; +import de.hysky.skyblocker.skyblock.item.SkyblockInventoryScreen; import de.hysky.skyblocker.utils.JoinWorldPlaceholderScreen; import de.hysky.skyblocker.utils.ReconfiguringPlaceholderScreen; import de.hysky.skyblocker.utils.Utils; @@ -9,8 +13,10 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.DownloadingTerrainScreen; import net.minecraft.client.gui.screen.ReconfiguringScreen; import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.InventoryScreen; import net.minecraft.client.network.ClientPlayNetworkHandler; import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.entity.player.PlayerEntity; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -54,4 +60,10 @@ public abstract class MinecraftClientMixin { private Screen modifyJoinWorld(Screen screen) { return Utils.isOnSkyblock() ? new JoinWorldPlaceholderScreen() : screen; } + + @WrapOperation(method = "handleInputEvents", at = @At(value = "NEW", target = "(Lnet/minecraft/entity/player/PlayerEntity;)Lnet/minecraft/client/gui/screen/ingame/InventoryScreen;")) + private InventoryScreen skyblocker$skyblockInventoryScreen(PlayerEntity player, Operation<InventoryScreen> original) { + if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().uiAndVisuals.showEquipmentInInventory) return new SkyblockInventoryScreen(player); + else return original.call(player); + } }
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java new file mode 100644 index 00000000..a9fae752 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java @@ -0,0 +1,22 @@ +package de.hysky.skyblocker.mixins; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.client.network.PingMeasurer; +import net.minecraft.util.profiler.MultiValueDebugSampleLogImpl; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(PingMeasurer.class) +public class PingMeasurerMixin { + + @WrapOperation(method = "onPingResult", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/profiler/MultiValueDebugSampleLogImpl;push(J)V")) + private void skyblocker$onPingResult(MultiValueDebugSampleLogImpl log, long ping, Operation<Void> operation) { + if (Utils.isInCrimson()) { + DojoManager.onPingResult(ping); + } + operation.call(log, ping); + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixins/accessors/MinecraftClientAccessor.java b/src/main/java/de/hysky/skyblocker/mixins/accessors/MinecraftClientAccessor.java new file mode 100644 index 00000000..a750ded2 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/accessors/MinecraftClientAccessor.java @@ -0,0 +1,12 @@ +package de.hysky.skyblocker.mixins.accessors; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.session.ProfileKeys; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(MinecraftClient.class) +public interface MinecraftClientAccessor { + @Accessor + ProfileKeys getProfileKeys(); +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java b/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java index d8cd6e48..8d0406cb 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/PetCache.java @@ -1,22 +1,10 @@ package de.hysky.skyblocker.skyblock; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.util.concurrent.CompletableFuture; -import java.util.Optional; - -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; - import com.google.gson.JsonParser; import com.mojang.logging.LogUtils; import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; import com.mojang.serialization.codecs.RecordCodecBuilder; - import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; @@ -26,6 +14,16 @@ import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtCompound; import net.minecraft.screen.slot.Slot; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; /** * Doesn't work with auto pet right now because thats complicated. @@ -135,12 +133,13 @@ public class PetCache { return CACHED_PETS.containsKey(uuid) && CACHED_PETS.get(uuid).containsKey(profileId) ? CACHED_PETS.get(uuid).get(profileId) : null; } - public record PetInfo(String type, double exp, String tier, Optional<String> uuid) { + public record PetInfo(String type, double exp, String tier, Optional<String> uuid, Optional<String> item) { public static final Codec<PetInfo> CODEC = RecordCodecBuilder.create(instance -> instance.group( Codec.STRING.fieldOf("type").forGetter(PetInfo::type), Codec.DOUBLE.fieldOf("exp").forGetter(PetInfo::exp), Codec.STRING.fieldOf("tier").forGetter(PetInfo::tier), - Codec.STRING.optionalFieldOf("uuid").forGetter(PetInfo::uuid)) + Codec.STRING.optionalFieldOf("uuid").forGetter(PetInfo::uuid), + Codec.STRING.optionalFieldOf("heldItem").forGetter(PetInfo::item)) .apply(instance, PetInfo::new)); private static final Codec<Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, PetInfo>>> SERIALIZATION_CODEC = Codec.unboundedMap(Codec.STRING, Codec.unboundedMap(Codec.STRING, CODEC).xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/Tips.java b/src/main/java/de/hysky/skyblocker/skyblock/Tips.java index 513dc4b7..5d983e20 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/Tips.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/Tips.java @@ -1,5 +1,6 @@ package de.hysky.skyblocker.skyblock; +import com.demonwav.mcdev.annotations.Translatable; import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.context.CommandContext; @@ -14,16 +15,16 @@ import net.minecraft.command.CommandRegistryAccess; import net.minecraft.text.ClickEvent; import net.minecraft.text.Text; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Random; import java.util.function.Supplier; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; public class Tips { - private static final Random RANDOM = new Random(); - private static int previousTipIndex = -1; - private static final List<Supplier<Text>> TIPS = List.of( + private static int currentTipIndex = 0; + private static final List<Supplier<Text>> TIPS = new ArrayList<>(List.of( getTipFactory("skyblocker.tips.customItemNames", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker custom renameItem"), getTipFactory("skyblocker.tips.customArmorDyeColors", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker custom dyeColor"), getTipFactory("skyblocker.tips.customArmorTrims", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker custom armorTrim"), @@ -36,47 +37,60 @@ public class Tips { getTipFactory("skyblocker.tips.gallery", ClickEvent.Action.OPEN_URL, "https://hysky.de/skyblocker/gallery"), getTipFactory("skyblocker.tips.itemRarityBackground", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), getTipFactory("skyblocker.tips.modMenuUpdate"), - getTipFactory("skyblocker.tips.issues", ClickEvent.Action.OPEN_URL, "https://github.com/SkyblockerMod/Skyblocker"), + getTipFactory("skyblocker.tips.issues", ClickEvent.Action.OPEN_URL, "https://github.com/SkyblockerMod/Skyblocker/issues"), getTipFactory("skyblocker.tips.beta", ClickEvent.Action.OPEN_URL, "https://github.com/SkyblockerMod/Skyblocker/actions"), + getTipFactory("skyblocker.tips.contribute", ClickEvent.Action.OPEN_URL, "https://github.com/SkyblockerMod/Skyblocker/wiki/contribute"), getTipFactory("skyblocker.tips.discord", ClickEvent.Action.OPEN_URL, "https://discord.gg/aNNJHQykck"), getTipFactory("skyblocker.tips.flameOverlay", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), getTipFactory("skyblocker.tips.wikiLookup", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), getTipFactory("skyblocker.tips.protectItem", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker protectItem"), getTipFactory("skyblocker.tips.fairySoulsEnigmaSoulsRelics", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker fairySouls"), - getTipFactory("skyblocker.tips.quickNav", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config") - ); + getTipFactory("skyblocker.tips.quickNav", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), + getTipFactory("skyblocker.tips.waypoints", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker waypoint"), + getTipFactory("skyblocker.tips.orderedWaypoints", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker waypoint ordered"), + getTipFactory("skyblocker.tips.visitorHelper"), + getTipFactory("skyblocker.tips.slotText"), + getTipFactory("skyblocker.tips.profileViewer", ClickEvent.Action.SUGGEST_COMMAND, "/pv"), + getTipFactory("skyblocker.tips.configSearch", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), + getTipFactory("skyblocker.tips.compactDamage", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), + getTipFactory("skyblocker.tips.skyblockerScreen", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker"), + getTipFactory("skyblocker.tips.tipsClick", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker tips next"), + getTipFactory("skyblocker.tips.eventNotifications", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), + getTipFactory("skyblocker.tips.signCalculator"), + getTipFactory("skyblocker.tips.calculateCommand", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker calculate"), + getTipFactory("skyblocker.tips.fancierBars", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker bars"), + getTipFactory("skyblocker.tips.crystalWaypointsShare", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker crystalWaypoints share"), + getTipFactory("skyblocker.tips.gardenMouseLock", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), + getTipFactory("skyblocker.tips.newYearCakesHelper"), + getTipFactory("skyblocker.tips.accessoryHelper"), + getTipFactory("skyblocker.tips.fancyAuctionHouseCheapHighlight") + )); private static boolean sentTip = false; - private static Supplier<Text> getTipFactory(String key) { + private static Supplier<Text> getTipFactory(@Translatable String key) { return () -> Text.translatable(key); } - private static Supplier<Text> getTipFactory(String key, ClickEvent.Action clickAction, String value) { + private static Supplier<Text> getTipFactory(@Translatable String key, ClickEvent.Action clickAction, String value) { return () -> Text.translatable(key).styled(style -> style.withClickEvent(new ClickEvent(clickAction, value))); } public static void init() { ClientCommandRegistrationCallback.EVENT.register(Tips::registerTipsCommand); SkyblockEvents.JOIN.register(Tips::sendNextTip); + Collections.shuffle(TIPS); } private static void registerTipsCommand(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) { dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("tips") .then(literal("enable").executes(Tips::enableTips)) .then(literal("disable").executes(Tips::disableTips)) - .then(literal("next").executes(Tips::nextTip)) + .then(literal("previous").executes(Tips::sendPreviousTipCommand)) + .then(literal("next").executes(Tips::sendNextTipCommand)) )); } - private static void sendNextTip() { - MinecraftClient client = MinecraftClient.getInstance(); - if (client.player != null && SkyblockerConfigManager.get().general.enableTips && !sentTip) { - client.player.sendMessage(nextTip(), false); - sentTip = true; - } - } - private static int enableTips(CommandContext<FabricClientCommandSource> context) { SkyblockerConfigManager.get().general.enableTips = true; SkyblockerConfigManager.save(); @@ -91,22 +105,52 @@ public class Tips { return Command.SINGLE_SUCCESS; } - private static int nextTip(CommandContext<FabricClientCommandSource> context) { - context.getSource().sendFeedback(nextTip()); + private static void sendNextTip() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player != null && SkyblockerConfigManager.get().general.enableTips && !sentTip) { + client.player.sendMessage(tipMessage(nextTip()), false); + sentTip = true; + } + } + + private static int sendNextTipCommand(CommandContext<FabricClientCommandSource> context) { + context.getSource().sendFeedback(tipMessage(nextTip())); return Command.SINGLE_SUCCESS; } - private static Text nextTip() { - return Constants.PREFIX.get().append(Text.translatable("skyblocker.tips.tip", nextTipInternal())) + public static Text nextTip() { + return Text.translatable("skyblocker.tips.tip", nextTipInternal()); + } + + private static Text nextTipInternal() { + currentTipIndex++; + currentTipIndex %= TIPS.size(); + return TIPS.get(currentTipIndex).get(); + } + + private static int sendPreviousTipCommand(CommandContext<FabricClientCommandSource> context) { + context.getSource().sendFeedback(tipMessage(previousTip())); + return Command.SINGLE_SUCCESS; + } + + public static Text previousTip() { + return Text.translatable("skyblocker.tips.tip", previousTipInternal()); + } + + private static Text previousTipInternal() { + currentTipIndex--; + currentTipIndex += TIPS.size(); + currentTipIndex %= TIPS.size(); + return TIPS.get(currentTipIndex).get(); + } + + private static Text tipMessage(Text tip) { + return Constants.PREFIX.get().append(tip) + .append(" ") + .append(Text.translatable("skyblocker.tips.clickPreviousTip").styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker tips previous")))) + .append(" ") .append(Text.translatable("skyblocker.tips.clickNextTip").styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker tips next")))) .append(" ") .append(Text.translatable("skyblocker.tips.clickDisable").styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker tips disable")))); } - - public static Text nextTipInternal() { - int randomInt = RANDOM.nextInt(TIPS.size()); - while (randomInt == previousTipIndex) randomInt = RANDOM.nextInt(TIPS.size()); - previousTipIndex = randomInt; - return TIPS.get(randomInt).get(); - } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java index e4f904b5..f984d751 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java @@ -209,10 +209,10 @@ public class ChocolateFactorySolver extends ContainerSolver { } Matcher costMatcher = COST_PATTERN.matcher(coachLore); - OptionalInt cost = RegexUtils.getIntFromMatcher(costMatcher, multiplierIncreaseMatcher.hasMatch() ? multiplierIncreaseMatcher.end() : 0); //Cost comes after the multiplier line + OptionalLong cost = RegexUtils.getLongFromMatcher(costMatcher, multiplierIncreaseMatcher.hasMatch() ? multiplierIncreaseMatcher.end() : 0); //Cost comes after the multiplier line if (cost.isEmpty()) return Optional.empty(); - return Optional.of(new Rabbit(totalCps / totalCpsMultiplier * (nextCpsMultiplier.getAsDouble() - currentCpsMultiplier.getAsDouble()), cost.getAsInt(), COACH_SLOT)); + return Optional.of(new Rabbit(totalCps / totalCpsMultiplier * (nextCpsMultiplier.getAsDouble() - currentCpsMultiplier.getAsDouble()), cost.getAsLong(), COACH_SLOT)); } private static Optional<Rabbit> getRabbit(ItemStack item, int slot) { @@ -227,9 +227,9 @@ public class ChocolateFactorySolver extends ContainerSolver { } Matcher costMatcher = COST_PATTERN.matcher(lore); - OptionalInt cost = RegexUtils.getIntFromMatcher(costMatcher, cpsMatcher.hasMatch() ? cpsMatcher.end() : 0); //Cost comes after the cps line + OptionalLong cost = RegexUtils.getLongFromMatcher(costMatcher, cpsMatcher.hasMatch() ? cpsMatcher.end() : 0); //Cost comes after the cps line if (cost.isEmpty()) return Optional.empty(); - return Optional.of(new Rabbit((nextCps.getAsInt() - currentCps.getAsInt())*(totalCpsMultiplier < 0 ? 1 : totalCpsMultiplier), cost.getAsInt(), slot)); + return Optional.of(new Rabbit((nextCps.getAsInt() - currentCps.getAsInt())*(totalCpsMultiplier < 0 ? 1 : totalCpsMultiplier), cost.getAsLong(), slot)); } private static Optional<ColorHighlight> getPrestigeHighlight() { @@ -244,14 +244,14 @@ public class ChocolateFactorySolver extends ContainerSolver { ItemStack item = slots.get(i); if (!item.isOf(Items.PLAYER_HEAD)) continue; String name = item.getName().getString(); - if (name.equals("CLICK ME!") || name.startsWith("GOLDEN RABBIT")) { + if (name.equals("CLICK ME!") || name.startsWith("Golden Rabbit - ")) { highlights.add(ColorHighlight.green(i)); } } return highlights; } - private record Rabbit(double cpsIncrease, int cost, int slot) { } + private record Rabbit(double cpsIncrease, long cost, int slot) { } public static final class Tooltip extends TooltipAdder { public Tooltip() { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ControlTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ControlTestHelper.java new file mode 100644 index 00000000..2f616a1e --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ControlTestHelper.java @@ -0,0 +1,77 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import net.minecraft.entity.mob.WitherSkeletonEntity; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; + +import java.awt.*; + +public class ControlTestHelper { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + + private static WitherSkeletonEntity correctWitherSkeleton; + private static Vec3d lastPos; + private static long lastUpdate; + private static Vec3d pingOffset; + private static Vec3d lastPingOffset; + + protected static void reset() { + correctWitherSkeleton = null; + lastPos = null; + lastUpdate = -1; + pingOffset = null; + lastPingOffset = null; + } + + /** + * Find the correct WitherSkeleton entity when it spawns to start tracking it + * + * @param entity spawned entity + */ + protected static void onEntitySpawn(Entity entity) { + if (entity instanceof WitherSkeletonEntity witherSkeleton && correctWitherSkeleton == null) { + correctWitherSkeleton = witherSkeleton; + } + } + + /** + * Finds where to look in 3 ticks effected by ping + */ + protected static void update() { + if (correctWitherSkeleton != null) { + //smoothly adjust the ping throughout the test + if (lastPos != null) { + lastPingOffset = pingOffset; + double ping = DojoManager.ping / 1000d; + //find distance between last position and current position of skeleton + Vec3d movementVector = correctWitherSkeleton.getPos().subtract(lastPos).multiply(1, 0.1, 1); + //adjust the vector to current ping (multiply by 1 + time in second until the next update offset by the players ping) + pingOffset = movementVector.multiply(1 + 3 / 20d + ping); + } + lastPos = correctWitherSkeleton.getPos(); + lastUpdate = System.currentTimeMillis(); + } + } + + /** + * Renders an outline around where the player should aim (assumes values are updated every 3 ticks) + * + * @param context render context + */ + protected static void render(WorldRenderContext context) { + if (CLIENT.player != null && correctWitherSkeleton != null && pingOffset != null && lastPingOffset != null) { + float tickDelta = context.tickCounter().getTickDelta(false); + //how long until net update + double updatePercent = (double) (System.currentTimeMillis() - lastUpdate) / 150; + Vec3d aimPos = correctWitherSkeleton.getEyePos().add(pingOffset.multiply(updatePercent)).add(lastPingOffset.multiply(1 - updatePercent)); + Box targetBox = new Box(aimPos.add(-0.5, -0.5, -0.5), aimPos.add(0.5, 0.5, 0.5)); + boolean playerLookingAtBox = targetBox.raycast(CLIENT.player.getCameraPosVec(tickDelta), CLIENT.player.getCameraPosVec(tickDelta).add(CLIENT.player.getRotationVec(tickDelta).multiply(30))).isPresent(); + float[] boxColor = playerLookingAtBox ? Color.GREEN.getColorComponents(new float[]{0, 0, 0}) : Color.LIGHT_GRAY.getColorComponents(new float[]{0, 0, 0}); + RenderHelper.renderOutline(context, targetBox, boxColor, 3, true); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DisciplineTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DisciplineTestHelper.java new file mode 100644 index 00000000..ab0a0781 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DisciplineTestHelper.java @@ -0,0 +1,66 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntMaps; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.minecraft.client.MinecraftClient; + +import java.util.Map; +import java.util.Objects; + +public class DisciplineTestHelper { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + + /** + * Stores what sword is needed for the name of a zombie + */ + private static final Map<String, String> SWORD_TO_NAME_LOOKUP = Map.of( + "WOOD_SWORD", "Wood", + "IRON_SWORD", "Iron", + "GOLD_SWORD", "Gold", + "DIAMOND_SWORD", "Diamond" + ); + + /** + * Stores a color related to the color of the sword: wood = brown, iron = silver, gold = gold, diamond = cyan + */ + private static final Object2IntMap<String> SWORD_TO_COLOR_LOOKUP = Object2IntMaps.unmodifiable(new Object2IntOpenHashMap<>(Map.of( + "WOOD_SWORD", 0xa52a2a, + "IRON_SWORD", 0xc0c0c0, + "GOLD_SWORD", 0xffd700, + "DIAMOND_SWORD", 0x00ffff + ))); + + /** + * Works out if a zombie should glow based on its name and the currently held item by the player + * + * @param name name of the zombie to see if it should glow + * @return if the zombie should glow + */ + protected static boolean shouldGlow(String name) { + if (CLIENT == null || CLIENT.player == null) { + return false; + } + String heldId = CLIENT.player.getMainHandStack().getSkyblockId(); + if (heldId == null) { + return false; + } + return Objects.equals(SWORD_TO_NAME_LOOKUP.get(heldId), name); + } + + /** + * gets the color linked to the currently held sword for zombies to glow + * + * @return color linked to sword + */ + protected static int getColor() { + if (DojoManager.currentChallenge != DojoManager.DojoChallenges.DISCIPLINE || CLIENT == null || CLIENT.player == null) { + return 0; + } + String heldId = CLIENT.player.getMainHandStack().getSkyblockId(); + if (heldId == null) { + return 0; + } + return SWORD_TO_COLOR_LOOKUP.getOrDefault(heldId, 0); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DojoManager.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DojoManager.java new file mode 100644 index 00000000..323c985c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/DojoManager.java @@ -0,0 +1,256 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientEntityEvents; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.fabricmc.fabric.api.event.player.AttackEntityCallback; +import net.minecraft.block.BlockState; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.packet.c2s.query.QueryPingC2SPacket; +import net.minecraft.network.packet.s2c.play.ParticleS2CPacket; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Formatting; +import net.minecraft.util.Hand; +import net.minecraft.util.Util; +import net.minecraft.util.hit.EntityHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DojoManager { + + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + private static final String START_MESSAGE = "[NPC] Master Tao: Ahhh, here we go! Let's get you into the Arena."; + private static final Pattern TEST_OF_PATTERN = Pattern.compile("\\s+Test of (\\w+) OBJECTIVES"); + private static final String CHALLENGE_FINISHED_REGEX = "\\s+CHALLENGE ((COMPLETED)|(FAILED))"; + + + protected enum DojoChallenges { + NONE("none", enabled -> false), + FORCE("Force", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableForceHelper), + STAMINA("Stamina", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableStaminaHelper), + MASTERY("Mastery", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableMasteryHelper), + DISCIPLINE("Discipline", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableDisciplineHelper), + SWIFTNESS("Swiftness", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableSwiftnessHelper), + CONTROL("Control", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableControlHelper), + TENACITY("Tenacity", enabled -> SkyblockerConfigManager.get().crimsonIsle.dojo.enableTenacityHelper); + + private final String name; + private final Predicate<Boolean> enabled; + + DojoChallenges(String name, Predicate<Boolean> enabled) { + this.name = name; + this.enabled = enabled; + } + + public static DojoChallenges from(String name) { + return Arrays.stream(DojoChallenges.values()).filter(n -> name.equals(n.name)).findFirst().orElse(NONE); + } + } + + protected static DojoChallenges currentChallenge = DojoChallenges.NONE; + public static boolean inArena = false; + protected static long ping = -1; + + public static void init() { + ClientReceiveMessageEvents.GAME.register(DojoManager::onMessage); + WorldRenderEvents.AFTER_TRANSLUCENT.register(DojoManager::render); + ClientPlayConnectionEvents.JOIN.register((_handler, _sender, _client) -> reset()); + ClientEntityEvents.ENTITY_LOAD.register(DojoManager::onEntitySpawn); + ClientEntityEvents.ENTITY_UNLOAD.register(DojoManager::onEntityDespawn); + AttackEntityCallback.EVENT.register(DojoManager::onEntityAttacked); + Scheduler.INSTANCE.scheduleCyclic(DojoManager::update, 3); + } + + private static void reset() { + inArena = false; + currentChallenge = DojoChallenges.NONE; + ForceTestHelper.reset(); + StaminaTestHelper.reset(); + MasteryTestHelper.reset(); + SwiftnessTestHelper.reset(); + ControlTestHelper.reset(); + TenacityTestHelper.reset(); + } + + /** + * works out if the player is in dojo and if so what challenge based on chat messages + * + * @param text message + * @param overlay is overlay + */ + private static void onMessage(Text text, Boolean overlay) { + if (!Utils.isInCrimson() || overlay) { + return; + } + if (Objects.equals(Formatting.strip(text.getString()), START_MESSAGE)) { + inArena = true; + //update the players ping + getPing(); + return; + } + if (!inArena) { + return; + } + if (text.getString().matches(CHALLENGE_FINISHED_REGEX)) { + reset(); + return; + } + + //look for a message saying what challenge is starting if one has not already been found + if (currentChallenge != DojoChallenges.NONE) { + return; + } + Matcher nextChallenge = TEST_OF_PATTERN.matcher(text.getString()); + if (nextChallenge.matches()) { + currentChallenge = DojoChallenges.from(nextChallenge.group(1)); + if (!currentChallenge.enabled.test(true)) { + currentChallenge = DojoChallenges.NONE; + } + } + } + + private static void getPing() { + ClientPlayNetworkHandler networkHandler = CLIENT.getNetworkHandler(); + if (networkHandler != null) { + networkHandler.sendPacket(new QueryPingC2SPacket(Util.getMeasuringTimeMs())); + } + } + + public static void onPingResult(long ping) { + DojoManager.ping = ping; + } + + private static void update() { + if (!Utils.isInCrimson() || !inArena) { + return; + } + switch (currentChallenge) { + case STAMINA -> StaminaTestHelper.update(); + case CONTROL -> ControlTestHelper.update(); + } + } + + /** + * called from the {@link de.hysky.skyblocker.skyblock.entity.MobGlow} class and checks the current challenge to see if zombies should be glowing + * + * @param name name of the zombie + * @return if the zombie should glow + */ + public static boolean shouldGlow(String name) { + if (!Utils.isInCrimson() || !inArena) { + return false; + } + return switch (currentChallenge) { + case FORCE -> ForceTestHelper.shouldGlow(name); + case DISCIPLINE -> DisciplineTestHelper.shouldGlow(name); + default -> false; + }; + } + + /** + * called from the {@link de.hysky.skyblocker.skyblock.entity.MobGlow} class and checks the current challenge to see zombie outline color + * + * @return if the zombie should glow + */ + public static int getColor() { + if (!Utils.isInCrimson() || !inArena) { + return 0xf57738; + } + return switch (currentChallenge) { + case FORCE -> ForceTestHelper.getColor(); + case DISCIPLINE -> DisciplineTestHelper.getColor(); + default -> 0xf57738; + }; + } + + /** + * when a block is updated check the current challenge and send the packet to correct helper + * + * @param pos the location of the updated block + * @param state the state of the new block + */ + public static void onBlockUpdate(BlockPos pos, BlockState state) { + if (!Utils.isInCrimson() || !inArena) { + return; + } + switch (currentChallenge) { + case MASTERY -> MasteryTestHelper.onBlockUpdate(pos, state); + case SWIFTNESS -> SwiftnessTestHelper.onBlockUpdate(pos, state); + } + } + + private static void onEntitySpawn(Entity entity, ClientWorld clientWorld) { + if (!Utils.isInCrimson() || !inArena || CLIENT == null || CLIENT.player == null) { + return; + } + // Check if within 50 blocks and 5 blocks vertically + if (entity.squaredDistanceTo(CLIENT.player) > 2500 || Math.abs(entity.getBlockY() - CLIENT.player.getBlockY()) > 5) { + return; + } + switch (currentChallenge) { + case FORCE -> ForceTestHelper.onEntitySpawn(entity); + case CONTROL -> ControlTestHelper.onEntitySpawn(entity); + case TENACITY -> TenacityTestHelper.onEntitySpawn(entity); + } + } + + private static void onEntityDespawn(Entity entity, ClientWorld clientWorld) { + if (!Utils.isInCrimson() || !inArena) { + return; + } + switch (currentChallenge) { + case FORCE -> ForceTestHelper.onEntityDespawn(entity); + case TENACITY -> TenacityTestHelper.onEntityDespawn(entity); + } + } + + private static ActionResult onEntityAttacked(PlayerEntity playerEntity, World world, Hand hand, Entity entity, EntityHitResult entityHitResult) { + if (!Utils.isInCrimson() || !inArena) { + return ActionResult.PASS; + } + if (currentChallenge == DojoChallenges.FORCE) { + ForceTestHelper.onEntityAttacked(entity); + } + return ActionResult.PASS; + } + + public static void onParticle(ParticleS2CPacket packet) { + if (!Utils.isInCrimson() || !inArena) { + return; + } + if (currentChallenge == DojoChallenges.TENACITY) { + TenacityTestHelper.onParticle(packet); + } + } + + private static void render(WorldRenderContext context) { + if (!Utils.isInCrimson() || !inArena) { + return; + } + switch (currentChallenge) { + case FORCE -> ForceTestHelper.render(context); + case STAMINA -> StaminaTestHelper.render(context); + case MASTERY -> MasteryTestHelper.render(context); + case SWIFTNESS -> SwiftnessTestHelper.render(context); + case CONTROL -> ControlTestHelper.render(context); + case TENACITY -> TenacityTestHelper.render(context); + } + } + +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ForceTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ForceTestHelper.java new file mode 100644 index 00000000..70d6a401 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/ForceTestHelper.java @@ -0,0 +1,81 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import de.hysky.skyblocker.utils.render.RenderHelper; +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.entity.Entity; +import net.minecraft.entity.mob.ZombieEntity; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.math.Vec3d; + +import java.awt.*; +import java.text.DecimalFormat; +import java.util.Map; + +public class ForceTestHelper { + + private static final DecimalFormat FORMATTER = new DecimalFormat("0.0"); + private static final int ZOMBIE_LIFE_TIME = 10100; + + private static final Object2LongOpenHashMap<ZombieEntity> zombies = new Object2LongOpenHashMap<>(); + + protected static void reset() { + zombies.clear(); + } + + /** + * If a zombie value is negative make it glow + * + * @param name zombies value + * @return if the zombie should glow + */ + protected static boolean shouldGlow(String name) { + return name.contains("-"); + } + + protected static int getColor() { + return Color.RED.getRGB(); + } + + protected static void onEntitySpawn(Entity entity) { + if (entity instanceof ZombieEntity zombie) { + zombies.put(zombie, System.currentTimeMillis() + ZOMBIE_LIFE_TIME); + } + } + + protected static void onEntityAttacked(Entity entity) { + if (entity instanceof ZombieEntity zombie) { + if (zombies.containsKey(zombie)) { + zombies.put(zombie, System.currentTimeMillis() + ZOMBIE_LIFE_TIME); //timer is reset when they are hit + } + } + } + + protected static void onEntityDespawn(Entity entity) { + if (entity instanceof ZombieEntity zombie) { + zombies.removeLong(zombie); + } + } + + protected static void render(WorldRenderContext context) { + //render times + long currentTime = System.currentTimeMillis(); + for (Map.Entry<ZombieEntity, Long> zombie : zombies.object2LongEntrySet()) { + float secondsTime = Math.max((zombie.getValue() - currentTime) / 1000f, 0); + + MutableText text = Text.literal(FORMATTER.format(secondsTime)); + if (secondsTime > 1) { + text = text.formatted(Formatting.GREEN); + } else if (secondsTime > 0) { + text = text.formatted(Formatting.YELLOW); + } else { + text = text.formatted(Formatting.RED); + } + + Vec3d labelPos = zombie.getKey().getCameraPosVec(context.tickCounter().getTickDelta(false)); + RenderHelper.renderText(context, text, labelPos, 1.5f, true); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/MasteryTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/MasteryTestHelper.java new file mode 100644 index 00000000..625b91eb --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/MasteryTestHelper.java @@ -0,0 +1,68 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import de.hysky.skyblocker.utils.render.RenderHelper; +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; + +import java.awt.*; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; + +public class MasteryTestHelper { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + private static final DecimalFormat FORMATTER = new DecimalFormat("0.00"); + /** + * How long it takes for a block to turn red + */ + private static final int BLOCK_LIFE_TIME = 6550; + + private static final List<BlockPos> blockOrder = new ArrayList<>(); + private static final Object2LongOpenHashMap<BlockPos> endTimes = new Object2LongOpenHashMap<>(); + + protected static void reset() { + blockOrder.clear(); + endTimes.clear(); + } + + protected static void onBlockUpdate(BlockPos pos, BlockState state) { + if (CLIENT == null || CLIENT.player == null) { + return; + } + if (state.isOf(Blocks.LIME_WOOL)) { + blockOrder.add(pos); + //add lifetime of a block to the time to get time when block expires + // work out how long it will take between the player firing and arrow hitting the block and to subtract from time + long travelTime = (long) (CLIENT.player.getPos().distanceTo(pos.toCenterPos()) * 1000 / 60); //an arrow speed is about 60 blocks a second from a full draw + endTimes.put(pos, System.currentTimeMillis() + BLOCK_LIFE_TIME - DojoManager.ping - travelTime); + } + if (state.isAir()) { + blockOrder.remove(pos); + endTimes.removeLong(pos); + } + } + + protected static void render(WorldRenderContext context) { + //render connecting lines + if (!blockOrder.isEmpty()) { + RenderHelper.renderLineFromCursor(context, blockOrder.getFirst().toCenterPos(), Color.LIGHT_GRAY.getColorComponents(new float[]{0, 0, 0}), 1f, 2); + } + if (blockOrder.size() >= 2) { + RenderHelper.renderLinesFromPoints(context, new Vec3d[]{blockOrder.get(0).toCenterPos(), blockOrder.get(1).toCenterPos()}, Color.LIGHT_GRAY.getColorComponents(new float[]{0, 0, 0}), 1, 2, false); + } + + //render times + long currentTime = System.currentTimeMillis(); + for (BlockPos pos : blockOrder) { + long blockEndTime = endTimes.getLong(pos); + float secondsTime = Math.max((blockEndTime - currentTime) / 1000f, 0); + RenderHelper.renderText(context, Text.literal(FORMATTER.format(secondsTime)), pos.add(0, 1, 0).toCenterPos(), 3, true); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/StaminaTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/StaminaTestHelper.java new file mode 100644 index 00000000..3f7dfe56 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/StaminaTestHelper.java @@ -0,0 +1,279 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import de.hysky.skyblocker.utils.render.RenderHelper; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.client.MinecraftClient; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.math.Vec3i; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class StaminaTestHelper { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + private static final int WALL_THRESHOLD_VALUE = 13; + private static final int WALL_HEIGHT = 5; + private static final float[] INCOMING_COLOR = new float[]{0f, 1f, 0f, 0f}; + private static final float[] OUTGOING_COLOR = new float[]{1f, 0.64f, 0f, 0f}; + + private static final List<Box> wallHoles = new ArrayList<>(); + private static final List<Box> lastHoles = new ArrayList<>(); + private static final Map<Box, HoleDirection> holeDirections = new HashMap<>(); + private static BlockPos middleBase; + + private enum HoleDirection { + POSITIVE_X, + POSITIVE_Z, + NEGATIVE_X, + NEGATIVE_Z, + NEW, + UNCHANGED + } + + protected static void reset() { + wallHoles.clear(); + lastHoles.clear(); + holeDirections.clear(); + middleBase = null; + } + + protected static void update() { + + //search the world around the player for walls 30 x 10 x 30 area centered on player + + List<BlockPos> currentBottomWallLocations = findWallBlocks(); + if (currentBottomWallLocations == null) { //stop here if the center pos has not been found + return; + } + //find walls + List<Box> walls = findWalls(currentBottomWallLocations); + + //find air then holes and add whole to list + lastHoles.clear(); + lastHoles.addAll(wallHoles); + wallHoles.clear(); + for (Box wall : walls) { + wallHoles.addAll(findHolesInBox(wall)); + } + // get direction for the holes + Map<Box, HoleDirection> lastHoleDirections = new HashMap<>(holeDirections); + holeDirections.clear(); + for (Box hole : wallHoles) { + HoleDirection holeDirection = getWholeDirection(hole); + if (holeDirection == HoleDirection.UNCHANGED) { + holeDirections.put(hole, lastHoleDirections.get(hole)); + continue; + } + holeDirections.put(hole, holeDirection); + } + } + + /** + * Locates the center of the game and once this is found scans the bottom of room for blocks that make up the walls + * + * @return list of blocks that make up the bottom of the walls + */ + private static List<BlockPos> findWallBlocks() { + if (CLIENT == null || CLIENT.player == null || CLIENT.world == null) { + return null; + } + BlockPos playerPos = CLIENT.player.getBlockPos(); + //find the center first before starting to look for walls + if (middleBase == null) { + for (int x = playerPos.getX() - 10; x < playerPos.getX() + 10; x++) { + for (int y = playerPos.getY() - 5; y < playerPos.getY(); y++) { + for (int z = playerPos.getZ() - 10; z < playerPos.getZ() + 10; z++) { + BlockPos pos = new BlockPos(x, y, z); + BlockState state = CLIENT.world.getBlockState(pos); + if (state.isOf(Blocks.CHISELED_STONE_BRICKS)) { + middleBase = pos; + return null; + } + } + } + } + return null; + } + List<BlockPos> currentBottomWallLocations = new ArrayList<>(); + for (int x = middleBase.getX() - 15; x < middleBase.getX() + 15; x++) { + for (int z = middleBase.getZ() - 15; z < middleBase.getZ() + 15; z++) { + BlockPos pos = new BlockPos(x, middleBase.getY() + 1, z); + BlockState state = CLIENT.world.getBlockState(pos); + //find the bottom of walls + if (!state.isAir()) { + currentBottomWallLocations.add(pos); + } + } + } + return currentBottomWallLocations; + } + + private static List<Box> findWalls(List<BlockPos> currentBottomWallLocations) { + Int2ObjectOpenHashMap<List<BlockPos>> possibleWallsX = new Int2ObjectOpenHashMap<>(); + Int2ObjectOpenHashMap<List<BlockPos>> possibleWallsZ = new Int2ObjectOpenHashMap<>(); + for (BlockPos block : currentBottomWallLocations) { + //add to the x walls + int x = block.getX(); + if (!possibleWallsX.containsKey(x)) { + possibleWallsX.put(x, new ArrayList<>()); + + } + possibleWallsX.get(x).add(block); + //add to the z walls + int z = block.getZ(); + if (!possibleWallsZ.containsKey(z)) { + possibleWallsZ.put(z, new ArrayList<>()); + } + possibleWallsZ.get(z).add(block); + } + + //extract only the lines that are long enough to be a wall and not from walls overlapping + List<List<BlockPos>> walls = new ArrayList<>(); + for (List<BlockPos> line : possibleWallsX.values()) { + if (line.size() >= WALL_THRESHOLD_VALUE) { + walls.add(line); + } + } + for (List<BlockPos> line : possibleWallsZ.values()) { + if (line.size() >= WALL_THRESHOLD_VALUE) { + walls.add(line); + } + } + + //final find the maximum values for each wall to output a box for them + List<Box> wallBoxes = new ArrayList<>(); + for (List<BlockPos> wall : walls) { + BlockPos minPos = wall.getFirst(); + BlockPos maxPos = wall.getFirst(); + for (BlockPos pos : wall) { + if (pos.getX() < minPos.getX()) { + minPos = new BlockPos(pos.getX(), minPos.getY(), minPos.getZ()); + } + if (pos.getZ() < minPos.getZ()) { + minPos = new BlockPos(minPos.getX(), minPos.getY(), pos.getZ()); + } + + if (pos.getX() > maxPos.getX()) { + maxPos = new BlockPos(pos.getX(), maxPos.getY(), maxPos.getZ()); + } + if (pos.getZ() > maxPos.getZ()) { + maxPos = new BlockPos(maxPos.getX(), maxPos.getY(), pos.getZ()); + } + } + //expand wall to top + maxPos = new BlockPos(maxPos.getX(), maxPos.getY() + WALL_HEIGHT, maxPos.getZ()); + + wallBoxes.add(Box.enclosing(minPos, maxPos)); + } + + return wallBoxes; + } + + private static List<Box> findHolesInBox(Box box) { + List<Box> holes = new ArrayList<>(); + if (CLIENT == null || CLIENT.player == null || CLIENT.world == null) { + return holes; + } + //get the direction vector + Vec3i wallDirection = box.getLengthX() == 1 ? new Vec3i(0, 0, 1) : new Vec3i(1, 0, 0); + //find the corners of boxes (only need 3) + List<BlockPos> topLeft = new ArrayList<>(); + List<BlockPos> topRight = new ArrayList<>(); + List<BlockPos> bottomLeft = new ArrayList<>(); + for (int z = (int) box.minZ; z < box.maxZ; z++) { + for (int x = (int) box.minX; x < box.maxX; x++) { + for (int y = (int) box.minY; y < box.maxY; y++) { + BlockPos pos = new BlockPos(x, y, z); + BlockState state = CLIENT.world.getBlockState(pos); + if (!state.isAir()) { + //do not check non-air + continue; + } + boolean top = y == box.maxY - 1 || !CLIENT.world.getBlockState(pos.add(0, 1, 0)).isAir(); + boolean bottom = !CLIENT.world.getBlockState(pos.add(0, -1, 0)).isAir(); + boolean left = !CLIENT.world.getBlockState(pos.add(wallDirection)).isAir(); + boolean right = !CLIENT.world.getBlockState(pos.subtract(wallDirection)).isAir(); + if (top) { + if (left) { + topLeft.add(pos); + } + if (right) { + topRight.add(pos); + } + } + if (bottom && left) { + bottomLeft.add(pos); + } + + } + } + } + // gets box around top of hole then expands to the bottom of hole + for (int i = 0; i < topLeft.size(); i++) { + if (topRight.size() <= i || bottomLeft.size() <= i) { + //if corners can not be found end looking + break; + } + Box hole = Box.enclosing(topLeft.get(i), topRight.get(i)); + hole = hole.stretch(0, bottomLeft.get(i).getY() - topLeft.get(i).getY(), 0); + holes.add(hole); + } + return holes; + } + + private static HoleDirection getWholeDirection(Box hole) { + //the value has not changed since last time + if (lastHoles.contains(hole)) { + return HoleDirection.UNCHANGED; + } + //check each direction to work out which way the whole is going + Box posX = hole.offset(1, 0, 0); + if (lastHoles.contains(posX)) { + return HoleDirection.POSITIVE_X; + } + Box negX = hole.offset(-1, 0, 0); + if (lastHoles.contains(negX)) { + return HoleDirection.NEGATIVE_X; + } + Box posZ = hole.offset(0, 0, 1); + if (lastHoles.contains(posZ)) { + return HoleDirection.POSITIVE_Z; + } + Box negZ = hole.offset(0, 0, -1); + if (lastHoles.contains(negZ)) { + return HoleDirection.NEGATIVE_Z; + } + // if pos can not be found mark as new + return HoleDirection.NEW; + + } + + protected static void render(WorldRenderContext context) { + if (wallHoles.isEmpty() || CLIENT == null || CLIENT.player == null) { + return; + } + BlockPos playerPos = CLIENT.player.getBlockPos(); + for (Box hole : wallHoles) { + float[] color = isHoleIncoming(hole, holeDirections.get(hole), playerPos) ? INCOMING_COLOR : OUTGOING_COLOR; + RenderHelper.renderFilled(context, new BlockPos((int) hole.minX, (int) hole.minY, (int) hole.minZ), new Vec3d(hole.getLengthX(), hole.getLengthY(), hole.getLengthZ()), color, 0.3f, false); + } + } + + private static boolean isHoleIncoming(Box holePos, HoleDirection holeDirection, BlockPos playerPos) { + return switch (holeDirection) { + case POSITIVE_X -> playerPos.getX() < holePos.minX; + case POSITIVE_Z -> playerPos.getZ() < holePos.minZ; + case NEGATIVE_X -> playerPos.getX() > holePos.maxX; + case NEGATIVE_Z -> playerPos.getZ() > holePos.maxZ; + + default -> true; + }; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/SwiftnessTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/SwiftnessTestHelper.java new file mode 100644 index 00000000..678005c4 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/SwiftnessTestHelper.java @@ -0,0 +1,34 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.util.math.BlockPos; + +public class SwiftnessTestHelper { + + private static BlockPos lastBlock; + + protected static void reset() { + lastBlock = null; + } + + protected static void onBlockUpdate(BlockPos pos, BlockState state) { + if (state.isOf(Blocks.LIME_WOOL)) { + lastBlock = pos.toImmutable(); + } + } + + /** + * Renders a green block around the newest block + * + * @param context render context + */ + protected static void render(WorldRenderContext context) { + if (lastBlock == null) { + return; + } + RenderHelper.renderFilled(context, lastBlock, new float[]{0f, 1f, 0f}, 0.5f, true); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/TenacityTestHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/TenacityTestHelper.java new file mode 100644 index 00000000..51e99fbd --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/dojo/TenacityTestHelper.java @@ -0,0 +1,100 @@ +package de.hysky.skyblocker.skyblock.crimson.dojo; + +import de.hysky.skyblocker.utils.render.RenderHelper; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import net.minecraft.entity.decoration.ArmorStandEntity; +import net.minecraft.network.packet.s2c.play.ParticleS2CPacket; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.RaycastContext; + +public class TenacityTestHelper { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + + private static final Object2ObjectOpenHashMap<ArmorStandEntity, Vec3d> fireBallsWithStartPos = new Object2ObjectOpenHashMap<>(); + private static final Object2ObjectOpenHashMap<ArmorStandEntity, Vec3d> particleOffsets = new Object2ObjectOpenHashMap<>(); + + protected static void reset() { + fireBallsWithStartPos.clear(); + particleOffsets.clear(); + } + + protected static void render(WorldRenderContext context) { + for (ArmorStandEntity fireball : fireBallsWithStartPos.keySet()) { + Vec3d lineStart = fireBallsWithStartPos.get(fireball).add(particleOffsets.getOrDefault(fireball, Vec3d.ZERO)); + Vec3d fireballPos = fireball.getPos().add(particleOffsets.getOrDefault(fireball, Vec3d.ZERO)); + + Vec3d distance = fireballPos.subtract(lineStart); + if (distance.length() > 0.02) { //if big enough gap try from start calculate and show trajectory + distance = distance.multiply(100); + Vec3d lineEnd = lineStart.add(distance); + + RenderHelper.renderLinesFromPoints(context, new Vec3d[]{lineStart, lineEnd}, new float[]{1f, 0f, 0f}, 1, 3, false); + + //get highlighted block + HitResult hitResult = raycast(lineStart, lineEnd, fireball); + if (hitResult != null && hitResult.getType() == HitResult.Type.BLOCK && hitResult instanceof BlockHitResult blockHitResult) { + RenderHelper.renderFilled(context, blockHitResult.getBlockPos(), new float[]{1f, 0f, 0f}, 0.5f, false); + } + } + } + } + + protected static HitResult raycast(Vec3d start, Vec3d end, ArmorStandEntity fireball) { + if (CLIENT == null || CLIENT.world == null) { + return null; + } + return CLIENT.world.raycast(new RaycastContext(start, end, RaycastContext.ShapeType.OUTLINE, RaycastContext.FluidHandling.ANY, fireball)); + } + + /** + * If a spawned entity is an armour stand add it to the fireballs map (assuming all armour stands are fireballs) + * + * @param entity spawned entity + */ + protected static void onEntitySpawn(Entity entity) { + if (entity instanceof ArmorStandEntity armorStand) { + fireBallsWithStartPos.put(armorStand, armorStand.getPos()); + } + } + + protected static void onEntityDespawn(Entity entity) { + if (entity instanceof ArmorStandEntity armorStand) { + fireBallsWithStartPos.remove(armorStand); + } + } + + /** + * Uses the particles spawned with the fireballs to offset from the armour stand position to get a more accurate guess of where it's going + * + * @param packet particle packet + */ + protected static void onParticle(ParticleS2CPacket packet) { + if (!ParticleTypes.FLAME.equals(packet.getParameters().getType())) { + return; + } + //get nearest fireball to particle + Vec3d particlePos = new Vec3d(packet.getX(), packet.getY(), packet.getZ()); + ArmorStandEntity neareastFireball = null; + double clostestDistance = 50; + for (ArmorStandEntity fireball : fireBallsWithStartPos.keySet()) { + double distance = fireball.getPos().distanceTo(particlePos); + if (distance < clostestDistance) { + neareastFireball = fireball; + clostestDistance = distance; + } + } + if (neareastFireball == null) { //can not find fireball near particle + return; + } + //adjust fireball offset with particle pos + Vec3d delta = particlePos.subtract(neareastFireball.getPos()); + //update values + particleOffsets.put(neareastFireball, delta); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java index 7a5abed1..d1fc08ec 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java @@ -304,7 +304,7 @@ public class DungeonScore { if (s.equals("You")) return MinecraftClient.getInstance().getSession().getUsername(); //This will be wrong if the dead player is called 'You' but that's unlikely else return s; }); - ProfileUtils.updateProfile(whoDied).thenAccept(player -> firstDeathHasSpiritPet = hasSpiritPet(player, whoDied)); + ProfileUtils.updateProfileByName(whoDied).thenAccept(player -> firstDeathHasSpiritPet = hasSpiritPet(player, whoDied)); } private static void checkMessageForWatcher(String message) { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CommissionHighlight.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CommissionHighlight.java new file mode 100644 index 00000000..de26809c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CommissionHighlight.java @@ -0,0 +1,38 @@ +package de.hysky.skyblocker.skyblock.dwarven; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.render.gui.ColorHighlight; +import de.hysky.skyblocker.utils.render.gui.ContainerSolver; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +public class CommissionHighlight extends ContainerSolver { + + public CommissionHighlight() { + super("^Commissions$"); + } + + @Override + protected boolean isEnabled() { + return SkyblockerConfigManager.get().mining.commissionHighlight; + } + + @Override + protected List<ColorHighlight> getColors(String[] groups, Int2ObjectMap<ItemStack> slots) { + List<ColorHighlight> highlights = new ArrayList<>(); + for (Int2ObjectMap.Entry<ItemStack> entry : slots.int2ObjectEntrySet()) { + ItemStack stack = entry.getValue(); + if (stack != null && stack.contains(DataComponentTypes.LORE)) { + if (ItemUtils.getLoreLineIf(stack, s -> s.contains("COMPLETED")) != null) { + highlights.add(ColorHighlight.green(entry.getIntKey())); + } + } + } + return highlights; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java index 6f4c86a7..d709181f 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java @@ -42,6 +42,12 @@ import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.arg import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; import static net.minecraft.command.CommandSource.suggestMatching; +/** + * Manager for Crystal Hollows waypoints that handles {@link #update() location detection}, + * {@link #extractLocationFromMessage(Text, Boolean) waypoints receiving}, {@link #shareWaypoint(String) sharing}, + * {@link #registerWaypointLocationCommands(CommandDispatcher, CommandRegistryAccess) commands}, and + * {@link #render(WorldRenderContext) rendering}. + */ public class CrystalsLocationsManager { private static final Logger LOGGER = LogUtils.getLogger(); private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); @@ -55,11 +61,15 @@ public class CrystalsLocationsManager { protected static Map<String, CrystalsWaypoint> activeWaypoints = new HashMap<>(); public static void init() { + // Crystal Hollows Waypoints Scheduler.INSTANCE.scheduleCyclic(CrystalsLocationsManager::update, 40); WorldRenderEvents.AFTER_TRANSLUCENT.register(CrystalsLocationsManager::render); ClientReceiveMessageEvents.GAME.register(CrystalsLocationsManager::extractLocationFromMessage); ClientCommandRegistrationCallback.EVENT.register(CrystalsLocationsManager::registerWaypointLocationCommands); ClientPlayConnectionEvents.JOIN.register((_handler, _sender, _client) -> reset()); + + // Nucleus Waypoints + WorldRenderEvents.AFTER_TRANSLUCENT.register(NucleusWaypoints::render); } private static void extractLocationFromMessage(Text message, Boolean overlay) { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/NucleusWaypoints.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/NucleusWaypoints.java new file mode 100644 index 00000000..8046ed19 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/NucleusWaypoints.java @@ -0,0 +1,61 @@ +package de.hysky.skyblocker.skyblock.dwarven; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.text.TextColor; +import net.minecraft.text.Style; +import net.minecraft.util.DyeColor; +import net.minecraft.util.math.BlockPos; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +public class NucleusWaypoints { + private static final Logger LOGGER = LoggerFactory.getLogger(NucleusWaypoints.class); + + private static class Waypoint { + BlockPos position; + String name; + DyeColor color; + + Waypoint(BlockPos position, String name, DyeColor color) { + this.position = position; + this.name = name; + this.color = color; + } + } + + private static final List<Waypoint> WAYPOINTS = List.of( + new Waypoint(new BlockPos(551, 116, 551), "Precursor Remnants", DyeColor.LIGHT_BLUE), + new Waypoint(new BlockPos(551, 116, 475), "Mithril Deposits", DyeColor.LIME), + new Waypoint(new BlockPos(475, 116, 551), "Goblin Holdout", DyeColor.ORANGE), + new Waypoint(new BlockPos(475, 116, 475), "Jungle", DyeColor.PURPLE), + new Waypoint(new BlockPos(513, 106, 524), "Nucleus", DyeColor.RED) + ); + + public static void render(WorldRenderContext context) { + try { + boolean enabled = SkyblockerConfigManager.get().mining.crystalHollows.nucleusWaypoints; + boolean inCrystalHollows = Utils.isInCrystalHollows(); + + if (enabled && inCrystalHollows) { + for (Waypoint waypoint : WAYPOINTS) { + + int rgb = waypoint.color.getFireworkColor(); + TextColor textColor = TextColor.fromRgb(rgb); + + MutableText text = Text.literal(waypoint.name).setStyle(Style.EMPTY.withColor(textColor)); + + RenderHelper.renderText(context, text, waypoint.position.toCenterPos().add(0, 5, 0), 8, true); + } + } + } catch (Exception e) { + LOGGER.error("[{}] Error occurred while rendering Nucleus waypoints. {}", LOGGER.getName(), e); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java index d6f9410b..81e328ca 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java @@ -1,6 +1,7 @@ package de.hysky.skyblocker.skyblock.entity; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; import de.hysky.skyblocker.skyblock.dungeon.LividColor; import de.hysky.skyblocker.skyblock.end.TheEnd; import de.hysky.skyblocker.utils.ItemUtils; @@ -10,6 +11,7 @@ import de.hysky.skyblocker.utils.render.culling.OcclusionCulling; import net.minecraft.entity.Entity; import net.minecraft.entity.decoration.ArmorStandEntity; import net.minecraft.entity.mob.EndermanEntity; +import net.minecraft.entity.mob.ZombieEntity; import net.minecraft.entity.passive.BatEntity; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.ItemStack; @@ -28,6 +30,7 @@ public class MobGlow { if (OcclusionCulling.getReducedCuller().isVisible(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) { String name = entity.getName().getString(); + // Dungeons if (Utils.isInDungeons() && !entity.isInvisible()) { return switch (entity) { @@ -46,6 +49,7 @@ public class MobGlow { }; } + return switch (entity) { // Rift case PlayerEntity p when Utils.isInTheRift() && !entity.isInvisible() && name.equals("Blobbercyst ") -> SkyblockerConfigManager.get().otherLocations.rift.blobbercystGlow; @@ -57,6 +61,9 @@ public class MobGlow { // Special Zelot case EndermanEntity enderman when Utils.isInTheEnd() && !entity.isInvisible() -> TheEnd.isSpecialZealot(enderman); + //dojo + case ZombieEntity zombie when Utils.isInCrimson() && DojoManager.inArena -> DojoManager.shouldGlow(getArmorStandName(zombie)); + default -> false; }; } @@ -66,6 +73,7 @@ public class MobGlow { /** * Checks if an entity is starred by checking if its armor stand contains a star in its name. + * * @param entity the entity to check. * @return true if the entity is starred, false otherwise */ @@ -74,6 +82,20 @@ public class MobGlow { return !armorStands.isEmpty() && armorStands.getFirst().getName().getString().contains("✯"); } + /** + * Returns name of entity by finding closed armor stand and getting name of that + * + * @param entity the entity to check + * @return the name string of the entities label + */ + public static String getArmorStandName(Entity entity) { + List<ArmorStandEntity> armorStands = getArmorStands(entity); + if (armorStands.isEmpty()) { + return ""; + } + return armorStands.getFirst().getName().getString(); + } + public static List<ArmorStandEntity> getArmorStands(Entity entity) { return getArmorStands(entity.getWorld(), entity.getBoundingBox()); } @@ -94,6 +116,7 @@ public class MobGlow { case EndermanEntity enderman when TheEnd.isSpecialZealot(enderman) -> Formatting.RED.getColorValue(); case ArmorStandEntity armorStand when isNukekubiHead(armorStand) -> 0x990099; + case ZombieEntity zombie when Utils.isInCrimson() && DojoManager.inArena -> DojoManager.getColor(); default -> 0xf57738; }; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java b/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java index da2a0c2f..e846a88a 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java @@ -13,7 +13,6 @@ import de.hysky.skyblocker.utils.Http; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.scheduler.Scheduler; import it.unimi.dsi.fastutil.ints.IntList; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.minecraft.client.MinecraftClient; @@ -29,6 +28,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; public class EventNotifications { private static final Logger LOGGER = LogUtils.getLogger(); @@ -39,21 +39,19 @@ public class EventNotifications { public static final IntList DEFAULT_REMINDERS = IntList.of(60, 60 * 5); - public static final Map<String, ItemStack> eventIcons = new Object2ObjectOpenHashMap<>(); - - static { - eventIcons.put("Dark Auction", new ItemStack(Items.NETHER_BRICK)); - eventIcons.put("Bonus Fishing Festival", new ItemStack(Items.FISHING_ROD)); - eventIcons.put("Bonus Mining Fiesta", new ItemStack(Items.IRON_PICKAXE)); - eventIcons.put(JACOBS, new ItemStack(Items.IRON_HOE)); - eventIcons.put("New Year Celebration", new ItemStack(Items.CAKE)); - eventIcons.put("Election Over!", new ItemStack(Items.JUKEBOX)); - eventIcons.put("Election Booth Opens", new ItemStack(Items.JUKEBOX)); - eventIcons.put("Spooky Festival", new ItemStack(Items.JACK_O_LANTERN)); - eventIcons.put("Season of Jerry", new ItemStack(Items.SNOWBALL)); - eventIcons.put("Jerry's Workshop Opens", new ItemStack(Items.SNOW_BLOCK)); - eventIcons.put("Traveling Zoo", new ItemStack(Items.HAY_BLOCK)); // change to the custom head one day - } + public static final Map<String, ItemStack> eventIcons = Map.ofEntries( + Map.entry("Dark Auction", new ItemStack(Items.NETHER_BRICK)), + Map.entry("Bonus Fishing Festival", new ItemStack(Items.FISHING_ROD)), + Map.entry("Bonus Mining Fiesta", new ItemStack(Items.IRON_PICKAXE)), + Map.entry(JACOBS, new ItemStack(Items.IRON_HOE)), + Map.entry("New Year Celebration", new ItemStack(Items.CAKE)), + Map.entry("Election Over!", new ItemStack(Items.JUKEBOX)), + Map.entry("Election Booth Opens", new ItemStack(Items.JUKEBOX)), + Map.entry("Spooky Festival", new ItemStack(Items.JACK_O_LANTERN)), + Map.entry("Season of Jerry", new ItemStack(Items.SNOWBALL)), + Map.entry("Jerry's Workshop Opens", new ItemStack(Items.SNOW_BLOCK)), + Map.entry("Traveling Zoo", new ItemStack(Items.HAY_BLOCK)) // change to the custom head one day + ); public static void init() { Scheduler.INSTANCE.scheduleCyclic(EventNotifications::timeUpdate, 20); @@ -85,7 +83,7 @@ public class EventNotifications { )); } - private static final Map<String, LinkedList<SkyblockEvent>> events = new Object2ObjectOpenHashMap<>(); + private static final Map<String, LinkedList<SkyblockEvent>> events = new ConcurrentHashMap<>(); public static Map<String, LinkedList<SkyblockEvent>> getEvents() { return events; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java b/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java index 50982d29..214ecc84 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java @@ -10,7 +10,6 @@ import com.mojang.brigadier.CommandDispatcher; import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; import com.mojang.serialization.codecs.RecordCodecBuilder; -import com.mojang.util.UndashedUuid; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.Http; @@ -44,6 +43,7 @@ import java.nio.file.Path; import java.util.Base64; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; public class MuseumItemCache { private static final Logger LOGGER = LoggerFactory.getLogger(MuseumItemCache.class); @@ -113,7 +113,7 @@ public class MuseumItemCache { String uuid = Utils.getUndashedUuid(); //Be safe about access to avoid NPEs Map<String, ProfileMuseumData> playerData = MUSEUM_ITEM_CACHE.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()); - playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY); + playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY.get()); playerData.get(profileId).collectedItemIds().add(itemId); save(); @@ -224,7 +224,7 @@ public class MuseumItemCache { if (loaded.isDone() && (!MUSEUM_ITEM_CACHE.containsKey(uuid) || !MUSEUM_ITEM_CACHE.getOrDefault(uuid, new Object2ObjectOpenHashMap<>()).containsKey(profileId))) { Map<String, ProfileMuseumData> playerData = MUSEUM_ITEM_CACHE.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()); - playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY); + playerData.putIfAbsent(profileId, ProfileMuseumData.EMPTY.get()); updateData4ProfileMember(uuid, profileId); } @@ -238,7 +238,7 @@ public class MuseumItemCache { } private record ProfileMuseumData(long lastResync, ObjectOpenHashSet<String> collectedItemIds) { - private static final ProfileMuseumData EMPTY = new ProfileMuseumData(0L, null); + private static final Supplier<ProfileMuseumData> EMPTY = () -> new ProfileMuseumData(0L, new ObjectOpenHashSet<>()); private static final long TIME_BETWEEN_RESYNCING_ALLOWED = 600_000L; private static final Codec<ProfileMuseumData> CODEC = RecordCodecBuilder.create(instance -> instance.group( Codec.LONG.fieldOf("lastResync").forGetter(ProfileMuseumData::lastResync), @@ -256,4 +256,4 @@ public class MuseumItemCache { return this.lastResync + TIME_BETWEEN_RESYNCING_ALLOWED < System.currentTimeMillis(); } } -}
\ No newline at end of file +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java new file mode 100644 index 00000000..42a52a85 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockInventoryScreen.java @@ -0,0 +1,194 @@ +package de.hysky.skyblocker.skyblock.item; + +import com.mojang.serialization.Codec; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.events.SkyblockEvents; +import de.hysky.skyblocker.mixins.accessors.SlotAccessor; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.MessageScheduler; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.InventoryScreen; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.inventory.Inventory; +import net.minecraft.inventory.SimpleInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.StringNbtReader; +import net.minecraft.nbt.visitor.StringNbtWriter; +import net.minecraft.screen.slot.Slot; +import net.minecraft.util.Identifier; +import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Opened here {@code de.hysky.skyblocker.mixins.MinecraftClientMixin#skyblocker$skyblockInventoryScreen} + * <br> + * Book button is moved here {@code de.hysky.skyblocker.mixins.InventoryScreenMixin#skyblocker} + */ +public class SkyblockInventoryScreen extends InventoryScreen { + private static final Logger LOGGER = LoggerFactory.getLogger("Equipment"); + private static final Supplier<ItemStack[]> EMPTY_EQUIPMENT = () -> new ItemStack[]{ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY, ItemStack.EMPTY}; + public static final ItemStack[] equipment = EMPTY_EQUIPMENT.get(); + public static final ItemStack[] equipment_rift = EMPTY_EQUIPMENT.get(); + private static final Codec<ItemStack[]> CODEC = ItemUtils.EMPTY_ALLOWING_ITEMSTACK_CODEC.listOf(4, 8) // min size at 4 for backwards compat + .xmap(itemStacks -> itemStacks.toArray(ItemStack[]::new), List::of).fieldOf("items").codec(); + + private static final Identifier SLOT_TEXTURE = Identifier.ofVanilla("container/slot"); + private static final Identifier EMPTY_SLOT = Identifier.of(SkyblockerMod.NAMESPACE, "equipment/empty_icon"); + private static final Path FOLDER = SkyblockerMod.CONFIG_DIR.resolve("equipment"); + + private final Slot[] equipmentSlots = new Slot[4]; + + private static void save(String profileId) { + try { + Files.createDirectories(FOLDER); + } catch (IOException e) { + LOGGER.error("[Skyblocker] Failed to create folder for equipment!", e); + } + Path resolve = FOLDER.resolve(profileId + ".nbt"); + + try (BufferedWriter writer = Files.newBufferedWriter(resolve)) { + + writer.write(new StringNbtWriter().apply(CODEC.encodeStart(NbtOps.INSTANCE, ArrayUtils.addAll(equipment, equipment_rift)).getOrThrow())); + } catch (Exception e) { + LOGGER.error("[Skyblocker] Failed to save Equipment data", e); + } + } + + private static void load(String profileId) { + Path resolve = FOLDER.resolve(profileId + ".nbt"); + CompletableFuture.supplyAsync(() -> { + try (BufferedReader reader = Files.newBufferedReader(resolve)) { + return CODEC.parse(NbtOps.INSTANCE, StringNbtReader.parse(reader.lines().collect(Collectors.joining()))).getOrThrow(); + } catch (NoSuchFileException ignored) { + } catch (Exception e) { + LOGGER.error("[Skyblocker] Failed to load Equipment data", e); + } + return EMPTY_EQUIPMENT.get(); + // Schedule on main thread to avoid any async weirdness + }).thenAccept(itemStacks -> MinecraftClient.getInstance().execute(() -> { + System.arraycopy(itemStacks, 0, equipment, 0, Math.min(itemStacks.length, 4)); + if (itemStacks.length <= 4) return; + System.arraycopy(itemStacks, 4, equipment_rift, 0, Math.clamp(itemStacks.length - 4, 0, 4)); + })); + } + + public static void initEquipment() { + + SkyblockEvents.PROFILE_CHANGE.register(((prevProfileId, profileId) -> { + if (!prevProfileId.isEmpty()) CompletableFuture.runAsync(() -> save(prevProfileId)).thenRun(() -> load(profileId)); + else load(profileId); + })); + + ClientLifecycleEvents.CLIENT_STOPPING.register(client1 -> { + String profileId = Utils.getProfileId(); + if (!profileId.isBlank()) { + CompletableFuture.runAsync(() -> save(profileId)); + } + }); + } + + public SkyblockInventoryScreen(PlayerEntity player) { + super(player); + SimpleInventory inventory = new SimpleInventory(Utils.isInTheRift() ? equipment_rift: equipment); + + Slot slot = handler.slots.get(45); + ((SlotAccessor) slot).setX(slot.x + 21); + for (int i = 0; i < 4; i++) { + equipmentSlots[i] = new EquipmentSlot(inventory, i, 77, 8 + i * 18); + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + for (Slot equipmentSlot : equipmentSlots) { + if (isPointWithinBounds(equipmentSlot.x, equipmentSlot.y, 16, 16, mouseX, mouseY)) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown("/equipment"); + return true; + } + } + return super.mouseClicked(mouseX, mouseY, button); + } + + /** + * Draws the equipment slots in the foreground layer after vanilla slots are drawn + * in {@link net.minecraft.client.gui.screen.ingame.HandledScreen#render(DrawContext, int, int, float) HandledScreen#render(DrawContext, int, int, float)}. + */ + @Override + protected void drawForeground(DrawContext context, int mouseX, int mouseY) { + for (Slot equipmentSlot : equipmentSlots) { + drawSlot(context, equipmentSlot); + if (isPointWithinBounds(equipmentSlot.x, equipmentSlot.y, 16, 16, mouseX, mouseY)) drawSlotHighlight(context, equipmentSlot.x, equipmentSlot.y, 0); + } + + super.drawForeground(context, mouseX, mouseY); + } + + @Override + protected void drawMouseoverTooltip(DrawContext context, int x, int y) { + super.drawMouseoverTooltip(context, x, y); + if (!handler.getCursorStack().isEmpty()) return; + for (Slot equipmentSlot : equipmentSlots) { + if (isPointWithinBounds(equipmentSlot.x, equipmentSlot.y, 16, 16, x, y) && equipmentSlot.hasStack()) { + ItemStack itemStack = equipmentSlot.getStack(); + context.drawTooltip(this.textRenderer, this.getTooltipFromItem(itemStack), itemStack.getTooltipData(), x, y); + } + } + } + + @Override + public void removed() { + super.removed(); + // put the handler back how it was, the handler is the same while the player is alive/in the same world + Slot slot = handler.slots.get(45); + ((SlotAccessor) slot).setX(slot.x - 21); + } + + @Override + protected void drawBackground(DrawContext context, float delta, int mouseX, int mouseY) { + super.drawBackground(context, delta, mouseX, mouseY); + for (int i = 0; i < 4; i++) { + context.drawGuiTexture(SLOT_TEXTURE, x + 76 + (i == 3 ? 21 : 0), y + 7 + i * 18, 18, 18); + } + } + + @Override + protected void drawSlot(DrawContext context, Slot slot) { + super.drawSlot(context, slot); + if (slot instanceof EquipmentSlot && !slot.hasStack()) { + context.drawGuiTexture(EMPTY_SLOT, slot.x, slot.y, 16, 16); + } + } + + private static class EquipmentSlot extends Slot { + + public EquipmentSlot(Inventory inventory, int index, int x, int y) { + super(inventory, index, x, y); + } + + @Override + public boolean canTakeItems(PlayerEntity playerEntity) { + return false; + } + + @Override + public boolean canInsert(ItemStack stack) { + return false; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java index c7ea17dc..d94d6405 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java @@ -48,7 +48,7 @@ public class CommunityShopAdder extends SlotTextAdder { String lastLine = lore.getLast().getString(); return List.of(SlotText.bottomLeft(switch (lastLine) { case "Maxed out!" -> Text.literal("Max").withColor(0xfab387); - case "Currently upgrading!" -> Text.literal("⏰").withColor(0xf9e2af).formatted(Formatting.BOLD); + case "Currently upgrading!", "Click to instantly upgrade!" -> Text.literal("⏰").withColor(0xf9e2af).formatted(Formatting.BOLD); case "Click to claim!" -> Text.literal("✅").withColor(0xa6e3a1).formatted(Formatting.BOLD); default -> Text.literal(String.valueOf(RomanNumerals.romanToDecimal(roman))).withColor(0xcba6f7); })); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java index 97a73a80..955ebc87 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java @@ -58,6 +58,7 @@ public class ItemTooltip { case "CRIMSON_HELMET", "CRIMSON_CHESTPLATE", "CRIMSON_LEGGINGS", "CRIMSON_BOOTS", "AURORA_HELMET", "AURORA_CHESTPLATE", "AURORA_LEGGINGS", "AURORA_BOOTS", "TERROR_HELMET", "TERROR_CHESTPLATE", "TERROR_LEGGINGS", "TERROR_BOOTS" -> apiId = id; + case "MIDAS_SWORD", "MIDAS_STAFF" -> apiId = id; default -> apiId = apiId.replace(":", "-"); } return apiId; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java index 7eda7646..01ffc144 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemStackBuilder.java @@ -21,8 +21,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; public class ItemStackBuilder { - private static final Pattern SKULL_UUID_PATTERN = Pattern.compile("(?<=SkullOwner:\\{)Id:\"(.{36})\""); - private static final Pattern SKULL_TEXTURE_PATTERN = Pattern.compile("(?<=Properties:\\{textures:\\[0:\\{Value:)\"(.+?)\""); + public static final Pattern SKULL_UUID_PATTERN = Pattern.compile("(?<=SkullOwner:\\{)Id:\"(.{36})\""); + public static final Pattern SKULL_TEXTURE_PATTERN = Pattern.compile("(?<=Properties:\\{textures:\\[0:\\{Value:)\"(.+?)\""); private static final Pattern COLOR_PATTERN = Pattern.compile("color:(\\d+)"); private static final Pattern EXPLOSION_COLOR_PATTERN = Pattern.compile("\\{Explosion:\\{(?:Type:[0-9a-z]+,)?Colors:\\[(?<color>[0-9]+)]\\}"); private static Map<String, Map<Rarity, PetNumbers>> petNums; @@ -138,7 +138,7 @@ public class ItemStackBuilder { return list; } - private static String injectData(String string, List<Pair<String, String>> injectors) { + public static String injectData(String string, List<Pair<String, String>> injectors) { for (Pair<String, String> injector : injectors) { string = string.replaceAll(injector.getLeft(), injector.getRight()); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java b/src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java new file mode 100644 index 00000000..7131a567 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/mayors/JerryTimer.java @@ -0,0 +1,37 @@ +package de.hysky.skyblocker.skyblock.mayors; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvents; +import net.minecraft.text.HoverEvent; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +public final class JerryTimer { + private JerryTimer() { + } + public static void init() { + //Example message: "§b ☺ §eThere is a §aGreen Jerry§e!" + //There are various formats, all of which start with the "§b ☺ " prefix and contain the word "<color> Jerry" + ClientReceiveMessageEvents.GAME.register((message, overlay) -> { + if (overlay || !Utils.getMayor().equals("Jerry") || !SkyblockerConfigManager.get().helpers.jerry.enableJerryTimer) return; + String text = message.getString(); + //This part of hypixel still uses legacy text formatting, so we can't directly check for the actual text + if (!text.startsWith("§b ☺ ") || !text.contains("Jerry")) return; + HoverEvent hoverEvent = message.getStyle().getHoverEvent(); + if (hoverEvent == null || hoverEvent.getAction() != HoverEvent.Action.SHOW_TEXT) return; + ClientPlayerEntity player = MinecraftClient.getInstance().player; + Scheduler.INSTANCE.schedule(() -> { + if (player == null || !Utils.isOnSkyblock()) return; + player.sendMessage(Constants.PREFIX.get().append(Text.literal("Jerry cooldown is over!")).formatted(Formatting.GREEN), false); + player.playSoundToPlayer(SoundEvents.ENTITY_VILLAGER_TRADE, SoundCategory.NEUTRAL, 100f, 1.0f); + }, 20*60*6); // 6 minutes + }); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerNavButton.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerNavButton.java new file mode 100644 index 00000000..d867a0e6 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerNavButton.java @@ -0,0 +1,65 @@ +package de.hysky.skyblocker.skyblock.profileviewer; + +import com.mojang.blaze3d.systems.RenderSystem; +import de.hysky.skyblocker.skyblock.profileviewer.utils.SkullCreator; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.item.ItemStack; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.util.Map; + +public class ProfileViewerNavButton extends ClickableWidget { + private final static Identifier BUTTON_TEXTURES_TOGGLED = Identifier.of("container/creative_inventory/tab_top_selected_2"); + private final static Identifier BUTTON_TEXTURES = Identifier.of("container/creative_inventory/tab_top_unselected_2"); + private boolean toggled; + private final int index; + private final ProfileViewerScreen screen; + private final ItemStack icon; + + private static final Map<String, ItemStack> HEAD_ICON = Map.ofEntries( + Map.entry("Skills", Ico.IRON_SWORD), + Map.entry("Slayers", SkullCreator.createSkull("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHBzOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzkzMzZkN2NjOTVjYmY2Njg5ZjVlOGM5NTQyOTRlYzhkMWVmYzQ5NGE0MDMxMzI1YmI0MjdiYzgxZDU2YTQ4NGQifX19")), + Map.entry("Pets", Ico.BONE), + Map.entry("Dungeons", SkullCreator.createSkull("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHBzOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzliNTY4OTViOTY1OTg5NmFkNjQ3ZjU4NTk5MjM4YWY1MzJkNDZkYjljMWIwMzg5YjhiYmViNzA5OTlkYWIzM2QifX19")), + Map.entry("Inventories", Ico.E_CHEST), + Map.entry("Collections", Ico.PAINTING) + ); + + public ProfileViewerNavButton(ProfileViewerScreen screen, String tabName, int index, boolean toggled) { + super(-100, -100, 28, 32, Text.empty()); + this.screen = screen; + this.toggled = toggled; + this.index = index; + this.icon = HEAD_ICON.getOrDefault(tabName, Ico.BARRIER); + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + RenderSystem.disableDepthTest(); + + context.drawGuiTexture(toggled ? BUTTON_TEXTURES_TOGGLED : BUTTON_TEXTURES, this.getX(), this.getY(), this.width, this.height - ((this.toggled) ? 0 : 4)); + context.drawItem(this.icon, this.getX() + 6, this.getY() + (this.toggled ? 7 : 9)); + + RenderSystem.enableDepthTest(); + } + + @Override + public void onClick(double mouseX, double mouseY) { + screen.onNavButtonClick(this); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} + + public void setToggled(boolean toggled) { + this.toggled = toggled; + } + + public int getIndex() { + return index; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerPage.java new file mode 100644 index 00000000..f5a5ec40 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerPage.java @@ -0,0 +1,19 @@ +package de.hysky.skyblocker.skyblock.profileviewer; + +import de.hysky.skyblocker.skyblock.profileviewer.utils.SubPageSelectButton; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.ClickableWidget; + +import java.util.List; + +public interface ProfileViewerPage { + void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY); + default List<ClickableWidget> getButtons() { + return null; + } + default void onNavButtonClick(SubPageSelectButton selectButton) {} + default void markWidgetsAsVisible() {} + default void markWidgetsAsInvisible() {} + default void nextPage() {} + default void previousPage() {} +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java new file mode 100644 index 00000000..1d0b21ca --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerScreen.java @@ -0,0 +1,230 @@ +package de.hysky.skyblocker.skyblock.profileviewer; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.mixins.accessors.SkullBlockEntityAccessor; +import de.hysky.skyblocker.skyblock.profileviewer.collections.CollectionsPage; +import de.hysky.skyblocker.skyblock.profileviewer.dungeons.DungeonsPage; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.InventoryPage; +import de.hysky.skyblocker.skyblock.profileviewer.skills.SkillsPage; +import de.hysky.skyblocker.skyblock.profileviewer.slayers.SlayersPage; +import de.hysky.skyblocker.utils.ApiUtils; +import de.hysky.skyblocker.utils.Http; +import de.hysky.skyblocker.utils.ProfileUtils; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.network.OtherClientPlayerEntity; +import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.client.util.SkinTextures; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerModelPart; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.*; +import java.io.IOException; +import java.util.List; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import static net.minecraft.client.gui.screen.ingame.InventoryScreen.drawEntity; + +public class ProfileViewerScreen extends Screen { + public static final Logger LOGGER = LoggerFactory.getLogger(ProfileViewerScreen.class); + private static final Text TITLE = Text.of("Skyblocker Profile Viewer"); + private static final String HYPIXEL_COLLECTIONS = "https://api.hypixel.net/v2/resources/skyblock/collections"; + private static final Object2ObjectOpenHashMap<String, Map<String, ?>> COLLECTIONS_CACHE = new Object2ObjectOpenHashMap<>(); + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/base_plate.png"); + private static final int GUI_WIDTH = 322; + private static final int GUI_HEIGHT = 180; + + private String playerName; + private JsonObject hypixelProfile; + private JsonObject playerProfile; + + private int activePage = 0; + private static final String[] PAGE_NAMES = {"Skills", "Slayers", "Dungeons", "Inventories", "Collections"}; + private final ProfileViewerPage[] profileViewerPages = new ProfileViewerPage[PAGE_NAMES.length]; + private final List<ProfileViewerNavButton> profileViewerNavButtons = new ArrayList<>(); + private OtherClientPlayerEntity entity; + private ProfileViewerTextWidget textWidget; + + public ProfileViewerScreen(String username) { + super(TITLE); + fetchPlayerData(username).thenRun(this::initialisePagesAndWidgets); + + for (int i = 0; i < PAGE_NAMES.length; i++) { + profileViewerNavButtons.add(new ProfileViewerNavButton(this, PAGE_NAMES[i], i, i == 0)); + } + } + + private void initialisePagesAndWidgets() { + textWidget = new ProfileViewerTextWidget(hypixelProfile, playerProfile); + + CompletableFuture<Void> skillsFuture = CompletableFuture.runAsync(() -> profileViewerPages[0] = new SkillsPage(hypixelProfile, playerProfile)); + CompletableFuture<Void> slayersFuture = CompletableFuture.runAsync(() -> profileViewerPages[1] = new SlayersPage(playerProfile)); + CompletableFuture<Void> dungeonsFuture = CompletableFuture.runAsync(() -> profileViewerPages[2] = new DungeonsPage(playerProfile)); + CompletableFuture<Void> inventoriesFuture = CompletableFuture.runAsync(() -> profileViewerPages[3] = new InventoryPage(playerProfile)); + CompletableFuture<Void> collectionsFuture = CompletableFuture.runAsync(() -> profileViewerPages[4] = new CollectionsPage(hypixelProfile, playerProfile)); + + CompletableFuture.allOf(skillsFuture, slayersFuture, dungeonsFuture, inventoriesFuture, collectionsFuture) + .thenRun(() -> { + synchronized (this) { + clearAndInit(); + } + }); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + synchronized (this) { + super.render(context, mouseX, mouseY, delta); + } + + int rootX = width / 2 - GUI_WIDTH / 2; + int rootY = height / 2 - GUI_HEIGHT / 2 + 5; + + context.drawTexture(TEXTURE, rootX, rootY, 0, 0, GUI_WIDTH, GUI_HEIGHT, GUI_WIDTH, GUI_HEIGHT); + for (ProfileViewerNavButton button : profileViewerNavButtons) { + button.setX(rootX + button.getIndex() * 28 + 4); + button.setY(rootY - 28); + button.render(context, mouseX, mouseY, delta); + } + + if (textWidget != null) textWidget.render(context, textRenderer, rootX + 8, rootY + 120); + drawPlayerEntity(context, playerName != null ? playerName : "Loading...", rootX, rootY, mouseX, mouseY); + + if (profileViewerPages[activePage] != null) { + profileViewerPages[activePage].markWidgetsAsVisible(); + profileViewerPages[activePage].render(context, mouseX, mouseY, delta, rootX + 93, rootY + 7); + } else { + context.drawText(textRenderer, "Loading...", rootX + 180, rootY + 80, Color.WHITE.getRGB(), true); + } + } + + private void drawPlayerEntity(DrawContext context, String username, int rootX, int rootY, int mouseX, int mouseY) { + if (entity != null) + drawEntity(context, rootX + 9, rootY + 16, rootX + 89, rootY + 124, 42, 0.0625F, mouseX, mouseY, entity); + context.drawCenteredTextWithShadow(textRenderer, username.length() > 15 ? username.substring(0, 15) : username, rootX + 47, rootY + 14, Color.WHITE.getRGB()); + + } + + private CompletableFuture<Void> fetchPlayerData(String username) { + CompletableFuture<Void> profileFuture = ProfileUtils.fetchFullProfile(username).thenAccept(profiles -> { + this.hypixelProfile = profiles.getAsJsonArray("profiles").asList().stream() + .map(JsonElement::getAsJsonObject) + .filter(profile -> profile.getAsJsonPrimitive("selected").getAsBoolean()) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No selected profile found!")); + + this.playerProfile = hypixelProfile.getAsJsonObject("members").get(ApiUtils.name2Uuid(username)).getAsJsonObject(); + }); + + CompletableFuture<Void> minecraftProfileFuture = SkullBlockEntityAccessor.invokeFetchProfileByName(username).thenAccept(profile -> { + this.playerName = profile.get().getName(); + entity = new OtherClientPlayerEntity(MinecraftClient.getInstance().world, profile.get()) { + @Override + public SkinTextures getSkinTextures() { + PlayerListEntry playerListEntry = new PlayerListEntry(profile.get(), false); + return playerListEntry.getSkinTextures(); + } + + @Override + public boolean isPartVisible(PlayerModelPart modelPart) { + return !(modelPart.getName().equals(PlayerModelPart.CAPE.getName())); + } + + @Override + public boolean isInvisibleTo(PlayerEntity player) { + return true; + } + }; + entity.setCustomNameVisible(false); + }).exceptionally(ex -> { + this.playerName = "User not found"; + return null; + }); + + return CompletableFuture.allOf(profileFuture, minecraftProfileFuture); + } + + public void onNavButtonClick(ProfileViewerNavButton clickedButton) { + if (profileViewerPages[activePage] != null) profileViewerPages[activePage].markWidgetsAsInvisible(); + for (ProfileViewerNavButton button : profileViewerNavButtons) { + button.setToggled(false); + } + activePage = clickedButton.getIndex(); + clickedButton.setToggled(true); + } + + @Override + public void init() { + profileViewerNavButtons.forEach(this::addDrawableChild); + for (ProfileViewerPage profileViewerPage : profileViewerPages) { + if (profileViewerPage != null && profileViewerPage.getButtons() != null) { + for (ClickableWidget button : profileViewerPage.getButtons()) { + if (button != null) addDrawableChild(button); + } + } + } + } + + public static void initClass() { + fetchCollectionsData(); // caching on launch + + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { + LiteralArgumentBuilder<FabricClientCommandSource> literalArgumentBuilder = ClientCommandManager.literal("pv") + .then(ClientCommandManager.argument("username", StringArgumentType.string()) + .executes(Scheduler.queueOpenScreenFactoryCommand(context -> new ProfileViewerScreen(StringArgumentType.getString(context, "username")))) + ) + .executes(Scheduler.queueOpenScreenCommand(() -> new ProfileViewerScreen(MinecraftClient.getInstance().getSession().getUsername()))); + dispatcher.register(literalArgumentBuilder); + dispatcher.register(ClientCommandManager.literal(SkyblockerMod.NAMESPACE).then(literalArgumentBuilder)); + }); + } + + @NotNull + public static Map<String, Map<String, ?>> fetchCollectionsData() { + if (!COLLECTIONS_CACHE.isEmpty()) return COLLECTIONS_CACHE; + try { + JsonObject jsonObject = JsonParser.parseString(Http.sendGetRequest(HYPIXEL_COLLECTIONS)).getAsJsonObject(); + if (jsonObject.get("success").getAsBoolean()) { + Map<String, String[]> collectionsMap = new HashMap<>(); + Map<String, List<Integer>> tierRequirementsMap = new HashMap<>(); + JsonObject collections = jsonObject.getAsJsonObject("collections"); + collections.entrySet().forEach(entry -> { + String category = entry.getKey(); + JsonObject itemsObject = entry.getValue().getAsJsonObject().getAsJsonObject("items"); + String[] items = itemsObject.keySet().toArray(new String[0]); + collectionsMap.put(category, items); + itemsObject.entrySet().forEach(itemEntry -> { + List<Integer> tierReqs = new ArrayList<>(); + itemEntry.getValue().getAsJsonObject().getAsJsonArray("tiers").forEach(req -> + tierReqs.add(req.getAsJsonObject().get("amountRequired").getAsInt())); + tierRequirementsMap.put(itemEntry.getKey(), tierReqs); + }); + }); + COLLECTIONS_CACHE.put("COLLECTIONS", collectionsMap); + COLLECTIONS_CACHE.put("TIER_REQS", tierRequirementsMap); + return COLLECTIONS_CACHE; + } + } catch (IOException | InterruptedException e) { + LOGGER.error("[Skyblocker Profile Viewer] Failed to fetch collections data", e); + } + return Collections.emptyMap(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerTextWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerTextWidget.java new file mode 100644 index 00000000..4ee2dbba --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/ProfileViewerTextWidget.java @@ -0,0 +1,55 @@ +package de.hysky.skyblocker.skyblock.profileviewer; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Colors; + +public class ProfileViewerTextWidget { + private static final int ROW_GAP = 9; + + private String PROFILE_NAME = "UNKNOWN"; + private int SKYBLOCK_LEVEL = 0; + private double PURSE = 0; + private double BANK = 0; + + public ProfileViewerTextWidget(JsonObject hypixelProfile, JsonObject playerProfile){ + try { + this.PROFILE_NAME = hypixelProfile.get("cute_name").getAsString(); + this.SKYBLOCK_LEVEL = playerProfile.getAsJsonObject("leveling").get("experience").getAsInt() / 100; + this.PURSE = playerProfile.getAsJsonObject("currencies").get("coin_purse").getAsDouble(); + this.BANK = hypixelProfile.getAsJsonObject("banking").get("balance").getAsDouble(); + } catch (Exception ignored) {} + } + + public void render(DrawContext context, TextRenderer textRenderer, int root_x, int root_y){ + // Profile Icon + MatrixStack matrices = context.getMatrices(); + matrices.push(); + matrices.scale(0.75f, 0.75f, 1); + int rootAdjustedX = (int) ((root_x) / 0.75f); + int rootAdjustedY = (int) ((root_y) / 0.75f); + context.drawItem(Ico.PAINTING, rootAdjustedX, rootAdjustedY); + matrices.pop(); + + context.drawText(textRenderer, "§n"+PROFILE_NAME, root_x + 14, root_y + 3, Colors.WHITE, true); + context.drawText(textRenderer, "§aLevel:§r " + SKYBLOCK_LEVEL, root_x + 2, root_y + 6 + ROW_GAP, Colors.WHITE, true); + context.drawText(textRenderer, "§6Purse:§r " + formatCoins(PURSE), root_x + 2, root_y + 6 + ROW_GAP * 2, Colors.WHITE, true); + context.drawText(textRenderer, "§6Bank:§r " + formatCoins(BANK), root_x + 2, root_y + 6 + ROW_GAP * 3, Colors.WHITE, true); + context.drawText(textRenderer, "§6NW:§r " + "Soon™", root_x + 2, root_y + 6 + ROW_GAP * 4, Colors.WHITE, true ); + } + + private String formatCoins(double amount) { + if (amount >= 1_000_000_000) { + return String.format("%.4gB", amount / 1_000_000_000); + } else if (amount >= 1_000_000) { + return String.format("%.4gM", amount / 1_000_000); + } else if (amount >= 1_000) { + return String.format("%.4gK", amount / 1_000); + } else { + return String.valueOf((int)(amount)); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/CollectionsPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/CollectionsPage.java new file mode 100644 index 00000000..b77c3e7a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/CollectionsPage.java @@ -0,0 +1,100 @@ +package de.hysky.skyblocker.skyblock.profileviewer.collections; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import de.hysky.skyblocker.skyblock.profileviewer.utils.SubPageSelectButton; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.item.ItemStack; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class CollectionsPage implements ProfileViewerPage { + private static final String[] COLLECTION_CATEGORIES = {"MINING", "FARMING", "COMBAT", "FISHING", "FORAGING", "RIFT"}; + private static final int TOTAL_HEIGHT = 165; + private static final Map<String, ItemStack> ICON_MAP = Map.ofEntries( + Map.entry("MINING", Ico.STONE_PICKAXE), + Map.entry("FARMING", Ico.GOLDEN_HOE), + Map.entry("COMBAT", Ico.STONE_SWORD), + Map.entry("FISHING", Ico.FISH_ROD), + Map.entry("FORAGING", Ico.JUNGLE_SAPLING), + // Map.entry("BOSS", Ico.WITHER), Not currently part of Collections API so skipping for now + Map.entry("RIFT", Ico.MYCELIUM) + ); + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + + private final GenericCategory[] collections = new GenericCategory[COLLECTION_CATEGORIES.length]; + private final List<SubPageSelectButton> collectionSelectButtons = new ArrayList<>(); + private int activePage = 0; + + + public CollectionsPage(JsonObject hProfile, JsonObject pProfile) { + for (int i = 0; i < COLLECTION_CATEGORIES.length; i++) { + try { + collectionSelectButtons.add(new SubPageSelectButton(this, -100, 0, i, ICON_MAP.getOrDefault(COLLECTION_CATEGORIES[i], Ico.BARRIER))); + collections[i] = new GenericCategory(hProfile, pProfile, COLLECTION_CATEGORIES[i]); + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Error creating Collections Page", e); + } + } + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + int startingY = rootY + (TOTAL_HEIGHT - collectionSelectButtons.size() * 21) / 2; + for (int i = 0; i < collectionSelectButtons.size(); i++) { + collectionSelectButtons.get(i).setX(rootX); + collectionSelectButtons.get(i).setY(startingY + i * 21); + collectionSelectButtons.get(i).render(context, mouseX, mouseY, delta); + } + + if (collections[activePage] == null) { + context.drawText(textRenderer, "No data...", rootX + 92, rootY + 72, Color.DARK_GRAY.getRGB(), false); + return; + } + + collections[activePage].markWidgetsAsVisible(); + collections[activePage].render(context, mouseX, mouseY, delta, rootX + 35, rootY + 6); + } + + public void onNavButtonClick(SubPageSelectButton selectButton) { + if (collections[activePage] != null) collections[activePage].markWidgetsAsInvisible(); + for (SubPageSelectButton button : collectionSelectButtons) { + button.setToggled(false); + } + activePage = selectButton.getIndex(); + selectButton.setToggled(true); + } + + @Override + public List<ClickableWidget> getButtons() { + List<ClickableWidget> clickableWidgets = new ArrayList<>(collectionSelectButtons); + for (ProfileViewerPage page : collections) { + if (page != null && page.getButtons() != null) clickableWidgets.addAll(page.getButtons()); + } + return clickableWidgets; + } + + @Override + public void markWidgetsAsVisible() { + for (SubPageSelectButton button : collectionSelectButtons) { + button.visible = true; + button.active = true; + } + } + + @Override + public void markWidgetsAsInvisible() { + for (SubPageSelectButton button : collectionSelectButtons) { + button.visible = false; + button.active = false; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/GenericCategory.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/GenericCategory.java new file mode 100644 index 00000000..ef26332e --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/collections/GenericCategory.java @@ -0,0 +1,136 @@ +package de.hysky.skyblocker.skyblock.profileviewer.collections; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.NEURepoManager; +import io.github.moulberry.repo.data.NEUItem; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.LoreComponent; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.tooltip.TooltipType; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.text.NumberFormat; +import java.util.List; +import java.util.*; + +import static de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen.fetchCollectionsData; + +public class GenericCategory implements ProfileViewerPage { + private final String category; + private final LinkedList<ItemStack> collections = new LinkedList<>(); + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final NumberFormat FORMATTER = NumberFormat.getInstance(Locale.US); + private static final Identifier BUTTON_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/button_icon_toggled.png"); + private static final int COLUMN_GAP = 26; + private static final int ROW_GAP = 34; + private static final int COLUMNS = 7; + + private final Map<String, String[]> collectionsMap; + private final Map<String, List<Integer>> tierRequirementsMap; + private final Map<String, String> ICON_TRANSLATION = Map.ofEntries( + Map.entry("MUSHROOM_COLLECTION", "RED_MUSHROOM")); + private final String[] ROMAN_NUMERALS = {"-", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII", "XIII", "XIV", "XV", "XVI", "XVII", "XVIII", "XIX", "XX"}; + + public GenericCategory(JsonObject hProfile, JsonObject pProfile, String collection) { + Map<String, Map<String, ?>> fetchedData = fetchCollectionsData(); + //noinspection unchecked + collectionsMap = (Map<String, String[]>) fetchedData.get("COLLECTIONS"); + //noinspection unchecked + tierRequirementsMap = (Map<String, List<Integer>>) fetchedData.get("TIER_REQS"); + this.category = collection; + setupItemStacks(hProfile, pProfile); + } + + private int calculateTier(int achieved, List<Integer> requirements) { + return (int) requirements.stream().filter(req -> achieved >= req).count(); + } + + private void setupItemStacks(JsonObject hProfile, JsonObject pProfile) { + JsonObject playerCollection = pProfile.getAsJsonObject("collection"); + + for (String collection : collectionsMap.get(this.category)) { + Map<String, NEUItem> items = NEURepoManager.NEU_REPO.getItems().getItems(); + ItemStack itemStack = items.values().stream() + .filter(i -> Formatting.strip(i.getSkyblockItemId()).equals(ICON_TRANSLATION.getOrDefault(collection, collection).replace(':', '-'))) + .findFirst() + .map(NEUItem::getSkyblockItemId) + .map(ItemRepository::getItemStack) + .map(ItemStack::copy) + .orElse(Ico.BARRIER.copy()); + + if (itemStack.getItem().getName().getString().equals("Barrier")) itemStack.set(DataComponentTypes.ITEM_NAME, Text.of(collection)); + + int personalColl = playerCollection != null && playerCollection.has(collection) ? playerCollection.get(collection).getAsInt() : 0; + + int coopColl = 0; + for (String member : hProfile.get("members").getAsJsonObject().keySet()) { + if (!hProfile.getAsJsonObject("members").getAsJsonObject(member).has("collection")) continue; + JsonObject memberColl = hProfile.getAsJsonObject("members").getAsJsonObject(member).getAsJsonObject("collection"); + coopColl += memberColl.has(collection) ? memberColl.get(collection).getAsInt() : 0; + } + + int collectionTier = calculateTier(coopColl, tierRequirementsMap.get(collection)); + List<Integer> tierRequirements = tierRequirementsMap.get(collection); + + List<Text> lore = new ArrayList<>(); + Style style = Style.EMPTY.withItalic(false); + lore.add(Text.literal("Collection: " + FORMATTER.format(personalColl)).setStyle(style).formatted(Formatting.YELLOW)); + if (hProfile.get("members").getAsJsonObject().keySet().size() > 1) { + lore.add(Text.literal("Co-op Collection: " + FORMATTER.format(coopColl)).setStyle(style).formatted(Formatting.AQUA)); + } + lore.add(Text.literal("Collection Tier: " + collectionTier).setStyle(style).formatted(Formatting.LIGHT_PURPLE)); + itemStack.set(DataComponentTypes.LORE, new LoreComponent(lore)); + + if (collectionTier == tierRequirements.size()) itemStack.set(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, true); + + collections.add(itemStack); + } + } + + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + Text categoryTitle = Text.literal(category.charAt(0) + category.substring(1).toLowerCase() + " Collections").formatted(Formatting.BOLD); + context.drawText(textRenderer, categoryTitle, rootX + 88 - (textRenderer.getWidth(categoryTitle) / 2), rootY, Color.DARK_GRAY.getRGB(), false); + + for (int i = 0; i < collections.size(); i++) { + int x = rootX + 2 + (i % COLUMNS) * COLUMN_GAP; + int y = rootY + 19 + (i / COLUMNS) * ROW_GAP; + + context.fill(x - 3, y - 3, x + 19, y + 19, Color.BLACK.getRGB()); + context.drawTexture(BUTTON_TEXTURE, x - 2, y - 2, 0, 0, 20, 20, 20, 20); + context.drawItem(collections.get(i), x, y); + + ItemStack itemStack = collections.get(i); + List<Text> lore = itemStack.getOrDefault(DataComponentTypes.LORE, LoreComponent.DEFAULT).lines(); + for (Text text : lore) { + if (!text.getString().startsWith("Collection Tier: ")) continue; + int cTier = Integer.parseInt(text.getString().substring("Collection Tier: ".length())); + Color colour = Boolean.TRUE.equals(itemStack.get(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE)) ? Color.MAGENTA : Color.darkGray; + context.drawText(textRenderer, Text.literal(toRomanNumerals(cTier)), x + 9 - (textRenderer.getWidth(toRomanNumerals(cTier)) / 2), y + 21, colour.getRGB(), false); + break; + } + + if (mouseX > x && mouseX < x + 16 && mouseY > y && mouseY < y + 16) { + List<Text> tooltip = collections.get(i).getTooltip(Item.TooltipContext.DEFAULT, MinecraftClient.getInstance().player, TooltipType.BASIC); + context.drawTooltip(textRenderer, tooltip, mouseX, mouseY); + } + } + } + + private String toRomanNumerals(int number) { + return number <= ROMAN_NUMERALS.length ? ROMAN_NUMERALS[number] : "Err"; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonClassWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonClassWidget.java new file mode 100644 index 00000000..3b847b1b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonClassWidget.java @@ -0,0 +1,62 @@ +package de.hysky.skyblocker.skyblock.profileviewer.dungeons; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.Map; + +public class DungeonClassWidget { + private final String className; + private LevelFinder.LevelInfo classLevel; + private static final int CLASS_CAP = 50; + private JsonObject classData; + private final ItemStack stack; + private boolean active = false; + + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/icon_data_widget.png"); + private static final Identifier ACTIVE_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/item_protection.png"); + private static final Identifier BAR_FILL = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_fill"); + private static final Identifier BAR_BACK = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_back"); + + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final Map<String, ItemStack> CLASS_ICON = Map.ofEntries( + Map.entry("Healer", Ico.S_POTION), + Map.entry("Mage", Ico.B_ROD), + Map.entry("Berserk", Ico.IRON_SWORD), + Map.entry("Archer", Ico.BOW), + Map.entry("Tank", Ico.CHESTPLATE) + ); + + public DungeonClassWidget(String className, JsonObject playerProfile) { + this.className = className; + stack = CLASS_ICON.getOrDefault(className, Ico.BARRIER); + try { + classData = playerProfile.getAsJsonObject("dungeons").getAsJsonObject("player_classes").getAsJsonObject(this.className.toLowerCase()); + classLevel = LevelFinder.getLevelInfo("Catacombs", classData.get("experience").getAsLong()); + active = playerProfile.getAsJsonObject("dungeons").get("selected_dungeon_class").getAsString().equals(className.toLowerCase()); + } catch (Exception ignored) { + classLevel = LevelFinder.getLevelInfo("", 0); + } + } + + public void render(DrawContext context, int x, int y) { + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 26, 109, 26); + context.drawItem(stack, x + 3, y + 5); + if (active) context.drawTexture(ACTIVE_TEXTURE, x + 3, y + 5, 0, 0, 16, 16, 16, 16); + + context.drawText(textRenderer, className + " " + classLevel.level, x + 31, y + 5, Color.WHITE.getRGB(), false); + Color fillColor = classLevel.level >= CLASS_CAP ? Color.MAGENTA : Color.GREEN; + context.drawGuiTexture(BAR_BACK, x + 30, y + 15, 75, 6); + RenderHelper.renderNineSliceColored(context, BAR_FILL, x + 30, y + 15, (int) (75 * classLevel.fill), 6, fillColor); + } + +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonFloorRunsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonFloorRunsWidget.java new file mode 100644 index 00000000..7c9206c0 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonFloorRunsWidget.java @@ -0,0 +1,55 @@ +package de.hysky.skyblocker.skyblock.profileviewer.dungeons; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.Map; + +public class DungeonFloorRunsWidget { + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/dungeons_body.png"); + + private static final String[] DUNGEONS = {"catacombs", "master_catacombs"}; + private JsonObject dungeonsStats; + + public DungeonFloorRunsWidget(JsonObject pProfile) { + try { + dungeonsStats = pProfile.getAsJsonObject("dungeons").getAsJsonObject("dungeon_types"); + } catch (Exception ignored) {} + } + + // TODO: Hovering on each floor should probably showcase best run times in a tooltip + public void render(DrawContext context, int x, int y) { + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 110, 109, 110); + context.drawText(textRenderer, Text.literal("Floor Runs").formatted(Formatting.BOLD), x + 6, y + 4, Color.WHITE.getRGB(), true); + + int columnX = x + 4; + int elementY = y + 15; + for (String dungeon : DUNGEONS) { + JsonObject dungeonData; + try { + dungeonData = dungeonsStats.getAsJsonObject(dungeon).getAsJsonObject(dungeon.equals("catacombs") ? "times_played" : "tier_completions"); + for (Map.Entry<String, JsonElement> entry : dungeonData.entrySet()) { + if (entry.getKey().equals("total")) continue; + + String textToRender = String.format((dungeon.equals("catacombs") ? "§aF" : "§cM") + "%s§r %s", entry.getKey(), entry.getValue().getAsInt()); + context.drawText(textRenderer, textToRender, columnX + 2, elementY + 2, Color.WHITE.getRGB(), true); + + elementY += 11; + } + columnX += 52; + elementY = y + 26; + } catch (Exception e) { + return; + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonHeaderWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonHeaderWidget.java new file mode 100644 index 00000000..1a62a47b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonHeaderWidget.java @@ -0,0 +1,46 @@ +package de.hysky.skyblocker.skyblock.profileviewer.dungeons; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.text.DecimalFormat; + +public class DungeonHeaderWidget { + private LevelFinder.LevelInfo classLevel; + private float classAvg; + + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final DecimalFormat DF = new DecimalFormat("#.##"); + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/dungeons_header.png"); + + public DungeonHeaderWidget(JsonObject playerProfile, String[] classes) { + try { + JsonObject DUNGEONS_PROFILE = playerProfile.getAsJsonObject("dungeons").getAsJsonObject("dungeon_types").getAsJsonObject("catacombs"); + this.classLevel = LevelFinder.getLevelInfo("Catacombs", DUNGEONS_PROFILE.get("experience").getAsLong()); + + float avg = 0; + JsonObject CLASS_DATA = playerProfile.getAsJsonObject("dungeons").getAsJsonObject("player_classes"); + for (String element : classes) { + avg += LevelFinder.getLevelInfo("Catacombs", CLASS_DATA.getAsJsonObject(element.toLowerCase()).get("experience").getAsLong()).level; + } + classAvg = avg/classes.length; + } catch (Exception ignored) { + this.classLevel = LevelFinder.getLevelInfo("", 0); + classAvg = 0; + } + } + + public void render(DrawContext context, int x, int y) { + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 26, 109, 26); + + context.drawText(textRenderer, "§i§6§lCatacombs §r" + this.classLevel.level, x + 3, y + 4, Color.WHITE.getRGB(), true); + + context.drawText(textRenderer, "§eClass Average §r" + DF.format(this.classAvg), x + 3, y + 14, Color.WHITE.getRGB(), true); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonMiscStatsWidgets.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonMiscStatsWidgets.java new file mode 100644 index 00000000..679cc575 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonMiscStatsWidgets.java @@ -0,0 +1,61 @@ +package de.hysky.skyblocker.skyblock.profileviewer.dungeons; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.text.DecimalFormat; +import java.util.HashMap; +import java.util.Map; + +public class DungeonMiscStatsWidgets { + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/icon_data_widget.png"); + private static final Identifier RUN_ICON = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/run_icon.png"); + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final DecimalFormat DF = new DecimalFormat("#.##"); + private static final String[] DUNGEONS = {"catacombs", "master_catacombs"}; + + private final Map<String, Integer> dungeonRuns = new HashMap<>(); + private int secrets = 0; + private int totalRuns = 0; + + public DungeonMiscStatsWidgets(JsonObject pProfile) { + JsonObject DUNGEONS_DATA = pProfile.getAsJsonObject("dungeons"); + try { + secrets = DUNGEONS_DATA.get("secrets").getAsInt(); + + for (String dungeon : DUNGEONS) { + JsonObject dungeonData = DUNGEONS_DATA.getAsJsonObject("dungeon_types").getAsJsonObject(dungeon).getAsJsonObject(dungeon.equals("catacombs") ? "times_played" : "tier_completions"); + int runs = 0; + for (Map.Entry<String, JsonElement> entry : dungeonData.entrySet()) { + String key = entry.getKey(); + if (key.equals("total")) continue; + runs += entry.getValue().getAsInt(); + } + dungeonRuns.put(dungeon, runs); + totalRuns += runs; + } + + } catch (Exception ignored) {} + } + + public void render(DrawContext context, int x, int y) { + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 26, 109, 26); + context.drawItem(Ico.FEATHER, x + 2, y + 4); + + context.drawText(textRenderer, "Secrets " + secrets, x + 30, y + 4, Color.WHITE.getRGB(), true); + context.drawText(textRenderer, "Avg " + (totalRuns > 0 ? DF.format(secrets / (float) totalRuns) : 0) + "/Run", x + 30, y + 14, Color.WHITE.getRGB(), true); + + context.drawTexture(TEXTURE, x, y + 28, 0, 0, 109, 26, 109, 26); + context.drawTexture(RUN_ICON, x + 4, y + 33, 0, 0, 14, 16, 14, 16); + + context.drawText(textRenderer, "§aNormal §r" + dungeonRuns.getOrDefault("catacombs", 0), x + 30, y + 32, Color.WHITE.getRGB(), true); + context.drawText(textRenderer, "§cMaster §r" + dungeonRuns.getOrDefault("master_catacombs", 0), x + 30, y + 42, Color.WHITE.getRGB(), true); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonsPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonsPage.java new file mode 100644 index 00000000..b1398661 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/dungeons/DungeonsPage.java @@ -0,0 +1,39 @@ +package de.hysky.skyblocker.skyblock.profileviewer.dungeons; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.utils.ProfileUtils; +import net.minecraft.client.gui.DrawContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class DungeonsPage implements ProfileViewerPage { + public static final Logger LOGGER = LoggerFactory.getLogger(ProfileUtils.class); + private static final String[] CLASSES = {"Healer", "Mage", "Berserk", "Archer", "Tank"}; + + private final DungeonHeaderWidget dungeonHeaderWidget; + private final List<DungeonClassWidget> dungeonClassWidgetsList = new ArrayList<>(); + private final DungeonFloorRunsWidget dungeonFloorRunsWidget; + private final DungeonMiscStatsWidgets dungeonMiscStatsWidgets; + + public DungeonsPage(JsonObject pProfile) { + dungeonHeaderWidget = new DungeonHeaderWidget(pProfile, CLASSES); + dungeonFloorRunsWidget = new DungeonFloorRunsWidget(pProfile); + dungeonMiscStatsWidgets = new DungeonMiscStatsWidgets(pProfile); + for (String element : CLASSES) { + dungeonClassWidgetsList.add(new DungeonClassWidget(element, pProfile)); + } + } + + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + dungeonHeaderWidget.render(context, rootX, rootY); + dungeonFloorRunsWidget.render(context, rootX + 113, rootY + 56); + dungeonMiscStatsWidgets.render(context, rootX + 113, rootY); + for (int i = 0; i < dungeonClassWidgetsList.size(); i++) { + dungeonClassWidgetsList.get(i).render(context, rootX, rootY + 28 + i * 28); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Inventory.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Inventory.java new file mode 100644 index 00000000..a2f7d9d6 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Inventory.java @@ -0,0 +1,120 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders.ItemLoader; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.tooltip.TooltipType; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +public class Inventory implements ProfileViewerPage { + private static final Identifier TEXTURE = Identifier.of("textures/gui/container/generic_54.png"); + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private final IntIntPair dimensions; + private final int itemsPerPage; + private final List<ItemStack> containerList; + private final String containerName; + private int activePage = 0; + private int totalPages = 1; + private final PaginationButton previousPage = new PaginationButton(this, -1000, 0, false); + private final PaginationButton nextPage = new PaginationButton(this, -1000, 0, true); + + public Inventory(String name, IntIntPair dimensions, JsonObject inventory) { + this(name, dimensions, inventory, new ItemLoader()); + } + + public Inventory(String name, IntIntPair dimensions, JsonObject inventory, ItemLoader itemLoader) { + containerName = name; + this.dimensions = dimensions; + itemsPerPage = dimensions.rightInt() * dimensions.leftInt(); + this.containerList = itemLoader.loadItems(inventory); + this.totalPages = (int) Math.ceil((double) containerList.size() / itemsPerPage); + } + + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + int rootYAdjusted = rootY + (26 - dimensions.leftInt() * 3); + context.drawTexture(TEXTURE, rootX, rootYAdjusted, 0, 0, dimensions.rightInt() * 18 + 7, dimensions.leftInt() * 18 + 17); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootYAdjusted, 169, 0, 7, dimensions.leftInt() * 18 + 17); + context.drawTexture(TEXTURE, rootX, rootYAdjusted + dimensions.leftInt() * 18 + 17, 0, 215, dimensions.rightInt() * 18 + 7, 7); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootYAdjusted + dimensions.leftInt() * 18 + 17, 169, 215, 7, 7); + + context.drawText(textRenderer, containerName, rootX + 7, rootYAdjusted + 7, Color.DARK_GRAY.getRGB(), false); + + if (containerList.size() > itemsPerPage) { + previousPage.setX(rootX + 44); + previousPage.setY(rootY + 136); + previousPage.render(context, mouseX, mouseY, delta); + + context.drawCenteredTextWithShadow(textRenderer, "Page: " + (activePage + 1) + "/" + totalPages, rootX + 88, rootY + 140, Color.WHITE.getRGB()); + + nextPage.setX(rootX + 121); + nextPage.setY(rootY + 136); + nextPage.render(context, mouseX, mouseY, delta); + } + + int startIndex = activePage * itemsPerPage; + int endIndex = Math.min(startIndex + itemsPerPage, containerList.size()); + for (int i = 0; i < endIndex - startIndex; i++) { + if (containerList.get(startIndex + i) == ItemStack.EMPTY) continue; + int column = i % dimensions.rightInt(); + int row = i / dimensions.rightInt(); + + int x = rootX + 8 + column * 18; + int y = rootYAdjusted + 18 + row * 18; + context.drawItem(containerList.get(startIndex + i), x, y); + context.drawItemInSlot(textRenderer, containerList.get(startIndex + i), x, y); + + if (mouseX > x && mouseX < x + 16 && mouseY > y && mouseY < y + 16) { + List<Text> tooltip = containerList.get(startIndex + i).getTooltip(Item.TooltipContext.DEFAULT, MinecraftClient.getInstance().player, TooltipType.BASIC); + context.drawTooltip(textRenderer, tooltip, mouseX, mouseY); + } + } + } + + public void nextPage() { + if (activePage < totalPages - 1) { + activePage++; + } + } + + public void previousPage() { + if (activePage > 0) { + activePage--; + } + } + + @Override + public void markWidgetsAsVisible() { + nextPage.visible = true; + previousPage.visible = true; + nextPage.active = true; + previousPage.active = true; + } + + @Override + public void markWidgetsAsInvisible() { + nextPage.visible = false; + previousPage.visible = false; + nextPage.active = false; + previousPage.active = false; + } + + @Override + public List<ClickableWidget> getButtons() { + List<ClickableWidget> buttons = new ArrayList<>(); + buttons.add(nextPage); + buttons.add(previousPage); + return buttons; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/InventoryPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/InventoryPage.java new file mode 100644 index 00000000..8b0cbefc --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/InventoryPage.java @@ -0,0 +1,113 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders.BackpackItemLoader; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders.PetsInventoryItemLoader; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders.WardrobeInventoryItemLoader; +import de.hysky.skyblocker.skyblock.profileviewer.utils.SkullCreator; +import de.hysky.skyblocker.skyblock.profileviewer.utils.SubPageSelectButton; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.item.ItemStack; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class InventoryPage implements ProfileViewerPage { + private static final String[] INVENTORY_PAGES = {"Inventory", "Enderchest", "Backpack", "Wardrobe", "Pets", "Accessory Bag"}; + private static final int TOTAL_HEIGHT = 165; + private static final Map<String, ItemStack> ICON_MAP = Map.ofEntries( + Map.entry("Wardrobe", Ico.L_CHESTPLATE), + Map.entry("Inventory", Ico.CHEST), + Map.entry("Backpack", SkullCreator.createSkull("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHBzOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzYyZjNiM2EwNTQ4MWNkZTc3MjQwMDA1YzBkZGNlZTFjMDY5ZTU1MDRhNjJjZTA5Nzc4NzlmNTVhMzkzOTYxNDYifX19")), + Map.entry("Pets", Ico.BONE), + Map.entry("Enderchest", Ico.E_CHEST), + Map.entry("Accessory Bag", SkullCreator.createSkull("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTYxYTkxOGMwYzQ5YmE4ZDA1M2U1MjJjYjkxYWJjNzQ2ODkzNjdiNGQ4YWEwNmJmYzFiYTkxNTQ3MzA5ODVmZiJ9fX0=")) + ); + + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private final ProfileViewerPage[] inventorySubPages = new ProfileViewerPage[6]; + private final List<SubPageSelectButton> inventorySelectButtons = new ArrayList<>(); + private int activePage = 0; + + public InventoryPage(JsonObject pProfile) { + for (int i = 0; i < INVENTORY_PAGES.length; i++) { + inventorySelectButtons.add(new SubPageSelectButton(this, -100, 0, i, ICON_MAP.getOrDefault(INVENTORY_PAGES[i], Ico.BARRIER))); + } + + try { + JsonObject inventoryData = pProfile.getAsJsonObject("inventory"); + if (inventoryData == null) return; + inventorySubPages[0] = new PlayerInventory(inventoryData); + inventorySubPages[1] = new Inventory(INVENTORY_PAGES[1], IntIntPair.of(5, 9), inventoryData.getAsJsonObject("ender_chest_contents")); + inventorySubPages[2] = new Inventory(INVENTORY_PAGES[2], IntIntPair.of(5, 9), inventoryData.getAsJsonObject("backpack_contents"), new BackpackItemLoader()); + inventorySubPages[3] = new Inventory(INVENTORY_PAGES[3], IntIntPair.of(4, 9), inventoryData.getAsJsonObject("wardrobe_contents"), new WardrobeInventoryItemLoader(inventoryData)); + inventorySubPages[4] = new Inventory(INVENTORY_PAGES[4], IntIntPair.of(4, 9), pProfile, new PetsInventoryItemLoader()); + inventorySubPages[5] = new Inventory(INVENTORY_PAGES[5], IntIntPair.of(5, 9), inventoryData.getAsJsonObject("bag_contents").getAsJsonObject("talisman_bag")); + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Error while loading inventory data: ", e); + } + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + int startingY = rootY + (TOTAL_HEIGHT - inventorySelectButtons.size() * 21) / 2; + for (int i = 0; i < inventorySelectButtons.size(); i++) { + inventorySelectButtons.get(i).setX(rootX); + inventorySelectButtons.get(i).setY(startingY + i * 21); + inventorySelectButtons.get(i).render(context, mouseX, mouseY, delta); + } + + if (inventorySubPages[activePage] == null) { + context.drawText(textRenderer, "No data...", rootX + 92, rootY + 72, Color.DARK_GRAY.getRGB(), false); + return; + } + + inventorySubPages[activePage].markWidgetsAsVisible(); + inventorySubPages[activePage].render(context, mouseX, mouseY, delta, rootX + 35, rootY + 6); + } + + public void onNavButtonClick(SubPageSelectButton clickedButton) { + if (inventorySubPages[activePage] != null) inventorySubPages[activePage].markWidgetsAsInvisible(); + for (SubPageSelectButton button : inventorySelectButtons) { + button.setToggled(false); + } + activePage = clickedButton.getIndex(); + clickedButton.setToggled(true); + } + + @Override + public List<ClickableWidget> getButtons() { + List<ClickableWidget> clickableWidgets = new ArrayList<>(inventorySelectButtons); + for (ProfileViewerPage page : inventorySubPages) { + if (page != null && page.getButtons() != null) clickableWidgets.addAll(page.getButtons()); + } + return clickableWidgets; + } + + @Override + public void markWidgetsAsVisible() { + if (inventorySubPages[activePage] != null) inventorySubPages[activePage].markWidgetsAsVisible(); + for (SubPageSelectButton button : inventorySelectButtons) { + button.visible = true; + button.active = true; + } + } + + @Override + public void markWidgetsAsInvisible() { + if (inventorySubPages[activePage] != null) inventorySubPages[activePage].markWidgetsAsInvisible(); + for (SubPageSelectButton button : inventorySelectButtons) { + button.visible = false; + button.active = false; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PaginationButton.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PaginationButton.java new file mode 100644 index 00000000..1a725e1a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PaginationButton.java @@ -0,0 +1,46 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory; + +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +public class PaginationButton extends ClickableWidget { + private final ProfileViewerPage screen; + private final boolean isNextButton; + private final Identifier TEXTURE; + private final Identifier HIGHLIGHT; + + public PaginationButton(ProfileViewerPage screen, int x, int y, boolean isNextButton) { + super(x, y, 12, 17, Text.empty()); + this.screen = screen; + this.isNextButton = isNextButton; + if (isNextButton) { + TEXTURE = Identifier.of("minecraft", "textures/gui/sprites/recipe_book/page_forward.png"); + HIGHLIGHT = Identifier.of("minecraft", "textures/gui/sprites/recipe_book/page_forward_highlighted.png"); + } else { + TEXTURE = Identifier.of("minecraft", "textures/gui/sprites/recipe_book/page_backward.png"); + HIGHLIGHT = Identifier.of("minecraft", "textures/gui/sprites/recipe_book/page_backward_highlighted.png"); + } + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawTexture(TEXTURE, this.getX(), this.getY(), 0, 0, 12, 17, 12, 17); + if (isMouseOver(mouseX, mouseY)) context.drawTexture(HIGHLIGHT, this.getX(), this.getY(), 0, 0, 12, 17, 12, 17); + } + + @Override + public void onClick(double mouseX, double mouseY) { + if (isNextButton) { + screen.nextPage(); + } else { + screen.previousPage(); + } + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Pet.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Pet.java new file mode 100644 index 00000000..b3389d39 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/Pet.java @@ -0,0 +1,186 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory; + +import de.hysky.skyblocker.skyblock.PetCache; +import de.hysky.skyblocker.skyblock.itemlist.ItemFixerUpper; +import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.NEURepoManager; +import io.github.moulberry.repo.constants.PetNumbers; +import io.github.moulberry.repo.data.NEUItem; +import io.github.moulberry.repo.data.Rarity; +import io.github.moulberry.repo.util.PetId; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.LoreComponent; +import net.minecraft.component.type.ProfileComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtString; +import net.minecraft.registry.Registries; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import net.minecraft.util.Pair; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.regex.Matcher; +import java.util.stream.Collectors; + +import static de.hysky.skyblocker.skyblock.itemlist.ItemStackBuilder.SKULL_TEXTURE_PATTERN; +import static de.hysky.skyblocker.skyblock.itemlist.ItemStackBuilder.SKULL_UUID_PATTERN; + +public class Pet { + private final String name; + private final double xp; + private final String tier; + private final Optional<String> heldItem; + private final int level; + private final ItemStack icon; + + private static final Map<String, Integer> TIER_MAP = Map.of( + "COMMON", 0, "UNCOMMON", 1, "RARE", 2, "EPIC", 3, "LEGENDARY", 4, "MYTHIC", 5 + ); + + public Pet(PetCache.PetInfo petData) { + this.name = petData.type(); + this.xp = petData.exp(); + this.heldItem = petData.item(); + if ((heldItem.isPresent() && heldItem.get().equals("PET_ITEM_TIER_BOOST"))) { + this.tier = switch (petData.tier()) { + case "COMMON" -> "UNCOMMON"; + case "UNCOMMON" -> "RARE"; + case "RARE" -> "EPIC"; + case "EPIC" -> "LEGENDARY"; + case "LEGENDARY" -> "MYTHIC"; + default -> petData.tier(); + }; + } else { + this.tier = petData.tier(); + } + this.level = LevelFinder.getLevelInfo(this.name.equals("GOLDEN_DRAGON") ? "PET_GREG" : "PET_" + this.tier, (long) xp).level; + this.icon = createIcon(); + } + + public String getName() { return name; } + public long getXP() { return (long) xp; } + public int getTier() { return TIER_MAP.getOrDefault(tier, 0); } + public String getTierAsString() { return tier; } + public String getSkin() { return null; } + public int getLevel() { return level; } + public ItemStack getIcon() { return icon; } + + + private ItemStack createIcon() { + if (NEURepoManager.isLoading() || !ItemRepository.filesImported()) return Ico.BARRIER; + Map<String, NEUItem> items = NEURepoManager.NEU_REPO.getItems().getItems(); + if (items == null) return Ico.BARRIER; + + String targetItemId = this.getName() + ";" + this.getTier(); + NEUItem item = items.values().stream() + .filter(i -> Formatting.strip(i.getSkyblockItemId()).equals(targetItemId)) + .findFirst().orElse(null); + + NEUItem petItem = null; + if (this.heldItem.isPresent()) { + petItem = items.values().stream() + .filter(i -> Formatting.strip(i.getSkyblockItemId()).equals(this.heldItem.get())) + .findFirst().orElse(null); + } + + return fromNEUItem(item, petItem); + } + + /** + * Converts NEU item data into an ItemStack. + * <p> This method converts NEU item data into a Pet by using the placeholder + * information from NEU-REPO and injecting the player's calculated pet stats into the lore and transforming + * the NBT Data into modern DataComponentTypes before returning the final ItemStack </p + * + * @param item The NEUItem representing the pet. + * @param helditem The NEUItem representing the held item, if any. + * @return The ItemStack representing the pet with all its properties set. + */ + private ItemStack fromNEUItem(NEUItem item, NEUItem helditem) { + if (item == null) return Ico.BARRIER; + List<Pair<String, String>> injectors = new ArrayList<>(createLoreReplacers(item.getSkyblockItemId(), helditem)); + Identifier itemId = Identifier.of(ItemFixerUpper.convertItemId(item.getMinecraftItemId(), item.getDamage())); + ItemStack stack = new ItemStack(Registries.ITEM.get(itemId)); + + NbtCompound customData = new NbtCompound(); + customData.put(ItemUtils.ID, NbtString.of(item.getSkyblockItemId())); + stack.set(DataComponentTypes.CUSTOM_NAME, Text.of(injectData(item.getDisplayName(), injectors))); + + stack.set(DataComponentTypes.LORE, new LoreComponent( + item.getLore().stream().map(line -> injectData(line, injectors)) + .filter(line -> !line.contains("SKIP")).map(Text::of) + .collect(Collectors.toList()))); + + Matcher skullUuid = SKULL_UUID_PATTERN.matcher(item.getNbttag()); + Matcher skullTexture = SKULL_TEXTURE_PATTERN.matcher(item.getNbttag()); + if (skullUuid.find() && skullTexture.find()) { + UUID uuid = UUID.fromString(skullUuid.group(1)); + String textureValue = this.getSkin() == null ? skullTexture.group(1) : this.getSkin(); + stack.set(DataComponentTypes.PROFILE, new ProfileComponent( + Optional.of(item.getSkyblockItemId()), Optional.of(uuid), + ItemUtils.propertyMapWithTexture(textureValue))); + } + return stack; + } + + /** + * Generates a list of placeholder-replacement pairs for the itemName of a pet item. + * <p> This method uses the pet's data from the NEU repository and uses PetInfo to generate replacers, and optionally + * includes data about a held item. </p> + * + * @param itemSkyblockID The initial itemName string containing the pet's name and tier separated by a semicolon. + * @param helditem The NEUItem representing the held item, if any. + * @return A list of placeholder-replacement pairs to be used for injecting data into the pet item's itemName. + */ + private List<Pair<String, String>> createLoreReplacers(String itemSkyblockID, NEUItem helditem) { + List<Pair<String, String>> list = new ArrayList<>(); + Map<@PetId String, Map<Rarity, PetNumbers>> petNums = NEURepoManager.NEU_REPO.getConstants().getPetNumbers(); + String petName = itemSkyblockID.split(";")[0]; + if (!itemSkyblockID.contains(";") || !petNums.containsKey(petName)) return list; + + Rarity rarity = Rarity.values()[Integer.parseInt(itemSkyblockID.split(";")[1])]; + try { + PetNumbers data = petNums.get(petName).get(rarity); + list.add(new Pair<>("\\{LVL\\}", String.valueOf(this.level))); + data.interpolatedStatsAtLevel(this.level).getStatNumbers().forEach((key, value) -> + list.add(new Pair<>("\\{" + key + "\\}", fixDecimals(value, true)))); + + List<Double> otherNumsMin = data.interpolatedStatsAtLevel(this.level).getOtherNumbers(); + for (int i = 0; i < otherNumsMin.size(); ++i) { + list.add(new Pair<>("\\{" + i + "\\}", fixDecimals(otherNumsMin.get(i), false))); + } + + list.add(new Pair<>("Right-click to add this pet to", + helditem != null ? "§r§6Held Item: " + helditem.getDisplayName() : "SKIP")); + list.add(new Pair<>("pet menu!", "SKIP")); + } catch (Exception e) { + if (petName.equals("GOLDEN_DRAGON")) { + list.add(new Pair<>("Golden Dragon", + "§r§7[Lvl " + this.level + "] " + "§6Golden Dragon Egg §c[Not Supported by NEU-Repo]")); + } + } + return list; + } + + private String injectData(String string, List<Pair<String, String>> injectors) { + for (Pair<String, String> injector : injectors) { + if (string.contains(injector.getLeft())) return injector.getRight(); + string = string.replaceAll(injector.getLeft(), injector.getRight()); + } + return string; + } + + private String fixDecimals(double num, boolean truncate) { + if (num % 1 == 0) return String.valueOf((int) num); + BigDecimal roundedNum = new BigDecimal(num).setScale(3, RoundingMode.HALF_UP); + return truncate && num > 1 ? String.valueOf(roundedNum.intValue()) + : roundedNum.stripTrailingZeros().toPlainString(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PlayerInventory.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PlayerInventory.java new file mode 100644 index 00000000..26673693 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/PlayerInventory.java @@ -0,0 +1,73 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders.InventoryItemLoader; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.tooltip.TooltipType; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.List; + +public class PlayerInventory implements ProfileViewerPage { + private static final Identifier TEXTURE = Identifier.of("textures/gui/container/generic_54.png"); + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private final List<ItemStack> containerList; + + public PlayerInventory(JsonObject inventory) { + this.containerList = new InventoryItemLoader().loadItems(inventory); + } + + // Z-STACKING forces this nonsense of separating the Background texture and Item Drawing :( + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + drawContainerTextures(context, "Armour", rootX, rootY + 108, IntIntPair.of(1, 4)); + drawContainerTextures(context, "Inventory", rootX, rootY + 2, IntIntPair.of(4, 9)); + drawContainerTextures(context, "Equipment", rootX + 90, rootY + 108, IntIntPair.of(1, 4)); + + drawContainerItems(context, rootX, rootY + 108, IntIntPair.of(1, 4), 36, 40, mouseX, mouseY); + drawContainerItems(context, rootX, rootY + 2, IntIntPair.of(4, 9), 0, 36, mouseX, mouseY); + drawContainerItems(context, rootX + 90, rootY + 108, IntIntPair.of(1, 4), 40, containerList.size(), mouseX, mouseY); + } + + private void drawContainerTextures(DrawContext context, String containerName, int rootX, int rootY, IntIntPair dimensions) { + if (containerName.equals("Inventory")) { + context.drawTexture(TEXTURE, rootX, rootY + dimensions.leftInt() + 10, 0, 136, dimensions.rightInt() * 18 + 7, dimensions.leftInt() * 18 + 17); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootY, 169, 0, 7, dimensions.leftInt() * 18 + 21); + context.drawTexture(TEXTURE, rootX, rootY, 0, 0, dimensions.rightInt() * 18 + 7, 14); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootY + dimensions.leftInt() * 18 + 21, 169, 215, 7, 7); + } else { + context.drawTexture(TEXTURE, rootX, rootY, 0, 0, dimensions.rightInt() * 18 + 7, dimensions.leftInt() * 18 + 17); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootY, 169, 0, 7, dimensions.leftInt() * 18 + 17); + context.drawTexture(TEXTURE, rootX, rootY + dimensions.leftInt() * 18 + 17, 0, 215, dimensions.rightInt() * 18 + 7, 7); + context.drawTexture(TEXTURE, rootX + dimensions.rightInt() * 18 + 7, rootY + dimensions.leftInt() * 18 + 17, 169, 215, 7, 7); + } + + context.drawText(textRenderer, containerName, rootX + 7, rootY + 7, Color.DARK_GRAY.getRGB(), false); + } + + private void drawContainerItems(DrawContext context, int rootX, int rootY, IntIntPair dimensions, int startIndex, int endIndex, int mouseX, int mouseY) { + for (int i = 0; i < endIndex - startIndex; i++) { + if (containerList.get(startIndex + i) == ItemStack.EMPTY) continue; + int column = i % dimensions.rightInt(); + int row = i / dimensions.rightInt(); + + int x = rootX + 8 + column * 18; + int y = (rootY + 18 + row * 18) + (dimensions.leftInt() > 1 && row + 1 == dimensions.leftInt() ? 4 : 0); + + context.drawItem(containerList.get(startIndex + i), x, y); + context.drawItemInSlot(textRenderer, containerList.get(startIndex + i), x, y); + + if (mouseX > x && mouseX < x + 16 && mouseY > y && mouseY < y + 16) { + List<Text> tooltip = containerList.get(startIndex + i).getTooltip(Item.TooltipContext.DEFAULT, MinecraftClient.getInstance().player, TooltipType.BASIC); + context.drawTooltip(textRenderer, tooltip, mouseX, mouseY); + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/BackpackItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/BackpackItemLoader.java new file mode 100644 index 00000000..99e728be --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/BackpackItemLoader.java @@ -0,0 +1,34 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import net.minecraft.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class BackpackItemLoader extends ItemLoader { + @Override + public List<ItemStack> loadItems(JsonObject data) { + List<ItemStack> backpackItems = new ArrayList<>(); + + // Sort the data by keys numerically + List<Map.Entry<String, JsonElement>> sortedEntries = data.entrySet().stream() + .sorted((e1, e2) -> { + int key1 = Integer.parseInt(e1.getKey()); + int key2 = Integer.parseInt(e2.getKey()); + return Integer.compare(key1, key2); + }).toList(); + + for (int i = 0; i < sortedEntries.size(); i++) { + backpackItems.addAll(super.loadItems(sortedEntries.get(i).getValue().getAsJsonObject())); + int padding = (i + 1) * 45 % (backpackItems.isEmpty() ? 1 : backpackItems.size()); + for (int j = 0; j < padding; j++) { + backpackItems.add(ItemStack.EMPTY); + } + } + + return backpackItems; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java new file mode 100644 index 00000000..f73661a1 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/InventoryItemLoader.java @@ -0,0 +1,29 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders; + +import com.google.gson.JsonObject; +import net.minecraft.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +public class InventoryItemLoader extends ItemLoader { + + private static final String[] INVENTORIES = {"inv_contents", "inv_armor", "equipment_contents"}; + + @Override + public List<ItemStack> loadItems(JsonObject data) { + List<ItemStack> inventoryItems = new ArrayList<>(); + for (String inventory : INVENTORIES) { + List<ItemStack> inv = super.loadItems(data.getAsJsonObject(inventory)); + switch (inventory) { + case "inv_armor" -> inventoryItems.addAll(inv.reversed()); + case "inv_contents" -> { + inventoryItems.addAll(inv.subList(9,inv.size())); + inventoryItems.addAll(inv.subList(0, 9)); + } + default -> inventoryItems.addAll(inv); + } + } + return inventoryItems; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/ItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/ItemLoader.java new file mode 100644 index 00000000..9d9b1b07 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/ItemLoader.java @@ -0,0 +1,139 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; +import de.hysky.skyblocker.skyblock.PetCache; +import de.hysky.skyblocker.skyblock.itemlist.ItemRepository; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.Pet; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.NEURepoManager; +import io.github.moulberry.repo.data.NEUItem; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.AttributeModifiersComponent; +import net.minecraft.component.type.DyedColorComponent; +import net.minecraft.component.type.LoreComponent; +import net.minecraft.component.type.ProfileComponent; +import net.minecraft.datafixer.fix.ItemIdFix; +import net.minecraft.datafixer.fix.ItemInstanceTheFlatteningFix; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.*; +import net.minecraft.registry.Registries; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; + +import java.io.ByteArrayInputStream; +import java.util.*; +import java.util.stream.Collectors; + +import static de.hysky.skyblocker.skyblock.itemlist.ItemRepository.getItemStack; + +public class ItemLoader { + + public List<ItemStack> loadItems(JsonObject data) { + NbtList containerContent = decompress(data); + List<ItemStack> itemList = new ArrayList<>(); + + for (int i = 0; i < containerContent.size(); i++) { + if (containerContent.getCompound(i).getInt("id") == 0) { + itemList.add(ItemStack.EMPTY); + continue; + } + + NbtCompound nbttag = containerContent.getCompound(i).getCompound("tag"); + String internalName = nbttag.getCompound("ExtraAttributes").getString("id"); + if (internalName.equals("PET")) { + NbtCompound extraAttributes = nbttag .getCompound("ExtraAttributes"); + PetCache.PetInfo petInfo = PetCache.PetInfo.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(extraAttributes.getString("petInfo"))).getOrThrow(); + Pet pet = new Pet(petInfo); + itemList.add(pet.getIcon()); + continue; + } + + Identifier itemId = identifierFromOldId(containerContent.getCompound(i).getInt("id"), containerContent.getCompound(i).getInt("Damage")); + ItemStack stack = itemId.toString().equals("minecraft:air") ? getItemStack(internalName) : new ItemStack(Registries.ITEM.get(itemId)); + + if (stack == null || stack.isEmpty() || stack.getItem().equals(Ico.BARRIER.getItem())) { + // Last ditch effort to find item in NEU REPO + Map<String, NEUItem> items = NEURepoManager.NEU_REPO.getItems().getItems(); + stack = items.values().stream() + .filter(j -> Formatting.strip(j.getSkyblockItemId()).equals(Formatting.strip(internalName).replace(":", "-"))) + .findFirst() + .map(NEUItem::getSkyblockItemId) + .map(ItemRepository::getItemStack) + .orElse(Ico.BARRIER.copy()); + + + if (stack.getName().getString().contains("barrier")) { + stack.set(DataComponentTypes.CUSTOM_NAME, Text.literal("Err: " + internalName)); + itemList.add(stack); + continue; + } + } + + // Custom Data + NbtCompound customData = new NbtCompound(); + + // Add Skyblock Item Id + customData.put(ItemUtils.ID, NbtString.of(internalName)); + + + // Item Name + stack.set(DataComponentTypes.CUSTOM_NAME, Text.of(nbttag.getCompound("display").getString("Name"))); + + // Lore + NbtList loreList = nbttag.getCompound("display").getList("Lore", 8); + stack.set(DataComponentTypes.LORE, new LoreComponent(loreList.stream() + .map(NbtElement::asString) + .map(Text::literal) + .collect(Collectors.toList()))); + + // add skull texture + NbtList texture = nbttag.getCompound("SkullOwner").getCompound("Properties").getList("textures", 10); + if (!texture.isEmpty()) { + stack.set(DataComponentTypes.PROFILE, new ProfileComponent(Optional.of(internalName), Optional.of(UUID.fromString(nbttag.getCompound("SkullOwner").get("Id").asString())), ItemUtils.propertyMapWithTexture(texture.getCompound(0).getString("Value")))); + } + + // Colour + if (nbttag.getCompound("display").contains("color")) { + int color = nbttag.getCompound("display").getInt("color"); + stack.set(DataComponentTypes.DYED_COLOR, new DyedColorComponent(color, false)); + } + + // add enchantment glint + if (nbttag.getKeys().contains("ench")) { + stack.set(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, true); + } + + // Hide weapon damage and other useless info + stack.set(DataComponentTypes.ATTRIBUTE_MODIFIERS, new AttributeModifiersComponent(List.of(), false)); + + // Set Count + stack.setCount(containerContent.getCompound(i).getInt("Count")); + + itemList.add(stack); + } + + return itemList; + } + + private static Identifier identifierFromOldId(int id, int damage) { + try { + return damage != 0 ? Identifier.of(ItemInstanceTheFlatteningFix.getItem(ItemIdFix.fromId(id), damage)) : Identifier.of(ItemIdFix.fromId(id)); + } catch (Exception e) { + return Identifier.of("air"); + } + } + + private static NbtList decompress(JsonObject data) { + try { + return NbtIo.readCompressed(new ByteArrayInputStream(Base64.getDecoder().decode(data.get("data").getAsString())), NbtSizeTracker.ofUnlimitedBytes()).getList("i", NbtElement.COMPOUND_TYPE); + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Failed to decompress item data", e); + } + return null; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/PetsInventoryItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/PetsInventoryItemLoader.java new file mode 100644 index 00000000..cd3b7a26 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/PetsInventoryItemLoader.java @@ -0,0 +1,42 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; +import de.hysky.skyblocker.skyblock.PetCache; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import de.hysky.skyblocker.skyblock.profileviewer.inventory.Pet; +import net.minecraft.item.ItemStack; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class PetsInventoryItemLoader extends ItemLoader { + private static final List<String> TIER_ORDER = List.of("MYTHIC", "LEGENDARY", "EPIC", "RARE", "UNCOMMON", "COMMON"); + + @Override + public List<ItemStack> loadItems(JsonObject data) { + List<Pet> petList = new ArrayList<>(); + try { + JsonObject petsData = data.getAsJsonObject("pets_data"); + if (petsData != null && petsData.has("pets")) { + for (var petElement : petsData.get("pets").getAsJsonArray()) { + PetCache.PetInfo petInfo = PetCache.PetInfo.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(petElement.toString())).getOrThrow(); + petList.add(new Pet(petInfo)); + } + } + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Failed to load pets", e); + } + + // Sort pets by tier (in reverse order) and level (in reverse order) + petList.sort(Comparator.comparingInt((Pet pet) -> TIER_ORDER.indexOf(pet.getTierAsString())).reversed().thenComparingInt(Pet::getLevel).reversed()); + + List<ItemStack> itemList = new ArrayList<>(); + for (Pet pet : petList) { + itemList.add(pet.getIcon()); + } + return itemList; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java new file mode 100644 index 00000000..9d434726 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/inventory/itemLoaders/WardrobeInventoryItemLoader.java @@ -0,0 +1,40 @@ +package de.hysky.skyblocker.skyblock.profileviewer.inventory.itemLoaders; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import net.minecraft.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +public class WardrobeInventoryItemLoader extends ItemLoader { + private final int activeSlot; + private final JsonObject activeArmorSet; + + public WardrobeInventoryItemLoader(JsonObject inventory) { + this.activeSlot = inventory.get("wardrobe_equipped_slot").getAsInt(); + this.activeArmorSet = inventory.get("inv_armor").getAsJsonObject(); + } + + @Override + public List<ItemStack> loadItems(JsonObject data) { + List<ItemStack> itemList = new ArrayList<>(); + + try { + itemList.addAll(super.loadItems(data)); + if (activeSlot != -1) { + List<ItemStack> activeArmour = super.loadItems(activeArmorSet).reversed(); + for (int i = 0; i < 4; i++) { + int baseIndex = activeSlot % 9; + int page = activeSlot / 9; + int slotIndex = (page * 36) + (i * 9) + baseIndex - 1; + itemList.set(slotIndex, activeArmour.get(i)); + } + } + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Failed to load wardrobe items", e); + } + + return itemList; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillWidget.java new file mode 100644 index 00000000..3a3870f3 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillWidget.java @@ -0,0 +1,95 @@ +package de.hysky.skyblocker.skyblock.profileviewer.skills; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; +import de.hysky.skyblocker.skyblock.profileviewer.utils.SkullCreator; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.Map; + +public class SkillWidget { + private final String SKILL_NAME; + private final LevelFinder.LevelInfo SKILL_LEVEL; + + private static final Identifier BAR_FILL = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_fill"); + private static final Identifier BAR_BACK = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_back"); + + private final ItemStack stack; + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final Map<String, ItemStack> SKILL_LOGO = Map.ofEntries( + Map.entry("Combat", Ico.STONE_SWORD), + Map.entry("Farming", Ico.GOLDEN_HOE), + Map.entry("Mining", Ico.STONE_PICKAXE), + Map.entry("Foraging", Ico.JUNGLE_SAPLING), + Map.entry("Fishing", Ico.FISH_ROD), + Map.entry("Enchanting", Ico.ENCHANTING_TABLE), + Map.entry("Alchemy", Ico.BREWING_STAND), + Map.entry("Taming", Ico.SPAWN_EGG), + Map.entry("Carpentry", Ico.CRAFTING_TABLE), + Map.entry("Catacombs", SkullCreator.createSkull("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHBzOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzliNTY4OTViOTY1OTg5NmFkNjQ3ZjU4NTk5MjM4YWY1MzJkNDZkYjljMWIwMzg5YjhiYmViNzA5OTlkYWIzM2QifX19")), + Map.entry("Runecraft", Ico.MAGMA_CREAM), + Map.entry("Social", Ico.EMERALD) + ); + private static final Map<String, Integer> SKILL_CAP = Map.ofEntries( + Map.entry("Combat", 60), + Map.entry("Farming", 60), + Map.entry("Mining", 60), + Map.entry("Foraging", 50), + Map.entry("Fishing", 50), + Map.entry("Enchanting", 60), + Map.entry("Alchemy", 50), + Map.entry("Taming", 60), + Map.entry("Carpentry", 50), + Map.entry("Catacombs", 50), + Map.entry("Runecraft", 25), + Map.entry("Social", 25) + ); + private static final Map<String, Integer> SOFT_SKILL_CAP = Map.of( + "Taming", 50, + "Farming", 50 + ); + + private static final Map<String, Integer> INFINITE = Map.of( + "Catacombs", 0 + ); + + public SkillWidget(String skill, long xp, int playerCap) { + this.SKILL_NAME = skill; + this.SKILL_LEVEL = LevelFinder.getLevelInfo(skill, xp); + if (SKILL_LEVEL.level >= SKILL_CAP.get(skill) && !INFINITE.containsKey(skill)) { + SKILL_LEVEL.fill = 1; + SKILL_LEVEL.level = SKILL_CAP.get(skill); + } + + this.stack = SKILL_LOGO.getOrDefault(skill, Ico.BARRIER); + if (playerCap != -1) { + this.SKILL_LEVEL.level = Math.min(SKILL_LEVEL.level, (SOFT_SKILL_CAP.get(this.SKILL_NAME) + playerCap)); + } + + } + + public void render(DrawContext context, int x, int y) { + context.drawItem(this.stack, x + 3, y + 2); + context.drawText(textRenderer, SKILL_NAME + " " + SKILL_LEVEL.level, x + 31, y + 2, Color.white.hashCode(), false); + + Color fillColor = Color.green; + if (SKILL_LEVEL.level >= SKILL_CAP.get(SKILL_NAME)) { + fillColor = Color.MAGENTA; + } + + if ((SOFT_SKILL_CAP.containsKey(SKILL_NAME) && SKILL_LEVEL.level > SOFT_SKILL_CAP.get(SKILL_NAME)) && SKILL_LEVEL.level < SKILL_CAP.get(SKILL_NAME) && SKILL_LEVEL.fill == 1 || + (SKILL_NAME.equals("Taming") && SKILL_LEVEL.level >= SOFT_SKILL_CAP.get(SKILL_NAME))) { + fillColor = Color.YELLOW; + } + + context.drawGuiTexture(BAR_BACK, x + 30, y + 12, 75, 6); + RenderHelper.renderNineSliceColored(context, BAR_FILL, x + 30, y + 12, (int) (75 * SKILL_LEVEL.fill), 6, fillColor); + } +}
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillsPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillsPage.java new file mode 100644 index 00000000..c331bbdd --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/skills/SkillsPage.java @@ -0,0 +1,93 @@ +package de.hysky.skyblocker.skyblock.profileviewer.skills; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.util.Identifier; + +import java.util.ArrayList; +import java.util.List; + +public class SkillsPage implements ProfileViewerPage { + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/icon_data_widget.png"); + private static final String[] SKILLS = {"Combat", "Mining", "Farming", "Foraging", "Fishing", "Enchanting", "Alchemy", "Taming", "Carpentry", "Catacombs", "Runecraft", "Social"}; + private static final int ROW_GAP = 28; + + private final JsonObject HYPIXEL_PROFILE; + private final JsonObject PLAYER_PROFILE; + + private final List<SkillWidget> skillWidgets = new ArrayList<>(); + private JsonObject skills; + + public SkillsPage(JsonObject hProfile, JsonObject pProfile) { + this.HYPIXEL_PROFILE = hProfile; + this.PLAYER_PROFILE = pProfile; + + try { + this.skills = this.PLAYER_PROFILE.getAsJsonObject("player_data").getAsJsonObject("experience"); + for (String skill : SKILLS) { + skillWidgets.add(new SkillWidget(skill, getSkillXP("SKILL_" + skill.toUpperCase()), getSkillCap(skill))); + } + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Error creating widgets.", e); + } + } + + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + int column2 = rootX + 113; + for (int i = 0; i < skillWidgets.size(); i++) { + int x = (i < 6) ? rootX : column2; + int y = rootY + (i % 6) * ROW_GAP; + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 26, 109, 26); + skillWidgets.get(i).render(context, x, y + 3); + } + } + + private int getSkillCap(String skill) { + try { + return switch (skill) { + case "Farming" -> this.PLAYER_PROFILE.getAsJsonObject("jacobs_contest").getAsJsonObject("perks").get("farming_level_cap").getAsInt(); + default -> -1; + }; + } catch (Exception e) { + return 0; + } + } + + private long getSkillXP(String skill) { + try { + return switch (skill) { + case "SKILL_CATACOMBS" -> getCatacombsXP(); + case "SKILL_SOCIAL" -> getCoopSocialXP(); + case "SKILL_RUNECRAFT" -> this.skills.get("SKILL_RUNECRAFTING").getAsLong(); + default -> this.skills.get(skill).getAsLong(); + }; + } catch (Exception e) { + return 0; + } + } + + private long getCatacombsXP() { + try { + JsonObject dungeonSkills = this.PLAYER_PROFILE.getAsJsonObject("dungeons").getAsJsonObject("dungeon_types"); + return dungeonSkills.getAsJsonObject("catacombs").get("experience").getAsLong(); + } catch (Exception e) { + return 0; + } + } + + private long getCoopSocialXP() { + long socialXP = 0; + JsonObject members = HYPIXEL_PROFILE.getAsJsonObject("members"); + for (String memberId : members.keySet()) { + try { + socialXP += members.getAsJsonObject(memberId).getAsJsonObject("player_data").getAsJsonObject("experience").get("SKILL_SOCIAL").getAsLong(); + } catch (Exception e) { + ProfileViewerScreen.LOGGER.warn("[Skyblocker Profile Viewer] Error calculating co-op social xp", e); + } + } + return socialXP; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayerWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayerWidget.java new file mode 100644 index 00000000..a9c05c11 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayerWidget.java @@ -0,0 +1,93 @@ +package de.hysky.skyblocker.skyblock.profileviewer.slayers; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.utils.LevelFinder; +import de.hysky.skyblocker.skyblock.tabhud.util.Ico; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.Map; + +public class SlayerWidget { + private final String slayerName; + private final LevelFinder.LevelInfo slayerLevel; + private JsonObject slayerData = null; + + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/icon_data_widget.png"); + private static final Identifier BAR_FILL = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_fill"); + private static final Identifier BAR_BACK = Identifier.of(SkyblockerMod.NAMESPACE, "bars/bar_back"); + private final Identifier item; + private final ItemStack drop; + private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private static final Map<String, Identifier> HEAD_ICON = Map.ofEntries( + Map.entry("Zombie", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/zombie.png")), + Map.entry("Spider", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/spider.png")), + Map.entry("Wolf", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/wolf.png")), + Map.entry("Enderman", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/enderman.png")), + Map.entry("Vampire", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/vampire.png")), + Map.entry("Blaze", Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/blaze.png")) + ); + + private static final Map<String, ItemStack> DROP_ICON = Map.ofEntries( + Map.entry("Zombie", Ico.FLESH), + Map.entry("Spider", Ico.STRING), + Map.entry("Wolf", Ico.MUTTON), + Map.entry("Enderman", Ico.E_PEARL), + Map.entry("Vampire", Ico.REDSTONE), + Map.entry("Blaze", Ico.B_POWDER) + ); + + public SlayerWidget(String slayer, long xp, JsonObject playerProfile) { + this.slayerName = slayer; + this.slayerLevel = LevelFinder.getLevelInfo(slayer, xp); + this.item = HEAD_ICON.get(slayer); + this.drop = DROP_ICON.getOrDefault(slayer, Ico.BARRIER); + try { + this.slayerData = playerProfile.getAsJsonObject("slayer").getAsJsonObject("slayer_bosses").getAsJsonObject(this.slayerName.toLowerCase()); + } catch (Exception ignored) {} + } + + public void render(DrawContext context, int x, int y) { + context.drawTexture(TEXTURE, x, y, 0, 0, 109, 26, 109, 26); + context.drawTexture(this.item, x + 1, y + 3, 0, 0, 20, 20, 20, 20); + context.drawText(textRenderer, slayerName + " " + slayerLevel.level, x + 31, y + 5, Color.white.hashCode(), false); + + int col2 = x + 113; + context.drawTexture(TEXTURE, col2, y, 0, 0, 109, 26, 109, 26); + context.drawItem(this.drop, col2 + 3, y + 5); + context.drawText(textRenderer, "§aKills: §r" + findTotalKills(), col2 + 30, y + 4, Color.white.hashCode(), true); + context.drawText(textRenderer, findTopTierKills(), findTopTierKills().equals("No Data") ? col2 + 30 : col2 + 29, y + 15, Color.white.hashCode(), true); + + context.drawGuiTexture(BAR_BACK, x + 30, y + 15, 75, 6); + Color fillColor = slayerLevel.fill == 1 ? Color.MAGENTA : Color.green; + RenderHelper.renderNineSliceColored(context, BAR_FILL, x + 30, y + 15, (int) (75 * slayerLevel.fill), 6, fillColor); + } + + private int findTotalKills() { + try { + int totalKills = 0; + for (String key : this.slayerData.keySet()) { + if (key.startsWith("boss_kills_tier_")) totalKills += this.slayerData.get(key).getAsInt(); + } + return totalKills; + } catch (Exception e) { + return 0; + } + } + + private String findTopTierKills() { + try { + for (int tier = 4; tier >= 0; tier--) { + String key = "boss_kills_tier_" + tier; + if (this.slayerData.has(key)) return "§cT" + (tier + 1) + " Kills: §r" + this.slayerData.get(key).getAsInt(); + } + } catch (Exception ignored) {} + return "No Data"; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayersPage.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayersPage.java new file mode 100644 index 00000000..08e2ca06 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/slayers/SlayersPage.java @@ -0,0 +1,41 @@ +package de.hysky.skyblocker.skyblock.profileviewer.slayers; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import net.minecraft.client.gui.DrawContext; + +import java.util.ArrayList; +import java.util.List; + +public class SlayersPage implements ProfileViewerPage { + private static final String[] SLAYERS = {"Zombie", "Spider", "Wolf", "Enderman", "Vampire", "Blaze"}; + private static final int ROW_GAP = 28; + + private final List<SlayerWidget> slayerWidgets = new ArrayList<>(); + + public SlayersPage(JsonObject pProfile) { + try { + for (String slayer : SLAYERS) { + slayerWidgets.add(new SlayerWidget(slayer, getSlayerXP(slayer.toLowerCase(), pProfile), pProfile)); + } + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Error creating slayer widgets", e); + } + } + + public void render(DrawContext context, int mouseX, int mouseY, float delta, int rootX, int rootY) { + for (int i = 0; i < slayerWidgets.size(); i++) { + slayerWidgets.get(i).render(context, rootX, rootY + i * ROW_GAP); + } + } + + private long getSlayerXP(String slayer, JsonObject pProfile) { + try { + return pProfile.getAsJsonObject("slayer").getAsJsonObject("slayer_bosses") + .getAsJsonObject(slayer).get("xp").getAsLong(); + } catch (Exception e) { + return 0; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/LevelFinder.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/LevelFinder.java new file mode 100644 index 00000000..b52fd579 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/LevelFinder.java @@ -0,0 +1,307 @@ +package de.hysky.skyblocker.skyblock.profileviewer.utils; + +import java.util.ArrayList; +import java.util.List; + +public class LevelFinder { + public static class LevelInfo { + public long xp; + public int level; + public double fill; + + public LevelInfo(long xp, int level) { + this.xp = xp; + this.level = level; + } + + public LevelInfo(int level, double fill) { + this.level = level; + this.fill = fill; + } + } + + private static final long CATA_XP_PER_LEVEL = 200_000_000; + private static final List<LevelInfo> GENERIC_SKILL_BOUNDARIES = createGenericSkillBoundaries(); + private static final List<LevelInfo> CATACOMBS_SKILL_BOUNDARIES = createCatacombsSkillBoundaries(); + private static final List<LevelInfo> RUNECRAFT_SKILL_BOUNDARIES = createRunecraftSkillBoundaries(); + private static final List<LevelInfo> SOCIAL_SKILL_BOUNDARIES = createSocialSkillBoundaries(); + + private static final List<LevelInfo> COMMON_PET_BOUNDARIES = createCommonPetBoundaries(); + private static final List<LevelInfo> UNCOMMON_PET_BOUNDARIES = createUncommonPetBoundaries(); + private static final List<LevelInfo> RARE_PET_BOUNDARIES = createRarePetBoundaries(); + private static final List<LevelInfo> EPIC_PET_BOUNDARIES = createEpicPetBoundaries(); + private static final List<LevelInfo> LEGENDARY_PET_BOUNDARIES = createLegendaryPetBoundaries(); + + + private static final List<LevelInfo> GENERIC_SLAYER_BOUNDARIES = createGenericSlayerBoundaries(); + private static final List<LevelInfo> VAMPIRE_SLAYER_BOUNDARIES = createVampireSlayerBoundaries(); + + private static List<LevelInfo> createGenericSkillBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 50L, 175L, 375L, 675L, 1175L, 1925L, 2925L, 4425L, 6425L, + 9925L, 14925L, 22425L, 32425L, 47425L, 67425L, 97425L, 147425L, + 222425L, 322425L, 522425L, 822425L, 1222425L, 1722425L, 2322425L, + 3022425L, 3822425L, 4722425L, 5722425L, 6822425L, 8022425L, + 9322425L, 10722425L, 12222425L, 13822425L, 15522425L, 17322425L, + 19222425L, 21222425L, 23322425L, 25522425L, 27822425L, 30222425L, + 32722425L, 35322425L, 38072425L, 40972425L, 44072425L, 47472425L, + 51172425L, 55172425L, 59472425L, 64072425L, 68972425L, 74172425L, + 79672425L, 85472425L, 91572425L, 97972425L, 104672425L, 111672425L + }; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + return boundaries; + } + + private static List<LevelInfo> createCatacombsSkillBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 50L, 125L, 235L, 395L, 625L, 955L, 1425L, 2095L, 3045L, + 4385L, 6275L, 8940L, 12700L, 17960L, 25340L, 35640L, 50040L, + 70040L, 97640L, 135640L, 188140L, 259640L, 356640L, 488640L, + 668640L, 911640L, 1239640L, 1684640L, 2284640L, 3084640L, + 4149640L, 5559640L, 7459640L, 9959640L, 13259640L, 17559640L, + 23159640L, 30359640L, 39359640L, 51359640L, 66359640L, 85359640L, + 109559640L, 139559640L, 177559640L, 225559640L, 295559640L, + 360559640L, 453559640L, 569809640L + }; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + return boundaries; + } + + private static List<LevelInfo> createRunecraftSkillBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 50L, 150L, 275L, 435L, 635L, 885L, 1200L, 1600L, 2100L, + 2725L, 3150L, 4510L, 5760L, 7325L, 9325L, 11825L, 14950L, + 18950L, 23950L, 30200L, 38050L, 47850L, 60100L, 75400L, 94500L + }; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + return boundaries; + } + + private static List<LevelInfo> createSocialSkillBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 50L, 150L, 300L, 550L, 1050L, 1800L, 2800L, 4050L, 5550L, + 7550L, 10050L, 13050L, 16800L, 21300L, 27300L, 35300L, 45300L, + 57800L, 72800L, 92800L, 117800L, 147800L, 182800L, 222800L, + 272800L + }; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + return boundaries; + } + + private static List<LevelInfo> createGenericSlayerBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = {0L, 5L, 15L, 200L, 1000L, 5000L,20000L,100000L,400000L,1000000L}; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + return boundaries; + } + + private static List<LevelInfo> createVampireSlayerBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = {0L, 20L, 75L, 240L, 840L, 2400L}; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + private static List<LevelInfo> createCommonPetBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 0L, 100L, 210L, 330L, 460L, 605L, 765L, 940L, 1130L, 1340L, 1570L, 1820L, 2095L, + 2395L, 2725L, 3085L, 3485L, 3925L, 4415L, 4955L, 5555L, 6215L, 6945L, 7745L, + 8625L, 9585L, 10635L, 11785L, 13045L, 14425L, 15935L, 17585L, 19385L, 21345L, + 23475L, 25785L, 28285L, 30985L, 33905L, 37065L, 40485L, 44185L, 48185L, 52535L, + 57285L, 62485L, 68185L, 74485L, 81485L, 89285L, 97985L, 107685L, 118485L, 130485L, + 143785L, 158485L, 174685L, 192485L, 211985L, 233285L, 256485L, 281685L, 309085L, + 338885L, 371285L, 406485L, 444685L, 486085L, 530885L, 579285L, 631485L, 687685L, + 748085L, 812885L, 882285L, 956485L, 1035685L, 1120385L, 1211085L, 1308285L, + 1412485L, 1524185L, 1643885L, 1772085L, 1909285L, 2055985L, 2212685L, 2380385L, + 2560085L, 2752785L, 2959485L, 3181185L, 3418885L, 3673585L, 3946285L, 4237985L, + 4549685L, 4883385L, 5241085L, 5624785L + }; + + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + private static List<LevelInfo> createUncommonPetBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 0L, 175L, 365L, 575L, 805L, 1055L, 1330L, 1630L, 1960L, 2320L, 2720L, 3160L, + 3650L, 4190L, 4790L, 5450L, 6180L, 6980L, 7860L, 8820L, 9870L, 11020L, 12280L, + 13660L, 15170L, 16820L, 18620L, 20580L, 22710L, 25020L, 27520L, 30220L, 33140L, + 36300L, 39720L, 43420L, 47420L, 51770L, 56520L, 61720L, 67420L, 73720L, 80720L, + 88520L, 97220L, 106920L, 117720L, 129720L, 143020L, 157720L, 173920L, 191720L, + 211220L, 232520L, 255720L, 280920L, 308320L, 338120L, 370520L, 405720L, 443920L, + 485320L, 530120L, 578520L, 630720L, 686920L, 747320L, 812120L, 881520L, 955720L, + 1034920L, 1119620L, 1210320L, 1307520L, 1411720L, 1523420L, 1643120L, 1771320L, + 1908520L, 2055220L, 2211920L, 2379620L, 2559320L, 2752020L, 2958720L, 3180420L, + 3418120L, 3672820L, 3945520L, 4237220L, 4548920L, 4882620L, 5240320L, 5624020L, + 6035720L, 6477420L, 6954120L, 7470820L, 8032520L, 8644220L + }; + + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + private static List<LevelInfo> createRarePetBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 0L, 275L, 575L, 905L, 1265L, 1665L, 2105L, 2595L, 3135L, 3735L, 4395L, 5125L, + 5925L, 6805L, 7765L, 8815L, 9965L, 11225L, 12605L, 14115L, 15765L, 17565L, 19525L, + 21655L, 23965L, 26465L, 29165L, 32085L, 35245L, 38665L, 42365L, 46365L, 50715L, + 55465L, 60665L, 66365L, 72665L, 79665L, 87465L, 96165L, 105865L, 116665L, 128665L, + 141965L, 156665L, 172865L, 190665L, 210165L, 231465L, 254665L, 279865L, 307265L, + 337065L, 369465L, 404665L, 442865L, 484265L, 529065L, 577465L, 629665L, 685865L, + 746265L, 811065L, 880465L, 954665L, 1033865L, 1118565L, 1209265L, 1306465L, + 1410665L, 1522365L, 1642065L, 1770265L, 1907465L, 2054165L, 2210865L, 2378565L, + 2558265L, 2750965L, 2957665L, 3179365L, 3417065L, 3671765L, 3944465L, 4236165L, + 4547865L, 4881565L, 5239265L, 5622965L, 6034665L, 6476365L, 6953065L, 7469765L, + 8031465L, 8643165L, 9309865L, 10036565L, 10828265L, 11689965L, 12626665L + }; + + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + private static List<LevelInfo> createEpicPetBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + long[] cumulativeXp = { + 0L, 0L, 440L, 930L, 1470L, 2070L, 2730L, 3460L, 4260L, 5140L, 6100L, 7150L, 8300L, + 9560L, 10940L, 12450L, 14100L, 15900L, 17860L, 19990L, 22300L, 24800L, 27500L, 30420L, + 33580L, 37000L, 40700L, 44700L, 49050L, 53800L, 59000L, 64700L, 71000L, 78000L, 85800L, + 94500L, 104200L, 115000L, 127000L, 140300L, 155000L, 171200L, 189000L, 208500L, 229800L, + 253000L, 278200L, 305600L, 335400L, 367800L, 403000L, 441200L, 482600L, 527400L, 575800L, + 628000L, 684200L, 744600L, 809400L, 878800L, 953000L, 1032200L, 1116900L, 1207600L, 1304800L, + 1409000L, 1520700L, 1640400L, 1768600L, 1905800L, 2052500L, 2209200L, 2376900L, 2556600L, + 2749300L, 2956000L, 3177700L, 3415400L, 3670100L, 3942800L, 4234500L, 4546200L, 4879900L, + 5237600L, 5621300L, 6033000L, 6474700L, 6951400L, 7468100L, 8029800L, 8641500L, 9308200L, + 10034900L, 10826600L, 11688300L, 12625000L, 13641700L, 14743400L, 15935100L, 17221800L, + 18608500L + }; + + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + private static List<LevelInfo> createLegendaryPetBoundaries() { + List<LevelInfo> boundaries = new ArrayList<>(); + Long[] cumulativeXp = { + 0L, 0L, 660L, 1390L, 2190L, 3070L, 4030L, 5080L, 6230L, 7490L, + 8870L, 10380L, 12030L, 13830L, 15790L, 17920L, 20230L, 22730L, + 25430L, 28350L, 31510L, 34930L, 38630L, 42630L, 46980L, 51730L, + 56930L, 62630L, 68930L, 75930L, 83730L, 92430L, 102130L, 112930L, + 124930L, 138230L, 152930L, 169130L, 186930L, 206430L, 227730L, + 250930L, 276130L, 303530L, 333330L, 365730L, 400930L, 439130L, + 480530L, 525330L, 573730L, 625930L, 682130L, 742530L, 807330L, + 876730L, 950930L, 1030130L, 1114830L, 1205530L, 1302730L, 1406930L, + 1518630L, 1638330L, 1766530L, 1903730L, 2050430L, 2207130L, 2374830L, + 2554530L, 2747230L, 2953930L, 3175630L, 3413330L, 3668030L, 3940730L, + 4232430L, 4544130L, 4877830L, 5235530L, 5619230L, 6030930L, 6472630L, + 6949330L, 7466030L, 8027730L, 8639430L, 9306130L, 10032830L, 10824530L, + 11686230L, 12622930L, 13639630L, 14741330L, 15933030L, 17219730L, 18606430L, + 20103130L, 21719830L, 23466530L, 25353230L, 25353230L, 25358785L, 27245485L, + 29132185L, 31018885L, 32905585L, 34792285L, 36678985L, 38565685L, 40452385L, + 42339085L, 44225785L, 46112485L, 47999185L, 49885885L, 51772585L, 53659285L, + 55545985L, 57432685L, 59319385L, 61206085L, 63092785L, 64979485L, 66866185L, + 68752885L, 70639585L, 72526285L, 74412985L, 76299685L, 78186385L, 80073085L, + 81959785L, 83846485L, 85733185L, 87619885L, 89506585L, 91393285L, 93279985L, + 95166685L, 97053385L, 98940085L, 100826785L, 102713485L, 104600185L, 106486885L, + 108373585L, 110260285L, 112146985L, 114033685L, 115920385L, 117807085L, 119693785L, + 121580485L, 123467185L, 125353885L, 127240585L, 129127285L, 131013985L, 132900685L, + 134787385L, 136674085L, 138560785L, 140447485L, 142334185L, 144220885L, 146107585L, + 147994285L, 149880985L, 151767685L, 153654385L, 155541085L, 157427785L, 159314485L, + 161201185L, 163087885L, 164974585L, 166861285L, 168747985L, 170634685L, 172521385L, + 174408085L, 176294785L, 178181485L, 180068185L, 181954885L, 183841585L, 185728285L, + 187614985L, 189501685L, 191388385L, 193275085L, 195161785L, 197048485L, 198935185L, + 200821885L, 202708585L, 204595285L, 206481985L, 208368685L, 210255385L + }; + for (int i = 0; i < cumulativeXp.length; i++) { + boundaries.add(new LevelInfo(cumulativeXp[i], i)); + } + + return boundaries; + } + + public static LevelInfo getLevelInfo(String name, long xp) { + List<LevelInfo> boundaries = getLevelBoundaries(name, xp); + for (int i = boundaries.size() - 1; i >= 0 ; i--) { + if (xp >= boundaries.get(i).xp) { + double fill; + if (i < boundaries.getLast().level) { + double currentLevelXP = boundaries.get(i).xp; + double nextLevelXP = boundaries.get(i + 1).xp; + double levelXPRange = nextLevelXP - currentLevelXP; + double xpInCurrentLevel = xp - currentLevelXP; + fill = xpInCurrentLevel / levelXPRange; + } else { + fill = 1.0; + } + return new LevelInfo(boundaries.get(i).level, fill); + } + } + return new LevelInfo(0L, 0); + } + + + private static List<LevelInfo> getLevelBoundaries(String levelName, long xp) { + return switch (levelName) { + case "Vampire" -> VAMPIRE_SLAYER_BOUNDARIES; + case "Zombie", "Spider", "Wolf", "Enderman", "Blaze" -> GENERIC_SLAYER_BOUNDARIES; + case "PET_COMMON" -> COMMON_PET_BOUNDARIES; + case "PET_UNCOMMON" -> UNCOMMON_PET_BOUNDARIES; + case "PET_RARE" -> RARE_PET_BOUNDARIES; + case "PET_EPIC" -> EPIC_PET_BOUNDARIES; + case "PET_LEGENDARY", "PET_MYTHIC" -> LEGENDARY_PET_BOUNDARIES.subList(0,101); + case "PET_GREG" -> LEGENDARY_PET_BOUNDARIES; + case "Social" -> SOCIAL_SKILL_BOUNDARIES; + case "Runecraft" -> RUNECRAFT_SKILL_BOUNDARIES; + case "Catacombs" -> calculateCatacombsSkillBoundaries(xp); + default -> GENERIC_SKILL_BOUNDARIES; + }; + } + + private static List<LevelInfo> calculateCatacombsSkillBoundaries(long xp) { + if (xp >= CATACOMBS_SKILL_BOUNDARIES.getLast().xp) { + int additionalLevels = (int) ((xp - CATACOMBS_SKILL_BOUNDARIES.getLast().xp) / CATA_XP_PER_LEVEL) ; + + List<LevelInfo> updatedBoundaries = new ArrayList<>(CATACOMBS_SKILL_BOUNDARIES); + for (int i = 0; i <= additionalLevels; i++) { + int level = CATACOMBS_SKILL_BOUNDARIES.getLast().level + i + 1; + long nextLevelXP = updatedBoundaries.getLast().xp + CATA_XP_PER_LEVEL; + updatedBoundaries.add(new LevelInfo(nextLevelXP, level)); + } + + return updatedBoundaries; + } + + return CATACOMBS_SKILL_BOUNDARIES; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SkullCreator.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SkullCreator.java new file mode 100644 index 00000000..b074952c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SkullCreator.java @@ -0,0 +1,27 @@ +package de.hysky.skyblocker.skyblock.profileviewer.utils; + +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerScreen; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.ProfileComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; + +import java.util.Optional; +import java.util.UUID; + +public class SkullCreator { + public static ItemStack createSkull(String textureB64) { + ItemStack skull = new ItemStack(Items.PLAYER_HEAD); + try { + PropertyMap map = new PropertyMap(); + map.put("textures", new Property("textures", textureB64)); + ProfileComponent profile = new ProfileComponent(Optional.of("skull"), Optional.of(UUID.randomUUID()), map); + skull.set(DataComponentTypes.PROFILE, profile); + } catch (Exception e) { + ProfileViewerScreen.LOGGER.error("[Skyblocker Profile Viewer] Failed to create skull", e); + } + return skull; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SubPageSelectButton.java b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SubPageSelectButton.java new file mode 100644 index 00000000..4c9dcda4 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/profileviewer/utils/SubPageSelectButton.java @@ -0,0 +1,65 @@ +package de.hysky.skyblocker.skyblock.profileviewer.utils; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.profileviewer.ProfileViewerPage; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ButtonTextures; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.LoreComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; + +import java.awt.*; + +public class SubPageSelectButton extends ClickableWidget { + private final ProfileViewerPage page; + private final int index; + private boolean toggled; + + private static final ButtonTextures TEXTURES = new ButtonTextures(Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/button_icon_toggled.png"), Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/button_icon.png"), Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/button_icon_toggled_highlighted.png"), Identifier.of(SkyblockerMod.NAMESPACE, "textures/gui/profile_viewer/button_icon_highlighted.png")); + private final ItemStack ICON; + + public SubPageSelectButton(ProfileViewerPage page, int x, int y, int index, ItemStack item) { + super(x, y, 22, 22, item.getName()); + this.ICON = item; + this.toggled = index == 0; + this.index = index; + this.page = page; + visible = false; + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + context.fill(this.getX(), this.getY(), this.getX() + 20, this.getY() + 20, Color.BLACK.getRGB()); + context.drawTexture(TEXTURES.get(toggled, isHovered()), this.getX() + 1, this.getY() + 1,0, 0, 18, 18, 18, 18); + context.drawItem(ICON, this.getX() + 2, this.getY() + 2); + if ((mouseX > getX() + 1 && mouseX < getX() + 19 && mouseY > getY() + 1 && mouseY < getY() + 19)) { + LoreComponent lore = ICON.get(DataComponentTypes.LORE); + if (lore != null) context.drawTooltip(MinecraftClient.getInstance().textRenderer, lore.lines(), mouseX, mouseY + 10); + } + } + + @Override + protected boolean clicked(double mouseX, double mouseY) { + return this.active && this.visible &&(mouseX > getX() + 1 && mouseX < getX() + 19 && mouseY > getY() + 1 && mouseY < getY() + 19); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} + + public void setToggled(boolean toggled) { + this.toggled = toggled; + } + + @Override + public void onClick(double mouseX, double mouseY) { + page.onNavButtonClick(this); + } + + public int getIndex() { + return index; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java index c2c952cf..21d66805 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/Shortcuts.java @@ -97,7 +97,6 @@ public class Shortcuts { // Party commandArgs.put("/pa", "/p accept"); - commands.put("/pv", "/p leave"); commands.put("/pd", "/p disband"); commands.put("/rp", "/reparty"); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/Ico.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/Ico.java index 818056f0..b37a3883 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/Ico.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/util/Ico.java @@ -1,8 +1,11 @@ package de.hysky.skyblocker.skyblock.tabhud.util; +import net.minecraft.enchantment.Enchantment; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; +import static net.minecraft.enchantment.Enchantments.PROTECTION; + /** * Stores convenient shorthands for common ItemStack definitions */ @@ -10,23 +13,29 @@ public class Ico { public static final ItemStack MAP = new ItemStack(Items.FILLED_MAP); public static final ItemStack NTAG = new ItemStack(Items.NAME_TAG); public static final ItemStack EMERALD = new ItemStack(Items.EMERALD); + public static final ItemStack MAGMA_CREAM = new ItemStack(Items.MAGMA_CREAM); public static final ItemStack AMETHYST_SHARD = new ItemStack(Items.AMETHYST_SHARD); public static final ItemStack CLOCK = new ItemStack(Items.CLOCK); public static final ItemStack DIASWORD = new ItemStack(Items.DIAMOND_SWORD); public static final ItemStack DBUSH = new ItemStack(Items.DEAD_BUSH); public static final ItemStack VILLAGER = new ItemStack(Items.VILLAGER_SPAWN_EGG); + public static final ItemStack SPAWN_EGG = new ItemStack(Items.GHAST_SPAWN_EGG); public static final ItemStack MOREGOLD = new ItemStack(Items.GOLDEN_APPLE); public static final ItemStack COMPASS = new ItemStack(Items.COMPASS); public static final ItemStack SUGAR = new ItemStack(Items.SUGAR); - public static final ItemStack HOE = new ItemStack(Items.IRON_HOE); + public static final ItemStack IRON_HOE = new ItemStack(Items.IRON_HOE); + public static final ItemStack GOLDEN_HOE = new ItemStack(Items.GOLDEN_HOE); public static final ItemStack GOLD = new ItemStack(Items.GOLD_INGOT); + public static final ItemStack IRON = new ItemStack(Items.IRON_INGOT); public static final ItemStack BONE = new ItemStack(Items.BONE); public static final ItemStack SIGN = new ItemStack(Items.OAK_SIGN); public static final ItemStack FISH_ROD = new ItemStack(Items.FISHING_ROD); - public static final ItemStack SWORD = new ItemStack(Items.IRON_SWORD); + public static final ItemStack STONE_SWORD = new ItemStack(Items.STONE_SWORD); + public static final ItemStack IRON_SWORD = new ItemStack(Items.IRON_SWORD); public static final ItemStack LANTERN = new ItemStack(Items.LANTERN); public static final ItemStack COOKIE = new ItemStack(Items.COOKIE); public static final ItemStack POTION = new ItemStack(Items.POTION); + public static final ItemStack S_POTION = new ItemStack(Items.SPLASH_POTION); public static final ItemStack BARRIER = new ItemStack(Items.BARRIER); public static final ItemStack PLAYER = new ItemStack(Items.PLAYER_HEAD); public static final ItemStack WATER = new ItemStack(Items.WATER_BUCKET); @@ -37,6 +46,7 @@ public class Ico { public static final ItemStack STRING = new ItemStack(Items.STRING); public static final ItemStack WITHER = new ItemStack(Items.WITHER_SKELETON_SKULL); public static final ItemStack FLESH = new ItemStack(Items.ROTTEN_FLESH); + public static final ItemStack MUTTON = new ItemStack(Items.MUTTON); public static final ItemStack DRAGON = new ItemStack(Items.DRAGON_HEAD); public static final ItemStack DIAMOND = new ItemStack(Items.DIAMOND); public static final ItemStack ICE = new ItemStack(Items.ICE); @@ -46,25 +56,18 @@ public class Ico { public static final ItemStack BOOK = new ItemStack(Items.WRITABLE_BOOK); public static final ItemStack FURNACE = new ItemStack(Items.FURNACE); public static final ItemStack CHESTPLATE = new ItemStack(Items.IRON_CHESTPLATE); + public static final ItemStack L_CHESTPLATE = new ItemStack(Items.LEATHER_CHESTPLATE); public static final ItemStack B_ROD = new ItemStack(Items.BLAZE_ROD); + public static final ItemStack B_POWDER = new ItemStack(Items.BLAZE_POWDER); public static final ItemStack BOW = new ItemStack(Items.BOW); public static final ItemStack COPPER = new ItemStack(Items.COPPER_INGOT); public static final ItemStack NETHERITE_UPGRADE_ST = new ItemStack(Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE); public static final ItemStack COMPOSTER = new ItemStack(Items.COMPOSTER); public static final ItemStack SAPLING = new ItemStack(Items.OAK_SAPLING); public static final ItemStack SEEDS = new ItemStack(Items.WHEAT_SEEDS); - public static final ItemStack WHEAT = new ItemStack(Items.WHEAT); - public static final ItemStack CARROT = new ItemStack(Items.CARROT); - public static final ItemStack POTATO = new ItemStack(Items.POTATO); - public static final ItemStack SUGAR_CANE = new ItemStack(Items.SUGAR_CANE); - public static final ItemStack NETHER_WART = new ItemStack(Items.NETHER_WART); - public static final ItemStack MUSHROOM = new ItemStack(Items.RED_MUSHROOM); - public static final ItemStack CACTUS = new ItemStack(Items.CACTUS); - public static final ItemStack MELON = new ItemStack(Items.MELON); - public static final ItemStack PUMPKIN = new ItemStack(Items.PUMPKIN); - public static final ItemStack COCOA_BEANS = new ItemStack(Items.COCOA_BEANS); public static final ItemStack MILESTONE = new ItemStack(Items.LODESTONE); - public static final ItemStack PICKAXE = new ItemStack(Items.IRON_PICKAXE); + public static final ItemStack STONE_PICKAXE = new ItemStack(Items.STONE_PICKAXE); + public static final ItemStack IRON_PICKAXE = new ItemStack(Items.IRON_PICKAXE); public static final ItemStack NETHER_STAR = new ItemStack(Items.NETHER_STAR); public static final ItemStack HEART_OF_THE_SEA = new ItemStack(Items.HEART_OF_THE_SEA); public static final ItemStack EXPERIENCE_BOTTLE = new ItemStack(Items.EXPERIENCE_BOTTLE); @@ -73,4 +76,13 @@ public class Ico { public static final ItemStack ENCHANTED_BOOK = new ItemStack(Items.ENCHANTED_BOOK); public static final ItemStack SPIDER_EYE = new ItemStack(Items.SPIDER_EYE); public static final ItemStack BLUE_ICE = new ItemStack(Items.BLUE_ICE); + public static final ItemStack JUNGLE_SAPLING = new ItemStack(Items.JUNGLE_SAPLING); + public static final ItemStack ENCHANTING_TABLE = new ItemStack(Items.ENCHANTING_TABLE); + public static final ItemStack BREWING_STAND = new ItemStack(Items.BREWING_STAND); + public static final ItemStack CRAFTING_TABLE = new ItemStack(Items.CRAFTING_TABLE); + public static final ItemStack PAINTING = new ItemStack(Items.PAINTING); + public static final ItemStack E_PEARL = new ItemStack(Items.ENDER_PEARL); + public static final ItemStack FEATHER = new ItemStack(Items.FEATHER); + public static final ItemStack E_CHEST = new ItemStack(Items.ENDER_CHEST); + public static final ItemStack MYCELIUM = new ItemStack(Items.MYCELIUM); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDeathWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDeathWidget.java index 9c299210..79193b51 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDeathWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonDeathWidget.java @@ -38,7 +38,7 @@ public class DungeonDeathWidget extends Widget { this.addComponent(deaths); } - this.addSimpleIcoText(Ico.SWORD, "Damage Dealt:", Formatting.RED, 26); + this.addSimpleIcoText(Ico.IRON_SWORD, "Damage Dealt:", Formatting.RED, 26); this.addSimpleIcoText(Ico.POTION, "Healing Done:", Formatting.RED, 27); this.addSimpleIcoText(Ico.NTAG, "Milestone:", Formatting.YELLOW, 28); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ElectionWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ElectionWidget.java index ec935faf..8f50f9ff 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ElectionWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ElectionWidget.java @@ -32,10 +32,10 @@ public class ElectionWidget extends Widget { static { MAYOR_DATA.put("Aatrox", Ico.DIASWORD); - MAYOR_DATA.put("Cole", Ico.PICKAXE); + MAYOR_DATA.put("Cole", Ico.IRON_PICKAXE); MAYOR_DATA.put("Diana", Ico.BONE); MAYOR_DATA.put("Diaz", Ico.GOLD); - MAYOR_DATA.put("Finnegan", Ico.HOE); + MAYOR_DATA.put("Finnegan", Ico.IRON_HOE); MAYOR_DATA.put("Foxy", Ico.SUGAR); MAYOR_DATA.put("Paul", Ico.COMPASS); MAYOR_DATA.put("Scorpius", Ico.MOREGOLD); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenSkillsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenSkillsWidget.java index 75652b33..9e1f3989 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenSkillsWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/GardenSkillsWidget.java @@ -82,14 +82,14 @@ public class GardenSkillsWidget extends Widget { Text speed = Widget.simpleEntryText(67, "SPD", Formatting.WHITE); IcoTextComponent spd = new IcoTextComponent(Ico.SUGAR, speed); Text farmfort = Widget.simpleEntryText(68, "FFO", Formatting.GOLD); - IcoTextComponent ffo = new IcoTextComponent(Ico.HOE, farmfort); + IcoTextComponent ffo = new IcoTextComponent(Ico.IRON_HOE, farmfort); TableComponent tc = new TableComponent(2, 1, Formatting.YELLOW.getColorValue()); tc.addToCell(0, 0, spd); tc.addToCell(1, 0, ffo); this.addComponent(tc); - this.addComponent(new IcoTextComponent(Ico.HOE, PlayerListMgr.textAt(70))); + this.addComponent(new IcoTextComponent(Ico.IRON_HOE, PlayerListMgr.textAt(70))); ProgressComponent pc2; Matcher milestoneMatcher = PlayerListMgr.regexAt(69, MS_PATTERN); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ReputationWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ReputationWidget.java index 3c218fb1..34d15a28 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ReputationWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/ReputationWidget.java @@ -44,7 +44,7 @@ public class ReputationWidget extends Widget { if (fname.equals("Mage")) { faction = new IcoTextComponent(Ico.POTION, Text.literal(fname).formatted(Formatting.DARK_AQUA)); } else { - faction = new IcoTextComponent(Ico.SWORD, Text.literal(fname).formatted(Formatting.RED)); + faction = new IcoTextComponent(Ico.IRON_SWORD, Text.literal(fname).formatted(Formatting.RED)); } } this.addComponent(faction); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/SkillsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/SkillsWidget.java index 379fbb62..c9cf61aa 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/SkillsWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/SkillsWidget.java @@ -58,13 +58,13 @@ public class SkillsWidget extends Widget { Text speed = Widget.simpleEntryText(67, "SPD", Formatting.WHITE); IcoTextComponent spd = new IcoTextComponent(Ico.SUGAR, speed); Text strength = Widget.simpleEntryText(68, "STR", Formatting.RED); - IcoTextComponent str = new IcoTextComponent(Ico.SWORD, strength); + IcoTextComponent str = new IcoTextComponent(Ico.IRON_SWORD, strength); Text critDmg = Widget.simpleEntryText(69, "CCH", Formatting.BLUE); - IcoTextComponent cdg = new IcoTextComponent(Ico.SWORD, critDmg); + IcoTextComponent cdg = new IcoTextComponent(Ico.IRON_SWORD, critDmg); Text critCh = Widget.simpleEntryText(70, "CDG", Formatting.BLUE); - IcoTextComponent cch = new IcoTextComponent(Ico.SWORD, critCh); + IcoTextComponent cch = new IcoTextComponent(Ico.IRON_SWORD, critCh); Text aSpeed = Widget.simpleEntryText(71, "ASP", Formatting.YELLOW); - IcoTextComponent asp = new IcoTextComponent(Ico.HOE, aSpeed); + IcoTextComponent asp = new IcoTextComponent(Ico.IRON_HOE, aSpeed); TableComponent tc = new TableComponent(2, 3, Formatting.YELLOW.getColorValue()); tc.addToCell(0, 0, spd); diff --git a/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java b/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java index fbf814ee..5f65c336 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java +++ b/src/main/java/de/hysky/skyblocker/utils/ApiAuthentication.java @@ -7,6 +7,8 @@ import java.util.Base64; import java.util.Objects; import java.util.UUID; +import de.hysky.skyblocker.mixins.accessors.MinecraftClientAccessor; +import net.minecraft.client.session.ProfileKeys; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -56,8 +58,9 @@ public class ApiAuthentication { * was generated by Mojang and is tied to said player. For information about what the randomly signed data is used for and why see {@link #getRandomSignedData(PrivateKey)} */ private static void updateToken() { + ProfileKeys profileKeys = ((MinecraftClientAccessor) CLIENT).getProfileKeys(); //The fetching runs async in ProfileKeysImpl#getKeyPair - CLIENT.getProfileKeys().fetchKeyPair().thenAcceptAsync(playerKeypairOpt -> { + profileKeys.fetchKeyPair().thenAcceptAsync(playerKeypairOpt -> { if (playerKeypairOpt.isPresent()) { PlayerKeyPair playerKeyPair = playerKeypairOpt.get(); diff --git a/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java b/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java index c63af3ba..93e314a7 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java @@ -1,16 +1,14 @@ package de.hysky.skyblocker.utils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.google.gson.JsonParser; import com.mojang.util.UndashedUuid; - import de.hysky.skyblocker.utils.Http.ApiResponse; import de.hysky.skyblocker.utils.scheduler.Scheduler; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.minecraft.client.MinecraftClient; import net.minecraft.client.session.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /* * Contains only basic helpers for using Http APIs diff --git a/src/main/java/de/hysky/skyblocker/utils/Http.java b/src/main/java/de/hysky/skyblocker/utils/Http.java index 99db0316..051bd52e 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Http.java +++ b/src/main/java/de/hysky/skyblocker/utils/Http.java @@ -1,5 +1,10 @@ package de.hysky.skyblocker.utils; +import de.hysky.skyblocker.SkyblockerMod; +import net.minecraft.SharedConstants; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -16,12 +21,6 @@ import java.time.Duration; import java.util.zip.GZIPInputStream; import java.util.zip.InflaterInputStream; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import de.hysky.skyblocker.SkyblockerMod; -import net.minecraft.SharedConstants; - /** * @implNote All http requests are sent using HTTP 2 */ @@ -34,7 +33,7 @@ public class Http { .followRedirects(Redirect.NORMAL) .build(); - private static ApiResponse sendCacheableGetRequest(String url, @Nullable String token) throws IOException, InterruptedException { + public static ApiResponse sendCacheableGetRequest(String url, @Nullable String token) throws IOException, InterruptedException { HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() .GET() .header("Accept", "application/json") @@ -67,9 +66,8 @@ public class Http { .build(); HttpResponse<InputStream> response = HTTP_CLIENT.send(request, BodyHandlers.ofInputStream()); - InputStream decodedInputStream = getDecodedInputStream(response); - return decodedInputStream; + return getDecodedInputStream(response); } public static String sendGetRequest(String url) throws IOException, InterruptedException { @@ -91,6 +89,7 @@ public class Http { public static String sendPostRequest(String url, String requestBody, String contentType) throws IOException, InterruptedException { HttpRequest request = HttpRequest.newBuilder() .POST(BodyPublishers.ofString(requestBody)) + .header("Accept", contentType) .header("Accept-Encoding", "gzip, deflate") .header("Content-Type", contentType) .header("User-Agent", USER_AGENT) @@ -125,16 +124,12 @@ public class Http { String encoding = getContentEncoding(response.headers()); try { - switch (encoding) { - case "": - return response.body(); - case "gzip": - return new GZIPInputStream(response.body()); - case "deflate": - return new InflaterInputStream(response.body()); - default: - throw new UnsupportedOperationException("The server sent content in an unexpected encoding: " + encoding); - } + return switch (encoding) { + case "" -> response.body(); + case "gzip" -> new GZIPInputStream(response.body()); + case "deflate" -> new InflaterInputStream(response.body()); + default -> throw new UnsupportedOperationException("The server sent content in an unexpected encoding: " + encoding); + }; } catch (IOException e) { throw new UncheckedIOException(e); } diff --git a/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java b/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java index a786e79f..aa7a0492 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/ProfileUtils.java @@ -4,7 +4,6 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import de.hysky.skyblocker.SkyblockerMod; import it.unimi.dsi.fastutil.objects.ObjectLongPair; -import net.minecraft.client.MinecraftClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,13 +16,24 @@ public class ProfileUtils { private static final long HYPIXEL_API_COOLDOWN = 300000; // 5min = 300000 public static Map<String, ObjectLongPair<JsonObject>> players = new HashMap<>(); - - public static CompletableFuture<JsonObject> updateProfile() { - return updateProfile(MinecraftClient.getInstance().getSession().getUsername()); + public static Map<String, ObjectLongPair<JsonObject>> profiles = new HashMap<>(); + + public static CompletableFuture<JsonObject> updateProfileByName(String name) { + return fetchFullProfile(name).thenApply(profile -> { + JsonObject player = profile.getAsJsonArray("profiles").asList().stream() + .map(JsonElement::getAsJsonObject) + .filter(profileObj -> profileObj.getAsJsonPrimitive("selected").getAsBoolean()) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No selected profile found!?")) + .getAsJsonObject("members").get(name).getAsJsonObject(); + + players.put(name, ObjectLongPair.of(player, System.currentTimeMillis())); + return player; + }); } - public static CompletableFuture<JsonObject> updateProfile(String name) { - ObjectLongPair<JsonObject> playerCache = players.get(name); + public static CompletableFuture<JsonObject> fetchFullProfile(String name) { + ObjectLongPair<JsonObject> playerCache = profiles.get(name); if (playerCache != null && playerCache.rightLong() + HYPIXEL_API_COOLDOWN > System.currentTimeMillis()) { return CompletableFuture.completedFuture(playerCache.left()); } @@ -32,19 +42,12 @@ public class ProfileUtils { String uuid = ApiUtils.name2Uuid(name); try (Http.ApiResponse response = Http.sendHypixelRequest("skyblock/profiles", "?uuid=" + uuid)) { if (!response.ok()) { - throw new IllegalStateException("Failed to get profile uuid for players " + name + "! Response: " + response.content()); + throw new IllegalStateException("Failed to get profile uuid for player: " + name + "! Response: " + response.content()); } - JsonObject responseJson = SkyblockerMod.GSON.fromJson(response.content(), JsonObject.class); - - JsonObject player = responseJson.getAsJsonArray("profiles").asList().stream() - .map(JsonElement::getAsJsonObject) - .filter(profile -> profile.getAsJsonPrimitive("selected").getAsBoolean()) - .findFirst() - .orElseThrow(() -> new IllegalStateException("No selected profile found!?")) - .getAsJsonObject("members").get(uuid).getAsJsonObject(); + JsonObject profile = SkyblockerMod.GSON.fromJson(response.content(), JsonObject.class); + profiles.put(name, ObjectLongPair.of(profile, System.currentTimeMillis())); - players.put(name, ObjectLongPair.of(player, System.currentTimeMillis())); - return player; + return profile; } catch (Exception e) { LOGGER.error("[Skyblocker Profile Utils] Failed to get Player Profile Data for players {}, is the API Down/Limited?", name, e); } diff --git a/src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java b/src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java index 045ecc4e..ec3effaf 100644 --- a/src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java +++ b/src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java @@ -21,10 +21,10 @@ public class SkyblockTime { public static void init() { updateTime(); //ScheduleCyclic already runs the task upon scheduling, so there's no need to call updateTime() here - Scheduler.INSTANCE.schedule(() -> Scheduler.INSTANCE.scheduleCyclic(SkyblockTime::updateTime, 1200 * 24), (int) (1200000 - (getSkyblockMillis() % 1200000)) / 50); + Scheduler.INSTANCE.schedule(() -> Scheduler.INSTANCE.scheduleCyclic(SkyblockTime::updateTime, 1200 * 20), (int) (1200000 - (getSkyblockMillis() % 1200000)) / 50); } - private static long getSkyblockMillis() { + public static long getSkyblockMillis() { return System.currentTimeMillis() - SKYBLOCK_EPOCH; } diff --git a/src/main/java/de/hysky/skyblocker/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java index 84b3cb9e..051110b2 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Utils.java +++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java @@ -3,7 +3,6 @@ package de.hysky.skyblocker.utils; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.mojang.util.UndashedUuid; - import de.hysky.skyblocker.events.SkyblockEvents; import de.hysky.skyblocker.mixins.accessors.MessageHandlerAccessor; import de.hysky.skyblocker.skyblock.item.MuseumItemCache; @@ -22,11 +21,11 @@ import net.minecraft.scoreboard.*; import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.util.Formatting; +import org.apache.http.client.HttpResponseException; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; import java.time.Instant; import java.util.Collections; import java.util.List; @@ -77,7 +76,8 @@ public class Utils { private static boolean canSendLocRaw = false; //This is required to prevent the location change event from being fired twice. private static boolean locationChanged = true; - + private static boolean mayorTickScheduled = false; + private static int mayorTickRetryAttempts = 0; private static String mayor = ""; /** @@ -95,15 +95,15 @@ public class Utils { } public static boolean isInDungeons() { - return location == Location.DUNGEON || FabricLoader.getInstance().isDevelopmentEnvironment(); + return location == Location.DUNGEON; } public static boolean isInCrystalHollows() { - return location == Location.CRYSTAL_HOLLOWS || FabricLoader.getInstance().isDevelopmentEnvironment(); + return location == Location.CRYSTAL_HOLLOWS; } public static boolean isInDwarvenMines() { - return location == Location.DWARVEN_MINES || location == Location.GLACITE_MINESHAFT || FabricLoader.getInstance().isDevelopmentEnvironment(); + return location == Location.DWARVEN_MINES || location == Location.GLACITE_MINESHAFT; } public static boolean isInTheRift() { @@ -120,6 +120,9 @@ public class Utils { public static boolean isInKuudra() { return location == Location.KUUDRAS_HOLLOW; } + public static boolean isInCrimson() { + return location == Location.CRIMSON_ISLE; + } public static boolean isInModernForagingIsland() { return location == Location.MODERN_FORAGING_ISLAND; @@ -191,11 +194,16 @@ public class Utils { } public static void init() { - SkyblockEvents.JOIN.register(() -> tickMayorCache(false)); + SkyblockEvents.JOIN.register(() -> { + if (!mayorTickScheduled) { + tickMayorCache(); + scheduleMayorTick(); + mayorTickScheduled = true; + } + }); ClientPlayConnectionEvents.JOIN.register(Utils::onClientWorldJoin); ClientReceiveMessageEvents.ALLOW_GAME.register(Utils::onChatMessage); ClientReceiveMessageEvents.GAME_CANCELED.register(Utils::onChatMessage); // Somehow this works even though onChatMessage returns a boolean - Scheduler.INSTANCE.scheduleCyclic(() -> tickMayorCache(true), 24_000, true); // Update every 20 minutes } /** @@ -358,8 +366,8 @@ public class Utils { // TODO: Combine with `ChocolateFactorySolver.formatTime` and move into `SkyblockTime`. public static Text getDurationText(int timeInSeconds) { int seconds = timeInSeconds % 60; - int minutes = (timeInSeconds/60) % 60; - int hours = (timeInSeconds/3600); + int minutes = (timeInSeconds / 60) % 60; + int hours = (timeInSeconds / 3600); MutableText time = Text.empty(); if (hours > 0) { @@ -460,7 +468,11 @@ public class Utils { if (message.startsWith(PROFILE_MESSAGE_PREFIX)) { profile = message.substring(PROFILE_MESSAGE_PREFIX.length()).split("§b")[0]; } else if (message.startsWith(PROFILE_ID_PREFIX)) { + String prevProfileId = profileId; profileId = message.substring(PROFILE_ID_PREFIX.length()); + if (!prevProfileId.equals(profileId)) { + SkyblockEvents.PROFILE_CHANGE.invoker().onSkyblockProfileChange(prevProfileId, profileId); + } MuseumItemCache.tick(profileId); } @@ -479,27 +491,47 @@ public class Utils { location = Location.UNKNOWN; } - private static void tickMayorCache(boolean refresh) { - if (!mayor.isEmpty() && !refresh) return; + private static void scheduleMayorTick() { + long currentYearMillis = SkyblockTime.getSkyblockMillis() % 446400000L; //446400000ms is 1 year, 105600000ms is the amount of time from early spring 1st to late spring 27th + // If current time is past late spring 27th, the next mayor change is at next year's spring 27th, otherwise it's at this year's spring 27th + long millisUntilNextMayorChange = currentYearMillis > 105600000L ? 446400000L - currentYearMillis + 105600000L : 105600000L - currentYearMillis; + Scheduler.INSTANCE.schedule(Utils::tickMayorCache, (int) (millisUntilNextMayorChange / 50) + 5 * 60 * 20); // 5 extra minutes to allow the cache to expire. This is a simpler than checking age and subtracting from max age and rescheduling again. + } + private static void tickMayorCache() { CompletableFuture.supplyAsync(() -> { try { - JsonObject json = JsonParser.parseString(Http.sendGetRequest("https://api.hypixel.net/v2/resources/skyblock/election")).getAsJsonObject(); - if (json.get("success").getAsBoolean()) return json.get("mayor").getAsJsonObject().get("name").getAsString(); - throw new IOException(json.get("cause").getAsString()); + Http.ApiResponse response = Http.sendCacheableGetRequest("https://api.hypixel.net/v2/resources/skyblock/election", null); //Authentication is not required for this endpoint + if (!response.ok()) throw new HttpResponseException(response.statusCode(), response.content()); + JsonObject json = JsonParser.parseString(response.content()).getAsJsonObject(); + if (!json.get("success").getAsBoolean()) throw new RuntimeException("Request failed!"); //Can't find a more appropriate exception to throw here. + return json.get("mayor").getAsJsonObject().get("name").getAsString(); } catch (Exception e) { - LOGGER.error("[Skyblocker] Failed to get mayor status!", e); + throw new RuntimeException(e); //Wrap the exception to be handled by the exceptionally block + } + }).exceptionally(throwable -> { + LOGGER.error("[Skyblocker] Failed to get mayor status!", throwable.getCause()); + if (mayorTickRetryAttempts < 5) { + int minutes = 5 * 1 << mayorTickRetryAttempts; //5, 10, 20, 40, 80 minutes + mayorTickRetryAttempts++; + LOGGER.warn("[Skyblocker] Retrying in {} minutes.", minutes); + Scheduler.INSTANCE.schedule(Utils::tickMayorCache, minutes * 60 * 20); + } else { + LOGGER.warn("[Skyblocker] Failed to get mayor status after 5 retries! Stopping further retries until next reboot."); + } + return ""; //Have to return a value for the thenAccept block. + }).thenAccept(result -> { + if (!result.isEmpty()) { + mayor = result; + LOGGER.info("[Skyblocker] Mayor set to {}.", mayor); + scheduleMayorTick(); //Ends up as a cyclic task with finer control over scheduled time } - return ""; - }).thenAccept(s -> { - if (!s.isEmpty()) mayor = s; }); - } /** * Used to avoid triggering things like chat rules or chat listeners infinitely, do not use otherwise. - * + * <p> * Bypasses MessageHandler#onGameMessage */ public static void sendMessageToBypassEvents(Text message) { diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java index 8a5d32be..9a1e3072 100644 --- a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java @@ -12,6 +12,7 @@ import de.hysky.skyblocker.skyblock.dungeon.terminal.ColorTerminal; import de.hysky.skyblocker.skyblock.dungeon.terminal.LightsOnTerminal; import de.hysky.skyblocker.skyblock.dungeon.terminal.OrderTerminal; import de.hysky.skyblocker.skyblock.dungeon.terminal.StartsWithTerminal; +import de.hysky.skyblocker.skyblock.dwarven.CommissionHighlight; import de.hysky.skyblocker.skyblock.experiment.ChronomatronSolver; import de.hysky.skyblocker.skyblock.experiment.SuperpairsSolver; import de.hysky.skyblocker.skyblock.experiment.UltrasequencerSolver; @@ -51,6 +52,7 @@ public class ContainerSolverManager { new StartsWithTerminal(), new LightsOnTerminal(), new CroesusHelper(), + new CommissionHighlight(), new CroesusProfit(), new ChronomatronSolver(), new SuperpairsSolver(), diff --git a/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java b/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java index 2f5375fe..547adc5c 100644 --- a/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java +++ b/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java @@ -2,6 +2,7 @@ package de.hysky.skyblocker.utils.scheduler; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; import it.unimi.dsi.fastutil.ints.AbstractInt2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; @@ -14,6 +15,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Function; import java.util.function.Supplier; /** @@ -73,18 +75,56 @@ public class Scheduler { } } + /** + * Creates a command that queues a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. + * + * @param screenFactory the factory of the screen to open + * @return the command + */ + public static Command<FabricClientCommandSource> queueOpenScreenFactoryCommand(Function<CommandContext<FabricClientCommandSource>, Screen> screenFactory) { + return context -> queueOpenScreen(screenFactory.apply(context)); + } + + /** + * Creates a command that queues a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. + * + * @param screenSupplier the supplier of the screen to open + * @return the command + */ public static Command<FabricClientCommandSource> queueOpenScreenCommand(Supplier<Screen> screenSupplier) { - return context -> INSTANCE.queueOpenScreen(screenSupplier); + return context -> queueOpenScreen(screenSupplier.get()); + } + + /** + * Creates a command that queues a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. + * + * @param screen the screen to open + * @return the command + */ + public static Command<FabricClientCommandSource> queueOpenScreenCommand(Screen screen) { + return context -> queueOpenScreen(screen); } /** * Schedules a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. * + * @deprecated Use {@link #queueOpenScreen(Screen)} instead * @param screenSupplier the supplier of the screen to open * @see #queueOpenScreenCommand(Supplier) */ - public int queueOpenScreen(Supplier<Screen> screenSupplier) { - MinecraftClient.getInstance().send(() -> MinecraftClient.getInstance().setScreen(screenSupplier.get())); + @Deprecated(forRemoval = true) + public static int queueOpenScreen(Supplier<Screen> screenSupplier) { + return queueOpenScreen(screenSupplier.get()); + } + + /** + * Schedules a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. + * + * @param screen the screen to open + * @see #queueOpenScreenFactoryCommand(Function) + */ + public static int queueOpenScreen(Screen screen) { + MinecraftClient.getInstance().send(() -> MinecraftClient.getInstance().setScreen(screen)); return Command.SINGLE_SUCCESS; } |