Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
package com.tiedup.remake.mixin;
|
||||
|
||||
import com.tiedup.remake.state.HumanChairHelper;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/**
|
||||
* Mixin to lock player body rotation during human chair mode.
|
||||
*
|
||||
* Vanilla {@code LivingEntity.tickHeadTurn()} gradually rotates yBodyRot
|
||||
* toward the head/movement direction. This overrides any value set from
|
||||
* a tick handler. By cancelling the method here, the body stays locked
|
||||
* at the stored facing direction.
|
||||
*/
|
||||
@Mixin(LivingEntity.class)
|
||||
public abstract class MixinLivingEntityBodyRot {
|
||||
|
||||
@Inject(method = "tickHeadTurn", at = @At("HEAD"), cancellable = true)
|
||||
private void tiedup$lockBodyForHumanChair(
|
||||
float yRot,
|
||||
float animStep,
|
||||
CallbackInfoReturnable<Float> cir
|
||||
) {
|
||||
LivingEntity self = (LivingEntity) (Object) this;
|
||||
if (!(self instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state == null || !state.isTiedUp()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
|
||||
if (bind.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HumanChairHelper.isActive(bind)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock body rotation to the stored facing and skip vanilla update
|
||||
float lockedYaw = HumanChairHelper.getFacing(bind);
|
||||
self.yBodyRot = lockedYaw;
|
||||
self.yBodyRotO = lockedYaw;
|
||||
cir.setReturnValue(animStep);
|
||||
}
|
||||
}
|
||||
186
src/main/java/com/tiedup/remake/mixin/MixinMCAMessenger.java
Normal file
186
src/main/java/com/tiedup/remake/mixin/MixinMCAMessenger.java
Normal file
@@ -0,0 +1,186 @@
|
||||
package com.tiedup.remake.mixin;
|
||||
|
||||
import com.tiedup.remake.compat.mca.MCACompat;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.GagTalkManager;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.MutableComponent;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/**
|
||||
* Mixin for MCA's VillagerEntityMCA to handle gagged speech.
|
||||
*
|
||||
* <p>This mixin provides method implementations that override the default methods
|
||||
* from MCA's Messenger interface. When the villager is gagged:
|
||||
* <ul>
|
||||
* <li>isSpeechImpaired() returns true (blocks TTS)</li>
|
||||
* <li>playSpeechEffect() does nothing (silence)</li>
|
||||
* <li>transformMessage() applies gagtalk transformation</li>
|
||||
* <li>playWelcomeSound() is cancelled (no greeting)</li>
|
||||
* <li>playSurprisedSound() is cancelled (no surprise sound)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Uses @Pseudo for soft dependency - only applies if MCA is present.
|
||||
*/
|
||||
@Pseudo
|
||||
@Mixin(targets = "forge.net.mca.entity.VillagerEntityMCA", remap = false)
|
||||
public abstract class MixinMCAMessenger {
|
||||
|
||||
/**
|
||||
* Override isSpeechImpaired to return true when gagged.
|
||||
*
|
||||
* <p>This blocks TTS voice generation in MCA's SpeechManager.
|
||||
* When the villager is not gagged, returns false (normal speech).
|
||||
*
|
||||
* @return true if gagged, false otherwise
|
||||
*/
|
||||
public boolean isSpeechImpaired() {
|
||||
try {
|
||||
LivingEntity entity = (LivingEntity) (Object) this;
|
||||
IBondageState state = MCACompat.getKidnappedState(entity);
|
||||
|
||||
if (state != null && state.isGagged()) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MCA] isSpeechImpaired check failed: {}",
|
||||
e.getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override playSpeechEffect to produce complete silence when gagged.
|
||||
*
|
||||
* <p>When the villager is gagged, this method does nothing (no sound).
|
||||
* When not gagged, delegates to default behavior.
|
||||
*/
|
||||
public void playSpeechEffect() {
|
||||
try {
|
||||
LivingEntity entity = (LivingEntity) (Object) this;
|
||||
IBondageState state = MCACompat.getKidnappedState(entity);
|
||||
|
||||
if (state != null && state.isGagged()) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MCA] playSpeechEffect cancelled for gagged villager"
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MCA] playSpeechEffect check failed: {}",
|
||||
e.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override transformMessage to apply gagtalk when gagged.
|
||||
*
|
||||
* <p>When the villager is gagged, transforms the message using
|
||||
* TiedUp's GagTalkManager. When not gagged, returns the original
|
||||
* message unchanged.
|
||||
*
|
||||
* @param message The original message
|
||||
* @return The gagged message if gagged, original otherwise
|
||||
*/
|
||||
public MutableComponent transformMessage(MutableComponent message) {
|
||||
try {
|
||||
LivingEntity entity = (LivingEntity) (Object) this;
|
||||
IBondageState state = MCACompat.getKidnappedState(entity);
|
||||
|
||||
if (state != null && state.isGagged()) {
|
||||
ItemStack gag = state.getEquipment(BodyRegionV2.MOUTH);
|
||||
String originalText = message.getString();
|
||||
String gaggedText = GagTalkManager.transformToGaggedSpeech(
|
||||
originalText,
|
||||
gag
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MCA] Applied gagtalk transformation for villager"
|
||||
);
|
||||
return Component.literal(gaggedText);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MCA] transformMessage failed: {}",
|
||||
e.getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept playWelcomeSound to block greeting sounds when gagged.
|
||||
*
|
||||
* <p>MCA calls playWelcomeSound() in interactAt() when a player interacts
|
||||
* with a villager. This plays the "hi"/"hello" greeting sound.
|
||||
*/
|
||||
@Inject(
|
||||
method = "playWelcomeSound",
|
||||
at = @At("HEAD"),
|
||||
cancellable = true,
|
||||
remap = false,
|
||||
require = 0
|
||||
)
|
||||
private void tiedup$blockWelcomeSound(CallbackInfo ci) {
|
||||
try {
|
||||
LivingEntity entity = (LivingEntity) (Object) this;
|
||||
IBondageState state = MCACompat.getKidnappedState(entity);
|
||||
|
||||
if (state != null && state.isGagged()) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MCA] playWelcomeSound blocked for gagged villager"
|
||||
);
|
||||
ci.cancel();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MCA] playWelcomeSound check failed: {}",
|
||||
e.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept playSurprisedSound to block surprised sounds when gagged.
|
||||
*/
|
||||
@Inject(
|
||||
method = "playSurprisedSound",
|
||||
at = @At("HEAD"),
|
||||
cancellable = true,
|
||||
remap = false,
|
||||
require = 0
|
||||
)
|
||||
private void tiedup$blockSurprisedSound(CallbackInfo ci) {
|
||||
try {
|
||||
LivingEntity entity = (LivingEntity) (Object) this;
|
||||
IBondageState state = MCACompat.getKidnappedState(entity);
|
||||
|
||||
if (state != null && state.isGagged()) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MCA] playSurprisedSound blocked for gagged villager"
|
||||
);
|
||||
ci.cancel();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MCA] playSurprisedSound check failed: {}",
|
||||
e.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
129
src/main/java/com/tiedup/remake/mixin/MixinMCAOpenAIChatAI.java
Normal file
129
src/main/java/com/tiedup/remake/mixin/MixinMCAOpenAIChatAI.java
Normal file
@@ -0,0 +1,129 @@
|
||||
package com.tiedup.remake.mixin;
|
||||
|
||||
import com.tiedup.remake.compat.mca.MCACompat;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.compat.mca.ai.chatai.TiedUpModule;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.GagTalkManager;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
|
||||
|
||||
/**
|
||||
* Mixin for MCA's OpenAIChatAI to integrate TiedUp bondage context.
|
||||
*
|
||||
* <p>This mixin:
|
||||
* <ul>
|
||||
* <li>Injects TiedUp state into the AI context (tied, gagged, blindfolded, etc.)</li>
|
||||
* <li>Transforms AI responses with gagtalk if the villager is gagged</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Note: Uses @Pseudo for soft dependency - only applies if MCA is present.
|
||||
*/
|
||||
@Pseudo
|
||||
@Mixin(targets = "forge.net.mca.entity.ai.chatAI.OpenAIChatAI", remap = false)
|
||||
public class MixinMCAOpenAIChatAI {
|
||||
|
||||
/**
|
||||
* Inject TiedUp context after MCA's modules are applied.
|
||||
*
|
||||
* <p>We capture the 'input' list local variable and add our TiedUp context to it.
|
||||
* This is called after PlayerModule.apply() which is the last module in the chain.
|
||||
*/
|
||||
@Inject(
|
||||
method = "answer",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lforge/net/mca/entity/ai/chatAI/modules/PlayerModule;apply(Ljava/util/List;Lforge/net/mca/entity/VillagerEntityMCA;Lnet/minecraft/server/network/ServerPlayerEntity;)V",
|
||||
shift = At.Shift.AFTER
|
||||
),
|
||||
locals = LocalCapture.CAPTURE_FAILSOFT,
|
||||
require = 0 // Soft requirement - don't crash if not found
|
||||
)
|
||||
private void tiedup$injectBondageContext(
|
||||
Object player, // ServerPlayerEntity
|
||||
Object villager, // VillagerEntityMCA
|
||||
String msg,
|
||||
CallbackInfoReturnable<Optional<String>> cir,
|
||||
// Local variables captured (order matters!)
|
||||
Object config,
|
||||
boolean isInHouse,
|
||||
String playerName,
|
||||
String villagerName,
|
||||
long time,
|
||||
List<?> pastDialogue,
|
||||
List<String> input
|
||||
) {
|
||||
try {
|
||||
if (
|
||||
villager instanceof LivingEntity living &&
|
||||
player instanceof net.minecraft.world.entity.player.Player p
|
||||
) {
|
||||
TiedUpModule.apply(input, living, p);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MCA AI] Injected TiedUp context for {}",
|
||||
living.getName().getString()
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MCA AI] Failed to inject context: {}",
|
||||
e.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the AI response with gagtalk if the villager is gagged.
|
||||
*
|
||||
* <p>Intercepts the return value and applies gagtalk transformation
|
||||
* using TiedUp's GagTalkManager.
|
||||
*/
|
||||
@Inject(
|
||||
method = "answer",
|
||||
at = @At("RETURN"),
|
||||
cancellable = true,
|
||||
require = 0
|
||||
)
|
||||
private void tiedup$transformGaggedResponse(
|
||||
Object player, // ServerPlayerEntity
|
||||
Object villager, // VillagerEntityMCA
|
||||
String msg,
|
||||
CallbackInfoReturnable<Optional<String>> cir
|
||||
) {
|
||||
try {
|
||||
Optional<String> result = cir.getReturnValue();
|
||||
if (result == null || result.isEmpty()) return;
|
||||
|
||||
if (!(villager instanceof LivingEntity living)) return;
|
||||
|
||||
IBondageState state = MCACompat.getKidnappedState(living);
|
||||
if (state == null || !state.isGagged()) return;
|
||||
|
||||
// Apply gagtalk transformation
|
||||
ItemStack gag = state.getEquipment(BodyRegionV2.MOUTH);
|
||||
String gaggedResponse = GagTalkManager.transformToGaggedSpeech(
|
||||
result.get(),
|
||||
gag
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug("[MCA AI] Applied gagtalk to AI response");
|
||||
cir.setReturnValue(Optional.of(gaggedResponse));
|
||||
} catch (Exception e) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MCA AI] Failed to transform gagged response: {}",
|
||||
e.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package com.tiedup.remake.mixin;
|
||||
|
||||
import com.tiedup.remake.compat.mca.MCACompat;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.items.ItemKey;
|
||||
import com.tiedup.remake.items.ItemMasterKey;
|
||||
import com.tiedup.remake.v2.bondage.IV2BondageItem;
|
||||
import com.tiedup.remake.items.base.ItemCollar;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/**
|
||||
* Mixin to manage TiedUp vs MCA interaction priority.
|
||||
*
|
||||
* <p>Handles TiedUp interactions DIRECTLY instead of just blocking MCA,
|
||||
* because vanilla leash/item handling happens at different points in the
|
||||
* interaction chain that MCA may bypass.
|
||||
*
|
||||
* <p>Priority rules:
|
||||
* <ol>
|
||||
* <li>Empty hand + leashed → detach leash</li>
|
||||
* <li>Holding lead + (tied OR collar owner) → attach leash</li>
|
||||
* <li>Holding key + collared → call key.interactLivingEntity()</li>
|
||||
* <li>Shift+click + tied + empty hand → let event handler untie</li>
|
||||
* <li>Holding bondage item → let item handle tying</li>
|
||||
* <li>Otherwise → let MCA handle</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Uses @Pseudo - mixin is optional and will be skipped if MCA is not installed.
|
||||
*/
|
||||
@Pseudo
|
||||
@Mixin(targets = "forge.net.mca.entity.VillagerEntityMCA", remap = false)
|
||||
public abstract class MixinMCAVillagerInteraction {
|
||||
|
||||
/**
|
||||
* Inject at HEAD of mobInteract to handle TiedUp interactions.
|
||||
* Method: InteractionResult mobInteract(Player, InteractionHand)
|
||||
*/
|
||||
@Inject(
|
||||
method = "m_6071_",
|
||||
at = @At("HEAD"),
|
||||
cancellable = true,
|
||||
remap = false
|
||||
)
|
||||
private void tiedup$handleMobInteract(
|
||||
Player player,
|
||||
InteractionHand hand,
|
||||
CallbackInfoReturnable<InteractionResult> cir
|
||||
) {
|
||||
InteractionResult result = tiedup$handleInteraction(player, hand);
|
||||
if (result != null) {
|
||||
cir.setReturnValue(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject at HEAD of interactAt to handle TiedUp interactions BEFORE MCA opens its menu.
|
||||
* Method: InteractionResult interactAt(Player, Vec3, InteractionHand)
|
||||
*/
|
||||
@Inject(
|
||||
method = "m_7111_",
|
||||
at = @At("HEAD"),
|
||||
cancellable = true,
|
||||
remap = false
|
||||
)
|
||||
private void tiedup$handleInteractAt(
|
||||
Player player,
|
||||
net.minecraft.world.phys.Vec3 vec,
|
||||
InteractionHand hand,
|
||||
CallbackInfoReturnable<InteractionResult> cir
|
||||
) {
|
||||
InteractionResult result = tiedup$handleInteraction(player, hand);
|
||||
if (result != null) {
|
||||
cir.setReturnValue(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common interaction handling logic for both mobInteract and interactAt.
|
||||
*
|
||||
* @param player The player interacting
|
||||
* @param hand The hand used for interaction
|
||||
* @return InteractionResult to return, or null to let MCA handle
|
||||
*/
|
||||
@Unique
|
||||
private InteractionResult tiedup$handleInteraction(
|
||||
Player player,
|
||||
InteractionHand hand
|
||||
) {
|
||||
Mob mob = (Mob) (Object) this;
|
||||
IBondageState state = MCACompat.getKidnappedState(mob);
|
||||
ItemStack heldItem = player.getItemInHand(hand);
|
||||
boolean isClientSide = player.level().isClientSide;
|
||||
|
||||
// 1. LEASH DETACHMENT: Empty hand + leashed → drop leash
|
||||
if (mob.isLeashed() && heldItem.isEmpty()) {
|
||||
if (!isClientSide) {
|
||||
mob.dropLeash(true, !player.getAbilities().instabuild);
|
||||
TiedUpMod.LOGGER.debug("[MCA] Detached leash from villager");
|
||||
}
|
||||
return InteractionResult.sidedSuccess(isClientSide);
|
||||
}
|
||||
|
||||
// 2. LEASH ATTACHMENT: Holding lead + (tied OR collar owner)
|
||||
if (heldItem.is(Items.LEAD) && !mob.isLeashed()) {
|
||||
if (tiedup$canLeash(player, state)) {
|
||||
if (!isClientSide) {
|
||||
mob.setLeashedTo(player, true);
|
||||
if (!player.getAbilities().instabuild) {
|
||||
heldItem.shrink(1);
|
||||
}
|
||||
TiedUpMod.LOGGER.debug("[MCA] Attached leash to villager");
|
||||
}
|
||||
return InteractionResult.sidedSuccess(isClientSide);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. KEY INTERACTION: Key + collared → call key's interactLivingEntity
|
||||
if (state != null && state.hasCollar()) {
|
||||
if (heldItem.getItem() instanceof ItemKey key) {
|
||||
return key.interactLivingEntity(heldItem, player, mob, hand);
|
||||
}
|
||||
if (heldItem.getItem() instanceof ItemMasterKey masterKey) {
|
||||
return masterKey.interactLivingEntity(
|
||||
heldItem,
|
||||
player,
|
||||
mob,
|
||||
hand
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. UNTYING: Shift+click + tied + empty hand → let event handler process
|
||||
if (
|
||||
state != null &&
|
||||
state.isTiedUp() &&
|
||||
heldItem.isEmpty() &&
|
||||
player.isShiftKeyDown()
|
||||
) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
// 5. BONDAGE ITEM: Let TiedUp item's interactLivingEntity handle tying
|
||||
if (heldItem.getItem() instanceof IV2BondageItem) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
// Let MCA handle
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a player can leash this MCA villager.
|
||||
*
|
||||
* @param player The player trying to leash
|
||||
* @param state The villager's IBondageState state (may be null)
|
||||
* @return true if leashing is allowed
|
||||
*/
|
||||
@Unique
|
||||
private boolean tiedup$canLeash(Player player, IBondageState state) {
|
||||
if (state == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Can leash if villager is tied up
|
||||
if (state.isTiedUp()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Can leash if player is a collar owner
|
||||
if (state.hasCollar()) {
|
||||
ItemStack collar = state.getEquipment(BodyRegionV2.NECK);
|
||||
if (collar.getItem() instanceof ItemCollar collarItem) {
|
||||
return collarItem.getOwners(collar).contains(player.getUUID());
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.tiedup.remake.mixin;
|
||||
|
||||
import com.tiedup.remake.compat.mca.MCACompat;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.items.base.ItemCollar;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/**
|
||||
* Mixin to allow vanilla leash attachment to MCA villagers when tied or collar owner.
|
||||
*
|
||||
* <p>By default, MCA villagers cannot be leashed. This mixin overrides
|
||||
* the canBeLeashed check to allow leashing when:
|
||||
* <ul>
|
||||
* <li>The villager is tied up (any bind), OR</li>
|
||||
* <li>The player is a collar owner</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Uses @Pseudo annotation - mixin is optional and will be skipped if MCA is not installed.
|
||||
*/
|
||||
@Pseudo
|
||||
@Mixin(targets = "forge.net.mca.entity.VillagerEntityMCA", remap = false)
|
||||
public class MixinMCAVillagerLeash {
|
||||
|
||||
/**
|
||||
* Override canBeLeashed to allow TiedUp leash mechanics.
|
||||
*
|
||||
* <p>The canBeLeashed method is a vanilla method (remapped), so we use remap = true.
|
||||
*
|
||||
* @param player The player trying to leash
|
||||
* @param cir Callback info for returning the result
|
||||
*/
|
||||
@Inject(
|
||||
method = "canBeLeashed",
|
||||
at = @At("HEAD"),
|
||||
cancellable = true,
|
||||
remap = true
|
||||
)
|
||||
private void tiedup$overrideCanBeLeashed(
|
||||
Player player,
|
||||
CallbackInfoReturnable<Boolean> cir
|
||||
) {
|
||||
LivingEntity entity = (LivingEntity) (Object) this;
|
||||
IBondageState state = MCACompat.getKidnappedState(entity);
|
||||
|
||||
// No TiedUp state - let vanilla/MCA handle it
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Already leashed - cannot leash again
|
||||
if (entity instanceof Mob mob && mob.isLeashed()) {
|
||||
cir.setReturnValue(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Can be leashed if tied up
|
||||
if (state.isTiedUp()) {
|
||||
cir.setReturnValue(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Can be leashed if player is collar owner
|
||||
if (state.hasCollar()) {
|
||||
ItemStack collar = state.getEquipment(BodyRegionV2.NECK);
|
||||
if (collar.getItem() instanceof ItemCollar collarItem) {
|
||||
if (collarItem.getOwners(collar).contains(player.getUUID())) {
|
||||
cir.setReturnValue(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default: let MCA handle it (usually returns false)
|
||||
}
|
||||
}
|
||||
473
src/main/java/com/tiedup/remake/mixin/MixinServerPlayer.java
Normal file
473
src/main/java/com/tiedup/remake/mixin/MixinServerPlayer.java
Normal file
@@ -0,0 +1,473 @@
|
||||
package com.tiedup.remake.mixin;
|
||||
|
||||
import com.tiedup.remake.entities.LeashProxyEntity;
|
||||
import com.tiedup.remake.network.ModNetwork;
|
||||
import com.tiedup.remake.network.sync.PacketSyncLeashProxy;
|
||||
import com.tiedup.remake.state.IPlayerLeashAccess;
|
||||
import net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.decoration.LeashFenceKnotEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
|
||||
/**
|
||||
* Mixin for ServerPlayer to add leash proxy functionality.
|
||||
*
|
||||
* This replaces the old EntityInvisibleSlaveTransporter mount-based system
|
||||
* with a proxy-based system where:
|
||||
* - The player does NOT ride an entity
|
||||
* - A LeashProxyEntity follows the player and holds the leash
|
||||
* - Traction is applied via push() when the player is too far from the holder
|
||||
*/
|
||||
@Mixin(ServerPlayer.class)
|
||||
public abstract class MixinServerPlayer implements IPlayerLeashAccess {
|
||||
|
||||
@Unique
|
||||
private final ServerPlayer tiedup$self = (ServerPlayer) (Object) this;
|
||||
|
||||
/** The proxy entity that follows this player and renders the leash */
|
||||
@Unique
|
||||
private LeashProxyEntity tiedup$leashProxy;
|
||||
|
||||
/** The entity holding this player's leash (master or fence knot) */
|
||||
@Unique
|
||||
private Entity tiedup$leashHolder;
|
||||
|
||||
/** Tick counter since last leash attachment (prevents immediate detach) */
|
||||
@Unique
|
||||
private int tiedup$leashAge;
|
||||
|
||||
/** Previous X position for stuck detection */
|
||||
@Unique
|
||||
private double tiedup$prevX;
|
||||
|
||||
/** Previous Z position for stuck detection */
|
||||
@Unique
|
||||
private double tiedup$prevZ;
|
||||
|
||||
/** Ticks spent stuck (not moving towards holder) */
|
||||
@Unique
|
||||
private int tiedup$leashStuckCounter;
|
||||
|
||||
/** Extra slack on leash - increases pull/max distances (for "pet leads" dogwalk) */
|
||||
@Unique
|
||||
private double tiedup$leashSlack = 0.0;
|
||||
|
||||
/** Tick counter for periodic leash proxy resync (for late-joining clients) */
|
||||
@Unique
|
||||
private int tiedup$leashResyncTimer = 0;
|
||||
|
||||
// ==================== Leash Constants ====================
|
||||
|
||||
/** Distance at which pull force starts (4 free blocks before any pull) */
|
||||
@Unique
|
||||
private static final double LEASH_PULL_START_DISTANCE = 4.0;
|
||||
|
||||
/** Maximum distance before instant teleport (6-block elastic zone) */
|
||||
@Unique
|
||||
private static final double LEASH_MAX_DISTANCE = 10.0;
|
||||
|
||||
/** Distance at which stuck detection activates (middle of elastic zone) */
|
||||
@Unique
|
||||
private static final double LEASH_TELEPORT_DISTANCE = 7.0;
|
||||
|
||||
/** Ticks of being stuck before safety teleport (2 seconds) */
|
||||
@Unique
|
||||
private static final int LEASH_STUCK_THRESHOLD = 40;
|
||||
|
||||
/** Maximum pull force cap */
|
||||
@Unique
|
||||
private static final double LEASH_MAX_FORCE = 0.14;
|
||||
|
||||
/** Force ramp per block beyond pull start */
|
||||
@Unique
|
||||
private static final double LEASH_FORCE_RAMP = 0.04;
|
||||
|
||||
/** Blend factor for pull vs momentum (0.6 = 60% pull, 40% momentum) */
|
||||
@Unique
|
||||
private static final double LEASH_BLEND_FACTOR = 0.6;
|
||||
|
||||
// ==================== IPlayerLeashAccess Implementation ====================
|
||||
|
||||
@Override
|
||||
public void tiedup$attachLeash(Entity holder) {
|
||||
if (holder == null) return;
|
||||
|
||||
tiedup$leashHolder = holder;
|
||||
|
||||
// Create proxy if not exists
|
||||
if (tiedup$leashProxy == null) {
|
||||
tiedup$leashProxy = new LeashProxyEntity(tiedup$self);
|
||||
tiedup$leashProxy.setPos(
|
||||
tiedup$self.getX(),
|
||||
tiedup$self.getY(),
|
||||
tiedup$self.getZ()
|
||||
);
|
||||
tiedup$self.level().addFreshEntity(tiedup$leashProxy);
|
||||
}
|
||||
|
||||
// Attach leash from proxy to holder
|
||||
tiedup$leashProxy.setLeashedTo(tiedup$leashHolder, true);
|
||||
tiedup$leashAge = tiedup$self.tickCount;
|
||||
tiedup$leashStuckCounter = 0;
|
||||
tiedup$leashResyncTimer = 0;
|
||||
|
||||
// Send sync packet to all tracking clients for smooth rendering
|
||||
PacketSyncLeashProxy packet = PacketSyncLeashProxy.attach(
|
||||
tiedup$self.getUUID(),
|
||||
tiedup$leashProxy.getId()
|
||||
);
|
||||
ModNetwork.sendToAllTrackingAndSelf(packet, tiedup$self);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tiedup$detachLeash() {
|
||||
tiedup$leashHolder = null;
|
||||
|
||||
if (tiedup$leashProxy != null) {
|
||||
if (
|
||||
tiedup$leashProxy.isAlive() &&
|
||||
!tiedup$leashProxy.proxyIsRemoved()
|
||||
) {
|
||||
tiedup$leashProxy.proxyRemove();
|
||||
}
|
||||
tiedup$leashProxy = null;
|
||||
|
||||
// Send detach packet to all tracking clients
|
||||
PacketSyncLeashProxy packet = PacketSyncLeashProxy.detach(
|
||||
tiedup$self.getUUID()
|
||||
);
|
||||
ModNetwork.sendToAllTrackingAndSelf(packet, tiedup$self);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tiedup$dropLeash() {
|
||||
// Don't drop if player is disconnected or dead (position may be invalid)
|
||||
if (tiedup$self.hasDisconnected() || !tiedup$self.isAlive()) {
|
||||
return;
|
||||
}
|
||||
tiedup$self.drop(new ItemStack(Items.LEAD), false, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tiedup$isLeashed() {
|
||||
return (
|
||||
tiedup$leashHolder != null &&
|
||||
tiedup$leashProxy != null &&
|
||||
!tiedup$leashProxy.proxyIsRemoved()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entity tiedup$getLeashHolder() {
|
||||
return tiedup$leashHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LeashProxyEntity tiedup$getLeashProxy() {
|
||||
return tiedup$leashProxy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tiedup$setLeashSlack(double slack) {
|
||||
this.tiedup$leashSlack = slack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double tiedup$getLeashSlack() {
|
||||
return this.tiedup$leashSlack;
|
||||
}
|
||||
|
||||
// ==================== Tick Update (called from Forge event) ====================
|
||||
|
||||
/**
|
||||
* Update leash state and apply traction if needed.
|
||||
* Called from LeashTickHandler via Forge TickEvent.
|
||||
*/
|
||||
@Override
|
||||
public void tiedup$tickLeash() {
|
||||
// Check if this player is still valid
|
||||
if (!tiedup$self.isAlive() || tiedup$self.hasDisconnected()) {
|
||||
tiedup$detachLeash();
|
||||
// Don't drop leash if we're disconnected (we won't be there to pick it up)
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if holder is still valid
|
||||
if (tiedup$leashHolder != null) {
|
||||
boolean holderInvalid =
|
||||
!tiedup$leashHolder.isAlive() || tiedup$leashHolder.isRemoved();
|
||||
|
||||
// If holder is a player, also check if they disconnected
|
||||
if (
|
||||
!holderInvalid &&
|
||||
tiedup$leashHolder instanceof ServerPlayer holderPlayer
|
||||
) {
|
||||
holderInvalid = holderPlayer.hasDisconnected();
|
||||
}
|
||||
|
||||
// If player is being used as vehicle, break leash
|
||||
if (!holderInvalid && tiedup$self.isVehicle()) {
|
||||
holderInvalid = true;
|
||||
}
|
||||
|
||||
if (holderInvalid) {
|
||||
tiedup$detachLeash();
|
||||
tiedup$dropLeash();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Sync proxy state with actual leash holder
|
||||
if (tiedup$leashProxy != null) {
|
||||
if (tiedup$leashProxy.proxyIsRemoved()) {
|
||||
tiedup$leashProxy = null;
|
||||
} else {
|
||||
Entity holderActual = tiedup$leashHolder;
|
||||
Entity holderFromProxy = tiedup$leashProxy.getLeashHolder();
|
||||
|
||||
// Leash was broken externally (by another player)
|
||||
if (holderFromProxy == null && holderActual != null) {
|
||||
tiedup$detachLeash();
|
||||
tiedup$dropLeash();
|
||||
return;
|
||||
}
|
||||
// Holder changed (shouldn't happen normally)
|
||||
else if (holderFromProxy != holderActual) {
|
||||
tiedup$leashHolder = holderFromProxy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic resync for late-joining clients (every 5 seconds)
|
||||
if (tiedup$leashProxy != null && ++tiedup$leashResyncTimer >= 100) {
|
||||
tiedup$leashResyncTimer = 0;
|
||||
PacketSyncLeashProxy packet = PacketSyncLeashProxy.attach(
|
||||
tiedup$self.getUUID(),
|
||||
tiedup$leashProxy.getId()
|
||||
);
|
||||
ModNetwork.sendToAllTrackingAndSelf(packet, tiedup$self);
|
||||
}
|
||||
|
||||
// Apply traction force
|
||||
tiedup$applyLeashPull();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply pull force towards the leash holder if player is too far.
|
||||
*
|
||||
* Uses normalized direction, progressive capped force, velocity blending,
|
||||
* and conditional sync to prevent jitter, oscillation, and runaway velocity.
|
||||
* Modeled after DamselAIController.tickLeashTraction().
|
||||
*/
|
||||
@Unique
|
||||
private void tiedup$applyLeashPull() {
|
||||
if (tiedup$leashHolder == null) return;
|
||||
|
||||
// Cross-dimension: detach leash cleanly instead of silently ignoring
|
||||
if (tiedup$leashHolder.level() != tiedup$self.level()) {
|
||||
tiedup$detachLeash();
|
||||
tiedup$dropLeash();
|
||||
return;
|
||||
}
|
||||
|
||||
float distance = tiedup$self.distanceTo(tiedup$leashHolder);
|
||||
|
||||
// Apply slack to effective distances (for "pet leads" dogwalk)
|
||||
double effectivePullStart =
|
||||
LEASH_PULL_START_DISTANCE + tiedup$leashSlack;
|
||||
double effectiveMaxDistance = LEASH_MAX_DISTANCE + tiedup$leashSlack;
|
||||
double effectiveTeleportDist =
|
||||
LEASH_TELEPORT_DISTANCE + tiedup$leashSlack;
|
||||
|
||||
// Close enough: no pull needed, reset stuck counter
|
||||
if (distance < effectivePullStart) {
|
||||
tiedup$leashStuckCounter = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Too far: teleport to holder instead of breaking
|
||||
if (distance > effectiveMaxDistance) {
|
||||
tiedup$teleportToSafePositionNearHolder();
|
||||
tiedup$leashStuckCounter = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Direction to holder
|
||||
double dx = tiedup$leashHolder.getX() - tiedup$self.getX();
|
||||
double dy = tiedup$leashHolder.getY() - tiedup$self.getY();
|
||||
double dz = tiedup$leashHolder.getZ() - tiedup$self.getZ();
|
||||
|
||||
// Normalized horizontal direction (replaces Math.signum bang-bang)
|
||||
double horizontalDist = Math.sqrt(dx * dx + dz * dz);
|
||||
double dirX = horizontalDist > 0.01 ? dx / horizontalDist : 0.0;
|
||||
double dirZ = horizontalDist > 0.01 ? dz / horizontalDist : 0.0;
|
||||
|
||||
// Calculate how much the player moved since last check
|
||||
double movedX = tiedup$self.getX() - tiedup$prevX;
|
||||
double movedZ = tiedup$self.getZ() - tiedup$prevZ;
|
||||
double movedHorizontal = Math.sqrt(movedX * movedX + movedZ * movedZ);
|
||||
|
||||
// Store current position for next stuck check
|
||||
tiedup$prevX = tiedup$self.getX();
|
||||
tiedup$prevZ = tiedup$self.getZ();
|
||||
|
||||
// Stuck detection - slack-aware threshold
|
||||
boolean isStuck =
|
||||
distance > effectiveTeleportDist &&
|
||||
tiedup$self.getDeltaMovement().lengthSqr() < 0.001 &&
|
||||
movedHorizontal < 0.05;
|
||||
|
||||
if (isStuck) {
|
||||
tiedup$leashStuckCounter++;
|
||||
|
||||
if (tiedup$leashStuckCounter >= LEASH_STUCK_THRESHOLD) {
|
||||
tiedup$teleportToSafePositionNearHolder();
|
||||
tiedup$leashStuckCounter = 0;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
tiedup$leashStuckCounter = 0;
|
||||
}
|
||||
|
||||
// Progressive capped force: 0.04 per block beyond pull start, max 0.14
|
||||
double distanceBeyond = distance - effectivePullStart;
|
||||
double forceFactor = Math.min(
|
||||
LEASH_MAX_FORCE,
|
||||
distanceBeyond * LEASH_FORCE_RAMP
|
||||
);
|
||||
|
||||
// Fence knots are static, need 1.3x pull
|
||||
if (tiedup$leashHolder instanceof LeashFenceKnotEntity) {
|
||||
forceFactor *= 1.3;
|
||||
}
|
||||
|
||||
// Velocity blending: 60% pull direction + 40% existing momentum
|
||||
net.minecraft.world.phys.Vec3 currentMotion =
|
||||
tiedup$self.getDeltaMovement();
|
||||
double pullVelX = dirX * forceFactor * 3.0;
|
||||
double pullVelZ = dirZ * forceFactor * 3.0;
|
||||
double newVelX =
|
||||
currentMotion.x * (1.0 - LEASH_BLEND_FACTOR) +
|
||||
pullVelX * LEASH_BLEND_FACTOR;
|
||||
double newVelZ =
|
||||
currentMotion.z * (1.0 - LEASH_BLEND_FACTOR) +
|
||||
pullVelZ * LEASH_BLEND_FACTOR;
|
||||
|
||||
// Soft auto-step (replaces 0.42 vanilla jump velocity)
|
||||
double newVelY = currentMotion.y;
|
||||
if (
|
||||
tiedup$self.onGround() &&
|
||||
movedHorizontal < 0.1 &&
|
||||
distanceBeyond > 0.5
|
||||
) {
|
||||
if (dy > 0.3) {
|
||||
newVelY += 0.08; // Holder is above: gentle upward boost
|
||||
} else {
|
||||
newVelY += 0.05; // Normal step-up
|
||||
}
|
||||
} else if (dy > 0.5 && !tiedup$self.onGround()) {
|
||||
newVelY += 0.02; // Gentle aerial drift
|
||||
}
|
||||
|
||||
tiedup$self.setDeltaMovement(newVelX, newVelY, newVelZ);
|
||||
|
||||
// Conditional velocity sync: only send packet when force is meaningful
|
||||
if (forceFactor > 0.02 && tiedup$self.connection != null) {
|
||||
tiedup$self.connection.send(
|
||||
new ClientboundSetEntityMotionPacket(tiedup$self)
|
||||
);
|
||||
// Only suppress impulse flag after we've actually synced to the client
|
||||
tiedup$self.hasImpulse = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Teleport player to a safe position near the leash holder.
|
||||
* Used when player is stuck and can't path to holder.
|
||||
*/
|
||||
@Unique
|
||||
private void tiedup$teleportToSafePositionNearHolder() {
|
||||
if (tiedup$leashHolder == null) return;
|
||||
|
||||
// Target: 2 blocks away from holder in player's direction
|
||||
double dx = tiedup$self.getX() - tiedup$leashHolder.getX();
|
||||
double dz = tiedup$self.getZ() - tiedup$leashHolder.getZ();
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
double offsetX = 0;
|
||||
double offsetZ = 0;
|
||||
if (dist > 0.1) {
|
||||
offsetX = (dx / dist) * 2.0;
|
||||
offsetZ = (dz / dist) * 2.0;
|
||||
}
|
||||
|
||||
double targetX = tiedup$leashHolder.getX() + offsetX;
|
||||
double targetZ = tiedup$leashHolder.getZ() + offsetZ;
|
||||
|
||||
// Find safe Y (ground level)
|
||||
double targetY = tiedup$findSafeY(
|
||||
targetX,
|
||||
tiedup$leashHolder.getY(),
|
||||
targetZ
|
||||
);
|
||||
|
||||
tiedup$self.teleportTo(targetX, targetY, targetZ);
|
||||
|
||||
// Sync position to client (null check for fake players)
|
||||
if (tiedup$self.connection != null) {
|
||||
tiedup$self.connection.send(
|
||||
new ClientboundSetEntityMotionPacket(tiedup$self)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a safe Y coordinate for teleporting.
|
||||
*
|
||||
* @param x Target X coordinate
|
||||
* @param startY Starting Y to search from
|
||||
* @param z Target Z coordinate
|
||||
* @return Safe Y coordinate on solid ground
|
||||
*/
|
||||
@Unique
|
||||
private double tiedup$findSafeY(double x, double startY, double z) {
|
||||
net.minecraft.core.BlockPos.MutableBlockPos mutable =
|
||||
new net.minecraft.core.BlockPos.MutableBlockPos();
|
||||
|
||||
// Search down first (max 5 blocks)
|
||||
for (int y = 0; y > -5; y--) {
|
||||
mutable.set((int) x, (int) startY + y, (int) z);
|
||||
if (
|
||||
tiedup$self
|
||||
.level()
|
||||
.getBlockState(mutable)
|
||||
.isSolidRender(tiedup$self.level(), mutable) &&
|
||||
tiedup$self.level().getBlockState(mutable.above()).isAir()
|
||||
) {
|
||||
return mutable.getY() + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Search up (max 5 blocks)
|
||||
for (int y = 1; y < 5; y++) {
|
||||
mutable.set((int) x, (int) startY + y, (int) z);
|
||||
if (
|
||||
tiedup$self
|
||||
.level()
|
||||
.getBlockState(mutable.below())
|
||||
.isSolidRender(tiedup$self.level(), mutable.below()) &&
|
||||
tiedup$self.level().getBlockState(mutable).isAir()
|
||||
) {
|
||||
return mutable.getY();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use holder's Y
|
||||
return startY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.tiedup.remake.mixin.client;
|
||||
|
||||
import com.tiedup.remake.items.base.ItemBind;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.items.base.PoseType;
|
||||
import com.tiedup.remake.state.HumanChairHelper;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/**
|
||||
* Mixin for Camera to lower first-person view when in DOG pose.
|
||||
*
|
||||
* In DOG pose, the player model is horizontal (like a dog), so the camera
|
||||
* should be lowered to match the eye level being closer to the ground.
|
||||
* Normal eye height is ~1.62 blocks, we lower it by ~0.6 blocks.
|
||||
*/
|
||||
@Mixin(Camera.class)
|
||||
public abstract class MixinCamera {
|
||||
|
||||
@Shadow
|
||||
private Vec3 position;
|
||||
|
||||
@Shadow
|
||||
protected abstract void setPosition(Vec3 pos);
|
||||
|
||||
@Inject(method = "setup", at = @At("TAIL"))
|
||||
private void tiedup$lowerCameraForDogPose(
|
||||
BlockGetter level,
|
||||
Entity entity,
|
||||
boolean detached,
|
||||
boolean thirdPersonReverse,
|
||||
float partialTick,
|
||||
CallbackInfo ci
|
||||
) {
|
||||
// Only affect first-person view
|
||||
if (detached) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(entity instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
|
||||
if (bind.isEmpty() || !(bind.getItem() instanceof ItemBind itemBind)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemBind.getPoseType() != PoseType.DOG) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Lower camera by 0.6 blocks to match the horizontal body position
|
||||
// Normal eye height is ~1.62, DOG pose should be around ~1.0
|
||||
setPosition(position.add(0, -0.6, 0));
|
||||
|
||||
// Human chair: move camera forward into the head
|
||||
if (HumanChairHelper.isActive(bind)) {
|
||||
float facing = HumanChairHelper.getFacing(bind);
|
||||
float facingRad = (float) Math.toRadians(facing);
|
||||
double fwdX = -Math.sin(facingRad) * 0.6;
|
||||
double fwdZ = Math.cos(facingRad) * 0.6;
|
||||
setPosition(position.add(fwdX, 0, fwdZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.tiedup.remake.mixin.client;
|
||||
|
||||
import com.tiedup.remake.client.state.PetBedClientState;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/**
|
||||
* Client-side mixin: prevent vanilla sleeping visuals (laying flat) for
|
||||
* players on a pet bed in SLEEP mode. Server-side isSleeping() is unaffected,
|
||||
* so night skip still works.
|
||||
*/
|
||||
@Mixin(LivingEntity.class)
|
||||
public abstract class MixinLivingEntitySleeping {
|
||||
|
||||
@Inject(method = "isSleeping", at = @At("HEAD"), cancellable = true)
|
||||
private void tiedup$hidePetBedSleeping(
|
||||
CallbackInfoReturnable<Boolean> cir
|
||||
) {
|
||||
LivingEntity self = (LivingEntity) (Object) this;
|
||||
if (self.level().isClientSide() && self instanceof Player player) {
|
||||
if (PetBedClientState.get(player.getUUID()) == 2) {
|
||||
cir.setReturnValue(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.tiedup.remake.mixin.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.tiedup.remake.compat.wildfire.WildfireCompat;
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/**
|
||||
* Mixin for MCA's PlayerEntityExtendedModel to hide breasts when Wildfire is loaded.
|
||||
*
|
||||
* <p>MCA adds breasts to players through PlayerEntityExtendedModel. When Wildfire
|
||||
* is also installed, we disable MCA's breasts to avoid double-rendering (Wildfire
|
||||
* renders its own breasts with physics).
|
||||
*
|
||||
* <p>Uses @Pseudo annotation - mixin is optional and will be skipped if MCA is not installed.
|
||||
*
|
||||
* <p>Target class: forge.net.mca.client.model.PlayerEntityExtendedModel
|
||||
*/
|
||||
@Pseudo
|
||||
@Mixin(
|
||||
targets = "forge.net.mca.client.model.PlayerEntityExtendedModel",
|
||||
remap = false
|
||||
)
|
||||
public class MixinMCAPlayerExtendedModel<T extends LivingEntity> {
|
||||
|
||||
/**
|
||||
* Shadow the breasts ModelPart to control visibility.
|
||||
*/
|
||||
@Shadow(remap = false)
|
||||
public ModelPart breasts;
|
||||
|
||||
/**
|
||||
* Shadow the breastsWear ModelPart (overlay layer).
|
||||
*/
|
||||
@Shadow(remap = false)
|
||||
public ModelPart breastsWear;
|
||||
|
||||
/**
|
||||
* Inject at the end of setAngles (m_6973_) to hide MCA breasts when Wildfire is loaded.
|
||||
*
|
||||
* <p>This runs after applyVillagerDimensions() which sets breast visibility.
|
||||
*/
|
||||
@Inject(method = "m_6973_", at = @At("TAIL"), remap = false)
|
||||
private void tiedup$hideBreastsInSetAngles(
|
||||
T entity,
|
||||
float limbSwing,
|
||||
float limbSwingAmount,
|
||||
float ageInTicks,
|
||||
float netHeadYaw,
|
||||
float headPitch,
|
||||
CallbackInfo ci
|
||||
) {
|
||||
tiedup$hideBreastsIfWildfire();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject at the end of copyVisibility to prevent it from re-enabling breasts.
|
||||
*
|
||||
* <p>MCA's copyVisibility sets breasts.visible = model.body.visible.
|
||||
*/
|
||||
@Inject(method = "copyVisibility", at = @At("TAIL"), remap = false)
|
||||
private void tiedup$hideBreastsAfterCopyVisibility(CallbackInfo ci) {
|
||||
tiedup$hideBreastsIfWildfire();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject at the end of render to hide breasts after breastsWear visibility is set.
|
||||
*
|
||||
* <p>MCA's render() sets breastsWear.visible = jacket.visible before rendering.
|
||||
*/
|
||||
@Inject(method = "m_7695_", at = @At("HEAD"), remap = false)
|
||||
private void tiedup$hideBreastsBeforeRender(
|
||||
PoseStack matrices,
|
||||
VertexConsumer vertices,
|
||||
int light,
|
||||
int overlay,
|
||||
float red,
|
||||
float green,
|
||||
float blue,
|
||||
float alpha,
|
||||
CallbackInfo ci
|
||||
) {
|
||||
tiedup$hideBreastsIfWildfire();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to hide both breast parts when Wildfire is loaded.
|
||||
*/
|
||||
private void tiedup$hideBreastsIfWildfire() {
|
||||
if (WildfireCompat.isLoaded()) {
|
||||
if (breasts != null) {
|
||||
breasts.visible = false;
|
||||
}
|
||||
if (breastsWear != null) {
|
||||
breastsWear.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.tiedup.remake.mixin.client;
|
||||
|
||||
import com.tiedup.remake.compat.mca.MCACompat;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/**
|
||||
* Mixin for MCA's SpeechManager to block TTS when villager is gagged.
|
||||
*
|
||||
* <p>MCA's speech system works via TTS (Text-to-Speech) on the client side:
|
||||
* <ol>
|
||||
* <li>Server sends VillagerMessage packet to client</li>
|
||||
* <li>ClientInteractionManagerImpl.handleVillagerMessage() receives it</li>
|
||||
* <li>SpeechManager.onChatMessage() is called</li>
|
||||
* <li>TTS plays the sound</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>This mixin intercepts onChatMessage and cancels it if the villager is gagged.
|
||||
*
|
||||
* <p>Uses @Pseudo for soft dependency - only applies if MCA is present.
|
||||
* CLIENT SIDE ONLY.
|
||||
*/
|
||||
@Pseudo
|
||||
@Mixin(targets = "forge.net.mca.client.tts.SpeechManager", remap = false)
|
||||
public class MixinMCASpeechManager {
|
||||
|
||||
/**
|
||||
* Inject at HEAD of onChatMessage to cancel TTS for gagged villagers.
|
||||
*
|
||||
* <p>MCA signature: void onChatMessage(Text text, UUID sender)
|
||||
* <p>Note: Text = net.minecraft.network.chat.Component (Yarn mapping)
|
||||
*/
|
||||
@Inject(
|
||||
method = "onChatMessage",
|
||||
at = @At("HEAD"),
|
||||
cancellable = true,
|
||||
require = 0
|
||||
)
|
||||
private void tiedup$cancelGaggedSpeech(
|
||||
Component text,
|
||||
UUID sender,
|
||||
CallbackInfo ci
|
||||
) {
|
||||
try {
|
||||
ClientLevel level = Minecraft.getInstance().level;
|
||||
if (level == null) return;
|
||||
|
||||
// Find entity by UUID in rendered entities
|
||||
for (Entity entity : level.entitiesForRendering()) {
|
||||
if (
|
||||
entity.getUUID().equals(sender) &&
|
||||
entity instanceof LivingEntity living
|
||||
) {
|
||||
IBondageState state = MCACompat.getKidnappedState(living);
|
||||
if (state != null && state.isGagged()) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MCA] Blocked TTS for gagged villager: {}",
|
||||
living.getName().getString()
|
||||
);
|
||||
ci.cancel();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MCA] TTS cancellation check failed: {}",
|
||||
e.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.tiedup.remake.mixin.client;
|
||||
|
||||
import com.tiedup.remake.client.animation.render.DogPoseRenderHandler;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.client.animation.util.DogPoseHelper;
|
||||
import com.tiedup.remake.items.base.ItemBind;
|
||||
import com.tiedup.remake.items.base.PoseType;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import net.minecraft.client.model.PlayerModel;
|
||||
import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/**
|
||||
* Mixin for PlayerModel to handle DOG pose head adjustments.
|
||||
*
|
||||
* When in DOG pose (body horizontal):
|
||||
* - Head pitch offset so player looks forward
|
||||
* - Head yaw converted to zRot (roll) since yRot axis is sideways when body is horizontal
|
||||
*/
|
||||
@Mixin(PlayerModel.class)
|
||||
public class MixinPlayerModel {
|
||||
|
||||
@Inject(method = "setupAnim", at = @At("TAIL"))
|
||||
private void tiedup$adjustDogPose(
|
||||
LivingEntity entity,
|
||||
float limbSwing,
|
||||
float limbSwingAmount,
|
||||
float ageInTicks,
|
||||
float netHeadYaw,
|
||||
float headPitch,
|
||||
CallbackInfo ci
|
||||
) {
|
||||
if (!(entity instanceof AbstractClientPlayer player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
|
||||
if (bind.isEmpty() || !(bind.getItem() instanceof ItemBind itemBind)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemBind.getPoseType() != PoseType.DOG) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerModel<?> model = (PlayerModel<?>) (Object) this;
|
||||
|
||||
// === HEAD ROTATION FOR HORIZONTAL BODY ===
|
||||
// Body is at -90° pitch (horizontal, face down)
|
||||
// We apply a rotation delta to the poseStack in PlayerArmHideEventHandler
|
||||
// The head needs to compensate for this transformation
|
||||
|
||||
float rotationDelta = DogPoseRenderHandler.getAppliedRotationDelta(
|
||||
player.getId()
|
||||
);
|
||||
boolean moving = DogPoseRenderHandler.isDogPoseMoving(player.getId());
|
||||
|
||||
// netHeadYaw is head relative to vanilla body (yHeadRot - yBodyRot)
|
||||
// We rotated the model by rotationDelta, so compensate:
|
||||
// effectiveHeadYaw = netHeadYaw + rotationDelta
|
||||
float headYaw = netHeadYaw + rotationDelta;
|
||||
|
||||
// Clamp based on movement state and apply head compensation
|
||||
float maxYaw = moving ? 60f : 90f;
|
||||
DogPoseHelper.applyHeadCompensationClamped(
|
||||
model.head,
|
||||
model.hat,
|
||||
headPitch,
|
||||
headYaw,
|
||||
maxYaw
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package com.tiedup.remake.mixin.client;
|
||||
|
||||
import com.tiedup.remake.client.animation.BondageAnimationManager;
|
||||
import com.tiedup.remake.client.animation.StaticPoseApplier;
|
||||
import com.tiedup.remake.client.animation.util.AnimationIdBuilder;
|
||||
import com.tiedup.remake.compat.mca.MCACompat;
|
||||
import com.tiedup.remake.items.base.ItemBind;
|
||||
import com.tiedup.remake.items.base.PoseType;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import dev.kosmx.playerAnim.impl.IAnimatedPlayer;
|
||||
import dev.kosmx.playerAnim.impl.animation.AnimationApplier;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.client.model.HumanoidModel;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/**
|
||||
* Mixin for MCA's VillagerEntityBaseModelMCA to apply tied poses.
|
||||
*
|
||||
* <p>This mixin injects at the end of setupAnim (m_6973_) to override MCA's default
|
||||
* animations when a villager is tied up. Without this, the bondage render layer
|
||||
* shows the tied pose but the underlying MCA model still shows normal walking/idle
|
||||
* animations.
|
||||
*
|
||||
* <p>Uses @Pseudo annotation - mixin is optional and will be skipped if MCA is not installed.
|
||||
*
|
||||
* <p>Target class: net.mca.client.model.VillagerEntityBaseModelMCA
|
||||
* <p>Target method: setupAnim (MCP name, remapped from Yarn by Architectury)
|
||||
*/
|
||||
@Pseudo
|
||||
@Mixin(
|
||||
targets = "forge.net.mca.client.model.VillagerEntityBaseModelMCA",
|
||||
remap = false
|
||||
)
|
||||
public class MixinVillagerEntityBaseModelMCA<T extends LivingEntity> {
|
||||
|
||||
// Note: Tick tracking moved to MCAAnimationTickCache for cleanup on world unload
|
||||
|
||||
/**
|
||||
* Inject at the end of setupAnim to apply tied pose after MCA has set its animations.
|
||||
*
|
||||
* <p>This completely overrides arm/leg positions when the villager is tied up.
|
||||
*
|
||||
* <p>Method signature: void setupAnim(T entity, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch)
|
||||
* <p>Note: MCA uses Architectury which remaps Yarn's method names to Forge/MCP names
|
||||
*/
|
||||
@Inject(method = "m_6973_", at = @At("TAIL"), remap = false)
|
||||
private void tiedup$applyTiedPose(
|
||||
T villager,
|
||||
float limbSwing,
|
||||
float limbSwingAmount,
|
||||
float ageInTicks,
|
||||
float netHeadYaw,
|
||||
float headPitch,
|
||||
CallbackInfo ci
|
||||
) {
|
||||
// Only process on client side
|
||||
if (villager.level() == null || !villager.level().isClientSide()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if MCA is loaded and this villager is tied
|
||||
if (!MCACompat.isMCALoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
IBondageState state = MCACompat.getKidnappedState(villager);
|
||||
if (state == null || !state.isTiedUp()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get pose info from bind item
|
||||
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
|
||||
PoseType poseType = PoseType.STANDARD;
|
||||
|
||||
if (bind.getItem() instanceof ItemBind itemBind) {
|
||||
poseType = itemBind.getPoseType();
|
||||
}
|
||||
|
||||
// Derive bound state from V2 regions, fallback to V1 bind mode NBT
|
||||
boolean armsBound = V2EquipmentHelper.isRegionOccupied(villager, BodyRegionV2.ARMS);
|
||||
boolean legsBound = V2EquipmentHelper.isRegionOccupied(villager, BodyRegionV2.LEGS);
|
||||
|
||||
if (!armsBound && !legsBound && bind.getItem() instanceof ItemBind) {
|
||||
armsBound = ItemBind.hasArmsBound(bind);
|
||||
legsBound = ItemBind.hasLegsBound(bind);
|
||||
}
|
||||
|
||||
// MCA doesn't track struggling state - use false for now
|
||||
// TODO: Add struggling support to MCA integration
|
||||
boolean isStruggling = false;
|
||||
|
||||
// Cast this mixin to HumanoidModel to apply pose
|
||||
// MCA's VillagerEntityBaseModelMCA extends HumanoidModel
|
||||
@SuppressWarnings("unchecked")
|
||||
HumanoidModel<?> model = (HumanoidModel<?>) (Object) this;
|
||||
|
||||
// Check if villager supports PlayerAnimator (via our mixin)
|
||||
if (villager instanceof IAnimatedPlayer animated) {
|
||||
// Build animation ID and play animation
|
||||
String animId = AnimationIdBuilder.build(
|
||||
poseType,
|
||||
armsBound,
|
||||
legsBound,
|
||||
null,
|
||||
isStruggling,
|
||||
true
|
||||
);
|
||||
BondageAnimationManager.playAnimation(villager, animId);
|
||||
|
||||
// Tick the animation stack only once per game tick (not every render frame)
|
||||
// ageInTicks increments by 1 each game tick, with fractional values between ticks
|
||||
int currentTick = (int) ageInTicks;
|
||||
UUID entityId = villager.getUUID();
|
||||
int lastTick =
|
||||
com.tiedup.remake.client.animation.tick.MCAAnimationTickCache.getLastTick(
|
||||
entityId
|
||||
);
|
||||
|
||||
if (lastTick != currentTick) {
|
||||
// New game tick - tick the animation
|
||||
animated.getAnimationStack().tick();
|
||||
com.tiedup.remake.client.animation.tick.MCAAnimationTickCache.setLastTick(
|
||||
entityId,
|
||||
currentTick
|
||||
);
|
||||
}
|
||||
|
||||
// Apply animation transforms to model parts
|
||||
AnimationApplier emote = animated.playerAnimator_getAnimation();
|
||||
if (emote != null && emote.isActive()) {
|
||||
// Use correct PlayerAnimator part names (torso, not body)
|
||||
emote.updatePart("head", model.head);
|
||||
emote.updatePart("torso", model.body);
|
||||
emote.updatePart("leftArm", model.leftArm);
|
||||
emote.updatePart("rightArm", model.rightArm);
|
||||
emote.updatePart("leftLeg", model.leftLeg);
|
||||
emote.updatePart("rightLeg", model.rightLeg);
|
||||
|
||||
// Force rotations using setRotation to ensure they're applied
|
||||
model.rightArm.setRotation(
|
||||
model.rightArm.xRot,
|
||||
model.rightArm.yRot,
|
||||
model.rightArm.zRot
|
||||
);
|
||||
model.leftArm.setRotation(
|
||||
model.leftArm.xRot,
|
||||
model.leftArm.yRot,
|
||||
model.leftArm.zRot
|
||||
);
|
||||
model.rightLeg.setRotation(
|
||||
model.rightLeg.xRot,
|
||||
model.rightLeg.yRot,
|
||||
model.rightLeg.zRot
|
||||
);
|
||||
model.leftLeg.setRotation(
|
||||
model.leftLeg.xRot,
|
||||
model.leftLeg.yRot,
|
||||
model.leftLeg.zRot
|
||||
);
|
||||
model.body.setRotation(
|
||||
model.body.xRot,
|
||||
model.body.yRot,
|
||||
model.body.zRot
|
||||
);
|
||||
model.head.setRotation(
|
||||
model.head.xRot,
|
||||
model.head.yRot,
|
||||
model.head.zRot
|
||||
);
|
||||
} else {
|
||||
// Fallback to static poses if animation not active
|
||||
StaticPoseApplier.applyStaticPose(
|
||||
model,
|
||||
poseType,
|
||||
armsBound,
|
||||
legsBound
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Fallback: entity doesn't support PlayerAnimator, use static poses
|
||||
StaticPoseApplier.applyStaticPose(model, poseType, armsBound, legsBound);
|
||||
}
|
||||
|
||||
// Hide arms for WRAP/LATEX_SACK poses (like DamselModel does)
|
||||
if (poseType == PoseType.WRAP || poseType == PoseType.LATEX_SACK) {
|
||||
model.leftArm.visible = false;
|
||||
model.rightArm.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.tiedup.remake.mixin.client;
|
||||
|
||||
import dev.kosmx.playerAnim.api.layered.AnimationStack;
|
||||
import dev.kosmx.playerAnim.api.layered.IAnimation;
|
||||
import dev.kosmx.playerAnim.impl.IAnimatedPlayer;
|
||||
import dev.kosmx.playerAnim.impl.animation.AnimationApplier;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Pseudo;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/**
|
||||
* Mixin to inject IAnimatedPlayer support into MCA villagers.
|
||||
*
|
||||
* <p>This allows MCA villagers to use PlayerAnimator animations for bondage poses,
|
||||
* instead of just static pose rotations.
|
||||
*
|
||||
* <p>Uses @Pseudo annotation - mixin is optional and will be skipped if MCA is not installed.
|
||||
*
|
||||
* <p>Target class: net.mca.entity.VillagerEntityMCA
|
||||
*/
|
||||
@Pseudo
|
||||
@Mixin(targets = "forge.net.mca.entity.VillagerEntityMCA", remap = false)
|
||||
public abstract class MixinVillagerEntityMCAAnimated
|
||||
implements IAnimatedPlayer
|
||||
{
|
||||
|
||||
/**
|
||||
* Animation stack for layered animations.
|
||||
*/
|
||||
@Unique
|
||||
private AnimationStack tiedup$animationStack;
|
||||
|
||||
/**
|
||||
* Animation applier for applying animations to model parts.
|
||||
*/
|
||||
@Unique
|
||||
private AnimationApplier tiedup$animationApplier;
|
||||
|
||||
/**
|
||||
* Storage for named animations.
|
||||
*/
|
||||
@Unique
|
||||
private final Map<ResourceLocation, IAnimation> tiedup$storedAnimations =
|
||||
new HashMap<>();
|
||||
|
||||
/**
|
||||
* Track if animation system has been initialized.
|
||||
*/
|
||||
@Unique
|
||||
private boolean tiedup$animInitialized = false;
|
||||
|
||||
/**
|
||||
* Initialize animation system after entity construction.
|
||||
*/
|
||||
@Inject(method = "<init>*", at = @At("RETURN"), remap = false)
|
||||
private void tiedup$initAnimations(CallbackInfo ci) {
|
||||
tiedup$ensureAnimationInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy initialization of animation system.
|
||||
* Called on first access to ensure system is ready.
|
||||
* Only initializes on CLIENT side!
|
||||
*/
|
||||
@Unique
|
||||
private void tiedup$ensureAnimationInit() {
|
||||
if (!tiedup$animInitialized) {
|
||||
// Only create animation stack on client side
|
||||
net.minecraft.world.entity.LivingEntity self =
|
||||
(net.minecraft.world.entity.LivingEntity) (Object) this;
|
||||
if (self.level() == null || !self.level().isClientSide()) {
|
||||
return; // Don't initialize on server
|
||||
}
|
||||
|
||||
this.tiedup$animationStack = new AnimationStack();
|
||||
this.tiedup$animationApplier = new AnimationApplier(
|
||||
this.tiedup$animationStack
|
||||
);
|
||||
tiedup$animInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// IAnimatedPlayer Implementation
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
public AnimationStack getAnimationStack() {
|
||||
tiedup$ensureAnimationInit();
|
||||
return this.tiedup$animationStack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AnimationApplier playerAnimator_getAnimation() {
|
||||
tiedup$ensureAnimationInit();
|
||||
return this.tiedup$animationApplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IAnimation playerAnimator_getAnimation(ResourceLocation id) {
|
||||
return this.tiedup$storedAnimations.get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IAnimation playerAnimator_setAnimation(
|
||||
ResourceLocation id,
|
||||
IAnimation animation
|
||||
) {
|
||||
if (animation == null) {
|
||||
return this.tiedup$storedAnimations.remove(id);
|
||||
} else {
|
||||
return this.tiedup$storedAnimations.put(id, animation);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user