Adds the three drops as Stonecutter versions (26.1's jar also accepts its hotfixes). loom-back-compat already picks the plain Loom for 26.x; the testmod's remap configurations are only created where there's something to remap, and the foojay resolver downloads the Java 25 toolchain these versions need. - 26.3 renamed PoseStack.mulPose(Quaternionfc) to rotate. - The testmod follows Fabric API's renames: ClientCommands (26.1), getClientLevel (26.1) and getConnection (26.2) for waiting on chunks. - 26.2 replaced Options.hideGui with the HUD's toggle, so the gametest toggles it through a helper. The screenshot gametest passes on 26.1, 26.2 and 26.3, and the bent limbs match 1.21.11's. Co-Authored-By: Claude Opus 5.5 <[email protected]>
127 lines
7.1 KiB
Markdown
127 lines
7.1 KiB
Markdown
# emotes
|
|
|
|
A player emote library for Saturn Client, with limb bending.
|
|
|
|
## Layout
|
|
|
|
- `core/`: Minecraft-independent animation model (`Bone`, `BoneTransform`, `Pose`, `Animation`,
|
|
`AnimationPlayer`), keyframes and easings, the Emotecraft JSON loader and the `EmoteRegistry`.
|
|
Plain Java 21 plus Jackson, with JUnit tests. Published as `org.saturnclient:emotes-core`.
|
|
- `src/main/`: the Fabric mod that puts poses on player models. It's built once per Minecraft
|
|
version with [Stonecutter](https://stonecutter.kikugie.dev/) and published as
|
|
`org.saturnclient:emotes-fabric:<version>+<mc>`, with `core` and Jackson 2.17.0 (the version Saturn
|
|
Client ships) bundled inside the jar.
|
|
- `src/testmod/`: a dev-only tester mod that is never published. It loads Saturn's emotes into the
|
|
library's registry and keeps its own hand-written test animations in a separate registry.
|
|
`/emotes play <id>` plays a registered emote, `/emotes test <name>` a test animation, and there's
|
|
`/emotes stop` and `/emotes info`. Press F5 to see yourself. It also has a Fabric client gametest
|
|
that creates a world and screenshots each animation from the front and side, into
|
|
`versions/<mc>/build/run/clientGameTest/screenshots/`.
|
|
- `tools/capture/`: a Python tool that measures an emote from a store page's 3D preview and writes
|
|
it as Emotecraft JSON. See its README.
|
|
- `tools/emotecraft/convert.py`: converts Emotecraft's binary `.emotecraft` files to Emotecraft JSON
|
|
(standard-library Python).
|
|
|
|
Supported versions: 1.21.4 to 1.21.11, 26.1 (and its hotfixes), 26.2 and 26.3. Each version's
|
|
Fabric API version is set in `stonecutter.properties.toml`. The code uses Mojang mappings, which
|
|
26.1+ ships as its own names, so `loom-back-compat` applies the remapping Loom for 1.21.x and the
|
|
plain one for 26.x. 26.x builds need Java 25; Gradle downloads it when it isn't installed.
|
|
|
|
## Using it
|
|
|
|
```java
|
|
// At startup: load emotes (Emotecraft / player-animation-lib JSON) into the registry.
|
|
try (InputStream in = MyMod.class.getResourceAsStream("/assets/mymod/player_animations/facepalm.json")) {
|
|
Emotes.registry().loadEmotecraft("facepalm", in);
|
|
}
|
|
|
|
Emotes.play(player.getUUID(), "facepalm"); // false if there's no such emote
|
|
Emotes.play(player.getUUID(), animation); // or any Animation, e.g. written in code
|
|
Emotes.stop(player.getUUID());
|
|
Emotes.playing(player.getUUID()); // Optional<AnimationPlayer>
|
|
```
|
|
|
|
Emotes work for any player you can see, run on game ticks, and a non-looping one stops by itself.
|
|
`Emotes.registry().all()` lists the emotes with their name, description and author, for an emote wheel.
|
|
|
|
An `Animation` returns a `Pose` for a time in ticks: a `BoneTransform` (offset in model pixels,
|
|
pitch/yaw/roll in radians, bend and bend axis) for any of `ROOT` (the whole player), `HEAD`, `BODY`,
|
|
`RIGHT_ARM`, `LEFT_ARM`, `RIGHT_LEG` and `LEFT_LEG`. Bones it doesn't set keep their vanilla pose.
|
|
`KeyframeAnimation` builds one from keyframes and `Easing`s.
|
|
|
|
`EmotecraftLoader` follows player-animation-lib's reader: absolute part pivots become offsets,
|
|
`body` (and `torso` before format version 3) is the whole player in blocks, angles are degrees unless
|
|
`"degrees": false`, `turn` adds whole turns, and loops jump back to `returnTick` after `endTick`.
|
|
Emotecraft's two-segment bends become our curved ones: a limb's bend changes sign, the torso's
|
|
doesn't, and a bend on the whole player bends the torso. Scale channels, `easingArg` and blending
|
|
with the vanilla pose aren't supported yet (issue #4). Binary `.emotecraft` files can be converted to
|
|
JSON first with `tools/emotecraft/convert.py`.
|
|
|
|
## How it hooks in
|
|
|
|
1. `LivingEntityRendererMixin` samples the entity's current pose into its render state (players only).
|
|
2. `HumanoidModelMixin` applies that pose at the end of `HumanoidModel.setupAnim`. `PlayerModel`
|
|
and the armor layers both go through this method, so armor follows the pose.
|
|
|
|
Both hook points exist unchanged in every supported version. Version-specific code goes behind
|
|
Stonecutter comments, like the vertex accessors in `BentCubeRenderer`:
|
|
|
|
```java
|
|
//? if >=1.21.9 {
|
|
return new float[] { vertex.x(), vertex.y(), vertex.z(), vertex.u(), vertex.v() };
|
|
//?} else
|
|
/*return new float[] { vertex.pos().x(), vertex.pos().y(), vertex.pos().z(), vertex.u(), vertex.v() };*/
|
|
```
|
|
|
|
## Bending
|
|
|
|
Each limb bends along a single axis (`BoneTransform.bend`, `bendAxis`). It works like the cloak
|
|
renderer: build a spine, put a ring of shared vertices along it, and draw the faces between the rings.
|
|
|
|
- `LimbBend` (core) runs the spine along a cubic Bézier (`Bezier`). The control points are the rigid
|
|
"upper half, joint, lower half" shape, and `sharpness` goes from a round curve (0) to a tight joint
|
|
(1). The spine is cut into one ring per texture row (12 per limb), spaced evenly by arc length, so
|
|
the limb keeps its length. Each ring moves its cross-section rigidly, so neighbouring pieces share
|
|
vertices and the mesh stays closed.
|
|
- `QuadSplitter` (core) cuts each vanilla face into strips at the rings and splits its UVs to match.
|
|
The texture is unchanged apart from the cuts.
|
|
- `ModelPartMixin` swaps in `BentCubeRenderer` for any part with a bend. That renderer deforms
|
|
vanilla's own cubes, so slim arms, sleeves, pants and armor all work. Overlays and armor use the
|
|
limb's spine, not their own inflated box, so they stay attached.
|
|
|
|
Axis 0 folds towards the front (-Z) and PI/2 towards +X, in the limb's own space. Knees fold back,
|
|
so they use axis PI.
|
|
|
|
The torso is anchored at the hips instead of its pivot (`LimbBend.anchoredAtEnd`), so the legs stay
|
|
attached and the chest leans. The head and arms are siblings of the body in the vanilla model, so
|
|
`PoseApplier` carries them: their pivots follow the bent torso, and they turn with its ring there
|
|
(`LimbBend.angleAt`). An arm carried by a forward lean swings back like a diver's, so emotes that want
|
|
hanging arms tilt them forward by about the lean (see the `twerk` test animation).
|
|
|
|
Held items still attach to the unbent arm, and the cape doesn't follow the torso yet.
|
|
|
|
## Commands
|
|
|
|
```sh
|
|
./gradlew :core:test # core unit tests
|
|
./gradlew :1.21.11:runTestmodClient # launch the game with the tester (run dir: ./run)
|
|
./gradlew :1.21.11:runClientGameTest # screenshot each test animation, then quit
|
|
./gradlew :1.21.4:compileTestmodJava # quick compile check for one version
|
|
./gradlew buildAll # every version's jar → build/libs/<version>/
|
|
GITEA_TOKEN=... ./gradlew publish # publish core + every version to Gitea
|
|
```
|
|
|
|
The active version is set in `stonecutter.gradle.kts` (`stonecutter active "..."`), and it's the
|
|
version whose code is uncommented in `src/`. Switch versions with the Stonecutter IntelliJ plugin
|
|
or the `Set active project to <v>` Gradle task. Run `Reset active project` before committing, so
|
|
the tree is committed with `vcsVersion` (1.21.11) active.
|
|
|
|
Loom 1.18 needs a Java 25 JVM to run Gradle, so the build is pinned to Loom 1.17, which builds
|
|
26.x as well (with a Java 25 toolchain for those versions only).
|
|
|
|
## Publishing
|
|
|
|
Artifacts go to the `saturnclientmc` organization's Maven registry:
|
|
`https://git.selimaj.dev/api/packages/saturnclientmc/maven`. Use a token with `package:write`, set
|
|
as `GITEA_TOKEN` or `-PgiteaToken=...`.
|