diff options
Diffstat (limited to 'src')
84 files changed, 2231 insertions, 683 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index 1e3c4e7d..681e24f3 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -5,9 +5,12 @@ import com.google.gson.GsonBuilder; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.debug.Debug; import de.hysky.skyblocker.skyblock.*; -import de.hysky.skyblocker.skyblock.waypoint.MythologicalRitual; import de.hysky.skyblocker.skyblock.dungeon.*; -import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonSecrets; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.CreeperBeams; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.DungeonBlaze; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.TicTacToe; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.waterboard.Waterboard; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import de.hysky.skyblocker.skyblock.dungeon.secrets.SecretsTracker; import de.hysky.skyblocker.skyblock.dwarven.DwarvenHud; import de.hysky.skyblocker.skyblock.item.*; @@ -18,11 +21,12 @@ import de.hysky.skyblocker.skyblock.quicknav.QuickNav; import de.hysky.skyblocker.skyblock.rift.TheRift; import de.hysky.skyblocker.skyblock.shortcut.Shortcuts; import de.hysky.skyblocker.skyblock.special.SpecialEffects; -import de.hysky.skyblocker.skyblock.waypoint.Relics; import de.hysky.skyblocker.skyblock.tabhud.TabHud; import de.hysky.skyblocker.skyblock.tabhud.screenbuilder.ScreenMaster; import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr; import de.hysky.skyblocker.skyblock.waypoint.FairySouls; +import de.hysky.skyblocker.skyblock.waypoint.MythologicalRitual; +import de.hysky.skyblocker.skyblock.waypoint.Relics; import de.hysky.skyblocker.utils.ApiUtils; import de.hysky.skyblocker.utils.NEURepoManager; import de.hysky.skyblocker.utils.Utils; @@ -79,8 +83,10 @@ public class SkyblockerMod implements ClientModInitializer { ClientTickEvents.END_CLIENT_TICK.register(this::tick); Utils.init(); SkyblockerConfigManager.init(); + Tips.init(); NEURepoManager.init(); ItemRepository.init(); + PlayerHeadHashCache.init(); HotbarSlotLock.init(); ItemTooltip.init(); WikiLookup.init(); @@ -98,8 +104,10 @@ public class SkyblockerMod implements ClientModInitializer { FishingHelper.init(); TabHud.init(); DungeonMap.init(); - DungeonSecrets.init(); + DungeonManager.init(); DungeonBlaze.init(); + Waterboard.init(); + DungeonScore.init(); ChestValue.init(); FireFreezeStaffTimer.init(); GuardianHealth.init(); @@ -127,7 +135,6 @@ public class SkyblockerMod implements ClientModInitializer { statusBarTracker.init(); Scheduler.INSTANCE.scheduleCyclic(Utils::update, 20); Scheduler.INSTANCE.scheduleCyclic(DiscordRPCManager::updateDataAndPresence, 200); - Scheduler.INSTANCE.scheduleCyclic(TicTacToe::tick, 4); Scheduler.INSTANCE.scheduleCyclic(LividColor::update, 10); Scheduler.INSTANCE.scheduleCyclic(BackpackPreview::tick, 50); Scheduler.INSTANCE.scheduleCyclic(DwarvenHud::update, 40); diff --git a/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java b/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java index 9a7a41b5..8b0f27a7 100644 --- a/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java +++ b/src/main/java/de/hysky/skyblocker/config/ConfigUtils.java @@ -1,9 +1,7 @@ package de.hysky.skyblocker.config; import dev.isxander.yacl3.api.Option; -import dev.isxander.yacl3.api.controller.BooleanControllerBuilder; -import dev.isxander.yacl3.api.controller.EnumControllerBuilder; -import dev.isxander.yacl3.api.controller.ValueFormatter; +import dev.isxander.yacl3.api.controller.*; import net.minecraft.text.Text; import net.minecraft.util.Formatting; import org.apache.commons.lang3.StringUtils; @@ -11,7 +9,7 @@ import org.apache.commons.lang3.StringUtils; import java.util.function.Function; public class ConfigUtils { - public static final Function<Formatting, String> FORMATTING_TO_STRING = formatting -> StringUtils.capitalize(formatting.getName().replaceAll("_", " ")); + public static final ValueFormatter<Formatting> FORMATTING_FORMATTER = formatting -> Text.literal(StringUtils.capitalize(formatting.getName().replaceAll("_", " "))); public static final ValueFormatter<Float> FLOAT_TWO_FORMATTER = value -> Text.literal(String.format("%,.2f", value).replaceAll("[\u00a0\u202F]", " ")); public static BooleanControllerBuilder createBooleanController(Option<Boolean> opt) { @@ -22,4 +20,18 @@ public class ConfigUtils { public static <E extends Enum<E>> EnumControllerBuilder<E> createEnumCyclingListController(Option<E> opt) { return EnumControllerBuilder.create(opt).enumClass((Class<E>) opt.binding().defaultValue().getClass()); } + + /** + * Creates a factory for {@link EnumDropdownControllerBuilder}s with the given function for converting enum constants to texts. + * Use this if a custom formatter function for an enum is needed. + * Use it like this: + * <pre>{@code Option.<MyEnum>createBuilder().controller(ConfigUtils.getEnumDropdownControllerFactory(MY_CUSTOM_ENUM_TO_TEXT_FUNCTION))}</pre> + * + * @param formatter The function used to convert enum constants to texts used for display, suggestion, and validation + * @param <E> the enum type + * @return a factory for {@link EnumDropdownControllerBuilder}s + */ + public static <E extends Enum<E>> Function<Option<E>, ControllerBuilder<E>> getEnumDropdownControllerFactory(ValueFormatter<E> formatter) { + return opt -> EnumDropdownControllerBuilder.create(opt).formatValue(formatter); + } } diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java index 8604913c..609e7c2f 100644 --- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java @@ -150,6 +150,9 @@ public class SkyblockerConfig { public static class General { @SerialEntry + public boolean enableTips = true; + + @SerialEntry public boolean acceptReparty = true; @SerialEntry @@ -163,6 +166,9 @@ public class SkyblockerConfig { @SerialEntry public boolean hideStatusEffectOverlay = false; + + @SerialEntry + public boolean dontStripSkinAlphaValues = true; @SerialEntry public TabHudConf tabHud = new TabHudConf(); @@ -591,6 +597,9 @@ public class SkyblockerConfig { public DoorHighlight doorHighlight = new DoorHighlight(); @SerialEntry + public DungeonScore dungeonScore = new DungeonScore(); + + @SerialEntry public DungeonChestProfit dungeonChestProfit = new DungeonChestProfit(); @SerialEntry @@ -630,6 +639,9 @@ public class SkyblockerConfig { public boolean solveTicTacToe = true; @SerialEntry + public boolean solveWaterboard = true; + + @SerialEntry public boolean fireFreezeStaffTimer = true; @SerialEntry @@ -644,10 +656,10 @@ public class SkyblockerConfig { public static class SecretWaypoints { @SerialEntry - public boolean enableSecretWaypoints = true; + public boolean enableRoomMatching = true; @SerialEntry - public boolean noInitSecretWaypoints = false; + public boolean enableSecretWaypoints = true; @SerialEntry public Waypoint.Type waypointType = Waypoint.Type.WAYPOINT; @@ -715,6 +727,32 @@ public class SkyblockerConfig { } } + public static class DungeonScore { + @SerialEntry + public boolean enableDungeonScore270Message = false; + + @SerialEntry + public boolean enableDungeonScore270Title = false; + + @SerialEntry + public boolean enableDungeonScore270Sound = false; + + @SerialEntry + public String dungeonScore270Message = "270 Score Reached!"; + + @SerialEntry + public boolean enableDungeonScore300Message = true; + + @SerialEntry + public boolean enableDungeonScore300Title = true; + + @SerialEntry + public boolean enableDungeonScore300Sound = true; + + @SerialEntry + public String dungeonScore300Message = "300 Score Reached!"; + } + public static class DungeonChestProfit { @SerialEntry public boolean enableProfitCalculator = true; @@ -749,6 +787,9 @@ public class SkyblockerConfig { public boolean enableLividColorText = true; @SerialEntry + public boolean enableLividColorTitle = true; + + @SerialEntry public String lividColorText = "The livid color is [color]"; } @@ -920,6 +961,9 @@ public class SkyblockerConfig { public ChatFilterResult hideShowOff = ChatFilterResult.PASS; @SerialEntry + public ChatFilterResult hideToggleSkyMall = ChatFilterResult.PASS; + + @SerialEntry public boolean hideMana = false; } diff --git a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java index 246611cc..ad64e2e4 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java @@ -12,7 +12,6 @@ import dev.isxander.yacl3.api.OptionGroup; import dev.isxander.yacl3.api.controller.FloatFieldControllerBuilder; import dev.isxander.yacl3.api.controller.IntegerFieldControllerBuilder; import dev.isxander.yacl3.api.controller.StringControllerBuilder; -import de.hysky.skyblocker.config.controllers.EnumDropdownControllerBuilder; import de.hysky.skyblocker.skyblock.dungeon.DungeonMapConfigScreen; import net.minecraft.client.MinecraftClient; import net.minecraft.text.Text; @@ -29,21 +28,21 @@ public class DungeonsCategory { .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints")) .collapsed(true) .option(Option.<Boolean>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableRoomMatching")) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableRoomMatching.@Tooltip"))) + .binding(defaults.locations.dungeons.secretWaypoints.enableRoomMatching, + () -> config.locations.dungeons.secretWaypoints.enableRoomMatching, + newValue -> config.locations.dungeons.secretWaypoints.enableRoomMatching = newValue) + .controller(ConfigUtils::createBooleanController) + .flag(OptionFlag.GAME_RESTART) + .build()) + .option(Option.<Boolean>createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableSecretWaypoints")) .binding(defaults.locations.dungeons.secretWaypoints.enableSecretWaypoints, () -> config.locations.dungeons.secretWaypoints.enableSecretWaypoints, newValue -> config.locations.dungeons.secretWaypoints.enableSecretWaypoints = newValue) .controller(ConfigUtils::createBooleanController) .build()) - .option(Option.<Boolean>createBuilder() - .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints")) - .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints.@Tooltip"))) - .binding(defaults.locations.dungeons.secretWaypoints.noInitSecretWaypoints, - () -> config.locations.dungeons.secretWaypoints.noInitSecretWaypoints, - newValue -> config.locations.dungeons.secretWaypoints.noInitSecretWaypoints = newValue) - .controller(ConfigUtils::createBooleanController) - .flag(OptionFlag.GAME_RESTART) - .build()) .option(Option.<Type>createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.general.waypoints.waypointType")) .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.waypoints.waypointType.@Tooltip"))) @@ -168,6 +167,76 @@ public class DungeonsCategory { .build()) .build()) + //Dungeon Score + .group(OptionGroup.createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore")) + .collapsed(true) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreMessage", 270)) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreMessage.@Tooltip", 270))) + .binding(defaults.locations.dungeons.dungeonScore.enableDungeonScore270Message, + () -> config.locations.dungeons.dungeonScore.enableDungeonScore270Message, + newValue -> config.locations.dungeons.dungeonScore.enableDungeonScore270Message = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreTitle", 270)) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreTitle.@Tooltip", 270))) + .binding(defaults.locations.dungeons.dungeonScore.enableDungeonScore270Title, + () -> config.locations.dungeons.dungeonScore.enableDungeonScore270Title, + newValue -> config.locations.dungeons.dungeonScore.enableDungeonScore270Title = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreSound", 270)) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreSound.@Tooltip", 270))) + .binding(defaults.locations.dungeons.dungeonScore.enableDungeonScore270Sound, + () -> config.locations.dungeons.dungeonScore.enableDungeonScore270Sound, + newValue -> config.locations.dungeons.dungeonScore.enableDungeonScore270Sound = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<String>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.dungeonScoreMessage", 270)) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.dungeonScoreMessage.@Tooltip", 270, 270))) + .binding(defaults.locations.dungeons.dungeonScore.dungeonScore270Message, + () -> config.locations.dungeons.dungeonScore.dungeonScore270Message, + newValue -> config.locations.dungeons.dungeonScore.dungeonScore270Message = newValue) + .controller(StringControllerBuilder::create) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreMessage", 300)) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreMessage.@Tooltip", 300))) + .binding(defaults.locations.dungeons.dungeonScore.enableDungeonScore300Message, + () -> config.locations.dungeons.dungeonScore.enableDungeonScore300Message, + newValue -> config.locations.dungeons.dungeonScore.enableDungeonScore300Message = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreTitle", 300)) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreTitle.@Tooltip", 300))) + .binding(defaults.locations.dungeons.dungeonScore.enableDungeonScore300Title, + () -> config.locations.dungeons.dungeonScore.enableDungeonScore300Title, + newValue -> config.locations.dungeons.dungeonScore.enableDungeonScore300Title = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreSound", 300)) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreSound.@Tooltip", 300))) + .binding(defaults.locations.dungeons.dungeonScore.enableDungeonScore300Sound, + () -> config.locations.dungeons.dungeonScore.enableDungeonScore300Sound, + newValue -> config.locations.dungeons.dungeonScore.enableDungeonScore300Sound = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<String>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.dungeonScoreMessage", 300)) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.dungeonScoreMessage.@Tooltip", 300, 300))) + .binding(defaults.locations.dungeons.dungeonScore.dungeonScore300Message, + () -> config.locations.dungeons.dungeonScore.dungeonScore300Message, + newValue -> config.locations.dungeons.dungeonScore.dungeonScore300Message = newValue) + .controller(StringControllerBuilder::create) + .build()) + .build()) + //Dungeon Chest Profit .group(OptionGroup.createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit")) @@ -209,21 +278,21 @@ public class DungeonsCategory { .binding(defaults.locations.dungeons.dungeonChestProfit.neutralColor, () -> config.locations.dungeons.dungeonChestProfit.neutralColor, newValue -> config.locations.dungeons.dungeonChestProfit.neutralColor = newValue) - .controller(EnumDropdownControllerBuilder.getFactory(ConfigUtils.FORMATTING_TO_STRING)) + .controller(ConfigUtils.getEnumDropdownControllerFactory(ConfigUtils.FORMATTING_FORMATTER)) .build()) .option(Option.<Formatting>createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.profitColor")) .binding(defaults.locations.dungeons.dungeonChestProfit.profitColor, () -> config.locations.dungeons.dungeonChestProfit.profitColor, newValue -> config.locations.dungeons.dungeonChestProfit.profitColor = newValue) - .controller(EnumDropdownControllerBuilder.getFactory(ConfigUtils.FORMATTING_TO_STRING)) + .controller(ConfigUtils.getEnumDropdownControllerFactory(ConfigUtils.FORMATTING_FORMATTER)) .build()) .option(Option.<Formatting>createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.lossColor")) .binding(defaults.locations.dungeons.dungeonChestProfit.lossColor, () -> config.locations.dungeons.dungeonChestProfit.lossColor, newValue -> config.locations.dungeons.dungeonChestProfit.lossColor = newValue) - .controller(EnumDropdownControllerBuilder.getFactory(ConfigUtils.FORMATTING_TO_STRING)) + .controller(ConfigUtils.getEnumDropdownControllerFactory(ConfigUtils.FORMATTING_FORMATTER)) .build()) .option(Option.<Formatting>createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.general.chestValue.incompleteColor")) @@ -231,7 +300,7 @@ public class DungeonsCategory { .binding(defaults.locations.dungeons.dungeonChestProfit.incompleteColor, () -> config.locations.dungeons.dungeonChestProfit.incompleteColor, newValue -> config.locations.dungeons.dungeonChestProfit.incompleteColor = newValue) - .controller(EnumDropdownControllerBuilder.getFactory(ConfigUtils.FORMATTING_TO_STRING)) + .controller(ConfigUtils.getEnumDropdownControllerFactory(ConfigUtils.FORMATTING_FORMATTER)) .build()) .build()) @@ -318,6 +387,14 @@ public class DungeonsCategory { .controller(ConfigUtils::createBooleanController) .build()) .option(Option.<Boolean>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.solveWaterboard")) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.solveWaterboard.@Tooltip"))) + .binding(defaults.locations.dungeons.solveWaterboard, + () -> config.locations.dungeons.solveWaterboard, + newValue -> config.locations.dungeons.solveWaterboard = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.fireFreezeStaffTimer")) .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.fireFreezeStaffTimer.@Tooltip"))) .binding(defaults.locations.dungeons.fireFreezeStaffTimer, @@ -354,6 +431,14 @@ public class DungeonsCategory { newValue -> config.locations.dungeons.lividColor.enableLividColorText = newValue) .controller(ConfigUtils::createBooleanController) .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.lividColor.enableLividColorTitle")) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.lividColor.enableLividColorTitle.@Tooltip"))) + .binding(defaults.locations.dungeons.lividColor.enableLividColorTitle, + () -> config.locations.dungeons.lividColor.enableLividColorTitle, + newValue -> config.locations.dungeons.lividColor.enableLividColorTitle = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) .option(Option.<String>createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.lividColor.lividColorText")) .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.lividColor.lividColorText.@Tooltip"))) 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 8d8c6f46..3b9cbe60 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java @@ -2,7 +2,6 @@ package de.hysky.skyblocker.config.categories; import de.hysky.skyblocker.config.ConfigUtils; import de.hysky.skyblocker.config.SkyblockerConfig; -import de.hysky.skyblocker.config.controllers.EnumDropdownControllerBuilder; import de.hysky.skyblocker.skyblock.shortcut.ShortcutsConfigScreen; import de.hysky.skyblocker.utils.render.title.TitleContainerConfigScreen; import de.hysky.skyblocker.utils.waypoint.Waypoint; @@ -22,6 +21,13 @@ public class GeneralCategory { //Ungrouped Options .option(Option.<Boolean>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.general.enableTips")) + .binding(defaults.general.enableTips, + () -> config.general.enableTips, + newValue -> config.general.enableTips = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.general.acceptReparty")) .binding(defaults.general.acceptReparty, () -> config.general.acceptReparty, @@ -56,6 +62,15 @@ public class GeneralCategory { newValue -> config.general.hideStatusEffectOverlay = newValue) .controller(ConfigUtils::createBooleanController) .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.general.dontStripSkinAlphaValues")) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.dontStripSkinAlphaValues.@Tooltip"))) + .binding(defaults.general.dontStripSkinAlphaValues, + () -> config.general.dontStripSkinAlphaValues, + newValue -> config.general.dontStripSkinAlphaValues = newValue) + .controller(ConfigUtils::createBooleanController) + .flag(OptionFlag.ASSET_RELOAD) + .build()) //Tab Hud .group(OptionGroup.createBuilder() @@ -474,7 +489,7 @@ public class GeneralCategory { .binding(defaults.general.chestValue.color, () -> config.general.chestValue.color, newValue -> config.general.chestValue.color = newValue) - .controller(EnumDropdownControllerBuilder.getFactory(ConfigUtils.FORMATTING_TO_STRING)) + .controller(ConfigUtils.getEnumDropdownControllerFactory(ConfigUtils.FORMATTING_FORMATTER)) .build()) .option(Option.<Formatting>createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.general.chestValue.incompleteColor")) @@ -482,7 +497,7 @@ public class GeneralCategory { .binding(defaults.general.chestValue.incompleteColor, () -> config.general.chestValue.incompleteColor, newValue -> config.general.chestValue.incompleteColor = newValue) - .controller(EnumDropdownControllerBuilder.getFactory(ConfigUtils.FORMATTING_TO_STRING)) + .controller(ConfigUtils.getEnumDropdownControllerFactory(ConfigUtils.FORMATTING_FORMATTER)) .build()) .build()) diff --git a/src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java index c63b933d..37f24d8c 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java @@ -87,6 +87,14 @@ public class MessageFilterCategory { newValue -> config.messages.hideShowOff = newValue) .controller(ConfigUtils::createEnumCyclingListController) .build()) + .option(Option.<ChatFilterResult>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.messages.hideToggleSkyMall")) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.messages.hideToggleSkyMall.@Tooltip"))) + .binding(defaults.messages.hideToggleSkyMall, + () -> config.messages.hideToggleSkyMall, + newValue -> config.messages.hideToggleSkyMall = newValue) + .controller(ConfigUtils::createEnumCyclingListController) + .build()) .option(Option.<Boolean>createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.messages.hideMana")) .binding(defaults.messages.hideMana, diff --git a/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownController.java b/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownController.java deleted file mode 100644 index 0b9a809d..00000000 --- a/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownController.java +++ /dev/null @@ -1,93 +0,0 @@ -package de.hysky.skyblocker.config.controllers; - -import dev.isxander.yacl3.api.Option; -import dev.isxander.yacl3.api.utils.Dimension; -import dev.isxander.yacl3.gui.AbstractWidget; -import dev.isxander.yacl3.gui.YACLScreen; -import dev.isxander.yacl3.gui.controllers.dropdown.AbstractDropdownController; -import org.jetbrains.annotations.NotNull; - -import java.util.Arrays; -import java.util.function.Function; -import java.util.stream.Stream; - -public class EnumDropdownController<E extends Enum<E>> extends AbstractDropdownController<E> { - /** - * The function used to convert enum constants to strings used for display, suggestion, and validation. Defaults to {@link Enum#toString}. - */ - protected final Function<E, String> toString; - - protected EnumDropdownController(Option<E> option, Function<E, String> toString) { - super(option); - this.toString = toString; - } - - @Override - public String getString() { - return toString.apply(option().pendingValue()); - } - - @Override - public void setFromString(String value) { - option().requestSet(getEnumFromString(value)); - } - - /** - * Searches through enum constants for one whose {@link #toString} result equals {@code value} - * - * @return The enum constant associated with the {@code value} or the pending value if none are found - * @implNote The return value of {@link #toString} on each enum constant should be unique in order to ensure accuracy - */ - private E getEnumFromString(String value) { - value = value.toLowerCase(); - for (E constant : option().pendingValue().getDeclaringClass().getEnumConstants()) { - if (toString.apply(constant).toLowerCase().equals(value)) return constant; - } - - return option().pendingValue(); - } - - @Override - public boolean isValueValid(String value) { - value = value.toLowerCase(); - for (E constant : option().pendingValue().getDeclaringClass().getEnumConstants()) { - if (toString.apply(constant).equals(value)) return true; - } - - return false; - } - - @Override - protected String getValidValue(String value, int offset) { - return getValidEnumConstants(value) - .skip(offset) - .findFirst() - .orElseGet(this::getString); - } - - /** - * Filters and sorts through enum constants for those whose {@link #toString} result equals {@code value} - * - * @return a sorted stream containing enum constants associated with the {@code value} - * @implNote The return value of {@link #toString} on each enum constant should be unique in order to ensure accuracy - */ - @NotNull - protected Stream<String> getValidEnumConstants(String value) { - String valueLowerCase = value.toLowerCase(); - return Arrays.stream(option().pendingValue().getDeclaringClass().getEnumConstants()) - .map(this.toString) - .filter(constant -> constant.toLowerCase().contains(valueLowerCase)) - .sorted((s1, s2) -> { - String s1LowerCase = s1.toLowerCase(); - String s2LowerCase = s2.toLowerCase(); - if (s1LowerCase.startsWith(valueLowerCase) && !s2LowerCase.startsWith(valueLowerCase)) return -1; - if (!s1LowerCase.startsWith(valueLowerCase) && s2LowerCase.startsWith(valueLowerCase)) return 1; - return s1.compareTo(s2); - }); - } - - @Override - public AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { - return new EnumDropdownControllerElement<>(this, screen, widgetDimension); - } -} diff --git a/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerBuilder.java b/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerBuilder.java deleted file mode 100644 index d451a88c..00000000 --- a/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerBuilder.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.hysky.skyblocker.config.controllers; - -import dev.isxander.yacl3.api.Option; -import dev.isxander.yacl3.api.controller.ControllerBuilder; - -import java.util.function.Function; - -public interface EnumDropdownControllerBuilder<E extends Enum<E>> extends ControllerBuilder<E> { - EnumDropdownControllerBuilder<E> toString(Function<E, String> toString); - - static <E extends Enum<E>> EnumDropdownControllerBuilder<E> create(Option<E> option) { - return new EnumDropdownControllerBuilderImpl<>(option); - } - - /** - * Creates a factory for {@link EnumDropdownControllerBuilder}s with the given function for converting enum constants to strings. - * Use this if a custom toString function for an enum is needed. - * Use it like this: - * <pre>{@code Option.<MyEnum>createBuilder().controller(createEnumDropdownControllerBuilder.getFactory(MY_CUSTOM_ENUM_TO_STRING_FUNCTION))}</pre> - * @param toString The function used to convert enum constants to strings used for display, suggestion, and validation - * @return a factory for {@link EnumDropdownControllerBuilder}s - * @param <E> the enum type - */ - static <E extends Enum<E>> Function<Option<E>, ControllerBuilder<E>> getFactory(Function<E, String> toString) { - return opt -> EnumDropdownControllerBuilder.create(opt).toString(toString); - } -} diff --git a/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerBuilderImpl.java b/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerBuilderImpl.java deleted file mode 100644 index 8f6dbb2a..00000000 --- a/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerBuilderImpl.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.hysky.skyblocker.config.controllers; - -import dev.isxander.yacl3.api.Controller; -import dev.isxander.yacl3.api.Option; -import dev.isxander.yacl3.impl.controller.AbstractControllerBuilderImpl; - -import java.util.function.Function; - -public class EnumDropdownControllerBuilderImpl<E extends Enum<E>> extends AbstractControllerBuilderImpl<E> implements EnumDropdownControllerBuilder<E> { - private Function<E, String> toString = Enum::toString; - - public EnumDropdownControllerBuilderImpl(Option<E> option) { - super(option); - } - - @Override - public EnumDropdownControllerBuilder<E> toString(Function<E, String> toString) { - this.toString = toString; - return this; - } - - @SuppressWarnings("UnstableApiUsage") - @Override - public Controller<E> build() { - return new EnumDropdownController<>(option, toString); - } -} diff --git a/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerElement.java b/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerElement.java deleted file mode 100644 index 2a8de609..00000000 --- a/src/main/java/de/hysky/skyblocker/config/controllers/EnumDropdownControllerElement.java +++ /dev/null @@ -1,26 +0,0 @@ -package de.hysky.skyblocker.config.controllers; - -import dev.isxander.yacl3.api.utils.Dimension; -import dev.isxander.yacl3.gui.YACLScreen; -import dev.isxander.yacl3.gui.controllers.dropdown.AbstractDropdownControllerElement; - -import java.util.List; - -public class EnumDropdownControllerElement<E extends Enum<E>> extends AbstractDropdownControllerElement<E, String> { - private final EnumDropdownController<E> controller; - - public EnumDropdownControllerElement(EnumDropdownController<E> control, YACLScreen screen, Dimension<Integer> dim) { - super(control, screen, dim); - this.controller = control; - } - - @Override - public List<String> computeMatchingValues() { - return controller.getValidEnumConstants(inputField).toList(); - } - - @Override - public String getString(String object) { - return object; - } -} diff --git a/src/main/java/de/hysky/skyblocker/debug/Debug.java b/src/main/java/de/hysky/skyblocker/debug/Debug.java index 1fc22d2a..86adcac6 100644 --- a/src/main/java/de/hysky/skyblocker/debug/Debug.java +++ b/src/main/java/de/hysky/skyblocker/debug/Debug.java @@ -1,14 +1,37 @@ package de.hysky.skyblocker.debug; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.ItemUtils; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.text.Text; + +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; public class Debug { private static final boolean DEBUG_ENABLED = Boolean.parseBoolean(System.getProperty("skyblocker.debug", "false")); + public static boolean debugEnabled() { + return DEBUG_ENABLED || FabricLoader.getInstance().isDevelopmentEnvironment(); + } + public static void init() { - if (DEBUG_ENABLED || FabricLoader.getInstance().isDevelopmentEnvironment()) { - ClientCommandRegistrationCallback.EVENT.register(DumpPlayersCommand::register); + if (debugEnabled()) { + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("debug") + .then(dumpPlayersCommand()) + .then(ItemUtils.dumpHeldItemNbtCommand()) + ))); } } + + private static LiteralArgumentBuilder<FabricClientCommandSource> dumpPlayersCommand() { + return literal("dumpPlayers") + .executes(context -> { + context.getSource().getWorld().getPlayers().forEach(player -> context.getSource().sendFeedback(Text.of("'" + player.getName().getString() + "'"))); + return Command.SINGLE_SUCCESS; + }); + } } diff --git a/src/main/java/de/hysky/skyblocker/debug/DumpPlayersCommand.java b/src/main/java/de/hysky/skyblocker/debug/DumpPlayersCommand.java deleted file mode 100644 index 5f6e0362..00000000 --- a/src/main/java/de/hysky/skyblocker/debug/DumpPlayersCommand.java +++ /dev/null @@ -1,31 +0,0 @@ -package de.hysky.skyblocker.debug; - -import com.mojang.brigadier.Command; -import com.mojang.brigadier.CommandDispatcher; -import de.hysky.skyblocker.SkyblockerMod; -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.minecraft.command.CommandRegistryAccess; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.text.Text; - -import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; - -public class DumpPlayersCommand { - - static void register(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) { - dispatcher.register(literal(SkyblockerMod.NAMESPACE) - .then(literal("debug") - .then(literal("dumpPlayers") - .executes(context -> { - FabricClientCommandSource source = context.getSource(); - - source.getWorld().getEntities().forEach(e -> { - if (e instanceof PlayerEntity player) { - source.sendFeedback(Text.of("'" + player.getName().getString() + "'")); - } - }); - - return Command.SINGLE_SUCCESS; - })))); - } -} diff --git a/src/main/java/de/hysky/skyblocker/events/ClientPlayerBlockBreakEvent.java b/src/main/java/de/hysky/skyblocker/events/ClientPlayerBlockBreakEvent.java deleted file mode 100644 index 83ac716f..00000000 --- a/src/main/java/de/hysky/skyblocker/events/ClientPlayerBlockBreakEvent.java +++ /dev/null @@ -1,23 +0,0 @@ -package de.hysky.skyblocker.events; - -import net.fabricmc.fabric.api.event.Event; -import net.fabricmc.fabric.api.event.EventFactory; -import net.minecraft.block.BlockState; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; - -// Fabric API currently doesn't have an event for this -public class ClientPlayerBlockBreakEvent { - public static final Event<AfterBlockBreak> AFTER = EventFactory.createArrayBacked(AfterBlockBreak.class, - (listeners) -> (world, player, pos, state) -> { - for (AfterBlockBreak listener : listeners) { - listener.afterBlockBreak(world, player, pos, state); - } - }); - - @FunctionalInterface - public interface AfterBlockBreak { - void afterBlockBreak(World world, PlayerEntity player, BlockPos pos, BlockState state); - } -} diff --git a/src/main/java/de/hysky/skyblocker/events/DungeonEvents.java b/src/main/java/de/hysky/skyblocker/events/DungeonEvents.java new file mode 100644 index 00000000..9efa3607 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/events/DungeonEvents.java @@ -0,0 +1,30 @@ +package de.hysky.skyblocker.events; + +import de.hysky.skyblocker.skyblock.dungeon.secrets.Room; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; + +public class DungeonEvents { + public static final Event<RoomMatched> PUZZLE_MATCHED = EventFactory.createArrayBacked(RoomMatched.class, callbacks -> room -> { + for (RoomMatched callback : callbacks) { + callback.onRoomMatched(room); + } + }); + + public static final Event<RoomMatched> ROOM_MATCHED = EventFactory.createArrayBacked(RoomMatched.class, callbacks -> room -> { + for (RoomMatched callback : callbacks) { + callback.onRoomMatched(room); + } + if (room.getType() == Room.Type.PUZZLE) { + PUZZLE_MATCHED.invoker().onRoomMatched(room); + } + }); + + @Environment(EnvType.CLIENT) + @FunctionalInterface + public interface RoomMatched { + void onRoomMatched(Room room); + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixin/BatEntityMixin.java b/src/main/java/de/hysky/skyblocker/mixin/BatEntityMixin.java index dc2fa673..fa97e546 100644 --- a/src/main/java/de/hysky/skyblocker/mixin/BatEntityMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixin/BatEntityMixin.java @@ -1,6 +1,6 @@ package de.hysky.skyblocker.mixin; -import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonSecrets; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import net.minecraft.entity.EntityType; import net.minecraft.entity.mob.AmbientEntity; import net.minecraft.entity.passive.BatEntity; @@ -16,6 +16,6 @@ public abstract class BatEntityMixin extends AmbientEntity { @Override public void onRemoved() { super.onRemoved(); - DungeonSecrets.onBatRemoved(this); + DungeonManager.onBatRemoved(this); } } diff --git a/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java index f177d2f8..4015dfa5 100644 --- a/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java @@ -3,32 +3,35 @@ package de.hysky.skyblocker.mixin; import com.llamalad7.mixinextras.injector.WrapWithCondition; import com.llamalad7.mixinextras.sugar.Local; import de.hysky.skyblocker.skyblock.FishingHelper; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import de.hysky.skyblocker.skyblock.waypoint.MythologicalRitual; -import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonSecrets; import de.hysky.skyblocker.utils.Utils; -import dev.cbyrne.betterinject.annotations.Inject; import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.ClientPlayNetworkHandler; import net.minecraft.entity.ItemEntity; import net.minecraft.entity.LivingEntity; import net.minecraft.network.packet.s2c.play.ParticleS2CPacket; import net.minecraft.network.packet.s2c.play.PlaySoundS2CPacket; +import net.minecraft.util.Identifier; + import org.slf4j.Logger; 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.ModifyVariable; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(ClientPlayNetworkHandler.class) public abstract class ClientPlayNetworkHandlerMixin { @Inject(method = "onPlaySound", at = @At("RETURN")) - private void skyblocker$onPlaySound(PlaySoundS2CPacket packet) { + private void skyblocker$onPlaySound(PlaySoundS2CPacket packet, CallbackInfo ci) { FishingHelper.onSound(packet); } @ModifyVariable(method = "onItemPickupAnimation", at = @At(value = "STORE", ordinal = 0)) private ItemEntity skyblocker$onItemPickup(ItemEntity itemEntity, @Local LivingEntity collector) { - DungeonSecrets.onItemPickup(itemEntity, collector, collector == MinecraftClient.getInstance().player); + DungeonManager.onItemPickup(itemEntity, collector, collector == MinecraftClient.getInstance().player); return itemEntity; } @@ -46,9 +49,19 @@ public abstract class ClientPlayNetworkHandlerMixin { private boolean skyblocker$cancelTeamWarning(Logger instance, String format, Object... arg) { return !Utils.isOnHypixel(); } + + @WrapWithCondition(method = { "onScoreboardScoreUpdate", "onScoreboardScoreReset" }, at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;)V", remap = false)) + private boolean skyblocker$cancelUnknownScoreboardObjectiveWarnings(Logger instance, String message, Object objectiveName) { + return !Utils.isOnHypixel(); + } + + @WrapWithCondition(method = "warnOnUnknownPayload", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;)V", remap = false)) + private boolean skyblocker$dropBadlionPacketWarnings(Logger instance, String message, Object identifier) { + return !(Utils.isOnHypixel() && ((Identifier) identifier).getNamespace().equals("badlion")); + } @Inject(method = "onParticle", at = @At("RETURN")) - private void skyblocker$onParticle(ParticleS2CPacket packet) { + private void skyblocker$onParticle(ParticleS2CPacket packet, CallbackInfo ci) { MythologicalRitual.onParticle(packet); } } diff --git a/src/main/java/de/hysky/skyblocker/mixin/ClientPlayerEntityMixin.java b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayerEntityMixin.java index 6813d654..4abadcea 100644 --- a/src/main/java/de/hysky/skyblocker/mixin/ClientPlayerEntityMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayerEntityMixin.java @@ -2,7 +2,6 @@ package de.hysky.skyblocker.mixin; import com.mojang.authlib.GameProfile; -import dev.cbyrne.betterinject.annotations.Inject; import de.hysky.skyblocker.skyblock.item.HotbarSlotLock; import de.hysky.skyblocker.skyblock.item.ItemProtection; import de.hysky.skyblocker.skyblock.rift.HealingMelonIndicator; @@ -12,6 +11,8 @@ import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.client.world.ClientWorld; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(ClientPlayerEntity.class) @@ -29,7 +30,7 @@ public abstract class ClientPlayerEntityMixin extends AbstractClientPlayerEntity } @Inject(method = "updateHealth", at = @At("RETURN")) - public void skyblocker$updateHealth() { + public void skyblocker$updateHealth(CallbackInfo ci) { HealingMelonIndicator.updateHealth(); } }
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/mixin/ClientPlayerInteractionManagerMixin.java b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayerInteractionManagerMixin.java deleted file mode 100644 index fab9a1ea..00000000 --- a/src/main/java/de/hysky/skyblocker/mixin/ClientPlayerInteractionManagerMixin.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.hysky.skyblocker.mixin; - -import de.hysky.skyblocker.events.ClientPlayerBlockBreakEvent; -import net.minecraft.block.BlockState; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerInteractionManager; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import org.spongepowered.asm.mixin.injection.callback.LocalCapture; - -@Mixin(ClientPlayerInteractionManager.class) -public class ClientPlayerInteractionManagerMixin { - @Shadow - @Final - private MinecraftClient client; - - @Inject(method = "breakBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/Block;onBroken(Lnet/minecraft/world/WorldAccess;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;)V"), locals = LocalCapture.CAPTURE_FAILHARD) - private void skyblocker$onBlockBroken(BlockPos pos, CallbackInfoReturnable<Boolean> cir, World world, BlockState blockState) { - ClientPlayerBlockBreakEvent.AFTER.invoker().afterBlockBreak(world, this.client.player, pos, blockState); - } -} diff --git a/src/main/java/de/hysky/skyblocker/mixin/DataTrackerMixin.java b/src/main/java/de/hysky/skyblocker/mixin/DataTrackerMixin.java new file mode 100644 index 00000000..03786876 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixin/DataTrackerMixin.java @@ -0,0 +1,19 @@ +package de.hysky.skyblocker.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.entity.data.DataTracker; + +@Mixin(DataTracker.class) +public class DataTrackerMixin { + + @Inject(method = "copyToFrom", at = @At(value = "NEW", target = "Ljava/lang/IllegalStateException;", shift = At.Shift.BEFORE), cancellable = true) + public void skyblocker$ignoreInvalidDataExceptions(CallbackInfo ci) { + //These exceptions cause annoying small lag spikes for some reason + if (Utils.isOnHypixel()) ci.cancel(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixin/DrawContextMixin.java b/src/main/java/de/hysky/skyblocker/mixin/DrawContextMixin.java index 10666874..b33a8b23 100644 --- a/src/main/java/de/hysky/skyblocker/mixin/DrawContextMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixin/DrawContextMixin.java @@ -8,8 +8,6 @@ import de.hysky.skyblocker.skyblock.item.AttributeShards; import de.hysky.skyblocker.skyblock.item.ItemCooldowns; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; -import dev.cbyrne.betterinject.annotations.Arg; -import dev.cbyrne.betterinject.annotations.Inject; import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.util.math.MatrixStack; @@ -21,6 +19,8 @@ import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(DrawContext.class) public abstract class DrawContextMixin { @@ -32,7 +32,7 @@ public abstract class DrawContextMixin { public abstract int drawText(TextRenderer textRenderer, @Nullable String text, int x, int y, int color, boolean shadow); @Inject(method = "drawItemInSlot(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V", at = @At("HEAD")) - private void skyblocker$renderAttributeShardDisplay(@Arg TextRenderer textRenderer, @Arg ItemStack stack, @Arg(ordinal = 0) int x, @Arg(ordinal = 1) int y, @Local(argsOnly = true) LocalRef<String> countOverride) { + private void skyblocker$renderAttributeShardDisplay(CallbackInfo ci, @Local(argsOnly = true) TextRenderer textRenderer, @Local(argsOnly = true) ItemStack stack, @Local(argsOnly = true, ordinal = 0) int x, @Local(argsOnly = true, ordinal = 1) int y, @Local(argsOnly = true) LocalRef<String> countOverride) { if (!SkyblockerConfigManager.get().general.itemInfoDisplay.attributeShardInfo) return; if (Utils.isOnSkyblock()) { diff --git a/src/main/java/de/hysky/skyblocker/mixin/EntityRenderDispatcherMixin.java b/src/main/java/de/hysky/skyblocker/mixin/EntityRenderDispatcherMixin.java new file mode 100644 index 00000000..5cf88588 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixin/EntityRenderDispatcherMixin.java @@ -0,0 +1,18 @@ +package de.hysky.skyblocker.mixin; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import de.hysky.skyblocker.debug.Debug; +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.client.render.entity.EntityRenderDispatcher; +import net.minecraft.entity.Entity; +import net.minecraft.entity.decoration.ArmorStandEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(EntityRenderDispatcher.class) +public class EntityRenderDispatcherMixin { + @ModifyExpressionValue(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;isInvisible()Z", ordinal = 1)) + private <E extends Entity> boolean skyblocker$armorStandHitboxVisible(boolean invisible, E entity) { + return (!(entity instanceof ArmorStandEntity) || !Utils.isOnHypixel() || !Debug.debugEnabled()) && invisible; + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixin/ItemStackMixin.java b/src/main/java/de/hysky/skyblocker/mixin/ItemStackMixin.java index c5b2438a..2a1ed125 100644 --- a/src/main/java/de/hysky/skyblocker/mixin/ItemStackMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixin/ItemStackMixin.java @@ -1,11 +1,9 @@ package de.hysky.skyblocker.mixin; - import com.llamalad7.mixinextras.injector.ModifyReturnValue; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; -import dev.cbyrne.betterinject.annotations.Inject; import it.unimi.dsi.fastutil.ints.IntIntPair; import net.minecraft.item.ItemStack; import net.minecraft.text.Text; @@ -13,6 +11,8 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(ItemStack.class) public abstract class ItemStackMixin { @@ -39,7 +39,7 @@ public abstract class ItemStackMixin { * Updates the durability of this item stack every tick when in the inventory. */ @Inject(method = "inventoryTick", at = @At("TAIL")) - private void skyblocker$updateDamage() { + private void skyblocker$updateDamage(CallbackInfo ci) { if (!skyblocker$shouldProcess()) { return; } diff --git a/src/main/java/de/hysky/skyblocker/mixin/LeverBlockMixin.java b/src/main/java/de/hysky/skyblocker/mixin/LeverBlockMixin.java index 97c0a7c0..a1cfe44b 100644 --- a/src/main/java/de/hysky/skyblocker/mixin/LeverBlockMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixin/LeverBlockMixin.java @@ -8,10 +8,10 @@ import net.minecraft.block.WallMountedBlock; import net.minecraft.util.shape.VoxelShape; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import dev.cbyrne.betterinject.annotations.Arg; -import dev.cbyrne.betterinject.annotations.Inject; +import com.llamalad7.mixinextras.sugar.Local; @Mixin(LeverBlock.class) public abstract class LeverBlockMixin extends WallMountedBlock { @@ -20,7 +20,7 @@ public abstract class LeverBlockMixin extends WallMountedBlock { } @Inject(method = "getOutlineShape", at = @At("HEAD"), cancellable = true) - public void skyblocker$onGetOutlineShape(@Arg BlockState state, CallbackInfoReturnable<VoxelShape> cir) { + public void skyblocker$onGetOutlineShape(CallbackInfoReturnable<VoxelShape> cir, @Local(argsOnly = true) BlockState state) { if (Utils.isOnSkyblock()) { VoxelShape shape = OldLever.getShape(state.get(FACE), state.get(FACING)); if (shape != null) cir.setReturnValue(shape); diff --git a/src/main/java/de/hysky/skyblocker/mixin/LivingEntityRendererMixin.java b/src/main/java/de/hysky/skyblocker/mixin/LivingEntityRendererMixin.java new file mode 100644 index 00000000..cf927f0c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixin/LivingEntityRendererMixin.java @@ -0,0 +1,18 @@ +package de.hysky.skyblocker.mixin; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import de.hysky.skyblocker.debug.Debug; +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.decoration.ArmorStandEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(LivingEntityRenderer.class) +public class LivingEntityRendererMixin { + @ModifyExpressionValue(method = "render(Lnet/minecraft/entity/LivingEntity;FFLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/LivingEntityRenderer;isVisible(Lnet/minecraft/entity/LivingEntity;)Z")) + private <T extends LivingEntity> boolean skyblocker$armorStandVisible(boolean visible, T entity) { + return entity instanceof ArmorStandEntity && Utils.isOnHypixel() && Debug.debugEnabled() || visible; + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixin/MinecraftClientMixin.java b/src/main/java/de/hysky/skyblocker/mixin/MinecraftClientMixin.java index 29889c28..cf8aab78 100644 --- a/src/main/java/de/hysky/skyblocker/mixin/MinecraftClientMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixin/MinecraftClientMixin.java @@ -1,25 +1,57 @@ package de.hysky.skyblocker.mixin; import de.hysky.skyblocker.skyblock.item.HotbarSlotLock; +import de.hysky.skyblocker.utils.JoinWorldPlaceholderScreen; +import de.hysky.skyblocker.utils.ReconfiguringPlaceholderScreen; import de.hysky.skyblocker.utils.Utils; 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.network.ClientPlayNetworkHandler; import net.minecraft.client.network.ClientPlayerEntity; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; -import dev.cbyrne.betterinject.annotations.Inject; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(MinecraftClient.class) public abstract class MinecraftClientMixin { + + @Shadow + @Nullable + public abstract ClientPlayNetworkHandler getNetworkHandler(); + @Shadow @Nullable public ClientPlayerEntity player; @Inject(method = "handleInputEvents", at = @At("HEAD")) - public void skyblocker$handleInputEvents() { + public void skyblocker$handleInputEvents(CallbackInfo ci) { if (Utils.isOnSkyblock()) { HotbarSlotLock.handleInputEvents(player); } } + + //Remove Downloading Terrain Screen and Reconfiguring Screen + @ModifyVariable(at = @At("HEAD"), method = "setScreen", ordinal = 0, argsOnly = true) + public Screen modifySetScreen(Screen screen) { + if (Utils.isOnSkyblock()) { + if (screen instanceof DownloadingTerrainScreen) { + return null; + } else if (screen instanceof ReconfiguringScreen && this.getNetworkHandler() != null) { + return new ReconfiguringPlaceholderScreen(this.getNetworkHandler().getConnection()); + } + } + return screen; + } + + @ModifyArg(method = "joinWorld", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;reset(Lnet/minecraft/client/gui/screen/Screen;)V"), index = 0) + private Screen modifyJoinWorld(Screen screen) { + return Utils.isOnSkyblock() ? new JoinWorldPlaceholderScreen() : screen; + } }
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/mixin/PlayerListHudMixin.java b/src/main/java/de/hysky/skyblocker/mixin/PlayerListHudMixin.java index 7330b1c1..9a9589d1 100644 --- a/src/main/java/de/hysky/skyblocker/mixin/PlayerListHudMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixin/PlayerListHudMixin.java @@ -15,10 +15,10 @@ import net.minecraft.text.Text; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import dev.cbyrne.betterinject.annotations.Arg; -import dev.cbyrne.betterinject.annotations.Inject; +import com.llamalad7.mixinextras.sugar.Local; @Environment(EnvType.CLIENT) @Mixin(PlayerListHud.class) @@ -26,8 +26,8 @@ public class PlayerListHudMixin { @Shadow private Text footer; - @Inject(at = @At("HEAD"), method = "render(Lnet/minecraft/client/gui/DrawContext;ILnet/minecraft/scoreboard/Scoreboard;Lnet/minecraft/scoreboard/ScoreboardObjective;)V", cancellable = true) - public void skyblocker$renderTabHud(@Arg DrawContext context, @Arg int w, CallbackInfo info) { + @Inject(at = @At("HEAD"), method = "render", cancellable = true) + public void skyblocker$renderTabHud(CallbackInfo info, @Local(argsOnly = true) DrawContext context, @Local(argsOnly = true) int w) { if (!Utils.isOnSkyblock() || !SkyblockerConfigManager.get().general.tabHud.tabHudEnabled || TabHud.defaultTgl.isPressed()) { return; } diff --git a/src/main/java/de/hysky/skyblocker/mixin/PlayerSkinTextureMixin.java b/src/main/java/de/hysky/skyblocker/mixin/PlayerSkinTextureMixin.java new file mode 100644 index 00000000..50f478b8 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixin/PlayerSkinTextureMixin.java @@ -0,0 +1,40 @@ +package de.hysky.skyblocker.mixin; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import com.llamalad7.mixinextras.injector.WrapWithCondition; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.item.PlayerHeadHashCache; +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.PlayerSkinTexture; + +@Mixin(PlayerSkinTexture.class) +public class PlayerSkinTextureMixin { + @Shadow + @Final + private String url; + + @Unique + private boolean isSkyblockSkinTexture; + + @Inject(method = "remapTexture", at = @At("HEAD")) + private void skyblocker$determineSkinSource(CallbackInfoReturnable<NativeImage> cir) { + if (Utils.isOnSkyblock()) { + int skinHash = PlayerHeadHashCache.getSkinHash(this.url).hashCode(); + this.isSkyblockSkinTexture = PlayerHeadHashCache.contains(skinHash); + } + } + + @WrapWithCondition(method = "remapTexture", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/texture/PlayerSkinTexture;stripAlpha(Lnet/minecraft/client/texture/NativeImage;IIII)V")) + private boolean skyblocker$dontStripAlphaValues(NativeImage image, int x1, int y1, int x2, int y2) { + return !(SkyblockerConfigManager.get().general.dontStripSkinAlphaValues && this.isSkyblockSkinTexture); + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixin/accessor/ItemStackAccessor.java b/src/main/java/de/hysky/skyblocker/mixin/accessor/ItemStackAccessor.java new file mode 100644 index 00000000..e3bd69aa --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixin/accessor/ItemStackAccessor.java @@ -0,0 +1,14 @@ +package de.hysky.skyblocker.mixin.accessor; + +import net.minecraft.item.ItemStack; +import net.minecraft.text.Style; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ItemStack.class) +public interface ItemStackAccessor { + @Accessor + static Style getLORE_STYLE() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java b/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java index 956f1008..5cdca216 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java @@ -20,7 +20,6 @@ import net.minecraft.item.ItemStack; import net.minecraft.screen.GenericContainerScreenHandler; import net.minecraft.screen.slot.Slot; import net.minecraft.text.Text; -import net.minecraft.util.Formatting; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -210,21 +209,13 @@ public class ChestValue { return stack.getTooltip(client.player, TooltipContext.BASIC).stream().map(Text::getString).filter(line -> line.contains(searchString)).findAny().orElse(null); } - private static Text getProfitText(long profit, boolean hasIncompleteData) { + static Text getProfitText(long profit, boolean hasIncompleteData) { SkyblockerConfig.DungeonChestProfit config = SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit; - return getProfitText(profit, hasIncompleteData, config.neutralThreshold, config.neutralColor, config.profitColor, config.lossColor, config.incompleteColor); + return Text.literal((profit > 0 ? " +" : ' ') + FORMATTER.format(profit) + " Coins").formatted(hasIncompleteData ? config.incompleteColor : (Math.abs(profit) < config.neutralThreshold) ? config.neutralColor : (profit > 0) ? config.profitColor : config.lossColor); } - static Text getProfitText(long profit, boolean hasIncompleteData, int neutralThreshold, Formatting neutralColor, Formatting profitColor, Formatting lossColor, Formatting incompleteColor) { - return Text.literal((profit > 0 ? " +" : ' ') + FORMATTER.format(profit) + " Coins").formatted(hasIncompleteData ? incompleteColor : (Math.abs(profit) < neutralThreshold) ? neutralColor : (profit > 0) ? profitColor : lossColor); - } - - private static Text getValueText(long value, boolean hasIncompleteData) { + static Text getValueText(long value, boolean hasIncompleteData) { SkyblockerConfig.ChestValue config = SkyblockerConfigManager.get().general.chestValue; - return getValueText(value, hasIncompleteData, config.color, config.incompleteColor); - } - - static Text getValueText(long value, boolean hasIncompleteData, Formatting color, Formatting incompleteColor) { - return Text.literal(' ' + FORMATTER.format(value) + " Coins").formatted(hasIncompleteData ? incompleteColor : color); + return Text.literal(' ' + FORMATTER.format(value) + " Coins").formatted(hasIncompleteData ? config.incompleteColor : config.color); } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/Tips.java b/src/main/java/de/hysky/skyblocker/skyblock/Tips.java new file mode 100644 index 00000000..94a5fe14 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/Tips.java @@ -0,0 +1,106 @@ +package de.hysky.skyblocker.skyblock; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.events.SkyblockEvents; +import de.hysky.skyblocker.utils.Constants; +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.command.CommandRegistryAccess; +import net.minecraft.text.ClickEvent; +import net.minecraft.text.Text; + +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 final List<Supplier<Text>> TIPS = 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"), + getTipFactory("skyblocker.tips.fancyTabExtraInfo"), + getTipFactory("skyblocker.tips.helpCommand", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker help"), + getTipFactory("skyblocker.tips.discordRichPresence", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), + getTipFactory("skyblocker.tips.customDungeonSecretWaypoints", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker dungeons secrets addWaypoint"), + getTipFactory("skyblocker.tips.shortcuts", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker shortcuts"), + 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.beta", ClickEvent.Action.OPEN_URL, "https://github.com/SkyblockerMod/Skyblocker/actions"), + 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.fairySoulsEnigmaSoulsRelics", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker fairySouls"), + getTipFactory("skyblocker.tips.quickNav", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config") + ); + + private static boolean sentTip = false; + + private static Supplier<Text> getTipFactory(String key) { + return () -> Text.translatable(key); + } + + private static Supplier<Text> getTipFactory(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); + } + + 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)) + )); + } + + 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(); + context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.tips.enabled")).append(" ").append(Text.translatable("skyblocker.tips.clickDisable").styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker tips disable"))))); + return Command.SINGLE_SUCCESS; + } + + private static int disableTips(CommandContext<FabricClientCommandSource> context) { + SkyblockerConfigManager.get().general.enableTips = false; + SkyblockerConfigManager.save(); + context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.tips.disabled")).append(" ").append(Text.translatable("skyblocker.tips.clickEnable").styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker tips enable"))))); + return Command.SINGLE_SUCCESS; + } + + private static int nextTip(CommandContext<FabricClientCommandSource> context) { + context.getSource().sendFeedback(nextTip()); + return Command.SINGLE_SUCCESS; + } + + private static Text nextTip() { + return Constants.PREFIX.get().append(Text.translatable("skyblocker.tips.tip", nextTipInternal())) + .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")))); + } + + private static Text nextTipInternal() { + return TIPS.get(RANDOM.nextInt(TIPS.size())).get(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java new file mode 100644 index 00000000..d67d6988 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java @@ -0,0 +1,76 @@ +package de.hysky.skyblocker.skyblock.dungeon; + +import de.hysky.skyblocker.config.SkyblockerConfig; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; +import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.MessageScheduler; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.sound.SoundEvents; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DungeonScore { + private static final SkyblockerConfig.DungeonScore CONFIG = SkyblockerConfigManager.get().locations.dungeons.dungeonScore; + private static final Pattern DUNGEON_CLEARED_PATTERN = Pattern.compile("Cleared: (?<cleared>\\d+)% \\((?<score>\\d+)\\)"); + private static boolean sent270; + private static boolean sent300; + + public static void init() { + Scheduler.INSTANCE.scheduleCyclic(DungeonScore::tick, 20); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); + } + + public static void tick() { + MinecraftClient client = MinecraftClient.getInstance(); + if (!Utils.isInDungeons() || client.player == null) { + reset(); + return; + } + + for (String sidebarLine : Utils.STRING_SCOREBOARD) { + Matcher dungeonClearedMatcher = DUNGEON_CLEARED_PATTERN.matcher(sidebarLine); + if (!dungeonClearedMatcher.matches()) { + continue; + } + int score = Integer.parseInt(dungeonClearedMatcher.group("score")); + if (!DungeonManager.isInBoss()) score += 28; + if (!sent270 && score >= 270 && score < 300) { + if (CONFIG.enableDungeonScore270Message) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown(Constants.PREFIX.get().getString() + CONFIG.dungeonScore270Message.replaceAll("\\[score]", "270")); + } + if (CONFIG.enableDungeonScore270Title) { + client.inGameHud.setDefaultTitleFade(); + client.inGameHud.setTitle(Constants.PREFIX.get().append(CONFIG.dungeonScore270Message.replaceAll("\\[score]", "270"))); + } + if (CONFIG.enableDungeonScore270Sound) { + client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 100f, 0.1f); + } + sent270 = true; + } + if (!sent300 && score >= 300) { + if (CONFIG.enableDungeonScore300Message) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown(Constants.PREFIX.get().getString() + CONFIG.dungeonScore300Message.replaceAll("\\[score]", "300")); + } + if (CONFIG.enableDungeonScore300Title) { + client.inGameHud.setDefaultTitleFade(); + client.inGameHud.setTitle(Constants.PREFIX.get().append(CONFIG.dungeonScore300Message.replaceAll("\\[score]", "300"))); + } + if (CONFIG.enableDungeonScore300Sound) { + client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 100f, 0.1f); + } + sent300 = true; + } + break; + } + } + + private static void reset() { + sent270 = false; + sent300 = false; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/LividColor.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/LividColor.java index f40b7859..b1ed5a22 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/LividColor.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/LividColor.java @@ -2,6 +2,7 @@ package de.hysky.skyblocker.skyblock.dungeon; import de.hysky.skyblocker.config.SkyblockerConfig; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.scheduler.MessageScheduler; import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; @@ -9,6 +10,8 @@ import net.minecraft.block.Block; import net.minecraft.block.Blocks; import net.minecraft.client.MinecraftClient; import net.minecraft.registry.Registries; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; import net.minecraft.util.Formatting; import net.minecraft.util.math.BlockPos; @@ -37,13 +40,14 @@ public class LividColor { "Doctor Livid", Formatting.GRAY, "Vendetta Livid", Formatting.WHITE ); + public static final SkyblockerConfig.LividColor CONFIG = SkyblockerConfigManager.get().locations.dungeons.lividColor; private static int tenTicks = 0; private static Formatting color; public static void init() { ClientReceiveMessageEvents.GAME.register((message, overlay) -> { SkyblockerConfig.LividColor config = SkyblockerConfigManager.get().locations.dungeons.lividColor; - if ((config.enableLividColorText || config.enableLividColorGlow) && message.getString().equals("[BOSS] Livid: I respect you for making it to here, but I'll be your undoing.")) { + if ((config.enableLividColorText || config.enableLividColorTitle || config.enableLividColorGlow) && message.getString().equals("[BOSS] Livid: I respect you for making it to here, but I'll be your undoing.")) { tenTicks = 8; } }); @@ -53,14 +57,14 @@ public class LividColor { MinecraftClient client = MinecraftClient.getInstance(); if (tenTicks != 0) { SkyblockerConfig.LividColor config = SkyblockerConfigManager.get().locations.dungeons.lividColor; - if ((config.enableLividColorText || config.enableLividColorGlow) && Utils.isInDungeons() && client.world != null) { + if ((config.enableLividColorText || config.enableLividColorTitle || config.enableLividColorGlow) && Utils.isInDungeons() && client.world != null) { if (tenTicks == 1) { - onLividColorFound(Blocks.RED_WOOL); + onLividColorFound(client, Blocks.RED_WOOL); return; } Block color = client.world.getBlockState(new BlockPos(5, 110, 42)).getBlock(); if (WOOL_TO_FORMATTING.containsKey(color) && !color.equals(Blocks.RED_WOOL)) { - onLividColorFound(color); + onLividColorFound(client, color); return; } tenTicks--; @@ -70,11 +74,21 @@ public class LividColor { } } - private static void onLividColorFound(Block color) { + private static void onLividColorFound(MinecraftClient client, Block color) { LividColor.color = WOOL_TO_FORMATTING.get(color); - if (SkyblockerConfigManager.get().locations.dungeons.lividColor.enableLividColorText) { - String colorString = Registries.BLOCK.getId(color).getPath(); - MessageScheduler.INSTANCE.sendMessageAfterCooldown(SkyblockerConfigManager.get().locations.dungeons.lividColor.lividColorText.replaceAll("\\[color]", colorString.substring(0, colorString.length() - 5))); + String colorString = Registries.BLOCK.getId(color).getPath(); + colorString = colorString.substring(0, colorString.length() - 5).toUpperCase(); + String[] messageParts = CONFIG.lividColorText.split("\\[color]"); + MutableText message = Constants.PREFIX.get().append(messageParts[0]); + for (int i = 1; i < messageParts.length; i++) { + message = message.append(Text.literal(colorString).formatted(LividColor.color)).append(Text.of(messageParts[i])); + } + if (CONFIG.enableLividColorText) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown(message.getString()); + } + if (CONFIG.enableLividColorTitle) { + client.inGameHud.setDefaultTitleFade(); + client.inGameHud.setTitle(message); } tenTicks = 0; } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CreeperBeams.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java index 5c7a01f9..db195003 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CreeperBeams.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java @@ -1,13 +1,10 @@ -package de.hysky.skyblocker.skyblock.dungeon; +package de.hysky.skyblocker.skyblock.dungeon.puzzle; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.render.RenderHelper; -import de.hysky.skyblocker.utils.scheduler.Scheduler; import it.unimi.dsi.fastutil.objects.ObjectDoublePair; -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.minecraft.block.Block; import net.minecraft.block.Blocks; import net.minecraft.client.MinecraftClient; @@ -27,8 +24,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; -public class CreeperBeams { - +public class CreeperBeams extends DungeonPuzzle { private static final Logger LOGGER = LoggerFactory.getLogger(CreeperBeams.class.getName()); private static final float[][] COLORS = { @@ -41,56 +37,55 @@ public class CreeperBeams { private static final int FLOOR_Y = 68; private static final int BASE_Y = 74; + private static final CreeperBeams INSTANCE = new CreeperBeams(); private static ArrayList<Beam> beams = new ArrayList<>(); private static BlockPos base = null; - private static boolean solved = false; + + private CreeperBeams() { + super("creeper", "creeper-room"); + } public static void init() { - Scheduler.INSTANCE.scheduleCyclic(CreeperBeams::update, 20); - WorldRenderEvents.BEFORE_DEBUG_RENDER.register(CreeperBeams::render); - ClientPlayConnectionEvents.JOIN.register(((handler, sender, client) -> reset())); } - private static void reset() { + @Override + public void reset() { + super.reset(); beams.clear(); base = null; - solved = false; } - private static void update() { + @Override + public void tick(MinecraftClient client) { // don't do anything if the room is solved - if (solved) { + if (!shouldSolve()) { return; } - MinecraftClient client = MinecraftClient.getInstance(); - ClientWorld world = client.world; - ClientPlayerEntity player = client.player; - // clear state if not in dungeon - if (world == null || player == null || !Utils.isInDungeons()) { + if (client.world == null || client.player == null || !Utils.isInDungeons()) { return; } // try to find base if not found and solve if (base == null) { - base = findCreeperBase(player, world); + base = findCreeperBase(client.player, client.world); if (base == null) { return; } Vec3d creeperPos = new Vec3d(base.getX() + 0.5, BASE_Y + 1.75, base.getZ() + 0.5); - ArrayList<BlockPos> targets = findTargets(world, base); + ArrayList<BlockPos> targets = findTargets(client.world, base); beams = findLines(creeperPos, targets); } // update the beam states - beams.forEach(b -> b.updateState(world)); + beams.forEach(b -> b.updateState(client.world)); // check if the room is solved - if (!isTarget(world, base)) { - solved = true; + if (!isTarget(client.world, base)) { + reset(); } } @@ -176,10 +171,11 @@ public class CreeperBeams { return result; } - private static void render(WorldRenderContext wrc) { + @Override + public void render(WorldRenderContext wrc) { // don't render if solved or disabled - if (solved || !SkyblockerConfigManager.get().locations.dungeons.creeperSolver) { + if (!shouldSolve() || !SkyblockerConfigManager.get().locations.dungeons.creeperSolver) { return; } @@ -239,11 +235,11 @@ public class CreeperBeams { if (toDo) { RenderHelper.renderOutline(wrc, outlineOne, color, 3, false); RenderHelper.renderOutline(wrc, outlineTwo, color, 3, false); - RenderHelper.renderLinesFromPoints(wrc, line, color, 1, 2); + RenderHelper.renderLinesFromPoints(wrc, line, color, 1, 2, false); } else { RenderHelper.renderOutline(wrc, outlineOne, GREEN_COLOR_COMPONENTS, 1, false); RenderHelper.renderOutline(wrc, outlineTwo, GREEN_COLOR_COMPONENTS, 1, false); - RenderHelper.renderLinesFromPoints(wrc, line, GREEN_COLOR_COMPONENTS, 0.75f, 1); + RenderHelper.renderLinesFromPoints(wrc, line, GREEN_COLOR_COMPONENTS, 0.75f, 1, false); } } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java index f49a2f2e..6b435d3c 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java @@ -1,12 +1,10 @@ -package de.hysky.skyblocker.skyblock.dungeon; +package de.hysky.skyblocker.skyblock.dungeon.puzzle; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.render.RenderHelper; -import de.hysky.skyblocker.utils.scheduler.Scheduler; import it.unimi.dsi.fastutil.objects.ObjectIntPair; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; -import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.client.world.ClientWorld; @@ -24,29 +22,34 @@ import java.util.List; /** * This class provides functionality to render outlines around Blaze entities */ -public class DungeonBlaze { +public class DungeonBlaze extends DungeonPuzzle { private static final Logger LOGGER = LoggerFactory.getLogger(DungeonBlaze.class.getName()); private static final float[] GREEN_COLOR_COMPONENTS = {0.0F, 1.0F, 0.0F}; private static final float[] WHITE_COLOR_COMPONENTS = {1.0f, 1.0f, 1.0f}; + private static final DungeonBlaze INSTANCE = new DungeonBlaze(); private static ArmorStandEntity highestBlaze = null; private static ArmorStandEntity lowestBlaze = null; private static ArmorStandEntity nextHighestBlaze = null; private static ArmorStandEntity nextLowestBlaze = null; + private DungeonBlaze() { + super("blaze", "blaze-room-1-high", "blaze-room-1-low"); + } + public static void init() { - Scheduler.INSTANCE.scheduleCyclic(DungeonBlaze::update, 4); - WorldRenderEvents.BEFORE_DEBUG_RENDER.register(DungeonBlaze::blazeRenderer); } /** * Updates the state of Blaze entities and triggers the rendering process if necessary. */ - public static void update() { - ClientWorld world = MinecraftClient.getInstance().world; - ClientPlayerEntity player = MinecraftClient.getInstance().player; - if (world == null || player == null || !Utils.isInDungeons()) return; - List<ObjectIntPair<ArmorStandEntity>> blazes = getBlazesInWorld(world, player); + @Override + public void tick(MinecraftClient client) { + if (!shouldSolve()) { + return; + } + if (client.world == null || client.player == null || !Utils.isInDungeons()) return; + List<ObjectIntPair<ArmorStandEntity>> blazes = getBlazesInWorld(client.world, client.player); sortBlazes(blazes); updateBlazeEntities(blazes); } @@ -104,7 +107,8 @@ public class DungeonBlaze { * * @param wrc The WorldRenderContext used for rendering. */ - public static void blazeRenderer(WorldRenderContext wrc) { + @Override + public void render(WorldRenderContext wrc) { try { if (highestBlaze != null && lowestBlaze != null && highestBlaze.isAlive() && lowestBlaze.isAlive() && SkyblockerConfigManager.get().locations.dungeons.blazeSolver) { if (highestBlaze.getY() < 69) { @@ -137,7 +141,7 @@ public class DungeonBlaze { Vec3d blazeCenter = blazeBox.getCenter(); Vec3d nextBlazeCenter = nextBlazeBox.getCenter(); - RenderHelper.renderLinesFromPoints(wrc, new Vec3d[]{blazeCenter, nextBlazeCenter}, WHITE_COLOR_COMPONENTS, 1f, 5f); + RenderHelper.renderLinesFromPoints(wrc, new Vec3d[]{blazeCenter, nextBlazeCenter}, WHITE_COLOR_COMPONENTS, 1f, 5f, false); } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java new file mode 100644 index 00000000..f5e0461d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java @@ -0,0 +1,59 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import com.mojang.brigadier.Command; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.events.DungeonEvents; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; +import de.hysky.skyblocker.skyblock.dungeon.secrets.Room; +import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.Tickable; +import de.hysky.skyblocker.utils.render.Renderable; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; + +public abstract class DungeonPuzzle implements Tickable, Renderable { + protected final String puzzleName; + @NotNull + private final Set<String> roomNames; + private boolean shouldSolve; + + public DungeonPuzzle(String puzzleName, String... roomName) { + this(puzzleName, Set.of(roomName)); + } + + public DungeonPuzzle(String puzzleName, @NotNull Set<String> roomNames) { + this.puzzleName = puzzleName; + this.roomNames = roomNames; + DungeonEvents.PUZZLE_MATCHED.register(room -> { + if (roomNames.contains(room.getName())) { + room.addSubProcess(this); + shouldSolve = true; + } + }); + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("puzzle").then(literal(puzzleName).then(literal("solve").executes(context -> { + Room currentRoom = DungeonManager.getCurrentRoom(); + if (currentRoom != null) { + reset(); + currentRoom.addSubProcess(this); + context.getSource().sendFeedback(Constants.PREFIX.get().append("§aSolving " + puzzleName + " puzzle in the current room.")); + } else { + context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null.")); + } + return Command.SINGLE_SUCCESS; + }))))))); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); + } + + public boolean shouldSolve() { + return shouldSolve; + } + + public void reset() { + shouldSolve = false; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdos.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdos.java index e1ab2fa8..c5e55f93 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdos.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdos.java @@ -1,4 +1,4 @@ -package de.hysky.skyblocker.skyblock.dungeon; +package de.hysky.skyblocker.skyblock.dungeon.puzzle; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.utils.chat.ChatFilterResult; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java index 7f249e7d..c8043288 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java @@ -1,16 +1,13 @@ -package de.hysky.skyblocker.skyblock.dungeon; +package de.hysky.skyblocker.skyblock.dungeon.puzzle; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.render.RenderHelper; import de.hysky.skyblocker.utils.tictactoe.TicTacToeUtils; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; -import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; import net.minecraft.block.Block; import net.minecraft.block.Blocks; import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.client.world.ClientWorld; import net.minecraft.entity.decoration.ItemFrameEntity; import net.minecraft.item.FilledMapItem; import net.minecraft.item.map.MapState; @@ -25,27 +22,32 @@ import java.util.List; /** * Thanks to Danker for a reference implementation! */ -public class TicTacToe { +public class TicTacToe extends DungeonPuzzle { private static final Logger LOGGER = LoggerFactory.getLogger(TicTacToe.class); private static final float[] RED_COLOR_COMPONENTS = {1.0F, 0.0F, 0.0F}; + private static final TicTacToe INSTANCE = new TicTacToe(); private static Box nextBestMoveToMake = null; + private TicTacToe() { + super("tic-tac-toe", "tic-tac-toe-1"); + } + public static void init() { - WorldRenderEvents.BEFORE_DEBUG_RENDER.register(TicTacToe::solutionRenderer); } - public static void tick() { - MinecraftClient client = MinecraftClient.getInstance(); - ClientWorld world = client.world; - ClientPlayerEntity player = client.player; + @Override + public void tick(MinecraftClient client) { + if (!shouldSolve()) { + return; + } nextBestMoveToMake = null; - if (world == null || player == null || !Utils.isInDungeons()) return; + if (client.world == null || client.player == null || !Utils.isInDungeons()) return; //Search within 21 blocks for item frames that contain maps - Box searchBox = new Box(player.getX() - 21, player.getY() - 21, player.getZ() - 21, player.getX() + 21, player.getY() + 21, player.getZ() + 21); - List<ItemFrameEntity> itemFramesThatHoldMaps = world.getEntitiesByClass(ItemFrameEntity.class, searchBox, ItemFrameEntity::containsMap); + Box searchBox = new Box(client.player.getX() - 21, client.player.getY() - 21, client.player.getZ() - 21, client.player.getX() + 21, client.player.getY() + 21, client.player.getZ() + 21); + List<ItemFrameEntity> itemFramesThatHoldMaps = client.world.getEntitiesByClass(ItemFrameEntity.class, searchBox, ItemFrameEntity::containsMap); try { //Only attempt to solve if its the player's turn @@ -56,7 +58,7 @@ public class TicTacToe { char facing = 'X'; for (ItemFrameEntity itemFrame : itemFramesThatHoldMaps) { - MapState mapState = world.getMapState(FilledMapItem.getMapName(itemFrame.getMapId().getAsInt())); + MapState mapState = client.world.getMapState(FilledMapItem.getMapName(itemFrame.getMapId().getAsInt())); if (mapState == null) continue; @@ -78,7 +80,7 @@ public class TicTacToe { facing = 'Z'; } - Block block = world.getBlockState(blockPos).getBlock(); + Block block = client.world.getBlockState(blockPos).getBlock(); if (block == Blocks.AIR || block == Blocks.STONE_BUTTON) { leftmostRow = blockPos; column = i; @@ -124,7 +126,8 @@ public class TicTacToe { } } - private static void solutionRenderer(WorldRenderContext context) { + @Override + public void render(WorldRenderContext context) { try { if (SkyblockerConfigManager.get().locations.dungeons.solveTicTacToe && nextBestMoveToMake != null) { RenderHelper.renderOutline(context, nextBestMoveToMake, RED_COLOR_COMPONENTS, 5, false); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Trivia.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Trivia.java index 53368c14..c331bd48 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Trivia.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Trivia.java @@ -1,4 +1,4 @@ -package de.hysky.skyblocker.skyblock.dungeon; +package de.hysky.skyblocker.skyblock.dungeon.puzzle; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.skyblock.waypoint.FairySouls; @@ -10,11 +10,15 @@ import net.minecraft.text.Text; import net.minecraft.util.Formatting; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import com.mojang.logging.LogUtils; import java.util.*; import java.util.regex.Matcher; public class Trivia extends ChatPatternListener { + private static final Logger LOGGER = LogUtils.getLogger(); private static final Map<String, String[]> answers; private List<String> solutions = Collections.emptyList(); @@ -42,14 +46,19 @@ public class Trivia extends ChatPatternListener { } private void updateSolutions(String question) { - String trimmedQuestion = question.trim(); - if (trimmedQuestion.equals("What SkyBlock year is it?")) { - long currentTime = System.currentTimeMillis() / 1000L; - long diff = currentTime - 1560276000; - int year = (int) (diff / 446400 + 1); - solutions = Collections.singletonList("Year " + year); - } else { - solutions = Arrays.asList(answers.get(trimmedQuestion)); + try { + String trimmedQuestion = question.trim(); + if (trimmedQuestion.equals("What SkyBlock year is it?")) { + long currentTime = System.currentTimeMillis() / 1000L; + long diff = currentTime - 1560276000; + int year = (int) (diff / 446400 + 1); + solutions = Collections.singletonList("Year " + year); + } else { + String[] questionAnswers = answers.get(trimmedQuestion); + if (questionAnswers != null) solutions = Arrays.asList(questionAnswers); + } + } catch (Exception e) { //Hopefully the solver doesn't go south + LOGGER.error("[Skyblocker] Failed to update the Trivia puzzle answers!", e); } } @@ -66,7 +75,7 @@ public class Trivia extends ChatPatternListener { answers.put("What is the status of Goldor?", new String[]{"The Wither Lords"}); answers.put("What is the status of Storm?", new String[]{"The Wither Lords"}); answers.put("What is the status of Necron?", new String[]{"The Wither Lords"}); - answers.put("What is the status of Maxor, Storm, Goldor and Necron?", new String[]{"The Wither Lords"}); + answers.put("What is the status of Maxor, Storm, Goldor, and Necron?", new String[]{"The Wither Lords"}); answers.put("Which brother is on the Spider's Den?", new String[]{"Rick"}); answers.put("What is the name of Rick's brother?", new String[]{"Pat"}); answers.put("What is the name of the Painter in the Hub?", new String[]{"Marco"}); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Cell.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Cell.java new file mode 100644 index 00000000..0279fed8 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Cell.java @@ -0,0 +1,51 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle.waterboard; + +public class Cell { + public static final Cell BLOCK = new Cell(Type.BLOCK); + public static final Cell EMPTY = new Cell(Type.EMPTY); + public final Type type; + + private Cell(Type type) { + this.type = type; + } + + public boolean isOpen() { + return type == Type.EMPTY; + } + + public static class SwitchCell extends Cell { + public final int id; + private boolean open; + + public SwitchCell(int id) { + super(Type.SWITCH); + this.id = id; + } + + public static SwitchCell ofOpened(int id) { + SwitchCell switchCell = new SwitchCell(id); + switchCell.open = true; + return switchCell; + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj) || obj instanceof SwitchCell switchCell && id == switchCell.id && open == switchCell.open; + } + + @Override + public boolean isOpen() { + return open; + } + + public void toggle() { + open = !open; + } + } + + public enum Type { + BLOCK, + EMPTY, + SWITCH + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Switch.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Switch.java new file mode 100644 index 00000000..bb8da61d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Switch.java @@ -0,0 +1,39 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle.waterboard; + +import org.jetbrains.annotations.NotNull; + +import java.util.AbstractCollection; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class Switch extends AbstractCollection<Cell.SwitchCell> { + public final int id; + public final List<Cell.SwitchCell> cells = new ArrayList<>(); + + public Switch(int id) { + this.id = id; + } + + @Override + @NotNull + public Iterator<Cell.SwitchCell> iterator() { + return cells.iterator(); + } + + @Override + public int size() { + return cells.size(); + } + + @Override + public boolean add(Cell.SwitchCell cell) { + return cells.add(cell); + } + + public void toggle() { + for (Cell.SwitchCell cell : cells) { + cell.toggle(); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Waterboard.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Waterboard.java new file mode 100644 index 00000000..3244996a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Waterboard.java @@ -0,0 +1,451 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle.waterboard; + +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.debug.Debug; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.DungeonPuzzle; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.waterboard.Cell.SwitchCell; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; +import de.hysky.skyblocker.skyblock.dungeon.secrets.Room; +import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.render.RenderHelper; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import de.hysky.skyblocker.utils.waypoint.Waypoint; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntMaps; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.fabricmc.fabric.api.event.player.UseBlockCallback; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; +import net.minecraft.block.LeverBlock; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.fluid.Fluids; +import net.minecraft.fluid.WaterFluid; +import net.minecraft.util.ActionResult; +import net.minecraft.util.DyeColor; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import org.joml.Vector2i; +import org.joml.Vector2ic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; + +public class Waterboard extends DungeonPuzzle { + private static final Logger LOGGER = LoggerFactory.getLogger(Waterboard.class); + public static final Waterboard INSTANCE = new Waterboard(); + private static final Object2IntMap<Block> SWITCH_BLOCKS = Object2IntMaps.unmodifiable(new Object2IntOpenHashMap<>(Map.of( + Blocks.COAL_BLOCK, 1, + Blocks.GOLD_BLOCK, 2, + Blocks.QUARTZ_BLOCK, 3, + Blocks.DIAMOND_BLOCK, 4, + Blocks.EMERALD_BLOCK, 5, + Blocks.TERRACOTTA, 6 + ))); + private static final BlockPos[] SWITCH_POSITIONS = new BlockPos[]{ + new BlockPos(20, 61, 10), + new BlockPos(20, 61, 15), + new BlockPos(20, 61, 20), + new BlockPos(10, 61, 20), + new BlockPos(10, 61, 15), + new BlockPos(10, 61, 10) + }; + public static final BlockPos WATER_LEVER = new BlockPos(15, 60, 5); + private static final float[] LIME_COLOR_COMPONENTS = DyeColor.LIME.getColorComponents(); + + private CompletableFuture<Void> solve; + private final Cell[][] cells = new Cell[19][19]; + private final Switch[] switches = new Switch[]{new Switch(0), new Switch(1), new Switch(2), new Switch(3), new Switch(4), new Switch(5)}; + private int doors = 0; + private final Result[] results = new Result[64]; + private int currentCombination; + private final IntList bestCombinations = new IntArrayList(); + private final Waypoint[] waypoints = new Waypoint[7]; + /** + * Used to check the water lever state since the block state does not update immediately after the lever is toggled. + */ + private boolean bestCombinationsUpdated; + + private Waterboard() { + super("waterboard", "water-puzzle"); + UseBlockCallback.EVENT.register(this::onUseBlock); + if (Debug.debugEnabled()) { + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("puzzle").then(literal(puzzleName) + .then(literal("printBoard").executes(context -> { + context.getSource().sendFeedback(Constants.PREFIX.get().append(boardToString(cells))); + return Command.SINGLE_SUCCESS; + })).then(literal("printDoors").executes(context -> { + context.getSource().sendFeedback(Constants.PREFIX.get().append(Integer.toBinaryString(INSTANCE.doors))); + return Command.SINGLE_SUCCESS; + })).then(literal("printSimulationResults").then(argument("combination", IntegerArgumentType.integer(0, 63)).executes(context -> { + context.getSource().sendFeedback(Constants.PREFIX.get().append(results[IntegerArgumentType.getInteger(context, "combination")].toString())); + return Command.SINGLE_SUCCESS; + }))).then(literal("printCurrentCombination").executes(context -> { + context.getSource().sendFeedback(Constants.PREFIX.get().append(Integer.toBinaryString(INSTANCE.currentCombination))); + return Command.SINGLE_SUCCESS; + })).then(literal("printBestCombination").executes(context -> { + context.getSource().sendFeedback(Constants.PREFIX.get().append(INSTANCE.bestCombinations.toString())); + return Command.SINGLE_SUCCESS; + })) + ))))); + } + } + + public static void init() { + } + + private static String boardToString(Cell[][] cells) { + StringBuilder sb = new StringBuilder(); + for (Cell[] row : cells) { + sb.append("\n"); + for (Cell cell : row) { + if (cell == null) { + sb.append('?'); + } else if (cell instanceof SwitchCell switchCell) { + sb.append(switchCell.id); + } else switch (cell.type) { + case BLOCK -> sb.append('#'); + case EMPTY -> sb.append('.'); + } + } + } + return sb.toString(); + } + + @Override + public void tick(MinecraftClient client) { + if (!SkyblockerConfigManager.get().locations.dungeons.solveWaterboard || client.world == null || !DungeonManager.isCurrentRoomMatched() || solve != null && !solve.isDone()) { + return; + } + Room room = DungeonManager.getCurrentRoom(); + solve = CompletableFuture.runAsync(() -> { + Changed changed = updateBoard(client.world, room); + if (changed == Changed.NONE) { + return; + } + if (results[0] == null) { + updateSwitches(); + simulateCombinations(); + clearSwitches(); + } + if (bestCombinations.isEmpty() || changed.doorChanged()) { + findBestCombinations(); + bestCombinationsUpdated = true; + } + }).exceptionally(e -> { + LOGGER.error("[Skyblocker Waterboard] Encountered an unknown exception while solving waterboard.", e); + return null; + }); + if (waypoints[0] == null) { + for (int i = 0; i < 6; i++) { + waypoints[i] = new Waypoint(room.relativeToActual(SWITCH_POSITIONS[i]), Waypoint.Type.HIGHLIGHT, LIME_COLOR_COMPONENTS); + } + waypoints[6] = new Waypoint(room.relativeToActual(WATER_LEVER), Waypoint.Type.HIGHLIGHT, LIME_COLOR_COMPONENTS); + waypoints[6].setFound(); + } + } + + private Changed updateBoard(World world, Room room) { + // Parse the waterboard. + BlockPos.Mutable pos = new BlockPos.Mutable(24, 78, 26); + Changed changed = Changed.NONE; + for (int row = 0; row < cells.length; pos.move(cells[row].length, -1, 0), row++) { + for (int col = 0; col < cells[row].length; pos.move(Direction.WEST), col++) { + Cell cell = parseBlock(world, room, pos); + if (!cell.equals(cells[row][col])) { + cells[row][col] = cell; + changed = changed.onCellChanged(); + } + } + } + + // Parse door states. + pos.set(15, 57, 15); + int prevDoors = doors; + doors = 0; + for (int i = 0; i < 5; pos.move(Direction.SOUTH), i++) { + doors |= world.getBlockState(room.relativeToActual(pos)).isAir() ? 1 << i : 0; + } + if (doors != prevDoors) { + changed = changed.onDoorChanged(); + } + + // Parse current combination of switches based on the levers. + currentCombination = 0; + for (int i = 0; i < 6; i++) { + currentCombination |= getSwitchState(world, room, i); + } + + return changed; + } + + private Cell parseBlock(World world, Room room, BlockPos.Mutable pos) { + // Check if the block is a switch. + BlockState state = world.getBlockState(room.relativeToActual(pos)); + int switch_ = SWITCH_BLOCKS.getInt(state.getBlock()); + if (switch_-- > 0) { + return new SwitchCell(switch_); + } + // Check if the block is an opened switch by checking the block behind it. + int switchBehind = SWITCH_BLOCKS.getInt(world.getBlockState(room.relativeToActual(pos.move(Direction.SOUTH))).getBlock()); + pos.move(Direction.NORTH); + if (switchBehind-- > 0) { + return SwitchCell.ofOpened(switchBehind); + } + + // Check if the block is empty otherwise the block is a wall. + return state.isAir() || state.isOf(Blocks.WATER) ? Cell.EMPTY : Cell.BLOCK; + } + + private static int getSwitchState(World world, Room room, int i) { + BlockState state = world.getBlockState(room.relativeToActual(SWITCH_POSITIONS[i])); + return state.contains(LeverBlock.POWERED) && state.get(LeverBlock.POWERED) ? 1 << i : 0; + } + + private void updateSwitches() { + clearSwitches(); + for (Cell[] row : cells) { + for (Cell cell : row) { + if (cell instanceof SwitchCell switchCell) { + switches[switchCell.id].add(switchCell); + } + } + } + } + + private void simulateCombinations() { + for (int combination = 0; combination < (1 << 6); combination++) { + for (int switchIndex = 0; switchIndex < 6; switchIndex++) { + if ((combination & (1 << switchIndex)) != 0) { + switches[switchIndex].toggle(); + } + } + results[combination] = simulateCombination(); + for (int switchIndex = 0; switchIndex < 6; switchIndex++) { + if ((combination & (1 << switchIndex)) != 0) { + switches[switchIndex].toggle(); + } + } + } + } + + private Result simulateCombination() { + List<Vector2i> waters = new ArrayList<>(); + waters.add(new Vector2i(9, 0)); + Result result = new Result(); + while (!waters.isEmpty()) { + List<Vector2i> newWaters = new ArrayList<>(); + for (Iterator<Vector2i> watersIt = waters.iterator(); watersIt.hasNext(); ) { + Vector2i water = watersIt.next(); + // Check if the water has reached a door. + if (water.y == 18) { + switch (water.x) { + case 0 -> result.reachedDoors |= 1 << 4; + case 4 -> result.reachedDoors |= 1 << 3; + case 9 -> result.reachedDoors |= 1 << 2; + case 14 -> result.reachedDoors |= 1 << 1; + case 18 -> result.reachedDoors |= 1; + } + watersIt.remove(); + continue; + } + // Check if the water can flow down. + if (water.y < 18 && cells[water.y + 1][water.x].isOpen()) { + result.putPath(water, 0); + water.add(0, 1); + continue; + } + + // Get the offset to the first block on the left and the right that can flow down. + int leftFlowDownOffset = findFlowDown(water, false); + int rightFlowDownOffset = findFlowDown(water, true); + // Check if left down is in range and is closer than right down. + // Note 1: The yarn name "getFlowSpeed" is incorrect as it actually returns the maximum distance that water will check for a hole to flow towards. + // Note 2: Skyblock's maximum offset is 5 instead of 4 for some reason. + if (-leftFlowDownOffset <= ((WaterFluid) Fluids.WATER).getFlowSpeed(null) + 1 && -leftFlowDownOffset < rightFlowDownOffset) { + result.putPath(water, leftFlowDownOffset); + water.add(leftFlowDownOffset, 1); + continue; + } + // Check if right down is in range and closer than left down. + if (rightFlowDownOffset <= ((WaterFluid) Fluids.WATER).getFlowSpeed(null) + 1 && rightFlowDownOffset < -leftFlowDownOffset) { + result.putPath(water, rightFlowDownOffset); + water.add(rightFlowDownOffset, 1); + continue; + } + + // Else flow to both sides if in range. + if (leftFlowDownOffset > Integer.MIN_VALUE + 1) { + result.putPath(water, leftFlowDownOffset); + newWaters.add(new Vector2i(water).add(leftFlowDownOffset, 1)); + } + if (rightFlowDownOffset < Integer.MAX_VALUE) { + result.putPath(water, rightFlowDownOffset); + newWaters.add(new Vector2i(water).add(rightFlowDownOffset, 1)); + } + watersIt.remove(); + } + waters.addAll(newWaters); + } + return result; + } + + /** + * Finds the first block on the left that can flow down. + */ + private int findFlowDown(Vector2i water, boolean direction) { + for (int i = 0; water.x + i >= 0 && water.x + i < 19 && i > -8 && i < 8 && cells[water.y][water.x + i].isOpen(); i += direction ? 1 : -1) { + if (cells[water.y + 1][water.x + i].isOpen()) { + return i; + } + } + return direction ? Integer.MAX_VALUE : Integer.MIN_VALUE + 1; + } + + private void findBestCombinations() { + bestCombinations.clear(); + for (int combination = 0, bestScore = 0; combination < (1 << 6); combination++) { + int newScore = Integer.bitCount(results[combination].reachedDoors ^ doors); + if (newScore >= bestScore) { + if (newScore > bestScore) { + bestCombinations.clear(); + bestScore = newScore; + } + bestCombinations.add(combination); + } + } + } + + @Override + public void render(WorldRenderContext context) { + if (!SkyblockerConfigManager.get().locations.dungeons.solveWaterboard || !DungeonManager.isCurrentRoomMatched()) return; + Room room = DungeonManager.getCurrentRoom(); + + // Render the best combination. + @SuppressWarnings("resource") + BlockState state = context.world().getBlockState(room.relativeToActual(WATER_LEVER)); + // bestCombinationsUpdated is needed because bestCombinations does not update immediately after the lever is turned off. + if (waypoints[0] != null && bestCombinationsUpdated && state.contains(LeverBlock.POWERED) && !state.get(LeverBlock.POWERED)) { + bestCombinations.intStream().mapToObj(bestCombination -> currentCombination ^ bestCombination).min(Comparator.comparingInt(Integer::bitCount)).ifPresent(bestDifference -> { + for (int i = 0; i < 6; i++) { + if ((bestDifference & 1 << i) != 0) { + waypoints[i].render(context); + } + } + if (bestDifference == 0 && !waypoints[6].shouldRender()) { + waypoints[6].setMissing(); + } + }); + } + if (waypoints[6] != null && waypoints[6].shouldRender()) { + waypoints[6].render(context); + } + + // Render the current path of the water. + BlockPos.Mutable pos = new BlockPos.Mutable(15, 79, 26); + RenderHelper.renderLinesFromPoints(context, new Vec3d[]{Vec3d.ofCenter(room.relativeToActual(pos)), Vec3d.ofCenter(room.relativeToActual(pos.move(Direction.DOWN)))}, LIME_COLOR_COMPONENTS, 1f, 5f, true); + Result currentResult = results[currentCombination]; + if (currentResult != null) { + for (Map.Entry<Vector2ic, Integer> entry : currentResult.path.entries()) { + Vec3d start = Vec3d.ofCenter(room.relativeToActual(pos.set(24 - entry.getKey().x(), 78 - entry.getKey().y(), 26))); + Vec3d middle = Vec3d.ofCenter(room.relativeToActual(pos.move(Direction.WEST, entry.getValue()))); + Vec3d end = Vec3d.ofCenter(room.relativeToActual(pos.move(Direction.DOWN))); + RenderHelper.renderLinesFromPoints(context, new Vec3d[]{start, middle}, LIME_COLOR_COMPONENTS, 1f, 5f, true); + RenderHelper.renderLinesFromPoints(context, new Vec3d[]{middle, end}, LIME_COLOR_COMPONENTS, 1f, 5f, true); + } + } + } + + private ActionResult onUseBlock(PlayerEntity player, World world, Hand hand, BlockHitResult blockHitResult) { + BlockState state = world.getBlockState(blockHitResult.getBlockPos()); + if (SkyblockerConfigManager.get().locations.dungeons.solveWaterboard && blockHitResult.getType() == HitResult.Type.BLOCK && waypoints[6] != null && DungeonManager.isCurrentRoomMatched() && blockHitResult.getBlockPos().equals(DungeonManager.getCurrentRoom().relativeToActual(WATER_LEVER)) && state.contains(LeverBlock.POWERED)) { + if (!state.get(LeverBlock.POWERED)) { + bestCombinationsUpdated = false; + Scheduler.INSTANCE.schedule(() -> waypoints[6].setMissing(), 50); + } + waypoints[6].setFound(); + } + return ActionResult.PASS; + } + + @Override + public void reset() { + super.reset(); + solve = null; + for (Cell[] row : cells) { + Arrays.fill(row, null); + } + clearSwitches(); + doors = 0; + Arrays.fill(results, null); + currentCombination = 0; + bestCombinations.clear(); + Arrays.fill(waypoints, null); + } + + public void clearSwitches() { + for (Switch switch_ : switches) { + switch_.clear(); + } + } + + private enum Changed { + NONE, CELL, DOOR, BOTH; + + private boolean cellChanged() { + return this == CELL || this == BOTH; + } + + private boolean doorChanged() { + return this == DOOR || this == BOTH; + } + + private Changed onCellChanged() { + return switch (this) { + case NONE, CELL -> Changed.CELL; + case DOOR, BOTH -> Changed.BOTH; + }; + } + + private Changed onDoorChanged() { + return switch (this) { + case NONE, DOOR -> Changed.DOOR; + case CELL, BOTH -> Changed.BOTH; + }; + } + } + + public static class Result { + private int reachedDoors; + private final Multimap<Vector2ic, Integer> path = MultimapBuilder.hashKeys().arrayListValues().build(); + + public boolean putPath(Vector2i water, int offset) { + return path.put(new Vector2i(water), offset); + } + + @Override + public String toString() { + return "Result[reachedDoors=" + Integer.toBinaryString(reachedDoors) + ", path=" + path + ']'; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java new file mode 100644 index 00000000..931d1d69 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java @@ -0,0 +1,60 @@ +package de.hysky.skyblocker.skyblock.dungeon.secrets; + +import de.hysky.skyblocker.utils.waypoint.Waypoint; +import it.unimi.dsi.fastutil.ints.IntRBTreeSet; +import it.unimi.dsi.fastutil.ints.IntSortedSet; +import it.unimi.dsi.fastutil.ints.IntSortedSets; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.registry.Registries; +import net.minecraft.util.math.BlockPos; +import org.apache.commons.lang3.tuple.MutableTriple; +import org.joml.Vector2ic; + +import java.util.*; + +public class DebugRoom extends Room { + private final List<Waypoint> checkedBlocks = Collections.synchronizedList(new ArrayList<>()); + + public DebugRoom(Type type, Vector2ic... physicalPositions) { + super(type, physicalPositions); + } + + public static DebugRoom ofSinglePossibleRoom(Type type, Vector2ic physicalPositions, String roomName, int[] roomData, Direction direction) { + return ofSinglePossibleRoom(type, new Vector2ic[]{physicalPositions}, roomName, roomData, direction); + } + + public static DebugRoom ofSinglePossibleRoom(Type type, Vector2ic[] physicalPositions, String roomName, int[] roomData, Direction direction) { + DebugRoom room = new DebugRoom(type, physicalPositions); + IntSortedSet segmentsX = IntSortedSets.unmodifiable(new IntRBTreeSet(room.segments.stream().mapToInt(Vector2ic::x).toArray())); + IntSortedSet segmentsY = IntSortedSets.unmodifiable(new IntRBTreeSet(room.segments.stream().mapToInt(Vector2ic::y).toArray())); + room.roomsData = Map.of(roomName, roomData); + room.possibleRooms = List.of(MutableTriple.of(direction, DungeonMapUtils.getPhysicalCornerPos(direction, segmentsX, segmentsY), List.of(roomName))); + return room; + } + + @Override + protected boolean checkBlock(ClientWorld world, BlockPos pos) { + byte id = DungeonManager.NUMERIC_ID.getByte(Registries.BLOCK.getId(world.getBlockState(pos).getBlock()).toString()); + if (id == 0) { + return false; + } + for (MutableTriple<Direction, Vector2ic, List<String>> directionRooms : possibleRooms) { + int block = posIdToInt(DungeonMapUtils.actualToRelative(directionRooms.getLeft(), directionRooms.getMiddle(), pos), id); + for (String room : directionRooms.getRight()) { + checkedBlocks.add(new Waypoint(pos, SecretWaypoint.TYPE_SUPPLIER, Arrays.binarySearch(roomsData.get(room), block) >= 0 ? Room.GREEN_COLOR_COMPONENTS : Room.RED_COLOR_COMPONENTS)); + } + } + return false; + } + + @Override + public void render(WorldRenderContext context) { + super.render(context); + synchronized (checkedBlocks) { + for (Waypoint checkedBlock : checkedBlocks) { + checkedBlock.render(context); + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java index 7f401fdb..32f0b7e3 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java @@ -7,16 +7,20 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.mojang.brigadier.Command; import com.mojang.brigadier.arguments.IntegerArgumentType; -import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.serialization.JsonOps; import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfig; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.debug.Debug; import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.Tickable; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.scheduler.Scheduler; import it.unimi.dsi.fastutil.objects.Object2ByteMap; +import it.unimi.dsi.fastutil.objects.Object2ByteMaps; import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectIntPair; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; @@ -27,8 +31,9 @@ import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; import net.fabricmc.fabric.api.event.player.UseBlockCallback; +import net.minecraft.block.Blocks; import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.command.CommandSource; import net.minecraft.command.argument.BlockPosArgumentType; import net.minecraft.command.argument.PosArgument; import net.minecraft.command.argument.TextArgumentType; @@ -37,10 +42,12 @@ import net.minecraft.entity.ItemEntity; import net.minecraft.entity.LivingEntity; import net.minecraft.entity.mob.AmbientEntity; import net.minecraft.entity.passive.BatEntity; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.FilledMapItem; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; import net.minecraft.item.map.MapState; +import net.minecraft.registry.Registry; import net.minecraft.resource.Resource; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.text.Text; @@ -75,18 +82,18 @@ import java.util.zip.InflaterInputStream; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; -public class DungeonSecrets { - protected static final Logger LOGGER = LoggerFactory.getLogger(DungeonSecrets.class); +public class DungeonManager { + protected static final Logger LOGGER = LoggerFactory.getLogger(DungeonManager.class); private static final String DUNGEONS_PATH = "dungeons"; private static final Path CUSTOM_WAYPOINTS_DIR = SkyblockerMod.CONFIG_DIR.resolve("custom_secret_waypoints.json"); private static final Pattern KEY_FOUND = Pattern.compile("^(?:\\[.+] )?(?<name>\\w+) has obtained (?<type>Wither|Blood) Key!$"); /** * Maps the block identifier string to a custom numeric block id used in dungeon rooms data. * - * @implNote Not using {@link net.minecraft.registry.Registry#getId(Object) Registry#getId(Block)} and {@link net.minecraft.block.Blocks Blocks} since this is also used by {@link de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonRoomsDFU DungeonRoomsDFU}, which runs outside of Minecraft. + * @implNote Not using {@link Registry#getId(Object) Registry#getId(Block)} and {@link Blocks Blocks} since this is also used by {@link de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonRoomsDFU DungeonRoomsDFU}, which runs outside of Minecraft. */ @SuppressWarnings("JavadocReference") - protected static final Object2ByteMap<String> NUMERIC_ID = new Object2ByteOpenHashMap<>(Map.ofEntries( + protected static final Object2ByteMap<String> NUMERIC_ID = Object2ByteMaps.unmodifiable(new Object2ByteOpenHashMap<>(Map.ofEntries( Map.entry("minecraft:stone", (byte) 1), Map.entry("minecraft:diorite", (byte) 2), Map.entry("minecraft:polished_diorite", (byte) 3), @@ -108,7 +115,7 @@ public class DungeonSecrets { Map.entry("minecraft:gray_terracotta", (byte) 19), Map.entry("minecraft:cyan_terracotta", (byte) 20), Map.entry("minecraft:black_terracotta", (byte) 21) - )); + ))); /** * Block data for dungeon rooms. See {@link de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonRoomsDFU DungeonRoomsDFU} for format details and how it's generated. * All access to this map must check {@link #isRoomsLoaded()} to prevent concurrent modification. @@ -140,6 +147,7 @@ public class DungeonSecrets { @Nullable private static Vector2ic physicalEntrancePos; private static Room currentRoom; + private static boolean inBoss; public static boolean isRoomsLoaded() { return roomsLoaded != null && roomsLoaded.isDone(); @@ -151,11 +159,13 @@ public class DungeonSecrets { @SuppressWarnings("unused") public static JsonObject getRoomMetadata(String room) { - return roomsJson.get(room).getAsJsonObject(); + JsonElement value = roomsJson.get(room); + return value != null ? value.getAsJsonObject() : null; } public static JsonArray getRoomWaypoints(String room) { - return waypointsJson.get(room).getAsJsonArray(); + JsonElement value = waypointsJson.get(room); + return value != null ? value.getAsJsonArray() : null; } /** @@ -190,35 +200,58 @@ public class DungeonSecrets { return customWaypoints.remove(room, pos); } + public static Room getCurrentRoom() { + return currentRoom; + } + + public static boolean isInBoss() { + return inBoss; + } + /** * Loads the dungeon secrets asynchronously from {@code /assets/skyblocker/dungeons}. * Use {@link #isRoomsLoaded()} to check for completion of loading. */ public static void init() { - if (SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.noInitSecretWaypoints) { + if (!SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableRoomMatching) { return; } // Execute with MinecraftClient as executor since we need to wait for MinecraftClient#resourceManager to be set - CompletableFuture.runAsync(DungeonSecrets::load, MinecraftClient.getInstance()).exceptionally(e -> { + CompletableFuture.runAsync(DungeonManager::load, MinecraftClient.getInstance()).exceptionally(e -> { LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secrets", e); return null; }); - ClientLifecycleEvents.CLIENT_STOPPING.register(DungeonSecrets::saveCustomWaypoints); - Scheduler.INSTANCE.scheduleCyclic(DungeonSecrets::update, 10); - WorldRenderEvents.AFTER_TRANSLUCENT.register(DungeonSecrets::render); - ClientReceiveMessageEvents.GAME.register(DungeonSecrets::onChatMessage); - ClientReceiveMessageEvents.GAME_CANCELED.register(DungeonSecrets::onChatMessage); + ClientLifecycleEvents.CLIENT_STOPPING.register(DungeonManager::saveCustomWaypoints); + Scheduler.INSTANCE.scheduleCyclic(DungeonManager::update, 5); + WorldRenderEvents.AFTER_TRANSLUCENT.register(DungeonManager::render); + ClientReceiveMessageEvents.GAME.register(DungeonManager::onChatMessage); + ClientReceiveMessageEvents.GAME_CANCELED.register(DungeonManager::onChatMessage); UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> onUseBlock(world, hitResult)); ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("secrets") .then(literal("markAsFound").then(markSecretsCommand(true))) .then(literal("markAsMissing").then(markSecretsCommand(false))) - .then(literal("getRelativePos").executes(DungeonSecrets::getRelativePos)) - .then(literal("getRelativeTargetPos").executes(DungeonSecrets::getRelativeTargetPos)) + .then(literal("getRelativePos").executes(DungeonManager::getRelativePos)) + .then(literal("getRelativeTargetPos").executes(DungeonManager::getRelativeTargetPos)) .then(literal("addWaypoint").then(addCustomWaypointCommand(false))) .then(literal("addWaypointRelatively").then(addCustomWaypointCommand(true))) .then(literal("removeWaypoint").then(removeCustomWaypointCommand(false))) .then(literal("removeWaypointRelatively").then(removeCustomWaypointCommand(true))) )))); + if (Debug.debugEnabled()) { + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("secrets") + .then(literal("matchAgainst").then(matchAgainstCommand())) + .then(literal("clearSubProcesses").executes(context -> { + if (currentRoom != null) { + currentRoom.tickables.clear(); + currentRoom.renderables.clear(); + context.getSource().sendFeedback(Constants.PREFIX.get().append("§rCleared sub processes in the current room.")); + } else { + context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null.")); + } + return Command.SINGLE_SUCCESS; + })) + )))); + } ClientPlayConnectionEvents.JOIN.register(((handler, sender, client) -> reset())); } @@ -304,7 +337,7 @@ public class DungeonSecrets { SkyblockerMod.GSON.fromJson(reader, JsonObject.class).asMap().forEach((room, jsonElement) -> map.put(room.toLowerCase().replaceAll(" ", "-"), jsonElement)); } - private static ArgumentBuilder<FabricClientCommandSource, RequiredArgumentBuilder<FabricClientCommandSource, Integer>> markSecretsCommand(boolean found) { + private static RequiredArgumentBuilder<FabricClientCommandSource, Integer> markSecretsCommand(boolean found) { return argument("secretIndex", IntegerArgumentType.integer()).executes(context -> { int secretIndex = IntegerArgumentType.getInteger(context, "secretIndex"); if (markSecrets(secretIndex, found)) { @@ -333,14 +366,14 @@ public class DungeonSecrets { Room room = getRoomAtPhysical(pos); if (isRoomMatched(room)) { BlockPos relativePos = currentRoom.actualToRelative(pos); - source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.posMessage", currentRoom.getName(), relativePos.getX(), relativePos.getY(), relativePos.getZ()))); + source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.posMessage", currentRoom.getName(), currentRoom.getDirection().asString(), relativePos.getX(), relativePos.getY(), relativePos.getZ()))); } else { source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); } return Command.SINGLE_SUCCESS; } - private static ArgumentBuilder<FabricClientCommandSource, RequiredArgumentBuilder<FabricClientCommandSource, PosArgument>> addCustomWaypointCommand(boolean relative) { + private static RequiredArgumentBuilder<FabricClientCommandSource, PosArgument> addCustomWaypointCommand(boolean relative) { return argument("pos", BlockPosArgumentType.blockPos()) .then(argument("secretIndex", IntegerArgumentType.integer()) .then(argument("category", SecretWaypoint.Category.CategoryArgumentType.category()) @@ -372,7 +405,7 @@ public class DungeonSecrets { return Command.SINGLE_SUCCESS; } - private static ArgumentBuilder<FabricClientCommandSource, RequiredArgumentBuilder<FabricClientCommandSource, PosArgument>> removeCustomWaypointCommand(boolean relative) { + private static RequiredArgumentBuilder<FabricClientCommandSource, PosArgument> removeCustomWaypointCommand(boolean relative) { return argument("pos", BlockPosArgumentType.blockPos()) .executes(context -> { // TODO Less hacky way with custom ClientBlockPosArgumentType @@ -400,6 +433,61 @@ public class DungeonSecrets { return Command.SINGLE_SUCCESS; } + private static RequiredArgumentBuilder<FabricClientCommandSource, String> matchAgainstCommand() { + return argument("room", StringArgumentType.string()).suggests((context, builder) -> CommandSource.suggestMatching(ROOMS_DATA.values().stream().map(Map::values).flatMap(Collection::stream).map(Map::keySet).flatMap(Collection::stream), builder)).then(argument("direction", Room.Direction.DirectionArgumentType.direction()).executes(context -> { + if (physicalEntrancePos == null || mapEntrancePos == null || mapRoomSize == 0) { + context.getSource().sendError(Constants.PREFIX.get().append("§cYou are not in a dungeon.")); + return Command.SINGLE_SUCCESS; + } + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player == null || client.world == null) { + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get player or world.")); + return Command.SINGLE_SUCCESS; + } + ItemStack stack = client.player.getInventory().main.get(8); + if (!stack.isOf(Items.FILLED_MAP)) { + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get dungeon map.")); + return Command.SINGLE_SUCCESS; + } + MapState map = FilledMapItem.getMapState(FilledMapItem.getMapId(stack), client.world); + if (map == null) { + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get dungeon map state.")); + return Command.SINGLE_SUCCESS; + } + + String roomName = StringArgumentType.getString(context, "room"); + Room.Direction direction = Room.Direction.DirectionArgumentType.getDirection(context, "direction"); + + Room room = newDebugRoom(roomName, direction, client.player, map); + if (room == null) { + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to find room with name " + roomName + ".")); + return Command.SINGLE_SUCCESS; + } + if (currentRoom != null) { + currentRoom.addSubProcess(room); + context.getSource().sendFeedback(Constants.PREFIX.get().append("§rMatching room " + roomName + " with direction " + direction + " against current room.")); + } else { + context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null.")); + } + + return Command.SINGLE_SUCCESS; + })); + } + + @Nullable + private static Room newDebugRoom(String roomName, Room.Direction direction, PlayerEntity player, MapState map) { + Room room = null; + int[] roomData; + if ((roomData = ROOMS_DATA.get("catacombs").get(Room.Shape.PUZZLE.shape).get(roomName)) != null) { + room = DebugRoom.ofSinglePossibleRoom(Room.Type.PUZZLE, DungeonMapUtils.getPhysicalRoomPos(player.getPos()), roomName, roomData, direction); + } else if ((roomData = ROOMS_DATA.get("catacombs").get(Room.Shape.TRAP.shape).get(roomName)) != null) { + room = DebugRoom.ofSinglePossibleRoom(Room.Type.TRAP, DungeonMapUtils.getPhysicalRoomPos(player.getPos()), roomName, roomData, direction); + } else if ((roomData = ROOMS_DATA.get("catacombs").values().stream().map(Map::entrySet).flatMap(Collection::stream).filter(entry -> entry.getKey().equals(roomName)).findAny().map(Map.Entry::getValue).orElse(null)) != null) { + room = DebugRoom.ofSinglePossibleRoom(Room.Type.ROOM, DungeonMapUtils.getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, DungeonMapUtils.getRoomSegments(map, DungeonMapUtils.getMapRoomPos(map, mapEntrancePos, mapRoomSize), mapRoomSize, Room.Type.ROOM.color)), roomName, roomData, direction); + } + return room; + } + /** * Updates the dungeon. The general idea is similar to the Dungeon Rooms Mod. * <p></p> @@ -421,14 +509,11 @@ public class DungeonSecrets { * <li> Create a new room. </li> * </ul> * <li> Sets {@link #currentRoom} to the current room, either created from the previous step or from {@link #rooms}. </li> - * <li> Calls {@link Room#update()} on {@link #currentRoom}. </li> + * <li> Calls {@link Tickable#tick(MinecraftClient)} on {@link #currentRoom}. </li> * </ul> */ @SuppressWarnings("JavadocReference") private static void update() { - if (!SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableSecretWaypoints) { - return; - } if (!Utils.isInDungeons()) { if (mapEntrancePos != null) { reset(); @@ -436,16 +521,15 @@ public class DungeonSecrets { return; } MinecraftClient client = MinecraftClient.getInstance(); - ClientPlayerEntity player = client.player; - if (player == null || client.world == null) { + if (client.player == null || client.world == null) { return; } if (physicalEntrancePos == null) { - Vec3d playerPos = player.getPos(); + Vec3d playerPos = client.player.getPos(); physicalEntrancePos = DungeonMapUtils.getPhysicalRoomPos(playerPos); currentRoom = newRoom(Room.Type.ENTRANCE, physicalEntrancePos); } - ItemStack stack = player.getInventory().main.get(8); + ItemStack stack = client.player.getInventory().main.get(8); if (!stack.isOf(Items.FILLED_MAP)) { return; } @@ -477,9 +561,13 @@ public class DungeonSecrets { } } if (room != null && currentRoom != room) { + if (currentRoom != null && room.getType() == Room.Type.FAIRY) { + currentRoom.nextRoom = room; + room.keyFound = currentRoom.keyFound; + } currentRoom = room; } - currentRoom.update(); + currentRoom.tick(client); } /** @@ -561,7 +649,10 @@ public class DungeonSecrets { if (message.equals("[BOSS] Bonzo: Gratz for making it this far, but I'm basically unbeatable.") || message.equals("[BOSS] Scarf: This is where the journey ends for you, Adventurers.") || message.equals("[BOSS] The Professor: I was burdened with terrible news recently...") || message.equals("[BOSS] Thorn: Welcome Adventurers! I am Thorn, the Spirit! And host of the Vegan Trials!") || message.equals("[BOSS] Livid: Welcome, you've arrived right on time. I am Livid, the Master of Shadows.") || message.equals("[BOSS] Sadan: So you made it all the way here... Now you wish to defy me? Sadan?!") - || message.equals("[BOSS] Maxor: WELL! WELL! WELL! LOOK WHO'S HERE!")) reset(); + || message.equals("[BOSS] Maxor: WELL! WELL! WELL! LOOK WHO'S HERE!")) { + reset(); + inBoss = true; + } } /** @@ -647,7 +738,7 @@ public class DungeonSecrets { * * @return {@code true} if {@link #currentRoom} is not null and {@link #isRoomMatched(Room)} */ - private static boolean isCurrentRoomMatched() { + public static boolean isCurrentRoomMatched() { return isRoomMatched(currentRoom); } @@ -663,12 +754,12 @@ public class DungeonSecrets { } /** - * Checks if the player is in a dungeon and {@link de.hysky.skyblocker.config.SkyblockerConfig.Dungeons#secretWaypoints Secret Waypoints} is enabled. + * Checks if {@link SkyblockerConfig.SecretWaypoints#enableRoomMatching room matching} is enabled and the player is in a dungeon. * - * @return whether dungeon secrets should be processed + * @return whether room matching and dungeon secrets should be processed */ private static boolean shouldProcess() { - return SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableSecretWaypoints && Utils.isInDungeons(); + return SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableRoomMatching && Utils.isInDungeons(); } /** @@ -680,5 +771,6 @@ public class DungeonSecrets { physicalEntrancePos = null; rooms.clear(); currentRoom = null; + inBoss = false; } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java index 01f2c9fc..b12bba62 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java @@ -130,7 +130,7 @@ public class DungeonMapUtils { return null; } Vector2ic offset = new Vector2i(mapEntrancePos.x() % mapRoomSizeWithGap, mapEntrancePos.y() % mapRoomSizeWithGap); - return mapPos.add(2, 2).sub(offset).sub(mapPos.x() % mapRoomSizeWithGap, mapPos.y() % mapRoomSizeWithGap).add(offset); + return mapPos.add(2, 2).sub(offset).sub(Math.floorMod(mapPos.x(), mapRoomSizeWithGap), Math.floorMod(mapPos.y(), mapRoomSizeWithGap)).add(offset); } /** @@ -271,7 +271,7 @@ public class DungeonMapUtils { queue.add(newMapPos); } } - DungeonSecrets.LOGGER.debug("[Skyblocker] Found dungeon room segments: {}", Arrays.toString(segments.toArray())); + DungeonManager.LOGGER.debug("[Skyblocker] Found dungeon room segments: {}", Arrays.toString(segments.toArray())); return segments.toArray(Vector2ic[]::new); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java index 7797513f..a1bafc20 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java @@ -2,13 +2,18 @@ package de.hysky.skyblocker.skyblock.dungeon.secrets; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.context.CommandContext; +import com.mojang.serialization.Codec; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.events.DungeonEvents; import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.Tickable; import de.hysky.skyblocker.utils.render.RenderHelper; +import de.hysky.skyblocker.utils.render.Renderable; import de.hysky.skyblocker.utils.scheduler.Scheduler; import it.unimi.dsi.fastutil.ints.IntRBTreeSet; import it.unimi.dsi.fastutil.ints.IntSortedSet; @@ -22,11 +27,13 @@ import net.minecraft.block.MapColor; import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.client.world.ClientWorld; +import net.minecraft.command.argument.EnumArgumentType; import net.minecraft.entity.ItemEntity; import net.minecraft.entity.LivingEntity; import net.minecraft.entity.mob.AmbientEntity; import net.minecraft.registry.Registries; import net.minecraft.text.Text; +import net.minecraft.util.StringIdentifiable; import net.minecraft.util.hit.BlockHitResult; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Box; @@ -44,16 +51,16 @@ import java.util.concurrent.CompletableFuture; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class Room { +public class Room implements Tickable, Renderable { private static final Pattern SECRET_INDEX = Pattern.compile("^(\\d+)"); private static final Pattern SECRETS = Pattern.compile("§7(\\d{1,2})/(\\d{1,2}) Secrets"); private static final Vec3d DOOR_SIZE = new Vec3d(3, 4, 3); - private static final float[] RED_COLOR_COMPONENTS = {1, 0, 0}; - private static final float[] GREEN_COLOR_COMPONENTS = {0, 1, 0}; + protected static final float[] RED_COLOR_COMPONENTS = {1, 0, 0}; + protected static final float[] GREEN_COLOR_COMPONENTS = {0, 1, 0}; @NotNull private final Type type; @NotNull - private final Set<Vector2ic> segments; + final Set<Vector2ic> segments; /** * The shape of the room. See {@link #getShape(IntSortedSet, IntSortedSet)}. @@ -63,11 +70,11 @@ public class Room { /** * The room data containing all rooms for a specific dungeon and {@link #shape}. */ - private Map<String, int[]> roomsData; + protected Map<String, int[]> roomsData; /** * Contains all possible dungeon rooms for this room. The list is gradually shrunk by checking blocks until only one room is left. */ - private List<MutableTriple<Direction, Vector2ic, List<String>>> possibleRooms; + protected List<MutableTriple<Direction, Vector2ic, List<String>>> possibleRooms; /** * Contains all blocks that have been checked to prevent checking the same block multiple times. */ @@ -75,7 +82,7 @@ public class Room { /** * The task that is used to check blocks. This is used to ensure only one such task can run at a time. */ - private CompletableFuture<Void> findRoom; + protected CompletableFuture<Void> findRoom; private int doubleCheckBlocks; /** * Represents the matching state of the room with the following possible values: @@ -84,17 +91,24 @@ public class Room { * <li>{@link MatchState#MATCHED} means that the room has a unique match ans has been double checked.</li> * <li>{@link MatchState#FAILED} means that the room has been checked and there is no match.</li> */ - private MatchState matchState = MatchState.MATCHING; + protected MatchState matchState = MatchState.MATCHING; private Table<Integer, BlockPos, SecretWaypoint> secretWaypoints; private String name; private Direction direction; private Vector2ic physicalCornerPos; + protected List<Tickable> tickables = new ArrayList<>(); + protected List<Renderable> renderables = new ArrayList<>(); + /** + * Stores the next room in the dungeon. Currently only used if the next room is the fairy room. + */ + @Nullable + protected Room nextRoom; @Nullable private BlockPos doorPos; @Nullable private Box doorBox; - private boolean keyFound; + protected boolean keyFound; public Room(@NotNull Type type, @NotNull Vector2ic... physicalPositions) { this.type = type; @@ -102,7 +116,7 @@ public class Room { IntSortedSet segmentsX = IntSortedSets.unmodifiable(new IntRBTreeSet(segments.stream().mapToInt(Vector2ic::x).toArray())); IntSortedSet segmentsY = IntSortedSets.unmodifiable(new IntRBTreeSet(segments.stream().mapToInt(Vector2ic::y).toArray())); shape = getShape(segmentsX, segmentsY); - roomsData = DungeonSecrets.ROOMS_DATA.getOrDefault("catacombs", Collections.emptyMap()).getOrDefault(shape.shape.toLowerCase(), Collections.emptyMap()); + roomsData = DungeonManager.ROOMS_DATA.getOrDefault("catacombs", Collections.emptyMap()).getOrDefault(shape.shape.toLowerCase(), Collections.emptyMap()); possibleRooms = getPossibleRooms(segmentsX, segmentsY); } @@ -122,6 +136,13 @@ public class Room { return name; } + /** + * Not null if {@link #isMatched()}. + */ + public Direction getDirection() { + return direction; + } + @Override public String toString() { return "Room{type=%s, segments=%s, shape=%s, matchState=%s, name=%s, direction=%s, physicalCornerPos=%s}".formatted(type, Arrays.toString(segments.toArray()), shape, matchState, name, direction, physicalCornerPos); @@ -129,12 +150,16 @@ public class Room { @NotNull private Shape getShape(IntSortedSet segmentsX, IntSortedSet segmentsY) { - return switch (segments.size()) { - case 1 -> Shape.ONE_BY_ONE; - case 2 -> Shape.ONE_BY_TWO; - case 3 -> segmentsX.size() == 2 && segmentsY.size() == 2 ? Shape.L_SHAPE : Shape.ONE_BY_THREE; - case 4 -> segmentsX.size() == 2 && segmentsY.size() == 2 ? Shape.TWO_BY_TWO : Shape.ONE_BY_FOUR; - default -> throw new IllegalArgumentException("There are no matching room shapes with this set of physical positions: " + Arrays.toString(segments.toArray())); + return switch (type) { + case PUZZLE -> Shape.PUZZLE; + case TRAP -> Shape.TRAP; + default -> switch (segments.size()) { + case 1 -> Shape.ONE_BY_ONE; + case 2 -> Shape.ONE_BY_TWO; + case 3 -> segmentsX.size() == 2 && segmentsY.size() == 2 ? Shape.L_SHAPE : Shape.ONE_BY_THREE; + case 4 -> segmentsX.size() == 2 && segmentsY.size() == 2 ? Shape.TWO_BY_TWO : Shape.ONE_BY_FOUR; + default -> throw new IllegalArgumentException("There are no matching room shapes with this set of physical positions: " + Arrays.toString(segments.toArray())); + }; }; } @@ -150,7 +175,7 @@ public class Room { @NotNull private Direction[] getPossibleDirections(IntSortedSet segmentsX, IntSortedSet segmentsY) { return switch (shape) { - case ONE_BY_ONE, TWO_BY_TWO -> Direction.values(); + case ONE_BY_ONE, TWO_BY_TWO, PUZZLE, TRAP -> Direction.values(); case ONE_BY_TWO, ONE_BY_THREE, ONE_BY_FOUR -> { if (segmentsX.size() > 1 && segmentsY.size() == 1) { yield new Direction[]{Direction.NW, Direction.SE}; @@ -186,7 +211,7 @@ public class Room { } /** - * Adds a custom waypoint relative to this room to {@link DungeonSecrets#customWaypoints} and all existing instances of this room. + * Adds a custom waypoint relative to this room to {@link DungeonManager#customWaypoints} and all existing instances of this room. * * @param secretIndex the index of the secret waypoint * @param category the category of the secret waypoint @@ -196,8 +221,8 @@ public class Room { @SuppressWarnings("JavadocReference") private void addCustomWaypoint(int secretIndex, SecretWaypoint.Category category, Text waypointName, BlockPos pos) { SecretWaypoint waypoint = new SecretWaypoint(secretIndex, category, waypointName, pos); - DungeonSecrets.addCustomWaypoint(name, waypoint); - DungeonSecrets.getRoomsStream().filter(r -> name.equals(r.getName())).forEach(r -> r.addCustomWaypoint(waypoint)); + DungeonManager.addCustomWaypoint(name, waypoint); + DungeonManager.getRoomsStream().filter(r -> name.equals(r.getName())).forEach(r -> r.addCustomWaypoint(waypoint)); } /** @@ -223,7 +248,7 @@ public class Room { } /** - * Removes a custom waypoint relative to this room from {@link DungeonSecrets#customWaypoints} and all existing instances of this room. + * Removes a custom waypoint relative to this room from {@link DungeonManager#customWaypoints} and all existing instances of this room. * * @param pos the position of the secret waypoint relative to this room * @return the removed secret waypoint or {@code null} if there was no secret waypoint at the given position @@ -231,9 +256,9 @@ public class Room { @SuppressWarnings("JavadocReference") @Nullable private SecretWaypoint removeCustomWaypoint(BlockPos pos) { - SecretWaypoint waypoint = DungeonSecrets.removeCustomWaypoint(name, pos); + SecretWaypoint waypoint = DungeonManager.removeCustomWaypoint(name, pos); if (waypoint != null) { - DungeonSecrets.getRoomsStream().filter(r -> name.equals(r.getName())).forEach(r -> r.removeCustomWaypoint(waypoint.secretIndex, pos)); + DungeonManager.getRoomsStream().filter(r -> name.equals(r.getName())).forEach(r -> r.removeCustomWaypoint(waypoint.secretIndex, pos)); } return waypoint; } @@ -249,6 +274,11 @@ public class Room { secretWaypoints.remove(secretIndex, actualPos); } + public <T extends Tickable & Renderable> void addSubProcess(T process) { + tickables.add(process); + renderables.add(process); + } + /** * Updates the room. * <p></p> @@ -268,16 +298,19 @@ public class Room { * </ul> */ @SuppressWarnings("JavadocReference") - protected void update() { - MinecraftClient client = MinecraftClient.getInstance(); - ClientWorld world = client.world; - if (world == null) { + @Override + public void tick(MinecraftClient client) { + if (client.world == null) { return; } + for (Tickable tickable : tickables) { + tickable.tick(client); + } + // Wither and blood door if (SkyblockerConfigManager.get().locations.dungeons.doorHighlight.enableDoorHighlight && doorPos == null) { - doorPos = DungeonMapUtils.getWitherBloodDoorPos(world, segments); + doorPos = DungeonMapUtils.getWitherBloodDoorPos(client.world, segments); if (doorPos != null) { doorBox = new Box(doorPos.getX(), doorPos.getY(), doorPos.getZ(), doorPos.getX() + DOOR_SIZE.getX(), doorPos.getY() + DOOR_SIZE.getY(), doorPos.getZ() + DOOR_SIZE.getZ()); } @@ -285,7 +318,7 @@ public class Room { // Room scanning and matching // Logical AND has higher precedence than logical OR - if (!type.needsScanning() || matchState != MatchState.MATCHING && matchState != MatchState.DOUBLE_CHECKING || !DungeonSecrets.isRoomsLoaded() || findRoom != null && !findRoom.isDone()) { + if (!type.needsScanning() || matchState != MatchState.MATCHING && matchState != MatchState.DOUBLE_CHECKING || !DungeonManager.isRoomsLoaded() || findRoom != null && !findRoom.isDone()) { return; } ClientPlayerEntity player = client.player; @@ -294,12 +327,12 @@ public class Room { } findRoom = CompletableFuture.runAsync(() -> { for (BlockPos pos : BlockPos.iterate(player.getBlockPos().add(-5, -5, -5), player.getBlockPos().add(5, 5, 5))) { - if (segments.contains(DungeonMapUtils.getPhysicalRoomPos(pos)) && notInDoorway(pos) && checkedBlocks.add(pos) && checkBlock(world, pos)) { + if (segments.contains(DungeonMapUtils.getPhysicalRoomPos(pos)) && notInDoorway(pos) && checkedBlocks.add(pos) && checkBlock(client.world, pos)) { break; } } }).exceptionally(e -> { - DungeonSecrets.LOGGER.error("[Skyblocker Dungeon Secrets] Encountered an unknown exception while matching room {}", this, e); + DungeonManager.LOGGER.error("[Skyblocker Dungeon Secrets] Encountered an unknown exception while matching room {}", this, e); return null; }); } @@ -318,7 +351,7 @@ public class Room { * <p></p> * This method: * <ul> - * <li> Checks if the block type is included in the dungeon rooms data. See {@link DungeonSecrets#NUMERIC_ID}. </li> + * <li> Checks if the block type is included in the dungeon rooms data. See {@link DungeonManager#NUMERIC_ID}. </li> * <li> For each possible direction: </li> * <ul> * <li> Rotate and convert the position to a relative position. See {@link DungeonMapUtils#actualToRelative(Direction, Vector2ic, BlockPos)}. </li> @@ -358,8 +391,8 @@ public class Room { * @param pos the position of the block to check * @return whether room matching should end. Either a match is found or there are no valid rooms left */ - private boolean checkBlock(ClientWorld world, BlockPos pos) { - byte id = DungeonSecrets.NUMERIC_ID.getByte(Registries.BLOCK.getId(world.getBlockState(pos).getBlock()).toString()); + protected boolean checkBlock(ClientWorld world, BlockPos pos) { + byte id = DungeonManager.NUMERIC_ID.getByte(Registries.BLOCK.getId(world.getBlockState(pos).getBlock()).toString()); if (id == 0) { return false; } @@ -375,9 +408,10 @@ public class Room { } int matchingRoomsSize = possibleRooms.stream().map(Triple::getRight).mapToInt(Collection::size).sum(); - if (matchingRoomsSize == 0) { + if (matchingRoomsSize == 0) synchronized (this) { // If no rooms match, reset the fields and scan again after 50 ticks. - DungeonSecrets.LOGGER.warn("[Skyblocker Dungeon Secrets] No dungeon room matched after checking {} block(s) including double checking {} block(s)", checkedBlocks.size(), doubleCheckBlocks); + matchState = MatchState.FAILED; + DungeonManager.LOGGER.warn("[Skyblocker Dungeon Secrets] No dungeon room matched after checking {} block(s) including double checking {} block(s)", checkedBlocks.size(), doubleCheckBlocks); Scheduler.INSTANCE.schedule(() -> matchState = MatchState.MATCHING, 50); reset(); return true; @@ -388,18 +422,20 @@ public class Room { name = directionRoom.getRight().get(0); direction = directionRoom.getLeft(); physicalCornerPos = directionRoom.getMiddle(); - DungeonSecrets.LOGGER.info("[Skyblocker Dungeon Secrets] Room {} matched after checking {} block(s), starting double checking", name, checkedBlocks.size()); + DungeonManager.LOGGER.info("[Skyblocker Dungeon Secrets] Room {} matched after checking {} block(s), starting double checking", name, checkedBlocks.size()); roomMatched(); return false; } else if (matchState == MatchState.DOUBLE_CHECKING && ++doubleCheckBlocks >= 10) { // If double-checked, set state to matched and discard the no longer needed fields. - DungeonSecrets.LOGGER.info("[Skyblocker Dungeon Secrets] Room {} matched after checking {} block(s) including double checking {} block(s)", name, checkedBlocks.size(), doubleCheckBlocks); + matchState = MatchState.MATCHED; + DungeonEvents.ROOM_MATCHED.invoker().onRoomMatched(this); + DungeonManager.LOGGER.info("[Skyblocker Dungeon Secrets] Room {} confirmed after checking {} block(s) including double checking {} block(s)", name, checkedBlocks.size(), doubleCheckBlocks); discard(); return true; } return false; } else { - DungeonSecrets.LOGGER.debug("[Skyblocker Dungeon Secrets] {} room(s) remaining after checking {} block(s)", matchingRoomsSize, checkedBlocks.size()); + DungeonManager.LOGGER.debug("[Skyblocker Dungeon Secrets] {} room(s) remaining after checking {} block(s)", matchingRoomsSize, checkedBlocks.size()); return false; } } @@ -411,12 +447,12 @@ public class Room { * @param id the custom numeric block id * @return the encoded integer */ - private int posIdToInt(BlockPos pos, byte id) { + protected int posIdToInt(BlockPos pos, byte id) { return pos.getX() << 24 | pos.getY() << 16 | pos.getZ() << 8 | id; } /** - * Loads the secret waypoints for the room from {@link DungeonSecrets#waypointsJson} once it has been matched + * Loads the secret waypoints for the room from {@link DungeonManager#waypointsJson} once it has been matched * and sets {@link #matchState} to {@link MatchState#DOUBLE_CHECKING}. * * @param directionRooms the direction, position, and name of the room @@ -424,23 +460,25 @@ public class Room { @SuppressWarnings("JavadocReference") private void roomMatched() { secretWaypoints = HashBasedTable.create(); - for (JsonElement waypointElement : DungeonSecrets.getRoomWaypoints(name)) { - JsonObject waypoint = waypointElement.getAsJsonObject(); - String secretName = waypoint.get("secretName").getAsString(); - Matcher secretIndexMatcher = SECRET_INDEX.matcher(secretName); - int secretIndex = secretIndexMatcher.find() ? Integer.parseInt(secretIndexMatcher.group(1)) : 0; - BlockPos pos = DungeonMapUtils.relativeToActual(direction, physicalCornerPos, waypoint); - secretWaypoints.put(secretIndex, pos, new SecretWaypoint(secretIndex, waypoint, secretName, pos)); + JsonArray secretWaypointsJson = DungeonManager.getRoomWaypoints(name); + if (secretWaypointsJson != null) { + for (JsonElement waypointElement : secretWaypointsJson) { + JsonObject waypoint = waypointElement.getAsJsonObject(); + String secretName = waypoint.get("secretName").getAsString(); + Matcher secretIndexMatcher = SECRET_INDEX.matcher(secretName); + int secretIndex = secretIndexMatcher.find() ? Integer.parseInt(secretIndexMatcher.group(1)) : 0; + BlockPos pos = DungeonMapUtils.relativeToActual(direction, physicalCornerPos, waypoint); + secretWaypoints.put(secretIndex, pos, new SecretWaypoint(secretIndex, waypoint, secretName, pos)); + } } - DungeonSecrets.getCustomWaypoints(name).values().forEach(this::addCustomWaypoint); + DungeonManager.getCustomWaypoints(name).values().forEach(this::addCustomWaypoint); matchState = MatchState.DOUBLE_CHECKING; } /** * Resets fields for another round of matching after room matching fails. */ - private void reset() { - matchState = MatchState.FAILED; + protected void reset() { IntSortedSet segmentsX = IntSortedSets.unmodifiable(new IntRBTreeSet(segments.stream().mapToInt(Vector2ic::x).toArray())); IntSortedSet segmentsY = IntSortedSets.unmodifiable(new IntRBTreeSet(segments.stream().mapToInt(Vector2ic::y).toArray())); possibleRooms = getPossibleRooms(segmentsX, segmentsY); @@ -457,7 +495,6 @@ public class Room { * These fields are no longer needed and are discarded to save memory. */ private void discard() { - matchState = MatchState.MATCHED; roomsData = null; possibleRooms = null; checkedBlocks = null; @@ -467,25 +504,32 @@ public class Room { /** * Fails if !{@link #isMatched()} */ - protected BlockPos actualToRelative(BlockPos pos) { + public BlockPos actualToRelative(BlockPos pos) { return DungeonMapUtils.actualToRelative(direction, physicalCornerPos, pos); } /** * Fails if !{@link #isMatched()} */ - protected BlockPos relativeToActual(BlockPos pos) { + public BlockPos relativeToActual(BlockPos pos) { return DungeonMapUtils.relativeToActual(direction, physicalCornerPos, pos); } /** * Calls {@link SecretWaypoint#render(WorldRenderContext)} on {@link #secretWaypoints all secret waypoints} and renders a highlight around the wither or blood door, if it exists. */ - protected void render(WorldRenderContext context) { - if (isMatched()) { - for (SecretWaypoint secretWaypoint : secretWaypoints.values()) { - if (secretWaypoint.shouldRender()) { - secretWaypoint.render(context); + @Override + public void render(WorldRenderContext context) { + for (Renderable renderable : renderables) { + renderable.render(context); + } + + synchronized (this) { + if (SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableSecretWaypoints && isMatched()) { + for (SecretWaypoint secretWaypoint : secretWaypoints.values()) { + if (secretWaypoint.shouldRender()) { + secretWaypoint.render(context); + } } } } @@ -580,7 +624,7 @@ public class Room { */ private void onSecretFound(SecretWaypoint secretWaypoint, String msg, Object... args) { secretWaypoints.row(secretWaypoint.secretIndex).values().forEach(SecretWaypoint::setFound); - DungeonSecrets.LOGGER.info(msg, args); + DungeonManager.LOGGER.info(msg, args); } protected boolean markSecrets(int secretIndex, boolean found) { @@ -594,6 +638,9 @@ public class Room { } protected void keyFound() { + if (nextRoom != null && nextRoom.type == Type.FAIRY) { + nextRoom.keyFound = true; + } keyFound = true; } @@ -623,13 +670,15 @@ public class Room { } } - private enum Shape { + protected enum Shape { ONE_BY_ONE("1x1"), ONE_BY_TWO("1x2"), ONE_BY_THREE("1x3"), ONE_BY_FOUR("1x4"), L_SHAPE("L-shape"), - TWO_BY_TWO("2x2"); + TWO_BY_TWO("2x2"), + PUZZLE("puzzle"), + TRAP("trap"); final String shape; Shape(String shape) { @@ -642,11 +691,36 @@ public class Room { } } - public enum Direction { - NW, NE, SW, SE + public enum Direction implements StringIdentifiable { + NW("northwest"), NE("northeast"), SW("southwest"), SE("southeast"); + private static final Codec<Direction> CODEC = StringIdentifiable.createCodec(Direction::values); + private final String name; + + Direction(String name) { + this.name = name; + } + + @Override + public String asString() { + return name; + } + + static class DirectionArgumentType extends EnumArgumentType<Direction> { + DirectionArgumentType() { + super(CODEC, Direction::values); + } + + static DirectionArgumentType direction() { + return new DirectionArgumentType(); + } + + static <S> Direction getDirection(CommandContext<S> context, String name) { + return context.getArgument(name, Direction.class); + } + } } - public enum MatchState { + protected enum MatchState { MATCHING, DOUBLE_CHECKING, MATCHED, FAILED } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java index 75a0c20f..44df9d8c 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java @@ -29,7 +29,7 @@ import java.util.function.Supplier; import java.util.function.ToDoubleFunction; public class SecretWaypoint extends Waypoint { - protected static final Logger LOGGER = LoggerFactory.getLogger(SecretWaypoint.class); + private static final Logger LOGGER = LoggerFactory.getLogger(SecretWaypoint.class); public static final Codec<SecretWaypoint> CODEC = RecordCodecBuilder.create(instance -> instance.group( Codec.INT.fieldOf("secretIndex").forGetter(secretWaypoint -> secretWaypoint.secretIndex), Category.CODEC.fieldOf("category").forGetter(secretWaypoint -> secretWaypoint.category), @@ -39,7 +39,7 @@ public class SecretWaypoint extends Waypoint { public static final Codec<List<SecretWaypoint>> LIST_CODEC = CODEC.listOf(); static final List<String> SECRET_ITEMS = List.of("Decoy", "Defuse Kit", "Dungeon Chest Key", "Healing VIII", "Inflatable Jerry", "Spirit Leap", "Training Weights", "Trap", "Treasure Talisman"); private static final Supplier<SkyblockerConfig.SecretWaypoints> CONFIG = () -> SkyblockerConfigManager.get().locations.dungeons.secretWaypoints; - private static final Supplier<Type> TYPE_SUPPLIER = () -> CONFIG.get().waypointType; + static final Supplier<Type> TYPE_SUPPLIER = () -> CONFIG.get().waypointType; final int secretIndex; final Category category; final Text name; @@ -90,8 +90,13 @@ public class SecretWaypoint extends Waypoint { return category.isBat(); } + @Override + public boolean equals(Object obj) { + return super.equals(obj) || obj instanceof SecretWaypoint other && secretIndex == other.secretIndex && category == other.category && name.equals(other.name) && pos.equals(other.pos); + } + /** - * Renders the secret waypoint, including a filled cube, a beacon beam, the name, and the distance from the player. + * Renders the secret waypoint, including a waypoint through {@link Waypoint#render(WorldRenderContext)}, the name, and the distance from the player. */ @Override public void render(WorldRenderContext context) { @@ -173,15 +178,15 @@ public class SecretWaypoint extends Waypoint { } static class CategoryArgumentType extends EnumArgumentType<Category> { - public CategoryArgumentType() { + CategoryArgumentType() { super(Category.CODEC, Category::values); } - public static CategoryArgumentType category() { + static CategoryArgumentType category() { return new CategoryArgumentType(); } - public static <S> Category getCategory(CommandContext<S> context, String name) { + static <S> Category getCategory(CommandContext<S> context, String name) { return context.getArgument(name, Category.class); } } 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 5e0995e6..fa9f6e4f 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java @@ -19,7 +19,7 @@ public class MobGlow { public static boolean shouldMobGlow(Entity entity) { Box box = entity.getBoundingBox(); - if (!entity.isInvisible() && OcclusionCulling.isVisible(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) { + if (!entity.isInvisible() && OcclusionCulling.getReducedCuller().isVisible(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) { String name = entity.getName().getString(); // Dungeons diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/ToggleSkyMallFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/ToggleSkyMallFilter.java new file mode 100644 index 00000000..94d10e3a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/ToggleSkyMallFilter.java @@ -0,0 +1,16 @@ +package de.hysky.skyblocker.skyblock.filters; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.chat.ChatFilterResult; + +public class ToggleSkyMallFilter extends SimpleChatFilter { + + public ToggleSkyMallFilter() { + super("^§8§oYou can disable this messaging by toggling Sky Mall in your /hotm!$"); + } + + @Override + protected ChatFilterResult state() { + return SkyblockerConfigManager.get().messages.hideToggleSkyMall; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java index f1c9239c..b28e5da1 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemCooldowns.java @@ -2,8 +2,8 @@ package de.hysky.skyblocker.skyblock.item; import com.google.common.collect.ImmutableList; import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.events.ClientPlayerBlockBreakEvent; import de.hysky.skyblocker.utils.ItemUtils; +import net.fabricmc.fabric.api.event.client.player.ClientPlayerBlockBreakEvents; import net.fabricmc.fabric.api.event.player.UseItemCallback; import net.minecraft.block.BlockState; import net.minecraft.entity.player.PlayerEntity; @@ -26,7 +26,7 @@ public class ItemCooldowns { private static final Map<String, CooldownEntry> ITEM_COOLDOWNS = new HashMap<>(); public static void init() { - ClientPlayerBlockBreakEvent.AFTER.register(ItemCooldowns::afterBlockBreak); + ClientPlayerBlockBreakEvents.AFTER.register(ItemCooldowns::afterBlockBreak); UseItemCallback.EVENT.register(ItemCooldowns::onItemInteract); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/ItemRarityBackgrounds.java b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemRarityBackgrounds.java index 8867af91..c9cdb99a 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/ItemRarityBackgrounds.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/ItemRarityBackgrounds.java @@ -28,6 +28,7 @@ public class ItemRarityBackgrounds { private static final Supplier<Sprite> SPRITE = () -> MinecraftClient.getInstance().getGuiAtlasManager().getSprite(CONFIG.itemRarityBackgroundStyle.tex); private static final ImmutableMap<String, SkyblockItemRarity> LORE_RARITIES = ImmutableMap.ofEntries( Map.entry("ADMIN", SkyblockItemRarity.ADMIN), + Map.entry("ULTIMATE", SkyblockItemRarity.ULTIMATE), Map.entry("SPECIAL", SkyblockItemRarity.SPECIAL), //Very special is the same color so this will cover it Map.entry("DIVINE", SkyblockItemRarity.DIVINE), Map.entry("MYTHIC", SkyblockItemRarity.MYTHIC), @@ -36,8 +37,7 @@ public class ItemRarityBackgrounds { Map.entry("EPIC", SkyblockItemRarity.EPIC), Map.entry("RARE", SkyblockItemRarity.RARE), Map.entry("UNCOMMON", SkyblockItemRarity.UNCOMMON), - Map.entry("COMMON", SkyblockItemRarity.COMMON) - ); + Map.entry("COMMON", SkyblockItemRarity.COMMON)); private static final Int2ReferenceOpenHashMap<SkyblockItemRarity> CACHE = new Int2ReferenceOpenHashMap<>(); public static void init() { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/PlayerHeadHashCache.java b/src/main/java/de/hysky/skyblocker/skyblock/item/PlayerHeadHashCache.java new file mode 100644 index 00000000..2a1688fc --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/PlayerHeadHashCache.java @@ -0,0 +1,64 @@ +package de.hysky.skyblocker.skyblock.item; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Base64; +import java.util.concurrent.CompletableFuture; + +import org.apache.commons.io.FilenameUtils; +import org.slf4j.Logger; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.mojang.logging.LogUtils; + +import de.hysky.skyblocker.utils.Http; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; + +public class PlayerHeadHashCache { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final IntOpenHashSet CACHE = new IntOpenHashSet(); + + public static void init() { + CompletableFuture.runAsync(PlayerHeadHashCache::loadSkins); + } + + private static void loadSkins() { + try { + String response = Http.sendGetRequest("https://api.hypixel.net/v2/resources/skyblock/items"); + JsonArray items = JsonParser.parseString(response).getAsJsonObject().getAsJsonArray("items"); + + items.asList().stream() + .map(JsonElement::getAsJsonObject) + .filter(item -> item.get("material").getAsString().equals("SKULL_ITEM")) + .filter(item -> item.has("skin")) + .map(item -> Base64.getDecoder().decode(item.get("skin").getAsString())) + .map(String::new) + .map(profile -> JsonParser.parseString(profile).getAsJsonObject()) + .map(profile -> profile.getAsJsonObject("textures").getAsJsonObject("SKIN").get("url").getAsString()) + .map(PlayerHeadHashCache::getSkinHash) + .mapToInt(String::hashCode) + .forEach(CACHE::add); + + LOGGER.info("[Skyblocker Player Head Hash Cache] Successfully cached the hashes of all player head items!"); + } catch (Exception e) { + LOGGER.error("[Skyblocker Player Head Hash Cache] Failed to cache skin hashes!", e); + } + } + + //From MinecraftProfileTexture#getHash + public static String getSkinHash(String url) { + try { + return FilenameUtils.getBaseName(new URL(url).getPath()); + } catch (MalformedURLException e) { + LOGGER.error("[Skyblocker Player Head Hash Cache] Malformed Skin URL! URL: {}", url, e); + } + + return ""; + } + + public static boolean contains(int hash) { + return CACHE.contains(hash); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockItemRarity.java b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockItemRarity.java index 07a566af..4addeac6 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockItemRarity.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockItemRarity.java @@ -4,6 +4,7 @@ import net.minecraft.util.Formatting; public enum SkyblockItemRarity { ADMIN(Formatting.DARK_RED), + ULTIMATE(Formatting.DARK_RED), VERY_SPECIAL(Formatting.RED), SPECIAL(Formatting.RED), DIVINE(Formatting.AQUA), 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 adc23bbb..e4e18f8b 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 @@ -11,6 +11,7 @@ import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.scheduler.Scheduler; import net.minecraft.client.MinecraftClient; import net.minecraft.client.item.TooltipContext; +import net.minecraft.item.DyeableItem; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtElement; @@ -20,8 +21,6 @@ import net.minecraft.util.Formatting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.text.ParseException; -import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -148,7 +147,7 @@ public class ItemTooltip { } if (TooltipInfoType.OBTAINED.isTooltipEnabled()) { - String timestamp = getTimestamp(stack); + String timestamp = ItemUtils.getTimestamp(stack); if (!timestamp.isEmpty()) { lines.add(Text.literal(String.format("%-21s", "Obtained: ")) @@ -183,10 +182,10 @@ public class ItemTooltip { } if (TooltipInfoType.COLOR.isTooltipEnabledAndHasOrNullWarning(internalID) && stack.getNbt() != null) { - final NbtElement color = stack.getNbt().getCompound("display").get("color"); + boolean hasCustomDye = SkyblockerConfigManager.get().general.customDyeColors.containsKey(ItemUtils.getItemUuid(stack)); - if (color != null) { - String colorHex = String.format("%06X", Integer.parseInt(color.asString())); + if (!hasCustomDye && stack.getItem() instanceof DyeableItem item && item.hasColor(stack)) { + String colorHex = String.format("%06X", item.getColor(stack)); String expectedHex = ExoticTooltip.getExpectedHex(internalID); boolean correctLine = false; @@ -221,43 +220,11 @@ public class ItemTooltip { } } - /** - * this method converts the "timestamp" variable into the same date format as Hypixel represents it in the museum. - * Currently, there are two types of timestamps the legacy which is built like this - * "dd/MM/yy hh:mm" ("25/04/20 16:38") and the current which is built like this - * "MM/dd/yy hh:mm aa" ("12/24/20 11:08 PM"). Since Hypixel transforms the two formats into one format without - * taking into account of their formats, we do the same. The final result looks like this - * "MMMM dd, yyyy" (December 24, 2020). - * Since the legacy format has a 25 as "month" SimpleDateFormat converts the 25 into 2 years and 1 month and makes - * "25/04/20 16:38" -> "January 04, 2022" instead of "April 25, 2020". - * This causes the museum rank to be much worse than it should be. - * - * @param stack the item under the pointer - * @return if the item have a "Timestamp" it will be shown formated on the tooltip - */ - public static String getTimestamp(ItemStack stack) { - NbtCompound ea = ItemUtils.getExtraAttributes(stack); - - if (ea != null && ea.contains("timestamp", 8)) { - SimpleDateFormat nbtFormat = new SimpleDateFormat("MM/dd/yy"); - - try { - Date date = nbtFormat.parse(ea.getString("timestamp")); - SimpleDateFormat skyblockerFormat = new SimpleDateFormat("MMMM dd, yyyy", Locale.ENGLISH); - return skyblockerFormat.format(date); - } catch (ParseException e) { - LOGGER.warn("[Skyblocker-tooltip] getTimestamp", e); - } - } - - return ""; - } - // TODO What in the world is this? public static String getInternalNameFromNBT(ItemStack stack, boolean internalIDOnly) { NbtCompound ea = ItemUtils.getExtraAttributes(stack); - if (ea == null || !ea.contains(ItemUtils.ID, 8)) { + if (ea == null || !ea.contains(ItemUtils.ID, NbtElement.STRING_TYPE)) { return null; } String internalName = ea.getString(ItemUtils.ID); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigListWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigListWidget.java index 43647ec6..df5c4de1 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigListWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/shortcut/ShortcutsConfigListWidget.java @@ -194,6 +194,8 @@ public class ShortcutsConfigListWidget extends ElementListWidget<ShortcutsConfig this.category = category; target = new TextFieldWidget(MinecraftClient.getInstance().textRenderer, width / 2 - 160, 5, 150, 20, category.targetName); replacement = new TextFieldWidget(MinecraftClient.getInstance().textRenderer, width / 2 + 10, 5, 150, 20, category.replacementName); + target.setMaxLength(48); + replacement.setMaxLength(48); target.setText(targetString); replacement.setText(category.shortcutsMap.getOrDefault(targetString, "")); children = List.of(target, replacement); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/FireSaleWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/FireSaleWidget.java index e08b4acf..b77941c2 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/FireSaleWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/FireSaleWidget.java @@ -6,6 +6,7 @@ import java.util.regex.Pattern; import de.hysky.skyblocker.skyblock.tabhud.util.Colors; import de.hysky.skyblocker.skyblock.tabhud.util.Ico; import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr; +import de.hysky.skyblocker.skyblock.tabhud.widget.component.IcoTextComponent; import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent; import de.hysky.skyblocker.skyblock.tabhud.widget.component.ProgressComponent; import net.minecraft.text.MutableText; @@ -17,7 +18,7 @@ import net.minecraft.util.Formatting; public class FireSaleWidget extends Widget { - private static final MutableText TITLE = Text.literal("Fire Sale").formatted(Formatting.DARK_AQUA, + private static final MutableText TITLE = Text.literal("Fire Sales").formatted(Formatting.DARK_AQUA, Formatting.BOLD); // matches a fire sale item @@ -32,20 +33,20 @@ public class FireSaleWidget extends Widget { @Override public void updateContent() { - String event = PlayerListMgr.strAt(46); + Text event = PlayerListMgr.textAt(46); if (event == null) { - this.addComponent(new PlainTextComponent(Text.literal("No Fire Sale!").formatted(Formatting.GRAY))); + this.addComponent(new PlainTextComponent(Text.literal("No Fire Sales!").formatted(Formatting.GRAY))); return; } - if (event.contains("Starts In")) { - this.addSimpleIcoText(Ico.CLOCK, "Starts in:", Formatting.DARK_AQUA, 46); + if (event.getString().contains("starting in")) { + this.addComponent(new IcoTextComponent(Ico.CLOCK, event)); return; } for (int i = 46;; i++) { - Matcher m = PlayerListMgr.regexAt( i, FIRE_PATTERN); + Matcher m = PlayerListMgr.regexAt(i, FIRE_PATTERN); if (m == null) { break; } @@ -57,7 +58,5 @@ public class FireSaleWidget extends Widget { ProgressComponent pc = new ProgressComponent(Ico.GOLD, itemTxt, prgressTxt, pcnt, Colors.pcntToCol(pcnt)); this.addComponent(pc); } - } - } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java index ad58c868..6629c377 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java @@ -9,6 +9,7 @@ import de.hysky.skyblocker.utils.render.RenderHelper; import de.hysky.skyblocker.utils.waypoint.Waypoint; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; import net.fabricmc.fabric.api.event.player.AttackBlockCallback; @@ -58,9 +59,10 @@ public class MythologicalRitual { UseBlockCallback.EVENT.register(MythologicalRitual::onUseBlock); UseItemCallback.EVENT.register(MythologicalRitual::onUseItem); ClientReceiveMessageEvents.GAME.register(MythologicalRitual::onChatMessage); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("diana") .then(literal("clearGriffinBurrows").executes(context -> { - griffinBurrows.clear(); + reset(); return Command.SINGLE_SUCCESS; })) .then(literal("clearGriffinBurrow") @@ -144,10 +146,10 @@ public class MythologicalRitual { } if (burrow.confirmed != TriState.FALSE) { if (burrow.nextBurrowLine != null) { - RenderHelper.renderLinesFromPoints(context, burrow.nextBurrowLine, ORANGE_COLOR_COMPONENTS, 0.5F, 5F); + RenderHelper.renderLinesFromPoints(context, burrow.nextBurrowLine, ORANGE_COLOR_COMPONENTS, 0.5F, 5F, false); } if (burrow.echoBurrowLine != null) { - RenderHelper.renderLinesFromPoints(context, burrow.echoBurrowLine, ORANGE_COLOR_COMPONENTS, 0.5F, 5F); + RenderHelper.renderLinesFromPoints(context, burrow.echoBurrowLine, ORANGE_COLOR_COMPONENTS, 0.5F, 5F, false); } } } @@ -189,6 +191,16 @@ public class MythologicalRitual { private static boolean isActive() { return SkyblockerConfigManager.get().general.mythologicalRitual.enableMythologicalRitualHelper && Utils.getLocationRaw().equals("hub"); } + + private static void reset() { + griffinBurrows.clear(); + lastDugBurrowPos = null; + previousBurrow = new GriffinBurrow(BlockPos.ORIGIN); + + // Put a root burrow so echo detection works without a previous burrow + previousBurrow.confirmed = TriState.DEFAULT; + griffinBurrows.put(BlockPos.ORIGIN, previousBurrow); + } private static class GriffinBurrow extends Waypoint { private int critParticle; diff --git a/src/main/java/de/hysky/skyblocker/utils/BasePlaceholderScreen.java b/src/main/java/de/hysky/skyblocker/utils/BasePlaceholderScreen.java new file mode 100644 index 00000000..b362dcb7 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/BasePlaceholderScreen.java @@ -0,0 +1,19 @@ +package de.hysky.skyblocker.utils; + +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; + +public abstract class BasePlaceholderScreen extends Screen { + public BasePlaceholderScreen(Text title) { + super(title); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + } + + @Override + public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) { + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java index ed46677d..880ebe76 100644 --- a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java +++ b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java @@ -1,45 +1,49 @@ package de.hysky.skyblocker.utils; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import de.hysky.skyblocker.mixin.accessor.ItemStackAccessor; import it.unimi.dsi.fastutil.ints.IntIntPair; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.item.TooltipContext; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; import net.minecraft.nbt.StringNbtReader; import net.minecraft.text.Text; +import net.minecraft.text.Texts; import net.minecraft.util.Formatting; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.*; import java.util.function.Predicate; import java.util.regex.Pattern; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; + public class ItemUtils { + private static final Logger LOGGER = LoggerFactory.getLogger(ItemUtils.class); public static final String EXTRA_ATTRIBUTES = "ExtraAttributes"; public static final String ID = "id"; public static final String UUID = "uuid"; + private static final DateTimeFormatter OBTAINED_DATE_FORMATTER = DateTimeFormatter.ofPattern("MMMM d, yyyy").withZone(ZoneId.systemDefault()).localizedBy(Locale.ENGLISH); + private static final SimpleDateFormat OLD_OBTAINED_DATE_FORMAT = new SimpleDateFormat("MM/dd/yy"); public static final Pattern NOT_DURABILITY = Pattern.compile("[^0-9 /]"); public static final Predicate<String> FUEL_PREDICATE = line -> line.contains("Fuel: "); - public static List<Text> getTooltips(ItemStack item) { - MinecraftClient client = MinecraftClient.getInstance(); - return client.player == null || item == null ? Collections.emptyList() : item.getTooltip(client.player, TooltipContext.Default.BASIC); - } - - @Nullable - public static String getTooltip(ItemStack item, Predicate<String> predicate) { - for (Text line : getTooltips(item)) { - String string = line.getString(); - if (predicate.test(string)) { - return string; - } - } - - return null; + public static LiteralArgumentBuilder<FabricClientCommandSource> dumpHeldItemNbtCommand() { + return literal("dumpHeldItemNbt").executes(context -> { + context.getSource().sendFeedback(Text.literal("[Skyblocker Debug] Held Item Nbt: " + context.getSource().getPlayer().getMainHandStack().writeNbt(new NbtCompound()))); + return Command.SINGLE_SUCCESS; + }); } /** @@ -105,6 +109,44 @@ public class ItemUtils { return extraAttributes != null ? extraAttributes.getString(UUID) : ""; } + /** + * This method converts the "timestamp" variable into the same date format as Hypixel represents it in the museum. + * Currently, there are two types of string timestamps the legacy which is built like this + * "dd/MM/yy hh:mm" ("25/04/20 16:38") and the current which is built like this + * "MM/dd/yy hh:mm aa" ("12/24/20 11:08 PM"). Since Hypixel transforms the two formats into one format without + * taking into account of their formats, we do the same. The final result looks like this + * "MMMM dd, yyyy" (December 24, 2020). + * Since the legacy format has a 25 as "month" SimpleDateFormat converts the 25 into 2 years and 1 month and makes + * "25/04/20 16:38" -> "January 04, 2022" instead of "April 25, 2020". + * This causes the museum rank to be much worse than it should be. + * <p> + * This also handles the long timestamp format introduced in January 2024 where the timestamp is in epoch milliseconds. + * + * @param stack the item under the pointer + * @return if the item have a "Timestamp" it will be shown formated on the tooltip + */ + public static String getTimestamp(ItemStack stack) { + NbtCompound ea = getExtraAttributes(stack); + + if (ea != null && ea.contains("timestamp", NbtElement.LONG_TYPE)) { + Instant date = Instant.ofEpochMilli(ea.getLong("timestamp")); + + return OBTAINED_DATE_FORMATTER.format(date); + } + + if (ea != null && ea.contains("timestamp", NbtElement.STRING_TYPE)) { + try { + Instant date = OLD_OBTAINED_DATE_FORMAT.parse(ea.getString("timestamp")).toInstant(); + + return OBTAINED_DATE_FORMATTER.format(date); + } catch (ParseException e) { + LOGGER.warn("[Skyblocker Item Utils] Encountered an unknown exception while parsing time stamp of item {} with extra attributes {}", stack, ea, e); + } + } + + return ""; + } + public static boolean hasCustomDurability(@NotNull ItemStack stack) { NbtCompound extraAttributes = getExtraAttributes(stack); return extraAttributes != null && (extraAttributes.contains("drill_fuel") || extraAttributes.getString(ID).equals("PICKONIMBUS")); @@ -123,7 +165,7 @@ public class ItemUtils { return IntIntPair.of(pickonimbusDurability, 5000); } - String drillFuel = Formatting.strip(getTooltip(stack, FUEL_PREDICATE)); + String drillFuel = Formatting.strip(getNbtTooltip(stack, FUEL_PREDICATE)); if (drillFuel != null) { String[] drillFuelStrings = NOT_DURABILITY.matcher(drillFuel).replaceAll("").trim().split("/"); return IntIntPair.of(Integer.parseInt(drillFuelStrings[0]), Integer.parseInt(drillFuelStrings[1]) * 1000); @@ -132,6 +174,27 @@ public class ItemUtils { return null; } + @Nullable + public static String getNbtTooltip(ItemStack item, Predicate<String> predicate) { + for (Text line : getNbtTooltips(item)) { + String string = line.getString(); + if (predicate.test(string)) { + return string; + } + } + + return null; + } + + public static List<Text> getNbtTooltips(ItemStack item) { + NbtCompound displayNbt = item.getSubNbt("display"); + if (displayNbt == null || !displayNbt.contains("Lore", NbtElement.LIST_TYPE)) { + return Collections.emptyList(); + } + + return displayNbt.getList("Lore", NbtElement.STRING_TYPE).stream().map(NbtElement::asString).map(Text.Serialization::fromJson).filter(Objects::nonNull).map(text -> Texts.setStyleIfAbsent(text, ItemStackAccessor.getLORE_STYLE())).map(Text.class::cast).toList(); + } + public static ItemStack getSkyblockerStack() { try { return ItemStack.fromNbt(StringNbtReader.parse("{id:\"minecraft:player_head\",Count:1,tag:{SkullOwner:{Id:[I;-300151517,-631415889,-1193921967,-1821784279],Properties:{textures:[{Value:\"e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDdjYzY2ODc0MjNkMDU3MGQ1NTZhYzUzZTA2NzZjYjU2M2JiZGQ5NzE3Y2Q4MjY5YmRlYmVkNmY2ZDRlN2JmOCJ9fX0=\"}]}}}}")); diff --git a/src/main/java/de/hysky/skyblocker/utils/JoinWorldPlaceholderScreen.java b/src/main/java/de/hysky/skyblocker/utils/JoinWorldPlaceholderScreen.java new file mode 100644 index 00000000..f64bcf6c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/JoinWorldPlaceholderScreen.java @@ -0,0 +1,9 @@ +package de.hysky.skyblocker.utils; + +import net.minecraft.text.Text; + +public final class JoinWorldPlaceholderScreen extends BasePlaceholderScreen { + public JoinWorldPlaceholderScreen() { + super(Text.translatable("connect.joining")); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/ReconfiguringPlaceholderScreen.java b/src/main/java/de/hysky/skyblocker/utils/ReconfiguringPlaceholderScreen.java new file mode 100644 index 00000000..4d415615 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/ReconfiguringPlaceholderScreen.java @@ -0,0 +1,22 @@ +package de.hysky.skyblocker.utils; + +import net.minecraft.network.ClientConnection; +import net.minecraft.text.Text; + +public final class ReconfiguringPlaceholderScreen extends BasePlaceholderScreen { + private final ClientConnection connection; + + public ReconfiguringPlaceholderScreen(final ClientConnection connection) { + super(Text.translatable("connect.reconfiguring")); + this.connection = connection; + } + + @Override + public void tick() { + if (this.connection.isOpen()) { + this.connection.tick(); + } else { + this.connection.handleDisconnection(); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/Tickable.java b/src/main/java/de/hysky/skyblocker/utils/Tickable.java new file mode 100644 index 00000000..dff34e19 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/Tickable.java @@ -0,0 +1,7 @@ +package de.hysky.skyblocker.utils; + +import net.minecraft.client.MinecraftClient; + +public interface Tickable { + void tick(MinecraftClient client); +} diff --git a/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java b/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java index 2c75ef0a..ebdb6f09 100644 --- a/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java +++ b/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java @@ -5,8 +5,8 @@ import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.skyblock.barn.HungryHiker; import de.hysky.skyblocker.skyblock.barn.TreasureHunter; import de.hysky.skyblocker.skyblock.dungeon.Reparty; -import de.hysky.skyblocker.skyblock.dungeon.ThreeWeirdos; -import de.hysky.skyblocker.skyblock.dungeon.Trivia; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.ThreeWeirdos; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.Trivia; import de.hysky.skyblocker.skyblock.dwarven.Fetchur; import de.hysky.skyblocker.skyblock.dwarven.Puzzler; import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; @@ -53,7 +53,8 @@ public interface ChatMessageListener { new MoltenWaveFilter(), new TeleportPadFilter(), new AutopetFilter(), - new ShowOffFilter() + new ShowOffFilter(), + new ToggleSkyMallFilter() }; // Register all listeners to EVENT for (ChatMessageListener listener : listeners) { diff --git a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java index 0f73df16..05514d02 100644 --- a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java +++ b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java @@ -60,7 +60,7 @@ public class RenderHelper { renderFilled(context, Vec3d.of(pos), dimensions, colorComponents, alpha, true); } } else { - if (OcclusionCulling.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + dimensions.x, pos.getY() + dimensions.y, pos.getZ() + dimensions.z)) { + if (OcclusionCulling.getRegularCuller().isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + dimensions.x, pos.getY() + dimensions.y, pos.getZ() + dimensions.z)) { renderFilled(context, Vec3d.of(pos), dimensions, colorComponents, alpha, false); } } @@ -140,8 +140,9 @@ public class RenderHelper { * @param colorComponents An array of R, G and B color components * @param alpha The alpha of the lines * @param lineWidth The width of the lines + * @param throughWalls Whether to render through walls or not */ - public static void renderLinesFromPoints(WorldRenderContext context, Vec3d[] points, float[] colorComponents, float alpha, float lineWidth) { + public static void renderLinesFromPoints(WorldRenderContext context, Vec3d[] points, float[] colorComponents, float alpha, float lineWidth, boolean throughWalls) { Vec3d camera = context.camera().getPos(); MatrixStack matrices = context.matrixStack(); @@ -163,6 +164,7 @@ public class RenderHelper { RenderSystem.defaultBlendFunc(); RenderSystem.disableCull(); RenderSystem.enableDepthTest(); + RenderSystem.depthFunc(throughWalls ? GL11.GL_ALWAYS : GL11.GL_LEQUAL); buffer.begin(DrawMode.LINE_STRIP, VertexFormats.LINES); @@ -182,6 +184,7 @@ public class RenderHelper { GL11.glDisable(GL11.GL_LINE_SMOOTH); RenderSystem.lineWidth(1f); RenderSystem.enableCull(); + RenderSystem.depthFunc(GL11.GL_LEQUAL); } public static void renderQuad(WorldRenderContext context, Vec3d[] points, float[] colorComponents, float alpha, boolean throughWalls) { diff --git a/src/main/java/de/hysky/skyblocker/utils/render/Renderable.java b/src/main/java/de/hysky/skyblocker/utils/render/Renderable.java new file mode 100644 index 00000000..b7743153 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/Renderable.java @@ -0,0 +1,7 @@ +package de.hysky.skyblocker.utils.render; + +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; + +public interface Renderable { + void render(WorldRenderContext context); +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCuller.java b/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCuller.java new file mode 100644 index 00000000..3c48a47e --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCuller.java @@ -0,0 +1,45 @@ +package de.hysky.skyblocker.utils.render.culling; + +import com.logisticscraft.occlusionculling.OcclusionCullingInstance; +import com.logisticscraft.occlusionculling.cache.ArrayOcclusionCache; +import com.logisticscraft.occlusionculling.util.Vec3d; + +import de.hysky.skyblocker.utils.render.FrustumUtils; +import net.minecraft.client.MinecraftClient; + +public class OcclusionCuller { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + + private final OcclusionCullingInstance instance; + + // Reused objects to reduce allocation overhead + private final Vec3d cameraPos = new Vec3d(0, 0, 0); + private final Vec3d min = new Vec3d(0, 0, 0); + private final Vec3d max = new Vec3d(0, 0, 0); + + OcclusionCuller(int tracingDistance, WorldProvider worldProvider, double aabbExpansion) { + this.instance = new OcclusionCullingInstance(tracingDistance, worldProvider, new ArrayOcclusionCache(tracingDistance), aabbExpansion); + } + + private void updateCameraPos() { + var camera = CLIENT.gameRenderer.getCamera().getPos(); + cameraPos.set(camera.x, camera.y, camera.z); + } + + /** + * This first checks checks if the bounding box is within the camera's FOV, if + * it is then it checks for whether it's occluded or not. + * + * @return A boolean representing whether the bounding box is fully visible or + * not as per the instance's settings. + */ + public boolean isVisible(double x1, double y1, double z1, double x2, double y2, double z2) { + if (!FrustumUtils.isVisible(x1, y1, z1, x2, y2, z2)) return false; + + updateCameraPos(); + min.set(x1, y1, z1); + max.set(x2, y2, z2); + + return instance.isAABBVisible(min, max, cameraPos); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCulling.java b/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCulling.java index 5f8d1592..b1ff10dd 100644 --- a/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCulling.java +++ b/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCulling.java @@ -1,47 +1,23 @@ package de.hysky.skyblocker.utils.render.culling; -import com.logisticscraft.occlusionculling.OcclusionCullingInstance; -import com.logisticscraft.occlusionculling.cache.ArrayOcclusionCache; -import com.logisticscraft.occlusionculling.util.Vec3d; -import de.hysky.skyblocker.utils.render.FrustumUtils; -import net.minecraft.client.MinecraftClient; - public class OcclusionCulling { private static final int TRACING_DISTANCE = 128; - private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); - private static OcclusionCullingInstance instance = null; - - // Reused objects to reduce allocation overhead - private static final Vec3d cameraPos = new Vec3d(0, 0, 0); - private static final Vec3d min = new Vec3d(0, 0, 0); - private static final Vec3d max = new Vec3d(0, 0, 0); + private static OcclusionCuller regularCuller = null; + private static OcclusionCuller reducedCuller = null; /** - * Initializes the occlusion culling instance + * Initializes the occlusion culling instances */ public static void init() { - instance = new OcclusionCullingInstance(TRACING_DISTANCE, new WorldProvider(), new ArrayOcclusionCache(TRACING_DISTANCE), 2); + regularCuller = new OcclusionCuller(TRACING_DISTANCE, new WorldProvider(), 2); + reducedCuller = new OcclusionCuller(TRACING_DISTANCE, new ReducedWorldProvider(), 0); } - private static void updateCameraPos() { - var camera = CLIENT.gameRenderer.getCamera().getPos(); - cameraPos.set(camera.x, camera.y, camera.z); + public static OcclusionCuller getRegularCuller() { + return regularCuller; } - /** - * This first checks checks if the bounding box is within the camera's FOV, if - * it is then it checks for whether it's occluded or not. - * - * @return A boolean representing whether the bounding box is fully visible or - * not. - */ - public static boolean isVisible(double x1, double y1, double z1, double x2, double y2, double z2) { - if (!FrustumUtils.isVisible(x1, y1, z1, x2, y2, z2)) return false; - - updateCameraPos(); - min.set(x1, y1, z1); - max.set(x2, y2, z2); - - return instance.isAABBVisible(min, max, cameraPos); + public static OcclusionCuller getReducedCuller() { + return reducedCuller; } } diff --git a/src/main/java/de/hysky/skyblocker/utils/render/culling/ReducedWorldProvider.java b/src/main/java/de/hysky/skyblocker/utils/render/culling/ReducedWorldProvider.java new file mode 100644 index 00000000..5a2b9d87 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/culling/ReducedWorldProvider.java @@ -0,0 +1,19 @@ +package de.hysky.skyblocker.utils.render.culling; + +import net.minecraft.block.BlockState; +import net.minecraft.registry.tag.BlockTags; +import net.minecraft.util.math.BlockPos; + +public class ReducedWorldProvider extends WorldProvider { + + @Override + public boolean isOpaqueFullCube(int x, int y, int z) { + BlockPos pos = new BlockPos(x, y, z); + BlockState state = this.world.getBlockState(pos); + + //Fixes edge cases where stairs etc aren't treated as being full blocks for the use case + boolean isException = state.isIn(BlockTags.STAIRS) || state.isIn(BlockTags.WALLS) || state.isIn(BlockTags.FENCES); + + return isException || this.world.getBlockState(pos).isOpaqueFullCube(this.world, pos); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/culling/WorldProvider.java b/src/main/java/de/hysky/skyblocker/utils/render/culling/WorldProvider.java index 7ee0f0ed..1d2db238 100644 --- a/src/main/java/de/hysky/skyblocker/utils/render/culling/WorldProvider.java +++ b/src/main/java/de/hysky/skyblocker/utils/render/culling/WorldProvider.java @@ -7,7 +7,7 @@ import net.minecraft.util.math.BlockPos; public class WorldProvider implements DataProvider { private final static MinecraftClient CLIENT = MinecraftClient.getInstance(); - private ClientWorld world = null; + protected ClientWorld world = null; @Override public boolean prepareChunk(int chunkX, int chunkZ) { diff --git a/src/main/java/de/hysky/skyblocker/utils/scheduler/MessageScheduler.java b/src/main/java/de/hysky/skyblocker/utils/scheduler/MessageScheduler.java index a67d8da0..43194938 100644 --- a/src/main/java/de/hysky/skyblocker/utils/scheduler/MessageScheduler.java +++ b/src/main/java/de/hysky/skyblocker/utils/scheduler/MessageScheduler.java @@ -1,6 +1,8 @@ package de.hysky.skyblocker.utils.scheduler; import net.minecraft.client.MinecraftClient; +import net.minecraft.util.StringHelper; +import org.apache.commons.lang3.StringUtils; /** * A scheduler for sending chat messages or commands. Use the instance in {@link #INSTANCE}. Do not instantiate this class. @@ -34,13 +36,17 @@ public class MessageScheduler extends Scheduler { } private void sendMessage(String message) { - if (MinecraftClient.getInstance().player != null) { - if (message.startsWith("/")) { - MinecraftClient.getInstance().player.networkHandler.sendCommand(message.substring(1)); - } else { - MinecraftClient.getInstance().inGameHud.getChatHud().addToMessageHistory(message); - MinecraftClient.getInstance().player.networkHandler.sendChatMessage(message); - } + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player == null) { + Scheduler.LOGGER.error("[Skyblocker Message Scheduler] Tried to send a message while player is null: {}", message); + return; + } + message = StringHelper.truncateChat(StringUtils.normalizeSpace(message.trim())); + if (message.startsWith("/")) { + client.player.networkHandler.sendCommand(message.substring(1)); + } else { + client.inGameHud.getChatHud().addToMessageHistory(message); + client.player.networkHandler.sendChatMessage(message); } } 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 139ac05e..2f5375fe 100644 --- a/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java +++ b/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java @@ -20,7 +20,7 @@ import java.util.function.Supplier; * A scheduler for running tasks at a later time. Tasks will be run synchronously on the main client thread. Use the instance stored in {@link #INSTANCE}. Do not instantiate this class. */ public class Scheduler { - private static final Logger LOGGER = LoggerFactory.getLogger(Scheduler.class); + protected static final Logger LOGGER = LoggerFactory.getLogger(Scheduler.class); public static final Scheduler INSTANCE = new Scheduler(); private int currentTick = 0; private final AbstractInt2ObjectMap<List<ScheduledTask>> tasks = new Int2ObjectOpenHashMap<>(); diff --git a/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java b/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java index eb30cf8d..2f9c9f63 100644 --- a/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java +++ b/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java @@ -19,15 +19,19 @@ public class Waypoint { final boolean throughWalls; private boolean shouldRender; - protected Waypoint(BlockPos pos, Supplier<Type> typeSupplier, float[] colorComponents) { + public Waypoint(BlockPos pos, Type type, float[] colorComponents) { + this(pos, type, colorComponents, DEFAULT_HIGHLIGHT_ALPHA); + } + + public Waypoint(BlockPos pos, Supplier<Type> typeSupplier, float[] colorComponents) { this(pos, typeSupplier, colorComponents, DEFAULT_HIGHLIGHT_ALPHA, DEFAULT_LINE_WIDTH); } - protected Waypoint(BlockPos pos, Type type, float[] colorComponents, float alpha) { + public Waypoint(BlockPos pos, Type type, float[] colorComponents, float alpha) { this(pos, () -> type, colorComponents, alpha, DEFAULT_LINE_WIDTH); } - protected Waypoint(BlockPos pos, Supplier<Type> typeSupplier, float[] colorComponents, float alpha, float lineWidth) { + public Waypoint(BlockPos pos, Supplier<Type> typeSupplier, float[] colorComponents, float alpha, float lineWidth) { this(pos, typeSupplier, colorComponents, alpha, lineWidth, true); } @@ -35,11 +39,11 @@ public class Waypoint { this(pos, typeSupplier, colorComponents, DEFAULT_HIGHLIGHT_ALPHA, DEFAULT_LINE_WIDTH, throughWalls); } - protected Waypoint(BlockPos pos, Supplier<Type> typeSupplier, float[] colorComponents, float alpha, float lineWidth, boolean throughWalls) { + public Waypoint(BlockPos pos, Supplier<Type> typeSupplier, float[] colorComponents, float alpha, float lineWidth, boolean throughWalls) { this(pos, typeSupplier, colorComponents, alpha, lineWidth, throughWalls, true); } - protected Waypoint(BlockPos pos, Supplier<Type> typeSupplier, float[] colorComponents, float alpha, float lineWidth, boolean throughWalls, boolean shouldRender) { + public Waypoint(BlockPos pos, Supplier<Type> typeSupplier, float[] colorComponents, float alpha, float lineWidth, boolean throughWalls, boolean shouldRender) { this.pos = pos; this.box = new Box(pos); this.typeSupplier = typeSupplier; @@ -62,6 +66,10 @@ public class Waypoint { this.shouldRender = true; } + public void toggle() { + this.shouldRender = !this.shouldRender; + } + protected float[] getColorComponents() { return colorComponents; } diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index 49c446df..d6f09de9 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -14,6 +14,7 @@ "text.autoconfig.skyblocker.title": "Skyblocker Settings", "text.autoconfig.skyblocker.category.general": "General", + "text.autoconfig.skyblocker.option.general.enableTips": "Enable Tips", "text.autoconfig.skyblocker.option.general.bars": "Health, Mana, Defence & XP Bars", "text.autoconfig.skyblocker.option.general.bars.enableBars": "Enable Bars", "text.autoconfig.skyblocker.option.general.bars.barpositions": "Configure Bar Positions", @@ -165,9 +166,9 @@ "text.autoconfig.skyblocker.option.locations.spidersDen.relics.highlightFoundRelics": "Highlight found relics", "text.autoconfig.skyblocker.option.locations.dungeons": "Dungeons", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints": "Dungeon Secret Waypoints", + "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableRoomMatching": "Enable Room Matching", + "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableRoomMatching.@Tooltip": "Disabling this option can save around 20 MB of RAM, but Secret Waypoint and §lsome puzzle solvers §rrequire this option to be enabled.", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableSecretWaypoints": "Enable Dungeon Secret Waypoints", - "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints": "Do Not Initialize Secret Waypoints", - "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints.@Tooltip": "This option can save around 20 MB of ram if enabled, but Secret Waypoint will require a restart after turning off this option to work.", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.showSecretText": "Show Secret Text", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableEntranceWaypoints" : "Enable Entrance Waypoints", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableSuperboomWaypoints" : "Enable Superboom Waypoints", @@ -189,6 +190,15 @@ "text.autoconfig.skyblocker.option.locations.dungeons.doorHighlight.doorHighlightType": "Door Highlight Type", "text.autoconfig.skyblocker.option.locations.dungeons.doorHighlight.doorHighlightType.@Tooltip": "Highlight: Only displays a highlight.\n\nOutlined Highlight: Displays both a highlight and an outline.\n\nOutline: Only displays an outline.", "text.autoconfig.skyblocker.option.locations.dungeons.doorHighlight.doorHighlightType.secretWaypointsNote": "\n\n\nNote: Dungeon Secret Waypoints must be enabled for this to work.", + "text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore": "Dungeon Score", + "text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreMessage": "Enable Dungeon Score %d Message", + "text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreMessage.@Tooltip": "Sends a message in chat when reaching %d score in dungeons.", + "text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreTitle": "Enable Dungeon Score %d Title", + "text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreTitle.@Tooltip": "Display a title when reaching %d score in dungeons.", + "text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreSound": "Enable Dungeon Score %d Sound", + "text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.enableDungeonScoreSound.@Tooltip": "Plays a sound when reaching %d score in dungeons.", + "text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.dungeonScoreMessage": "Dungeon Score %d Message", + "text.autoconfig.skyblocker.option.locations.dungeons.dungeonScore.dungeonScoreMessage.@Tooltip": "Message which will be sent in the chat when reaching %d score in dungeons. The string \"[score]\" will be replaced with the dungeon score (%d).", "text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit": "Dungeon Chest Profit Calculator", "text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.enableProfitCalculator": "Enable Profit Calculator", "text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.enableProfitCalculator.@Tooltip": "Displays the profit of a dungeon chest in the chest screen's title.\nGreen if there's profit.\nRed if there isn't profit.\nGray if you don't gain or lose anything.\nBlue if calculations were based on incomplete data.", @@ -218,11 +228,15 @@ "text.autoconfig.skyblocker.option.locations.dungeons.solveTrivia": "Solve Trivia Puzzle", "text.autoconfig.skyblocker.option.locations.dungeons.solveTicTacToe": "Solve Tic Tac Toe Puzzle", "text.autoconfig.skyblocker.option.locations.dungeons.solveTicTacToe.@Tooltip": "Puts a red box around the next best move for you to make!", + "text.autoconfig.skyblocker.option.locations.dungeons.solveWaterboard": "Solve Waterboard Puzzle", + "text.autoconfig.skyblocker.option.locations.dungeons.solveWaterboard.@Tooltip": "Click the levers with green boxes to solve the puzzle.", "text.autoconfig.skyblocker.option.locations.dungeons.lividColor": "Livid Color", "text.autoconfig.skyblocker.option.locations.dungeons.lividColor.enableLividColorGlow": "Enable Livid Color Glow", "text.autoconfig.skyblocker.option.locations.dungeons.lividColor.enableLividColorGlow.@Tooltip": "Applies the glowing effect to the correct Livid in F5/M5.", "text.autoconfig.skyblocker.option.locations.dungeons.lividColor.enableLividColorText": "Enable Livid Color Text", "text.autoconfig.skyblocker.option.locations.dungeons.lividColor.enableLividColorText.@Tooltip": "Send the livid color in the chat during the Livid boss fight.", + "text.autoconfig.skyblocker.option.locations.dungeons.lividColor.enableLividColorTitle": "Enable Livid Color Title", + "text.autoconfig.skyblocker.option.locations.dungeons.lividColor.enableLividColorTitle.@Tooltip": "Display the livid color in the title during the Livid boss fight.", "text.autoconfig.skyblocker.option.locations.dungeons.lividColor.lividColorText": "Livid Color Text", "text.autoconfig.skyblocker.option.locations.dungeons.lividColor.lividColorText.@Tooltip": "Text which will be sent in the chat during the Livid boss fight. The string \"[color]\" will be replaced with the livid color.", "text.autoconfig.skyblocker.option.locations.dungeons.terminals": "Terminal Solvers", @@ -274,6 +288,8 @@ "text.autoconfig.skyblocker.option.messages.hideMana.@Tooltip": "Gives a better experience with FancyBar", "text.autoconfig.skyblocker.option.messages.hideShowOff": "Hide Show Off Messages", "text.autoconfig.skyblocker.option.messages.hideShowOff.@Tooltip": "Filters messages from the /show command", + "text.autoconfig.skyblocker.option.messages.hideToggleSkyMall": "Hide Toggle Sky Mall Messages", + "text.autoconfig.skyblocker.option.messages.hideToggleSkyMall.@Tooltip": "Hides those pesky messages telling you to disable the Sky Mall HOTM perk when you want it enabled!", "text.autoconfig.skyblocker.category.slayer": "Slayers", "text.autoconfig.skyblocker.option.slayer.vampireSlayer": "Vampire Slayer", "text.autoconfig.skyblocker.option.slayer.vampireSlayer.enableEffigyWaypoints": "Enable Effigy Waypoints", @@ -295,6 +311,8 @@ "text.autoconfig.skyblocker.option.general.hideEmptyTooltips": "Hide empty item tooltips in menus", "text.autoconfig.skyblocker.option.general.hideStatusEffectOverlay": "Hide Status Effect Overlay", + "text.autoconfig.skyblocker.option.general.dontStripSkinAlphaValues": "Correct Transparent Skin Pixels", + "text.autoconfig.skyblocker.option.general.dontStripSkinAlphaValues.@Tooltip": "When enabled, the alpha values of pixels in skin textures are no longer stripped while in Skyblock.\n\nThis results in the \"filler\" pixels on items using Player Heads to now be completely transparent, although this may have some side effects in odd cases.", "skyblocker.updaterepository.failed": "§cUpdating local repository failed. Remove files manually and restart game.", @@ -303,7 +321,7 @@ "skyblocker.dungeons.secrets.markSecretMissing": "§rMarked secret #%d as missing.", "skyblocker.dungeons.secrets.markSecretFoundUnable": "§cUnable to mark secret #%d as found.", "skyblocker.dungeons.secrets.markSecretMissingUnable": "§cUnable to mark secret #%d as missing.", - "skyblocker.dungeons.secrets.posMessage": "§rRoom: %s, X: %d, Y: %d, Z: %d", + "skyblocker.dungeons.secrets.posMessage": "§rRoom: %s, Direction: %s, X: %d, Y: %d, Z: %d", "skyblocker.dungeons.secrets.noTarget": "§cNo target block found! (Are you pointing at a block in range?)", "skyblocker.dungeons.secrets.notMatched": "§cThe current room is not matched! (Are you in a dungeon room?)", "skyblocker.dungeons.secrets.customWaypointAdded": "§rAdded a custom waypoint at X: %d, Y: %d, Z: %d for room %s secret #%d of category %s with name '%s'.", @@ -377,5 +395,30 @@ "skyblocker.itemProtection.noItemUuid": "§cYou must be holding an item that has a uuid in order to protect it!", "skyblocker.itemProtection.unableToProtect": "§cUnable to protect this item :( (Are you in skyblock?, are you holding an item?)", + "skyblocker.tips.enabled": "§aEnabled Tips.", + "skyblocker.tips.disabled": "§cDisabled Tips.", + "skyblocker.tips.clickEnable": "§a[Click to Enable Tips]", + "skyblocker.tips.clickDisable": "§c[Click to Disable Tips]", + "skyblocker.tips.clickNextTip": "§a[Click for Next Tip]", + "skyblocker.tips.tip": "§aTip: %s\n", + "skyblocker.tips.customItemNames": "Customize the names of your items with /skyblocker custom renameItem", + "skyblocker.tips.customArmorDyeColors": "Apply a custom dye color to your leather armour with /skyblocker custom dyeColor", + "skyblocker.tips.customArmorTrims": "You can set custom armor trims on your armor using /skyblocker custom armorTrim.", + "skyblocker.tips.fancyTabExtraInfo": "Did you know you can see extra info on our fancy tab menu when holding N or M?\n", + "skyblocker.tips.helpCommand": "Use command /skyblocker help and you might find some more nifty features!", + "skyblocker.tips.discordRichPresence": "Use Discord Rich Presence to show your friends how loaded you are!", + "skyblocker.tips.customDungeonSecretWaypoints": "You can add custom secret waypoints to any dungeon room with /skyblocker dungeons secrets addWaypoint.", + "skyblocker.tips.shortcuts": "Use /skyblocker shortcuts to create and edit command and message shortcuts.", + "skyblocker.tips.gallery": "Check out https://hysky.de/skyblocker/gallery for pictures of the mod's features in action!", + "skyblocker.tips.itemRarityBackground": "See an item's rarity easily with Item Rarity Backgrounds in the config's Item Info Display section.", + "skyblocker.tips.modMenuUpdate": "ModMenu will let you know if there's an update available for Skyblocker for your game version.", + "skyblocker.tips.issues": "Submit bug reports and feature requests to https://github.com/SkyblockerMod/Skyblocker.", + "skyblocker.tips.beta": "We often have beta versions available from GitHub Actions that contain new and experimental features.", + "skyblocker.tips.discord": "Join our discord at https://discord.gg/aNNJHQykck to keep up with the latest news about Skyblocker!", + "skyblocker.tips.flameOverlay": "Find that the flame overlay takes up too much screen space? Check out the config to make it smaller", + "skyblocker.tips.wikiLookup": "Press F4 while hovering over an item to open its wiki page in your web browser.", + "skyblocker.tips.fairySoulsEnigmaSoulsRelics": "Don't know where to find Fairy Souls, Enigma Souls, or Relics? Enable the helpers to aid your exploration, they'll remember which souls you've already found.", + "skyblocker.tips.quickNav": "You can customize the QuickNav buttons in the config.", + "emi.category.skyblocker.skyblock": "Skyblock" }
\ No newline at end of file diff --git a/src/main/resources/assets/skyblocker/lang/pt_br.json b/src/main/resources/assets/skyblocker/lang/pt_br.json index f14b8684..11242adf 100644 --- a/src/main/resources/assets/skyblocker/lang/pt_br.json +++ b/src/main/resources/assets/skyblocker/lang/pt_br.json @@ -239,7 +239,6 @@ "text.autoconfig.skyblocker.option.locations.spidersDen": "Covil da Aranha", "text.autoconfig.skyblocker.option.locations.barn.solveTreasureHunter": "Guia sobre Treasure Hunter", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableSecretWaypoints": "Ativar marcadores de segredos para dungeon", - "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints": "Não iniciar marcadores de segredos", "text.autoconfig.skyblocker.option.general.itemInfoDisplay.itemRarityBackgroundStyle.@Tooltip": "Escolha entre um estilo de fundo circular ou quadrado!", "text.autoconfig.skyblocker.option.general.itemInfoDisplay.itemRarityBackgroundStyle": "Estilo de fundo da raridade do item", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints": "Marcadores de segredos da dungeon", @@ -281,6 +280,5 @@ "text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.includeKismet.@Tooltip": "Se ativado, o preço de uma Kismet usada será subtraído no cálculo da margem de lucro", "text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.includeEssence": "Incluir essência", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.showSecretText": "Exibir texto secreto", - "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints.@Tooltip": "Essa opção pode salvar por volta de 20 MB de ram se ativada, mas os marcadores de segredos vão precisar de um reinício do jogo após desligar essa opção para funcionar.", "text.autoconfig.skyblocker.option.quickNav.button.clickEvent": "Evento ao clicar" } diff --git a/src/main/resources/assets/skyblocker/lang/zh_cn.json b/src/main/resources/assets/skyblocker/lang/zh_cn.json index 83317c00..a268d5a5 100644 --- a/src/main/resources/assets/skyblocker/lang/zh_cn.json +++ b/src/main/resources/assets/skyblocker/lang/zh_cn.json @@ -134,10 +134,8 @@ "text.autoconfig.skyblocker.option.locations.dungeons.solveTicTacToe": "井字棋谜题助手", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints": "地牢秘密路径点", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableSecretWaypoints": "启用地牢秘密路径点", - "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints": "不对地牢秘密路径点进行初始化", "text.autoconfig.skyblocker.option.general.tabHud.nameSorting.@Tooltip": "\"Alphabetical\" 以词典序排列玩家, 而 \"Default\" 以Hypixel默认顺序排列", "text.autoconfig.skyblocker.option.locations.dungeons.solveTicTacToe.@Tooltip": "以红色方块标记井字棋的下一步!", - "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints.@Tooltip": "此选项可节约20MB左右的内存, 但秘密路径点需要关闭此选项并重启后才能使用。", "text.autoconfig.skyblocker.option.general.quiverWarning.enableQuiverWarningInDungeons": "在地牢内启用箭袋提示", "text.autoconfig.skyblocker.option.general.quiverWarning": "箭袋提示", "text.autoconfig.skyblocker.option.general.quiverWarning.enableQuiverWarning": "启用箭袋提示", diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 717c72c2..6b0b4538 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -18,9 +18,6 @@ "client": [ "de.hysky.skyblocker.SkyblockerMod" ], - "preLaunch": [ - "dev.cbyrne.betterinject.BetterInject::initialize" - ], "modmenu": [ "de.hysky.skyblocker.compatibility.modmenu.ModMenuEntry" ], @@ -34,10 +31,10 @@ "mixins": [ "skyblocker.mixins.json" ], - "accesswidener": "skyblocker.accesswidener", + "accessWidener": "skyblocker.accesswidener", "depends": { "fabricloader": ">=0.15.0", - "fabric-api": ">=0.91.1+1.20.3", + "fabric-api": ">=0.92.0+1.20.4", "yet_another_config_lib_v3": ">=3.3.1+1.20.4", "minecraft": "~1.20.3" }, diff --git a/src/main/resources/skyblocker.mixins.json b/src/main/resources/skyblocker.mixins.json index d7ae26e1..22480cc7 100644 --- a/src/main/resources/skyblocker.mixins.json +++ b/src/main/resources/skyblocker.mixins.json @@ -8,10 +8,11 @@ "ArmorTrimMixin", "BatEntityMixin", "ClientPlayerEntityMixin", - "ClientPlayerInteractionManagerMixin", "ClientPlayNetworkHandlerMixin", + "DataTrackerMixin", "DrawContextMixin", "DyeableItemMixin", + "EntityRenderDispatcherMixin", "FarmlandBlockMixin", "GenericContainerScreenHandlerMixin", "HandledScreenMixin", @@ -21,9 +22,11 @@ "ItemMixin", "ItemStackMixin", "LeverBlockMixin", + "LivingEntityRendererMixin", "MinecraftClientMixin", "PlayerListHudMixin", "PlayerSkinProviderMixin", + "PlayerSkinTextureMixin", "ScoreboardMixin", "SocialInteractionsPlayerListWidgetMixin", "WorldRendererMixin", @@ -33,6 +36,7 @@ "accessor.DrawContextInvoker", "accessor.FrustumInvoker", "accessor.HandledScreenAccessor", + "accessor.ItemStackAccessor", "accessor.PlayerListHudAccessor", "accessor.RecipeBookWidgetAccessor", "accessor.ScreenAccessor", diff --git a/src/test/java/de/hysky/skyblocker/MixinsTest.java b/src/test/java/de/hysky/skyblocker/MixinsTest.java new file mode 100644 index 00000000..0aaf6bed --- /dev/null +++ b/src/test/java/de/hysky/skyblocker/MixinsTest.java @@ -0,0 +1,28 @@ +package de.hysky.skyblocker; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.mixin.transformer.IMixinTransformer; + +import net.minecraft.Bootstrap; +import net.minecraft.SharedConstants; + +public class MixinsTest { + + @BeforeAll + public static void setupEnvironment() { + SharedConstants.createGameVersion(); + Bootstrap.initialize(); + } + + @Test + public void auditMixins() { + //Ensure that the transformer is active so that the Mixins can be audited + Assertions.assertInstanceOf(IMixinTransformer.class, MixinEnvironment.getCurrentEnvironment().getActiveTransformer()); + + //If this fails check the report to get the full stack trace + MixinEnvironment.getCurrentEnvironment().audit(); + } +} diff --git a/src/test/java/de/hysky/skyblocker/skyblock/ChestValueTest.java b/src/test/java/de/hysky/skyblocker/skyblock/ChestValueTest.java index 261ab517..a8c4975c 100644 --- a/src/test/java/de/hysky/skyblocker/skyblock/ChestValueTest.java +++ b/src/test/java/de/hysky/skyblocker/skyblock/ChestValueTest.java @@ -1,21 +1,18 @@ package de.hysky.skyblocker.skyblock; -import de.hysky.skyblocker.config.SkyblockerConfig; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class ChestValueTest { @Test void testProfitText() { - SkyblockerConfig.DungeonChestProfit dCPConfig = new SkyblockerConfig.DungeonChestProfit(); - Assertions.assertEquals("literal{ 0 Coins}[style={color=dark_gray}]", ChestValue.getProfitText(0, false, dCPConfig.neutralThreshold, dCPConfig.neutralColor, dCPConfig.profitColor, dCPConfig.lossColor, dCPConfig.incompleteColor).toString()); - Assertions.assertEquals("literal{ 0 Coins}[style={color=blue}]", ChestValue.getProfitText(0, true, dCPConfig.neutralThreshold, dCPConfig.neutralColor, dCPConfig.profitColor, dCPConfig.lossColor, dCPConfig.incompleteColor).toString()); - Assertions.assertEquals("literal{ +10,000 Coins}[style={color=dark_green}]", ChestValue.getProfitText(10000, false, dCPConfig.neutralThreshold, dCPConfig.neutralColor, dCPConfig.profitColor, dCPConfig.lossColor, dCPConfig.incompleteColor).toString()); - Assertions.assertEquals("literal{ +10,000 Coins}[style={color=blue}]", ChestValue.getProfitText(10000, true, dCPConfig.neutralThreshold, dCPConfig.neutralColor, dCPConfig.profitColor, dCPConfig.lossColor, dCPConfig.incompleteColor).toString()); - Assertions.assertEquals("literal{ -10,000 Coins}[style={color=red}]", ChestValue.getProfitText(-10000, false, dCPConfig.neutralThreshold, dCPConfig.neutralColor, dCPConfig.profitColor, dCPConfig.lossColor, dCPConfig.incompleteColor).toString()); - Assertions.assertEquals("literal{ -10,000 Coins}[style={color=blue}]", ChestValue.getProfitText(-10000, true, dCPConfig.neutralThreshold, dCPConfig.neutralColor, dCPConfig.profitColor, dCPConfig.lossColor, dCPConfig.incompleteColor).toString()); - SkyblockerConfig.ChestValue cVConfig = new SkyblockerConfig.ChestValue(); - Assertions.assertEquals("literal{ 10,000 Coins}[style={color=dark_green}]", ChestValue.getValueText(10000, false, cVConfig.color, cVConfig.incompleteColor).toString()); - Assertions.assertEquals("literal{ 10,000 Coins}[style={color=blue}]", ChestValue.getValueText(10000, true, cVConfig.color, cVConfig.incompleteColor).toString()); + Assertions.assertEquals("literal{ 0 Coins}[style={color=dark_gray}]", ChestValue.getProfitText(0, false).toString()); + Assertions.assertEquals("literal{ 0 Coins}[style={color=blue}]", ChestValue.getProfitText(0, true).toString()); + Assertions.assertEquals("literal{ +10,000 Coins}[style={color=dark_green}]", ChestValue.getProfitText(10000, false).toString()); + Assertions.assertEquals("literal{ +10,000 Coins}[style={color=blue}]", ChestValue.getProfitText(10000, true).toString()); + Assertions.assertEquals("literal{ -10,000 Coins}[style={color=red}]", ChestValue.getProfitText(-10000, false).toString()); + Assertions.assertEquals("literal{ -10,000 Coins}[style={color=blue}]", ChestValue.getProfitText(-10000, true).toString()); + Assertions.assertEquals("literal{ 10,000 Coins}[style={color=dark_green}]", ChestValue.getValueText(10000, false).toString()); + Assertions.assertEquals("literal{ 10,000 Coins}[style={color=blue}]", ChestValue.getValueText(10000, true).toString()); } } diff --git a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdosTest.java b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdosTest.java index 3772fd75..22683698 100644 --- a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdosTest.java +++ b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdosTest.java @@ -1,4 +1,4 @@ -package de.hysky.skyblocker.skyblock.dungeon; +package de.hysky.skyblocker.skyblock.dungeon.puzzle; import de.hysky.skyblocker.utils.chat.ChatPatternListenerTest; import org.junit.jupiter.api.Test; diff --git a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/TriviaTest.java b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TriviaTest.java index 1df5a8e1..55a59a68 100644 --- a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/TriviaTest.java +++ b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TriviaTest.java @@ -1,4 +1,4 @@ -package de.hysky.skyblocker.skyblock.dungeon; +package de.hysky.skyblocker.skyblock.dungeon.puzzle; import de.hysky.skyblocker.utils.chat.ChatPatternListenerTest; import org.junit.jupiter.api.Test; diff --git a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonRoomsDFU.java b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonRoomsDFU.java index 3d2993cf..d45a2172 100644 --- a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonRoomsDFU.java +++ b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonRoomsDFU.java @@ -21,7 +21,7 @@ import java.util.zip.InflaterInputStream; * Utility class to convert the old dungeon rooms data from Dungeon Rooms Mod to a new format. * The new format is similar to <a href="https://quantizr.github.io/posts/how-it-works/">DRM's format</a>, but uses ints instead of longs and a custom numeric block id to store the block states. * The first byte is the x position, the second byte is the y position, the third byte is the z position, and the fourth byte is the custom numeric block id. - * Use {@link DungeonSecrets#NUMERIC_ID} to get the custom numeric block id of a block. + * Use {@link DungeonManager#NUMERIC_ID} to get the custom numeric block id of a block. * Run this manually when updating dungeon rooms data with DRM's data in {@code src/test/resources/assets/skyblocker/dungeons/dungeonrooms}. */ public class DungeonRoomsDFU { @@ -131,7 +131,7 @@ public class DungeonRoomsDFU { if (newId == null) { newId = ItemIdFix.fromId(oldId / 100); } - return x << 24 | y << 16 | z << 8 | DungeonSecrets.NUMERIC_ID.getByte(newId); + return x << 24 | y << 16 | z << 8 | DungeonManager.NUMERIC_ID.getByte(newId); } private static CompletableFuture<Void> save() { diff --git a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypointTest.java b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypointTest.java index 4c81bfb4..12bfe98b 100644 --- a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypointTest.java +++ b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypointTest.java @@ -4,8 +4,11 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.mojang.serialization.JsonOps; +import net.minecraft.Bootstrap; +import net.minecraft.SharedConstants; import net.minecraft.util.math.BlockPos; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.util.List; @@ -13,47 +16,52 @@ import java.util.List; public class SecretWaypointTest { private final Gson gson = new Gson(); - //These tests throw java.lang.NoClassDefFoundError - /*@Test + @BeforeAll + public static void setup() { + SharedConstants.createGameVersion(); + Bootstrap.initialize(); + } + + @Test void testCodecSerialize() { - SecretWaypoint waypoint = new SecretWaypoint(0, SecretWaypoint.Category.DEFAULT, "name", BlockPos.ORIGIN); + SecretWaypoint waypoint = new SecretWaypoint(0, SecretWaypoint.Category.DEFAULT, "name", new BlockPos(-1, 0, 1)); JsonElement json = SecretWaypoint.CODEC.encodeStart(JsonOps.INSTANCE, waypoint).result().orElseThrow(); - String expectedJson = "{\"secretIndex\":0,\"category\":\"default\",\"name\":{\"text\":\"name\"},\"pos\":[0,0,0]}"; + String expectedJson = "{\"secretIndex\":0,\"category\":\"default\",\"name\":\"name\",\"pos\":[-1,0,1]}"; Assertions.assertEquals(expectedJson, json.toString()); } @Test void testCodecDeserialize() { - String json = "{\"secretIndex\":0,\"category\":\"default\",\"name\":{\"text\":\"name\"},\"pos\":[0,0,0]}"; + String json = "{\"secretIndex\":0,\"category\":\"default\",\"name\":\"name\",\"pos\":[-1,0,1]}"; SecretWaypoint waypoint = SecretWaypoint.CODEC.parse(JsonOps.INSTANCE, gson.fromJson(json, JsonElement.class)).result().orElseThrow(); - SecretWaypoint expectedWaypoint = new SecretWaypoint(0, SecretWaypoint.Category.DEFAULT, "name", BlockPos.ORIGIN); + SecretWaypoint expectedWaypoint = new SecretWaypoint(0, SecretWaypoint.Category.DEFAULT, "name", new BlockPos(-1, 0, 1)); - equal(expectedWaypoint, waypoint); + Assertions.assertEquals(expectedWaypoint, waypoint); } @Test void testListCodecSerialize() { - List<SecretWaypoint> waypoints = List.of(new SecretWaypoint(0, SecretWaypoint.Category.DEFAULT, "name", BlockPos.ORIGIN), new SecretWaypoint(1, SecretWaypoint.Category.CHEST, "name", new BlockPos(-1, 0, 1))); + List<SecretWaypoint> waypoints = List.of(new SecretWaypoint(0, SecretWaypoint.Category.DEFAULT, "name", new BlockPos(-1, 0, 1)), new SecretWaypoint(1, SecretWaypoint.Category.CHEST, "name", new BlockPos(2, 0, -2))); JsonElement json = SecretWaypoint.LIST_CODEC.encodeStart(JsonOps.INSTANCE, waypoints).result().orElseThrow(); - String expectedJson = "[{\"secretIndex\":0,\"category\":\"default\",\"name\":{\"text\":\"name\"},\"pos\":[0,0,0]},{\"secretIndex\":1,\"category\":\"chest\",\"name\":{\"text\":\"name\"},\"pos\":[-1,0,1]}]"; + String expectedJson = "[{\"secretIndex\":0,\"category\":\"default\",\"name\":\"name\",\"pos\":[-1,0,1]},{\"secretIndex\":1,\"category\":\"chest\",\"name\":\"name\",\"pos\":[2,0,-2]}]"; Assertions.assertEquals(expectedJson, json.toString()); } @Test void testListCodecDeserialize() { - String json = "[{\"secretIndex\":0,\"category\":\"default\",\"name\":{\"text\":\"name\"},\"pos\":[0,0,0]},{\"secretIndex\":1,\"category\":\"chest\",\"name\":{\"text\":\"name\"},\"pos\":[-1,0,1]}]"; + String json = "[{\"secretIndex\":0,\"category\":\"default\",\"name\":\"name\",\"pos\":[-1,0,1]},{\"secretIndex\":1,\"category\":\"chest\",\"name\":\"name\",\"pos\":[2,0,-2]}]"; List<SecretWaypoint> waypoints = SecretWaypoint.LIST_CODEC.parse(JsonOps.INSTANCE, gson.fromJson(json, JsonElement.class)).result().orElseThrow(); - List<SecretWaypoint> expectedWaypoints = List.of(new SecretWaypoint(0, SecretWaypoint.Category.DEFAULT, "name", BlockPos.ORIGIN), new SecretWaypoint(1, SecretWaypoint.Category.CHEST, "name", new BlockPos(-1, 0, 1))); + List<SecretWaypoint> expectedWaypoints = List.of(new SecretWaypoint(0, SecretWaypoint.Category.DEFAULT, "name", new BlockPos(-1, 0, 1)), new SecretWaypoint(1, SecretWaypoint.Category.CHEST, "name", new BlockPos(2, 0, -2))); Assertions.assertEquals(expectedWaypoints.size(), waypoints.size()); for (int i = 0; i < expectedWaypoints.size(); i++) { SecretWaypoint expectedWaypoint = expectedWaypoints.get(i); SecretWaypoint waypoint = waypoints.get(i); - equal(expectedWaypoint, waypoint); + Assertions.assertEquals(expectedWaypoint, waypoint); } - }*/ + } @Test void testGetCategory() { @@ -70,11 +78,4 @@ public class SecretWaypointTest { SecretWaypoint.Category category = SecretWaypoint.Category.get(waypointJson); Assertions.assertEquals(SecretWaypoint.Category.DEFAULT, category); } - - private static void equal(SecretWaypoint expectedWaypoint, SecretWaypoint waypoint) { - Assertions.assertEquals(expectedWaypoint.secretIndex, waypoint.secretIndex); - Assertions.assertEquals(expectedWaypoint.category, waypoint.category); - Assertions.assertEquals(expectedWaypoint.name, waypoint.name); - Assertions.assertEquals(expectedWaypoint.pos, waypoint.pos); - } } diff --git a/src/test/java/de/hysky/skyblocker/utils/ItemUtilsTest.java b/src/test/java/de/hysky/skyblocker/utils/ItemUtilsTest.java new file mode 100644 index 00000000..71ff29ef --- /dev/null +++ b/src/test/java/de/hysky/skyblocker/utils/ItemUtilsTest.java @@ -0,0 +1,64 @@ +package de.hysky.skyblocker.utils; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.Bootstrap; +import net.minecraft.SharedConstants; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.StringNbtReader; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class ItemUtilsTest { + private final ItemStack JUJU_SHORTBOW_OLD = ItemStack.fromNbt(StringNbtReader.parse("{Count:1b,id:\"minecraft:bow\",tag:{Damage:0,Enchantments:[{id:\"minecraft:protection\",lvl:0s}],ExtraAttributes:{art_of_war_count:1,dungeon_item_level:5,enchantments:{aiming:5,chance:4,cubism:5,impaling:3,infinite_quiver:10,overload:5,piercing:1,power:6,snipe:3,telekinesis:1,ultimate_soul_eater:5},hot_potato_count:15,id:\"JUJU_SHORTBOW\",modifier:\"spiritual\",originTag:\"QUICK_CRAFTING\",rarity_upgrades:1,runes:{DRAGON:3},stats_book:54778,timestamp:\"6/5/21 4:41 AM\",uuid:\"b06b8fe2-470a-43f3-b844-658472f20996\"},HideFlags:255,Unbreakable:1b,display:{Lore:['{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Gear Score: \"},{\"color\":\"light_purple\",\"text\":\"724 \"},{\"color\":\"dark_gray\",\"text\":\"(2297)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Damage: \"},{\"color\":\"red\",\"text\":\"+371 \"},{\"color\":\"yellow\",\"text\":\"(+30) \"},{\"color\":\"dark_gray\",\"text\":\"(+1,275)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Strength: \"},{\"color\":\"red\",\"text\":\"+107 \"},{\"color\":\"yellow\",\"text\":\"(+30) \"},{\"color\":\"gold\",\"text\":\"[+5] \"},{\"color\":\"blue\",\"text\":\"(+28) \"},{\"color\":\"dark_gray\",\"text\":\"(+386.25)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Crit Chance: \"},{\"color\":\"red\",\"text\":\"+28% \"},{\"color\":\"blue\",\"text\":\"(+12%) \"},{\"color\":\"dark_gray\",\"text\":\"(+40.5%)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Crit Damage: \"},{\"color\":\"red\",\"text\":\"+181% \"},{\"color\":\"blue\",\"text\":\"(+55%) \"},{\"color\":\"dark_gray\",\"text\":\"(+637.5%)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Shot Cooldown: \"},{\"color\":\"green\",\"text\":\"0.5s\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"bold\":true,\"color\":\"light_purple\",\"text\":\"\"},{\"bold\":true,\"color\":\"light_purple\",\"text\":\"Soul Eater V\"},{\"color\":\"blue\",\"text\":\", \"},{\"color\":\"blue\",\"text\":\"Chance IV\"},{\"color\":\"blue\",\"text\":\", \"},{\"color\":\"blue\",\"text\":\"Cubism V\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"blue\",\"text\":\"Dragon Tracer V\"},{\"color\":\"blue\",\"text\":\", \"},{\"color\":\"blue\",\"text\":\"Impaling III\"},{\"color\":\"blue\",\"text\":\", \"},{\"color\":\"blue\",\"text\":\"Infinite Quiver X\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"blue\",\"text\":\"Overload V\"},{\"color\":\"blue\",\"text\":\", \"},{\"color\":\"blue\",\"text\":\"Piercing I\"},{\"color\":\"blue\",\"text\":\", \"},{\"color\":\"blue\",\"text\":\"Power VI\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"blue\",\"text\":\"Snipe III\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"dark_purple\",\"text\":\"◆ End Rune III\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Hits \"},{\"color\":\"red\",\"text\":\"3 \"},{\"color\":\"gray\",\"text\":\"mobs on impact.\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Can damage endermen.\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"white\",\"text\":\"Kills: \"},{\"color\":\"gold\",\"text\":\"54,778\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"blue\",\"text\":\"Spiritual Bonus\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Grants a \"},{\"color\":\"green\",\"text\":\"10% \"},{\"color\":\"gray\",\"text\":\"chance to spawn\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"a Spirit Decoy when you kill an\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"enemy in a dungeon.\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gold\",\"text\":\"Shortbow: Instantly shoots!\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"\"},{\"color\":\"dark_red\",\"text\":\"☠ \"},{\"color\":\"red\",\"text\":\"Requires \"},{\"color\":\"dark_purple\",\"text\":\"Enderman Slayer\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"dark_purple\",\"text\":\"5\"},{\"color\":\"red\",\"text\":\".\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"bold\":true,\"obfuscated\":true,\"color\":\"gold\",\"text\":\"a\"},{\"text\":\"\"},{\"bold\":false,\"italic\":false,\"underlined\":false,\"obfuscated\":false,\"strikethrough\":false,\"extra\":[{\"text\":\" \"}],\"text\":\"\"},{\"bold\":true,\"color\":\"gold\",\"text\":\"\"},{\"bold\":true,\"color\":\"gold\",\"text\":\"LEGENDARY DUNGEON BOW \"},{\"bold\":true,\"obfuscated\":true,\"color\":\"gold\",\"text\":\"a\"}],\"text\":\"\"}'],Name:'{\"italic\":false,\"extra\":[{\"color\":\"gold\",\"text\":\"Spiritual Juju Shortbow \"},{\"color\":\"gold\",\"text\":\"✪\"},{\"color\":\"gold\",\"text\":\"✪\"},{\"color\":\"gold\",\"text\":\"✪\"},{\"color\":\"gold\",\"text\":\"✪\"},{\"color\":\"gold\",\"text\":\"✪\"}],\"text\":\"\"}'}}}")); + private final ItemStack JUJU_SHORTBOW = ItemStack.fromNbt(StringNbtReader.parse("{Count:1b,id:\"minecraft:bow\",tag:{Damage:0,Enchantments:[{id:\"minecraft:protection\",lvl:0s}],ExtraAttributes:{art_of_war_count:1,dungeon_item_level:5,enchantments:{aiming:5,chance:4,cubism:5,impaling:3,infinite_quiver:10,overload:5,piercing:1,power:6,snipe:3,telekinesis:1,ultimate_soul_eater:5},hot_potato_count:15,id:\"JUJU_SHORTBOW\",modifier:\"spiritual\",originTag:\"QUICK_CRAFTING\",rarity_upgrades:1,runes:{DRAGON:3},stats_book:54778,timestamp:1622882460000L,uuid:\"b06b8fe2-470a-43f3-b844-658472f20996\"},HideFlags:255,Unbreakable:1b,display:{Lore:['{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Gear Score: \"},{\"color\":\"light_purple\",\"text\":\"724 \"},{\"color\":\"dark_gray\",\"text\":\"(2297)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Damage: \"},{\"color\":\"red\",\"text\":\"+371 \"},{\"color\":\"yellow\",\"text\":\"(+30) \"},{\"color\":\"dark_gray\",\"text\":\"(+1,275)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Strength: \"},{\"color\":\"red\",\"text\":\"+107 \"},{\"color\":\"yellow\",\"text\":\"(+30) \"},{\"color\":\"gold\",\"text\":\"[+5] \"},{\"color\":\"blue\",\"text\":\"(+28) \"},{\"color\":\"dark_gray\",\"text\":\"(+386.25)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Crit Chance: \"},{\"color\":\"red\",\"text\":\"+28% \"},{\"color\":\"blue\",\"text\":\"(+12%) \"},{\"color\":\"dark_gray\",\"text\":\"(+40.5%)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Crit Damage: \"},{\"color\":\"red\",\"text\":\"+181% \"},{\"color\":\"blue\",\"text\":\"(+55%) \"},{\"color\":\"dark_gray\",\"text\":\"(+637.5%)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Shot Cooldown: \"},{\"color\":\"green\",\"text\":\"0.5s\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"bold\":true,\"color\":\"light_purple\",\"text\":\"\"},{\"bold\":true,\"color\":\"light_purple\",\"text\":\"Soul Eater V\"},{\"color\":\"blue\",\"text\":\", \"},{\"color\":\"blue\",\"text\":\"Chance IV\"},{\"color\":\"blue\",\"text\":\", \"},{\"color\":\"blue\",\"text\":\"Cubism V\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"blue\",\"text\":\"Dragon Tracer V\"},{\"color\":\"blue\",\"text\":\", \"},{\"color\":\"blue\",\"text\":\"Impaling III\"},{\"color\":\"blue\",\"text\":\", \"},{\"color\":\"blue\",\"text\":\"Infinite Quiver X\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"blue\",\"text\":\"Overload V\"},{\"color\":\"blue\",\"text\":\", \"},{\"color\":\"blue\",\"text\":\"Piercing I\"},{\"color\":\"blue\",\"text\":\", \"},{\"color\":\"blue\",\"text\":\"Power VI\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"blue\",\"text\":\"Snipe III\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"dark_purple\",\"text\":\"◆ End Rune III\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Hits \"},{\"color\":\"red\",\"text\":\"3 \"},{\"color\":\"gray\",\"text\":\"mobs on impact.\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Can damage endermen.\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gold\",\"text\":\"Shortbow: Instantly shoots!\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"white\",\"text\":\"Kills: \"},{\"color\":\"gold\",\"text\":\"54,778\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"blue\",\"text\":\"Spiritual Bonus\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Grants a \"},{\"color\":\"green\",\"text\":\"10% \"},{\"color\":\"gray\",\"text\":\"chance to spawn a\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Spirit Decoy when you kill an enemy\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"in a dungeon.\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"\"},{\"color\":\"dark_red\",\"text\":\"☠ \"},{\"color\":\"red\",\"text\":\"Requires \"},{\"color\":\"dark_purple\",\"text\":\"Enderman Slayer 5\"},{\"color\":\"red\",\"text\":\".\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"bold\":true,\"obfuscated\":true,\"color\":\"gold\",\"text\":\"a\"},{\"text\":\"\"},{\"bold\":false,\"italic\":false,\"underlined\":false,\"obfuscated\":false,\"strikethrough\":false,\"extra\":[{\"text\":\" \"}],\"text\":\"\"},{\"bold\":true,\"color\":\"gold\",\"text\":\"\"},{\"bold\":true,\"color\":\"gold\",\"text\":\"LEGENDARY DUNGEON BOW \"},{\"bold\":true,\"obfuscated\":true,\"color\":\"gold\",\"text\":\"a\"}],\"text\":\"\"}'],Name:'{\"italic\":false,\"extra\":[{\"color\":\"gold\",\"text\":\"Spiritual Juju Shortbow \"},{\"color\":\"gold\",\"text\":\"✪\"},{\"color\":\"gold\",\"text\":\"✪\"},{\"color\":\"gold\",\"text\":\"✪\"},{\"color\":\"gold\",\"text\":\"✪\"},{\"color\":\"gold\",\"text\":\"✪\"}],\"text\":\"\"}'}}}")); + private final ItemStack LIVID_DAGGER = ItemStack.fromNbt(StringNbtReader.parse("{Count:1b,id:\"minecraft:iron_sword\",tag:{Damage:0,Enchantments:[{id:\"minecraft:protection\",lvl:0s}],ExtraAttributes:{dungeon_item_level:5,enchantments:{telekinesis:1,ultimate_one_for_all:1},gems:{JASPER_0:\"FLAWLESS\"},hot_potato_count:15,id:\"LIVID_DAGGER\",modifier:\"fabled\",originTag:\"UNKNOWN\",rarity_upgrades:1,timestamp:\"2/20/21 3:09 PM\",uuid:\"214333db-d71c-4080-8487-00b4666bb9a4\"},HideFlags:255,Unbreakable:1b,display:{Lore:['{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Gear Score: \"},{\"color\":\"light_purple\",\"text\":\"933 \"},{\"color\":\"dark_gray\",\"text\":\"(2389)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Damage: \"},{\"color\":\"red\",\"text\":\"+261 \"},{\"color\":\"yellow\",\"text\":\"(+30) \"},{\"color\":\"dark_gray\",\"text\":\"(+900)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Strength: \"},{\"color\":\"red\",\"text\":\"+183 \"},{\"color\":\"yellow\",\"text\":\"(+30) \"},{\"color\":\"blue\",\"text\":\"(+75) \"},{\"color\":\"light_purple\",\"text\":\"(+12) \"},{\"color\":\"dark_gray\",\"text\":\"(+663.75)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Crit Chance: \"},{\"color\":\"red\",\"text\":\"+100% \"},{\"color\":\"dark_gray\",\"text\":\"(+150%)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Crit Damage: \"},{\"color\":\"red\",\"text\":\"+105% \"},{\"color\":\"blue\",\"text\":\"(+50%) \"},{\"color\":\"dark_gray\",\"text\":\"(+375%)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Bonus Attack Speed: \"},{\"color\":\"red\",\"text\":\"+55% \"},{\"color\":\"dark_gray\",\"text\":\"(+75%)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"text\":\" \"},{\"color\":\"dark_purple\",\"text\":\"[\"},{\"color\":\"light_purple\",\"text\":\"❁\"},{\"color\":\"dark_purple\",\"text\":\"]\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"bold\":true,\"color\":\"light_purple\",\"text\":\"\"},{\"bold\":true,\"color\":\"light_purple\",\"text\":\"One For All I\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gold\",\"text\":\"Ability: Throw \"},{\"bold\":true,\"color\":\"yellow\",\"text\":\"RIGHT CLICK\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Throw your dagger at your\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"enemies!\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"dark_gray\",\"text\":\"Mana Cost: \"},{\"color\":\"dark_aqua\",\"text\":\"150\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"dark_gray\",\"text\":\"Cooldown: \"},{\"color\":\"green\",\"text\":\"5s\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Your Critical Hits deal \"},{\"color\":\"blue\",\"text\":\"100%\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"blue\",\"text\":\"\"},{\"color\":\"gray\",\"text\":\"more damage if you are behind\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"your target.\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"blue\",\"text\":\"Fabled Bonus\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Critical hits have a chance to\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"deal up to \"},{\"color\":\"green\",\"text\":\"15% \"},{\"color\":\"gray\",\"text\":\"extra damage.\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"bold\":true,\"obfuscated\":true,\"color\":\"light_purple\",\"text\":\"a\"},{\"text\":\"\"},{\"bold\":false,\"italic\":false,\"underlined\":false,\"obfuscated\":false,\"strikethrough\":false,\"extra\":[{\"text\":\" \"}],\"text\":\"\"},{\"bold\":true,\"color\":\"light_purple\",\"text\":\"\"},{\"bold\":true,\"color\":\"light_purple\",\"text\":\"MYTHIC DUNGEON SWORD \"},{\"bold\":true,\"obfuscated\":true,\"color\":\"light_purple\",\"text\":\"a\"}],\"text\":\"\"}'],Name:'{\"italic\":false,\"extra\":[{\"color\":\"light_purple\",\"text\":\"Fabled Livid Dagger \"},{\"color\":\"gold\",\"text\":\"✪\"},{\"color\":\"gold\",\"text\":\"✪\"},{\"color\":\"gold\",\"text\":\"✪\"},{\"color\":\"gold\",\"text\":\"✪\"},{\"color\":\"gold\",\"text\":\"✪\"}],\"text\":\"\"}'}}}")); + private final ItemStack MITHRIL_DRILL_1 = ItemStack.fromNbt(StringNbtReader.parse("{Count:1b,id:\"minecraft:prismarine_shard\",tag:{Damage:21,Enchantments:[{id:\"minecraft:protection\",lvl:0s}],ExtraAttributes:{compact_blocks:97683,drill_fuel:2979,enchantments:{compact:7,efficiency:5,experience:3,fortune:3,telekinesis:1},id:\"MITHRIL_DRILL_1\",modifier:\"fleet\",originTag:\"UNKNOWN\",timestamp:\"1/30/21 5:52 AM\",uuid:\"575ce2c9-251e-4435-9020-de1a2e24b1d0\"},HideFlags:255,display:{Lore:['{\"italic\":false,\"extra\":[{\"color\":\"dark_gray\",\"text\":\"Breaking Power 5\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Damage: \"},{\"color\":\"red\",\"text\":\"+65\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Mining Speed: \"},{\"color\":\"green\",\"text\":\"+585 \"},{\"color\":\"blue\",\"text\":\"(+25)\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Mining Fortune: \"},{\"color\":\"green\",\"text\":\"+30\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Mining Wisdom: \"},{\"color\":\"green\",\"text\":\"+7\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"blue\",\"text\":\"Compact VII \"},{\"color\":\"dark_gray\",\"text\":\"97,683\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Gain \"},{\"color\":\"dark_aqua\",\"text\":\"+7☯ Mining Wisdom \"},{\"color\":\"gray\",\"text\":\"and a \"},{\"color\":\"green\",\"text\":\"0.4%\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"green\",\"text\":\"\"},{\"color\":\"gray\",\"text\":\"chance to drop an enchanted item.\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"dark_gray\",\"text\":\"150k blocks to tier up!\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"blue\",\"text\":\"Efficiency V\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Grants \"},{\"color\":\"green\",\"text\":\"+110 \"},{\"color\":\"gold\",\"text\":\"⸕ Mining Speed\"},{\"color\":\"gray\",\"text\":\".\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"blue\",\"text\":\"Experience III\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Grants a \"},{\"color\":\"green\",\"text\":\"37.5% \"},{\"color\":\"gray\",\"text\":\"chance for mobs and\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"ores to drop double experience.\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"blue\",\"text\":\"Fortune III\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Grants \"},{\"color\":\"gold\",\"text\":\"+30☘ Mining Fortune\"},{\"color\":\"gray\",\"text\":\", which\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"increases your chance for multiple\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"drops.\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Gain \"},{\"color\":\"green\",\"text\":\"+15% \"},{\"color\":\"dark_green\",\"text\":\"Mithril Powder\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"when using this Drill!\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Fuel Tank: \"},{\"color\":\"red\",\"text\":\"Not Installed\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"\"},{\"color\":\"gray\",\"text\":\"Increases fuel capacity with part\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"installed.\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Drill Engine: \"},{\"color\":\"red\",\"text\":\"Not Installed\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"\"},{\"color\":\"gray\",\"text\":\"Increases \"},{\"color\":\"gold\",\"text\":\"⸕ Mining Speed \"},{\"color\":\"gray\",\"text\":\"with part\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"installed.\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Upgrade Module: \"},{\"color\":\"red\",\"text\":\"Not Installed\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"\"},{\"color\":\"gray\",\"text\":\"Applies a passive upgrade with part\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"installed.\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Apply Drill Parts to this Drill by\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"talking to a \"},{\"color\":\"dark_green\",\"text\":\"Drill Mechanic\"},{\"color\":\"gray\",\"text\":\"!\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Fuel: \"},{\"color\":\"dark_green\",\"text\":\"2,979\"},{\"color\":\"dark_gray\",\"text\":\"/3k\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gold\",\"text\":\"Ability: Mining Speed Boost \"},{\"bold\":true,\"color\":\"yellow\",\"text\":\"RIGHT CLICK\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"Grants \"},{\"color\":\"green\",\"text\":\"+\"},{\"color\":\"green\",\"text\":\"200% \"},{\"color\":\"gold\",\"text\":\"⸕ Mining Speed \"},{\"color\":\"gray\",\"text\":\"for\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"gray\",\"text\":\"\"},{\"color\":\"green\",\"text\":\"15s\"},{\"color\":\"gray\",\"text\":\".\"}],\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"color\":\"dark_gray\",\"text\":\"Cooldown: \"},{\"color\":\"green\",\"text\":\"120s\"}],\"text\":\"\"}','{\"italic\":false,\"text\":\"\"}','{\"italic\":false,\"extra\":[{\"bold\":true,\"color\":\"blue\",\"text\":\"RARE DRILL\"}],\"text\":\"\"}'],Name:'{\"italic\":false,\"extra\":[{\"color\":\"blue\",\"text\":\"Fleet Mithril Drill SX-R226\"}],\"text\":\"\"}'}}}")); + + public ItemUtilsTest() throws CommandSyntaxException { + } + + @BeforeAll + public static void setup() { + SharedConstants.createGameVersion(); + Bootstrap.initialize(); + } + + @Test + void testGetExtraAttributes() { + Assertions.assertEquals("{art_of_war_count:1,dungeon_item_level:5,enchantments:{aiming:5,chance:4,cubism:5,impaling:3,infinite_quiver:10,overload:5,piercing:1,power:6,snipe:3,telekinesis:1,ultimate_soul_eater:5},hot_potato_count:15,id:\"JUJU_SHORTBOW\",modifier:\"spiritual\",originTag:\"QUICK_CRAFTING\",rarity_upgrades:1,runes:{DRAGON:3},stats_book:54778,timestamp:1622882460000L,uuid:\"b06b8fe2-470a-43f3-b844-658472f20996\"}", ItemUtils.getExtraAttributes(JUJU_SHORTBOW).toString()); + Assertions.assertEquals("{dungeon_item_level:5,enchantments:{telekinesis:1,ultimate_one_for_all:1},gems:{JASPER_0:\"FLAWLESS\"},hot_potato_count:15,id:\"LIVID_DAGGER\",modifier:\"fabled\",originTag:\"UNKNOWN\",rarity_upgrades:1,timestamp:\"2/20/21 3:09 PM\",uuid:\"214333db-d71c-4080-8487-00b4666bb9a4\"}", ItemUtils.getExtraAttributes(LIVID_DAGGER).toString()); + Assertions.assertEquals("{compact_blocks:97683,drill_fuel:2979,enchantments:{compact:7,efficiency:5,experience:3,fortune:3,telekinesis:1},id:\"MITHRIL_DRILL_1\",modifier:\"fleet\",originTag:\"UNKNOWN\",timestamp:\"1/30/21 5:52 AM\",uuid:\"575ce2c9-251e-4435-9020-de1a2e24b1d0\"}", ItemUtils.getExtraAttributes(MITHRIL_DRILL_1).toString()); + } + + @Test + void testGetItemId() { + Assertions.assertEquals("JUJU_SHORTBOW", ItemUtils.getItemId(JUJU_SHORTBOW)); + Assertions.assertEquals("LIVID_DAGGER", ItemUtils.getItemId(LIVID_DAGGER)); + Assertions.assertEquals("MITHRIL_DRILL_1", ItemUtils.getItemId(MITHRIL_DRILL_1)); + } + + @Test + void testGetItemUuid() { + Assertions.assertEquals("b06b8fe2-470a-43f3-b844-658472f20996", ItemUtils.getItemUuid(JUJU_SHORTBOW)); + Assertions.assertEquals("214333db-d71c-4080-8487-00b4666bb9a4", ItemUtils.getItemUuid(LIVID_DAGGER)); + Assertions.assertEquals("575ce2c9-251e-4435-9020-de1a2e24b1d0", ItemUtils.getItemUuid(MITHRIL_DRILL_1)); + } + + @Test + void testGetTimestamp() { + Assertions.assertEquals("June 5, 2021", ItemUtils.getTimestamp(JUJU_SHORTBOW_OLD)); + Assertions.assertEquals("June 5, 2021", ItemUtils.getTimestamp(JUJU_SHORTBOW)); + Assertions.assertEquals("February 20, 2021", ItemUtils.getTimestamp(LIVID_DAGGER)); + Assertions.assertEquals("January 30, 2021", ItemUtils.getTimestamp(MITHRIL_DRILL_1)); + } + + @Test + void testGetDurability() { + IntIntPair durability = ItemUtils.getDurability(MITHRIL_DRILL_1); + Assertions.assertNotNull(durability); + Assertions.assertEquals(durability.leftInt(), 2979); + Assertions.assertEquals(durability.rightInt(), 3000); + } +} diff --git a/src/test/java/de/hysky/skyblocker/utils/PosUtilsTest.java b/src/test/java/de/hysky/skyblocker/utils/PosUtilsTest.java new file mode 100644 index 00000000..1f433af3 --- /dev/null +++ b/src/test/java/de/hysky/skyblocker/utils/PosUtilsTest.java @@ -0,0 +1,17 @@ +package de.hysky.skyblocker.utils; + +import net.minecraft.util.math.BlockPos; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class PosUtilsTest { + @Test + void testParsePosString() { + Assertions.assertEquals(PosUtils.parsePosString("-1,0,1"), new BlockPos(-1, 0, 1)); + } + + @Test + void testGetPosString() { + Assertions.assertEquals(PosUtils.getPosString(new BlockPos(-1, 0, 1)), "-1,0,1"); + } +} |