D1 remaining property codecs : bondage-relevant subset serialized
Audit of 29 remaining properties without name+codec :
- Category A (trivial data types) : 7 properties — direct Codec added
(fixed_head_rotation, reset_living_motion, no_gravity_time, move_time,
coord_update_time, coord_start_keyframe_index, coord_dest_keyframe_index).
Two new shared codecs : LIVING_MOTION_CODEC (ExtendableEnum string lookup)
and TIME_PAIR_LIST_CODEC (even-length float list, odd-count = codec error).
- Category B (functional/lambda) : 3 properties — dispatch registry pattern
(like D2 AnimationAction) with base impls for immediate artist use.
pose_modifier, play_speed_modifier, elapsed_time_modifier. The property
static type stays the wider functional interface for backward compat with
existing consumers (AnimationPlayer.tick, StaticAnimation.modifyPose) via
a subinterface + xmap upcast.
- Category C (combat-only, EF legacy stripped) : 14 properties skipped with
explicit TODO markers referencing V3-REW-11. Covers all 4 AttackPhase
properties (their super-constructor is already commented out) plus
ON_ITEM_CHANGE_EVENT, COORD, COORD_SET_{BEGIN,TICK}, COORD_GET,
DEST_LOCATION_PROVIDER, ENTITY_YROT_PROVIDER, DEST_COORD_YROT_PROVIDER.
- Category D (already sub-file parsed / baked at load) : 4 properties
skipped to avoid duplication : TRANSITION_ANIMATIONS_FROM/TO (need
AnimationAccessor resolution, chicken-and-egg), IK_DEFINITION,
BAKED_IK_DEFINITION.
After this commit : ~31 / 47 properties serializable (baseline 21 pre-D1).
Non-serialized remainder is combat-legacy or intentionally sub-file-only.
Registries created :
- PoseModifierRegistry (3 impls : joint_rotation_offset,
joint_translation_offset, chain)
- PlaybackSpeedModifierRegistry (2 impls : constant_factor, linear_ramp)
- PlaybackTimeModifierRegistry (1 impl : loop_section)
Design note : ChainedPoseModifier.CODEC uses a hand-rolled Codec.of(...)
with inlined Encoder/Decoder bodies rather than RecordCodecBuilder, to
break a static-init cycle between the chain codec and the pose modifier
dispatch codec. Mojang DFU 6.0.8 has no Codec.recursive() — a lazy closure
inside encode/decode is the cleanest workaround.
Artist impact : locomotion tweaks (pose_modifier, play_speed_modifier,
elapsed_time_modifier, reset_living_motion, no_gravity_time and the coord
keyframe indexes) now controllable from JSON. Per-joint rotation /
translation nudges unlock bondage pose constraints without recompiling.
Tests : +41 (411 → 452 GREEN).
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackSpeedModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 3 D1 — data-driven concrete implementation of
|
||||||
|
* {@link PlaybackSpeedModifier}. The base functional interface takes five
|
||||||
|
* parameters ; datapack authors never need all of them, so concrete impls
|
||||||
|
* ignore the irrelevant ones.
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* "play_speed_modifier": { "type": "tiedup:constant_factor", "factor": 0.5 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Consumer : {@link com.tiedup.remake.rig.anim.AnimationPlayer#tick} reads
|
||||||
|
* the modifier and multiplies the current {@code playbackSpeed} by whatever
|
||||||
|
* the modifier returns each tick — so a modifier that returns {@code 0.5F}
|
||||||
|
* halves the animation speed.
|
||||||
|
*/
|
||||||
|
public interface PlaybackSpeedModifierImpl extends PlaybackSpeedModifier {
|
||||||
|
|
||||||
|
Codec<PlaybackSpeedModifierImpl> CODEC = PlaybackSpeedModifierRegistry.dispatchCodec();
|
||||||
|
|
||||||
|
ResourceLocation type();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
float modify(DynamicAnimation self, LivingEntityPatch<?> entitypatch, float speed, float prevElapsedTime, float elapsedTime);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.DataResult;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.ConstantFactorSpeedModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.LinearRampSpeedModifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry of {@link PlaybackSpeedModifierImpl} type ids → codecs. Same
|
||||||
|
* dispatch pattern as {@link PoseModifierRegistry}.
|
||||||
|
*
|
||||||
|
* <p>Base impls :
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code tiedup:constant_factor} — multiply speed by a fixed factor</li>
|
||||||
|
* <li>{@code tiedup:linear_ramp} — linear interpolation between a start and
|
||||||
|
* end factor over {@code elapsedTime} from 0 to {@code duration}</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public final class PlaybackSpeedModifierRegistry {
|
||||||
|
|
||||||
|
private static final Map<ResourceLocation, Codec<? extends PlaybackSpeedModifierImpl>> TYPES = new HashMap<>();
|
||||||
|
|
||||||
|
private PlaybackSpeedModifierRegistry() {}
|
||||||
|
|
||||||
|
public static <T extends PlaybackSpeedModifierImpl> void register(ResourceLocation id, Codec<T> codec) {
|
||||||
|
if (TYPES.containsKey(id)) {
|
||||||
|
throw new IllegalStateException("Playback speed modifier type " + id + " is already registered.");
|
||||||
|
}
|
||||||
|
|
||||||
|
TYPES.put(id, codec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<ResourceLocation, Codec<? extends PlaybackSpeedModifierImpl>> types() {
|
||||||
|
return Collections.unmodifiableMap(TYPES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Codec<? extends PlaybackSpeedModifierImpl> getCodec(ResourceLocation id) {
|
||||||
|
return TYPES.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Codec<PlaybackSpeedModifierImpl> dispatchCodec() {
|
||||||
|
return ResourceLocation.CODEC.partialDispatch(
|
||||||
|
"type",
|
||||||
|
mod -> DataResult.success(mod.type()),
|
||||||
|
id -> {
|
||||||
|
Codec<? extends PlaybackSpeedModifierImpl> codec = TYPES.get(id);
|
||||||
|
if (codec == null) {
|
||||||
|
return DataResult.error(() -> "Unknown playback speed modifier type: " + id);
|
||||||
|
}
|
||||||
|
return DataResult.success(codec);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
register(ConstantFactorSpeedModifier.ID, ConstantFactorSpeedModifier.CODEC);
|
||||||
|
register(LinearRampSpeedModifier.ID, LinearRampSpeedModifier.CODEC);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackTimeModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 3 D1 — data-driven concrete implementation of
|
||||||
|
* {@link PlaybackTimeModifier}. The base interface returns a
|
||||||
|
* {@code Pair<Float, Float>} of {@code (prevElapsed, elapsed)}, typically used
|
||||||
|
* to loop a sub-section of an animation (set {@code elapsed} back to the
|
||||||
|
* looppoint when it crosses a threshold).
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* "elapsed_time_modifier": { "type": "tiedup:loop_section",
|
||||||
|
* "loop_start": 0.3, "loop_end": 0.8 }
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
public interface PlaybackTimeModifierImpl extends PlaybackTimeModifier {
|
||||||
|
|
||||||
|
Codec<PlaybackTimeModifierImpl> CODEC = PlaybackTimeModifierRegistry.dispatchCodec();
|
||||||
|
|
||||||
|
ResourceLocation type();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Pair<Float, Float> modify(DynamicAnimation self, LivingEntityPatch<?> entitypatch, float speed, float prevElapsedTime, float elapsedTime);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.DataResult;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.LoopSectionTimeModifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry of {@link PlaybackTimeModifierImpl} type ids → codecs.
|
||||||
|
*
|
||||||
|
* <p>One base impl today :
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code tiedup:loop_section} — rewind elapsed time back to a loop
|
||||||
|
* start when it crosses a loop end threshold</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Not a ton of base impls because the common «replay this window»
|
||||||
|
* behaviour is what 95% of bondage loops need ; more exotic time warps
|
||||||
|
* (ping-pong, jitter) can be added later without a schema break.
|
||||||
|
*/
|
||||||
|
public final class PlaybackTimeModifierRegistry {
|
||||||
|
|
||||||
|
private static final Map<ResourceLocation, Codec<? extends PlaybackTimeModifierImpl>> TYPES = new HashMap<>();
|
||||||
|
|
||||||
|
private PlaybackTimeModifierRegistry() {}
|
||||||
|
|
||||||
|
public static <T extends PlaybackTimeModifierImpl> void register(ResourceLocation id, Codec<T> codec) {
|
||||||
|
if (TYPES.containsKey(id)) {
|
||||||
|
throw new IllegalStateException("Playback time modifier type " + id + " is already registered.");
|
||||||
|
}
|
||||||
|
|
||||||
|
TYPES.put(id, codec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<ResourceLocation, Codec<? extends PlaybackTimeModifierImpl>> types() {
|
||||||
|
return Collections.unmodifiableMap(TYPES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Codec<? extends PlaybackTimeModifierImpl> getCodec(ResourceLocation id) {
|
||||||
|
return TYPES.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Codec<PlaybackTimeModifierImpl> dispatchCodec() {
|
||||||
|
return ResourceLocation.CODEC.partialDispatch(
|
||||||
|
"type",
|
||||||
|
mod -> DataResult.success(mod.type()),
|
||||||
|
id -> {
|
||||||
|
Codec<? extends PlaybackTimeModifierImpl> codec = TYPES.get(id);
|
||||||
|
if (codec == null) {
|
||||||
|
return DataResult.error(() -> "Unknown playback time modifier type: " + id);
|
||||||
|
}
|
||||||
|
return DataResult.success(codec);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
register(LoopSectionTimeModifier.ID, LoopSectionTimeModifier.CODEC);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.PoseModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 3 D1 — data-driven concrete implementation of the functional
|
||||||
|
* {@link PoseModifier} interface.
|
||||||
|
*
|
||||||
|
* <p>{@link PoseModifier} is a raw lambda {@code (self, pose, patch, elapsed,
|
||||||
|
* partialTick) -> void}, which is not serializable on its own. This interface
|
||||||
|
* extends it with a {@link #type()} identifier and a dispatch codec so that
|
||||||
|
* datapack authors can write concrete modifiers in JSON :
|
||||||
|
* <pre>{@code
|
||||||
|
* "pose_modifier": { "type": "tiedup:joint_rotation_offset",
|
||||||
|
* "joint": "upper_arm_left",
|
||||||
|
* "pitch": 15.0, "yaw": 0.0, "roll": 0.0 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Unlike {@link com.tiedup.remake.rig.anim.action.AnimationAction} which
|
||||||
|
* supports lists (a single animation can fire many actions), a single
|
||||||
|
* {@code pose_modifier} slot accepts exactly one modifier — because
|
||||||
|
* {@code StaticAnimationProperty.POSE_MODIFIER} is declared as {@code <PoseModifier>}
|
||||||
|
* (singular) and the consumer
|
||||||
|
* ({@link com.tiedup.remake.rig.anim.types.StaticAnimation#modifyPose}) calls
|
||||||
|
* {@code poseModifier.modify(...)} exactly once. Authors who want composition
|
||||||
|
* should chain via {@link com.tiedup.remake.rig.anim.modifier.impl.ChainedPoseModifier}.
|
||||||
|
*
|
||||||
|
* <p>All implementations must be side-safe : {@link PoseModifier#modify} is
|
||||||
|
* invoked in both client render and server logic ticks. Implementations that
|
||||||
|
* mutate the pose must do so deterministically from the inputs only, no IO or
|
||||||
|
* world state.
|
||||||
|
*/
|
||||||
|
public interface PoseModifierImpl extends PoseModifier {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch codec — reads the {@code "type"} field and delegates to the
|
||||||
|
* codec registered for that {@link ResourceLocation}.
|
||||||
|
*/
|
||||||
|
Codec<PoseModifierImpl> CODEC = PoseModifierRegistry.dispatchCodec();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The registered type id of this modifier (e.g.
|
||||||
|
* {@code tiedup:joint_rotation_offset}).
|
||||||
|
*/
|
||||||
|
ResourceLocation type();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forwarded from {@link PoseModifier#modify}. Subinterface so that the
|
||||||
|
* generic functional contract stays visible at the implementation site
|
||||||
|
* without Java default-method ambiguity.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
void modify(DynamicAnimation self, Pose pose, LivingEntityPatch<?> entitypatch, float elapsedTime, float partialTick);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.DataResult;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.ChainedPoseModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.JointRotationOffsetModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.JointTranslationOffsetModifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry of {@link PoseModifierImpl} type ids → codecs. Mirrors the design
|
||||||
|
* of {@link com.tiedup.remake.rig.anim.action.AnimationActionRegistry}.
|
||||||
|
*
|
||||||
|
* <p>Three base impls are registered in the static initializer :
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code tiedup:joint_rotation_offset} — nudge a single joint's rotation</li>
|
||||||
|
* <li>{@code tiedup:joint_translation_offset} — nudge a single joint's translation</li>
|
||||||
|
* <li>{@code tiedup:chain} — run a list of modifiers in order</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Registering via the static init means a single reference to
|
||||||
|
* {@link PoseModifierImpl#CODEC} in a parse path is enough to bootstrap the
|
||||||
|
* dispatch table, same bootstrap contract as the action registry.
|
||||||
|
*/
|
||||||
|
public final class PoseModifierRegistry {
|
||||||
|
|
||||||
|
private static final Map<ResourceLocation, Codec<? extends PoseModifierImpl>> TYPES = new HashMap<>();
|
||||||
|
|
||||||
|
private PoseModifierRegistry() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws IllegalStateException if {@code id} is already registered
|
||||||
|
*/
|
||||||
|
public static <T extends PoseModifierImpl> void register(ResourceLocation id, Codec<T> codec) {
|
||||||
|
if (TYPES.containsKey(id)) {
|
||||||
|
throw new IllegalStateException("Pose modifier type " + id + " is already registered.");
|
||||||
|
}
|
||||||
|
|
||||||
|
TYPES.put(id, codec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmodifiable view of all registered modifier types.
|
||||||
|
*/
|
||||||
|
public static Map<ResourceLocation, Codec<? extends PoseModifierImpl>> types() {
|
||||||
|
return Collections.unmodifiableMap(TYPES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Codec<? extends PoseModifierImpl> getCodec(ResourceLocation id) {
|
||||||
|
return TYPES.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Codec<PoseModifierImpl> dispatchCodec() {
|
||||||
|
return ResourceLocation.CODEC.partialDispatch(
|
||||||
|
"type",
|
||||||
|
mod -> DataResult.success(mod.type()),
|
||||||
|
id -> {
|
||||||
|
Codec<? extends PoseModifierImpl> codec = TYPES.get(id);
|
||||||
|
if (codec == null) {
|
||||||
|
return DataResult.error(() -> "Unknown pose modifier type: " + id);
|
||||||
|
}
|
||||||
|
return DataResult.success(codec);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
register(JointRotationOffsetModifier.ID, JointRotationOffsetModifier.CODEC);
|
||||||
|
register(JointTranslationOffsetModifier.ID, JointTranslationOffsetModifier.CODEC);
|
||||||
|
register(ChainedPoseModifier.ID, ChainedPoseModifier.CODEC);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier.impl;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.DataResult;
|
||||||
|
import com.mojang.serialization.Decoder;
|
||||||
|
import com.mojang.serialization.DynamicOps;
|
||||||
|
import com.mojang.serialization.Encoder;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PoseModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composite modifier — runs a list of modifiers sequentially on the same pose.
|
||||||
|
* Compensates for the singular {@code POSE_MODIFIER} property slot (only one
|
||||||
|
* modifier can be attached to an animation, but authors often need several
|
||||||
|
* per-joint nudges).
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* { "type": "tiedup:chain",
|
||||||
|
* "modifiers": [
|
||||||
|
* { "type": "tiedup:joint_rotation_offset", "joint": "upper_arm_left", "pitch": 15.0 },
|
||||||
|
* { "type": "tiedup:joint_translation_offset","joint": "hand_right", "y": -0.05 }
|
||||||
|
* ] }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Nested chains are allowed (no cycle detection — an author-written cycle
|
||||||
|
* would manifest as a {@link StackOverflowError} at tick time, which is
|
||||||
|
* identifiable and not worth defensive code). Execution order is list order.
|
||||||
|
*/
|
||||||
|
public record ChainedPoseModifier(
|
||||||
|
List<PoseModifierImpl> modifiers
|
||||||
|
) implements PoseModifierImpl {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("chain");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hand-rolled lazy codec. We cannot use a {@link RecordCodecBuilder} that
|
||||||
|
* references {@link PoseModifierImpl#CODEC} at static-init time because
|
||||||
|
* that would create a cycle :
|
||||||
|
* {@code ChainedPoseModifier.CODEC → PoseModifierImpl.CODEC
|
||||||
|
* → PoseModifierRegistry.<clinit> → ChainedPoseModifier.CODEC}
|
||||||
|
* which leaves ChainedPoseModifier.CODEC null during the registry
|
||||||
|
* registration call. Instead, we defer the recursive lookup by calling
|
||||||
|
* {@link PoseModifierImpl#CODEC} inside the encode/decode bodies, which
|
||||||
|
* run only at (de)serialization time — long after all static inits have
|
||||||
|
* completed.
|
||||||
|
*/
|
||||||
|
public static final Codec<ChainedPoseModifier> CODEC = Codec.of(
|
||||||
|
new Encoder<ChainedPoseModifier>() {
|
||||||
|
@Override
|
||||||
|
public <T> DataResult<T> encode(ChainedPoseModifier input, DynamicOps<T> ops, T prefix) {
|
||||||
|
return PoseModifierImpl.CODEC.listOf()
|
||||||
|
.encodeStart(ops, input.modifiers())
|
||||||
|
.flatMap(list -> ops.mergeToMap(prefix, ops.createString("modifiers"), list));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Decoder<ChainedPoseModifier>() {
|
||||||
|
@Override
|
||||||
|
public <T> DataResult<com.mojang.datafixers.util.Pair<ChainedPoseModifier, T>> decode(DynamicOps<T> ops, T input) {
|
||||||
|
return ops.getMap(input).flatMap(map -> {
|
||||||
|
T modifiersField = map.get("modifiers");
|
||||||
|
if (modifiersField == null) {
|
||||||
|
return DataResult.error(() -> "Missing field 'modifiers' in tiedup:chain");
|
||||||
|
}
|
||||||
|
return PoseModifierImpl.CODEC.listOf()
|
||||||
|
.parse(ops, modifiersField)
|
||||||
|
.map(list -> com.mojang.datafixers.util.Pair.of(new ChainedPoseModifier(list), input));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modify(DynamicAnimation self, Pose pose, LivingEntityPatch<?> entitypatch, float elapsedTime, float partialTick) {
|
||||||
|
for (PoseModifierImpl m : this.modifiers) {
|
||||||
|
m.modify(self, pose, entitypatch, elapsedTime, partialTick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier.impl;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PlaybackSpeedModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiply the current playback speed by a fixed factor every tick.
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* "play_speed_modifier": { "type": "tiedup:constant_factor", "factor": 0.5 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>A {@code factor} of {@code 1.0} is a no-op. Negative factors technically
|
||||||
|
* work (animation plays backward) but the reverse flag in
|
||||||
|
* {@link com.tiedup.remake.rig.anim.AnimationPlayer} already handles that
|
||||||
|
* cleanly — prefer that over a negative speed modifier.
|
||||||
|
*/
|
||||||
|
public record ConstantFactorSpeedModifier(float factor) implements PlaybackSpeedModifierImpl {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("constant_factor");
|
||||||
|
|
||||||
|
public static final Codec<ConstantFactorSpeedModifier> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
Codec.FLOAT.fieldOf("factor").forGetter(ConstantFactorSpeedModifier::factor)
|
||||||
|
).apply(i, ConstantFactorSpeedModifier::new));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float modify(DynamicAnimation self, LivingEntityPatch<?> entitypatch, float speed, float prevElapsedTime, float elapsedTime) {
|
||||||
|
return speed * this.factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier.impl;
|
||||||
|
|
||||||
|
import org.joml.Quaternionf;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PoseModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.armature.JointTransform;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate a single named joint by a fixed Euler offset (degrees) at every tick
|
||||||
|
* of the animation. Bondage pipeline use-case : lock a restrained arm joint to
|
||||||
|
* a tighter angle than the authored animation provides.
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* { "type": "tiedup:joint_rotation_offset",
|
||||||
|
* "joint": "upper_arm_left",
|
||||||
|
* "pitch": 15.0,
|
||||||
|
* "yaw": 0.0,
|
||||||
|
* "roll": 0.0 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Angles default to zero if omitted — a {@code joint_rotation_offset} with
|
||||||
|
* all three zero is a no-op (author error, logged nowhere — by design, since
|
||||||
|
* it's a cheap operation and a valid edge case during iteration).
|
||||||
|
*
|
||||||
|
* <p>If the joint is absent from the pose, this modifier is a silent no-op :
|
||||||
|
* the authored animation simply doesn't touch that joint on this frame. This
|
||||||
|
* matches the semantics of {@link Pose#orElseEmpty} used elsewhere.
|
||||||
|
*/
|
||||||
|
public record JointRotationOffsetModifier(
|
||||||
|
String joint,
|
||||||
|
float pitch,
|
||||||
|
float yaw,
|
||||||
|
float roll
|
||||||
|
) implements PoseModifierImpl {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("joint_rotation_offset");
|
||||||
|
|
||||||
|
public static final Codec<JointRotationOffsetModifier> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
Codec.STRING.fieldOf("joint").forGetter(JointRotationOffsetModifier::joint),
|
||||||
|
Codec.FLOAT.optionalFieldOf("pitch", 0.0F).forGetter(JointRotationOffsetModifier::pitch),
|
||||||
|
Codec.FLOAT.optionalFieldOf("yaw", 0.0F).forGetter(JointRotationOffsetModifier::yaw),
|
||||||
|
Codec.FLOAT.optionalFieldOf("roll", 0.0F).forGetter(JointRotationOffsetModifier::roll)
|
||||||
|
).apply(i, JointRotationOffsetModifier::new));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modify(DynamicAnimation self, Pose pose, LivingEntityPatch<?> entitypatch, float elapsedTime, float partialTick) {
|
||||||
|
if (!pose.hasTransform(this.joint)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JointTransform jt = pose.get(this.joint);
|
||||||
|
Quaternionf offset = new Quaternionf().rotationXYZ(
|
||||||
|
this.pitch * Mth.DEG_TO_RAD,
|
||||||
|
this.yaw * Mth.DEG_TO_RAD,
|
||||||
|
this.roll * Mth.DEG_TO_RAD
|
||||||
|
);
|
||||||
|
jt.rotation().mul(offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier.impl;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PoseModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.armature.JointTransform;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shift a single named joint by a fixed translation offset at every tick of
|
||||||
|
* the animation. Use-case : pull a constrained wrist joint closer to the
|
||||||
|
* furniture anchor than the authored animation puts it.
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* { "type": "tiedup:joint_translation_offset",
|
||||||
|
* "joint": "hand_right",
|
||||||
|
* "x": 0.0, "y": -0.05, "z": 0.02 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>All three axes default to zero — a fully-zero offset is a no-op.
|
||||||
|
* Missing joint is a silent no-op (same contract as the rotation variant).
|
||||||
|
*/
|
||||||
|
public record JointTranslationOffsetModifier(
|
||||||
|
String joint,
|
||||||
|
float x,
|
||||||
|
float y,
|
||||||
|
float z
|
||||||
|
) implements PoseModifierImpl {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("joint_translation_offset");
|
||||||
|
|
||||||
|
public static final Codec<JointTranslationOffsetModifier> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
Codec.STRING.fieldOf("joint").forGetter(JointTranslationOffsetModifier::joint),
|
||||||
|
Codec.FLOAT.optionalFieldOf("x", 0.0F).forGetter(JointTranslationOffsetModifier::x),
|
||||||
|
Codec.FLOAT.optionalFieldOf("y", 0.0F).forGetter(JointTranslationOffsetModifier::y),
|
||||||
|
Codec.FLOAT.optionalFieldOf("z", 0.0F).forGetter(JointTranslationOffsetModifier::z)
|
||||||
|
).apply(i, JointTranslationOffsetModifier::new));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modify(DynamicAnimation self, Pose pose, LivingEntityPatch<?> entitypatch, float elapsedTime, float partialTick) {
|
||||||
|
if (!pose.hasTransform(this.joint)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JointTransform jt = pose.get(this.joint);
|
||||||
|
jt.translation().add(this.x, this.y, this.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier.impl;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PlaybackSpeedModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ramp the playback speed factor linearly from {@code from} to {@code to} over
|
||||||
|
* the time window {@code [startTime, endTime]} (in seconds of animation
|
||||||
|
* elapsed). Outside the window, clamps to the boundary value.
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* "play_speed_modifier": { "type": "tiedup:linear_ramp",
|
||||||
|
* "from": 0.2, "to": 1.0,
|
||||||
|
* "start_time": 0.0, "end_time": 0.5 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Use-case : hesitant-start animations (slow initial frames, then ramp to
|
||||||
|
* full speed for the payoff). The modifier's output is multiplied against the
|
||||||
|
* base speed by {@link com.tiedup.remake.rig.anim.AnimationPlayer#tick}, so
|
||||||
|
* {@code from=1.0, to=1.0} is a no-op.
|
||||||
|
*
|
||||||
|
* <p>{@code end_time <= start_time} degenerates to returning {@code to} for
|
||||||
|
* all {@code elapsedTime >= start_time} (clamped path above saturation).
|
||||||
|
*/
|
||||||
|
public record LinearRampSpeedModifier(
|
||||||
|
float from,
|
||||||
|
float to,
|
||||||
|
float startTime,
|
||||||
|
float endTime
|
||||||
|
) implements PlaybackSpeedModifierImpl {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("linear_ramp");
|
||||||
|
|
||||||
|
public static final Codec<LinearRampSpeedModifier> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
Codec.FLOAT.fieldOf("from").forGetter(LinearRampSpeedModifier::from),
|
||||||
|
Codec.FLOAT.fieldOf("to").forGetter(LinearRampSpeedModifier::to),
|
||||||
|
Codec.FLOAT.optionalFieldOf("start_time", 0.0F).forGetter(LinearRampSpeedModifier::startTime),
|
||||||
|
Codec.FLOAT.optionalFieldOf("end_time", 1.0F).forGetter(LinearRampSpeedModifier::endTime)
|
||||||
|
).apply(i, LinearRampSpeedModifier::new));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float modify(DynamicAnimation self, LivingEntityPatch<?> entitypatch, float speed, float prevElapsedTime, float elapsedTime) {
|
||||||
|
float factor;
|
||||||
|
if (elapsedTime <= this.startTime) {
|
||||||
|
factor = this.from;
|
||||||
|
} else if (elapsedTime >= this.endTime || this.endTime <= this.startTime) {
|
||||||
|
factor = this.to;
|
||||||
|
} else {
|
||||||
|
float progress = (elapsedTime - this.startTime) / (this.endTime - this.startTime);
|
||||||
|
factor = Mth.lerp(progress, this.from, this.to);
|
||||||
|
}
|
||||||
|
return speed * factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier.impl;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PlaybackTimeModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewind the animation playhead back to {@code loopStart} every time it
|
||||||
|
* crosses {@code loopEnd}. Matches the «sustain loop» pattern used e.g. for
|
||||||
|
* breathing idles inside a rest phase — the intro plays once, a middle section
|
||||||
|
* loops, and the outro is triggered by a separate state transition (not this
|
||||||
|
* modifier).
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* "elapsed_time_modifier": { "type": "tiedup:loop_section",
|
||||||
|
* "loop_start": 0.3, "loop_end": 0.8 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>This modifier is a pure function of {@code elapsedTime} — it does not
|
||||||
|
* mutate the animation. The {@code prevElapsedTime} is rewound symmetrically
|
||||||
|
* so event triggers using {@code [prev, elapsed]} windows still fire cleanly
|
||||||
|
* after the rewind.
|
||||||
|
*
|
||||||
|
* <p>Invariant : {@code loopStart < loopEnd}. Violating this yields undefined
|
||||||
|
* behaviour (probably a NaN or an instant loop) — no defensive check because
|
||||||
|
* the author would notice immediately on first playback.
|
||||||
|
*/
|
||||||
|
public record LoopSectionTimeModifier(
|
||||||
|
float loopStart,
|
||||||
|
float loopEnd
|
||||||
|
) implements PlaybackTimeModifierImpl {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("loop_section");
|
||||||
|
|
||||||
|
public static final Codec<LoopSectionTimeModifier> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
Codec.FLOAT.fieldOf("loop_start").forGetter(LoopSectionTimeModifier::loopStart),
|
||||||
|
Codec.FLOAT.fieldOf("loop_end").forGetter(LoopSectionTimeModifier::loopEnd)
|
||||||
|
).apply(i, LoopSectionTimeModifier::new));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pair<Float, Float> modify(DynamicAnimation self, LivingEntityPatch<?> entitypatch, float speed, float prevElapsedTime, float elapsedTime) {
|
||||||
|
if (elapsedTime >= this.loopEnd) {
|
||||||
|
float overshoot = elapsedTime - this.loopEnd;
|
||||||
|
float sectionLen = this.loopEnd - this.loopStart;
|
||||||
|
float wrapped = this.loopStart + (overshoot % sectionLen);
|
||||||
|
float prevWrapped = prevElapsedTime - (elapsedTime - wrapped);
|
||||||
|
return Pair.of(prevWrapped, wrapped);
|
||||||
|
}
|
||||||
|
return Pair.of(prevElapsedTime, elapsedTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import com.google.common.collect.Maps;
|
|||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.mojang.datafixers.util.Pair;
|
import com.mojang.datafixers.util.Pair;
|
||||||
import com.mojang.serialization.Codec;
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.DataResult;
|
||||||
import com.mojang.serialization.JsonOps;
|
import com.mojang.serialization.JsonOps;
|
||||||
|
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
@@ -29,6 +30,9 @@ import com.tiedup.remake.rig.anim.LivingMotion;
|
|||||||
import com.tiedup.remake.rig.anim.Pose;
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
import com.tiedup.remake.rig.anim.TransformSheet;
|
import com.tiedup.remake.rig.anim.TransformSheet;
|
||||||
import com.tiedup.remake.rig.anim.action.DataDrivenAnimationEvents;
|
import com.tiedup.remake.rig.anim.action.DataDrivenAnimationEvents;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PlaybackSpeedModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PlaybackTimeModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PoseModifierImpl;
|
||||||
import com.tiedup.remake.rig.anim.property.AnimationEvent.SimpleEvent;
|
import com.tiedup.remake.rig.anim.property.AnimationEvent.SimpleEvent;
|
||||||
import com.tiedup.remake.rig.anim.property.MoveCoordFunctions.MoveCoordGetter;
|
import com.tiedup.remake.rig.anim.property.MoveCoordFunctions.MoveCoordGetter;
|
||||||
import com.tiedup.remake.rig.anim.property.MoveCoordFunctions.MoveCoordSetter;
|
import com.tiedup.remake.rig.anim.property.MoveCoordFunctions.MoveCoordSetter;
|
||||||
@@ -47,6 +51,53 @@ import com.tiedup.remake.rig.patch.item.CapabilityItem;
|
|||||||
public abstract class AnimationProperty<T> {
|
public abstract class AnimationProperty<T> {
|
||||||
private static final Map<String, AnimationProperty<?>> SERIALIZABLE_ANIMATION_PROPERTY_KEYS = Maps.newHashMap();
|
private static final Map<String, AnimationProperty<?>> SERIALIZABLE_ANIMATION_PROPERTY_KEYS = Maps.newHashMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 3 D1 — {@link LivingMotion} codec. {@code LivingMotion} is an
|
||||||
|
* {@link com.tiedup.remake.rig.util.ExtendableEnum} so its string form is
|
||||||
|
* the {@link #toString()} lowercased ; we deserialize via
|
||||||
|
* {@code ENUM_MANAGER.getOrThrow(String)} which throws if unknown.
|
||||||
|
*/
|
||||||
|
public static final Codec<LivingMotion> LIVING_MOTION_CODEC = Codec.STRING.flatXmap(
|
||||||
|
name -> {
|
||||||
|
try {
|
||||||
|
return DataResult.success(LivingMotion.ENUM_MANAGER.getOrThrow(name));
|
||||||
|
} catch (java.util.NoSuchElementException e) {
|
||||||
|
return DataResult.error(() -> "Unknown living motion: " + name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
motion -> DataResult.success(motion.toString().toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 3 D1 — {@link TimePairList} codec. Authored as a flat list of
|
||||||
|
* floats that must have an even length (pairs of {@code begin, end}).
|
||||||
|
* Odd-length lists surface as a codec error (logged + property skipped).
|
||||||
|
*
|
||||||
|
* <p>JSON shape : {@code [0.0, 0.3, 0.6, 0.9]} → two pairs, {@code [0,0.3]}
|
||||||
|
* and {@code [0.6,0.9]}. We go through a float[] because {@code
|
||||||
|
* TimePairList.create} is varargs. Decoding via
|
||||||
|
* {@link TimePairList#create(float...)} surfaces the odd-count invariant
|
||||||
|
* via {@link IllegalArgumentException} — wrapped into a {@link DataResult}
|
||||||
|
* error.
|
||||||
|
*/
|
||||||
|
public static final Codec<TimePairList> TIME_PAIR_LIST_CODEC = Codec.FLOAT.listOf().flatXmap(
|
||||||
|
list -> {
|
||||||
|
if ((list.size() & 1) != 0) {
|
||||||
|
return DataResult.error(() -> "TimePairList must have an even number of floats, got " + list.size());
|
||||||
|
}
|
||||||
|
float[] arr = new float[list.size()];
|
||||||
|
for (int i = 0; i < arr.length; i++) {
|
||||||
|
arr[i] = list.get(i);
|
||||||
|
}
|
||||||
|
return DataResult.success(TimePairList.create(arr));
|
||||||
|
},
|
||||||
|
// Encode back : no public accessor on TimePairList's internal pairs,
|
||||||
|
// and the property pipeline is read-only from JSON (never re-serialized
|
||||||
|
// to disk) — we therefore refuse encoding. If round-trip becomes a
|
||||||
|
// requirement, expose TimePairList#asFloatArray() first.
|
||||||
|
tpl -> DataResult.error(() -> "TimePairList encoding is not supported (read-only datapack property)")
|
||||||
|
);
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public static <T> AnimationProperty<T> getSerializableProperty(String name) {
|
public static <T> AnimationProperty<T> getSerializableProperty(String name) {
|
||||||
if (!SERIALIZABLE_ANIMATION_PROPERTY_KEYS.containsKey(name)) {
|
if (!SERIALIZABLE_ANIMATION_PROPERTY_KEYS.containsKey(name)) {
|
||||||
@@ -135,38 +186,116 @@ public abstract class AnimationProperty<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* An event triggered when entity changes an item in hand.
|
* An event triggered when entity changes an item in hand.
|
||||||
* Retenu comme hook datapack : un event écrit en JSON par un datapack tiers peut s'abonner
|
*
|
||||||
* au changement d'item porté pour déclencher une réaction (voir Phase 3 data-driven anims).
|
* <p>Phase 3 D1 — Category C, SKIPPED. The item-change event is a
|
||||||
|
* combat/equipment hook from Epic Fight upstream ; no bondage pipeline
|
||||||
|
* consumer reads this property today. Re-enable (register a codec) when
|
||||||
|
* weapon/equipment reactive animations are reintroduced.
|
||||||
*/
|
*/
|
||||||
public static final StaticAnimationProperty<SimpleEvent<AnimationEvent.E2<CapabilityItem, CapabilityItem>>> ON_ITEM_CHANGE_EVENT = new StaticAnimationProperty<SimpleEvent<AnimationEvent.E2<CapabilityItem, CapabilityItem>>> ();
|
public static final StaticAnimationProperty<SimpleEvent<AnimationEvent.E2<CapabilityItem, CapabilityItem>>> ON_ITEM_CHANGE_EVENT = new StaticAnimationProperty<SimpleEvent<AnimationEvent.E2<CapabilityItem, CapabilityItem>>> ();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* You can modify the playback speed of the animation.
|
* You can modify the playback speed of the animation.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — serializable via the
|
||||||
|
* {@link PlaybackSpeedModifierImpl#CODEC} dispatch codec (Category B).
|
||||||
|
* JSON example :
|
||||||
|
* <pre>{@code
|
||||||
|
* "play_speed_modifier": { "type": "tiedup:constant_factor", "factor": 0.5 }
|
||||||
|
* }</pre>
|
||||||
|
* The upcast {@code PlaybackSpeedModifierImpl → PlaybackSpeedModifier}
|
||||||
|
* is implicit (sub-interface) so the property stays typed as the wider
|
||||||
|
* functional type for backward-compat with existing consumers in
|
||||||
|
* {@link com.tiedup.remake.rig.anim.AnimationPlayer#tick} and
|
||||||
|
* {@link com.tiedup.remake.rig.anim.client.AnimationSubFileReader}.
|
||||||
*/
|
*/
|
||||||
public static final StaticAnimationProperty<PlaybackSpeedModifier> PLAY_SPEED_MODIFIER = new StaticAnimationProperty<PlaybackSpeedModifier> ();
|
public static final StaticAnimationProperty<PlaybackSpeedModifier> PLAY_SPEED_MODIFIER = new StaticAnimationProperty<PlaybackSpeedModifier> (
|
||||||
|
"play_speed_modifier",
|
||||||
|
PlaybackSpeedModifierImpl.CODEC.xmap(
|
||||||
|
impl -> (PlaybackSpeedModifier) impl,
|
||||||
|
base -> {
|
||||||
|
if (base instanceof PlaybackSpeedModifierImpl impl) return impl;
|
||||||
|
throw new IllegalStateException("PlaybackSpeedModifier value is not a serializable impl: " + base);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* You can modify the playback speed of the animation.
|
* You can modify the elapsed playback time of the animation.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — serializable via the
|
||||||
|
* {@link PlaybackTimeModifierImpl#CODEC} dispatch codec (Category B).
|
||||||
|
* JSON example :
|
||||||
|
* <pre>{@code
|
||||||
|
* "elapsed_time_modifier": { "type": "tiedup:loop_section",
|
||||||
|
* "loop_start": 0.3, "loop_end": 0.8 }
|
||||||
|
* }</pre>
|
||||||
*/
|
*/
|
||||||
public static final StaticAnimationProperty<PlaybackTimeModifier> ELAPSED_TIME_MODIFIER = new StaticAnimationProperty<PlaybackTimeModifier> ();
|
public static final StaticAnimationProperty<PlaybackTimeModifier> ELAPSED_TIME_MODIFIER = new StaticAnimationProperty<PlaybackTimeModifier> (
|
||||||
|
"elapsed_time_modifier",
|
||||||
|
PlaybackTimeModifierImpl.CODEC.xmap(
|
||||||
|
impl -> (PlaybackTimeModifier) impl,
|
||||||
|
base -> {
|
||||||
|
if (base instanceof PlaybackTimeModifierImpl impl) return impl;
|
||||||
|
throw new IllegalStateException("PlaybackTimeModifier value is not a serializable impl: " + base);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This property will be called both in client and server when modifying the pose
|
* This property will be called both in client and server when modifying the pose.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — serializable via the
|
||||||
|
* {@link PoseModifierImpl#CODEC} dispatch codec (Category B). Key
|
||||||
|
* artist unlock for bondage : per-joint nudges can be authored
|
||||||
|
* from JSON :
|
||||||
|
* <pre>{@code
|
||||||
|
* "pose_modifier": { "type": "tiedup:chain",
|
||||||
|
* "modifiers": [
|
||||||
|
* { "type": "tiedup:joint_rotation_offset", "joint": "upper_arm_left", "pitch": 15.0 },
|
||||||
|
* { "type": "tiedup:joint_translation_offset", "joint": "hand_right", "y": -0.05 }
|
||||||
|
* ] }
|
||||||
|
* }</pre>
|
||||||
*/
|
*/
|
||||||
public static final StaticAnimationProperty<PoseModifier> POSE_MODIFIER = new StaticAnimationProperty<PoseModifier> ();
|
public static final StaticAnimationProperty<PoseModifier> POSE_MODIFIER = new StaticAnimationProperty<PoseModifier> (
|
||||||
|
"pose_modifier",
|
||||||
|
PoseModifierImpl.CODEC.xmap(
|
||||||
|
impl -> (PoseModifier) impl,
|
||||||
|
base -> {
|
||||||
|
if (base instanceof PoseModifierImpl impl) return impl;
|
||||||
|
throw new IllegalStateException("PoseModifier value is not a serializable impl: " + base);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fix the head rotation to the player's body rotation
|
* Fix the head rotation to the player's body rotation.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category A, trivial boolean codec.
|
||||||
*/
|
*/
|
||||||
public static final StaticAnimationProperty<Boolean> FIXED_HEAD_ROTATION = new StaticAnimationProperty<Boolean> ();
|
public static final StaticAnimationProperty<Boolean> FIXED_HEAD_ROTATION = new StaticAnimationProperty<Boolean> (
|
||||||
|
"fixed_head_rotation",
|
||||||
|
Codec.BOOL
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines static animations as link animation when the animation is followed by a specific animation
|
* Defines static animations as link animation when the animation is followed by a specific animation.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category D, SKIPPED. The map value is an
|
||||||
|
* {@link AnimationAccessor} which is an internal registry handle (not a
|
||||||
|
* data-describable object) ; resolving accessors from ids requires the
|
||||||
|
* {@link com.tiedup.remake.rig.anim.AnimationManager} to already be
|
||||||
|
* populated, which is only true after full resource-pack load. The
|
||||||
|
* existing sub-file reader path handles transitions correctly — moving
|
||||||
|
* the parse here would create a chicken-and-egg bootstrapping issue.
|
||||||
*/
|
*/
|
||||||
public static final StaticAnimationProperty<Map<ResourceLocation, AnimationAccessor<? extends StaticAnimation>>> TRANSITION_ANIMATIONS_FROM = new StaticAnimationProperty<Map<ResourceLocation, AnimationAccessor<? extends StaticAnimation>>> ();
|
public static final StaticAnimationProperty<Map<ResourceLocation, AnimationAccessor<? extends StaticAnimation>>> TRANSITION_ANIMATIONS_FROM = new StaticAnimationProperty<Map<ResourceLocation, AnimationAccessor<? extends StaticAnimation>>> ();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines static animations as link animation when the animation is following a specific animation
|
* Defines static animations as link animation when the animation is following a specific animation.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category D, SKIPPED (same reason as
|
||||||
|
* {@link #TRANSITION_ANIMATIONS_FROM}).
|
||||||
*/
|
*/
|
||||||
public static final StaticAnimationProperty<Map<ResourceLocation, AnimationAccessor<? extends StaticAnimation>>> TRANSITION_ANIMATIONS_TO = new StaticAnimationProperty<Map<ResourceLocation, AnimationAccessor<? extends StaticAnimation>>> ();
|
public static final StaticAnimationProperty<Map<ResourceLocation, AnimationAccessor<? extends StaticAnimation>>> TRANSITION_ANIMATIONS_TO = new StaticAnimationProperty<Map<ResourceLocation, AnimationAccessor<? extends StaticAnimation>>> ();
|
||||||
|
|
||||||
@@ -176,19 +305,38 @@ public abstract class AnimationProperty<T> {
|
|||||||
public static final StaticAnimationProperty<Boolean> NO_PHYSICS = new StaticAnimationProperty<Boolean> ("no_physics", Codec.BOOL);
|
public static final StaticAnimationProperty<Boolean> NO_PHYSICS = new StaticAnimationProperty<Boolean> ("no_physics", Codec.BOOL);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inverse kinematics information
|
* Inverse kinematics information.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category D, SKIPPED. {@link InverseKinematicsDefinition}
|
||||||
|
* holds complex baked state (joint chains, pole targets, constraint
|
||||||
|
* weights) that is authored via a dedicated IK definition file, not
|
||||||
|
* via the animation properties block. See the sub-file reader in
|
||||||
|
* {@link com.tiedup.remake.rig.anim.client.AnimationSubFileReader}.
|
||||||
*/
|
*/
|
||||||
public static final StaticAnimationProperty<List<InverseKinematicsDefinition>> IK_DEFINITION = new StaticAnimationProperty<List<InverseKinematicsDefinition>> ();
|
public static final StaticAnimationProperty<List<InverseKinematicsDefinition>> IK_DEFINITION = new StaticAnimationProperty<List<InverseKinematicsDefinition>> ();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This property automatically baked when animation is loaded
|
* This property automatically baked when animation is loaded.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category D, SKIPPED. Never authored in JSON : this
|
||||||
|
* slot is populated at load time by
|
||||||
|
* {@link com.tiedup.remake.rig.anim.types.StaticAnimation#loadAnimation}
|
||||||
|
* from {@link #IK_DEFINITION} which is then cleared.
|
||||||
*/
|
*/
|
||||||
public static final StaticAnimationProperty<List<BakedInverseKinematicsDefinition>> BAKED_IK_DEFINITION = new StaticAnimationProperty<List<BakedInverseKinematicsDefinition>> ();
|
public static final StaticAnimationProperty<List<BakedInverseKinematicsDefinition>> BAKED_IK_DEFINITION = new StaticAnimationProperty<List<BakedInverseKinematicsDefinition>> ();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This property reset the entity's living motion
|
* This property reset the entity's living motion.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category A, codec uses the shared
|
||||||
|
* {@link AnimationProperty#LIVING_MOTION_CODEC} which resolves against
|
||||||
|
* {@link LivingMotion#ENUM_MANAGER}. JSON example :
|
||||||
|
* <pre>{@code "reset_living_motion": "idle"}</pre>
|
||||||
*/
|
*/
|
||||||
public static final StaticAnimationProperty<LivingMotion> RESET_LIVING_MOTION = new StaticAnimationProperty<LivingMotion> ();
|
public static final StaticAnimationProperty<LivingMotion> RESET_LIVING_MOTION = new StaticAnimationProperty<LivingMotion> (
|
||||||
|
"reset_living_motion",
|
||||||
|
LIVING_MOTION_CODEC
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ActionAnimationProperty<T> extends StaticAnimationProperty<T> {
|
public static class ActionAnimationProperty<T> extends StaticAnimationProperty<T> {
|
||||||
@@ -218,11 +366,25 @@ public abstract class AnimationProperty<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This property determines the time of entity not affected by gravity.
|
* This property determines the time of entity not affected by gravity.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category A, codec uses the shared
|
||||||
|
* {@link AnimationProperty#TIME_PAIR_LIST_CODEC}. JSON example :
|
||||||
|
* <pre>{@code "no_gravity_time": [0.1, 0.4, 0.6, 0.9]}</pre>
|
||||||
|
* (two no-gravity windows : [0.1..0.4] and [0.6..0.9]).
|
||||||
*/
|
*/
|
||||||
public static final ActionAnimationProperty<TimePairList> NO_GRAVITY_TIME = new ActionAnimationProperty<TimePairList> ();
|
public static final ActionAnimationProperty<TimePairList> NO_GRAVITY_TIME = new ActionAnimationProperty<TimePairList> (
|
||||||
|
"no_gravity_time",
|
||||||
|
TIME_PAIR_LIST_CODEC
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coord of action animation
|
* Coord of action animation.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category C, SKIPPED. {@link TransformSheet} is
|
||||||
|
* populated from the animation's baked keyframes by
|
||||||
|
* {@link com.tiedup.remake.rig.asset.JsonAssetLoader} at load time
|
||||||
|
* (not authored in the properties block). Exposing a codec here would
|
||||||
|
* double-write an already-computed value.
|
||||||
*/
|
*/
|
||||||
public static final ActionAnimationProperty<TransformSheet> COORD = new ActionAnimationProperty<TransformSheet> ();
|
public static final ActionAnimationProperty<TransformSheet> COORD = new ActionAnimationProperty<TransformSheet> ();
|
||||||
|
|
||||||
@@ -233,21 +395,38 @@ public abstract class AnimationProperty<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* You can specify the coord movement time in action animation. Must be registered in order of time.
|
* You can specify the coord movement time in action animation. Must be registered in order of time.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category A, codec uses
|
||||||
|
* {@link AnimationProperty#TIME_PAIR_LIST_CODEC}.
|
||||||
*/
|
*/
|
||||||
public static final ActionAnimationProperty<TimePairList> MOVE_TIME = new ActionAnimationProperty<TimePairList> ();
|
public static final ActionAnimationProperty<TimePairList> MOVE_TIME = new ActionAnimationProperty<TimePairList> (
|
||||||
|
"move_time",
|
||||||
|
TIME_PAIR_LIST_CODEC
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the dynamic coordinates of {@link ActionAnimation}. Called before creation of {@link LinkAnimation}.
|
* Set the dynamic coordinates of {@link ActionAnimation}. Called before creation of {@link LinkAnimation}.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category C, SKIPPED. {@link MoveCoordSetter} is a
|
||||||
|
* combat/action-movement hook from Epic Fight upstream ; the bondage
|
||||||
|
* pipeline never authors these at the datapack level (coord work is
|
||||||
|
* done via coord joints and the sub-file reader).
|
||||||
*/
|
*/
|
||||||
public static final ActionAnimationProperty<MoveCoordSetter> COORD_SET_BEGIN = new ActionAnimationProperty<MoveCoordSetter> ();
|
public static final ActionAnimationProperty<MoveCoordSetter> COORD_SET_BEGIN = new ActionAnimationProperty<MoveCoordSetter> ();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the dynamic coordinates of {@link ActionAnimation}.
|
* Set the dynamic coordinates of {@link ActionAnimation}.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category C, SKIPPED (same reason as
|
||||||
|
* {@link #COORD_SET_BEGIN}).
|
||||||
*/
|
*/
|
||||||
public static final ActionAnimationProperty<MoveCoordSetter> COORD_SET_TICK = new ActionAnimationProperty<MoveCoordSetter> ();
|
public static final ActionAnimationProperty<MoveCoordSetter> COORD_SET_TICK = new ActionAnimationProperty<MoveCoordSetter> ();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the coordinates of action animation.
|
* Set the coordinates of action animation.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category C, SKIPPED (same reason as
|
||||||
|
* {@link #COORD_SET_BEGIN}).
|
||||||
*/
|
*/
|
||||||
public static final ActionAnimationProperty<MoveCoordGetter> COORD_GET = new ActionAnimationProperty<MoveCoordGetter> ();
|
public static final ActionAnimationProperty<MoveCoordGetter> COORD_GET = new ActionAnimationProperty<MoveCoordGetter> ();
|
||||||
|
|
||||||
@@ -268,8 +447,17 @@ public abstract class AnimationProperty<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This property determines the update time of {@link ActionAnimationProperty#COORD_SET_TICK}. If the current time out of the bound it uses {@link MoveCoordFunctions#RAW_COORD and MoveCoordFunctions#DIFF_FROM_PREV_COORD}}
|
* This property determines the update time of {@link ActionAnimationProperty#COORD_SET_TICK}. If the current time out of the bound it uses {@link MoveCoordFunctions#RAW_COORD and MoveCoordFunctions#DIFF_FROM_PREV_COORD}}
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category A, codec uses
|
||||||
|
* {@link AnimationProperty#TIME_PAIR_LIST_CODEC}. Still serialized
|
||||||
|
* despite being tied to {@link #COORD_SET_TICK} (Category C) because
|
||||||
|
* the underlying datatype is data and authoring the time windows
|
||||||
|
* without the setter is still legal (it degrades to a no-op, cheap).
|
||||||
*/
|
*/
|
||||||
public static final ActionAnimationProperty<TimePairList> COORD_UPDATE_TIME = new ActionAnimationProperty<TimePairList> ();
|
public static final ActionAnimationProperty<TimePairList> COORD_UPDATE_TIME = new ActionAnimationProperty<TimePairList> (
|
||||||
|
"coord_update_time",
|
||||||
|
TIME_PAIR_LIST_CODEC
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This property determines if it reset the player basic attack combo counter or not.
|
* This property determines if it reset the player basic attack combo counter or not.
|
||||||
@@ -278,29 +466,48 @@ public abstract class AnimationProperty<T> {
|
|||||||
public static final ActionAnimationProperty<Boolean> RESET_PLAYER_COMBO_COUNTER = new ActionAnimationProperty<Boolean> ("reset_combo_attack_counter", Codec.BOOL);
|
public static final ActionAnimationProperty<Boolean> RESET_PLAYER_COMBO_COUNTER = new ActionAnimationProperty<Boolean> ("reset_combo_attack_counter", Codec.BOOL);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide destination of action animation {@link MoveCoordFunctions}
|
* Provide destination of action animation {@link MoveCoordFunctions}.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category C, SKIPPED. Combat-movement hook ; the
|
||||||
|
* bondage pipeline would need a whole new destination-provider
|
||||||
|
* registry to expose this via JSON, and there is no artist use-case
|
||||||
|
* today.
|
||||||
*/
|
*/
|
||||||
public static final ActionAnimationProperty<DestLocationProvider> DEST_LOCATION_PROVIDER = new ActionAnimationProperty<DestLocationProvider> ();
|
public static final ActionAnimationProperty<DestLocationProvider> DEST_LOCATION_PROVIDER = new ActionAnimationProperty<DestLocationProvider> ();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide y rotation of entity {@link MoveCoordFunctions}
|
* Provide y rotation of entity {@link MoveCoordFunctions}.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category C, SKIPPED (combat-rotation hook).
|
||||||
*/
|
*/
|
||||||
public static final ActionAnimationProperty<YRotProvider> ENTITY_YROT_PROVIDER = new ActionAnimationProperty<YRotProvider> ();
|
public static final ActionAnimationProperty<YRotProvider> ENTITY_YROT_PROVIDER = new ActionAnimationProperty<YRotProvider> ();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provide y rotation of tracing coord {@link MoveCoordFunctions}
|
* Provide y rotation of tracing coord {@link MoveCoordFunctions}.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category C, SKIPPED (combat-rotation hook).
|
||||||
*/
|
*/
|
||||||
public static final ActionAnimationProperty<YRotProvider> DEST_COORD_YROT_PROVIDER = new ActionAnimationProperty<YRotProvider> ();
|
public static final ActionAnimationProperty<YRotProvider> DEST_COORD_YROT_PROVIDER = new ActionAnimationProperty<YRotProvider> ();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decides the index of start key frame for coord transform, See also with {@link MoveCoordFunctions#TRACE_ORIGIN_AS_DESTINATION}
|
* Decides the index of start key frame for coord transform, See also with {@link MoveCoordFunctions#TRACE_ORIGIN_AS_DESTINATION}.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category A, trivial int codec.
|
||||||
*/
|
*/
|
||||||
public static final ActionAnimationProperty<Integer> COORD_START_KEYFRAME_INDEX = new ActionAnimationProperty<Integer> ();
|
public static final ActionAnimationProperty<Integer> COORD_START_KEYFRAME_INDEX = new ActionAnimationProperty<Integer> (
|
||||||
|
"coord_start_keyframe_index",
|
||||||
|
Codec.INT
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decides the index of destination key frame for coord transform, See also with {@link MoveCoordFunctions#TRACE_ORIGIN_AS_DESTINATION}
|
* Decides the index of destination key frame for coord transform, See also with {@link MoveCoordFunctions#TRACE_ORIGIN_AS_DESTINATION}.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category A, trivial int codec.
|
||||||
*/
|
*/
|
||||||
public static final ActionAnimationProperty<Integer> COORD_DEST_KEYFRAME_INDEX = new ActionAnimationProperty<Integer> ();
|
public static final ActionAnimationProperty<Integer> COORD_DEST_KEYFRAME_INDEX = new ActionAnimationProperty<Integer> (
|
||||||
|
"coord_dest_keyframe_index",
|
||||||
|
Codec.INT
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if an entity should look where a camera is looking at the beginning of an animation (player only)
|
* Determines if an entity should look where a camera is looking at the beginning of an animation (player only)
|
||||||
@@ -343,6 +550,21 @@ public abstract class AnimationProperty<T> {
|
|||||||
public static final AttackAnimationProperty<Float> REACH = new AttackAnimationProperty<Float> ("reach", Codec.FLOAT);
|
public static final AttackAnimationProperty<Float> REACH = new AttackAnimationProperty<Float> ("reach", Codec.FLOAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combat-phase properties for attack animations.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — entire class body is Category C (combat-only, EF
|
||||||
|
* legacy). The constructor no longer registers properties in the shared
|
||||||
|
* dispatch map (see the commented {@code super(...)} call below) — this
|
||||||
|
* was deliberately neutered in a prior audit because the bondage pipeline
|
||||||
|
* never reads {@link AttackPhaseProperty} values. The four
|
||||||
|
* {@link ValueModifier} constants kept their names purely for static
|
||||||
|
* reference by surviving combat-adjacent code paths ; the sound/tag/
|
||||||
|
* location properties are forever null-codec. Do not add codecs here
|
||||||
|
* until a combat feature is reintroduced (see V3-REW-11+ in the
|
||||||
|
* roadmap) — instead, resurrect the commented {@code super} call first
|
||||||
|
* so the dispatch map picks them up consistently.
|
||||||
|
*/
|
||||||
public static class AttackPhaseProperty<T> {
|
public static class AttackPhaseProperty<T> {
|
||||||
public AttackPhaseProperty(String rl, @Nullable Codec<? extends T> codecs) {
|
public AttackPhaseProperty(String rl, @Nullable Codec<? extends T> codecs) {
|
||||||
//super(rl, codecs);
|
//super(rl, codecs);
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import com.mojang.serialization.DataResult;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.ConstantFactorSpeedModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.LinearRampSpeedModifier;
|
||||||
|
|
||||||
|
class PlaybackSpeedModifierRegistryTest {
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void forceClinit() {
|
||||||
|
assertNotNull(ConstantFactorSpeedModifier.ID);
|
||||||
|
assertNotNull(LinearRampSpeedModifier.ID);
|
||||||
|
assertNotNull(PlaybackSpeedModifierImpl.CODEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void baseImpls_registered() {
|
||||||
|
assertNotNull(PlaybackSpeedModifierRegistry.getCodec(ConstantFactorSpeedModifier.ID));
|
||||||
|
assertNotNull(PlaybackSpeedModifierRegistry.getCodec(LinearRampSpeedModifier.ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCodec_unknownType_returnsNull() {
|
||||||
|
assertNull(PlaybackSpeedModifierRegistry.getCodec(new ResourceLocation("foo", "bar")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void register_duplicateId_throws() {
|
||||||
|
assertThrows(IllegalStateException.class, () ->
|
||||||
|
PlaybackSpeedModifierRegistry.register(ConstantFactorSpeedModifier.ID, ConstantFactorSpeedModifier.CODEC)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void constantFactor_roundtrip() {
|
||||||
|
ConstantFactorSpeedModifier original = new ConstantFactorSpeedModifier(0.5F);
|
||||||
|
|
||||||
|
DataResult<JsonElement> encoded = PlaybackSpeedModifierImpl.CODEC.encodeStart(JsonOps.INSTANCE, original);
|
||||||
|
assertTrue(encoded.result().isPresent(), "encode error : " + encoded.error());
|
||||||
|
JsonObject json = encoded.result().get().getAsJsonObject();
|
||||||
|
assertEquals("tiedup:constant_factor", json.get("type").getAsString());
|
||||||
|
|
||||||
|
DataResult<PlaybackSpeedModifierImpl> decoded = PlaybackSpeedModifierImpl.CODEC.parse(JsonOps.INSTANCE, json);
|
||||||
|
assertTrue(decoded.result().isPresent());
|
||||||
|
assertEquals(original, decoded.result().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void constantFactor_halvesSpeed() {
|
||||||
|
ConstantFactorSpeedModifier mod = new ConstantFactorSpeedModifier(0.5F);
|
||||||
|
assertEquals(1.0F, mod.modify(null, null, 2.0F, 0F, 0F), 1e-5F);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void linearRamp_roundtrip() {
|
||||||
|
LinearRampSpeedModifier original = new LinearRampSpeedModifier(0.2F, 1.0F, 0.0F, 0.5F);
|
||||||
|
|
||||||
|
DataResult<JsonElement> encoded = PlaybackSpeedModifierImpl.CODEC.encodeStart(JsonOps.INSTANCE, original);
|
||||||
|
assertTrue(encoded.result().isPresent());
|
||||||
|
JsonObject json = encoded.result().get().getAsJsonObject();
|
||||||
|
|
||||||
|
DataResult<PlaybackSpeedModifierImpl> decoded = PlaybackSpeedModifierImpl.CODEC.parse(JsonOps.INSTANCE, json);
|
||||||
|
assertTrue(decoded.result().isPresent());
|
||||||
|
assertEquals(original, decoded.result().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void linearRamp_interpolatesCorrectly() {
|
||||||
|
LinearRampSpeedModifier mod = new LinearRampSpeedModifier(0.0F, 1.0F, 0.0F, 1.0F);
|
||||||
|
// At elapsedTime=0.0, factor should be 0.0 → speed stays * 0 = 0
|
||||||
|
assertEquals(0.0F, mod.modify(null, null, 10F, 0F, 0.0F), 1e-5F);
|
||||||
|
// At elapsedTime=0.5, factor should be 0.5 → speed * 0.5
|
||||||
|
assertEquals(5.0F, mod.modify(null, null, 10F, 0F, 0.5F), 1e-4F);
|
||||||
|
// At elapsedTime=1.0, factor should be 1.0 → speed * 1.0
|
||||||
|
assertEquals(10.0F, mod.modify(null, null, 10F, 0F, 1.0F), 1e-5F);
|
||||||
|
// Beyond end_time → clamp to `to`.
|
||||||
|
assertEquals(10.0F, mod.modify(null, null, 10F, 0F, 2.0F), 1e-5F);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dispatchCodec_unknownType_returnsError() {
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.add("type", new JsonPrimitive("foo:unknown"));
|
||||||
|
|
||||||
|
DataResult<PlaybackSpeedModifierImpl> decoded = PlaybackSpeedModifierImpl.CODEC.parse(JsonOps.INSTANCE, json);
|
||||||
|
assertFalse(decoded.result().isPresent());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import com.mojang.serialization.DataResult;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.LoopSectionTimeModifier;
|
||||||
|
|
||||||
|
class PlaybackTimeModifierRegistryTest {
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void forceClinit() {
|
||||||
|
assertNotNull(LoopSectionTimeModifier.ID);
|
||||||
|
assertNotNull(PlaybackTimeModifierImpl.CODEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void baseImpl_registered() {
|
||||||
|
assertNotNull(PlaybackTimeModifierRegistry.getCodec(LoopSectionTimeModifier.ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCodec_unknownType_returnsNull() {
|
||||||
|
assertNull(PlaybackTimeModifierRegistry.getCodec(new ResourceLocation("foo", "bar")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void register_duplicateId_throws() {
|
||||||
|
assertThrows(IllegalStateException.class, () ->
|
||||||
|
PlaybackTimeModifierRegistry.register(LoopSectionTimeModifier.ID, LoopSectionTimeModifier.CODEC)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loopSection_roundtrip() {
|
||||||
|
LoopSectionTimeModifier original = new LoopSectionTimeModifier(0.3F, 0.8F);
|
||||||
|
|
||||||
|
DataResult<JsonElement> encoded = PlaybackTimeModifierImpl.CODEC.encodeStart(JsonOps.INSTANCE, original);
|
||||||
|
assertTrue(encoded.result().isPresent(), "encode error : " + encoded.error());
|
||||||
|
JsonObject json = encoded.result().get().getAsJsonObject();
|
||||||
|
assertEquals("tiedup:loop_section", json.get("type").getAsString());
|
||||||
|
|
||||||
|
DataResult<PlaybackTimeModifierImpl> decoded = PlaybackTimeModifierImpl.CODEC.parse(JsonOps.INSTANCE, json);
|
||||||
|
assertTrue(decoded.result().isPresent());
|
||||||
|
assertEquals(original, decoded.result().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loopSection_passthroughWithinWindow() {
|
||||||
|
LoopSectionTimeModifier mod = new LoopSectionTimeModifier(0.3F, 0.8F);
|
||||||
|
Pair<Float, Float> result = mod.modify(null, null, 1F, 0.4F, 0.5F);
|
||||||
|
assertEquals(0.4F, result.getFirst(), 1e-5F);
|
||||||
|
assertEquals(0.5F, result.getSecond(), 1e-5F);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loopSection_wrapsOnOvershoot() {
|
||||||
|
LoopSectionTimeModifier mod = new LoopSectionTimeModifier(0.3F, 0.8F);
|
||||||
|
// elapsed=0.85 → overshoot 0.05 → wrapped=0.35
|
||||||
|
Pair<Float, Float> result = mod.modify(null, null, 1F, 0.82F, 0.85F);
|
||||||
|
assertEquals(0.35F, result.getSecond(), 1e-4F);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dispatchCodec_unknownType_returnsError() {
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.add("type", new JsonPrimitive("foo:unknown"));
|
||||||
|
|
||||||
|
DataResult<PlaybackTimeModifierImpl> decoded = PlaybackTimeModifierImpl.CODEC.parse(JsonOps.INSTANCE, json);
|
||||||
|
assertFalse(decoded.result().isPresent());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import org.joml.Quaternionf;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import com.mojang.serialization.DataResult;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.ChainedPoseModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.JointRotationOffsetModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.JointTranslationOffsetModifier;
|
||||||
|
import com.tiedup.remake.rig.armature.JointTransform;
|
||||||
|
|
||||||
|
class PoseModifierRegistryTest {
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void forceClinit() {
|
||||||
|
assertNotNull(JointRotationOffsetModifier.ID);
|
||||||
|
assertNotNull(JointTranslationOffsetModifier.ID);
|
||||||
|
assertNotNull(ChainedPoseModifier.ID);
|
||||||
|
assertNotNull(PoseModifierImpl.CODEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Registry =====
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void baseImpls_registered() {
|
||||||
|
assertNotNull(PoseModifierRegistry.getCodec(JointRotationOffsetModifier.ID));
|
||||||
|
assertNotNull(PoseModifierRegistry.getCodec(JointTranslationOffsetModifier.ID));
|
||||||
|
assertNotNull(PoseModifierRegistry.getCodec(ChainedPoseModifier.ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void types_returnsThreeEntries_atMinimum() {
|
||||||
|
assertTrue(PoseModifierRegistry.types().size() >= 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getCodec_unknownType_returnsNull() {
|
||||||
|
assertNull(PoseModifierRegistry.getCodec(new ResourceLocation("foo", "bar")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void register_duplicateId_throws() {
|
||||||
|
assertThrows(IllegalStateException.class, () ->
|
||||||
|
PoseModifierRegistry.register(JointRotationOffsetModifier.ID, JointRotationOffsetModifier.CODEC)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Dispatch roundtrip =====
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dispatchCodec_jointRotationOffset_roundtrip() {
|
||||||
|
JointRotationOffsetModifier original = new JointRotationOffsetModifier(
|
||||||
|
"upper_arm_left", 15.0F, 5.0F, 0.0F
|
||||||
|
);
|
||||||
|
|
||||||
|
DataResult<JsonElement> encoded = PoseModifierImpl.CODEC.encodeStart(JsonOps.INSTANCE, original);
|
||||||
|
assertTrue(encoded.result().isPresent(), "encode must succeed : " + encoded.error());
|
||||||
|
JsonObject json = encoded.result().get().getAsJsonObject();
|
||||||
|
assertEquals("tiedup:joint_rotation_offset", json.get("type").getAsString());
|
||||||
|
assertEquals("upper_arm_left", json.get("joint").getAsString());
|
||||||
|
|
||||||
|
DataResult<PoseModifierImpl> decoded = PoseModifierImpl.CODEC.parse(JsonOps.INSTANCE, json);
|
||||||
|
assertTrue(decoded.result().isPresent(), "decode must succeed : " + decoded.error());
|
||||||
|
assertEquals(original, decoded.result().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dispatchCodec_jointTranslationOffset_roundtrip() {
|
||||||
|
JointTranslationOffsetModifier original = new JointTranslationOffsetModifier(
|
||||||
|
"hand_right", 0.0F, -0.05F, 0.02F
|
||||||
|
);
|
||||||
|
|
||||||
|
DataResult<JsonElement> encoded = PoseModifierImpl.CODEC.encodeStart(JsonOps.INSTANCE, original);
|
||||||
|
assertTrue(encoded.result().isPresent());
|
||||||
|
JsonObject json = encoded.result().get().getAsJsonObject();
|
||||||
|
assertEquals("tiedup:joint_translation_offset", json.get("type").getAsString());
|
||||||
|
|
||||||
|
DataResult<PoseModifierImpl> decoded = PoseModifierImpl.CODEC.parse(JsonOps.INSTANCE, json);
|
||||||
|
assertEquals(original, decoded.result().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dispatchCodec_unknownType_returnsError() {
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.add("type", new JsonPrimitive("foo:unknown_mod"));
|
||||||
|
|
||||||
|
DataResult<PoseModifierImpl> decoded = PoseModifierImpl.CODEC.parse(JsonOps.INSTANCE, json);
|
||||||
|
assertFalse(decoded.result().isPresent());
|
||||||
|
assertTrue(decoded.error().isPresent());
|
||||||
|
assertTrue(decoded.error().get().message().contains("foo:unknown_mod"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Functional behaviour =====
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jointRotationOffset_missingJoint_isNoOp() {
|
||||||
|
Pose pose = new Pose();
|
||||||
|
JointRotationOffsetModifier mod = new JointRotationOffsetModifier("nope", 45F, 0F, 0F);
|
||||||
|
mod.modify(null, pose, null, 0F, 0F);
|
||||||
|
// No throw ; pose stays empty.
|
||||||
|
assertTrue(pose.getJointTransformData().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jointRotationOffset_rotatesJoint() {
|
||||||
|
Pose pose = new Pose();
|
||||||
|
JointTransform jt = JointTransform.empty();
|
||||||
|
pose.putJointData("head", jt);
|
||||||
|
Quaternionf before = new Quaternionf(jt.rotation());
|
||||||
|
|
||||||
|
JointRotationOffsetModifier mod = new JointRotationOffsetModifier("head", 0F, 90F, 0F);
|
||||||
|
mod.modify(null, pose, null, 0F, 0F);
|
||||||
|
|
||||||
|
// Y component of the quaternion should have changed (90° yaw).
|
||||||
|
assertFalse(pose.get("head").rotation().equals(before, 1e-4F));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jointTranslationOffset_appliesDelta() {
|
||||||
|
Pose pose = new Pose();
|
||||||
|
JointTransform jt = JointTransform.empty();
|
||||||
|
pose.putJointData("hand_right", jt);
|
||||||
|
|
||||||
|
JointTranslationOffsetModifier mod = new JointTranslationOffsetModifier("hand_right", 0.1F, 0.2F, 0.3F);
|
||||||
|
mod.modify(null, pose, null, 0F, 0F);
|
||||||
|
|
||||||
|
assertEquals(0.1F, pose.get("hand_right").translation().x, 1e-5F);
|
||||||
|
assertEquals(0.2F, pose.get("hand_right").translation().y, 1e-5F);
|
||||||
|
assertEquals(0.3F, pose.get("hand_right").translation().z, 1e-5F);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void chained_appliesAllModifiersInOrder() {
|
||||||
|
Pose pose = new Pose();
|
||||||
|
JointTransform jt = JointTransform.empty();
|
||||||
|
pose.putJointData("hand_right", jt);
|
||||||
|
|
||||||
|
ChainedPoseModifier chain = new ChainedPoseModifier(java.util.List.of(
|
||||||
|
new JointTranslationOffsetModifier("hand_right", 0.1F, 0.0F, 0.0F),
|
||||||
|
new JointTranslationOffsetModifier("hand_right", 0.0F, 0.2F, 0.0F)
|
||||||
|
));
|
||||||
|
chain.modify(null, pose, null, 0F, 0F);
|
||||||
|
|
||||||
|
assertEquals(0.1F, pose.get("hand_right").translation().x, 1e-5F);
|
||||||
|
assertEquals(0.2F, pose.get("hand_right").translation().y, 1e-5F);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void chained_roundtrip() {
|
||||||
|
ChainedPoseModifier original = new ChainedPoseModifier(java.util.List.of(
|
||||||
|
new JointRotationOffsetModifier("head", 10F, 0F, 0F),
|
||||||
|
new JointTranslationOffsetModifier("hand_right", 0F, 0.1F, 0F)
|
||||||
|
));
|
||||||
|
|
||||||
|
DataResult<JsonElement> encoded = PoseModifierImpl.CODEC.encodeStart(JsonOps.INSTANCE, original);
|
||||||
|
assertTrue(encoded.result().isPresent(), "encode error : " + encoded.error());
|
||||||
|
JsonElement json = encoded.result().get();
|
||||||
|
|
||||||
|
DataResult<PoseModifierImpl> decoded = PoseModifierImpl.CODEC.parse(JsonOps.INSTANCE, json);
|
||||||
|
assertTrue(decoded.result().isPresent(), "decode error : " + decoded.error());
|
||||||
|
assertTrue(decoded.result().get() instanceof ChainedPoseModifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.property;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import com.mojang.serialization.DataResult;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.LivingMotion;
|
||||||
|
import com.tiedup.remake.rig.anim.LivingMotions;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PlaybackSpeedModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PlaybackTimeModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PoseModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.ConstantFactorSpeedModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.LinearRampSpeedModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.LoopSectionTimeModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.ActionAnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackSpeedModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackTimeModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.PoseModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.StaticAnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.util.TimePairList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 3 D1 — codec roundtrip and registration tests for the newly-serialized
|
||||||
|
* animation properties (Categories A & B). Pure Java — no MC bootstrap.
|
||||||
|
*
|
||||||
|
* <p>Covers :
|
||||||
|
* <ul>
|
||||||
|
* <li>Category A trivial codecs : {@code fixed_head_rotation},
|
||||||
|
* {@code reset_living_motion}, {@code no_gravity_time}, {@code move_time},
|
||||||
|
* {@code coord_update_time}, {@code coord_start_keyframe_index},
|
||||||
|
* {@code coord_dest_keyframe_index}</li>
|
||||||
|
* <li>Category B dispatch codecs : {@code pose_modifier},
|
||||||
|
* {@code play_speed_modifier}, {@code elapsed_time_modifier}</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
class AnimationPropertyCodecTest {
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void forceClinit() {
|
||||||
|
// Touch each property so its static field is initialized before any
|
||||||
|
// getSerializableProperty(...) lookup runs.
|
||||||
|
assertNotNull(StaticAnimationProperty.FIXED_HEAD_ROTATION);
|
||||||
|
assertNotNull(StaticAnimationProperty.RESET_LIVING_MOTION);
|
||||||
|
assertNotNull(StaticAnimationProperty.PLAY_SPEED_MODIFIER);
|
||||||
|
assertNotNull(StaticAnimationProperty.ELAPSED_TIME_MODIFIER);
|
||||||
|
assertNotNull(StaticAnimationProperty.POSE_MODIFIER);
|
||||||
|
assertNotNull(ActionAnimationProperty.NO_GRAVITY_TIME);
|
||||||
|
assertNotNull(ActionAnimationProperty.MOVE_TIME);
|
||||||
|
assertNotNull(ActionAnimationProperty.COORD_UPDATE_TIME);
|
||||||
|
assertNotNull(ActionAnimationProperty.COORD_START_KEYFRAME_INDEX);
|
||||||
|
assertNotNull(ActionAnimationProperty.COORD_DEST_KEYFRAME_INDEX);
|
||||||
|
// Bootstrap the modifier registries.
|
||||||
|
assertNotNull(PoseModifierImpl.CODEC);
|
||||||
|
assertNotNull(PlaybackSpeedModifierImpl.CODEC);
|
||||||
|
assertNotNull(PlaybackTimeModifierImpl.CODEC);
|
||||||
|
// Also force LivingMotions clinit so the enum manager knows IDLE etc.
|
||||||
|
assertNotNull(LivingMotions.IDLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Category A : trivial codecs =====
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fixedHeadRotation_registeredAndParses() {
|
||||||
|
AnimationProperty<?> prop = AnimationProperty.getSerializableProperty("fixed_head_rotation");
|
||||||
|
assertEquals(StaticAnimationProperty.FIXED_HEAD_ROTATION, prop);
|
||||||
|
|
||||||
|
Boolean value = StaticAnimationProperty.FIXED_HEAD_ROTATION.parseFrom(new JsonPrimitive(true));
|
||||||
|
assertTrue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resetLivingMotion_registeredAndParses() {
|
||||||
|
AnimationProperty<?> prop = AnimationProperty.getSerializableProperty("reset_living_motion");
|
||||||
|
assertEquals(StaticAnimationProperty.RESET_LIVING_MOTION, prop);
|
||||||
|
|
||||||
|
LivingMotion motion = StaticAnimationProperty.RESET_LIVING_MOTION.parseFrom(new JsonPrimitive("idle"));
|
||||||
|
assertEquals(LivingMotions.IDLE, motion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resetLivingMotion_unknownName_throws() {
|
||||||
|
// parseFrom throws on empty orElseThrow (result is empty because of the error)
|
||||||
|
assertThrows(Exception.class, () ->
|
||||||
|
StaticAnimationProperty.RESET_LIVING_MOTION.parseFrom(new JsonPrimitive("totally_not_a_motion"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noGravityTime_registeredAndParses() {
|
||||||
|
AnimationProperty<?> prop = AnimationProperty.getSerializableProperty("no_gravity_time");
|
||||||
|
assertEquals(ActionAnimationProperty.NO_GRAVITY_TIME, prop);
|
||||||
|
|
||||||
|
JsonArray arr = JsonParser.parseString("[0.1, 0.4, 0.6, 0.9]").getAsJsonArray();
|
||||||
|
TimePairList tpl = ActionAnimationProperty.NO_GRAVITY_TIME.parseFrom(arr);
|
||||||
|
assertNotNull(tpl);
|
||||||
|
// (0.1..0.4) is a no-gravity window ; 0.3 is inside, 0.5 is outside.
|
||||||
|
assertTrue(tpl.isTimeInPairs(0.3F));
|
||||||
|
assertFalse(tpl.isTimeInPairs(0.5F));
|
||||||
|
assertTrue(tpl.isTimeInPairs(0.7F));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void noGravityTime_oddLength_codecError() {
|
||||||
|
JsonArray arr = JsonParser.parseString("[0.1, 0.4, 0.6]").getAsJsonArray();
|
||||||
|
// parseFrom calls orElseThrow on an error → any exception is fine.
|
||||||
|
assertThrows(Exception.class, () ->
|
||||||
|
ActionAnimationProperty.NO_GRAVITY_TIME.parseFrom(arr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void moveTime_registered() {
|
||||||
|
AnimationProperty<?> prop = AnimationProperty.getSerializableProperty("move_time");
|
||||||
|
assertEquals(ActionAnimationProperty.MOVE_TIME, prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void coordUpdateTime_registered() {
|
||||||
|
AnimationProperty<?> prop = AnimationProperty.getSerializableProperty("coord_update_time");
|
||||||
|
assertEquals(ActionAnimationProperty.COORD_UPDATE_TIME, prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void coordStartKeyframeIndex_registeredAndParses() {
|
||||||
|
AnimationProperty<?> prop = AnimationProperty.getSerializableProperty("coord_start_keyframe_index");
|
||||||
|
assertEquals(ActionAnimationProperty.COORD_START_KEYFRAME_INDEX, prop);
|
||||||
|
|
||||||
|
Integer value = ActionAnimationProperty.COORD_START_KEYFRAME_INDEX.parseFrom(new JsonPrimitive(3));
|
||||||
|
assertEquals(3, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void coordDestKeyframeIndex_registeredAndParses() {
|
||||||
|
AnimationProperty<?> prop = AnimationProperty.getSerializableProperty("coord_dest_keyframe_index");
|
||||||
|
assertEquals(ActionAnimationProperty.COORD_DEST_KEYFRAME_INDEX, prop);
|
||||||
|
|
||||||
|
Integer value = ActionAnimationProperty.COORD_DEST_KEYFRAME_INDEX.parseFrom(new JsonPrimitive(7));
|
||||||
|
assertEquals(7, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Category B : dispatch codecs =====
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void poseModifier_registeredAndParses() {
|
||||||
|
AnimationProperty<?> prop = AnimationProperty.getSerializableProperty("pose_modifier");
|
||||||
|
assertEquals(StaticAnimationProperty.POSE_MODIFIER, prop);
|
||||||
|
|
||||||
|
String jsonText = """
|
||||||
|
{ "type": "tiedup:joint_rotation_offset", "joint": "upper_arm_left", "pitch": 15.0 }
|
||||||
|
""";
|
||||||
|
JsonElement json = JsonParser.parseString(jsonText);
|
||||||
|
PoseModifier mod = StaticAnimationProperty.POSE_MODIFIER.parseFrom(json);
|
||||||
|
assertNotNull(mod);
|
||||||
|
assertTrue(mod instanceof PoseModifierImpl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void poseModifier_unknownType_throws() {
|
||||||
|
String jsonText = """
|
||||||
|
{ "type": "foo:unknown_mod", "joint": "head" }
|
||||||
|
""";
|
||||||
|
JsonElement json = JsonParser.parseString(jsonText);
|
||||||
|
assertThrows(Exception.class, () ->
|
||||||
|
StaticAnimationProperty.POSE_MODIFIER.parseFrom(json)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void playSpeedModifier_registeredAndParses() {
|
||||||
|
AnimationProperty<?> prop = AnimationProperty.getSerializableProperty("play_speed_modifier");
|
||||||
|
assertEquals(StaticAnimationProperty.PLAY_SPEED_MODIFIER, prop);
|
||||||
|
|
||||||
|
String jsonText = """
|
||||||
|
{ "type": "tiedup:constant_factor", "factor": 0.5 }
|
||||||
|
""";
|
||||||
|
JsonElement json = JsonParser.parseString(jsonText);
|
||||||
|
PlaybackSpeedModifier mod = StaticAnimationProperty.PLAY_SPEED_MODIFIER.parseFrom(json);
|
||||||
|
assertNotNull(mod);
|
||||||
|
// Functional check : given speed=2.0, modifier should return 1.0.
|
||||||
|
assertEquals(1.0F, mod.modify(null, null, 2.0F, 0F, 0F), 1e-5F);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void elapsedTimeModifier_registeredAndParses() {
|
||||||
|
AnimationProperty<?> prop = AnimationProperty.getSerializableProperty("elapsed_time_modifier");
|
||||||
|
assertEquals(StaticAnimationProperty.ELAPSED_TIME_MODIFIER, prop);
|
||||||
|
|
||||||
|
String jsonText = """
|
||||||
|
{ "type": "tiedup:loop_section", "loop_start": 0.3, "loop_end": 0.8 }
|
||||||
|
""";
|
||||||
|
JsonElement json = JsonParser.parseString(jsonText);
|
||||||
|
PlaybackTimeModifier mod = StaticAnimationProperty.ELAPSED_TIME_MODIFIER.parseFrom(json);
|
||||||
|
assertNotNull(mod);
|
||||||
|
// Functional check : elapsed=0.9 should wrap back into [0.3..0.8).
|
||||||
|
Pair<Float, Float> wrapped = mod.modify(null, null, 1.0F, 0.85F, 0.9F);
|
||||||
|
assertTrue(wrapped.getSecond() >= 0.3F && wrapped.getSecond() < 0.8F,
|
||||||
|
"wrapped elapsed should be in [0.3..0.8), got " + wrapped.getSecond());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Registry summary =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration-ish : after clinit of all the touched properties, the
|
||||||
|
* dispatch map must contain all 10 new names + the 3 existing D2 names
|
||||||
|
* + the 3 existing Wave A client-properties names.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void allNewPropertyNames_registered() {
|
||||||
|
String[] names = {
|
||||||
|
"fixed_head_rotation",
|
||||||
|
"reset_living_motion",
|
||||||
|
"no_gravity_time",
|
||||||
|
"move_time",
|
||||||
|
"coord_update_time",
|
||||||
|
"coord_start_keyframe_index",
|
||||||
|
"coord_dest_keyframe_index",
|
||||||
|
"pose_modifier",
|
||||||
|
"play_speed_modifier",
|
||||||
|
"elapsed_time_modifier"
|
||||||
|
};
|
||||||
|
for (String name : names) {
|
||||||
|
assertNotNull(
|
||||||
|
AnimationProperty.getSerializableProperty(name),
|
||||||
|
"Property '" + name + "' must be registered"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user