P3-11 : add PacketPlayRigAnim + ModNetwork registration (handler stub)

Record packet S->C pour animations cinematic one-shot (capture NPC, hit
stun, death). Carries entityId, animId (ResourceLocation), transitionTime
(float), et priorityOrdinal (byte = Layer.Priority.ordinal()).

Encode via writeVarInt/writeResourceLocation/writeFloat/writeByte ; decode
symétrique. Factory PacketPlayRigAnim.of() pour build lisible avec
Layer.Priority, et getter priority() avec fallback LOWEST si ordinal
hors-range (masking & 0xFF pour gérer byte négatif).

Handler client = stub qui LOGGER.debug + setPacketHandled. Le corps
complet (resolve entity, lookup LivingEntityPatch, animator.playAnimation)
viendra en P3-12 avec la méthode serveur playAnimationSync().

Registration ajoutée en fin de ModNetwork.register() — ID sequential 75
(76 packets total).

7 tests unitaires PacketPlayRigAnimTest : roundtrip nominal, entityId
boundary (0/MAX/MIN), transitionTime zero+negative (pas de validation
silencieuse), loop sur les 5 Layer.Priority, fallback LOWEST sur ordinal
malformé (99, -1, PRIORITY_COUNT), ResourceLocation avec underscores et
slashes, et sanity check equals.

Rig tests : 65 -> 72 GREEN. Full suite : 162 GREEN, 0 failure.
This commit is contained in:
notevil
2026-04-23 15:11:08 +02:00
parent 5d108f51b4
commit 2a4ec170ef
3 changed files with 242 additions and 0 deletions

View File

@@ -64,6 +64,7 @@ import com.tiedup.remake.network.sync.PacketSyncPetBedState;
import com.tiedup.remake.network.sync.PacketSyncStruggleState; import com.tiedup.remake.network.sync.PacketSyncStruggleState;
import com.tiedup.remake.network.trader.PacketBuyCaptive; import com.tiedup.remake.network.trader.PacketBuyCaptive;
import com.tiedup.remake.network.trader.PacketOpenTraderScreen; import com.tiedup.remake.network.trader.PacketOpenTraderScreen;
import com.tiedup.remake.rig.network.PacketPlayRigAnim;
import com.tiedup.remake.v2.bondage.network.PacketSyncV2Equipment; import com.tiedup.remake.v2.bondage.network.PacketSyncV2Equipment;
import com.tiedup.remake.v2.bondage.network.PacketV2LockToggle; import com.tiedup.remake.v2.bondage.network.PacketV2LockToggle;
import com.tiedup.remake.v2.bondage.network.PacketV2SelfEquip; import com.tiedup.remake.v2.bondage.network.PacketV2SelfEquip;
@@ -592,6 +593,14 @@ public class ModNetwork {
PacketSyncMovementStyle::handle PacketSyncMovementStyle::handle
); );
// RIG animation system (S2C cinematic one-shot)
reg(
PacketPlayRigAnim.class,
PacketPlayRigAnim::encode,
PacketPlayRigAnim::decode,
PacketPlayRigAnim::handleOnClient
);
TiedUpMod.LOGGER.info("Registered {} network packets", packetId); TiedUpMod.LOGGER.info("Registered {} network packets", packetId);
} }

View File

@@ -0,0 +1,85 @@
/*
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
*/
package com.tiedup.remake.rig.network;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.network.NetworkEvent;
import com.tiedup.remake.rig.TiedUpRigConstants;
import com.tiedup.remake.rig.anim.client.Layer;
/**
* Packet S→C : déclenche une animation one-shot sur une entité distante.
*
* <p>Utilisé pour les animations cinematic serveur-authoritatives (NPC capture
* grab, hit stun, death). Le client résout l'entité par ID, récupère son
* {@code LivingEntityPatch} via {@code TiedUpCapabilities}, et appelle
* {@code animator.playAnimation(anim, transitionTime)} avec la priorité donnée.
*
* <p>Le server-side dispatch (méthode {@code LivingEntityPatch.playAnimationSync})
* sera implémenté en P3-12. Ici on pose juste l'infra packet + handler stub
* qui log et termine.
*
* @param entityId ID runtime de l'entité ciblée (Entity.getId())
* @param animId ResourceLocation de l'animation à jouer
* @param transitionTime durée de blend en secondes (typiquement 0.15F = 3 ticks)
* @param priorityOrdinal ordinal de Layer.Priority (LOWEST=0...HIGHEST=4),
* transmis sous forme byte pour compacité
*/
public record PacketPlayRigAnim(
int entityId,
ResourceLocation animId,
float transitionTime,
byte priorityOrdinal
) {
public static final int PRIORITY_COUNT = Layer.Priority.values().length;
/** Factory avec Layer.Priority (plus expressif qu'un byte direct). */
public static PacketPlayRigAnim of(int entityId, ResourceLocation animId, float transitionTime, Layer.Priority priority) {
return new PacketPlayRigAnim(entityId, animId, transitionTime, (byte) priority.ordinal());
}
public Layer.Priority priority() {
// Guard out-of-range si un peer malveillant envoie un byte invalide
int idx = priorityOrdinal & 0xFF;
if (idx < 0 || idx >= PRIORITY_COUNT) {
return Layer.Priority.LOWEST; // fallback safe
}
return Layer.Priority.values()[idx];
}
public void encode(FriendlyByteBuf buf) {
buf.writeVarInt(this.entityId);
buf.writeResourceLocation(this.animId);
buf.writeFloat(this.transitionTime);
buf.writeByte(this.priorityOrdinal);
}
public static PacketPlayRigAnim decode(FriendlyByteBuf buf) {
int entityId = buf.readVarInt();
ResourceLocation animId = buf.readResourceLocation();
float transitionTime = buf.readFloat();
byte priorityOrdinal = buf.readByte();
return new PacketPlayRigAnim(entityId, animId, transitionTime, priorityOrdinal);
}
/**
* Handler client — stub P3-11. Le corps complet (resolve entity, lookup
* patch, playAnimation) viendra en P3-12.
*/
public static void handleOnClient(PacketPlayRigAnim pkt, Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> {
TiedUpRigConstants.LOGGER.debug(
"[PacketPlayRigAnim] received (stub P3-11): entityId={}, animId={}, transition={}s, priority={}",
pkt.entityId, pkt.animId, pkt.transitionTime, pkt.priority()
);
// TODO P3-12 : resolve entity + patch + animator.playAnimation
});
ctx.get().setPacketHandled(true);
}
}

View File

@@ -0,0 +1,148 @@
/*
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
*/
package com.tiedup.remake.rig.network;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import io.netty.buffer.Unpooled;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import org.junit.jupiter.api.Test;
import com.tiedup.remake.rig.anim.client.Layer;
/**
* Tests de {@link PacketPlayRigAnim} : encode/decode roundtrip + priority
* fallback. Aucune dépendance MC bootstrap — on instancie juste
* {@code FriendlyByteBuf(Unpooled.buffer())} et on vérifie que les bytes
* écrits roundtrip correctement.
*/
class PacketPlayRigAnimTest {
private static FriendlyByteBuf newBuf() {
return new FriendlyByteBuf(Unpooled.buffer());
}
/** Roundtrip nominal : packet construit avec valeurs usuelles. */
@Test
void encode_decode_roundtrip_nominal() {
ResourceLocation anim = ResourceLocation.fromNamespaceAndPath("tiedup", "context_stand_idle");
PacketPlayRigAnim original = PacketPlayRigAnim.of(42, anim, 0.15F, Layer.Priority.MIDDLE);
FriendlyByteBuf buf = newBuf();
original.encode(buf);
PacketPlayRigAnim decoded = PacketPlayRigAnim.decode(buf);
assertEquals(original.entityId(), decoded.entityId(), "entityId roundtrip");
assertEquals(original.animId(), decoded.animId(), "animId roundtrip");
assertEquals(original.transitionTime(), decoded.transitionTime(), 0.0F, "transitionTime roundtrip");
assertEquals(original.priorityOrdinal(), decoded.priorityOrdinal(), "priorityOrdinal roundtrip");
assertEquals(original, decoded, "record equals()");
assertEquals(0, buf.readableBytes(), "buffer must be fully consumed");
}
/** Boundary entity IDs : 0, Integer.MAX_VALUE, negatif. */
@Test
void encode_decode_entityId_boundary() {
ResourceLocation anim = ResourceLocation.fromNamespaceAndPath("tiedup", "test");
for (int entityId : new int[]{0, 1, Integer.MAX_VALUE, -1, Integer.MIN_VALUE}) {
PacketPlayRigAnim original = PacketPlayRigAnim.of(entityId, anim, 0.1F, Layer.Priority.LOW);
FriendlyByteBuf buf = newBuf();
original.encode(buf);
PacketPlayRigAnim decoded = PacketPlayRigAnim.decode(buf);
assertEquals(entityId, decoded.entityId(),
"entityId " + entityId + " must roundtrip via writeVarInt/readVarInt");
}
}
/**
* transitionTime=0.0F roundtrip (no blend = cut) + negative (-1.0F) roundtrip.
* Le packet ne doit PAS valider silencieusement — roundtrip brut.
*/
@Test
void encode_decode_transitionTime_zero_and_negative() {
ResourceLocation anim = ResourceLocation.fromNamespaceAndPath("tiedup", "test");
for (float t : new float[]{0.0F, -1.0F, 99.9F, Float.MIN_VALUE, Float.MAX_VALUE}) {
PacketPlayRigAnim original = PacketPlayRigAnim.of(7, anim, t, Layer.Priority.LOWEST);
FriendlyByteBuf buf = newBuf();
original.encode(buf);
PacketPlayRigAnim decoded = PacketPlayRigAnim.decode(buf);
assertEquals(t, decoded.transitionTime(), 0.0F,
"transitionTime " + t + " must roundtrip verbatim (no silent validation)");
}
}
/** Loop sur les 5 Layer.Priority : of() + roundtrip, priority() retourne pareil. */
@Test
void encode_decode_allPriorities() {
ResourceLocation anim = ResourceLocation.fromNamespaceAndPath("tiedup", "test");
for (Layer.Priority p : Layer.Priority.values()) {
PacketPlayRigAnim original = PacketPlayRigAnim.of(1, anim, 0.15F, p);
FriendlyByteBuf buf = newBuf();
original.encode(buf);
PacketPlayRigAnim decoded = PacketPlayRigAnim.decode(buf);
assertEquals(p, decoded.priority(),
"priority " + p + " must roundtrip and priority() must return same enum");
assertEquals((byte) p.ordinal(), decoded.priorityOrdinal(),
"priorityOrdinal must match enum.ordinal()");
}
}
/**
* priorityOrdinal hors-range (99) : priority() doit retourner LOWEST (fallback safe),
* PAS throw. Simule un peer malveillant ou un mismatch de protocole.
*/
@Test
void priority_outOfRange_returnsLowestFallback() {
ResourceLocation anim = ResourceLocation.fromNamespaceAndPath("tiedup", "test");
// Construit directement avec ordinal invalide (bypass of())
PacketPlayRigAnim malformed = new PacketPlayRigAnim(1, anim, 0.1F, (byte) 99);
assertEquals(Layer.Priority.LOWEST, malformed.priority(),
"priorityOrdinal=99 doit fallback sur LOWEST (pas de throw)");
// Idem pour ordinal négatif signé (qui devient 128+ via & 0xFF)
PacketPlayRigAnim negative = new PacketPlayRigAnim(1, anim, 0.1F, (byte) -1);
assertEquals(Layer.Priority.LOWEST, negative.priority(),
"priorityOrdinal=-1 (byte) doit fallback sur LOWEST (& 0xFF → 255, hors range)");
// Sanity : ordinal exactement PRIORITY_COUNT (5) est hors-range
PacketPlayRigAnim boundary = new PacketPlayRigAnim(1, anim, 0.1F, (byte) PacketPlayRigAnim.PRIORITY_COUNT);
assertEquals(Layer.Priority.LOWEST, boundary.priority(),
"priorityOrdinal=PRIORITY_COUNT doit fallback sur LOWEST");
}
/** animId avec namespace + path contenant underscores et slashes. */
@Test
void encode_decode_animId_namespaceSpecialChars() {
ResourceLocation anim = ResourceLocation.fromNamespaceAndPath("my_mod", "deep/path/with_underscores");
PacketPlayRigAnim original = PacketPlayRigAnim.of(123, anim, 0.2F, Layer.Priority.HIGH);
FriendlyByteBuf buf = newBuf();
original.encode(buf);
PacketPlayRigAnim decoded = PacketPlayRigAnim.decode(buf);
assertEquals(anim, decoded.animId(), "ResourceLocation complexe doit roundtrip verbatim");
assertEquals("my_mod", decoded.animId().getNamespace());
assertEquals("deep/path/with_underscores", decoded.animId().getPath());
}
/** Sanity check : deux packets distincts ne sont pas equal. */
@Test
void records_notEqual_whenDifferent() {
ResourceLocation anim = ResourceLocation.fromNamespaceAndPath("tiedup", "a");
PacketPlayRigAnim a = PacketPlayRigAnim.of(1, anim, 0.15F, Layer.Priority.LOWEST);
PacketPlayRigAnim b = PacketPlayRigAnim.of(2, anim, 0.15F, Layer.Priority.LOWEST);
assertNotEquals(a, b, "packets avec entityId différents ne doivent pas être equal");
}
}