Revert Option B — IDLE bindings are first-class
User decision 2026-04-24 : IDLE bindings must be supported because modders need to define idle poses for their items (armbinder can't work without arms-behind-back idle pose). TiedUp will also ship default IDLE bindings for its core items once assets are authored. Technical : EF ClientAnimator has 2 separate maps (livingAnimations for BASE layer + compositeLivingAnimations for COMPOSITE layers). Both support IDLE bindings without race — last-put-wins for base override, or automatic composition via JointMask for partial overlay. The previous WARN was a reviewer over-correction based on misread of the 2-map structure. - Removed WARN parse-time in DataDrivenItemParser.parseAnimationBindings - Updated test : IDLE binding is accepted silently (no warn) - Cleaned 'Option B' mentions in javadoc comments - Design doc (gitignored) updated with 2-path explanation.
This commit is contained in:
@@ -90,9 +90,12 @@ public abstract class PlayerPatch<T extends Player> extends LivingEntityPatch<T>
|
||||
* <li>walk seul → {@link LivingMotions#WALK}</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>Défaut (idle) → {@link LivingMotions#IDLE} vanilla — les items
|
||||
* bondage bindent leurs poses spécifiques ailleurs (Option B du
|
||||
* design doc, §5.1 BONDAGE_ANIMATION_DESIGN.md).</li>
|
||||
* <li>Défaut (idle) → {@link LivingMotions#IDLE} vanilla. Les items
|
||||
* bondage peuvent binder IDLE via deux paths :
|
||||
* BASE layer override (écrase vanilla IDLE dans {@code livingAnimations})
|
||||
* ou COMPOSITE overlay avec JointMask (vanilla IDLE joue sur le corps,
|
||||
* overlay masque seulement certains joints). Voir §5.1
|
||||
* BONDAGE_ANIMATION_DESIGN.md.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p><b>Pattern EF</b> : {@code EntityState.inaction()} gate la transition
|
||||
|
||||
@@ -717,24 +717,16 @@ public final class DataDrivenItemParser {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// P3 Wave α — Option B convention warn : binding IDLE fonctionne
|
||||
// techniquement mais écrase silencieusement le default EF re-injecté
|
||||
// par ClientAnimator.resetLivingAnimations() post-rebuild. Design
|
||||
// BONDAGE_ANIMATION_DESIGN.md §5.1 recommande d'utiliser des motions
|
||||
// custom (POSE_KNEEL_BOUND / STRUGGLE_BOUND / WALK_BOUND) plutôt que
|
||||
// d'écraser IDLE. On accepte le binding quand même (rétrocompat +
|
||||
// cas légitimes rares), juste on pointe le modder vers la convention.
|
||||
if (motion == LivingMotions.IDLE) {
|
||||
LOGGER.warn(
|
||||
"[DataDrivenItemParser] Item {} binds IDLE motion. Convention design Option B "
|
||||
+ "(BONDAGE_ANIMATION_DESIGN.md §5.1) recommends not binding IDLE — "
|
||||
+ "ClientAnimator.resetLivingAnimations() restores default EF IDLE after "
|
||||
+ "rebuild, so binding IDLE overwrites the default silently. Prefer custom "
|
||||
+ "motions like POSE_KNEEL_BOUND / STRUGGLE_BOUND / WALK_BOUND. The binding "
|
||||
+ "will still work but may cause surprises.",
|
||||
itemId
|
||||
);
|
||||
}
|
||||
// IDLE bindings are first-class citizens (user decision 2026-04-24).
|
||||
// Two paths supported natively by EF's 2-map ClientAnimator structure :
|
||||
// - BASE layer override : anim LayerType=BASE_LAYER → written into
|
||||
// livingAnimations, overrides vanilla IDLE after reset (last-put-wins).
|
||||
// Use case : armbinder forcing "arms behind back" full-body pose.
|
||||
// - COMPOSITE overlay : anim LayerType=COMPOSITE_LAYER + JointMask →
|
||||
// written into compositeLivingAnimations (separate map). Vanilla IDLE
|
||||
// still plays on base body, overlay masks specified joints only.
|
||||
// Use case : cuffs posing only the arms while rest stays vanilla.
|
||||
// No WARN — the parser accepts IDLE bindings without any log noise.
|
||||
ResourceLocation animId = tryParseAnimationRL(
|
||||
entry.getValue(),
|
||||
"living_motions['" + motionName + "']",
|
||||
|
||||
@@ -85,11 +85,24 @@ import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
|
||||
*
|
||||
* <h2>Préconditions</h2>
|
||||
* <ul>
|
||||
* <li><b>Option B modder convention</b> : les items bondage JSON NE bindent
|
||||
* PAS {@link com.tiedup.remake.rig.anim.LivingMotions#IDLE}. Le default
|
||||
* EF IDLE re-injecté par {@code resetLivingAnimations} reste donc
|
||||
* visible. Laisser un item binder IDLE est toléré (l'anim custom
|
||||
* écrase) mais c'est un code smell — voir docs plan P3.</li>
|
||||
* <li><b>IDLE bindings are first-class</b> (user decision 2026-04-24) : les
|
||||
* items bondage PEUVENT binder
|
||||
* {@link com.tiedup.remake.rig.anim.LivingMotions#IDLE}. EF supporte
|
||||
* nativement deux paths via sa structure 2-map :
|
||||
* <ul>
|
||||
* <li><b>BASE layer override</b> — anim {@code LayerType=BASE_LAYER}
|
||||
* écrit dans {@code livingAnimations}, écrase la vanilla IDLE
|
||||
* re-injectée par {@code resetLivingAnimations} (last-put-wins).
|
||||
* Usage : armbinder forçant pose "bras derrière dos" totale.</li>
|
||||
* <li><b>COMPOSITE overlay</b> — anim {@code LayerType=COMPOSITE_LAYER}
|
||||
* + {@code JointMask} écrit dans {@code compositeLivingAnimations}
|
||||
* (map séparée). Vanilla IDLE joue sur le corps, overlay écrase
|
||||
* seulement les joints masqués. Usage : cuffs bras seulement.</li>
|
||||
* </ul>
|
||||
* Aucune race condition — EF compose les 2 maps via
|
||||
* {@code ClientAnimator.getPose}. TiedUp fournira des IDLE defaults
|
||||
* pour ses items core (armbinder, collar, cuffs, shackles) une fois
|
||||
* les assets Blender authorés.</li>
|
||||
* <li>Appelé depuis le <b>client main thread</b> uniquement. L'animator
|
||||
* n'est pas thread-safe.</li>
|
||||
* <li>Le package {@code v2.client} est strictement client-only malgré
|
||||
|
||||
@@ -169,22 +169,24 @@ class DataDrivenItemParserAnimationsTest {
|
||||
assertNotNull(result.livingMotions().get(TiedUpLivingMotions.STRUGGLE_BOUND));
|
||||
}
|
||||
|
||||
// ========== P3 Wave α : Option B convention warn on IDLE ==========
|
||||
// ========== IDLE bindings are first-class (user decision 2026-04-24) ==========
|
||||
|
||||
/**
|
||||
* Option B convention (design BONDAGE_ANIMATION_DESIGN.md §5.1) : les modders
|
||||
* ne devraient PAS binder {@link LivingMotions#IDLE} car
|
||||
* {@code ClientAnimator.resetLivingAnimations()} re-injecte le default EF
|
||||
* IDLE post-rebuild, et le binding custom l'écrase silencieusement — comportement
|
||||
* fonctionnel mais non-idiomatique. Le parser log un WARN explicite pour pointer
|
||||
* le modder vers la convention, mais accepte le binding quand même (rétrocompat).
|
||||
*
|
||||
* <p>Ce test verrouille le comportement "accepte + ne throw pas" — la présence
|
||||
* du WARN log n'est pas directement vérifiable sans log capture, mais le
|
||||
* binding doit être bel et bien présent dans le résultat.</p>
|
||||
* IDLE bindings sont supportés sans warning (user decision 2026-04-24). EF a deux
|
||||
* maps séparées dans {@code ClientAnimator} :
|
||||
* <ul>
|
||||
* <li>{@code livingAnimations} (BASE layer) — un binding IDLE
|
||||
* {@code LayerType=BASE_LAYER} écrase le default EF injecté par
|
||||
* {@code resetLivingAnimations()} via last-put-wins.</li>
|
||||
* <li>{@code compositeLivingAnimations} (COMPOSITE layers, JointMask) — un
|
||||
* binding IDLE {@code LayerType=COMPOSITE_LAYER} va dans cette map séparée,
|
||||
* composition automatique avec vanilla IDLE sur les joints non-masqués.</li>
|
||||
* </ul>
|
||||
* Les 2 paths fonctionnent nativement. Le parser accepte le binding IDLE
|
||||
* sans log warn.
|
||||
*/
|
||||
@Test
|
||||
void parseAnimations_IDLEBinding_logsWarnButAccepts() {
|
||||
void parseAnimations_IDLEBinding_isAcceptedSilently() {
|
||||
String jsonStr = """
|
||||
{
|
||||
"animations": {
|
||||
@@ -199,13 +201,52 @@ class DataDrivenItemParserAnimationsTest {
|
||||
DataDrivenItemParser.parseAnimationBindings(json(jsonStr), ITEM_ID);
|
||||
|
||||
assertNotNull(result,
|
||||
"IDLE binding est tolere (non-idiomatique mais fonctionnel) => result non-null");
|
||||
"IDLE binding est accepte (first-class) => result non-null");
|
||||
assertEquals(1, result.livingMotions().size(),
|
||||
"Le binding IDLE est accepte apres le WARN de convention");
|
||||
"Le binding IDLE est present dans la map livingMotions");
|
||||
assertEquals(
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "arms_cuffed_idle"),
|
||||
result.livingMotions().get(LivingMotions.IDLE),
|
||||
"Le binding IDLE est bien present dans le resultat malgre le WARN"
|
||||
"Le binding IDLE resout vers la ResourceLocation attendue"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanity check : un binding IDLE aux côtés d'autres motions (WALK, SNEAK,
|
||||
* STRUGGLE_BOUND) est parsé sans différence — IDLE est une motion comme les
|
||||
* autres du point de vue du parser. Aucun traitement spécial.
|
||||
*/
|
||||
@Test
|
||||
void parseAnimations_IDLEBinding_coexistsWithOtherMotions() {
|
||||
String jsonStr = """
|
||||
{
|
||||
"animations": {
|
||||
"living_motions": {
|
||||
"IDLE": "tiedup:arms_cuffed_idle",
|
||||
"WALK": "tiedup:arms_cuffed_walk",
|
||||
"SNEAK": "tiedup:arms_cuffed_sneak",
|
||||
"STRUGGLE_BOUND": "tiedup:arms_cuffed_struggle"
|
||||
},
|
||||
"on_equip": "tiedup:cuffs_equip_oneshot"
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
AnimationBindings result =
|
||||
DataDrivenItemParser.parseAnimationBindings(json(jsonStr), ITEM_ID);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(4, result.livingMotions().size(),
|
||||
"4 motions parsees (IDLE + WALK + SNEAK + STRUGGLE_BOUND)");
|
||||
assertTrue(result.livingMotions().containsKey(LivingMotions.IDLE),
|
||||
"Le binding IDLE est present");
|
||||
assertEquals(
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "arms_cuffed_idle"),
|
||||
result.livingMotions().get(LivingMotions.IDLE)
|
||||
);
|
||||
assertEquals(
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "cuffs_equip_oneshot"),
|
||||
result.onEquip()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user