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,842 @@
|
||||
package com.tiedup.remake.minigame;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.items.ItemShockCollar;
|
||||
import com.tiedup.remake.items.base.ItemBind;
|
||||
import com.tiedup.remake.items.base.ItemCollar;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||
import com.tiedup.remake.minigame.ContinuousStruggleMiniGameState.TickResult;
|
||||
import com.tiedup.remake.minigame.ContinuousStruggleMiniGameState.UpdateType;
|
||||
import com.tiedup.remake.network.ModNetwork;
|
||||
import com.tiedup.remake.network.minigame.PacketContinuousStruggleState;
|
||||
import com.tiedup.remake.network.sync.SyncManager;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Manages continuous struggle mini-game sessions.
|
||||
*
|
||||
* <p>Extracted from {@link MiniGameSessionManager} (M15 split). Handles all
|
||||
* struggle variants: bind struggle, accessory struggle, V2 region struggle,
|
||||
* and furniture escape struggle.
|
||||
*
|
||||
* <p>Singleton, thread-safe via ConcurrentHashMap.
|
||||
*/
|
||||
public class StruggleSessionManager {
|
||||
|
||||
private static final StruggleSessionManager INSTANCE =
|
||||
new StruggleSessionManager();
|
||||
|
||||
/**
|
||||
* Active continuous struggle mini-game sessions by player UUID
|
||||
*/
|
||||
private final Map<
|
||||
UUID,
|
||||
ContinuousStruggleMiniGameState
|
||||
> continuousSessions = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Mapping from legacy V1 slot indices to V2 BodyRegionV2.
|
||||
* Used to convert V1 session targetSlot ordinals to V2 regions.
|
||||
* Index: 0=ARMS, 1=MOUTH, 2=EYES, 3=EARS, 4=NECK, 5=TORSO, 6=HANDS.
|
||||
*/
|
||||
private static final BodyRegionV2[] SLOT_TO_REGION = {
|
||||
BodyRegionV2.ARMS, // 0 = BIND
|
||||
BodyRegionV2.MOUTH, // 1 = GAG
|
||||
BodyRegionV2.EYES, // 2 = BLINDFOLD
|
||||
BodyRegionV2.EARS, // 3 = EARPLUGS
|
||||
BodyRegionV2.NECK, // 4 = COLLAR
|
||||
BodyRegionV2.TORSO, // 5 = CLOTHES
|
||||
BodyRegionV2.HANDS, // 6 = MITTENS
|
||||
};
|
||||
|
||||
private StruggleSessionManager() {}
|
||||
|
||||
public static StruggleSessionManager getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
// ==================== SESSION START VARIANTS ====================
|
||||
|
||||
/**
|
||||
* Start a new continuous struggle session for a player.
|
||||
*
|
||||
* @param player The server player
|
||||
* @param targetResistance Current bind resistance
|
||||
* @param isLocked Whether the bind is locked
|
||||
* @return The new session
|
||||
*/
|
||||
public ContinuousStruggleMiniGameState startContinuousStruggleSession(
|
||||
ServerPlayer player,
|
||||
int targetResistance,
|
||||
boolean isLocked
|
||||
) {
|
||||
UUID playerId = player.getUUID();
|
||||
|
||||
// Remove any existing continuous session
|
||||
ContinuousStruggleMiniGameState existing = continuousSessions.get(
|
||||
playerId
|
||||
);
|
||||
if (existing != null) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[StruggleSessionManager] Replacing existing continuous struggle session for {}",
|
||||
player.getName().getString()
|
||||
);
|
||||
continuousSessions.remove(playerId);
|
||||
}
|
||||
|
||||
// Create new session with configurable rate
|
||||
int ticksPerResistance =
|
||||
com.tiedup.remake.core.SettingsAccessor.getStruggleContinuousRate(
|
||||
player.level().getGameRules()
|
||||
);
|
||||
ContinuousStruggleMiniGameState session =
|
||||
new ContinuousStruggleMiniGameState(
|
||||
playerId,
|
||||
targetResistance,
|
||||
isLocked,
|
||||
null,
|
||||
ticksPerResistance
|
||||
);
|
||||
continuousSessions.put(playerId, session);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleSessionManager] Started continuous struggle session {} for {} (resistance: {}, locked: {})",
|
||||
session.getSessionId().toString().substring(0, 8),
|
||||
player.getName().getString(),
|
||||
targetResistance,
|
||||
isLocked
|
||||
);
|
||||
|
||||
// Set struggle animation state and sync to client
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
state.setStruggling(true, player.level().getGameTime());
|
||||
SyncManager.syncStruggleState(player);
|
||||
}
|
||||
|
||||
// Notify nearby kidnappers
|
||||
GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new continuous struggle session for an accessory.
|
||||
*
|
||||
* @param player The server player
|
||||
* @param targetSlot Target accessory slot ordinal
|
||||
* @param lockResistance Current lock resistance
|
||||
* @return The new session
|
||||
*/
|
||||
public ContinuousStruggleMiniGameState startContinuousAccessoryStruggleSession(
|
||||
ServerPlayer player,
|
||||
int targetSlot,
|
||||
int lockResistance
|
||||
) {
|
||||
UUID playerId = player.getUUID();
|
||||
|
||||
// Remove any existing session
|
||||
ContinuousStruggleMiniGameState existing = continuousSessions.get(
|
||||
playerId
|
||||
);
|
||||
if (existing != null) {
|
||||
continuousSessions.remove(playerId);
|
||||
}
|
||||
|
||||
// Create new session with target slot and configurable rate
|
||||
int ticksPerResistance =
|
||||
com.tiedup.remake.core.SettingsAccessor.getStruggleContinuousRate(
|
||||
player.level().getGameRules()
|
||||
);
|
||||
ContinuousStruggleMiniGameState session =
|
||||
new ContinuousStruggleMiniGameState(
|
||||
playerId,
|
||||
lockResistance,
|
||||
true,
|
||||
targetSlot,
|
||||
ticksPerResistance
|
||||
);
|
||||
continuousSessions.put(playerId, session);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleSessionManager] Started continuous accessory struggle session {} for {} (slot: {}, resistance: {})",
|
||||
session.getSessionId().toString().substring(0, 8),
|
||||
player.getName().getString(),
|
||||
targetSlot,
|
||||
lockResistance
|
||||
);
|
||||
|
||||
// Set struggle animation state and sync to client
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
state.setStruggling(true, player.level().getGameTime());
|
||||
SyncManager.syncStruggleState(player);
|
||||
}
|
||||
|
||||
// Notify nearby kidnappers
|
||||
GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new continuous struggle session for a V2 bondage item.
|
||||
*
|
||||
* @param player The server player
|
||||
* @param targetRegion V2 body region to struggle against
|
||||
* @param targetResistance Current resistance value
|
||||
* @param isLocked Whether the item is locked
|
||||
* @return The new session, or null if creation failed
|
||||
*/
|
||||
public ContinuousStruggleMiniGameState startV2StruggleSession(
|
||||
ServerPlayer player,
|
||||
com.tiedup.remake.v2.BodyRegionV2 targetRegion,
|
||||
int targetResistance,
|
||||
boolean isLocked
|
||||
) {
|
||||
UUID playerId = player.getUUID();
|
||||
|
||||
// RISK-001 fix: reject if an active session already exists (prevents direction re-roll exploit)
|
||||
ContinuousStruggleMiniGameState existing = continuousSessions.get(
|
||||
playerId
|
||||
);
|
||||
if (existing != null) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[StruggleSessionManager] Rejected V2 session: active session already exists for {}",
|
||||
player.getName().getString()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
int ticksPerResistance =
|
||||
com.tiedup.remake.core.SettingsAccessor.getStruggleContinuousRate(
|
||||
player.level().getGameRules()
|
||||
);
|
||||
ContinuousStruggleMiniGameState session =
|
||||
new ContinuousStruggleMiniGameState(
|
||||
playerId,
|
||||
targetResistance,
|
||||
isLocked,
|
||||
null,
|
||||
ticksPerResistance
|
||||
);
|
||||
session.setTargetRegion(targetRegion);
|
||||
continuousSessions.put(playerId, session);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleSessionManager] Started V2 struggle session {} for {} (region: {}, resistance: {}, locked: {})",
|
||||
session.getSessionId().toString().substring(0, 8),
|
||||
player.getName().getString(),
|
||||
targetRegion.name(),
|
||||
targetResistance,
|
||||
isLocked
|
||||
);
|
||||
|
||||
// Set struggle animation state and sync to client
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
state.setStruggling(true, player.level().getGameTime());
|
||||
SyncManager.syncStruggleState(player);
|
||||
}
|
||||
|
||||
GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new continuous struggle session for a furniture seat escape.
|
||||
*
|
||||
* <p>The session behaves identically to a V2 struggle (direction-hold to
|
||||
* reduce resistance) but on completion it unlocks the seat and dismounts
|
||||
* the player instead of removing a bondage item.</p>
|
||||
*
|
||||
* @param player The server player (must be seated and locked)
|
||||
* @param furnitureEntityId Entity ID of the furniture
|
||||
* @param seatId The locked seat ID
|
||||
* @param totalDifficulty Combined resistance (seat base + item bonus)
|
||||
* @return The new session, or null if creation failed
|
||||
*/
|
||||
public ContinuousStruggleMiniGameState startFurnitureStruggleSession(
|
||||
ServerPlayer player,
|
||||
int furnitureEntityId,
|
||||
String seatId,
|
||||
int totalDifficulty
|
||||
) {
|
||||
UUID playerId = player.getUUID();
|
||||
|
||||
// Reject if an active session already exists
|
||||
ContinuousStruggleMiniGameState existing = continuousSessions.get(playerId);
|
||||
if (existing != null) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[StruggleSessionManager] Rejected furniture session: active session already exists for {}",
|
||||
player.getName().getString()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
int ticksPerResistance =
|
||||
com.tiedup.remake.core.SettingsAccessor.getStruggleContinuousRate(
|
||||
player.level().getGameRules()
|
||||
);
|
||||
ContinuousStruggleMiniGameState session =
|
||||
new ContinuousStruggleMiniGameState(
|
||||
playerId,
|
||||
totalDifficulty,
|
||||
true, // furniture seats are always "locked" in context
|
||||
null,
|
||||
ticksPerResistance
|
||||
);
|
||||
session.setFurnitureContext(furnitureEntityId, seatId);
|
||||
continuousSessions.put(playerId, session);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleSessionManager] Started furniture struggle session {} for {} (entity: {}, seat: '{}', difficulty: {})",
|
||||
session.getSessionId().toString().substring(0, 8),
|
||||
player.getName().getString(),
|
||||
furnitureEntityId,
|
||||
seatId,
|
||||
totalDifficulty
|
||||
);
|
||||
|
||||
// Set struggle animation state and sync to client
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
state.setStruggling(true, player.level().getGameTime());
|
||||
SyncManager.syncStruggleState(player);
|
||||
}
|
||||
|
||||
// Play struggle loop sound from furniture definition (plays once on start;
|
||||
// true looping sound would require client-side sound management -- future scope)
|
||||
net.minecraft.world.entity.Entity furnitureEntity = player.level().getEntity(furnitureEntityId);
|
||||
if (furnitureEntity instanceof com.tiedup.remake.v2.furniture.EntityFurniture furniture) {
|
||||
com.tiedup.remake.v2.furniture.FurnitureDefinition def = furniture.getDefinition();
|
||||
if (def != null && def.feedback().struggleLoopSound() != null) {
|
||||
player.level().playSound(null, player.getX(), player.getY(), player.getZ(),
|
||||
net.minecraft.sounds.SoundEvent.createVariableRangeEvent(def.feedback().struggleLoopSound()),
|
||||
SoundSource.PLAYERS, 0.6f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);
|
||||
return session;
|
||||
}
|
||||
|
||||
// ==================== SESSION QUERY ====================
|
||||
|
||||
/**
|
||||
* Get active continuous struggle session for a player.
|
||||
*
|
||||
* @param playerId The player UUID
|
||||
* @return The session, or null if none active
|
||||
*/
|
||||
@Nullable
|
||||
public ContinuousStruggleMiniGameState getContinuousStruggleSession(
|
||||
UUID playerId
|
||||
) {
|
||||
ContinuousStruggleMiniGameState session = continuousSessions.get(
|
||||
playerId
|
||||
);
|
||||
if (session != null && session.isExpired()) {
|
||||
continuousSessions.remove(playerId);
|
||||
return null;
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a continuous struggle session.
|
||||
*
|
||||
* @param playerId The player UUID
|
||||
* @param sessionId The session UUID to validate
|
||||
* @return true if session is valid and active
|
||||
*/
|
||||
public boolean validateContinuousStruggleSession(
|
||||
UUID playerId,
|
||||
UUID sessionId
|
||||
) {
|
||||
ContinuousStruggleMiniGameState session = getContinuousStruggleSession(
|
||||
playerId
|
||||
);
|
||||
if (session == null) {
|
||||
return false;
|
||||
}
|
||||
return session.getSessionId().equals(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* End a continuous struggle session.
|
||||
*
|
||||
* @param playerId The player UUID
|
||||
* @param success Whether the session ended in success (escape)
|
||||
*/
|
||||
public void endContinuousStruggleSession(UUID playerId, boolean success) {
|
||||
ContinuousStruggleMiniGameState session = continuousSessions.remove(
|
||||
playerId
|
||||
);
|
||||
if (session != null) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleSessionManager] Ended continuous struggle session for player {} (success: {})",
|
||||
playerId.toString().substring(0, 8),
|
||||
success
|
||||
);
|
||||
|
||||
// Clear struggle animation state
|
||||
// We need to find the player to clear the animation
|
||||
// This will be handled by the caller if they have access to the player
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== TICK ====================
|
||||
|
||||
/**
|
||||
* Tick all continuous struggle sessions.
|
||||
* Should be called every server tick.
|
||||
*
|
||||
* @param server The server instance for player lookups
|
||||
* @param currentTick Current game tick
|
||||
*/
|
||||
public void tickContinuousSessions(
|
||||
net.minecraft.server.MinecraftServer server,
|
||||
long currentTick
|
||||
) {
|
||||
if (continuousSessions.isEmpty()) return;
|
||||
|
||||
// Collect sessions to process (avoid ConcurrentModificationException)
|
||||
List<Map.Entry<UUID, ContinuousStruggleMiniGameState>> toProcess =
|
||||
new ArrayList<>(continuousSessions.entrySet());
|
||||
|
||||
for (Map.Entry<
|
||||
UUID,
|
||||
ContinuousStruggleMiniGameState
|
||||
> entry : toProcess) {
|
||||
UUID playerId = entry.getKey();
|
||||
ContinuousStruggleMiniGameState session = entry.getValue();
|
||||
|
||||
// Get player
|
||||
ServerPlayer player = server.getPlayerList().getPlayer(playerId);
|
||||
if (player == null) {
|
||||
// Player disconnected, clean up
|
||||
continuousSessions.remove(playerId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for expired/completed sessions
|
||||
if (session.isExpired() || session.isComplete()) {
|
||||
continuousSessions.remove(playerId);
|
||||
clearStruggleAnimation(player);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tick the session
|
||||
TickResult result = session.tick(currentTick);
|
||||
|
||||
// Handle tick result
|
||||
switch (result) {
|
||||
case DIRECTION_CHANGE -> {
|
||||
sendContinuousStruggleUpdate(
|
||||
player,
|
||||
session,
|
||||
UpdateType.DIRECTION_CHANGE
|
||||
);
|
||||
}
|
||||
case RESISTANCE_UPDATE -> {
|
||||
// Update actual bind resistance
|
||||
updateBindResistance(player, session);
|
||||
// Send update to client immediately for real-time feedback
|
||||
sendContinuousStruggleUpdate(
|
||||
player,
|
||||
session,
|
||||
UpdateType.RESISTANCE_UPDATE
|
||||
);
|
||||
}
|
||||
case ESCAPED -> {
|
||||
handleStruggleEscape(player, session);
|
||||
}
|
||||
case NO_CHANGE -> {
|
||||
// No action needed
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
// Check shock collar (separate from tick result)
|
||||
if (
|
||||
shouldCheckShockCollar(player) &&
|
||||
session.shouldTriggerShock(currentTick)
|
||||
) {
|
||||
handleShockCollar(player, session);
|
||||
}
|
||||
|
||||
// Check kidnapper notification
|
||||
if (session.shouldNotifyKidnappers(currentTick)) {
|
||||
GuardNotificationHelper.notifyNearbyKidnappersOfStruggle(player);
|
||||
}
|
||||
|
||||
// Play struggle sound periodically
|
||||
if (session.shouldPlayStruggleSound(currentTick)) {
|
||||
playStruggleSound(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PRIVATE HELPERS ====================
|
||||
|
||||
/**
|
||||
* Play struggle sound for player and nearby entities.
|
||||
*/
|
||||
private void playStruggleSound(ServerPlayer player) {
|
||||
// Play leather creaking sound - audible to player and nearby
|
||||
player
|
||||
.serverLevel()
|
||||
.playSound(
|
||||
null, // null = all players can hear
|
||||
player.getX(),
|
||||
player.getY(),
|
||||
player.getZ(),
|
||||
SoundEvents.ARMOR_EQUIP_LEATHER,
|
||||
SoundSource.PLAYERS,
|
||||
0.8f, // volume
|
||||
0.9f + player.getRandom().nextFloat() * 0.2f // slight pitch variation
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if shock collar check is applicable for this player.
|
||||
*/
|
||||
private boolean shouldCheckShockCollar(ServerPlayer player) {
|
||||
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
|
||||
if (collar.isEmpty()) return false;
|
||||
|
||||
// Only shock collars can trigger during struggle
|
||||
if (!(collar.getItem() instanceof ItemShockCollar)) return false;
|
||||
|
||||
// Must be locked
|
||||
if (collar.getItem() instanceof ItemCollar collarItem) {
|
||||
return collarItem.isLocked(collar);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle shock collar trigger.
|
||||
*/
|
||||
private void handleShockCollar(
|
||||
ServerPlayer player,
|
||||
ContinuousStruggleMiniGameState session
|
||||
) {
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
state.shockKidnapped(" (Your struggle was interrupted!)", 2.0f);
|
||||
}
|
||||
|
||||
session.triggerShock();
|
||||
sendContinuousStruggleUpdate(player, session, UpdateType.SHOCK);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[StruggleSessionManager] Shock collar triggered for {} during struggle",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the actual bind resistance based on session state.
|
||||
*/
|
||||
private void updateBindResistance(
|
||||
ServerPlayer player,
|
||||
ContinuousStruggleMiniGameState session
|
||||
) {
|
||||
// V2 region-based resistance update
|
||||
if (session.isV2Struggle()) {
|
||||
com.tiedup.remake.v2.BodyRegionV2 region =
|
||||
session.getTargetRegion();
|
||||
ItemStack stack =
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
|
||||
.getInRegion(player, region);
|
||||
if (stack.isEmpty()) return;
|
||||
|
||||
if (
|
||||
stack.getItem() instanceof
|
||||
com.tiedup.remake.items.base.IHasResistance resistanceItem
|
||||
) {
|
||||
resistanceItem.setCurrentResistance(
|
||||
stack,
|
||||
session.getCurrentResistance()
|
||||
);
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.sync(
|
||||
player
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.isAccessoryStruggle()) {
|
||||
// Handle accessory resistance update (V1 path -- slot index is legacy ordinal)
|
||||
Integer slotIndex = session.getTargetSlot();
|
||||
if (slotIndex == null || slotIndex < 0 || slotIndex >= SLOT_TO_REGION.length) return;
|
||||
|
||||
BodyRegionV2 region = SLOT_TO_REGION[slotIndex];
|
||||
ItemStack accessoryStack = V2EquipmentHelper.getInRegion(player, region);
|
||||
if (accessoryStack.isEmpty()) return;
|
||||
|
||||
if (
|
||||
accessoryStack.getItem() instanceof
|
||||
com.tiedup.remake.items.base.ILockable lockable
|
||||
) {
|
||||
// Update the lock resistance to match session state
|
||||
lockable.setCurrentLockResistance(
|
||||
accessoryStack,
|
||||
session.getCurrentResistance()
|
||||
);
|
||||
// Sync V2 equipment state
|
||||
V2EquipmentHelper.sync(player);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Update bind resistance
|
||||
ItemStack bindStack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS);
|
||||
if (
|
||||
bindStack.isEmpty() ||
|
||||
!(bindStack.getItem() instanceof ItemBind bind)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
bind.setCurrentResistance(bindStack, session.getCurrentResistance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle successful escape from struggle.
|
||||
*/
|
||||
private void handleStruggleEscape(
|
||||
ServerPlayer player,
|
||||
ContinuousStruggleMiniGameState session
|
||||
) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleSessionManager] Player {} escaped from struggle!",
|
||||
player.getName().getString()
|
||||
);
|
||||
|
||||
// Send escape update to client
|
||||
sendContinuousStruggleUpdate(player, session, UpdateType.ESCAPE);
|
||||
|
||||
// Clear animation state
|
||||
clearStruggleAnimation(player);
|
||||
|
||||
// Furniture escape: unlock seat + dismount
|
||||
if (session.isFurnitureStruggle()) {
|
||||
handleFurnitureEscape(player, session);
|
||||
continuousSessions.remove(player.getUUID());
|
||||
return;
|
||||
}
|
||||
|
||||
// V2 region-based escape
|
||||
if (session.isV2Struggle()) {
|
||||
com.tiedup.remake.v2.BodyRegionV2 region =
|
||||
session.getTargetRegion();
|
||||
// BUG-001 fix: break lock before unequip (consistent with V1 accessory escape)
|
||||
ItemStack stackInRegion =
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
|
||||
.getInRegion(player, region);
|
||||
if (!stackInRegion.isEmpty()
|
||||
&& stackInRegion.getItem() instanceof com.tiedup.remake.items.base.ILockable lockable) {
|
||||
lockable.breakLock(stackInRegion);
|
||||
}
|
||||
ItemStack removed =
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
|
||||
.unequipFromRegion(player, region, true);
|
||||
if (!removed.isEmpty()) {
|
||||
if (!player.getInventory().add(removed)) {
|
||||
player.drop(removed, false);
|
||||
}
|
||||
}
|
||||
continuousSessions.remove(player.getUUID());
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the bind
|
||||
if (!session.isAccessoryStruggle()) {
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
ItemStack bind = state.unequip(BodyRegionV2.ARMS);
|
||||
if (!bind.isEmpty()) {
|
||||
state.kidnappedDropItem(bind);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle accessory escape (V1 path -- slot index is legacy ordinal)
|
||||
Integer slotIndex = session.getTargetSlot();
|
||||
if (slotIndex != null && slotIndex >= 0 && slotIndex < SLOT_TO_REGION.length) {
|
||||
BodyRegionV2 region = SLOT_TO_REGION[slotIndex];
|
||||
ItemStack accessoryStack = V2EquipmentHelper.getInRegion(player, region);
|
||||
if (!accessoryStack.isEmpty()) {
|
||||
// Break the lock on the accessory
|
||||
if (
|
||||
accessoryStack.getItem() instanceof
|
||||
com.tiedup.remake.items.base.ILockable lockable
|
||||
) {
|
||||
lockable.breakLock(accessoryStack);
|
||||
}
|
||||
// Remove the accessory from the region and drop it
|
||||
ItemStack removed = V2EquipmentHelper.unequipFromRegion(
|
||||
player, region, true
|
||||
);
|
||||
if (!removed.isEmpty()) {
|
||||
// Drop the item at player's feet
|
||||
player.drop(removed, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove session
|
||||
continuousSessions.remove(player.getUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle successful furniture escape: unlock the seat, dismount the player,
|
||||
* play sounds, and broadcast state to tracking clients.
|
||||
*/
|
||||
private void handleFurnitureEscape(
|
||||
ServerPlayer player,
|
||||
ContinuousStruggleMiniGameState session
|
||||
) {
|
||||
int furnitureEntityId = session.getFurnitureEntityId();
|
||||
String seatId = session.getFurnitureSeatId();
|
||||
|
||||
net.minecraft.world.entity.Entity entity = player.level().getEntity(furnitureEntityId);
|
||||
if (entity == null || entity.isRemoved()) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[StruggleSessionManager] Furniture entity {} no longer exists for escape",
|
||||
furnitureEntityId
|
||||
);
|
||||
// Player still needs to be freed even if entity is gone
|
||||
player.stopRiding();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(entity instanceof com.tiedup.remake.v2.furniture.ISeatProvider provider)) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[StruggleSessionManager] Entity {} is not an ISeatProvider",
|
||||
furnitureEntityId
|
||||
);
|
||||
player.stopRiding();
|
||||
return;
|
||||
}
|
||||
|
||||
// Unlock the seat
|
||||
provider.setSeatLocked(seatId, false);
|
||||
|
||||
// Clear persistent data tag (reconnection system)
|
||||
net.minecraft.nbt.CompoundTag persistentData = player.getPersistentData();
|
||||
persistentData.remove("tiedup_locked_furniture");
|
||||
|
||||
// Dismount the player
|
||||
player.stopRiding();
|
||||
|
||||
// Play escape sound: prefer furniture-specific sound, fall back to CHAIN_BREAK
|
||||
net.minecraft.sounds.SoundEvent escapeSound = net.minecraft.sounds.SoundEvents.CHAIN_BREAK;
|
||||
if (entity instanceof com.tiedup.remake.v2.furniture.EntityFurniture furniture) {
|
||||
com.tiedup.remake.v2.furniture.FurnitureDefinition def = furniture.getDefinition();
|
||||
if (def != null && def.feedback().escapeSound() != null) {
|
||||
escapeSound = net.minecraft.sounds.SoundEvent.createVariableRangeEvent(
|
||||
def.feedback().escapeSound()
|
||||
);
|
||||
}
|
||||
}
|
||||
player.serverLevel().playSound(
|
||||
null,
|
||||
player.getX(), player.getY(), player.getZ(),
|
||||
escapeSound,
|
||||
net.minecraft.sounds.SoundSource.PLAYERS,
|
||||
1.0f, 1.0f
|
||||
);
|
||||
|
||||
// Broadcast updated furniture state to all tracking clients
|
||||
if (entity instanceof com.tiedup.remake.v2.furniture.EntityFurniture furniture) {
|
||||
com.tiedup.remake.v2.furniture.network.PacketSyncFurnitureState.sendToTracking(furniture);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleSessionManager] {} escaped furniture {} seat '{}'",
|
||||
player.getName().getString(), furnitureEntityId, seatId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a continuous struggle state update to the client.
|
||||
*/
|
||||
private void sendContinuousStruggleUpdate(
|
||||
ServerPlayer player,
|
||||
ContinuousStruggleMiniGameState session,
|
||||
UpdateType updateType
|
||||
) {
|
||||
ModNetwork.sendToPlayer(
|
||||
new PacketContinuousStruggleState(
|
||||
session.getSessionId(),
|
||||
updateType,
|
||||
session.getCurrentDirectionIndex(),
|
||||
session.getCurrentResistance(),
|
||||
session.getMaxResistance(),
|
||||
session.isLocked()
|
||||
),
|
||||
player
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear struggle animation state for a player and sync to clients.
|
||||
*/
|
||||
private void clearStruggleAnimation(ServerPlayer player) {
|
||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
||||
if (state != null) {
|
||||
state.setStruggling(false, 0);
|
||||
SyncManager.syncStruggleState(player);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CLEANUP ====================
|
||||
|
||||
/**
|
||||
* Clean up all struggle sessions for a player (called on disconnect).
|
||||
*
|
||||
* @param playerId The player UUID
|
||||
*/
|
||||
public void cleanupPlayer(UUID playerId) {
|
||||
continuousSessions.remove(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodic cleanup of expired struggle 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;
|
||||
}
|
||||
|
||||
continuousSessions
|
||||
.entrySet()
|
||||
.removeIf(entry -> entry.getValue().isExpired());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of active struggle sessions (for debugging).
|
||||
*/
|
||||
public int getActiveSessionCount() {
|
||||
return continuousSessions.size();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user