diff --git a/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java index fa9bf37648..7b3f60d940 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java @@ -296,6 +296,53 @@ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig .build()) .build()) + //Smooth AOTE + .group(OptionGroup.createBuilder() + .name(Text.translatable("skyblocker.config.uiAndVisuals.smoothAOTE")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.smoothAOTE.@Tooltip"))) + .collapsed(true) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.uiAndVisuals.smoothAOTE.enableWeirdTransmission")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.smoothAOTE.enableWeirdTransmission.@Tooltip"))) + .binding(defaults.uiAndVisuals.smoothAOTE.enableWeirdTransmission, + () -> config.uiAndVisuals.smoothAOTE.enableWeirdTransmission, + newValue -> config.uiAndVisuals.smoothAOTE.enableWeirdTransmission = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.uiAndVisuals.smoothAOTE.enableInstantTransmission")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.smoothAOTE.enableInstantTransmission.@Tooltip"))) + .binding(defaults.uiAndVisuals.smoothAOTE.enableInstantTransmission, + () -> config.uiAndVisuals.smoothAOTE.enableInstantTransmission, + newValue -> config.uiAndVisuals.smoothAOTE.enableInstantTransmission = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.uiAndVisuals.smoothAOTE.enableEtherTransmission")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.smoothAOTE.enableEtherTransmission.@Tooltip"))) + .binding(defaults.uiAndVisuals.smoothAOTE.enableEtherTransmission, + () -> config.uiAndVisuals.smoothAOTE.enableEtherTransmission, + newValue -> config.uiAndVisuals.smoothAOTE.enableEtherTransmission = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.uiAndVisuals.smoothAOTE.enableSinrecallTransmission")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.smoothAOTE.enableSinrecallTransmission.@Tooltip"))) + .binding(defaults.uiAndVisuals.smoothAOTE.enableSinrecallTransmission, + () -> config.uiAndVisuals.smoothAOTE.enableSinrecallTransmission, + newValue -> config.uiAndVisuals.smoothAOTE.enableSinrecallTransmission = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.uiAndVisuals.smoothAOTE.enableWitherImpact")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.smoothAOTE.enableWitherImpact.@Tooltip"))) + .binding(defaults.uiAndVisuals.smoothAOTE.enableWitherImpact, + () -> config.uiAndVisuals.smoothAOTE.enableWitherImpact, + newValue -> config.uiAndVisuals.smoothAOTE.enableWitherImpact = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .build()) + //Search overlay .group(OptionGroup.createBuilder() .name(Text.translatable("skyblocker.config.uiAndVisuals.searchOverlay")) diff --git a/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java index 598cbf5498..4daad52538 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java @@ -55,6 +55,9 @@ public class UIAndVisualsConfig { @SerialEntry public TeleportOverlay teleportOverlay = new TeleportOverlay(); + @SerialEntry + public SmoothAOTE smoothAOTE = new SmoothAOTE(); + @SerialEntry public SearchOverlay searchOverlay = new SearchOverlay(); @@ -223,6 +226,24 @@ public static class TeleportOverlay { public boolean enableWitherImpact = true; } + public static class SmoothAOTE { + + @SerialEntry + public boolean enableWeirdTransmission = true; + + @SerialEntry + public boolean enableInstantTransmission = true; + + @SerialEntry + public boolean enableEtherTransmission = true; + + @SerialEntry + public boolean enableSinrecallTransmission = true; + + @SerialEntry + public boolean enableWitherImpact = true; + } + public static class SearchOverlay { @SerialEntry public boolean enableBazaar = true; diff --git a/src/main/java/de/hysky/skyblocker/mixins/CameraMixin.java b/src/main/java/de/hysky/skyblocker/mixins/CameraMixin.java new file mode 100644 index 0000000000..78d9e547fe --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/CameraMixin.java @@ -0,0 +1,22 @@ +package de.hysky.skyblocker.mixins; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import de.hysky.skyblocker.skyblock.SmoothAOTE; +import net.minecraft.client.render.Camera; +import net.minecraft.util.math.Vec3d; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(Camera.class) +public class CameraMixin { + + @ModifyReturnValue(method = "getPos", at = @At("RETURN")) + private Vec3d skyblocker$onCameraUpdate(Vec3d original) { + Vec3d pos = SmoothAOTE.getInterpolatedPos(); + if (pos != null) { + return pos; + } + + return original; + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java index fe35e7d2d6..8e231a2a53 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java @@ -5,15 +5,17 @@ import com.llamalad7.mixinextras.sugar.Local; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.config.configs.SlayersConfig; +import de.hysky.skyblocker.config.configs.UIAndVisualsConfig; import de.hysky.skyblocker.skyblock.CompactDamage; import de.hysky.skyblocker.skyblock.FishingHelper; +import de.hysky.skyblocker.skyblock.SmoothAOTE; import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder; import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; import de.hysky.skyblocker.skyblock.crimson.slayer.FirePillarAnnouncer; import de.hysky.skyblocker.skyblock.dungeon.DungeonScore; import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; -import de.hysky.skyblocker.skyblock.dwarven.WishingCompassSolver; import de.hysky.skyblocker.skyblock.dwarven.CrystalsChestHighlighter; +import de.hysky.skyblocker.skyblock.dwarven.WishingCompassSolver; import de.hysky.skyblocker.skyblock.end.EnderNodes; import de.hysky.skyblocker.skyblock.end.TheEnd; import de.hysky.skyblocker.skyblock.slayers.SlayerEntitiesGlow; @@ -41,9 +43,9 @@ public abstract class ClientPlayNetworkHandlerMixin { @Shadow private ClientWorld world; - @Shadow - @Final - private static Logger LOGGER; + @Shadow + @Final + private static Logger LOGGER; @Inject(method = "method_64896", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientWorld;removeEntity(ILnet/minecraft/entity/Entity$RemovalReason;)V")) private void skyblocker$onItemDestroy(int entityId, CallbackInfo ci) { @@ -52,87 +54,106 @@ public abstract class ClientPlayNetworkHandlerMixin { } } - @ModifyVariable(method = "onItemPickupAnimation", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientWorld;removeEntity(ILnet/minecraft/entity/Entity$RemovalReason;)V", ordinal = 0)) - private ItemEntity skyblocker$onItemPickup(ItemEntity itemEntity) { - DungeonManager.onItemPickup(itemEntity); - return itemEntity; - } - - @WrapWithCondition(method = "onEntityPassengersSet", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;)V", remap = false)) - private boolean skyblocker$cancelEntityPassengersWarning(Logger instance, String msg) { - return !Utils.isOnHypixel(); - } - - @WrapWithCondition(method = "onPlayerList", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V", remap = false)) - private boolean skyblocker$cancelPlayerListWarning(Logger instance, String format, Object arg1, Object arg2) { - return !Utils.isOnHypixel(); - } - - @Inject(method = "onPlaySound", at = @At("RETURN")) - private void skyblocker$onPlaySound(PlaySoundS2CPacket packet, CallbackInfo ci) { - FishingHelper.onSound(packet); - CrystalsChestHighlighter.onSound(packet); - } - - @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")); - } - - @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 = "onTeam", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;[Ljava/lang/Object;)V", remap = false)) - private boolean skyblocker$cancelTeamWarning(Logger instance, String format, Object... arg) { - return !Utils.isOnHypixel(); - } - - @Inject(method = "onParticle", at = @At("RETURN")) - private void skyblocker$onParticle(ParticleS2CPacket packet, CallbackInfo ci) { - MythologicalRitual.onParticle(packet); - DojoManager.onParticle(packet); - CrystalsChestHighlighter.onParticle(packet); - EnderNodes.onParticle(packet); - WishingCompassSolver.onParticle(packet); - } - - @ModifyExpressionValue(method = "onEntityStatus", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/packet/s2c/play/EntityStatusS2CPacket;getEntity(Lnet/minecraft/world/World;)Lnet/minecraft/entity/Entity;")) - private Entity skyblocker$onEntityDeath(Entity entity, @Local(argsOnly = true) EntityStatusS2CPacket packet) { - if (packet.getStatus() == EntityStatuses.PLAY_DEATH_SOUND_OR_ADD_PROJECTILE_HIT_PARTICLES) { - DungeonScore.handleEntityDeath(entity); - TheEnd.onEntityDeath(entity); - SlayerEntitiesGlow.onEntityDeath(entity); - } - return entity; - } - - @Inject(method = "onEntityTrackerUpdate", at = @At("TAIL")) - private void skyblocker$onEntityTrackerUpdate(EntityTrackerUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) { - if (!(entity instanceof ArmorStandEntity armorStandEntity)) return; - - if (SkyblockerConfigManager.get().slayers.highlightMinis == SlayersConfig.HighlightSlayerEntities.GLOW && SlayerEntitiesGlow.isSlayerMiniMob(armorStandEntity) - || SkyblockerConfigManager.get().slayers.highlightBosses == SlayersConfig.HighlightSlayerEntities.GLOW && SlayerEntitiesGlow.isSlayer(armorStandEntity)) { - if (armorStandEntity.isDead()) { - SlayerEntitiesGlow.cleanupArmorstand(armorStandEntity); - } else { - SlayerEntitiesGlow.setSlayerMobGlow(armorStandEntity); - } - } - - if (SkyblockerConfigManager.get().slayers.blazeSlayer.firePillarCountdown != SlayersConfig.BlazeSlayer.FirePillar.OFF) FirePillarAnnouncer.checkFirePillar(entity); - - EggFinder.checkIfEgg(armorStandEntity); - try { //Prevent packet handling fails if something goes wrong so that entity trackers still update, just without compact damage numbers - CompactDamage.compactDamage(armorStandEntity); - } catch (Exception e) { - LOGGER.error("[Skyblocker Compact Damage] Failed to compact damage number", e); - } - } - - @Inject(method = "onEntityEquipmentUpdate", at = @At(value = "TAIL")) - private void skyblocker$onEntityEquip(EntityEquipmentUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) { - EggFinder.checkIfEgg(entity); - } + @ModifyVariable(method = "onItemPickupAnimation", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientWorld;removeEntity(ILnet/minecraft/entity/Entity$RemovalReason;)V", ordinal = 0)) + private ItemEntity skyblocker$onItemPickup(ItemEntity itemEntity) { + DungeonManager.onItemPickup(itemEntity); + return itemEntity; + } + + @WrapWithCondition(method = "onEntityPassengersSet", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;)V", remap = false)) + private boolean skyblocker$cancelEntityPassengersWarning(Logger instance, String msg) { + return !Utils.isOnHypixel(); + } + + @WrapWithCondition(method = "onPlayerList", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V", remap = false)) + private boolean skyblocker$cancelPlayerListWarning(Logger instance, String format, Object arg1, Object arg2) { + return !Utils.isOnHypixel(); + } + + @Inject(method = "onPlaySound", at = @At("RETURN")) + private void skyblocker$onPlaySound(PlaySoundS2CPacket packet, CallbackInfo ci) { + FishingHelper.onSound(packet); + CrystalsChestHighlighter.onSound(packet); + } + + @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")); + } + + @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 = "onTeam", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;[Ljava/lang/Object;)V", remap = false)) + private boolean skyblocker$cancelTeamWarning(Logger instance, String format, Object... arg) { + return !Utils.isOnHypixel(); + } + + @Inject(method = "onParticle", at = @At("RETURN")) + private void skyblocker$onParticle(ParticleS2CPacket packet, CallbackInfo ci) { + MythologicalRitual.onParticle(packet); + DojoManager.onParticle(packet); + CrystalsChestHighlighter.onParticle(packet); + EnderNodes.onParticle(packet); + WishingCompassSolver.onParticle(packet); + } + + @ModifyExpressionValue(method = "onEntityStatus", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/packet/s2c/play/EntityStatusS2CPacket;getEntity(Lnet/minecraft/world/World;)Lnet/minecraft/entity/Entity;")) + private Entity skyblocker$onEntityDeath(Entity entity, @Local(argsOnly = true) EntityStatusS2CPacket packet) { + if (packet.getStatus() == EntityStatuses.PLAY_DEATH_SOUND_OR_ADD_PROJECTILE_HIT_PARTICLES) { + DungeonScore.handleEntityDeath(entity); + TheEnd.onEntityDeath(entity); + SlayerEntitiesGlow.onEntityDeath(entity); + } + return entity; + } + + @Inject(method = "onEntityTrackerUpdate", at = @At("TAIL")) + private void skyblocker$onEntityTrackerUpdate(EntityTrackerUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) { + if (!(entity instanceof ArmorStandEntity armorStandEntity)) return; + + if (SkyblockerConfigManager.get().slayers.highlightMinis == SlayersConfig.HighlightSlayerEntities.GLOW && SlayerEntitiesGlow.isSlayerMiniMob(armorStandEntity) + || SkyblockerConfigManager.get().slayers.highlightBosses == SlayersConfig.HighlightSlayerEntities.GLOW && SlayerEntitiesGlow.isSlayer(armorStandEntity)) { + if (armorStandEntity.isDead()) { + SlayerEntitiesGlow.cleanupArmorstand(armorStandEntity); + } else { + SlayerEntitiesGlow.setSlayerMobGlow(armorStandEntity); + } + } + + if (SkyblockerConfigManager.get().slayers.blazeSlayer.firePillarCountdown != SlayersConfig.BlazeSlayer.FirePillar.OFF) + FirePillarAnnouncer.checkFirePillar(entity); + + EggFinder.checkIfEgg(armorStandEntity); + try { //Prevent packet handling fails if something goes wrong so that entity trackers still update, just without compact damage numbers + CompactDamage.compactDamage(armorStandEntity); + } catch (Exception e) { + LOGGER.error("[Skyblocker Compact Damage] Failed to compact damage number", e); + } + } + + @Inject(method = "onEntityEquipmentUpdate", at = @At(value = "TAIL")) + private void skyblocker$onEntityEquip(EntityEquipmentUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) { + EggFinder.checkIfEgg(entity); + } + + @Inject(method = "onPlayerPositionLook", at = @At("TAIL")) + private void onPlayerTeleported(PlayerPositionLookS2CPacket packet, CallbackInfo ci) { + //player has been teleported by the server tell the smooth AOTE this + SmoothAOTE.playerTeleported(); + } + + @ModifyExpressionValue(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/DebugHud;shouldShowPacketSizeAndPingCharts()Z")) + private boolean shouldShowPacketSizeAndPingCharts(boolean original) { + //make the f3+3 screen always send ping packets even when closed + //this is needed to make smooth AOTE work so check if its enabled + UIAndVisualsConfig.SmoothAOTE options = SkyblockerConfigManager.get().uiAndVisuals.smoothAOTE; + if (Utils.isOnSkyblock() && !SmoothAOTE.teleportDisabled && (options.enableWeirdTransmission || options.enableEtherTransmission || options.enableInstantTransmission || options.enableSinrecallTransmission || options.enableWitherImpact)) { + return true; + } + return original; + + } } diff --git a/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java index 3273968690..b09abc64b1 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/PingMeasurerMixin.java @@ -1,5 +1,6 @@ package de.hysky.skyblocker.mixins; +import de.hysky.skyblocker.skyblock.SmoothAOTE; import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager; import de.hysky.skyblocker.utils.Utils; import net.minecraft.client.network.PingMeasurer; @@ -15,6 +16,7 @@ public class PingMeasurerMixin { if (Utils.isInCrimson()) { DojoManager.onPingResult(ping); } + SmoothAOTE.updatePing(ping); return ping; } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/SmoothAOTE.java b/src/main/java/de/hysky/skyblocker/skyblock/SmoothAOTE.java new file mode 100644 index 0000000000..23eaf83e41 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/SmoothAOTE.java @@ -0,0 +1,464 @@ +package de.hysky.skyblocker.skyblock; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.annotations.Init; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.dungeon.DungeonBoss; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.Location; +import de.hysky.skyblocker.utils.Utils; +import net.fabricmc.fabric.api.event.player.UseBlockCallback; +import net.fabricmc.fabric.api.event.player.UseItemCallback; +import net.minecraft.block.*; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.Perspective; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.state.property.Properties; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.world.World; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SmoothAOTE { + + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + + private static final Pattern MANA_LORE = Pattern.compile("Mana Cost: (\\d+)"); + private static final long MAX_TELEPORT_TIME = 2500; //2.5 seconds + + private static long startTime; + private static Vec3d startPos; + private static Vec3d cameraStartPos; + private static Vec3d teleportVector; + private static long lastPing; + private static int teleportsAhead; + private static long lastTeleportTime; + public static boolean teleportDisabled; + + @Init + public static void init() { + UseItemCallback.EVENT.register(SmoothAOTE::onItemInteract); + UseBlockCallback.EVENT.register(SmoothAOTE::onBlockInteract); + } + + /** + * When a player receives a teleport packet finish a teleport + */ + public static void playerTeleported() { + //the player has been teleported so 1 less teleport ahead + teleportsAhead = Math.max(0, teleportsAhead - 1); + //re-enable the animation if the player is teleported as this means they can teleport again. and reset timer for last teleport update + lastTeleportTime = System.currentTimeMillis(); + teleportDisabled = false; + + //if the server is in sync in number of teleports + if (teleportsAhead == 0) { + //reset when player has reached the end of the teleports + startPos = null; + teleportVector = null; + } + } + + /** + * checks to see if a teleport device is using transmission tuner to increase the range + * + * @param customData the custom data of the teleport device + * @param baseRange the base range for the device without tuner + * @return the range with tuner + */ + private static int extractTunedCustomData(NbtCompound customData, int baseRange) { + return customData != null && customData.contains("tuned_transmission") ? baseRange + customData.getInt("tuned_transmission") : baseRange; + } + + /** + * When an item is right-clicked send off to calculate teleport with the clicked item + * + * @param playerEntity player + * @param world world + * @param hand held item + * @return pass + */ + private static ActionResult onItemInteract(PlayerEntity playerEntity, World world, Hand hand) { + if (CLIENT.player == null) { + return null; + } + calculateTeleportUse(hand); + return ActionResult.PASS; + } + + /** + * Allows shovel teleport items to be used when aiming at interactable blocks + * + * @param playerEntity player + * @param world world + * @param hand hand item + * @param blockHitResult target block + * @return always pass + */ + private static ActionResult onBlockInteract(PlayerEntity playerEntity, World world, Hand hand, BlockHitResult blockHitResult) { + ItemStack itemStack = playerEntity.getStackInHand(hand); + if (isShovel(itemStack) && canShovelActOnBlock(world.getBlockState(blockHitResult.getBlockPos()).getBlock())) { + calculateTeleportUse(hand); + } + return ActionResult.PASS; + } + + private static boolean isShovel(ItemStack itemStack) { + return itemStack.isOf(Items.WOODEN_SHOVEL) || + itemStack.isOf(Items.STONE_SHOVEL) || + itemStack.isOf(Items.IRON_SHOVEL) || + itemStack.isOf(Items.GOLDEN_SHOVEL) || + itemStack.isOf(Items.DIAMOND_SHOVEL); + } + + /** + * Checks if the block is one that the shovel can turn into a path (e.g., grass or dirt) + * + * @param block block to check + * @return if block can be turned into path + */ + private static boolean canShovelActOnBlock(Block block) { + return block == Blocks.GRASS_BLOCK || + block == Blocks.DIRT || + block == Blocks.COARSE_DIRT || + block == Blocks.PODZOL; + } + + /** + * Finds if a player uses a teleport and then saves the start position and time. then works out final position and saves that too + * + * @param hand what the player is holding + */ + + private static void calculateTeleportUse(Hand hand) { + //stop checking if player does not exist + if (CLIENT.player == null || CLIENT.world == null) { + return; + } + //get return item + ItemStack stack = CLIENT.player.getStackInHand(hand); + + //make sure it's not disabled + if (teleportDisabled) { + return; + } + + // make sure the camera is not in 3rd person + if (CLIENT.options.getPerspective() != Perspective.FIRST_PERSON) { + return; + } + + //make sure the player is in an area teleporting is allowed not allowed in glacite mineshafts and floor 7 boss + if (!isAllowedLocation()) { + return; + } + + //work out if the player is holding a teleporting item that is enabled and if so how far the item will take them + ItemStack heldItem = CLIENT.player.getMainHandStack(); + String itemId = heldItem.getSkyblockId(); + NbtCompound customData = ItemUtils.getCustomData(heldItem); + + int distance; + switch (itemId) { + case "ASPECT_OF_THE_LEECH_1" -> { + if (SkyblockerConfigManager.get().uiAndVisuals.smoothAOTE.enableWeirdTransmission) { + distance = 3; + break; + } + return; + + } + case "ASPECT_OF_THE_LEECH_2" -> { + if (SkyblockerConfigManager.get().uiAndVisuals.smoothAOTE.enableWeirdTransmission) { + distance = 4; + break; + } + return; + } + case "ASPECT_OF_THE_END", "ASPECT_OF_THE_VOID" -> { + if (CLIENT.options.sneakKey.isPressed() && customData.getInt("ethermerge") == 1) { + if (SkyblockerConfigManager.get().uiAndVisuals.smoothAOTE.enableEtherTransmission) { + distance = extractTunedCustomData(customData, 57); + break; + } + } else if (SkyblockerConfigManager.get().uiAndVisuals.smoothAOTE.enableInstantTransmission) { + distance = extractTunedCustomData(customData, 8); + break; + } + return; + } + case "ETHERWARP_CONDUIT" -> { + if (SkyblockerConfigManager.get().uiAndVisuals.smoothAOTE.enableEtherTransmission) { + distance = extractTunedCustomData(customData, 57); + break; + } + return; + } + case "SINSEEKER_SCYTHE" -> { + if (SkyblockerConfigManager.get().uiAndVisuals.smoothAOTE.enableSinrecallTransmission) { + distance = extractTunedCustomData(customData, 4); + break; + } + return; + } + case "NECRON_BLADE", "ASTRAEA", "HYPERION", "SCYLLA", "VALKYRIE" -> { + if (SkyblockerConfigManager.get().uiAndVisuals.smoothAOTE.enableWitherImpact) { + distance = 10; + break; + } + return; + } + default -> { + return; + } + } + //make sure the player has enough mana to do the teleport + Matcher manaNeeded = ItemUtils.getLoreLineIfMatch(heldItem, MANA_LORE); + if (manaNeeded != null && manaNeeded.matches()) { + int manaCost = Integer.parseInt(manaNeeded.group(1)); + int predictedMana = SkyblockerMod.getInstance().statusBarTracker.getMana().value() - teleportsAhead * manaCost; + if (predictedMana < manaCost) { // todo the players mana can lag behind as it is updated server side. client side mana calculations would help with this + return; + } + } + + //work out start pos of warp and set start time. if there is an active warp going on make the end of that the start of the next one + if (teleportsAhead == 0 || startPos == null || teleportVector == null) { + //start of teleport sequence + startPos = CLIENT.player.getPos().add(0, 1.62, 0); // the eye poss should not be affected by crouching + cameraStartPos = CLIENT.player.getEyePos(); + lastTeleportTime = System.currentTimeMillis(); + } else { + //add to the end of the teleport sequence + startPos = startPos.add(teleportVector); + //set the camera start pos to how far though the teleport the player is to make is smoother + cameraStartPos = getInterpolatedPos(); + } + + startTime = System.currentTimeMillis(); + + + // calculate the vector the player will follow for the teleport + //get direction + float pitch = CLIENT.player.getPitch(); + float yaw = CLIENT.player.getYaw(); + Vec3d look = CLIENT.player.getRotationVector(pitch, yaw); + + //find target location depending on how far the item they are using takes them + teleportVector = raycast(distance, look, startPos); + if (teleportVector == null) { + startPos = null; + return; + } + + //compensate for hypixel round to center of block (to x.5 y.62 z.5) + Vec3d predictedEnd = startPos.add(teleportVector); + Vec3d offsetVec = new Vec3d(predictedEnd.x - roundToCenter(predictedEnd.x), predictedEnd.y - (Math.ceil(predictedEnd.y) + 0.62), predictedEnd.z - roundToCenter(predictedEnd.z)); + teleportVector = teleportVector.subtract(offsetVec); + //add 1 to teleports ahead + teleportsAhead += 1; + } + + /** + * Rounds a value to the nearest 0.5 + * + * @param input number to round + * @return rounded number + */ + private static double roundToCenter(double input) { + return Math.round(input - 0.5) + 0.5; + } + + /** + * Works out if the players location lets them use teleportation or not + * + * @return if the player should be allowed to teleport + */ + private static boolean isAllowedLocation() { + //check mines shafts + if (Utils.getMap().equals("Mineshaft")) { + return false; + } else if (Utils.getIslandArea().equals("⏣ Jungle Temple")) { //do not allow in jungle temple + return false; + } else if (Utils.getLocation() == Location.PRIVATE_ISLAND && !Utils.getIslandArea().equals("⏣ Your Island")) { //do not allow it when visiting + return false; + } else if (Utils.isInDungeons()) { //check places in dungeons where you can't teleport + if (DungeonManager.isInBoss() && DungeonManager.getBoss() == DungeonBoss.MAXOR) { + return false; + } + //make sure the player is in a room then check for disallowed rooms + if (!DungeonManager.isCurrentRoomMatched()) { + return true; + } + //does not work in boulder room + if (DungeonManager.getCurrentRoom().getName().equals("boxes-room")) { + return false; + } + //does not work in teleport maze room + if (DungeonManager.getCurrentRoom().getName().equals("teleport-pad-room")) { + return false; + } + //does not work in trap room + if (DungeonManager.getCurrentRoom().getName().startsWith("trap")) { + return false; + } + } + + return true; + } + + /** + * Custom raycast for teleporting checks for blocks for each 1 block forward in teleport. (very similar to hypixels method) + * + * @param distance maximum distance + * @return teleport vector + */ + private static Vec3d raycast(int distance, Vec3d direction, Vec3d startPos) { + if (CLIENT.world == null || direction == null || startPos == null) { + return null; + } + + //based on which way the ray is going get the needed vector for checking diagonals + BlockPos xDiagonalOffset; + BlockPos zDiagonalOffset; + if (direction.getX() > 0) { + xDiagonalOffset = new BlockPos(-1, 0, 0); + } else { + xDiagonalOffset = new BlockPos(1, 0, 0); + } + if (direction.getZ() > 0) { + zDiagonalOffset = new BlockPos(0, 0, -1); + } else { + zDiagonalOffset = new BlockPos(0, 0, 1); + } + + //initialise the closest floor value outside of possible values + int closeFloorY = 1000; + + //loop though each block of a teleport checking each block if there are blocks in the way + for (double offset = 0; offset <= distance; offset++) { + Vec3d pos = startPos.add(direction.multiply(offset)); + BlockPos checkPos = BlockPos.ofFloored(pos); + + //check if there is a block at the check location + if (!canTeleportThrough(checkPos)) { + if (offset == 0) { + // no teleport can happen + return null; + } + return direction.multiply(offset - 1); + } + + //check if the block at head height is free + if (!canTeleportThrough(checkPos.up())) { + if (offset == 0) { + //cancel the check if starting height is too low + Vec3d justAhead = startPos.add(direction.multiply(0.2)); + if ((justAhead.getY() - Math.floor(justAhead.getY())) <= 0.495) { + continue; + } + // no teleport can happen + return null; + } + return direction.multiply(offset - 1); + } + + //check the diagonals to make sure player is not going through diagonal wall (full height block in the way on both sides at either height) + if (offset != 0 && (isBlockFloor(checkPos.add(xDiagonalOffset)) || isBlockFloor(checkPos.up().add(xDiagonalOffset))) && (isBlockFloor(checkPos.add(zDiagonalOffset)) || isBlockFloor(checkPos.up().add(zDiagonalOffset)))) { + return direction.multiply(offset - 1); + } + + //if the player is close to the floor (including diagonally) save Y and when player goes bellow this y finish teleport + if (offset != 0 && (isBlockFloor(checkPos.down()) || (isBlockFloor(checkPos.down().subtract(xDiagonalOffset)) && isBlockFloor(checkPos.down().subtract(zDiagonalOffset)))) && (pos.getY() - Math.floor(pos.getY())) < 0.31) { + closeFloorY = checkPos.getY() - 1; + } + + //if the checking Y is same as closeY finish + if (closeFloorY == checkPos.getY()) { + return direction.multiply(offset - 1); + } + } + + //return full distance if no collision found + return direction.multiply(distance); + } + + /** + * Checks to see if a block is in the allowed list to teleport though + * Air, Buttons, carpets, crops, pots, mushrooms, nether wart, redstone, ladder, water, fire, lava, 3 or less snow layers + * + * @param blockPos block location + * @return if a block location can be teleported though + */ + private static Boolean canTeleportThrough(BlockPos blockPos) { + if (CLIENT.world == null) { + return false; + } + + BlockState blockState = CLIENT.world.getBlockState(blockPos); + if (blockState.isAir()) { + return true; + } + Block block = blockState.getBlock(); + return block instanceof ButtonBlock || block instanceof CarpetBlock || block instanceof CropBlock || block instanceof FlowerPotBlock || block.equals(Blocks.BROWN_MUSHROOM) || block.equals(Blocks.RED_MUSHROOM) || block.equals(Blocks.NETHER_WART) || block.equals(Blocks.REDSTONE_WIRE) || block.equals(Blocks.LADDER) || block.equals(Blocks.FIRE) || (block.equals(Blocks.SNOW) && blockState.get(Properties.LAYERS) <= 3) || block.equals(Blocks.WATER) || block.equals(Blocks.LAVA); + } + + /** + * Checks to see if a block goes to the top if so class it as a floor + * + * @param blockPos block location + * @return if it's a floor block + */ + private static Boolean isBlockFloor(BlockPos blockPos) { + if (CLIENT.world == null) { + return false; + } + + BlockState blockState = CLIENT.world.getBlockState(blockPos); + VoxelShape shape = blockState.getCollisionShape(CLIENT.world, blockPos); + if (shape.isEmpty()) { + return false; + } + return shape.getBoundingBox().maxY == 1; + } + + /** + * works out where they player should be based on how far though the predicted teleport time. + * + * @return the camera position for the interpolated pos + */ + + public static Vec3d getInterpolatedPos() { + if (CLIENT.player == null || teleportVector == null || startPos == null || teleportDisabled) { + return null; + } + long gap = System.currentTimeMillis() - startTime; + //make sure the player is actually getting teleported if not disable teleporting until they are teleported again + if (System.currentTimeMillis() - lastTeleportTime > Math.min(2 * lastPing, MAX_TELEPORT_TIME)) { + teleportDisabled = true; + startPos = null; + teleportVector = null; + teleportsAhead = 0; + return null; + } + double percentage = Math.min((double) (gap) / Math.min(lastPing, MAX_TELEPORT_TIME), 1); + + return cameraStartPos.add(teleportVector.multiply(percentage)); + } + + public static void updatePing(long ping) { + lastPing = ping; + } + + +} diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index eea3fd24b0..a531fabbc1 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -778,6 +778,19 @@ "skyblocker.config.uiAndVisuals.showEquipmentInInventory": "Show Equipment in Inventory", + "skyblocker.config.uiAndVisuals.smoothAOTE": "Smooth AOTE", + "skyblocker.config.uiAndVisuals.smoothAOTE.@Tooltip": "Smooths out teleporting with right click teleport abilities.", + "skyblocker.config.uiAndVisuals.smoothAOTE.enableEtherTransmission": "Enable Ether Transmission", + "skyblocker.config.uiAndVisuals.smoothAOTE.enableEtherTransmission.@Tooltip": "For: Etherwarp Conduit and Ether Merged.", + "skyblocker.config.uiAndVisuals.smoothAOTE.enableInstantTransmission": "Enable Instant Transmission", + "skyblocker.config.uiAndVisuals.smoothAOTE.enableInstantTransmission.@Tooltip": "For: AOTE and AOTV.", + "skyblocker.config.uiAndVisuals.smoothAOTE.enableSinrecallTransmission": "Enable Sinrecall Transmission", + "skyblocker.config.uiAndVisuals.smoothAOTE.enableSinrecallTransmission.@Tooltip": "For: Sinseeker Scythe.", + "skyblocker.config.uiAndVisuals.smoothAOTE.enableWeirdTransmission": "Enable Weird Transmission", + "skyblocker.config.uiAndVisuals.smoothAOTE.enableWeirdTransmission.@Tooltip": "For: Aspect of the Leech.", + "skyblocker.config.uiAndVisuals.smoothAOTE.enableWitherImpact": "Enable Wither Impact", + "skyblocker.config.uiAndVisuals.smoothAOTE.enableWitherImpact.@Tooltip": "For: Necron's Blade, Hyperion, Astraea, Scylla, and Valkyrie.", + "skyblocker.config.uiAndVisuals.tabHud": "Fancy tab HUD (Temporarily disabled outside dungeons)", "skyblocker.config.uiAndVisuals.tabHud.enableHudBackground": "Enable HUD Background", "skyblocker.config.uiAndVisuals.tabHud.enableHudBackground.@Tooltip": "Enables the background of the non-tab HUD.", diff --git a/src/main/resources/skyblocker.mixins.json b/src/main/resources/skyblocker.mixins.json index b011be6d59..0ea5e2776d 100644 --- a/src/main/resources/skyblocker.mixins.json +++ b/src/main/resources/skyblocker.mixins.json @@ -7,6 +7,7 @@ "BackgroundRendererMixin", "BatEntityMixin", "BossBarHudMixin", + "CameraMixin", "ClientPlayerEntityMixin", "ClientPlayNetworkHandlerMixin", "ClientWorldMixin",