Files
emotes/README.md
T
selimaj-devandclaude 970e9aef51 Read binary Emotecraft emotes and fix the torso's bend direction
tools/emotecraft/convert.py turns Emotecraft's binary .emotecraft files (its network packet, with
the animation in player-animator's legacy format) into the Emotecraft JSON EmotecraftLoader reads.

Converting a few community emotes showed the loader bent the torso the wrong way: Get the Griddy,
which hunches forward, arched back. Emotecraft bends limbs the other way from us, but not the torso,
whose bend is anchored at the hips here and so already turned round. The loader now keeps the
torso's bend sign, and gives the whole player's bend to the torso, as Emotecraft does. fit.py writes
torso bends to match, so re-exported captures look the same.

Bumps the version to 0.1.1 for the fix.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 17:37:18 +02:00

125 lines
6.9 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, the same range as Saturn Client. Each version's Fabric API
version is set in `stonecutter.properties.toml`. The code uses Mojang mappings.
## 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 until the
toolchain moves. Minecraft 26.x will need that upgrade.
## 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=...`.