package com.tiedup.remake.client.events; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.items.ModItems; import com.tiedup.remake.items.base.BindVariant; import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.util.KidnappedHelper; import com.tiedup.remake.v2.BodyRegionV2; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import net.minecraft.client.Minecraft; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec3; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import net.minecraftforge.event.TickEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; /** * Client-side handler for smooth leash proxy positioning. * * FIX: Changed from RenderLevelStageEvent.AFTER_ENTITIES to ClientTickEvent. * AFTER_ENTITIES positioned the proxy AFTER rendering, causing 1-frame lag. * ClientTickEvent positions BEFORE rendering for smooth leash display. * * Instead of waiting for server position updates (which causes lag), * this handler repositions the proxy entity locally each tick based * on the player's current position. */ @OnlyIn(Dist.CLIENT) @Mod.EventBusSubscriber( modid = TiedUpMod.MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.FORGE ) public class LeashProxyClientHandler { /** * Map of player UUID -> proxy entity ID. * Uses UUID for player (persistent) and entity ID for proxy (runtime). */ private static final Map playerToProxy = new ConcurrentHashMap<>(); /** Default Y offset for normal standing pose (neck height) */ private static final double DEFAULT_Y_OFFSET = 1.3; /** Y offset for dogwalk pose (back/hip level) */ private static final double DOGWALK_Y_OFFSET = 0.35; /** * Handle sync packet from server. * Called when a player gets leashed or unleashed. */ public static void handleSyncPacket( UUID targetPlayerUUID, int proxyId, boolean attach ) { if (attach) { playerToProxy.put(targetPlayerUUID, proxyId); TiedUpMod.LOGGER.debug( "[LeashProxyClient] Registered proxy {} for player {}", proxyId, targetPlayerUUID ); } else { playerToProxy.remove(targetPlayerUUID); TiedUpMod.LOGGER.debug( "[LeashProxyClient] Removed proxy for player {}", targetPlayerUUID ); } } /** * FIX: Use ClientTickEvent instead of RenderLevelStageEvent.AFTER_ENTITIES. * This positions proxies BEFORE rendering, eliminating the 1-frame lag * that caused jittery leash rendering. */ @SubscribeEvent public static void onClientTick(TickEvent.ClientTickEvent event) { // Only run at end of tick (after player position is updated) if (event.phase != TickEvent.Phase.END) { return; } if (playerToProxy.isEmpty()) { return; } Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.level == null || mc.isPaused()) { return; } Level level = mc.level; // Reposition each tracked proxy for (Map.Entry entry : playerToProxy.entrySet()) { UUID playerUUID = entry.getKey(); int proxyId = entry.getValue(); Player playerEntity = level.getPlayerByUUID(playerUUID); Entity proxyEntity = level.getEntity(proxyId); if (playerEntity != null && proxyEntity != null) { // FIX: Calculate Y offset based on bind type (dogwalk vs normal) double yOffset = calculateYOffset(playerEntity); // Use current position (interpolation will be handled by renderer) double x = playerEntity.getX(); double y = playerEntity.getY() + yOffset; double z = playerEntity.getZ() - 0.15; // Set proxy position proxyEntity.setPos(x, y, z); // Update old positions for smooth interpolation proxyEntity.xOld = proxyEntity.xo = x; proxyEntity.yOld = proxyEntity.yo = y; proxyEntity.zOld = proxyEntity.zo = z; } } } /** * Calculate Y offset based on player's bind type. * Dogwalk (DOGBINDER) uses lower offset for 4-legged pose. */ private static double calculateYOffset(Player player) { IBondageState state = KidnappedHelper.getKidnappedState(player); if (state != null && state.isTiedUp()) { ItemStack bind = state.getEquipment(BodyRegionV2.ARMS); if ( !bind.isEmpty() && bind.getItem() == ModItems.getBind(BindVariant.DOGBINDER) ) { return DOGWALK_Y_OFFSET; } } return DEFAULT_Y_OFFSET; } /** * Clear all tracked proxies. * Called when disconnecting from server. */ public static void clearAll() { playerToProxy.clear(); } }