diff --git a/common/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java b/common/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java index fabadace5..064d1f425 100644 --- a/common/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java +++ b/common/src/main/java/com/lambda/mixin/entity/ClientPlayerEntityMixin.java @@ -25,11 +25,11 @@ import com.lambda.interaction.PlayerPacketManager; import com.lambda.interaction.request.rotation.RotationManager; import com.lambda.module.modules.player.PortalGui; +import com.lambda.module.modules.render.ViewModel; import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.DeathScreen; import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.screen.ingame.HandledScreen; import net.minecraft.client.input.Input; +import net.minecraft.client.network.AbstractClientPlayerEntity; import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.entity.MovementType; import net.minecraft.entity.damage.DamageSource; @@ -154,6 +154,18 @@ void onSwingHandPre(Hand hand, CallbackInfo ci) { if (EventFlow.post(new PlayerEvent.SwingHand(hand)).isCanceled()) ci.cancel(); } + @Redirect(method = "swingHand", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/AbstractClientPlayerEntity;swingHand(Lnet/minecraft/util/Hand;)V")) + private void adjustSwing(AbstractClientPlayerEntity instance, Hand hand) { + ViewModel viewModel = ViewModel.INSTANCE; + + if (!viewModel.isEnabled()) { + instance.swingHand(hand, false); + return; + } + + viewModel.adjustSwing(hand, instance); + } + @Inject(method = "damage", at = @At("HEAD"), cancellable = true) public void damage(DamageSource source, float amount, CallbackInfoReturnable cir) { if (EventFlow.post(new PlayerEvent.Damage(source, amount)).isCanceled()) cir.setReturnValue(false); diff --git a/common/src/main/java/com/lambda/mixin/entity/LivingEntityMixin.java b/common/src/main/java/com/lambda/mixin/entity/LivingEntityMixin.java index 0092e7eb3..aa9e62d63 100644 --- a/common/src/main/java/com/lambda/mixin/entity/LivingEntityMixin.java +++ b/common/src/main/java/com/lambda/mixin/entity/LivingEntityMixin.java @@ -21,15 +21,14 @@ import com.lambda.event.EventFlow; import com.lambda.event.events.MovementEvent; import com.lambda.interaction.request.rotation.RotationManager; +import com.lambda.module.modules.render.ViewModel; import net.minecraft.entity.LivingEntity; import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.Vec3d; 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.Redirect; -import org.spongepowered.asm.mixin.injection.Slice; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.*; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(LivingEntity.class) @@ -38,6 +37,9 @@ public abstract class LivingEntityMixin extends EntityMixin { @Shadow protected abstract float getJumpVelocity(); + @Unique + private final LivingEntity lambda$instance = (LivingEntity) (Object) this; + /** * Overwrites the jump function to use our rotation and movements *
{@code
@@ -55,7 +57,7 @@ public abstract class LivingEntityMixin extends EntityMixin {
      */
     @Inject(method = "jump", at = @At("HEAD"), cancellable = true)
     void onJump(CallbackInfo ci) {
-        LivingEntity self = (LivingEntity) (Object) this;
+        LivingEntity self = lambda$instance;
         if (self != Lambda.getMc().player) return;
         ci.cancel();
 
@@ -78,15 +80,14 @@ void onJump(CallbackInfo ci) {
 
     @Inject(method = "travel", at = @At("HEAD"), cancellable = true)
     void onTravelPre(Vec3d movementInput, CallbackInfo ci) {
-        LivingEntity entity = (LivingEntity) (Object) this;
-        if (EventFlow.post(new MovementEvent.Entity.Pre(entity, movementInput)).isCanceled()) {
+        if (EventFlow.post(new MovementEvent.Entity.Pre(lambda$instance, movementInput)).isCanceled()) {
             ci.cancel();
         }
     }
 
     @Inject(method = "travel", at = @At("TAIL"))
     void onTravelPost(Vec3d movementInput, CallbackInfo ci) {
-        EventFlow.post(new MovementEvent.Entity.Post((LivingEntity) (Object) this, movementInput));
+        EventFlow.post(new MovementEvent.Entity.Post(lambda$instance, movementInput));
     }
 
     /**
@@ -123,7 +124,7 @@ private float hookModifyFallFlyingPitch(LivingEntity entity) {
      */
     @Redirect(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;getYaw()F"), slice = @Slice(to = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;getYaw()F", ordinal = 1)))
     private float rotBody(LivingEntity entity) {
-        if ((Object) this != Lambda.getMc().player) {
+        if (lambda$instance != Lambda.getMc().player) {
             return entity.getYaw();
         }
 
@@ -154,11 +155,18 @@ private float rotBody(LivingEntity entity) {
      */
     @Redirect(method = "turnHead", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;getYaw()F"))
     private float rotHead(LivingEntity entity) {
-        if ((Object) this != Lambda.getMc().player) {
+        if (lambda$instance != Lambda.getMc().player) {
             return entity.getYaw();
         }
 
         Float yaw = RotationManager.getRenderYaw();
         return (yaw == null) ? entity.getYaw() : yaw;
     }
+
+    @ModifyConstant(method = "getHandSwingDuration", constant = @Constant(intValue = 6))
+    private int getHandSwingDuration(int constant) {
+        if (lambda$instance != Lambda.getMc().player || ViewModel.INSTANCE.isDisabled()) return constant;
+
+        return ViewModel.INSTANCE.getSwingDuration();
+    }
 }
diff --git a/common/src/main/java/com/lambda/mixin/render/HeldItemRendererMixin.java b/common/src/main/java/com/lambda/mixin/render/HeldItemRendererMixin.java
new file mode 100644
index 000000000..905a74025
--- /dev/null
+++ b/common/src/main/java/com/lambda/mixin/render/HeldItemRendererMixin.java
@@ -0,0 +1,88 @@
+package com.lambda.mixin.render;
+
+import com.google.common.base.MoreObjects;
+import com.lambda.Lambda;
+import com.lambda.module.modules.render.ViewModel;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.AbstractClientPlayerEntity;
+import net.minecraft.client.render.VertexConsumerProvider;
+import net.minecraft.client.render.item.HeldItemRenderer;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.Hand;
+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.ModifyArg;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(HeldItemRenderer.class)
+public class HeldItemRendererMixin {
+    @Final @Shadow private MinecraftClient client;
+    @Shadow private ItemStack mainHand;
+    @Shadow private ItemStack offHand;
+    @Shadow private float equipProgressMainHand;
+    @Shadow private float equipProgressOffHand;
+
+    @Inject(method = "renderFirstPersonItem", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/item/HeldItemRenderer;renderArmHoldingItem(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;IFFLnet/minecraft/util/Arm;)V"))
+    private void onRenderArmHoldingItem(AbstractClientPlayerEntity player, float tickDelta, float pitch, Hand hand, float swingProgress, ItemStack itemStack, float equipProgress, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, CallbackInfo ci) {
+        if (!ViewModel.INSTANCE.isEnabled()) return;
+
+        ViewModel.INSTANCE.transform(itemStack, hand, matrices);
+    }
+
+    @Inject(method = "renderFirstPersonItem", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/item/HeldItemRenderer;renderItem(Lnet/minecraft/entity/LivingEntity;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/render/model/json/ModelTransformationMode;ZLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V"))
+    private void onRenderFirstPersonItem(AbstractClientPlayerEntity player, float tickDelta, float pitch, Hand hand, float swingProgress, ItemStack itemStack, float equipProgress, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, CallbackInfo ci) {
+        if (!ViewModel.INSTANCE.isEnabled()) return;
+
+        ViewModel.INSTANCE.transform(itemStack, hand, matrices);
+    }
+
+    @ModifyArg(method = "updateHeldItems", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/math/MathHelper;clamp(FFF)F", ordinal = 2), index = 0)
+    private float modifyEquipProgressMainHand(float value) {
+        if (client.player == null || ViewModel.INSTANCE.isDisabled()) return value;
+
+        ViewModel config = ViewModel.INSTANCE;
+        ItemStack currentStack = client.player.getMainHandStack();
+        if (config.getOldAnimations() && !config.getSwapAnimation()) {
+            mainHand = currentStack;
+        }
+
+        float progress = config.getOldAnimations() ? 1 : (float) Math.pow(client.player.getAttackCooldownProgress(1), 3);
+
+        return (ItemStack.areEqual(mainHand, currentStack) ? progress : 0) - equipProgressMainHand;
+    }
+
+    @ModifyArg(method = "updateHeldItems", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/math/MathHelper;clamp(FFF)F", ordinal = 3), index = 0)
+    private float modifyEquipProgressOffHand(float value) {
+        if (client.player == null || ViewModel.INSTANCE.isDisabled()) return value;
+
+        ViewModel config = ViewModel.INSTANCE;
+
+        ItemStack currentStack = client.player.getOffHandStack();
+        if (config.getOldAnimations() && !config.getSwapAnimation()) {
+            offHand = currentStack;
+        }
+
+        return (ItemStack.areEqual(offHand, currentStack) ? 1 : 0) - equipProgressOffHand;
+    }
+
+    @ModifyVariable(method = "renderItem(FLnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider$Immediate;Lnet/minecraft/client/network/ClientPlayerEntity;I)V", at = @At(value = "STORE", ordinal = 0), index = 6)
+    private float modifySwing(float swingProgress) {
+        ViewModel config = ViewModel.INSTANCE;
+        MinecraftClient mc = Lambda.getMc();
+        if (config.isDisabled() || mc.player == null) return swingProgress;
+        Hand hand = MoreObjects.firstNonNull(mc.player.preferredHand, Hand.MAIN_HAND);
+
+        if (hand == Hand.MAIN_HAND) {
+            return swingProgress + config.getMainSwingProgress();
+        } else if (hand == Hand.OFF_HAND) {
+            return swingProgress + config.getOffhandSwingProgress();
+        }
+
+        return swingProgress;
+    }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/lambda/module/modules/render/ViewModel.kt b/common/src/main/kotlin/com/lambda/module/modules/render/ViewModel.kt
new file mode 100644
index 000000000..9475833bc
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/module/modules/render/ViewModel.kt
@@ -0,0 +1,244 @@
+package com.lambda.module.modules.render
+
+import com.lambda.Lambda.mc
+import com.lambda.event.events.KeyboardEvent
+import com.lambda.event.events.MouseEvent
+import com.lambda.event.events.TickEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.module.Module
+import com.lambda.module.tag.ModuleTag
+import net.minecraft.client.network.AbstractClientPlayerEntity
+import net.minecraft.client.util.math.MatrixStack
+import net.minecraft.item.ItemStack
+import net.minecraft.util.Arm
+import net.minecraft.util.Hand
+import net.minecraft.util.math.RotationAxis
+import org.joml.Matrix4f
+import org.joml.Vector3f
+import org.joml.Vector3i
+import kotlin.math.tan
+
+object ViewModel : Module(
+    name = "View Model",
+    description = "Adjusts hand and held item rendering",
+    defaultTags = setOf(ModuleTag.RENDER)
+) {
+    private val page by setting("Page", Page.General)
+
+    private val swingMode by setting("Swing Mode", SwingMode.Standard, "Changes which hands swing") { page == Page.General }
+    val swingDuration by setting("Swing Duration", 6, 0..20, 1, "Adjusts how fast the player swings", "ticks") { page == Page.General }
+    private val noSwingDelay by setting("No Swing Delay", false, "Removes the delay between swings") { page == Page.General }
+    val mainSwingProgress by setting("Main Swing Progress", 0.0f, 0.0f..1.0f, 0.025f, "Renders as if the players main hand was this progress through the swing animation") { page == Page.General }
+    val offhandSwingProgress by setting("Offhand Swing Progress", 0.0f, 0.0f..1.0f, 0.025f, "Renders as if the players offhand was this progress through the swing animation") { page == Page.General }
+    val oldAnimations by setting("Old Animations", false, "Adjusts the animations to look like they did in 1.8") { page == Page.General }
+    val swapAnimation by setting("Swap Animation", true, "If disabled, removes the drop down animation when swapping item") { page == Page.General && oldAnimations }
+    //ToDo: Implement
+//    val shadow by setting("Shadows", true, "If disabled, removes shadows on the model") { page == Page.General }
+
+    private val splitScale by setting("Split Scale", false, "Splits left and right hand scale settings") { page == Page.Scale }
+    private val xScale by setting("X Scale", 1.0f, -1.0f..1.0f, 0.025f) { page == Page.Scale && !splitScale }.onValueChange { _, to -> leftXScale = to; rightXScale = to }
+    private val yScale by setting("Y Scale", 1.0f, -1.0f..1.0f, 0.025f) { page == Page.Scale && !splitScale }.onValueChange { _, to -> leftYScale = to; rightYScale = to }
+    private val zScale by setting("Z Scale", 1.0f, -1.0f..1.0f, 0.025f) { page == Page.Scale && !splitScale }.onValueChange { _, to -> leftZScale = to; rightZScale = to }
+    private var leftXScale by setting("Left X Scale", 1.0f, -1.0f..1.0f, 0.025f) { page == Page.Scale && splitScale }
+    private var leftYScale by setting("Left Y Scale", 1.0f, -1.0f..1.0f, 0.025f) { page == Page.Scale && splitScale }
+    private var leftZScale by setting("Left Z Scale", 1.0f, -1.0f..1.0f, 0.025f) { page == Page.Scale && splitScale }
+    private var rightXScale by setting("Right X Scale", 1.0f, -1.0f..1.0f, 0.025f) { page == Page.Scale && splitScale }
+    private var rightYScale by setting("Right Y Scale", 1.0f, -1.0f..1.0f, 0.025f) { page == Page.Scale && splitScale }
+    private var rightZScale by setting("Right Z Scale", 1.0f, -1.0f..1.0f, 0.025f) { page == Page.Scale && splitScale }
+
+    private val splitPosition by setting("Split Position", false, "Splits left and right position settings") { page == Page.Position }
+    private val xPosition by setting("X Position", 1.0f, -1.0f..1.0f, 0.025f) { page == Page.Position && !splitPosition }.onValueChange { _, to -> leftXPosition = to; rightXPosition = to }
+    private val yPosition by setting("Y Position", 1.0f, -1.0f..1.0f, 0.025f) { page == Page.Position && !splitPosition }.onValueChange { _, to -> leftYPosition = to; rightYPosition = to }
+    private val zPosition by setting("Z Position", 1.0f, -1.0f..1.0f, 0.025f) { page == Page.Position && !splitPosition }.onValueChange { _, to -> leftZPosition = to; rightZPosition = to }
+    private var leftXPosition by setting("Left X Position", 0.0f, -1.0f..1.0f, 0.025f) { page == Page.Position && splitPosition }
+    private var leftYPosition by setting("Left Y Position", 0.0f, -1.0f..1.0f, 0.025f) { page == Page.Position && splitPosition }
+    private var leftZPosition by setting("Left Z Position", 0.0f, -1.0f..1.0f, 0.025f) { page == Page.Position && splitPosition }
+    private var rightXPosition by setting("Right X Position", 0.0f, -1.0f..1.0f, 0.025f) { page == Page.Position && splitPosition }
+    private var rightYPosition by setting("Right Y Position", 0.0f, -1.0f..1.0f, 0.025f) { page == Page.Position && splitPosition }
+    private var rightZPosition by setting("Right Z Position", 0.0f, -1.0f..1.0f, 0.025f) { page == Page.Position && splitPosition }
+
+    private val splitRotation by setting("Split Rotation", false, "Splits left and right rotation settings") { page == Page.Rotation }
+    private val xRotation by setting("X Rotation", 0, -180..180, 1) { page == Page.Rotation && !splitRotation }.onValueChange { _, to -> leftXRotation = to; rightXRotation = to }
+    private val yRotation by setting("Y Rotation", 0, -180..180, 1) { page == Page.Rotation && !splitRotation }.onValueChange { _, to -> leftYRotation = to; rightYRotation = to }
+    private val zRotation by setting("Z Rotation", 0, -180..180, 1) { page == Page.Rotation && !splitRotation }.onValueChange { _, to -> leftZRotation = to; rightZRotation = to }
+    private var leftXRotation by setting("Left X Rotation", 0, -180..180, 1) { page == Page.Rotation && splitRotation }
+    private var leftYRotation by setting("Left Y Rotation", 0, -180..180, 1) { page == Page.Rotation && splitRotation }
+    private var leftZRotation by setting("Left Z Rotation", 0, -180..180, 1) { page == Page.Rotation && splitRotation }
+    private var rightXRotation by setting("Right X Rotation", 0, -180..180, 1) { page == Page.Rotation && splitRotation }
+    private var rightYRotation by setting("Right Y Rotation", 0, -180..180, 1) { page == Page.Rotation && splitRotation }
+    private var rightZRotation by setting("Right Z Rotation", 0, -180..180, 1) { page == Page.Rotation && splitRotation }
+
+    private val splitFov by setting("Split FOV", false, "Splits left and right Fov settings") { page == Page.Fov }
+    private val fov by setting("FOV", 70, 10..180, 1) { page == Page.Fov && !splitFov }.onValueChange { _, to -> leftFov = to; rightFov = to }
+    private val fovAnchorDistance by setting("Anchor Distance", 0.5f, 0.0f..1.0f, 0.01f, "The distance to anchor the FOV transformation from") { page == Page.Fov && !splitFov }.onValueChange { _, to -> leftFovAnchorDistance = to; rightFovAnchorDistance = to }
+    private var leftFov by setting("Left FOV", 70, 10..180, 1) { page == Page.Fov  && splitFov}
+    private var leftFovAnchorDistance by setting("Left Anchor Distance", 0.5f, 0.0f..1.0f, 0.01f, "The distance to anchor the left FOV transformation from") { page == Page.Fov && splitFov }
+    private var rightFov by setting("Right FOV", 70, 10..180, 1) { page == Page.Fov && splitFov }
+    private var rightFovAnchorDistance by setting("Right Anchor Distance", 0.5f, 0.0f..1.0f, 0.01f, "The distance to anchor the right FOV transformation from") { page == Page.Fov && splitFov }
+
+    private val enableHand by setting("Hand", false, "Enables settings for the players hand") { page == Page.Hand }
+    private val handXScale by setting("Hand X Scale", 1.0f, -1.0f..1.0f, 0.025f) { page == Page.Hand && enableHand }
+    private val handYScale by setting("Hand Y Scale", 1.0f, -1.0f..1.0f, 0.025f) { page == Page.Hand && enableHand }
+    private val handZScale by setting("Hand Z Scale", 1.0f, -1.0f..1.0f, 0.025f) { page == Page.Hand && enableHand }
+    private val handXPosition by setting("Hand X Position", 0.0f, -1.0f..1.0f, 0.025f) { page == Page.Hand && enableHand }
+    private val handYPosition by setting("Hand Y Position", 0.0f, -1.0f..1.0f, 0.025f) { page == Page.Hand && enableHand }
+    private val handZPosition by setting("Hand Z Position", 0.0f, -1.0f..1.0f, 0.025f) { page == Page.Hand && enableHand }
+    private val handXRotation by setting("Hand X Rotation", 0, -180..180, 1) { page == Page.Hand && enableHand }
+    private val handYRotation by setting("Hand Y Rotation", 0, -180..180, 1) { page == Page.Hand && enableHand }
+    private val handZRotation by setting("Hand Z Rotation", 0, -180..180, 1) { page == Page.Hand && enableHand }
+    private val handFov by setting("Hand FOV", 70, 10..180, 1) { page == Page.Hand && enableHand }
+    private val handFovAnchorDistance by setting("Hand FOV Anchor Distance", 0.5f, 0.0f..1.0f, 0.01f, "The distance to anchor the hands FOV transformation from") { page == Page.Hand && enableHand }
+
+    private var attackKeyTicksPressed = -1
+
+    init {
+        listen { event ->
+            if (event.button == mc.options.attackKey.boundKey.code)
+                attackKeyTicksPressed = if (event.action == 0) -1 else 0
+        }
+
+        listen { event ->
+            if (event.keyCode == mc.options.attackKey.boundKey.code) {
+                if (event.isPressed) {
+                    attackKeyTicksPressed = 0
+                } else if (event.isReleased) {
+                    attackKeyTicksPressed = -1
+                }
+            }
+        }
+
+        listen {
+            if (attackKeyTicksPressed != -1)
+                attackKeyTicksPressed++
+        }
+    }
+
+    fun transform(itemStack: ItemStack, hand: Hand, matrices: MatrixStack) {
+        val side = if (mc.options.mainArm.value == Arm.LEFT) {
+            if (hand == Hand.MAIN_HAND) Side.Left else Side.Right
+        } else {
+            if (hand == Hand.MAIN_HAND) Side.Right else Side.Left
+        }
+
+        val emptyHand = itemStack.isEmpty
+        if (!enableHand && emptyHand) return
+
+        applyItemFov(matrices, side, emptyHand)
+        scale(side, matrices, emptyHand)
+        position(side, matrices, emptyHand)
+        rotate(side, matrices, emptyHand)
+    }
+
+    private fun applyItemFov(matrices: MatrixStack, side: Side, emptyHand: Boolean) {
+        val fov = when {
+            side == Side.Left -> leftFov
+            emptyHand -> handFov
+            else -> rightFov
+        }.toFloat()
+
+        if (fov == 70f) return
+
+        val fovRatio = tan(Math.toRadians(fov.toDouble()/2)).toFloat() / tan(Math.toRadians(70.0/2)).toFloat()
+
+        val matrix = matrices.peek().positionMatrix
+
+        val distance = if (emptyHand) {
+            handFovAnchorDistance
+        } else {
+            when (side) {
+                Side.Left -> leftFovAnchorDistance
+                Side.Right -> rightFovAnchorDistance
+            }
+        }
+
+        val warpMatrix = Matrix4f().apply {
+            translate(0f, 0f, -distance)
+            scale(1f, 1f, fovRatio)
+            translate(0f, 0f, distance)
+        }
+
+        matrix.mul(warpMatrix)
+    }
+
+    private fun scale(side: Side, matrices: MatrixStack, emptyHand: Boolean) {
+        val scaleVec = getScaleVec(side, emptyHand)
+        matrices.scale(scaleVec.x, scaleVec.y, scaleVec.z)
+    }
+
+    private fun getScaleVec(side: Side, emptyHand: Boolean): Vector3f =
+        if (emptyHand) Vector3f(handXScale, handYScale, handZScale)
+        else when (side) {
+            Side.Left -> Vector3f(leftXScale, leftYScale, leftZScale)
+            Side.Right -> Vector3f(rightXScale, rightYScale, rightZScale)
+        }
+
+    private fun position(side: Side, matrices: MatrixStack, emptyHand: Boolean) {
+        val positionVec = getPositionVec(side, emptyHand)
+        matrices.translate(positionVec.x, positionVec.y, positionVec.z)
+    }
+
+    private fun getPositionVec(side: Side, emptyHand: Boolean) =
+        when (side) {
+            Side.Left ->
+                if (emptyHand) Vector3f(-handXPosition, handYPosition, handZPosition)
+                else Vector3f(-leftXPosition, leftYPosition, leftZPosition)
+            Side.Right ->
+                if (emptyHand) Vector3f(handXPosition, handYPosition, handZPosition)
+                else Vector3f(rightXPosition, rightYPosition, rightZPosition)
+        }
+
+    private fun rotate(side: Side, matrices: MatrixStack, emptyHand: Boolean) {
+        val rotationVec = getRotationVec(side, emptyHand)
+        matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(rotationVec.x.toFloat()))
+        matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(rotationVec.y.toFloat()))
+        matrices.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(rotationVec.z.toFloat()))
+    }
+
+    private fun getRotationVec(side: Side, emptyHand: Boolean) =
+        when (side) {
+            Side.Left -> {
+                if (emptyHand) Vector3i(handXRotation, -handYRotation, -handZRotation)
+                else Vector3i(leftXRotation, -leftYRotation, -leftZRotation)
+            }
+            Side.Right -> {
+                if (emptyHand) Vector3i(handXRotation, handYRotation, handZRotation)
+                else Vector3i(rightXRotation, rightYRotation, rightZRotation)
+            }
+        }
+
+    fun adjustSwing(hand: Hand, player: AbstractClientPlayerEntity) =
+        when (swingMode) {
+            SwingMode.Standard -> swingHand(hand, player)
+            SwingMode.Opposites ->
+                if (hand == Hand.MAIN_HAND) swingHand(Hand.OFF_HAND, player)
+                else swingHand(Hand.MAIN_HAND, player)
+            SwingMode.MainHand -> swingHand(Hand.MAIN_HAND, player)
+            SwingMode.OffHand -> swingHand(Hand.OFF_HAND, player)
+            SwingMode.None -> {}
+        }
+
+    private fun swingHand(hand: Hand, player: AbstractClientPlayerEntity) =
+        with(player) {
+            if (
+                (!handSwinging || handSwingTicks >= handSwingDuration / 2) ||
+                handSwingTicks < 0 ||
+                (noSwingDelay && attackKeyTicksPressed <= 1))
+            {
+                handSwingTicks = -1
+                handSwinging = true
+                preferredHand = hand
+            }
+        }
+
+    private enum class Page {
+        General, Scale, Position, Rotation, Fov, Hand
+    }
+
+    private enum class Side {
+        Left, Right
+    }
+
+    private enum class SwingMode {
+        Standard, Opposites, MainHand, OffHand, None
+    }
+}
diff --git a/common/src/main/resources/lambda.accesswidener b/common/src/main/resources/lambda.accesswidener
index 9e33b952c..31054ddb6 100644
--- a/common/src/main/resources/lambda.accesswidener
+++ b/common/src/main/resources/lambda.accesswidener
@@ -6,6 +6,7 @@ accessible field net/minecraft/client/MinecraftClient paused Z
 accessible field net/minecraft/client/MinecraftClient pausedTickDelta F
 accessible field net/minecraft/client/MinecraftClient thread Ljava/lang/Thread;
 accessible field net/minecraft/client/MinecraftClient uptimeInTicks J
+accessible field net/minecraft/client/option/KeyBinding boundKey Lnet/minecraft/client/util/InputUtil$Key;
 
 # World
 accessible field net/minecraft/client/world/ClientWorld entityManager Lnet/minecraft/client/world/ClientEntityManager;
@@ -30,6 +31,7 @@ accessible field net/minecraft/entity/Entity pos Lnet/minecraft/util/math/Vec3d;
 accessible field net/minecraft/client/network/ClientPlayerInteractionManager lastSelectedSlot I
 accessible method net/minecraft/entity/LivingEntity modifyAppliedDamage (Lnet/minecraft/entity/damage/DamageSource;F)F
 accessible method net/minecraft/entity/LivingEntity applyArmorToDamage (Lnet/minecraft/entity/damage/DamageSource;F)F
+accessible method net/minecraft/entity/LivingEntity getHandSwingDuration ()I
 
 # Camera
 accessible method net/minecraft/client/render/Camera setPos (DDD)V
diff --git a/common/src/main/resources/lambda.mixins.common.json b/common/src/main/resources/lambda.mixins.common.json
index 133a8b1a4..c46270921 100644
--- a/common/src/main/resources/lambda.mixins.common.json
+++ b/common/src/main/resources/lambda.mixins.common.json
@@ -37,6 +37,7 @@
     "render.ElytraFeatureRendererMixin",
     "render.GameRendererMixin",
     "render.GlStateManagerMixin",
+    "render.HeldItemRendererMixin",
     "render.InGameHudMixin",
     "render.InGameOverlayRendererMixin",
     "render.LightmapTextureManagerMixin",