From faad0ced0fa21e8e823d5c2d1e447bd4a6a807e1 Mon Sep 17 00:00:00 2001 From: notevil Date: Wed, 22 Apr 2026 21:23:01 +0200 Subject: [PATCH] Phase 2.3 : capability system RIG (EntityPatchProvider + events) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5 classes ajoutées dans rig/patch/ : - TiedUpCapabilities.java Holder du Capability CAPABILITY_ENTITY (CapabilityToken auto-register) + helpers getEntityPatch / getPlayerPatch / getPlayerPatchAsOptional. Simplifié de EF (pas de ITEM/PROJECTILE/SKILL caps, combat only). - EntityPatchProvider.java ICapabilityProvider + Map>>>. registerEntityPatches() pour commonSetup (EntityType.PLAYER seul Phase 2), registerEntityPatchesClient() pour clientSetup (dispatch LocalPlayerPatch vs ClientPlayerPatch 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. 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 : overrideRender=true, @OnlyIn CLIENT - LocalPlayerPatch extends ClientPlayerPatch : 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. --- .../remake/rig/patch/ClientPlayerPatch.java | 54 +++++++ .../remake/rig/patch/EntityPatchProvider.java | 151 ++++++++++++++++++ .../remake/rig/patch/LocalPlayerPatch.java | 30 ++++ .../remake/rig/patch/ServerPlayerPatch.java | 52 ++++++ .../remake/rig/patch/TiedUpCapabilities.java | 88 ++++++++++ .../rig/patch/TiedUpCapabilityEvents.java | 73 +++++++++ 6 files changed, 448 insertions(+) create mode 100644 src/main/java/com/tiedup/remake/rig/patch/ClientPlayerPatch.java create mode 100644 src/main/java/com/tiedup/remake/rig/patch/EntityPatchProvider.java create mode 100644 src/main/java/com/tiedup/remake/rig/patch/LocalPlayerPatch.java create mode 100644 src/main/java/com/tiedup/remake/rig/patch/ServerPlayerPatch.java create mode 100644 src/main/java/com/tiedup/remake/rig/patch/TiedUpCapabilities.java create mode 100644 src/main/java/com/tiedup/remake/rig/patch/TiedUpCapabilityEvents.java 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); + } +}