Scaffold the emote library with a Stonecutter Fabric mod and tester
- core/: Minecraft-independent animation model (Bone, BoneTransform with bend fields, Pose, Animation, AnimationPlayer) with JUnit tests. - src/main: Fabric mod built for 1.21.4-1.21.11 with Stonecutter on Mojang mappings. Samples each player's pose into its render state and applies it at the end of HumanoidModel.setupAnim (armor follows too). - src/testmod: dev-only tester with /emotes play|stop|info and a mixin audit on startup. - Publishing to the saturnclientmc Gitea Maven registry as org.saturnclient:emotes-core and emotes-fabric:<v>+<mc>. Loom is pinned to 1.17 because 1.18 needs a Java 25 JVM. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
#
|
||||||
|
# https://help.github.com/articles/dealing-with-line-endings/
|
||||||
|
#
|
||||||
|
# Linux start script should use lf
|
||||||
|
/gradlew text eol=lf
|
||||||
|
|
||||||
|
# These are Windows script files and should use crlf
|
||||||
|
*.bat text eol=crlf
|
||||||
|
|
||||||
+34
-20
@@ -1,26 +1,40 @@
|
|||||||
# ---> Java
|
# gradle
|
||||||
# Compiled class file
|
|
||||||
*.class
|
|
||||||
|
|
||||||
# Log file
|
.gradle/
|
||||||
*.log
|
build/
|
||||||
|
out/
|
||||||
|
classes/
|
||||||
|
|
||||||
# BlueJ files
|
# eclipse
|
||||||
*.ctxt
|
|
||||||
|
|
||||||
# Mobile Tools for Java (J2ME)
|
*.launch
|
||||||
.mtj.tmp/
|
|
||||||
|
|
||||||
# Package Files #
|
# idea
|
||||||
*.jar
|
|
||||||
*.war
|
|
||||||
*.nar
|
|
||||||
*.ear
|
|
||||||
*.zip
|
|
||||||
*.tar.gz
|
|
||||||
*.rar
|
|
||||||
|
|
||||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
.idea/
|
||||||
hs_err_pid*
|
*.iml
|
||||||
replay_pid*
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# vscode
|
||||||
|
|
||||||
|
.settings/
|
||||||
|
.vscode/
|
||||||
|
bin/
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
|
||||||
|
# macos
|
||||||
|
|
||||||
|
*.DS_Store
|
||||||
|
|
||||||
|
# fabric
|
||||||
|
|
||||||
|
run/
|
||||||
|
|
||||||
|
# java
|
||||||
|
|
||||||
|
hs_err_*.log
|
||||||
|
replay_*.log
|
||||||
|
*.hprof
|
||||||
|
*.jfr
|
||||||
|
|||||||
@@ -1,3 +1,58 @@
|
|||||||
# emote
|
# emotes
|
||||||
|
|
||||||
A powerful emote library for Saturn Client, with bending
|
A player emote library for Saturn Client, with limb bending.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
- `core/`: Minecraft-independent animation model (`Bone`, `BoneTransform`, `Pose`, `Animation`,
|
||||||
|
`AnimationPlayer`). Plain Java 21 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` bundled inside the jar.
|
||||||
|
- `src/testmod/`: a dev-only tester mod that is never published. It adds `/emotes play <name>`,
|
||||||
|
`/emotes stop` and `/emotes info`. Press F5 to see yourself.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
```java
|
||||||
|
//? if >=1.21.9 {
|
||||||
|
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
|
||||||
|
//?} else
|
||||||
|
/*import net.minecraft.client.renderer.entity.state.PlayerRenderState;*/
|
||||||
|
```
|
||||||
|
|
||||||
|
Bending (`BoneTransform.bend` / `bendAxis`) is part of the model but isn't rendered 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.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=...`.
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
// Build script for each Minecraft version node (versions/<mc>/). The root project itself
|
||||||
|
// is configured by stonecutter.gradle.kts.
|
||||||
|
plugins {
|
||||||
|
// Applies the right Loom variant for the node's Minecraft version.
|
||||||
|
id("dev.kikugie.loom-back-compat")
|
||||||
|
`maven-publish`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DO NOT set group = ...! (Stonecutter/loom-back-compat reads mod.group.)
|
||||||
|
val modVersion = property("mod.version") as String
|
||||||
|
version = "$modVersion+${sc.current.version}"
|
||||||
|
base.archivesName = "emotes-fabric"
|
||||||
|
|
||||||
|
val requiredJava: JavaVersion = when {
|
||||||
|
sc.current.parsed >= "26.1" -> JavaVersion.VERSION_25
|
||||||
|
else -> JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
|
||||||
|
val testmod: SourceSet = sourceSets.create("testmod") {
|
||||||
|
compileClasspath += sourceSets.main.get().compileClasspath + sourceSets.main.get().output
|
||||||
|
runtimeClasspath += sourceSets.main.get().runtimeClasspath + sourceSets.main.get().output
|
||||||
|
}
|
||||||
|
|
||||||
|
loom {
|
||||||
|
createRemapConfigurations(testmod)
|
||||||
|
|
||||||
|
mods {
|
||||||
|
register("saturn_emotes") { sourceSet(sourceSets.main.get()) }
|
||||||
|
register("saturn_emotes_testmod") { sourceSet(testmod) }
|
||||||
|
}
|
||||||
|
|
||||||
|
decompilerOptions.named("vineflower") {
|
||||||
|
options.put("mark-corresponding-synthetics", "1") // Names lambdas, useful for mixins
|
||||||
|
}
|
||||||
|
|
||||||
|
runs {
|
||||||
|
// ./gradlew :<mc>:runTestmodClient launches the game with the tester mod loaded.
|
||||||
|
register("testmodClient") {
|
||||||
|
client()
|
||||||
|
name = "Testmod Client"
|
||||||
|
source(testmod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runConfigs.all {
|
||||||
|
preferGradleTask = true
|
||||||
|
generateRunConfig = true
|
||||||
|
runDirectory = rootProject.file("run") // Shared by every version
|
||||||
|
jvmArguments.add("-Dmixin.debug.export=true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
fun fapi(vararg modules: String) {
|
||||||
|
for (it in modules) modImplementation(fabricApi.module(it, sc.properties["deps.fabric_api"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
minecraft("com.mojang:minecraft:${sc.current.version}")
|
||||||
|
loomx.applyMojangMappings()
|
||||||
|
|
||||||
|
modImplementation("net.fabricmc:fabric-loader:${property("deps.fabric_loader")}")
|
||||||
|
fapi("fabric-api-base", "fabric-lifecycle-events-v1")
|
||||||
|
|
||||||
|
// The core is Minecraft-independent; ship it inside the mod jar.
|
||||||
|
api(project(":core"))
|
||||||
|
include(project(":core"))
|
||||||
|
|
||||||
|
"modTestmodImplementation"(fabricApi.module("fabric-command-api-v2", sc.properties["deps.fabric_api"]))
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
withSourcesJar()
|
||||||
|
targetCompatibility = requiredJava
|
||||||
|
sourceCompatibility = requiredJava
|
||||||
|
toolchain.languageVersion = JavaLanguageVersion.of(requiredJava.majorVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
val props = mapOf(
|
||||||
|
"id" to sc.properties["mod.id"],
|
||||||
|
"name" to sc.properties["mod.name"],
|
||||||
|
"version" to version.toString(),
|
||||||
|
"minecraft" to sc.properties["mod.mc_compat"],
|
||||||
|
"loader" to Regex("\\d+\\.\\d+").find(sc.properties.get<String>("deps.fabric_loader"))!!.value,
|
||||||
|
"java" to "JAVA_${requiredJava.majorVersion}",
|
||||||
|
)
|
||||||
|
|
||||||
|
withType<ProcessResources>().configureEach {
|
||||||
|
inputs.properties(props)
|
||||||
|
filesMatching(listOf("fabric.mod.json", "*.mixins.json")) { expand(props) }
|
||||||
|
}
|
||||||
|
|
||||||
|
withType<Jar>().configureEach {
|
||||||
|
from(rootProject.file("LICENSE")) { rename { "${it}_saturn_emotes" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
register<Copy>("buildAndCollect") {
|
||||||
|
group = "build"
|
||||||
|
description = "Builds the mod jar and copies it to build/libs/<mod version>/ in the root project"
|
||||||
|
from(loomx.modJar.flatMap { it.archiveFile })
|
||||||
|
into(rootProject.layout.buildDirectory.dir("libs/$modVersion"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
create<MavenPublication>("maven") {
|
||||||
|
groupId = property("mod.group") as String
|
||||||
|
artifactId = "emotes-fabric"
|
||||||
|
from(components["java"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = rootProject.file("gradle/gitea.gradle.kts"))
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
plugins {
|
||||||
|
`java-library`
|
||||||
|
`maven-publish`
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "org.saturnclient"
|
||||||
|
version = rootProject.property("mod.version") as String
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
// Kept at 21 so it runs on every supported Minecraft version.
|
||||||
|
toolchain.languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
create<MavenPublication>("maven") {
|
||||||
|
artifactId = "emotes-core"
|
||||||
|
from(components["java"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(from = rootProject.file("gradle/gitea.gradle.kts"))
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package org.saturnclient.emotes.core;
|
||||||
|
|
||||||
|
/** Something that produces a {@link Pose} for any point in time. */
|
||||||
|
public interface Animation {
|
||||||
|
/**
|
||||||
|
* @param tick time since the animation started, in game ticks (20 per second). Fractional
|
||||||
|
* values come from the render partial tick.
|
||||||
|
*/
|
||||||
|
Pose sample(float tick);
|
||||||
|
|
||||||
|
/** Length in ticks. Looping animations repeat after this, others end. */
|
||||||
|
float length();
|
||||||
|
|
||||||
|
default boolean loops() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package org.saturnclient.emotes.core;
|
||||||
|
|
||||||
|
/** Plays one {@link Animation} from a fixed start tick. */
|
||||||
|
public final class AnimationPlayer {
|
||||||
|
private final Animation animation;
|
||||||
|
private final long startTick;
|
||||||
|
|
||||||
|
public AnimationPlayer(Animation animation, long startTick) {
|
||||||
|
this.animation = animation;
|
||||||
|
this.startTick = startTick;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Animation animation() {
|
||||||
|
return animation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Elapsed animation time, wrapped for looping animations. */
|
||||||
|
public float time(long tick, float partialTick) {
|
||||||
|
float elapsed = Math.max(0, tick - startTick + partialTick);
|
||||||
|
float length = animation.length();
|
||||||
|
if (animation.loops() && length > 0) {
|
||||||
|
return elapsed % length;
|
||||||
|
}
|
||||||
|
return Math.min(elapsed, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFinished(long tick) {
|
||||||
|
return !animation.loops() && tick - startTick >= animation.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pose sample(long tick, float partialTick) {
|
||||||
|
if (isFinished(tick)) {
|
||||||
|
return Pose.EMPTY;
|
||||||
|
}
|
||||||
|
return animation.sample(time(tick, partialTick));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.saturnclient.emotes.core;
|
||||||
|
|
||||||
|
/** The parts of the player model an animation can move. */
|
||||||
|
public enum Bone {
|
||||||
|
HEAD,
|
||||||
|
BODY,
|
||||||
|
RIGHT_ARM,
|
||||||
|
LEFT_ARM,
|
||||||
|
RIGHT_LEG,
|
||||||
|
LEFT_LEG
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package org.saturnclient.emotes.core;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How one bone is posed. Offsets are in model pixels (1/16 block) relative to the bone's default
|
||||||
|
* pivot, rotations are in radians and replace the vanilla rotation.
|
||||||
|
*
|
||||||
|
* @param bend how far the lower half of the limb (forearm, shin, lower torso) folds, in radians
|
||||||
|
* @param bendAxis the direction the limb folds in, as an angle around the limb's long axis
|
||||||
|
*/
|
||||||
|
public record BoneTransform(
|
||||||
|
float x, float y, float z,
|
||||||
|
float pitch, float yaw, float roll,
|
||||||
|
float bend, float bendAxis) {
|
||||||
|
|
||||||
|
public static final BoneTransform IDENTITY = new BoneTransform(0, 0, 0, 0, 0, 0, 0, 0);
|
||||||
|
|
||||||
|
public static BoneTransform rotation(float pitch, float yaw, float roll) {
|
||||||
|
return new BoneTransform(0, 0, 0, pitch, yaw, roll, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BoneTransform withBend(float bend, float bendAxis) {
|
||||||
|
return new BoneTransform(x, y, z, pitch, yaw, roll, bend, bendAxis);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package org.saturnclient.emotes.core;
|
||||||
|
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/** An immutable snapshot of bone transforms. Bones without a transform keep their vanilla pose. */
|
||||||
|
public final class Pose {
|
||||||
|
public static final Pose EMPTY = new Pose(new EnumMap<>(Bone.class));
|
||||||
|
|
||||||
|
private final EnumMap<Bone, BoneTransform> bones;
|
||||||
|
|
||||||
|
private Pose(EnumMap<Bone, BoneTransform> bones) {
|
||||||
|
this.bones = bones;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<BoneTransform> get(Bone bone) {
|
||||||
|
return Optional.ofNullable(bones.get(bone));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Bone, BoneTransform> bones() {
|
||||||
|
return Map.copyOf(bones);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return bones.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Builder {
|
||||||
|
private final EnumMap<Bone, BoneTransform> bones = new EnumMap<>(Bone.class);
|
||||||
|
|
||||||
|
private Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder set(Bone bone, BoneTransform transform) {
|
||||||
|
bones.put(bone, transform);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pose build() {
|
||||||
|
return bones.isEmpty() ? EMPTY : new Pose(new EnumMap<>(bones));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package org.saturnclient.emotes.core;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class AnimationPlayerTest {
|
||||||
|
/** Rotates the head by one radian per tick. */
|
||||||
|
private static Animation headSpin(float length, boolean loops) {
|
||||||
|
return new Animation() {
|
||||||
|
@Override
|
||||||
|
public Pose sample(float tick) {
|
||||||
|
return Pose.builder().set(Bone.HEAD, BoneTransform.rotation(0, tick, 0)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float length() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean loops() {
|
||||||
|
return loops;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float headYaw(Pose pose) {
|
||||||
|
return pose.get(Bone.HEAD).orElseThrow().yaw();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void samplesRelativeToStartTick() {
|
||||||
|
AnimationPlayer player = new AnimationPlayer(headSpin(20, false), 100);
|
||||||
|
assertEquals(5.5f, headYaw(player.sample(105, 0.5f)), 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void finishesAfterLength() {
|
||||||
|
AnimationPlayer player = new AnimationPlayer(headSpin(20, false), 100);
|
||||||
|
assertFalse(player.isFinished(119));
|
||||||
|
assertTrue(player.isFinished(120));
|
||||||
|
assertTrue(player.sample(120, 0).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loopingAnimationsWrap() {
|
||||||
|
AnimationPlayer player = new AnimationPlayer(headSpin(20, true), 0);
|
||||||
|
assertFalse(player.isFinished(1_000));
|
||||||
|
assertEquals(5f, headYaw(player.sample(45, 0)), 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void timeBeforeStartClampsToZero() {
|
||||||
|
AnimationPlayer player = new AnimationPlayer(headSpin(20, false), 100);
|
||||||
|
assertEquals(0f, player.time(90, 0), 1e-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Gradle options only; mod and dependency versions live in stonecutter.properties.toml.
|
||||||
|
org.gradle.jvmargs=-Xmx2G
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.caching=true
|
||||||
|
org.gradle.configuration-cache=true
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Shared publishing target: the saturnclientmc org's Maven registry on Gitea.
|
||||||
|
// Publish with a token that has package:write: GITEA_TOKEN=... ./gradlew publish
|
||||||
|
configure<PublishingExtension> {
|
||||||
|
repositories {
|
||||||
|
maven {
|
||||||
|
name = "Gitea"
|
||||||
|
url = uri("https://git.selimaj.dev/api/packages/saturnclientmc/maven")
|
||||||
|
credentials(HttpHeaderCredentials::class) {
|
||||||
|
name = "Authorization"
|
||||||
|
value = "token ${findProperty("giteaToken") ?: System.getenv("GITEA_TOKEN") ?: ""}"
|
||||||
|
}
|
||||||
|
authentication {
|
||||||
|
create<HttpHeaderAuthentication>("header")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
BIN
Binary file not shown.
+9
@@ -0,0 +1,9 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
retries=0
|
||||||
|
retryBackOffMs=500
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# gradlew start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh gradlew
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
Vendored
+112
@@ -0,0 +1,112 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem gradlew startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||||
|
setlocal EnableExtensions
|
||||||
|
|
||||||
|
@rem Catch executions from older scripts and ensure they exit cleanly.
|
||||||
|
@rem This can be removed once we can be reasonably confident that few people
|
||||||
|
@rem will be migrating directly to this new wrapper.
|
||||||
|
goto afterSafetyNet
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||||
|
goto exitWithErrorLevel
|
||||||
|
:afterSafetyNet
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
1>&2 echo.
|
||||||
|
1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
1>&2 echo.
|
||||||
|
1>&2 echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
1>&2 echo location of your Java installation.
|
||||||
|
|
||||||
|
"%COMSPEC%" /c exit 1
|
||||||
|
goto exitWithErrorLevel
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
1>&2 echo.
|
||||||
|
1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
1>&2 echo.
|
||||||
|
1>&2 echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
1>&2 echo location of your Java installation.
|
||||||
|
|
||||||
|
"%COMSPEC%" /c exit 1
|
||||||
|
goto exitWithErrorLevel
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute gradlew
|
||||||
|
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||||
|
@rem which allows us to clear the local environment before executing the java command
|
||||||
|
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel
|
||||||
|
|
||||||
|
@rem This label must not be changed. We rely on old scripts being able to jump to this point.
|
||||||
|
:exitWithErrorLevel
|
||||||
|
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||||
|
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
maven("https://maven.fabricmc.net/")
|
||||||
|
maven("https://maven.kikugie.dev/releases") { name = "KikuGie Releases" }
|
||||||
|
maven("https://maven.kikugie.dev/snapshots") { name = "KikuGie Snapshots" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.kikugie.stonecutter") version "0.9.8"
|
||||||
|
// Picks the right Loom variant per Minecraft version (remapping for 1.21.x, plain for 26.1+).
|
||||||
|
id("dev.kikugie.loom-back-compat") version "0.4.2"
|
||||||
|
}
|
||||||
|
|
||||||
|
stonecutter {
|
||||||
|
create(rootProject) {
|
||||||
|
// Same range as Saturn Client. Per-version dependencies live in stonecutter.properties.toml.
|
||||||
|
versions("1.21.4", "1.21.5", "1.21.6", "1.21.7", "1.21.8", "1.21.9", "1.21.10", "1.21.11")
|
||||||
|
vcsVersion = "1.21.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "emotes"
|
||||||
|
|
||||||
|
// Minecraft-independent animation code, shared by every version node.
|
||||||
|
include("core")
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package org.saturnclient.emotes.fabric;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import org.saturnclient.emotes.core.Animation;
|
||||||
|
import org.saturnclient.emotes.core.AnimationPlayer;
|
||||||
|
import org.saturnclient.emotes.core.Pose;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import net.fabricmc.api.ClientModInitializer;
|
||||||
|
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||||
|
|
||||||
|
/** Client-side entry point: which player is playing which animation. */
|
||||||
|
public final class Emotes implements ClientModInitializer {
|
||||||
|
public static final String MOD_ID = "saturn_emotes";
|
||||||
|
public static final String VERSION = /*$ mod_version*/ "0.1.0";
|
||||||
|
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
|
||||||
|
|
||||||
|
private static final Map<UUID, AnimationPlayer> PLAYING = new ConcurrentHashMap<>();
|
||||||
|
private static long tick;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInitializeClient() {
|
||||||
|
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||||
|
tick++;
|
||||||
|
PLAYING.values().removeIf(player -> player.isFinished(tick));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Starts {@code animation} on the player, replacing whatever they were playing. */
|
||||||
|
public static void play(UUID player, Animation animation) {
|
||||||
|
PLAYING.put(player, new AnimationPlayer(animation, tick));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void stop(UUID player) {
|
||||||
|
PLAYING.remove(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void stopAll() {
|
||||||
|
PLAYING.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<AnimationPlayer> playing(UUID player) {
|
||||||
|
return Optional.ofNullable(PLAYING.get(player));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The pose to render for this player this frame, or {@link Pose#EMPTY}. */
|
||||||
|
public static Pose sample(UUID player, float partialTick) {
|
||||||
|
AnimationPlayer animation = PLAYING.get(player);
|
||||||
|
return animation == null ? Pose.EMPTY : animation.sample(tick, partialTick);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package org.saturnclient.emotes.fabric;
|
||||||
|
|
||||||
|
import org.saturnclient.emotes.core.Bone;
|
||||||
|
import org.saturnclient.emotes.core.BoneTransform;
|
||||||
|
import org.saturnclient.emotes.core.Pose;
|
||||||
|
|
||||||
|
import net.minecraft.client.model.HumanoidModel;
|
||||||
|
import net.minecraft.client.model.geom.ModelPart;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a {@link Pose} onto a humanoid model after vanilla has posed it. Overlay parts (hat, sleeves,
|
||||||
|
* jacket, pants) are children of the base parts, so they follow automatically.
|
||||||
|
*/
|
||||||
|
public final class PoseApplier {
|
||||||
|
private PoseApplier() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void apply(HumanoidModel<?> model, Pose pose) {
|
||||||
|
for (var entry : pose.bones().entrySet()) {
|
||||||
|
apply(part(model, entry.getKey()), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModelPart part(HumanoidModel<?> model, Bone bone) {
|
||||||
|
return switch (bone) {
|
||||||
|
case HEAD -> model.head;
|
||||||
|
case BODY -> model.body;
|
||||||
|
case RIGHT_ARM -> model.rightArm;
|
||||||
|
case LEFT_ARM -> model.leftArm;
|
||||||
|
case RIGHT_LEG -> model.rightLeg;
|
||||||
|
case LEFT_LEG -> model.leftLeg;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void apply(ModelPart part, BoneTransform transform) {
|
||||||
|
// The model's pose was reset to its defaults before setupAnim, so offsets are relative to those.
|
||||||
|
part.x += transform.x();
|
||||||
|
part.y += transform.y();
|
||||||
|
part.z += transform.z();
|
||||||
|
part.xRot = transform.pitch();
|
||||||
|
part.yRot = transform.yaw();
|
||||||
|
part.zRot = transform.roll();
|
||||||
|
// TODO: bend (transform.bend(), transform.bendAxis()) needs a custom mesh.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package org.saturnclient.emotes.fabric.access;
|
||||||
|
|
||||||
|
import org.saturnclient.emotes.core.Pose;
|
||||||
|
|
||||||
|
/** Mixed into render states so the model knows the pose of the entity it's drawing. */
|
||||||
|
public interface EmotePoseHolder {
|
||||||
|
Pose saturnEmotes$getPose();
|
||||||
|
|
||||||
|
void saturnEmotes$setPose(Pose pose);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.saturnclient.emotes.fabric.mixin;
|
||||||
|
|
||||||
|
import org.saturnclient.emotes.core.Pose;
|
||||||
|
import org.saturnclient.emotes.fabric.PoseApplier;
|
||||||
|
import org.saturnclient.emotes.fabric.access.EmotePoseHolder;
|
||||||
|
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 net.minecraft.client.model.HumanoidModel;
|
||||||
|
import net.minecraft.client.renderer.entity.state.HumanoidRenderState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PlayerModel.setupAnim ends by calling this, and armor layers call it with the same state,
|
||||||
|
* so the player and their armor both get the pose.
|
||||||
|
*/
|
||||||
|
@Mixin(HumanoidModel.class)
|
||||||
|
public class HumanoidModelMixin {
|
||||||
|
@Inject(
|
||||||
|
method = "setupAnim(Lnet/minecraft/client/renderer/entity/state/HumanoidRenderState;)V",
|
||||||
|
at = @At("TAIL"))
|
||||||
|
private void saturnEmotes$applyPose(HumanoidRenderState state, CallbackInfo ci) {
|
||||||
|
Pose pose = ((EmotePoseHolder) state).saturnEmotes$getPose();
|
||||||
|
if (!pose.isEmpty()) {
|
||||||
|
PoseApplier.apply((HumanoidModel<?>) (Object) this, pose);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package org.saturnclient.emotes.fabric.mixin;
|
||||||
|
|
||||||
|
import org.saturnclient.emotes.core.Pose;
|
||||||
|
import org.saturnclient.emotes.fabric.access.EmotePoseHolder;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.Unique;
|
||||||
|
|
||||||
|
import net.minecraft.client.renderer.entity.state.LivingEntityRenderState;
|
||||||
|
|
||||||
|
@Mixin(LivingEntityRenderState.class)
|
||||||
|
public class LivingEntityRenderStateMixin implements EmotePoseHolder {
|
||||||
|
@Unique
|
||||||
|
private Pose saturnEmotes$pose = Pose.EMPTY;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pose saturnEmotes$getPose() {
|
||||||
|
return saturnEmotes$pose;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saturnEmotes$setPose(Pose pose) {
|
||||||
|
saturnEmotes$pose = pose;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.saturnclient.emotes.fabric.mixin;
|
||||||
|
|
||||||
|
import org.saturnclient.emotes.core.Pose;
|
||||||
|
import org.saturnclient.emotes.fabric.Emotes;
|
||||||
|
import org.saturnclient.emotes.fabric.access.EmotePoseHolder;
|
||||||
|
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 net.minecraft.client.renderer.entity.LivingEntityRenderer;
|
||||||
|
import net.minecraft.client.renderer.entity.state.LivingEntityRenderState;
|
||||||
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
|
||||||
|
@Mixin(LivingEntityRenderer.class)
|
||||||
|
public class LivingEntityRendererMixin {
|
||||||
|
@Inject(
|
||||||
|
method = "extractRenderState(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;F)V",
|
||||||
|
at = @At("TAIL"))
|
||||||
|
private void saturnEmotes$extractPose(LivingEntity entity, LivingEntityRenderState state, float partialTick,
|
||||||
|
CallbackInfo ci) {
|
||||||
|
// Render states are reused between entities, so always overwrite.
|
||||||
|
Pose pose = entity instanceof Player ? Emotes.sample(entity.getUUID(), partialTick) : Pose.EMPTY;
|
||||||
|
((EmotePoseHolder) state).saturnEmotes$setPose(pose);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "${id}",
|
||||||
|
"version": "${version}",
|
||||||
|
"name": "${name}",
|
||||||
|
"description": "Player emote library for Saturn Client, with limb bending.",
|
||||||
|
"authors": ["Saturn Client"],
|
||||||
|
"contact": {
|
||||||
|
"sources": "https://git.selimaj.dev/saturnclientmc/emotes"
|
||||||
|
},
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"environment": "client",
|
||||||
|
"entrypoints": {
|
||||||
|
"client": ["org.saturnclient.emotes.fabric.Emotes"]
|
||||||
|
},
|
||||||
|
"mixins": ["saturn_emotes.mixins.json"],
|
||||||
|
"depends": {
|
||||||
|
"fabricloader": ">=${loader}",
|
||||||
|
"minecraft": "${minecraft}",
|
||||||
|
"fabric-lifecycle-events-v1": "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"required": true,
|
||||||
|
"package": "org.saturnclient.emotes.fabric.mixin",
|
||||||
|
"compatibilityLevel": "${java}",
|
||||||
|
"client": [
|
||||||
|
"HumanoidModelMixin",
|
||||||
|
"LivingEntityRenderStateMixin",
|
||||||
|
"LivingEntityRendererMixin"
|
||||||
|
],
|
||||||
|
"injectors": {
|
||||||
|
"defaultRequire": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package org.saturnclient.emotes.testmod;
|
||||||
|
|
||||||
|
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument;
|
||||||
|
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
|
||||||
|
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
import org.saturnclient.emotes.core.Animation;
|
||||||
|
import org.saturnclient.emotes.fabric.Emotes;
|
||||||
|
import org.spongepowered.asm.mixin.MixinEnvironment;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
|
import com.mojang.brigadier.context.CommandContext;
|
||||||
|
|
||||||
|
import net.fabricmc.api.ClientModInitializer;
|
||||||
|
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
|
||||||
|
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dev-only tester. In game: {@code /emotes play <name>}, {@code /emotes stop}, {@code /emotes info}.
|
||||||
|
* Switch to third person (F5) to see yourself.
|
||||||
|
*/
|
||||||
|
public final class EmotesTestmod implements ClientModInitializer {
|
||||||
|
private static final String MINECRAFT = /*$ minecraft*/ "1.21.11";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInitializeClient() {
|
||||||
|
// Applies every mixin now, so a broken target crashes at startup instead of in a world.
|
||||||
|
MixinEnvironment.getCurrentEnvironment().audit();
|
||||||
|
Emotes.LOGGER.info("Mixin audit passed on Minecraft {}", MINECRAFT);
|
||||||
|
|
||||||
|
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(
|
||||||
|
literal("emotes")
|
||||||
|
.then(literal("play").then(argument("name", StringArgumentType.word())
|
||||||
|
.suggests((ctx, builder) -> {
|
||||||
|
TestAnimations.ALL.keySet().forEach(builder::suggest);
|
||||||
|
return builder.buildFuture();
|
||||||
|
})
|
||||||
|
.executes(EmotesTestmod::play)))
|
||||||
|
.then(literal("stop").executes(ctx -> {
|
||||||
|
Emotes.stop(ctx.getSource().getPlayer().getUUID());
|
||||||
|
return 1;
|
||||||
|
}))
|
||||||
|
.then(literal("info").executes(ctx -> {
|
||||||
|
ctx.getSource().sendFeedback(Component.literal("Saturn Emotes " + Emotes.VERSION
|
||||||
|
+ " on Minecraft " + MINECRAFT + ". Animations: "
|
||||||
|
+ String.join(", ", new TreeSet<>(TestAnimations.ALL.keySet()))));
|
||||||
|
return 1;
|
||||||
|
}))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int play(CommandContext<FabricClientCommandSource> ctx) {
|
||||||
|
String name = StringArgumentType.getString(ctx, "name");
|
||||||
|
Animation animation = TestAnimations.ALL.get(name);
|
||||||
|
if (animation == null) {
|
||||||
|
ctx.getSource().sendError(Component.literal("Unknown animation: " + name));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
Emotes.play(ctx.getSource().getPlayer().getUUID(), animation);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package org.saturnclient.emotes.testmod;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.saturnclient.emotes.core.Animation;
|
||||||
|
import org.saturnclient.emotes.core.Bone;
|
||||||
|
import org.saturnclient.emotes.core.BoneTransform;
|
||||||
|
import org.saturnclient.emotes.core.Pose;
|
||||||
|
|
||||||
|
/** Hand-written animations for checking that poses reach the model on every version. */
|
||||||
|
final class TestAnimations {
|
||||||
|
private static final float HALF_PI = (float) (Math.PI / 2);
|
||||||
|
|
||||||
|
static final Map<String, Animation> ALL = Map.of(
|
||||||
|
// Right arm raised, waving side to side. Tests rotation.
|
||||||
|
"wave", of(40, true, t -> Pose.builder()
|
||||||
|
.set(Bone.RIGHT_ARM, BoneTransform.rotation((float) Math.PI, 0,
|
||||||
|
0.4f * (float) Math.sin(t * 0.4f)))
|
||||||
|
.build()),
|
||||||
|
// Arms straight out, then back after 3 seconds. Tests non-looping end.
|
||||||
|
"tpose", of(60, false, t -> Pose.builder()
|
||||||
|
.set(Bone.RIGHT_ARM, BoneTransform.rotation(0, 0, HALF_PI))
|
||||||
|
.set(Bone.LEFT_ARM, BoneTransform.rotation(0, 0, -HALF_PI))
|
||||||
|
.build()),
|
||||||
|
// Body bobbing down and up. Tests offsets.
|
||||||
|
"bob", of(20, true, t -> {
|
||||||
|
float drop = 2f * (1 - (float) Math.cos(t / 20f * 2 * Math.PI));
|
||||||
|
return Pose.builder()
|
||||||
|
.set(Bone.HEAD, new BoneTransform(0, drop, 0, 0, 0, 0, 0, 0))
|
||||||
|
.set(Bone.BODY, new BoneTransform(0, drop, 0, 0, 0, 0, 0, 0))
|
||||||
|
.set(Bone.RIGHT_ARM, new BoneTransform(0, drop, 0, 0, 0, 0, 0, 0))
|
||||||
|
.set(Bone.LEFT_ARM, new BoneTransform(0, drop, 0, 0, 0, 0, 0, 0))
|
||||||
|
.build();
|
||||||
|
}),
|
||||||
|
// Arms forward with elbows folding in and out. Bend isn't rendered yet.
|
||||||
|
"bend", of(40, true, t -> {
|
||||||
|
float bend = HALF_PI * 0.5f * (1 - (float) Math.cos(t / 40f * 2 * Math.PI));
|
||||||
|
return Pose.builder()
|
||||||
|
.set(Bone.RIGHT_ARM, BoneTransform.rotation(-HALF_PI, 0, 0).withBend(bend, 0))
|
||||||
|
.set(Bone.LEFT_ARM, BoneTransform.rotation(-HALF_PI, 0, 0).withBend(bend, 0))
|
||||||
|
.build();
|
||||||
|
}));
|
||||||
|
|
||||||
|
private TestAnimations() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Animation of(float length, boolean loops, Function<Float, Pose> sampler) {
|
||||||
|
return new Animation() {
|
||||||
|
@Override
|
||||||
|
public Pose sample(float tick) {
|
||||||
|
return sampler.apply(tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float length() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean loops() {
|
||||||
|
return loops;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "saturn_emotes_testmod",
|
||||||
|
"version": "${version}",
|
||||||
|
"name": "Saturn Emotes Tester",
|
||||||
|
"description": "Dev-only commands for trying Saturn Emotes in game.",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"environment": "client",
|
||||||
|
"entrypoints": {
|
||||||
|
"client": ["org.saturnclient.emotes.testmod.EmotesTestmod"]
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"saturn_emotes": "*",
|
||||||
|
"fabric-command-api-v2": "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
plugins {
|
||||||
|
id("dev.kikugie.stonecutter")
|
||||||
|
}
|
||||||
|
|
||||||
|
stonecutter active "1.21.11"
|
||||||
|
|
||||||
|
// See https://stonecutter.kikugie.dev/wiki/config/params
|
||||||
|
stonecutter parameters {
|
||||||
|
swaps["mod_version"] = "\"${property("mod.version")}\";"
|
||||||
|
swaps["minecraft"] = "\"${node.metadata.version}\";"
|
||||||
|
|
||||||
|
replacements {
|
||||||
|
string(current.parsed >= "1.21.11") {
|
||||||
|
replace("ResourceLocation", "Identifier")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds and collects every version's jar into build/libs/<mod version>/.
|
||||||
|
tasks.register("buildAll") {
|
||||||
|
group = "build"
|
||||||
|
dependsOn(stonecutter.tasks.named("buildAndCollect"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# See https://stonecutter.kikugie.dev/wiki/config/properties#centralized-properties
|
||||||
|
mod.id = "saturn_emotes"
|
||||||
|
mod.name = "Saturn Emotes"
|
||||||
|
mod.group = "org.saturnclient"
|
||||||
|
mod.version = "0.1.0"
|
||||||
|
|
||||||
|
deps.fabric_loader = "0.19.5"
|
||||||
|
loomx.loom_version = "1.17.21"
|
||||||
|
|
||||||
|
["1.21.4"]
|
||||||
|
mod.mc_compat = "1.21.4"
|
||||||
|
deps.fabric_api = "0.119.4+1.21.4"
|
||||||
|
|
||||||
|
["1.21.5"]
|
||||||
|
mod.mc_compat = "1.21.5"
|
||||||
|
deps.fabric_api = "0.128.2+1.21.5"
|
||||||
|
|
||||||
|
["1.21.6"]
|
||||||
|
mod.mc_compat = "1.21.6"
|
||||||
|
deps.fabric_api = "0.128.2+1.21.6"
|
||||||
|
|
||||||
|
["1.21.7"]
|
||||||
|
mod.mc_compat = "1.21.7"
|
||||||
|
deps.fabric_api = "0.129.0+1.21.7"
|
||||||
|
|
||||||
|
["1.21.8"]
|
||||||
|
mod.mc_compat = "1.21.8"
|
||||||
|
deps.fabric_api = "0.136.1+1.21.8"
|
||||||
|
|
||||||
|
["1.21.9"]
|
||||||
|
mod.mc_compat = "1.21.9"
|
||||||
|
deps.fabric_api = "0.134.1+1.21.9"
|
||||||
|
|
||||||
|
["1.21.10"]
|
||||||
|
mod.mc_compat = "1.21.10"
|
||||||
|
deps.fabric_api = "0.138.4+1.21.10"
|
||||||
|
|
||||||
|
["1.21.11"]
|
||||||
|
mod.mc_compat = "1.21.11"
|
||||||
|
deps.fabric_api = "0.141.6+1.21.11"
|
||||||
Reference in New Issue
Block a user