diff --git a/src/main/java/com/tiedup/remake/network/ModNetwork.java b/src/main/java/com/tiedup/remake/network/ModNetwork.java index 49db8ae..dc13654 100644 --- a/src/main/java/com/tiedup/remake/network/ModNetwork.java +++ b/src/main/java/com/tiedup/remake/network/ModNetwork.java @@ -64,6 +64,7 @@ import com.tiedup.remake.network.sync.PacketSyncPetBedState; import com.tiedup.remake.network.sync.PacketSyncStruggleState; import com.tiedup.remake.network.trader.PacketBuyCaptive; 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.PacketV2LockToggle; import com.tiedup.remake.v2.bondage.network.PacketV2SelfEquip; @@ -592,6 +593,14 @@ public class ModNetwork { 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); } diff --git a/src/main/java/com/tiedup/remake/rig/network/PacketPlayRigAnim.java b/src/main/java/com/tiedup/remake/rig/network/PacketPlayRigAnim.java new file mode 100644 index 0000000..ae7f65c --- /dev/null +++ b/src/main/java/com/tiedup/remake/rig/network/PacketPlayRigAnim.java @@ -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. + * + *

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. + * + *

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 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); + } +} diff --git a/src/test/java/com/tiedup/remake/rig/network/PacketPlayRigAnimTest.java b/src/test/java/com/tiedup/remake/rig/network/PacketPlayRigAnimTest.java new file mode 100644 index 0000000..dbd6a03 --- /dev/null +++ b/src/test/java/com/tiedup/remake/rig/network/PacketPlayRigAnimTest.java @@ -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"); + } +}