diff --git a/core/src/main/java/org/saturnclient/emotes/core/bend/LimbBend.java b/core/src/main/java/org/saturnclient/emotes/core/bend/LimbBend.java index e30bc8e..d5ee0bc 100644 --- a/core/src/main/java/org/saturnclient/emotes/core/bend/LimbBend.java +++ b/core/src/main/java/org/saturnclient/emotes/core/bend/LimbBend.java @@ -96,6 +96,16 @@ public final class LimbBend { 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. */ public float[] cuts() { float[] cuts = new float[segments - 1]; diff --git a/src/main/java/org/saturnclient/emotes/fabric/BendTransforms.java b/src/main/java/org/saturnclient/emotes/fabric/BendTransforms.java new file mode 100644 index 0000000..149bbe3 --- /dev/null +++ b/src/main/java/org/saturnclient/emotes/fabric/BendTransforms.java @@ -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); + } +} diff --git a/src/main/java/org/saturnclient/emotes/fabric/mixin/FirstPersonHandMixin.java b/src/main/java/org/saturnclient/emotes/fabric/mixin/FirstPersonHandMixin.java new file mode 100644 index 0000000..66f30bd --- /dev/null +++ b/src/main/java/org/saturnclient/emotes/fabric/mixin/FirstPersonHandMixin.java @@ -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); + } +} diff --git a/src/main/java/org/saturnclient/emotes/fabric/mixin/PlayerModelMixin.java b/src/main/java/org/saturnclient/emotes/fabric/mixin/PlayerModelMixin.java new file mode 100644 index 0000000..20c4352 --- /dev/null +++ b/src/main/java/org/saturnclient/emotes/fabric/mixin/PlayerModelMixin.java @@ -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()); + } + } +} diff --git a/src/main/resources/saturn_emotes.mixins.json b/src/main/resources/saturn_emotes.mixins.json index d9835c0..7bec329 100644 --- a/src/main/resources/saturn_emotes.mixins.json +++ b/src/main/resources/saturn_emotes.mixins.json @@ -3,10 +3,12 @@ "package": "org.saturnclient.emotes.fabric.mixin", "compatibilityLevel": "${java}", "client": [ + "FirstPersonHandMixin", "HumanoidModelMixin", "LivingEntityRenderStateMixin", "LivingEntityRendererMixin", - "ModelPartMixin" + "ModelPartMixin", + "PlayerModelMixin" ], "injectors": { "defaultRequire": 1 diff --git a/src/testmod/java/org/saturnclient/emotes/testmod/EmotesGameTest.java b/src/testmod/java/org/saturnclient/emotes/testmod/EmotesGameTest.java index d85c047..48fbff7 100644 --- a/src/testmod/java/org/saturnclient/emotes/testmod/EmotesGameTest.java +++ b/src/testmod/java/org/saturnclient/emotes/testmod/EmotesGameTest.java @@ -49,6 +49,29 @@ public final class EmotesGameTest implements FabricClientGameTest { } 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; + }); } } diff --git a/src/testmod/java/org/saturnclient/emotes/testmod/TestAnimations.java b/src/testmod/java/org/saturnclient/emotes/testmod/TestAnimations.java index 3203423..6573576 100644 --- a/src/testmod/java/org/saturnclient/emotes/testmod/TestAnimations.java +++ b/src/testmod/java/org/saturnclient/emotes/testmod/TestAnimations.java @@ -66,6 +66,10 @@ final class TestAnimations { .set(Bone.BODY, BoneTransform.IDENTITY.withBend(lean, 0)) .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)); private TestAnimations() {