diff --git a/src/main/java/meteordevelopment/meteorclient/mixin/Vec3dMixin.java b/src/main/java/meteordevelopment/meteorclient/mixin/Vec3dMixin.java index 818a571744..2349e8f786 100644 --- a/src/main/java/meteordevelopment/meteorclient/mixin/Vec3dMixin.java +++ b/src/main/java/meteordevelopment/meteorclient/mixin/Vec3dMixin.java @@ -19,20 +19,26 @@ public abstract class Vec3dMixin implements IVec3d { @Shadow @Final @Mutable public double z; @Override - public void meteor$set(double x, double y, double z) { + public Vec3d meteor$set(double x, double y, double z) { this.x = x; this.y = y; this.z = z; + + return (Vec3d) (Object) this; } @Override - public void meteor$setXZ(double x, double z) { + public Vec3d meteor$setXZ(double x, double z) { this.x = x; this.z = z; + + return (Vec3d) (Object) this; } @Override - public void meteor$setY(double y) { + public Vec3d meteor$setY(double y) { this.y = y; + + return (Vec3d) (Object) this; } } diff --git a/src/main/java/meteordevelopment/meteorclient/mixininterface/IVec3d.java b/src/main/java/meteordevelopment/meteorclient/mixininterface/IVec3d.java index a600199b1e..2cbb4c97da 100644 --- a/src/main/java/meteordevelopment/meteorclient/mixininterface/IVec3d.java +++ b/src/main/java/meteordevelopment/meteorclient/mixininterface/IVec3d.java @@ -5,21 +5,27 @@ package meteordevelopment.meteorclient.mixininterface; +import net.minecraft.util.math.Vec3d; import net.minecraft.util.math.Vec3i; import org.joml.Vector3d; +@SuppressWarnings("UnusedReturnValue") public interface IVec3d { - void meteor$set(double x, double y, double z); + Vec3d meteor$set(double x, double y, double z); - default void meteor$set(Vec3i vec) { - meteor$set(vec.getX(), vec.getY(), vec.getZ()); + default Vec3d meteor$set(Vec3i vec) { + return meteor$set(vec.getX(), vec.getY(), vec.getZ()); } - default void meteor$set(Vector3d vec) { - meteor$set(vec.x, vec.y, vec.z); + default Vec3d meteor$set(Vector3d vec) { + return meteor$set(vec.x, vec.y, vec.z); } - void meteor$setXZ(double x, double z); + default Vec3d meteor$set(Vec3d pos) { + return meteor$set(pos.x, pos.y, pos.z); + } + + Vec3d meteor$setXZ(double x, double z); - void meteor$setY(double y); + Vec3d meteor$setY(double y); } diff --git a/src/main/java/meteordevelopment/meteorclient/systems/modules/combat/ArrowDodge.java b/src/main/java/meteordevelopment/meteorclient/systems/modules/combat/ArrowDodge.java index ba8236dfca..322d0fa837 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/modules/combat/ArrowDodge.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/modules/combat/ArrowDodge.java @@ -9,12 +9,13 @@ import meteordevelopment.meteorclient.settings.*; import meteordevelopment.meteorclient.systems.modules.Categories; import meteordevelopment.meteorclient.systems.modules.Module; -import meteordevelopment.meteorclient.utils.entity.ProjectileEntitySimulator; +import meteordevelopment.meteorclient.utils.entity.simulator.ProjectileEntitySimulator; import meteordevelopment.meteorclient.utils.misc.Pool; import meteordevelopment.orbit.EventHandler; import net.minecraft.entity.Entity; import net.minecraft.entity.projectile.ArrowEntity; import net.minecraft.entity.projectile.ProjectileEntity; +import net.minecraft.entity.projectile.SpectralArrowEntity; import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Vec3d; @@ -54,13 +55,6 @@ public class ArrowDodge extends Module { .build() ); - private final Setting accurate = sgGeneral.add(new BoolSetting.Builder() - .name("accurate") - .description("Whether or not to calculate more accurate.") - .defaultValue(false) - .build() - ); - private final Setting groundCheck = sgGeneral.add(new BoolSetting.Builder() .name("ground-check") .description("Tries to prevent you from falling to your death.") @@ -78,7 +72,7 @@ public class ArrowDodge extends Module { private final Setting ignoreOwn = sgGeneral.add(new BoolSetting.Builder() .name("ignore-own") .description("Ignore your own projectiles.") - .defaultValue(false) + .defaultValue(true) .build() ); @@ -116,16 +110,16 @@ private void onTick(TickEvent.Pre event) { for (Entity e : mc.world.getEntities()) { if (!(e instanceof ProjectileEntity projectile)) continue; - if (!allProjectiles.get() && !(projectile instanceof ArrowEntity)) continue; + if (!allProjectiles.get() && !(projectile instanceof ArrowEntity || projectile instanceof SpectralArrowEntity)) continue; if (ignoreOwn.get()) { Entity owner = projectile.getOwner(); if (owner != null && owner.getUuid().equals(mc.player.getUuid())) continue; } - if (!simulator.set(projectile, accurate.get())) continue; + if (!simulator.set(projectile)) continue; for (int i = 0; i < (simulationSteps.get() > 0 ? simulationSteps.get() : Integer.MAX_VALUE); i++) { points.add(vec3s.get().set(simulator.pos)); - if (simulator.tick() != null) break; + if (simulator.tick().shouldStop) break; } } diff --git a/src/main/java/meteordevelopment/meteorclient/systems/modules/render/Trajectories.java b/src/main/java/meteordevelopment/meteorclient/systems/modules/render/Trajectories.java index 34f769ddfb..8d5077999a 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/modules/render/Trajectories.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/modules/render/Trajectories.java @@ -11,7 +11,8 @@ import meteordevelopment.meteorclient.systems.modules.Categories; import meteordevelopment.meteorclient.systems.modules.Module; import meteordevelopment.meteorclient.utils.Utils; -import meteordevelopment.meteorclient.utils.entity.ProjectileEntitySimulator; +import meteordevelopment.meteorclient.utils.entity.simulator.ProjectileEntitySimulator; +import meteordevelopment.meteorclient.utils.entity.simulator.SimulationStep; import meteordevelopment.meteorclient.utils.misc.Pool; import meteordevelopment.meteorclient.utils.render.color.SettingColor; import meteordevelopment.orbit.EventHandler; @@ -19,6 +20,7 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.projectile.ProjectileEntity; +import net.minecraft.entity.projectile.TridentEntity; import net.minecraft.entity.projectile.WitherSkullEntity; import net.minecraft.item.*; import net.minecraft.registry.Registries; @@ -86,6 +88,14 @@ public class Trajectories extends Module { // Render + private final Setting ignoreFirstTicks = sgRender.add(new IntSetting.Builder() + .name("ignore-rendering-first-ticks") + .description("Ignores rendering the first given ticks, to make the rest of the path more visible.") + .defaultValue(3) + .min(0) + .build() + ); + private final Setting shapeMode = sgRender.add(new EnumSetting.Builder() .name("shape-mode") .description("How the shapes are rendered.") @@ -189,14 +199,17 @@ private void calculatePath(PlayerEntity player, float tickDelta) { // Calculate paths if (!simulator.set(player, itemStack, 0, accurate.get(), tickDelta)) return; - getEmptyPath().calculate(); + Path p = getEmptyPath().calculate(); + if (player == mc.player) p.ignoreFirstTicks(); if (itemStack.getItem() instanceof CrossbowItem && Utils.hasEnchantment(itemStack, Enchantments.MULTISHOT)) { if (!simulator.set(player, itemStack, MULTISHOT_OFFSET, accurate.get(), tickDelta)) return; // left multishot arrow - getEmptyPath().calculate(); + p = getEmptyPath().calculate(); + if (player == mc.player) p.ignoreFirstTicks(); if (!simulator.set(player, itemStack, -MULTISHOT_OFFSET, accurate.get(), tickDelta)) return; // right multishot arrow - getEmptyPath().calculate(); + p = getEmptyPath().calculate(); + if (player == mc.player) p.ignoreFirstTicks(); } } @@ -204,7 +217,7 @@ private void calculateFiredPath(Entity entity, double tickDelta) { for (Path path : paths) path.clear(); // Calculate paths - if (!simulator.set(entity, accurate.get())) return; + if (!simulator.set(entity)) return; getEmptyPath().setStart(entity, tickDelta).calculate(); } @@ -221,7 +234,10 @@ private void onRender(Render3DEvent event) { if (firedProjectiles.get()) { for (Entity entity : mc.world.getEntities()) { - if (entity instanceof ProjectileEntity && (!ignoreWitherSkulls.get() || !(entity instanceof WitherSkullEntity))) { + if (entity instanceof ProjectileEntity) { + if (ignoreWitherSkulls.get() && entity instanceof WitherSkullEntity) continue; + if (entity instanceof TridentEntity trident && trident.noClip) continue; // when it's returning via loyalty + calculateFiredPath(entity, tickDelta); for (Path path : paths) path.render(event); } @@ -235,31 +251,33 @@ private class Path { private boolean hitQuad, hitQuadHorizontal; private double hitQuadX1, hitQuadY1, hitQuadZ1, hitQuadX2, hitQuadY2, hitQuadZ2; - private Entity collidingEntity; + private final List collidingEntities = new ArrayList<>(); public Vector3d lastPoint; + private int start; public void clear() { vec3s.freeAll(points); points.clear(); hitQuad = false; - collidingEntity = null; + collidingEntities.clear(); lastPoint = null; + start = 0; } - public void calculate() { + public Path calculate() { addPoint(); for (int i = 0; i < (simulationSteps.get() > 0 ? simulationSteps.get() : Integer.MAX_VALUE); i++) { - HitResult result = simulator.tick(); + SimulationStep result = simulator.tick(); - if (result != null) { - processHitResult(result); - break; - } + processHitResults(result); + if (result.shouldStop) break; addPoint(); } + + return this; } public Path setStart(Entity entity, double tickDelta) { @@ -276,58 +294,75 @@ private void addPoint() { points.add(vec3s.get().set(simulator.pos)); } - private void processHitResult(HitResult result) { - if (result.getType() == HitResult.Type.BLOCK) { - BlockHitResult r = (BlockHitResult) result; - - hitQuad = true; - hitQuadX1 = r.getPos().x; - hitQuadY1 = r.getPos().y; - hitQuadZ1 = r.getPos().z; - hitQuadX2 = r.getPos().x; - hitQuadY2 = r.getPos().y; - hitQuadZ2 = r.getPos().z; - - if (r.getSide() == Direction.UP || r.getSide() == Direction.DOWN) { - hitQuadHorizontal = true; - hitQuadX1 -= 0.25; - hitQuadZ1 -= 0.25; - hitQuadX2 += 0.25; - hitQuadZ2 += 0.25; - } - else if (r.getSide() == Direction.NORTH || r.getSide() == Direction.SOUTH) { - hitQuadHorizontal = false; - hitQuadX1 -= 0.25; - hitQuadY1 -= 0.25; - hitQuadX2 += 0.25; - hitQuadY2 += 0.25; - } - else { - hitQuadHorizontal = false; - hitQuadZ1 -= 0.25; - hitQuadY1 -= 0.25; - hitQuadZ2 += 0.25; - hitQuadY2 += 0.25; + private void processHitResults(SimulationStep step) { + for (int i = 0; i < step.hitResults.length; i++) { + HitResult result = step.hitResults[i]; + if (result.getType() == HitResult.Type.BLOCK) { + BlockHitResult r = (BlockHitResult) result; + + hitQuad = true; + hitQuadX1 = r.getPos().x; + hitQuadY1 = r.getPos().y; + hitQuadZ1 = r.getPos().z; + hitQuadX2 = r.getPos().x; + hitQuadY2 = r.getPos().y; + hitQuadZ2 = r.getPos().z; + + if (r.getSide() == Direction.UP || r.getSide() == Direction.DOWN) { + hitQuadHorizontal = true; + hitQuadX1 -= 0.25; + hitQuadZ1 -= 0.25; + hitQuadX2 += 0.25; + hitQuadZ2 += 0.25; + } + else if (r.getSide() == Direction.NORTH || r.getSide() == Direction.SOUTH) { + hitQuadHorizontal = false; + hitQuadX1 -= 0.25; + hitQuadY1 -= 0.25; + hitQuadX2 += 0.25; + hitQuadY2 += 0.25; + } + else { + hitQuadHorizontal = false; + hitQuadZ1 -= 0.25; + hitQuadY1 -= 0.25; + hitQuadZ2 += 0.25; + hitQuadY2 += 0.25; + } + + points.add(Utils.set(vec3s.get(), result.getPos())); } + else if (result.getType() == HitResult.Type.ENTITY) { + Entity entity = ((EntityHitResult) result).getEntity(); + collidingEntities.add(entity); - points.add(Utils.set(vec3s.get(), result.getPos())); + if (step.shouldStop && i == step.hitResults.length - 1) { + points.add(Utils.set(vec3s.get(), result.getPos())); + } + } } - else if (result.getType() == HitResult.Type.ENTITY) { - collidingEntity = ((EntityHitResult) result).getEntity(); + } - points.add(Utils.set(vec3s.get(), result.getPos()).add(0, collidingEntity.getHeight() / 2, 0)); - } + public void ignoreFirstTicks() { + start = points.size() <= ignoreFirstTicks.get() ? 0 : ignoreFirstTicks.get(); } public void render(Render3DEvent event) { // Render path - for (Vector3d point : points) { + for (int i = start; i < points.size(); i++) { + Vector3d point = points.get(i); + if (lastPoint != null) { event.renderer.line(lastPoint.x, lastPoint.y, lastPoint.z, point.x, point.y, point.z, lineColor.get()); - if (renderPositionBox.get()) - event.renderer.box(point.x - positionBoxSize.get(), point.y - positionBoxSize.get(), point.z - positionBoxSize.get(), - point.x + positionBoxSize.get(), point.y + positionBoxSize.get(), point.z + positionBoxSize.get(), positionSideColor.get(), positionLineColor.get(), shapeMode.get(), 0); + if (renderPositionBox.get()) { + event.renderer.box( + point.x - positionBoxSize.get(), point.y - positionBoxSize.get(), point.z - positionBoxSize.get(), + point.x + positionBoxSize.get(), point.y + positionBoxSize.get(), point.z + positionBoxSize.get(), + positionSideColor.get(), positionLineColor.get(), shapeMode.get(), 0 + ); + } } + lastPoint = point; } @@ -338,7 +373,7 @@ public void render(Render3DEvent event) { } // Render entity - if (collidingEntity != null) { + for (Entity collidingEntity : collidingEntities) { double x = (collidingEntity.getX() - collidingEntity.lastX) * event.tickDelta; double y = (collidingEntity.getY() - collidingEntity.lastY) * event.tickDelta; double z = (collidingEntity.getZ() - collidingEntity.lastZ) * event.tickDelta; diff --git a/src/main/java/meteordevelopment/meteorclient/utils/entity/ProjectileEntitySimulator.java b/src/main/java/meteordevelopment/meteorclient/utils/entity/ProjectileEntitySimulator.java deleted file mode 100644 index 54ebd68005..0000000000 --- a/src/main/java/meteordevelopment/meteorclient/utils/entity/ProjectileEntitySimulator.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). - * Copyright (c) Meteor Development. - */ - -package meteordevelopment.meteorclient.utils.entity; - -import meteordevelopment.meteorclient.mixin.CrossbowItemAccessor; -import meteordevelopment.meteorclient.mixin.ProjectileInGroundAccessor; -import meteordevelopment.meteorclient.mixininterface.IVec3d; -import meteordevelopment.meteorclient.utils.Utils; -import meteordevelopment.meteorclient.utils.misc.MissHitResult; -import meteordevelopment.meteorclient.utils.player.Rotations; -import net.minecraft.component.DataComponentTypes; -import net.minecraft.component.type.ChargedProjectilesComponent; -import net.minecraft.entity.Entity; -import net.minecraft.entity.EntityType; -import net.minecraft.entity.projectile.*; -import net.minecraft.entity.projectile.thrown.*; -import net.minecraft.fluid.FluidState; -import net.minecraft.fluid.Fluids; -import net.minecraft.item.*; -import net.minecraft.util.hit.HitResult; -import net.minecraft.util.math.*; -import net.minecraft.world.RaycastContext; -import org.joml.Quaterniond; -import org.joml.Vector3d; - -import static meteordevelopment.meteorclient.MeteorClient.mc; - -public class ProjectileEntitySimulator { - private static final BlockPos.Mutable blockPos = new BlockPos.Mutable(); - - private static final Vec3d pos3d = new Vec3d(0, 0, 0); - private static final Vec3d prevPos3d = new Vec3d(0, 0, 0); - - public final Vector3d pos = new Vector3d(); - private final Vector3d velocity = new Vector3d(); - - private Entity simulatingEntity; - private double gravity; - private double airDrag, waterDrag; - private float height, width; - - - // held items - - public boolean set(Entity user, ItemStack itemStack, double simulated, boolean accurate, float tickDelta) { - Item item = itemStack.getItem(); - - switch (item) { - case BowItem ignored -> { - double charge = BowItem.getPullProgress(mc.player.getItemUseTime()); - if (charge <= 0.1) return false; - - set(user, 0, charge * 3, simulated, 0.05, 0.6, accurate, tickDelta, EntityType.ARROW); - } - case CrossbowItem ignored -> { - ChargedProjectilesComponent projectilesComponent = itemStack.get(DataComponentTypes.CHARGED_PROJECTILES); - if (projectilesComponent == null) return false; - - if (projectilesComponent.contains(Items.FIREWORK_ROCKET)) { - set(user, 0, CrossbowItemAccessor.meteor$getSpeed(projectilesComponent), simulated, 0, 0.6, accurate, tickDelta, EntityType.FIREWORK_ROCKET); - } - else set(user, 0, CrossbowItemAccessor.meteor$getSpeed(projectilesComponent), simulated, 0.05, 0.6, accurate, tickDelta, EntityType.ARROW); - } - case WindChargeItem ignored -> { - set(user, 0, 1.5, simulated, 0, 1.0, accurate, tickDelta, EntityType.WIND_CHARGE); - this.airDrag = 1.0; - } - case FishingRodItem ignored -> setFishingBobber(user, tickDelta); - case TridentItem ignored -> set(user, 0, 2.5, simulated, 0.05, 0.99, accurate, tickDelta, EntityType.TRIDENT); - case SnowballItem ignored -> set(user, 0, 1.5, simulated, 0.03, 0.8, accurate, tickDelta, EntityType.SNOWBALL); - case EggItem ignored -> set(user, 0, 1.5, simulated, 0.03, 0.8, accurate, tickDelta, EntityType.EGG); - case EnderPearlItem ignored -> set(user, 0, 1.5, simulated, 0.03, 0.8, accurate, tickDelta, EntityType.ENDER_PEARL); - case ExperienceBottleItem ignored -> set(user, -20, 0.7, simulated, 0.07, 0.8, accurate, tickDelta, EntityType.EXPERIENCE_BOTTLE); - case SplashPotionItem ignored -> set(user, -20, 0.5, simulated, 0.05, 0.8, accurate, tickDelta, EntityType.SPLASH_POTION); - case LingeringPotionItem ignored -> set(user, -20, 0.5, simulated, 0.05, 0.8, accurate, tickDelta, EntityType.LINGERING_POTION); - default -> { - return false; - } - } - - return true; - } - - public void set(Entity user, double roll, double speed, double simulated, double gravity, double waterDrag, boolean accurate, float tickDelta, EntityType type) { - Utils.set(pos, user, tickDelta).add(0, user.getEyeHeight(user.getPose()), 0); - - double yaw; - double pitch; - - if (user == mc.player && Rotations.rotating) { - yaw = Rotations.serverYaw; - pitch = Rotations.serverPitch; - } else { - yaw = user.getYaw(tickDelta); - pitch = user.getPitch(tickDelta); - } - - double x, y, z; - - if (simulated == 0) { - x = -Math.sin(yaw * 0.017453292) * Math.cos(pitch * 0.017453292); - y = -Math.sin((pitch + roll) * 0.017453292); - z = Math.cos(yaw * 0.017453292) * Math.cos(pitch * 0.017453292); - } - else { - Vec3d vec3d = user.getOppositeRotationVector(1.0F); - Quaterniond quaternion = new Quaterniond().setAngleAxis(simulated, vec3d.x, vec3d.y, vec3d.z); - Vec3d vec3d2 = user.getRotationVec(1.0F); - Vector3d vector3f = new Vector3d(vec3d2.x, vec3d2.y, vec3d2.z); - vector3f.rotate(quaternion); - - x = vector3f.x; - y = vector3f.y; - z = vector3f.z; - } - - velocity.set(x, y, z).normalize().mul(speed); - - if (accurate) { - Vec3d vel = user.getVelocity(); - velocity.add(vel.x, user.isOnGround() ? 0.0D : vel.y, vel.z); - } - - this.simulatingEntity = type.create(mc.world, null); - this.gravity = gravity; - this.airDrag = 0.99; - this.waterDrag = waterDrag; - this.width = type.getWidth(); - this.height = type.getHeight(); - } - - - // fired projectiles - - public boolean set(Entity entity, boolean accurate) { - // skip entities in ground - if (entity instanceof ProjectileInGroundAccessor ppe && ppe.meteor$invokeIsInGround()) return false; - - if (entity instanceof ArrowEntity) { - set(entity, 0.05, 0.6, accurate); - } else if (entity instanceof TridentEntity) { - set(entity, 0.05, 0.99, accurate); - } - - else if (entity instanceof EnderPearlEntity || entity instanceof SnowballEntity || entity instanceof EggEntity) { - set(entity, 0.03, 0.8, accurate); - } else if (entity instanceof ExperienceBottleEntity) { - set(entity, 0.07, 0.8, accurate); - } else if (entity instanceof PotionEntity) { - set(entity, 0.05, 0.8, accurate); - } - - else if (entity instanceof WitherSkullEntity || entity instanceof FireballEntity || entity instanceof DragonFireballEntity || entity instanceof WindChargeEntity) { - // drag isn't actually 1, but this provides accurate results in 99.9% in of real situations. - set(entity, 0, 1.0, accurate); - this.airDrag = 1.0; - } - else { - return false; - } - - if (entity.hasNoGravity()) { - this.gravity = 0; - } - - return true; - } - - public void set(Entity entity, double gravity, double waterDrag, boolean accurate) { - pos.set(entity.getX(), entity.getY(), entity.getZ()); - - double speed = entity.getVelocity().length(); - velocity.set(entity.getVelocity().x, entity.getVelocity().y, entity.getVelocity().z).normalize().mul(speed); - - if (accurate) { - Vec3d vel = entity.getVelocity(); - velocity.add(vel.x, entity.isOnGround() ? 0.0D : vel.y, vel.z); - } - - this.simulatingEntity = entity; - this.gravity = gravity; - this.airDrag = 0.99; - this.waterDrag = waterDrag; - this.width = entity.getWidth(); - this.height = entity.getHeight(); - } - - public void setFishingBobber(Entity user, float tickDelta) { - double yaw; - double pitch; - - if (user == mc.player && Rotations.rotating) { - yaw = Rotations.serverYaw; - pitch = Rotations.serverPitch; - } else { - yaw = user.getYaw(tickDelta); - pitch = user.getPitch(tickDelta); - } - - double h = Math.cos(-yaw * 0.017453292F - 3.1415927F); - double i = Math.sin(-yaw * 0.017453292F - 3.1415927F); - double j = -Math.cos(-pitch * 0.017453292F); - double k = Math.sin(-pitch * 0.017453292F); - - Utils.set(pos, user, tickDelta).sub(i * 0.3, 0, h * 0.3).add(0, user.getEyeHeight(user.getPose()), 0); - - velocity.set(-i, MathHelper.clamp(-(k / j), -5, 5), -h); - - double l = velocity.length(); - velocity.mul(0.6 / l + 0.5, 0.6 / l + 0.5, 0.6 / l + 0.5); - - simulatingEntity = EntityType.FISHING_BOBBER.create(mc.world, null); - gravity = 0.03; - airDrag = 0.92; - waterDrag = 0; - width = EntityType.FISHING_BOBBER.getWidth(); - height = EntityType.FISHING_BOBBER.getHeight(); - } - - public HitResult tick() { - // Apply velocity - ((IVec3d) prevPos3d).meteor$set(pos); - pos.add(velocity); - - // Update velocity - velocity.mul(isTouchingWater() ? waterDrag : airDrag); - velocity.sub(0, gravity, 0); - - // Check if below world - if (pos.y < mc.world.getBottomY()) return MissHitResult.INSTANCE; - - // Check if chunk is loaded - int chunkX = ChunkSectionPos.getSectionCoord(pos.x); - int chunkZ = ChunkSectionPos.getSectionCoord(pos.z); - if (!mc.world.getChunkManager().isChunkLoaded(chunkX, chunkZ)) return MissHitResult.INSTANCE; - - // Check for collision - ((IVec3d) pos3d).meteor$set(pos); - if (pos3d.equals(prevPos3d)) return MissHitResult.INSTANCE; - - HitResult hitResult = getCollision(); - - return hitResult.getType() == HitResult.Type.MISS ? null : hitResult; - } - - private boolean isTouchingWater() { - blockPos.set(pos.x, pos.y, pos.z); - - FluidState fluidState = mc.world.getFluidState(blockPos); - if (fluidState.getFluid() != Fluids.WATER && fluidState.getFluid() != Fluids.FLOWING_WATER) return false; - - return pos.y - (int) pos.y <= fluidState.getHeight(); - } - - private HitResult getCollision() { - HitResult hitResult = mc.world.raycast(new RaycastContext(prevPos3d, pos3d, RaycastContext.ShapeType.COLLIDER, waterDrag == 0 ? RaycastContext.FluidHandling.ANY : RaycastContext.FluidHandling.NONE, simulatingEntity)); - if (hitResult.getType() != HitResult.Type.MISS) { - ((IVec3d) pos3d).meteor$set(hitResult.getPos().x, hitResult.getPos().y, hitResult.getPos().z); - } - - // Vanilla uses the current and next positions to check collisions, we use the previous and current positions - Box box = new Box(prevPos3d.x - (width / 2f), prevPos3d.y, prevPos3d.z - (width / 2f), prevPos3d.x + (width / 2f), prevPos3d.y + height, prevPos3d.z + (width / 2f)) - .stretch(velocity.x, velocity.y, velocity.z).expand(1.0D); - HitResult hitResult2 = ProjectileUtil.getEntityCollision( - mc.world, - simulatingEntity, - prevPos3d, - pos3d, - box, - entity -> !entity.isSpectator() && entity.isAlive() && entity.canHit(), - ProjectileUtil.getToleranceMargin(simulatingEntity) - ); - if (hitResult2 != null) { - hitResult = hitResult2; - } - - return hitResult; - } -} diff --git a/src/main/java/meteordevelopment/meteorclient/utils/entity/simulator/ProjectileEntitySimulator.java b/src/main/java/meteordevelopment/meteorclient/utils/entity/simulator/ProjectileEntitySimulator.java new file mode 100644 index 0000000000..0c125b7e6c --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/utils/entity/simulator/ProjectileEntitySimulator.java @@ -0,0 +1,455 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.utils.entity.simulator; + +import meteordevelopment.meteorclient.mixin.CrossbowItemAccessor; +import meteordevelopment.meteorclient.mixin.ProjectileInGroundAccessor; +import meteordevelopment.meteorclient.mixininterface.IVec3d; +import meteordevelopment.meteorclient.systems.modules.Modules; +import meteordevelopment.meteorclient.systems.modules.movement.NoSlow; +import meteordevelopment.meteorclient.systems.modules.movement.Sneak; +import meteordevelopment.meteorclient.utils.Utils; +import meteordevelopment.meteorclient.utils.player.Rotations; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.ChargedProjectilesComponent; +import net.minecraft.enchantment.Enchantments; +import net.minecraft.entity.*; +import net.minecraft.entity.mob.BreezeEntity; +import net.minecraft.entity.projectile.*; +import net.minecraft.entity.projectile.thrown.*; +import net.minecraft.fluid.FluidState; +import net.minecraft.item.*; +import net.minecraft.registry.tag.FluidTags; +import net.minecraft.registry.tag.TagKey; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.EntityHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.*; +import net.minecraft.world.RaycastContext; +import org.joml.Quaterniond; +import org.joml.Vector3d; + +import java.util.ArrayList; +import java.util.Collection; + +import static meteordevelopment.meteorclient.MeteorClient.mc; + +public class ProjectileEntitySimulator { + private final BlockPos.Mutable blockPos = new BlockPos.Mutable(); + + private final Vec3d pos3d = new Vec3d(0, 0, 0); + private final Vec3d prevPos3d = new Vec3d(0, 0, 0); + + public final Vector3d pos = new Vector3d(); + private final Vector3d velocity = new Vector3d(); + + private ProjectileEntity simulatingEntity; + private EntityDimensions dimensions; + private int age, pierceLevel; + private double gravity; + private float airDrag, waterDrag; + private boolean isTouchingWater; + + public record MotionData(float power, float roll, double gravity, float airDrag, float waterDrag, EntityType entity) { + public MotionData withPower(float power) { + return new MotionData(power, this.roll(), this.gravity(), this.airDrag(), this.waterDrag(), this.entity()); + } + } + + // https://minecraft.wiki/w/Projectile + // https://minecraft.wiki/w/Entity#Motion + + // ThrownEntity + private static final MotionData EGG = new MotionData(1.5f, 0, 0.03, 0.99f, 0.8f, EntityType.EGG); + private static final MotionData ENDER_PEARL = new MotionData(1.5f, 0, 0.03, 0.99f, 0.8f, EntityType.ENDER_PEARL); + private static final MotionData SNOWBALL = new MotionData(1.5f, 0, 0.03, 0.99f, 0.8f, EntityType.SNOWBALL); + private static final MotionData EXPERIENCE_BOTTLE = new MotionData(0.7f, -20, 0.07, 0.99f, 0.8f, EntityType.EXPERIENCE_BOTTLE); + private static final MotionData LINGERING_POTION = new MotionData(0.5f, -20, 0.05, 0.99f, 0.8f, EntityType.LINGERING_POTION); + private static final MotionData SPLASH_POTION = new MotionData(0.5f, -20, 0.05, 0.99f, 0.8f, EntityType.SPLASH_POTION); + + // ExplosiveProjectileEntity + private static final MotionData EXPLOSIVE = new MotionData(0, 0, 0, 1, 1, null); // fireball, wither skull, etc. + private static final MotionData WIND_CHARGE = new MotionData(1.5f, 0, 0, 1, 1, EntityType.WIND_CHARGE); + + // PersistentProjectileEntity + private static final MotionData ARROW = new MotionData(0, 0, 0.05, 0.99f, 0.6f, EntityType.ARROW); + private static final MotionData TRIDENT = new MotionData(2.5f, 0, 0.05, 0.99f, 0.99f, EntityType.TRIDENT); + + // Other + private static final MotionData FIREWORK_ROCKET = new MotionData(0, 0, 0, 1, 1, EntityType.FIREWORK_ROCKET); + private static final MotionData FISHING_BOBBER = new MotionData(0, 0, 0.03, 0.92f, 0, EntityType.FISHING_BOBBER); + private static final MotionData LLAMA_SPIT = new MotionData(1.5f, 0, 0.06, 0.99f, 0, EntityType.LLAMA_SPIT); + + + // held items + + public boolean set(Entity user, ItemStack itemStack, double angleOffset, boolean accurate, float tickDelta) { + Item item = itemStack.getItem(); + + switch (item) { + case BowItem ignored -> { + if (!(user instanceof LivingEntity livingEntity)) return false; + float charge = BowItem.getPullProgress(livingEntity.getItemUseTime()); + + if (charge <= 0.1) { + if (user == mc.player) charge = 1; + else return false; + } + + set(user, angleOffset, accurate, tickDelta, ARROW.withPower(charge * 3)); + } + case CrossbowItem ignored -> { + ChargedProjectilesComponent projectilesComponent = itemStack.get(DataComponentTypes.CHARGED_PROJECTILES); + if (projectilesComponent == null) return false; + + float speed = CrossbowItemAccessor.meteor$getSpeed(projectilesComponent); + if (projectilesComponent.contains(Items.FIREWORK_ROCKET)) { + set(user, angleOffset, accurate, tickDelta, FIREWORK_ROCKET.withPower(speed)); + } + else set(user, angleOffset, accurate, tickDelta, ARROW.withPower(speed)); + + this.pierceLevel = projectilesComponent.contains(Items.FIREWORK_ROCKET) ? 0 : Utils.getEnchantmentLevel(itemStack, Enchantments.PIERCING); + } + case WindChargeItem ignored -> set(user, angleOffset, accurate, tickDelta, WIND_CHARGE); + case TridentItem ignored -> set(user, angleOffset, accurate, tickDelta, TRIDENT); + case SnowballItem ignored -> set(user, angleOffset, accurate, tickDelta, SNOWBALL); + case EggItem ignored -> set(user, angleOffset, accurate, tickDelta, EGG); + case EnderPearlItem ignored -> set(user, angleOffset, accurate, tickDelta, ENDER_PEARL); + case ExperienceBottleItem ignored -> set(user, angleOffset, accurate, tickDelta, EXPERIENCE_BOTTLE); + case SplashPotionItem ignored -> set(user, angleOffset, accurate, tickDelta, SPLASH_POTION); + case LingeringPotionItem ignored -> set(user, angleOffset, accurate, tickDelta, LINGERING_POTION); + case FishingRodItem ignored -> setFishingBobber(user, tickDelta, FISHING_BOBBER); + default -> { + return false; + } + } + + return true; + } + + public void set(Entity user, double angleOffset, boolean accurate, float tickDelta, MotionData data) { + // I lost my mind for an hour trying to figure out why arrows and tridents were spawning lower than expected, + // and it was because no slow air strict was silently causing the player to crouch AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + EntityPose pose = user.getPose(); + if (user == mc.player && (Modules.get().get(NoSlow.class).airStrict() || Modules.get().get(Sneak.class).doPacket())) pose = EntityPose.CROUCHING; + Utils.set(pos, user, tickDelta).add(0, user.getEyeHeight(pose) - 0.1f, 0); + + double yaw; + double pitch; + + if (user == mc.player && Rotations.rotating) { + yaw = Rotations.serverYaw; + pitch = Rotations.serverPitch; + } else { + yaw = user.getYaw(tickDelta); + pitch = user.getPitch(tickDelta); + } + + double x, y, z; + + if (angleOffset == 0) { + x = -Math.sin(yaw * 0.017453292) * Math.cos(pitch * 0.017453292); + y = -Math.sin((pitch + data.roll()) * 0.017453292); + z = Math.cos(yaw * 0.017453292) * Math.cos(pitch * 0.017453292); + } + else { + Vec3d oppositeRotationVec = user.getOppositeRotationVector(1.0F); + Quaterniond quaternion = new Quaterniond().setAngleAxis(angleOffset, oppositeRotationVec.x, oppositeRotationVec.y, oppositeRotationVec.z); + Vec3d rotationVec = user.getRotationVec(1.0F); + Vector3d vector3d = new Vector3d(rotationVec.x, rotationVec.y, rotationVec.z); + vector3d.rotate(quaternion); + + x = vector3d.x; + y = vector3d.y; + z = vector3d.z; + } + + velocity.set(x, y, z).normalize().mul(data.power()); + + if (accurate) { + Vec3d vel = user.getMovement(); + velocity.add(vel.x, user.isOnGround() ? 0.0D : vel.y, vel.z); + } + + setSimulationData((ProjectileEntity) data.entity().create(mc.world, null), data); + } + + public void setFishingBobber(Entity user, float tickDelta, MotionData data) { + double yaw; + double pitch; + + if (user == mc.player && Rotations.rotating) { + yaw = Rotations.serverYaw; + pitch = Rotations.serverPitch; + } else { + yaw = user.getYaw(tickDelta); + pitch = user.getPitch(tickDelta); + } + + double h = Math.cos(-yaw * 0.017453292F - 3.1415927F); + double i = Math.sin(-yaw * 0.017453292F - 3.1415927F); + double j = -Math.cos(-pitch * 0.017453292F); + double k = Math.sin(-pitch * 0.017453292F); + + EntityPose pose = user.getPose(); + if (user == mc.player && (Modules.get().get(NoSlow.class).airStrict() || Modules.get().get(Sneak.class).doPacket())) pose = EntityPose.CROUCHING; + Utils.set(pos, user, tickDelta).sub(i * 0.3, 0, h * 0.3).add(0, user.getEyeHeight(pose), 0); + + velocity.set(-i, MathHelper.clamp(-(k / j), -5, 5), -h); + + double l = velocity.length(); + velocity.mul(0.6 / l + 0.5, 0.6 / l + 0.5, 0.6 / l + 0.5); + + setSimulationData((ProjectileEntity) data.entity().create(mc.world, null), data); + } + + + // fired projectiles + + public boolean set(Entity entity) { + // skip entities in ground + if (entity instanceof ProjectileInGroundAccessor ppe && ppe.meteor$invokeIsInGround()) return false; + + switch (entity) { + case ArrowEntity e -> set(e, ARROW); + case SpectralArrowEntity e -> set(e, ARROW); + case TridentEntity e -> set(e, TRIDENT); + case EnderPearlEntity e -> set(e, ENDER_PEARL); + case SnowballEntity e -> set(e, SNOWBALL); + case EggEntity e -> set(e, EGG); + case ExperienceBottleEntity e -> set(e, EXPERIENCE_BOTTLE); + case SplashPotionEntity e -> set(e, SPLASH_POTION); + case LingeringPotionEntity e -> set(e, LINGERING_POTION); + case AbstractWindChargeEntity e -> set(e, WIND_CHARGE); + case ExplosiveProjectileEntity e -> set(e, EXPLOSIVE); + case LlamaSpitEntity e -> set(e, LLAMA_SPIT); + default -> { + return false; + } + } + + if (entity.hasNoGravity()) { + this.gravity = 0; + } + + return true; + } + + public void set(ProjectileEntity entity, MotionData data) { + pos.set(entity.getX(), entity.getY(), entity.getZ()); + + double speed = entity.getVelocity().length(); + velocity.set(entity.getVelocity().x, entity.getVelocity().y, entity.getVelocity().z).normalize().mul(speed); + + setSimulationData(entity, data); + } + + private void setSimulationData(ProjectileEntity entity, MotionData data) { + this.gravity = data.gravity(); + this.airDrag = data.airDrag(); + this.waterDrag = data.waterDrag(); + this.simulatingEntity = entity; + this.dimensions = simulatingEntity.getDimensions(simulatingEntity.getPose()); + this.isTouchingWater = simulatingEntity.isTouchingWater(); + this.age = simulatingEntity.age; + this.pierceLevel = 0; + } + + public SimulationStep tick() { + age++; + ((IVec3d) prevPos3d).meteor$set(pos); + + // gravity -> drag -> position + if (simulatingEntity instanceof ThrownEntity || simulatingEntity instanceof ExplosiveProjectileEntity) { + velocity.sub(0, gravity, 0); + velocity.mul(isTouchingWater ? waterDrag : airDrag); + pos.add(velocity); + tickIsTouchingWater(); + } + // position -> drag -> gravity + else if (simulatingEntity instanceof PersistentProjectileEntity || simulatingEntity instanceof LlamaSpitEntity) { + pos.add(velocity); + velocity.mul(isTouchingWater ? waterDrag : airDrag); + velocity.sub(0, gravity, 0); + tickIsTouchingWater(); + } + // gravity -> position > drag + // accurate for fishing bobbers and firework rockets, will need to revisit if more projectiles are added + else if (simulatingEntity instanceof ProjectileEntity) { + tickIsTouchingWater(); + velocity.sub(0, gravity, 0); + pos.add(velocity); + velocity.mul(isTouchingWater ? waterDrag : airDrag); + } + + // Check if below world + if (pos.y < mc.world.getBottomY()) return SimulationStep.MISS; + + // Check if chunk is loaded + int chunkX = ChunkSectionPos.getSectionCoord(pos.x); + int chunkZ = ChunkSectionPos.getSectionCoord(pos.z); + if (!mc.world.getChunkManager().isChunkLoaded(chunkX, chunkZ)) return SimulationStep.MISS; + + // Check for collision + ((IVec3d) pos3d).meteor$set(pos); + if (pos3d.equals(prevPos3d)) return SimulationStep.MISS; + + return getCollision(); + } + + /** + * {@link Entity#updateMovementInFluid(TagKey, double)} + */ + public void tickIsTouchingWater() { + Box box = dimensions.getBoxAt(pos.x, pos.y, pos.z).contract(0.001); + int minX = MathHelper.floor(box.minX); + int maxX = MathHelper.ceil(box.maxX); + int minY = MathHelper.floor(box.minY); + int maxY = MathHelper.ceil(box.maxY); + int minZ = MathHelper.floor(box.minZ); + int maxZ = MathHelper.ceil(box.maxZ); + + for (int x = minX; x < maxX; x++) { + for (int y = minY; y < maxY; y++) { + for (int z = minZ; z < maxZ; z++) { + blockPos.set(x, y, z); + FluidState fluidState = mc.world.getFluidState(blockPos); + if (fluidState.isIn(FluidTags.WATER)) { + double fluidY = y + fluidState.getHeight(mc.world, blockPos); + if (fluidY >= box.minY) { + isTouchingWater = true; + return; + } + } + } + } + } + + isTouchingWater = false; + } + + /** + * {@link ProjectileUtil#getCollision(Vec3d, Entity, java.util.function.Predicate, Vec3d, net.minecraft.world.World, float, RaycastContext.ShapeType)} + *

+ * Vanilla checks from the current to the next position, while we check from the previous to the current positions. + * This solves the issue of the collision check from the starting position not working properly - otherwise, the + * simulated projectile may move from its start position through a block, only running the collision check afterwards. + * The vanilla game has other code to deal with this but this is the easiest way for us to fix it. + */ + private SimulationStep getCollision() { + HitResult blockCollision = mc.world.getCollisionsIncludingWorldBorder(new RaycastContext( + prevPos3d, + pos3d, + RaycastContext.ShapeType.COLLIDER, + waterDrag == 0 ? RaycastContext.FluidHandling.ANY : RaycastContext.FluidHandling.NONE, + simulatingEntity + )); + if (blockCollision.getType() != HitResult.Type.MISS) { + ((IVec3d) pos3d).meteor$set(blockCollision.getPos()); + } + + /** {@link PersistentProjectileEntity#applyCollision(BlockHitResult)} */ + if (simulatingEntity instanceof PersistentProjectileEntity) { + Collection entityCollisions = ProjectileUtil.collectPiercingCollisions( + mc.world, + simulatingEntity, + prevPos3d, + pos3d, + dimensions.getBoxAt(prevPos3d).stretch(velocity.x, velocity.y, velocity.z).expand(1.0D), + entity -> !entity.isSpectator() && entity.isAlive() && entity.canHit(), + getToleranceMargin(), + RaycastContext.ShapeType.COLLIDER, + false + ); + + // prevent simulating projectiles as colliding with ourselves on the first tick of movement + entityCollisions.removeIf(collision -> age <= 1 && collision.getEntity() == mc.player); + if (entityCollisions.isEmpty()) return new SimulationStep(hitOrDeflect(blockCollision), blockCollision); + + boolean stop = false; + ArrayList hits = new ArrayList<>(); + for (EntityHitResult result : entityCollisions) { + boolean hit = hitOrDeflect(result); + if (!hit) break; + + hits.add(result); + if (pierceLevel <= 0) { + stop = true; + break; + } + + pierceLevel--; + } + + return new SimulationStep(stop, hits.toArray(new HitResult[0])); + } + else { + HitResult entityCollision = ProjectileUtil.getEntityCollision( + mc.world, + simulatingEntity, + prevPos3d, + pos3d, + dimensions.getBoxAt(prevPos3d).stretch(velocity.x, velocity.y, velocity.z).expand(1.0D), + entity -> !entity.isSpectator() && entity.isAlive() && entity.canHit(), + getToleranceMargin() + ); + + if (entityCollision == null || (age <= 1 && entityCollision instanceof EntityHitResult ehr && ehr.getEntity() == mc.player)) { + return new SimulationStep(hitOrDeflect(blockCollision), blockCollision); + } + + if (hitOrDeflect(entityCollision)) return new SimulationStep(true, entityCollision); + return new SimulationStep(false); + } + } + + /** + * {@link ProjectileEntity#hitOrDeflect(HitResult)} + * {@link ProjectileEntity#onCollision(HitResult)} + * {@link ProjectileDeflection} + */ + private boolean hitOrDeflect(HitResult hitResult) { + if (hitResult instanceof EntityHitResult entityHitResult) { + Entity entity = entityHitResult.getEntity(); + Utils.set(pos, entityHitResult.getPos()); + + if ((entity instanceof BreezeEntity && !(simulatingEntity instanceof AbstractWindChargeEntity)) || entity.getProjectileDeflection(simulatingEntity) == ProjectileDeflection.SIMPLE) { + velocity.mul(-0.5); + return false; + } + + // if we keep this it makes trajectories look awful when you throw wind charges +// if (entity.getType().isIn(EntityTypeTags.REDIRECTABLE_PROJECTILE) && entity instanceof ProjectileEntity projectileEntity) { +// Utils.set(velocity, projectileEntity.getRotationVector()); +// return false; +// } + + // not perfectly accurate but you would otherwise have to trace significant amounts of the damage stack + if (entity instanceof LivingEntity livingEntity && livingEntity.isBlocking() && simulatingEntity instanceof PersistentProjectileEntity) { + velocity.mul(-0.5).mul(0.2); + return velocity.lengthSquared() < 1.0E-7; + } + + return true; + } + else if (hitResult instanceof BlockHitResult bhr) { + Utils.set(pos, bhr.getPos()); + + if (simulatingEntity.deflectsAgainstWorldBorder() && bhr.isAgainstWorldBorder()) { + velocity.mul(-0.5).mul(0.2); + return false; + } + + return bhr.getType() != HitResult.Type.MISS; + } + + return false; + } + + private float getToleranceMargin() { + return Math.max(0.0F, Math.min(0.3F, (age - 2) / 20.0F)); + } +} diff --git a/src/main/java/meteordevelopment/meteorclient/utils/entity/simulator/SimulationStep.java b/src/main/java/meteordevelopment/meteorclient/utils/entity/simulator/SimulationStep.java new file mode 100644 index 0000000000..af46e70dbb --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/utils/entity/simulator/SimulationStep.java @@ -0,0 +1,20 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.utils.entity.simulator; + +import net.minecraft.util.hit.HitResult; + +public class SimulationStep { + public static final SimulationStep MISS = new SimulationStep(true); + + public boolean shouldStop; + public HitResult[] hitResults; + + public SimulationStep(boolean stop, HitResult... hitResults) { + this.shouldStop = stop; + this.hitResults = hitResults; + } +} diff --git a/src/main/resources/meteor-client.accesswidener b/src/main/resources/meteor-client.accesswidener index 35e17dc785..996e32cc19 100644 --- a/src/main/resources/meteor-client.accesswidener +++ b/src/main/resources/meteor-client.accesswidener @@ -30,3 +30,5 @@ accessible method net/minecraft/client/network/ClientPlayerInteractionManager accessible class net/minecraft/client/gui/DrawContext$ScissorStack accessible field net/minecraft/client/render/RenderSetup outputTarget Lnet/minecraft/client/render/OutputTarget; + +accessible method net/minecraft/entity/projectile/ProjectileEntity deflectsAgainstWorldBorder ()Z