Merge pull request 'Make held items follow bent arms and straighten the first-person arm' (#3) from held-items into master

Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
2026-09-26 18:31:22 +00:00
7 changed files with 144 additions and 1 deletions
@@ -96,6 +96,16 @@ public final class LimbBend {
return segments; return segments;
} }
/** Top of the limb along Y (its pivot end), as given to the constructor. */
public float start() {
return start;
}
/** Bottom of the limb along Y (the hand or foot end), as given to the constructor. */
public float end() {
return start + length;
}
/** Y positions where the limb is cut into pieces, not counting its ends. */ /** Y positions where the limb is cut into pieces, not counting its ends. */
public float[] cuts() { public float[] cuts() {
float[] cuts = new float[segments - 1]; float[] cuts = new float[segments - 1];
@@ -0,0 +1,28 @@
package org.saturnclient.emotes.fabric;
import org.joml.Quaternionf;
import org.saturnclient.emotes.core.bend.LimbBend;
import org.saturnclient.emotes.core.bend.Vec3;
import com.mojang.blaze3d.vertex.PoseStack;
/** Pose stack helpers for attaching things to bent limbs. */
public final class BendTransforms {
private BendTransforms() {
}
/**
* Moves whatever is drawn next from the straight limb onto the bent one, as if it were fixed to the
* limb at {@code y} (model pixels, in the limb's space). This is the same rigid move the ring at
* {@code y} makes, so anything attached there stays attached the way the mesh does. The pose stack
* must already be in the limb's space, i.e. after {@code ModelPart.translateAndRotate}.
*/
public static void attachAt(PoseStack poseStack, LimbBend bend, float y) {
float[] center = new float[3];
bend.bendPoint(0, y, 0, center);
Vec3 axis = bend.axis();
poseStack.translate(center[0] / 16, center[1] / 16, center[2] / 16);
poseStack.mulPose(new Quaternionf().rotationAxis(bend.angleAt(y), axis.x(), axis.y(), axis.z()));
poseStack.translate(0, -y / 16, 0);
}
}
@@ -0,0 +1,30 @@
package org.saturnclient.emotes.fabric.mixin;
import org.saturnclient.emotes.fabric.access.BendablePart;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.model.geom.ModelPart;
//? if >=1.21.9 {
import net.minecraft.client.renderer.entity.player.AvatarRenderer;
//?} else
/*import net.minecraft.client.renderer.entity.player.PlayerRenderer;*/
/**
* The first-person arm is drawn from the same shared model without calling setupAnim, so it would
* keep whatever bend the last third-person player drawn left on it. It should never be bent.
*/
//? if >=1.21.9 {
@Mixin(AvatarRenderer.class)
//?} else
/*@Mixin(PlayerRenderer.class)*/
public class FirstPersonHandMixin {
@Inject(method = "renderHand", at = @At("HEAD"))
private void saturnEmotes$straightenArm(CallbackInfo ci, @Local(argsOnly = true) ModelPart arm) {
((BendablePart) (Object) arm).saturnEmotes$setBend(null);
}
}
@@ -0,0 +1,46 @@
package org.saturnclient.emotes.fabric.mixin;
import org.saturnclient.emotes.core.bend.LimbBend;
import org.saturnclient.emotes.fabric.BendTransforms;
import org.saturnclient.emotes.fabric.access.BendablePart;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.model.HumanoidModel;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.world.entity.HumanoidArm;
//? if >=1.21.11 {
import net.minecraft.client.model.player.PlayerModel;
//?} else
/*import net.minecraft.client.model.PlayerModel;*/
/**
* Held items are placed at the hand of the straight arm. PlayerModel overrides translateToHand (for
* slim arms) without calling HumanoidModel's, so this has to hook PlayerModel's own.
*/
@Mixin(PlayerModel.class)
public class PlayerModelMixin {
//? if >=1.21.9 {
@Unique
private static final String TRANSLATE_TO_HAND = "translateToHand(Lnet/minecraft/client/renderer/entity/state/AvatarRenderState;Lnet/minecraft/world/entity/HumanoidArm;Lcom/mojang/blaze3d/vertex/PoseStack;)V";
//?} else
/*@Unique private static final String TRANSLATE_TO_HAND = "translateToHand(Lnet/minecraft/world/entity/HumanoidArm;Lcom/mojang/blaze3d/vertex/PoseStack;)V";*/
/** Moves the item from the hand of the straight arm to the hand of the bent one. */
@Inject(method = TRANSLATE_TO_HAND, at = @At("TAIL"))
private void saturnEmotes$followBentArm(CallbackInfo ci, @Local(argsOnly = true) HumanoidArm arm,
@Local(argsOnly = true) PoseStack poseStack) {
HumanoidModel<?> model = (HumanoidModel<?>) (Object) this;
ModelPart part = arm == HumanoidArm.LEFT ? model.leftArm : model.rightArm;
LimbBend bend = ((BendablePart) (Object) part).saturnEmotes$getBend();
if (bend != null && !bend.isStraight()) {
BendTransforms.attachAt(poseStack, bend, bend.end());
}
}
}
+3 -1
View File
@@ -3,10 +3,12 @@
"package": "org.saturnclient.emotes.fabric.mixin", "package": "org.saturnclient.emotes.fabric.mixin",
"compatibilityLevel": "${java}", "compatibilityLevel": "${java}",
"client": [ "client": [
"FirstPersonHandMixin",
"HumanoidModelMixin", "HumanoidModelMixin",
"LivingEntityRenderStateMixin", "LivingEntityRenderStateMixin",
"LivingEntityRendererMixin", "LivingEntityRendererMixin",
"ModelPartMixin" "ModelPartMixin",
"PlayerModelMixin"
], ],
"injectors": { "injectors": {
"defaultRequire": 1 "defaultRequire": 1
@@ -49,6 +49,29 @@ public final class EmotesGameTest implements FabricClientGameTest {
} }
context.runOnClient(client -> Emotes.stop(client.player.getUUID())); context.runOnClient(client -> Emotes.stop(client.player.getUUID()));
} }
// A held item should come out of the fist on the bent forearm.
singleplayer.getServer().runCommand("give @a diamond_sword");
context.runOnClient(client -> Emotes.play(client.player.getUUID(), TestAnimations.ALL.get("hold")));
context.waitTicks(10);
screenshot(context, "hold-0", 0);
screenshot(context, "hold-90", 90);
// The first-person arm shares the model, so it must not keep the bend from the frame above.
// Vanilla only draws the arm with an empty hand, and hiding the GUI hides it too.
singleplayer.getServer().runCommand("clear @a");
context.waitTicks(2);
context.runOnClient(client -> {
client.options.hideGui = false;
client.options.setCameraType(CameraType.FIRST_PERSON);
});
// Give the hand time to come back up after the item change.
context.waitTicks(20);
context.takeScreenshot(MINECRAFT + "-hold-firstperson");
context.runOnClient(client -> {
Emotes.stop(client.player.getUUID());
client.options.hideGui = true;
});
} }
} }
@@ -66,6 +66,10 @@ final class TestAnimations {
.set(Bone.BODY, BoneTransform.IDENTITY.withBend(lean, 0)) .set(Bone.BODY, BoneTransform.IDENTITY.withBend(lean, 0))
.build(); .build();
}), }),
// Right arm forward with the forearm folded up, as if showing off a sword. Tests held items.
"hold", of(20, true, t -> Pose.builder()
.set(Bone.RIGHT_ARM, BoneTransform.rotation(-0.9f, 0, 0).withBend(1.4f, 0))
.build()),
"twerk", of(8, true, TestAnimations::twerk)); "twerk", of(8, true, TestAnimations::twerk));
private TestAnimations() { private TestAnimations() {