package com.tiedup.remake.client; import com.mojang.blaze3d.platform.InputConstants; import com.tiedup.remake.client.gui.screens.AdjustmentScreen; import com.tiedup.remake.client.gui.screens.UnifiedBondageScreen; import com.tiedup.remake.core.ModConfig; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.items.base.ILockable; import com.tiedup.remake.items.base.ItemCollar; import com.tiedup.remake.network.ModNetwork; import com.tiedup.remake.network.action.PacketForceSeatModifier; import com.tiedup.remake.network.action.PacketStruggle; import com.tiedup.remake.network.action.PacketTighten; import com.tiedup.remake.network.bounty.PacketRequestBounties; import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; import com.tiedup.remake.v2.bondage.network.PacketV2StruggleStart; import net.minecraft.ChatFormatting; import net.minecraft.client.KeyMapping; import net.minecraft.client.Minecraft; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.client.event.RegisterKeyMappingsEvent; import net.minecraftforge.event.TickEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; import org.jetbrains.annotations.Nullable; /** * * Manages key mappings and sends packets to server when keys are pressed. * * Based on original KeyBindings from 1.12.2 */ @Mod.EventBusSubscriber( modid = TiedUpMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT ) public class ModKeybindings { /** * Key category for TiedUp keybindings */ private static final String CATEGORY = "key.categories.tiedup"; /** * Struggle keybinding - Press to struggle against binds * Default: R key */ public static final KeyMapping STRUGGLE_KEY = new KeyMapping( "key.tiedup.struggle", // Translation key InputConstants.Type.KEYSYM, InputConstants.KEY_R, // Default key: R CATEGORY ); /** * Adjustment screen keybinding - Open item adjustment screen * Default: K key */ public static final KeyMapping ADJUSTMENT_KEY = new KeyMapping( "key.tiedup.adjustment_screen", InputConstants.Type.KEYSYM, InputConstants.KEY_K, // Default key: K CATEGORY ); /** * Bondage inventory keybinding - Open bondage inventory screen * Default: J key */ public static final KeyMapping INVENTORY_KEY = new KeyMapping( "key.tiedup.bondage_inventory", InputConstants.Type.KEYSYM, InputConstants.KEY_J, // Default key: J CATEGORY ); /** * Slave management keybinding - Open slave management dashboard * Default: L key */ public static final KeyMapping SLAVE_MANAGEMENT_KEY = new KeyMapping( "key.tiedup.slave_management", InputConstants.Type.KEYSYM, InputConstants.KEY_L, // Default key: L CATEGORY ); /** * Bounty list keybinding - Open bounty list screen * Default: B key */ public static final KeyMapping BOUNTY_KEY = new KeyMapping( "key.tiedup.bounties", InputConstants.Type.KEYSYM, InputConstants.KEY_B, // Default key: B CATEGORY ); /** * Force seat keybinding - Hold to force captive on/off vehicles * Default: Left ALT key */ public static final KeyMapping FORCE_SEAT_KEY = new KeyMapping( "key.tiedup.force_seat", InputConstants.Type.KEYSYM, InputConstants.KEY_LALT, // Default key: Left ALT CATEGORY ); /** * Tighten bind keybinding - Tighten binds on looked-at target * Default: T key */ public static final KeyMapping TIGHTEN_KEY = new KeyMapping( "key.tiedup.tighten", InputConstants.Type.KEYSYM, InputConstants.KEY_T, // Default key: T CATEGORY ); /** Track last sent state to avoid spamming packets */ private static boolean lastForceSeatState = false; /** * Check if Force Seat key is currently pressed. */ public static boolean isForceSeatPressed() { return FORCE_SEAT_KEY.isDown(); } /** * Register keybindings. * Called during mod initialization (MOD bus). * * @param event The registration event */ public static void register(RegisterKeyMappingsEvent event) { event.register(STRUGGLE_KEY); event.register(ADJUSTMENT_KEY); event.register(INVENTORY_KEY); event.register(SLAVE_MANAGEMENT_KEY); event.register(BOUNTY_KEY); event.register(FORCE_SEAT_KEY); event.register(TIGHTEN_KEY); TiedUpMod.LOGGER.info("Registered {} keybindings", 7); } // ==================== STRUGGLE MINI-GAME (uses vanilla movement keys) ==================== /** * Get the vanilla movement keybind for a given direction index. * Uses Minecraft's movement keys so AZERTY/QWERTY is already configured. * @param index 0=FORWARD, 1=LEFT, 2=BACK, 3=RIGHT * @return The keybind or null if invalid index */ public static KeyMapping getStruggleDirectionKey(int index) { Minecraft mc = Minecraft.getInstance(); if (mc.options == null) return null; return switch (index) { case 0 -> mc.options.keyUp; // Forward (W/Z) case 1 -> mc.options.keyLeft; // Strafe Left (A/Q) case 2 -> mc.options.keyDown; // Back (S) case 3 -> mc.options.keyRight; // Strafe Right (D) default -> null; }; } /** * Check if a keycode matches any vanilla movement keybind. * @param keyCode The GLFW key code * @return The direction index (0-3) or -1 if not a movement key */ public static int getStruggleDirectionFromKeyCode(int keyCode) { Minecraft mc = Minecraft.getInstance(); if (mc.options == null) return -1; if (mc.options.keyUp.matches(keyCode, 0)) return 0; if (mc.options.keyLeft.matches(keyCode, 0)) return 1; if (mc.options.keyDown.matches(keyCode, 0)) return 2; if (mc.options.keyRight.matches(keyCode, 0)) return 3; return -1; } /** * Get the display name of a vanilla movement key. * Shows the actual bound key (W for QWERTY, Z for AZERTY, etc.) * @param index 0=FORWARD, 1=LEFT, 2=BACK, 3=RIGHT * @return The key's display name */ public static String getStruggleDirectionKeyName(int index) { KeyMapping key = getStruggleDirectionKey(index); if (key == null) return "?"; return key.getTranslatedKeyMessage().getString().toUpperCase(); } /** * Handle key presses on client tick. * Called every client tick (FORGE bus). * * @param event The tick event */ @SubscribeEvent public static void onClientTick(TickEvent.ClientTickEvent event) { // Only run at end of tick if (event.phase != TickEvent.Phase.END) { return; } Minecraft mc = Minecraft.getInstance(); if (mc.player == null || mc.level == null) { return; } // Sync Force Seat keybind state to server (only send on change) boolean currentForceSeatState = isForceSeatPressed(); if (currentForceSeatState != lastForceSeatState) { lastForceSeatState = currentForceSeatState; ModNetwork.sendToServer( new PacketForceSeatModifier(currentForceSeatState) ); } // Check struggle key - Flow based on bind/accessories while (STRUGGLE_KEY.consumeClick()) { handleStruggleKey(); } // Check adjustment screen key while (ADJUSTMENT_KEY.consumeClick()) { // Only open if not already in a screen and player has adjustable items if (mc.screen == null && AdjustmentScreen.canOpen()) { mc.setScreen(new AdjustmentScreen()); TiedUpMod.LOGGER.debug( "[CLIENT] Adjustment key pressed - opening screen" ); } } // Check bondage inventory key - opens UnifiedBondageScreen in SELF or MASTER mode while (INVENTORY_KEY.consumeClick()) { if (mc.screen == null) { LivingEntity masterTarget = findOwnedCollarTarget(mc.player); if (masterTarget != null) { mc.setScreen(new UnifiedBondageScreen(masterTarget)); } else { mc.setScreen(new UnifiedBondageScreen()); } } } // SLAVE_MANAGEMENT_KEY: now handled by [J] with master mode detection (see above) while (SLAVE_MANAGEMENT_KEY.consumeClick()) { // consumed but no-op — kept registered to avoid key conflict during transition } // Check bounty list key while (BOUNTY_KEY.consumeClick()) { // Request bounty list from server (server will open the screen) if (mc.screen == null) { ModNetwork.sendToServer(new PacketRequestBounties()); TiedUpMod.LOGGER.debug( "[CLIENT] Bounty key pressed - requesting bounty list" ); } } // Check tighten key while (TIGHTEN_KEY.consumeClick()) { // Send tighten packet to server (server finds target) if (mc.screen == null) { ModNetwork.sendToServer(new PacketTighten()); TiedUpMod.LOGGER.debug( "[CLIENT] Tighten key pressed - sending tighten request" ); } } } /** * * Flow: * 1. If bind equipped: Send PacketStruggle to server (struggle against bind) * 2. If no bind: Check for locked accessories * - If locked accessories exist: Open StruggleChoiceScreen * - If no locked accessories: Show "Nothing to struggle" message */ private static void handleStruggleKey() { Minecraft mc = Minecraft.getInstance(); Player player = mc.player; if (player == null || mc.screen != null) { return; } // V2 path: check if player has V2 equipment to struggle against if ( com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.hasAnyEquipment( player ) ) { handleV2Struggle(player); return; } PlayerBindState state = PlayerBindState.getInstance(player); if (state == null) { return; } // Check if player has bind equipped if (state.isTiedUp()) { // Has bind - struggle against it if (ModConfig.SERVER.struggleMiniGameEnabled.get()) { // New: Start struggle mini-game ModNetwork.sendToServer( new PacketV2StruggleStart(BodyRegionV2.ARMS) ); TiedUpMod.LOGGER.debug( "[CLIENT] Struggle key pressed - starting V2 struggle mini-game" ); } else { // Legacy: Probability-based struggle ModNetwork.sendToServer(new PacketStruggle()); TiedUpMod.LOGGER.debug( "[CLIENT] Struggle key pressed - legacy struggle against bind" ); } return; } // No bind - check for locked accessories boolean hasLockedAccessories = hasAnyLockedAccessory(player); if (hasLockedAccessories) { // Open UnifiedBondageScreen in self mode mc.setScreen(new UnifiedBondageScreen()); TiedUpMod.LOGGER.debug( "[CLIENT] Struggle key pressed - opening unified bondage screen" ); } else { // No locked accessories - show message player.displayClientMessage( Component.translatable("tiedup.struggle.nothing").withStyle( ChatFormatting.GRAY ), true ); TiedUpMod.LOGGER.debug( "[CLIENT] Struggle key pressed - nothing to struggle" ); } } /** * Handle struggle key for V2 equipment. * Auto-targets the highest posePriority item. */ private static void handleV2Struggle(Player player) { java.util.Map equipped = com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.getAllEquipped( player ); if (equipped.isEmpty()) return; // Auto-target: find highest posePriority item com.tiedup.remake.v2.BodyRegionV2 bestRegion = null; int bestPriority = Integer.MIN_VALUE; for (java.util.Map.Entry< com.tiedup.remake.v2.BodyRegionV2, ItemStack > entry : equipped.entrySet()) { ItemStack stack = entry.getValue(); if ( stack.getItem() instanceof com.tiedup.remake.v2.bondage.IV2BondageItem item ) { if (item.getPosePriority(stack) > bestPriority) { bestPriority = item.getPosePriority(stack); bestRegion = entry.getKey(); } } } if (bestRegion != null) { ModNetwork.sendToServer( new com.tiedup.remake.v2.bondage.network.PacketV2StruggleStart( bestRegion ) ); TiedUpMod.LOGGER.debug( "[CLIENT] V2 Struggle key pressed - targeting region {}", bestRegion.name() ); } } /** * Check the crosshair entity: if it is a LivingEntity wearing a collar owned by the player, * return it as the MASTER mode target. Returns null if no valid target. */ @Nullable private static LivingEntity findOwnedCollarTarget(Player player) { if (player == null) return null; Minecraft mc = Minecraft.getInstance(); net.minecraft.world.entity.Entity crosshair = mc.crosshairPickEntity; if (crosshair instanceof LivingEntity living) { return checkCollarOwnership(living, player) ? living : null; } return null; } /** * Returns true if the given entity has a collar in the NECK region that lists the player as an owner. */ private static boolean checkCollarOwnership( LivingEntity target, Player player ) { ItemStack collarStack = com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.getInRegion( target, BodyRegionV2.NECK ); if ( !collarStack.isEmpty() && collarStack.getItem() instanceof ItemCollar collar ) { return collar.isOwner(collarStack, player); } return false; } /** * Check if player has any locked accessories. */ private static boolean hasAnyLockedAccessory(Player player) { BodyRegionV2[] accessoryRegions = { BodyRegionV2.MOUTH, BodyRegionV2.EYES, BodyRegionV2.EARS, BodyRegionV2.NECK, BodyRegionV2.TORSO, BodyRegionV2.HANDS, }; for (BodyRegionV2 region : accessoryRegions) { ItemStack stack = V2EquipmentHelper.getInRegion(player, region); if ( !stack.isEmpty() && stack.getItem() instanceof ILockable lockable ) { if (lockable.isLocked(stack)) { return true; } } } return false; } }