package com.tiedup.remake.minigame; import com.tiedup.remake.core.TiedUpMod; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import org.jetbrains.annotations.Nullable; /** * Manages lockpick mini-game sessions. * *

Extracted from {@link MiniGameSessionManager} (M15 split) to give lockpick * sessions their own focused manager. Singleton, thread-safe via ConcurrentHashMap. */ public class LockpickSessionManager { private static final LockpickSessionManager INSTANCE = new LockpickSessionManager(); /** * Active lockpick mini-game sessions by player UUID */ private final Map lockpickSessions = new ConcurrentHashMap<>(); private LockpickSessionManager() {} public static LockpickSessionManager getInstance() { return INSTANCE; } /** * Start a new lockpick session for a player. * If player already has an active session, it will be replaced (handles ESC cancel case). * * @param player The server player * @param targetSlot The bondage slot being picked * @param sweetSpotWidth The width of the sweet spot (based on tool) * @return The new session */ public LockpickMiniGameState startLockpickSession( ServerPlayer player, int targetSlot, float sweetSpotWidth ) { UUID playerId = player.getUUID(); // Check for existing session - remove it (handles ESC cancel case) LockpickMiniGameState existing = lockpickSessions.get(playerId); if (existing != null) { TiedUpMod.LOGGER.debug( "[LockpickSessionManager] Replacing existing lockpick session for {}", player.getName().getString() ); lockpickSessions.remove(playerId); } // Create new session LockpickMiniGameState session = new LockpickMiniGameState( playerId, targetSlot, sweetSpotWidth ); lockpickSessions.put(playerId, session); TiedUpMod.LOGGER.info( "[LockpickSessionManager] Started lockpick session {} for {} (slot: {}, width: {}%)", session.getSessionId().toString().substring(0, 8), player.getName().getString(), targetSlot, (int) (sweetSpotWidth * 100) ); // Notify nearby guards about lockpicking attempt GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player); return session; } /** * Play lockpick attempt sound and notify guards. * Called when player attempts to pick (tests position). * * @param player The player attempting to pick */ public void onLockpickAttempt(ServerPlayer player) { // Play metallic clicking sound player .serverLevel() .playSound( null, player.getX(), player.getY(), player.getZ(), SoundEvents.CHAIN_HIT, SoundSource.PLAYERS, 0.6f, 1.2f + player.getRandom().nextFloat() * 0.3f ); // Notify guards GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player); } /** * Get active lockpick session for a player. * * @param playerId The player UUID * @return The session, or null if none active */ @Nullable public LockpickMiniGameState getLockpickSession(UUID playerId) { LockpickMiniGameState session = lockpickSessions.get(playerId); if (session != null && session.isExpired()) { lockpickSessions.remove(playerId); return null; } return session; } /** * Validate a lockpick session. * * @param playerId The player UUID * @param sessionId The session UUID to validate * @return true if session is valid and active */ public boolean validateLockpickSession(UUID playerId, UUID sessionId) { LockpickMiniGameState session = getLockpickSession(playerId); if (session == null) { return false; } return session.getSessionId().equals(sessionId); } /** * End a lockpick session. * * @param playerId The player UUID * @param success Whether the session ended in success */ public void endLockpickSession(UUID playerId, boolean success) { LockpickMiniGameState session = lockpickSessions.remove(playerId); if (session != null) { TiedUpMod.LOGGER.info( "[LockpickSessionManager] Ended lockpick session for player {} (success: {})", playerId.toString().substring(0, 8), success ); } } /** * Clean up all lockpick sessions for a player (called on disconnect). * * @param playerId The player UUID */ public void cleanupPlayer(UUID playerId) { lockpickSessions.remove(playerId); } /** * Periodic cleanup of expired lockpick sessions. * Should be called from server tick handler. */ public void tickCleanup(long currentTick) { // Only run every 100 ticks (5 seconds) if (currentTick % 100 != 0) { return; } lockpickSessions .entrySet() .removeIf(entry -> entry.getValue().isExpired()); } /** * Get count of active lockpick sessions (for debugging). */ public int getActiveSessionCount() { return lockpickSessions.size(); } }