diff options
Diffstat (limited to 'src/main/java')
71 files changed, 2036 insertions, 635 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; } |