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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user