refactor(S-02/S-05): extract PlayerMovement component + fix thread safety

- Extract 11 movement fields from PlayerBindState into PlayerMovement component
- Replace volatile isStruggling/struggleStartTick pair with atomic StruggleSnapshot record
- Remove 5+2 misleading synchronized keywords (different monitors, all server-thread-only)
- Update all 36 MovementStyleManager field accesses to use getMovement() getters/setters
This commit is contained in:
NotEvil
2026-04-15 13:17:00 +02:00
parent 70f85b58a6
commit 22d79a452b
4 changed files with 251 additions and 155 deletions

View File

@@ -11,6 +11,7 @@ import com.tiedup.remake.state.components.PlayerClothesPermission;
import com.tiedup.remake.state.components.PlayerDataRetrieval;
import com.tiedup.remake.state.components.PlayerEquipment;
import com.tiedup.remake.state.components.PlayerLifecycle;
import com.tiedup.remake.state.components.PlayerMovement;
import com.tiedup.remake.state.components.PlayerSale;
import com.tiedup.remake.state.components.PlayerShockCollar;
import com.tiedup.remake.state.components.PlayerSpecialActions;
@@ -49,8 +50,10 @@ import org.jetbrains.annotations.Nullable;
* - Enslavement lifecycle (can be enslaved, act as master)
* - Advanced collar features (shocks, GPS tracking)
*
* Thread Safety: This class is accessed from both server and client threads.
* Use appropriate synchronization when accessing the instances map.
* Thread Safety: All mutating methods must be called from the server thread
* (via enqueueWork for packet handlers). The only cross-thread access is
* the StruggleSnapshot (atomic volatile record) and the Player reference (volatile).
* The instances map uses ConcurrentHashMap for thread-safe lookup.
*
* Refactoring: Component-Host pattern
*/
@@ -138,89 +141,27 @@ public class PlayerBindState implements IRestrainable, IPlayerBindStateHost {
private PlayerCaptorManager captorManager;
// Note: transport field removed - now using IPlayerLeashAccess mixin
// Struggle animation state
// volatile: accessed from multiple threads (network, tick, render)
private volatile boolean isStruggling = false;
private volatile long struggleStartTick = 0;
// Struggle animation state — atomic snapshot for cross-thread visibility.
// Server thread writes via setStruggling(), render thread reads via isStruggling()/getStruggleStartTick().
// Single volatile reference guarantees both fields are visible atomically.
private record StruggleSnapshot(boolean struggling, long startTick) {}
private volatile StruggleSnapshot struggleState = new StruggleSnapshot(false, 0);
// ========== Movement Style State (Phase: Movement Styles) ==========
// Managed exclusively by MovementStyleManager. Stored here to piggyback
// on existing lifecycle cleanup hooks (death, logout, dimension change).
// ========== Movement Style State ==========
// Encapsulated in PlayerMovement component. Managed exclusively by MovementStyleManager.
private final PlayerMovement movement;
/** Currently active movement style, or null if none. */
@Nullable
private com.tiedup.remake.v2.bondage.movement.MovementStyle activeMovementStyle;
/** Resolved speed multiplier for the active style (0.0-1.0). */
private float resolvedMovementSpeed = 1.0f;
/** Whether jumping is currently disabled by the active style. */
private boolean resolvedJumpDisabled = false;
@Nullable
public com.tiedup.remake.v2.bondage.movement.MovementStyle getActiveMovementStyle() {
return activeMovementStyle;
/** Access movement style state (hop, crawl, position tracking, etc.). */
public PlayerMovement getMovement() {
return movement;
}
public void setActiveMovementStyle(
@Nullable com.tiedup.remake.v2.bondage.movement.MovementStyle style
) {
this.activeMovementStyle = style;
}
public float getResolvedMovementSpeed() {
return resolvedMovementSpeed;
}
public void setResolvedMovementSpeed(float speed) {
this.resolvedMovementSpeed = speed;
}
public boolean isResolvedJumpDisabled() {
return resolvedJumpDisabled;
}
public void setResolvedJumpDisabled(boolean disabled) {
this.resolvedJumpDisabled = disabled;
}
/** Ticks until next hop is allowed (HOP style). */
public int hopCooldown = 0;
/** True during the 4-tick startup delay before the first hop. */
public boolean hopStartupPending = false;
/** Countdown ticks for the hop startup delay. */
public int hopStartupTicks = 0;
/** True if crawl deactivated but player can't stand yet (space blocked). */
public boolean pendingPoseRestore = false;
/** Last known X position for movement detection (position delta). */
public double lastX;
/** Last known Y position for movement detection (position delta). */
public double lastY;
/** Last known Z position for movement detection (position delta). */
public double lastZ;
/** Consecutive non-moving ticks counter for hop startup reset. */
public int hopNotMovingTicks = 0;
/**
* Resets all movement style state to defaults.
* Called on death, logout, and dimension change to ensure clean re-activation.
*/
public void clearMovementState() {
this.activeMovementStyle = null;
this.resolvedMovementSpeed = 1.0f;
this.resolvedJumpDisabled = false;
this.hopCooldown = 0;
this.hopStartupPending = false;
this.hopStartupTicks = 0;
this.pendingPoseRestore = false;
this.hopNotMovingTicks = 0;
movement.clear();
}
// ========== Constructor ==========
@@ -249,6 +190,9 @@ public class PlayerBindState implements IRestrainable, IPlayerBindStateHost {
this.lifecycle = new PlayerLifecycle(this);
this.captivity = new PlayerCaptivity(this);
// Initialize movement component
this.movement = new PlayerMovement(this);
this.captor = null;
this.captorManager = new PlayerCaptorManager(player);
}
@@ -578,47 +522,42 @@ public class PlayerBindState implements IRestrainable, IPlayerBindStateHost {
/**
* Check if player is currently playing struggle animation.
* Thread-safe (volatile field).
* IPlayerBindStateHost implementation.
* Thread-safe: reads from atomic StruggleSnapshot.
*/
@Override
public boolean isStruggling() {
return isStruggling;
return struggleState.struggling();
}
/**
* Get the tick when struggle animation started.
* Thread-safe (volatile field).
* IPlayerBindStateHost implementation.
* Thread-safe: reads from atomic StruggleSnapshot.
*/
@Override
public long getStruggleStartTick() {
return struggleStartTick;
return struggleState.startTick();
}
/**
* Set struggle animation state (server-side).
* IPlayerBindStateHost implementation.
* @param struggling True to start struggle animation, false to stop
* @param currentTick Current game time tick (for timer)
* Single volatile write ensures both fields are visible atomically to render thread.
*/
@Override
public void setStruggling(boolean struggling, long currentTick) {
this.isStruggling = struggling;
if (struggling) {
this.struggleStartTick = currentTick;
}
this.struggleState = new StruggleSnapshot(
struggling,
struggling ? currentTick : struggleState.startTick()
);
}
/**
* Set struggle animation flag (client-side only).
* IPlayerBindStateHost implementation.
* Used by network sync - does NOT update timer (server manages timer).
* @param struggling True if struggling, false otherwise
*/
@Override
public void setStrugglingClient(boolean struggling) {
this.isStruggling = struggling;
StruggleSnapshot current = this.struggleState;
this.struggleState = new StruggleSnapshot(struggling, current.startTick());
}
/**
@@ -635,31 +574,34 @@ public class PlayerBindState implements IRestrainable, IPlayerBindStateHost {
* Delegated to PlayerEquipment component
*/
@Override
public synchronized int getCurrentBindResistance() {
public int getCurrentBindResistance() {
return equipment.getCurrentBindResistance();
}
/**
* Delegated to PlayerEquipment component
* Delegated to PlayerEquipment component.
* Thread safety: must be called from the server thread.
*/
@Override
public synchronized void setCurrentBindResistance(int resistance) {
public void setCurrentBindResistance(int resistance) {
equipment.setCurrentBindResistance(resistance);
}
/**
* Delegated to PlayerEquipment component
* Delegated to PlayerEquipment component.
* Thread safety: must be called from the server thread.
*/
@Override
public synchronized int getCurrentCollarResistance() {
public int getCurrentCollarResistance() {
return equipment.getCurrentCollarResistance();
}
/**
* Delegated to PlayerEquipment component
* Delegated to PlayerEquipment component.
* Thread safety: must be called from the server thread.
*/
@Override
public synchronized void setCurrentCollarResistance(int resistance) {
public void setCurrentCollarResistance(int resistance) {
equipment.setCurrentCollarResistance(resistance);
}
@@ -1006,12 +948,12 @@ public class PlayerBindState implements IRestrainable, IPlayerBindStateHost {
// Delegated to PlayerEquipment component (except TORSO - handled by PlayerClothesPermission)
@Override
public synchronized ItemStack replaceEquipment(
public ItemStack replaceEquipment(
BodyRegionV2 region,
ItemStack newStack,
boolean force
) {
// MEDIUM FIX: Synchronized to prevent race condition during equipment replacement
// Thread safety: must be called from the server thread.
return switch (region) {
case ARMS -> equipment.replaceBind(newStack, force);
case MOUTH -> equipment.replaceGag(newStack, force);