Phase 2.3 : capability system RIG (EntityPatchProvider + events)

5 classes ajoutées dans rig/patch/ :

- TiedUpCapabilities.java
  Holder du Capability<EntityPatch> CAPABILITY_ENTITY (CapabilityToken
  auto-register) + helpers getEntityPatch / getPlayerPatch /
  getPlayerPatchAsOptional. Simplifié de EF (pas de ITEM/PROJECTILE/SKILL
  caps, combat only).

- EntityPatchProvider.java
  ICapabilityProvider + Map<EntityType, Function<Entity, Supplier<EntityPatch<?>>>>.
  registerEntityPatches() pour commonSetup (EntityType.PLAYER seul Phase 2),
  registerEntityPatchesClient() pour clientSetup (dispatch LocalPlayerPatch vs
  ClientPlayerPatch<RemotePlayer> vs ServerPlayerPatch). CUSTOM_CAPABILITIES
  pour extensions futures. Pas de GlobalMobPatch combat fallback.
  IMPORTANT : n'enregistre PAS EntityType.VILLAGER (MCA conflict V3-REW-10).

- TiedUpCapabilityEvents.java
  @Mod.EventBusSubscriber sur AttachCapabilitiesEvent<Entity>. Check oldPatch
  pour éviter double-attach, construit provider, appelle onConstructed eager
  (D-01 pattern EF), addCapability. Priority NORMAL (order d'attachement
  ne matière pas, c'est les runtime cross-cap reads qui importent et ceux-là
  sont déjà lazy dans onConstructed).

3 stubs PlayerPatch subclasses (placeholders Phase 2.4) :

- ServerPlayerPatch : overrideRender=false, getArmature=null stub, updateMotion no-op
- ClientPlayerPatch<T extends AbstractClientPlayer> : overrideRender=true, @OnlyIn CLIENT
- LocalPlayerPatch extends ClientPlayerPatch<LocalPlayer> : vide pour l'instant

Ces stubs satisfont le compile de EntityPatchProvider.registerEntityPatchesClient().
Le getArmature() null est non-bloquant Phase 2.3 mais devra être fixé Phase 2.4
pour le vrai rendering (lien avec TiedUpRigRegistry.BIPED à créer Phase 2.7).

Compile BUILD SUCCESSFUL + 11 tests bridge GREEN maintenus.
This commit is contained in:
notevil
2026-04-22 21:23:01 +02:00
parent 3aec681436
commit faad0ced0f
6 changed files with 448 additions and 0 deletions

View File

@@ -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.
*
* <p>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).</p>
*
* <p>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.</p>
*/
@OnlyIn(Dist.CLIENT)
public class ClientPlayerPatch<T extends AbstractClientPlayer> extends PlayerPatch<T> {
@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);
}
}

View File

@@ -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}.
*
* <p>Simplifié de EF {@code yesman.epicfight.world.capabilities.provider.EntityPatchProvider} :</p>
* <ul>
* <li>Pas de projectile (ProjectilePatch / ArrowPatch strippés)</li>
* <li>Pas de GlobalMobPatch fallback sur gamerule stun</li>
* <li>Pas d'EntityPatchRegistryEvent posté à ModLoader (pas de tiers mods EF pour l'instant)</li>
* <li>{@link #CUSTOM_CAPABILITIES} reste exposé via {@link #putCustomEntityPatch(EntityType, Function)}
* pour un hook d'extension future</li>
* </ul>
*
* <p><b>Attention MCA</b> : 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).</p>
*
* <p><b>Init order</b> : {@link #registerEntityPatches()} doit être appelé en
* {@code FMLCommonSetupEvent} (serveur + client), {@link #registerEntityPatchesClient()}
* en {@code FMLClientSetupEvent} uniquement.</p>
*/
public class EntityPatchProvider implements ICapabilityProvider, NonNullSupplier<EntityPatch<?>> {
/** Provider map populée au setup, lu au spawn entity. */
private static final Map<EntityType<?>, Function<Entity, Supplier<EntityPatch<?>>>> CAPABILITIES = Maps.newHashMap();
/** Hook extension pour mods/datapacks tiers. Prioritaire sur {@link #CAPABILITIES}. */
private static final Map<EntityType<?>, Function<Entity, Supplier<EntityPatch<?>>>> CUSTOM_CAPABILITIES = Maps.newHashMap();
/**
* À appeler en {@code FMLCommonSetupEvent.event.enqueueWork(...)}.
*
* <p>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.</p>
*/
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(...)}.
*
* <p>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).</p>
*/
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<net.minecraft.client.player.RemotePlayer>::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.
*
* <p>Prioritaire sur {@link #CAPABILITIES} : un call custom override la
* version par défaut si elle existe.</p>
*/
public static void putCustomEntityPatch(EntityType<?> entityType, Function<Entity, Supplier<EntityPatch<?>>> 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<EntityPatch<?>> optional = LazyOptional.of(this);
public EntityPatchProvider(Entity entity) {
Function<Entity, Supplier<EntityPatch<?>>> 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 <T> LazyOptional<T> getCapability(Capability<T> cap, Direction side) {
return cap == TiedUpCapabilities.CAPABILITY_ENTITY ? this.optional.cast() : LazyOptional.empty();
}
}

View File

@@ -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).
*
* <p>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.</p>
*
* <p>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.</p>
*/
@OnlyIn(Dist.CLIENT)
public class LocalPlayerPatch extends ClientPlayerPatch<LocalPlayer> {
// Hooks first-person / caméra / input → Phase 2.4
}

View File

@@ -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+.
*
* <p>Garanties actuelles :</p>
* <ul>
* <li>{@code overrideRender() = false} — serveur ne rend rien, pas de besoin
* d'intercepter le render pipeline</li>
* <li>{@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}.</li>
* <li>{@code updateMotion} no-op — Phase 2.7 hook tick réel</li>
* </ul>
*/
public class ServerPlayerPatch extends PlayerPatch<ServerPlayer> {
@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);
}
}

View File

@@ -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 :
*
* <ul>
* <li>{@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.</li>
* </ul>
*
* <p>EF a également {@code CAPABILITY_ITEM}, {@code CAPABILITY_PROJECTILE},
* {@code CAPABILITY_SKILL} — tous combat-only, strippés pour TiedUp.</p>
*
* <p><b>Auto-registration</b> : {@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).</p>
*/
@SuppressWarnings("rawtypes")
public class TiedUpCapabilities {
public static final Capability<EntityPatch> 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 extends EntityPatch> T getEntityPatch(@Nullable Entity entity, Class<T> 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<PlayerPatch<?>> 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();
}
}

View File

@@ -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).
*
* <p>Pattern EF {@code yesman.epicfight.events.CapabilityEvents:36-59} mais
* simplifié : pas de dual-capability pour CAPABILITY_SKILL (combat strippé).</p>
*
* <p><b>Init paresseux</b> : 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.</p>
*/
@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<Entity> 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);
}
}