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
|
||||
# Compiled class file
|
||||
*.class
|
||||
# gradle
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
.gradle/
|
||||
build/
|
||||
out/
|
||||
classes/
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
# eclipse
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
*.launch
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
# idea
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
.idea/
|
||||
*.iml
|
||||
*.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