diff --git a/src/main/java/com/tiedup/remake/rig/patch/ClientPlayerPatch.java b/src/main/java/com/tiedup/remake/rig/patch/ClientPlayerPatch.java
new file mode 100644
index 0000000..532562e
--- /dev/null
+++ b/src/main/java/com/tiedup/remake/rig/patch/ClientPlayerPatch.java
@@ -0,0 +1,54 @@
+/*
+ * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
+ * by the Epic Fight Team, licensed under GPLv3.
+ * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
+ */
+
+package com.tiedup.remake.rig.patch;
+
+import net.minecraft.client.player.AbstractClientPlayer;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.api.distmarker.OnlyIn;
+
+import com.tiedup.remake.rig.armature.Armature;
+import com.tiedup.remake.rig.math.OpenMatrix4f;
+
+/**
+ * Stub Phase 2.3 — patch côté client pour les joueurs remote (autres joueurs
+ * dans une session multi). Etoffé Phase 2.4 avec la logique cape / scale /
+ * slim vs default model selection.
+ *
+ *
Version minimale : {@code overrideRender=true} (on veut que le renderer
+ * patched intercepte et dispatch au RIG), {@code getArmature=null} stub (TODO
+ * Phase 2.4), {@code updateMotion} no-op (Phase 2.7).
+ *
+ * Forke conceptuellement {@code yesman.epicfight.client.world.capabilites.entitypatch.player.AbstractClientPlayerPatch}
+ * (EF 479 LOC) mais réécrit from scratch (D-04) car ~80% du contenu original
+ * est combat/skill non réutilisable pour TiedUp.
+ */
+@OnlyIn(Dist.CLIENT)
+public class ClientPlayerPatch extends PlayerPatch {
+
+ @Override
+ public void updateMotion(boolean considerInaction) {
+ // no-op stub — Phase 2.7
+ }
+
+ @Override
+ public Armature getArmature() {
+ // TODO Phase 2.4 — retourner TiedUpRigRegistry.BIPED.get()
+ return null;
+ }
+
+ @Override
+ public boolean overrideRender() {
+ // On veut que le render pipeline patched prenne la main pour les
+ // remote players visibles.
+ return true;
+ }
+
+ @Override
+ public OpenMatrix4f getModelMatrix(float partialTick) {
+ return getMatrix(partialTick);
+ }
+}
diff --git a/src/main/java/com/tiedup/remake/rig/patch/EntityPatchProvider.java b/src/main/java/com/tiedup/remake/rig/patch/EntityPatchProvider.java
new file mode 100644
index 0000000..f696fae
--- /dev/null
+++ b/src/main/java/com/tiedup/remake/rig/patch/EntityPatchProvider.java
@@ -0,0 +1,151 @@
+/*
+ * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
+ * by the Epic Fight Team, licensed under GPLv3.
+ * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
+ */
+
+package com.tiedup.remake.rig.patch;
+
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import com.google.common.collect.Maps;
+
+import net.minecraft.core.Direction;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityType;
+import net.minecraftforge.common.capabilities.Capability;
+import net.minecraftforge.common.capabilities.ICapabilityProvider;
+import net.minecraftforge.common.util.LazyOptional;
+import net.minecraftforge.common.util.NonNullSupplier;
+
+import com.tiedup.remake.rig.TiedUpRigConstants;
+
+/**
+ * Dispatcher + {@link ICapabilityProvider} qui construit l'{@link EntityPatch}
+ * approprié pour un {@link Entity} donné, basé sur son {@link EntityType}.
+ *
+ * Simplifié de EF {@code yesman.epicfight.world.capabilities.provider.EntityPatchProvider} :
+ *
+ * - Pas de projectile (ProjectilePatch / ArrowPatch strippés)
+ * - Pas de GlobalMobPatch fallback sur gamerule stun
+ * - Pas d'EntityPatchRegistryEvent posté à ModLoader (pas de tiers mods EF pour l'instant)
+ * - {@link #CUSTOM_CAPABILITIES} reste exposé via {@link #putCustomEntityPatch(EntityType, Function)}
+ * pour un hook d'extension future
+ *
+ *
+ * Attention MCA : on ne doit PAS enregistrer {@link EntityType#VILLAGER}
+ * dans {@link #CAPABILITIES}. MCA gère son propre rendering custom via
+ * {@code MixinVillagerEntityMCAAnimated} et attacher notre patch causerait
+ * double-rendering + rupture des anims MCA (cf.
+ * {@code docs/plans/rig/V3_REWORK_BACKLOG.md} V3-REW-10).
+ *
+ * Init order : {@link #registerEntityPatches()} doit être appelé en
+ * {@code FMLCommonSetupEvent} (serveur + client), {@link #registerEntityPatchesClient()}
+ * en {@code FMLClientSetupEvent} uniquement.
+ */
+public class EntityPatchProvider implements ICapabilityProvider, NonNullSupplier> {
+
+ /** Provider map populée au setup, lu au spawn entity. */
+ private static final Map, Function>>> CAPABILITIES = Maps.newHashMap();
+
+ /** Hook extension pour mods/datapacks tiers. Prioritaire sur {@link #CAPABILITIES}. */
+ private static final Map, Function>>> CUSTOM_CAPABILITIES = Maps.newHashMap();
+
+ /**
+ * À appeler en {@code FMLCommonSetupEvent.event.enqueueWork(...)}.
+ *
+ * Phase 2 : enregistre uniquement {@link EntityType#PLAYER} côté serveur.
+ * Les NPCs TiedUp (Damsel, Maid, Master, Kidnapper) seront ajoutés Phase 5
+ * quand on aura des animations adaptées.
+ */
+ public static void registerEntityPatches() {
+ CAPABILITIES.put(EntityType.PLAYER, (entityIn) -> ServerPlayerPatch::new);
+
+ TiedUpRigConstants.LOGGER.debug(
+ "EntityPatchProvider: registered {} entity types (common)", CAPABILITIES.size()
+ );
+ }
+
+ /**
+ * À appeler en {@code FMLClientSetupEvent.event.enqueueWork(...)}.
+ *
+ * Surcharge {@link EntityType#PLAYER} côté client pour dispatcher vers
+ * {@link LocalPlayerPatch} (joueur local) vs {@link ClientPlayerPatch}
+ * (remote players) vs {@link ServerPlayerPatch} (si logical server même JVM).
+ */
+ public static void registerEntityPatchesClient() {
+ CAPABILITIES.put(EntityType.PLAYER, (entityIn) -> {
+ if (entityIn instanceof net.minecraft.client.player.LocalPlayer) {
+ return LocalPlayerPatch::new;
+ } else if (entityIn instanceof net.minecraft.client.player.RemotePlayer) {
+ return ClientPlayerPatch::new;
+ } else if (entityIn instanceof net.minecraft.server.level.ServerPlayer) {
+ return ServerPlayerPatch::new;
+ }
+ return () -> null;
+ });
+
+ TiedUpRigConstants.LOGGER.debug(
+ "EntityPatchProvider: client-side player dispatch installed"
+ );
+ }
+
+ /**
+ * Enregistre un patch provider custom pour un {@link EntityType} donné.
+ * Utilisé pour extensions tierces (compat mods) ou datapack-driven patches.
+ *
+ * Prioritaire sur {@link #CAPABILITIES} : un call custom override la
+ * version par défaut si elle existe.
+ */
+ public static void putCustomEntityPatch(EntityType> entityType, Function>> provider) {
+ CUSTOM_CAPABILITIES.put(entityType, provider);
+ }
+
+ /**
+ * Nettoie les custom patches (typiquement au datapack reload).
+ */
+ public static void clearCustom() {
+ CUSTOM_CAPABILITIES.clear();
+ }
+
+ // ---------- instance ----------
+
+ private EntityPatch> capability;
+ private final LazyOptional> optional = LazyOptional.of(this);
+
+ public EntityPatchProvider(Entity entity) {
+ Function>> provider =
+ CUSTOM_CAPABILITIES.getOrDefault(entity.getType(), CAPABILITIES.get(entity.getType()));
+
+ if (provider != null) {
+ try {
+ this.capability = provider.apply(entity).get();
+ } catch (Exception e) {
+ TiedUpRigConstants.LOGGER.error(
+ "EntityPatchProvider: failed to construct patch for {}",
+ entity.getType(), e
+ );
+ }
+ }
+ }
+
+ /**
+ * @return true si ce provider a bien construit un patch pour l'entity
+ * (= entity est dans la whitelist PLAYER / future NPCs TiedUp).
+ */
+ public boolean hasCapability() {
+ return this.capability != null;
+ }
+
+ @Override
+ public EntityPatch> get() {
+ return this.capability;
+ }
+
+ @Override
+ public LazyOptional getCapability(Capability cap, Direction side) {
+ return cap == TiedUpCapabilities.CAPABILITY_ENTITY ? this.optional.cast() : LazyOptional.empty();
+ }
+}
diff --git a/src/main/java/com/tiedup/remake/rig/patch/LocalPlayerPatch.java b/src/main/java/com/tiedup/remake/rig/patch/LocalPlayerPatch.java
new file mode 100644
index 0000000..829bf2b
--- /dev/null
+++ b/src/main/java/com/tiedup/remake/rig/patch/LocalPlayerPatch.java
@@ -0,0 +1,30 @@
+/*
+ * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
+ * by the Epic Fight Team, licensed under GPLv3.
+ * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
+ */
+
+package com.tiedup.remake.rig.patch;
+
+import net.minecraft.client.player.LocalPlayer;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.api.distmarker.OnlyIn;
+
+/**
+ * Stub Phase 2.3 — patch spécifique au joueur local (self). Spécialisation de
+ * {@link ClientPlayerPatch} pour les hooks first-person / caméra / input
+ * capture bondage-specific (struggle keys, adjustment menu).
+ *
+ * Version minimale : hérite de {@link ClientPlayerPatch} sans override.
+ * Phase 2.4 ajoutera les hooks first-person hide (arms/hands invisibles sous
+ * wrap/latex_sack), camera sync leash, etc.
+ *
+ * Forke conceptuellement {@code yesman.epicfight.client.world.capabilites.entitypatch.player.LocalPlayerPatch}
+ * (EF 572 LOC) mais réécrit from scratch (D-04) car skill UI state / input /
+ * camera cinematics non réutilisables.
+ */
+@OnlyIn(Dist.CLIENT)
+public class LocalPlayerPatch extends ClientPlayerPatch {
+
+ // Hooks first-person / caméra / input → Phase 2.4
+}
diff --git a/src/main/java/com/tiedup/remake/rig/patch/ServerPlayerPatch.java b/src/main/java/com/tiedup/remake/rig/patch/ServerPlayerPatch.java
new file mode 100644
index 0000000..2d56af0
--- /dev/null
+++ b/src/main/java/com/tiedup/remake/rig/patch/ServerPlayerPatch.java
@@ -0,0 +1,52 @@
+/*
+ * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
+ * by the Epic Fight Team, licensed under GPLv3.
+ * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
+ */
+
+package com.tiedup.remake.rig.patch;
+
+import net.minecraft.server.level.ServerPlayer;
+
+import com.tiedup.remake.rig.armature.Armature;
+import com.tiedup.remake.rig.math.OpenMatrix4f;
+
+/**
+ * Stub Phase 2.3 — patch côté serveur pour les joueurs. Version minimale pour
+ * débloquer le dispatch via {@link EntityPatchProvider}. La version complète
+ * (kidnap state sync, struggle progression, ownership) est Phase 2.4+.
+ *
+ * Garanties actuelles :
+ *
+ * - {@code overrideRender() = false} — serveur ne rend rien, pas de besoin
+ * d'intercepter le render pipeline
+ * - {@code getArmature() = null} — le serveur n'a pas besoin de mesh data,
+ * il joue des animations "blind" (calcule la pose pour syncer aux clients).
+ * Sera fixé en Phase 2.4 avec un fallback vers {@code TiedUpRigRegistry.BIPED}.
+ * - {@code updateMotion} no-op — Phase 2.7 hook tick réel
+ *
+ */
+public class ServerPlayerPatch extends PlayerPatch {
+
+ @Override
+ public void updateMotion(boolean considerInaction) {
+ // no-op stub — Phase 2.7
+ }
+
+ @Override
+ public Armature getArmature() {
+ // TODO Phase 2.4 — retourner TiedUpRigRegistry.BIPED.get()
+ return null;
+ }
+
+ @Override
+ public boolean overrideRender() {
+ // Serveur ne rend rien.
+ return false;
+ }
+
+ @Override
+ public OpenMatrix4f getModelMatrix(float partialTick) {
+ return getMatrix(partialTick);
+ }
+}
diff --git a/src/main/java/com/tiedup/remake/rig/patch/TiedUpCapabilities.java b/src/main/java/com/tiedup/remake/rig/patch/TiedUpCapabilities.java
new file mode 100644
index 0000000..c960de4
--- /dev/null
+++ b/src/main/java/com/tiedup/remake/rig/patch/TiedUpCapabilities.java
@@ -0,0 +1,88 @@
+/*
+ * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
+ * by the Epic Fight Team, licensed under GPLv3.
+ * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
+ */
+
+package com.tiedup.remake.rig.patch;
+
+import java.util.Optional;
+
+import javax.annotation.Nullable;
+
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.player.Player;
+import net.minecraftforge.common.capabilities.Capability;
+import net.minecraftforge.common.capabilities.CapabilityManager;
+import net.minecraftforge.common.capabilities.CapabilityToken;
+
+/**
+ * Holder du {@link Capability} central RIG, analogue à EF
+ * {@code yesman.epicfight.world.capabilities.EpicFightCapabilities} mais réduit
+ * à l'essentiel pour TiedUp :
+ *
+ *
+ * - {@link #CAPABILITY_ENTITY} — attaché à {@code Player} (Phase 2) et NPCs
+ * TiedUp (Phase 5). Porte l'{@link EntityPatch} qui contient l'Animator,
+ * l'Armature, l'état d'animation, etc.
+ *
+ *
+ * EF a également {@code CAPABILITY_ITEM}, {@code CAPABILITY_PROJECTILE},
+ * {@code CAPABILITY_SKILL} — tous combat-only, strippés pour TiedUp.
+ *
+ * Auto-registration : {@link CapabilityToken} utilise une TypeToken
+ * pour enregistrer le capability via réflection. Pas besoin d'abonner un
+ * {@code RegisterCapabilitiesEvent} (comportement Forge 1.20.1, déjà utilisé
+ * pour {@code V2_BONDAGE_EQUIPMENT} dans TiedUp).
+ */
+@SuppressWarnings("rawtypes")
+public class TiedUpCapabilities {
+
+ public static final Capability CAPABILITY_ENTITY =
+ CapabilityManager.get(new CapabilityToken<>() {});
+
+ private TiedUpCapabilities() {}
+
+ /**
+ * Extrait l'{@link EntityPatch} d'un entity avec null-check + type-check.
+ *
+ * @return le patch cast au type demandé, ou null si entity null / pas de
+ * capability / type incompatible
+ */
+ @SuppressWarnings("unchecked")
+ @Nullable
+ public static T getEntityPatch(@Nullable Entity entity, Class type) {
+ if (entity == null) {
+ return null;
+ }
+ EntityPatch> patch = entity.getCapability(CAPABILITY_ENTITY).orElse(null);
+ if (patch != null && type.isAssignableFrom(patch.getClass())) {
+ return (T) patch;
+ }
+ return null;
+ }
+
+ /**
+ * Helper court pour extraire un {@link PlayerPatch} d'un {@link Player}.
+ */
+ @Nullable
+ public static PlayerPatch> getPlayerPatch(@Nullable Player player) {
+ if (player == null) {
+ return null;
+ }
+ EntityPatch> patch = player.getCapability(CAPABILITY_ENTITY).orElse(null);
+ return patch instanceof PlayerPatch> pp ? pp : null;
+ }
+
+ /**
+ * Version {@link Optional} de {@link #getPlayerPatch(Player)} pour les
+ * call sites qui veulent un flow {@code .ifPresent(...)}.
+ */
+ public static Optional> getPlayerPatchAsOptional(@Nullable Entity entity) {
+ if (entity == null) {
+ return Optional.empty();
+ }
+ EntityPatch> patch = entity.getCapability(CAPABILITY_ENTITY).orElse(null);
+ return patch instanceof PlayerPatch> pp ? Optional.of(pp) : Optional.empty();
+ }
+}
diff --git a/src/main/java/com/tiedup/remake/rig/patch/TiedUpCapabilityEvents.java b/src/main/java/com/tiedup/remake/rig/patch/TiedUpCapabilityEvents.java
new file mode 100644
index 0000000..4419e69
--- /dev/null
+++ b/src/main/java/com/tiedup/remake/rig/patch/TiedUpCapabilityEvents.java
@@ -0,0 +1,73 @@
+/*
+ * Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
+ * by the Epic Fight Team, licensed under GPLv3.
+ * Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
+ */
+
+package com.tiedup.remake.rig.patch;
+
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.entity.Entity;
+import net.minecraftforge.event.AttachCapabilitiesEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.common.Mod;
+
+import com.tiedup.remake.core.TiedUpMod;
+import com.tiedup.remake.rig.TiedUpRigConstants;
+
+/**
+ * Subscriber Forge qui attache un {@link EntityPatchProvider} sur chaque
+ * {@link Entity} dont l'{@link net.minecraft.world.entity.EntityType} est
+ * enregistré dans {@link EntityPatchProvider#CAPABILITIES CAPABILITIES}
+ * (Phase 2 : PLAYER uniquement).
+ *
+ * Pattern EF {@code yesman.epicfight.events.CapabilityEvents:36-59} mais
+ * simplifié : pas de dual-capability pour CAPABILITY_SKILL (combat strippé).
+ *
+ * Init paresseux : l'{@code onConstructed} de {@link EntityPatch}
+ * ne fait PAS de cross-capability lookup (pas de
+ * {@code entity.getCapability(V2_BONDAGE_EQUIPMENT)}) pour éviter toute race
+ * avec l'ordre d'attachement inter-mods Forge. Voir adversarial review
+ * 2026-04-22 §2 pour le raisonnement.
+ */
+@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
+public class TiedUpCapabilityEvents {
+
+ private static final ResourceLocation ENTITY_CAPABILITY_KEY =
+ ResourceLocation.fromNamespaceAndPath(TiedUpMod.MOD_ID, "rig_entity_cap");
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @SubscribeEvent
+ public static void attachEntityCapability(AttachCapabilitiesEvent event) {
+ Entity entity = event.getObject();
+
+ // Skip si déjà attaché par un autre subscriber (ex. datapack override via
+ // CUSTOM_CAPABILITIES) — évite double-attach silencieux.
+ EntityPatch oldPatch = TiedUpCapabilities.getEntityPatch(entity, EntityPatch.class);
+ if (oldPatch != null) {
+ return;
+ }
+
+ EntityPatchProvider provider = new EntityPatchProvider(entity);
+ if (!provider.hasCapability()) {
+ return; // pas dans la whitelist (VILLAGER MCA, mobs non TiedUp, etc.)
+ }
+
+ EntityPatch patch = provider.getCapability(
+ TiedUpCapabilities.CAPABILITY_ENTITY, null
+ ).orElse(null);
+
+ if (patch == null) {
+ TiedUpRigConstants.LOGGER.warn(
+ "TiedUpCapabilityEvents: EntityPatchProvider reported hasCapability but CAPABILITY_ENTITY empty for {}",
+ entity.getType()
+ );
+ return;
+ }
+
+ // Eager init (pattern EF) : construire l'Animator + Armature avant que
+ // l'entity commence son premier tick.
+ patch.onConstructed(entity);
+ event.addCapability(ENTITY_CAPABILITY_KEY, provider);
+ }
+}