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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user