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);
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.tiedup.remake.state.components;
|
||||
|
||||
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
|
||||
import com.tiedup.remake.v2.bondage.movement.MovementStyle;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Component responsible for movement style state management.
|
||||
* Stores all per-player movement fields used exclusively by MovementStyleManager.
|
||||
*
|
||||
* <p>Thread safety: All fields are server-tick-only (PlayerTickEvent + LivingJumpEvent,
|
||||
* both on the server thread). No synchronization needed.</p>
|
||||
*/
|
||||
public class PlayerMovement {
|
||||
|
||||
private final IPlayerBindStateHost host;
|
||||
|
||||
// --- Resolved style state ---
|
||||
@Nullable
|
||||
private MovementStyle activeMovementStyle;
|
||||
private float resolvedMovementSpeed = 1.0f;
|
||||
private boolean resolvedJumpDisabled = false;
|
||||
|
||||
// --- Hop state ---
|
||||
private int hopCooldown = 0;
|
||||
private boolean hopStartupPending = false;
|
||||
private int hopStartupTicks = 0;
|
||||
private int hopNotMovingTicks = 0;
|
||||
|
||||
// --- Crawl state ---
|
||||
private boolean pendingPoseRestore = false;
|
||||
|
||||
// --- Position tracking for movement detection ---
|
||||
private double lastX;
|
||||
private double lastY;
|
||||
private double lastZ;
|
||||
|
||||
public PlayerMovement(IPlayerBindStateHost host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
// --- Resolved style ---
|
||||
|
||||
@Nullable
|
||||
public MovementStyle getActiveMovementStyle() {
|
||||
return activeMovementStyle;
|
||||
}
|
||||
|
||||
public void setActiveMovementStyle(@Nullable 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;
|
||||
}
|
||||
|
||||
// --- Hop ---
|
||||
|
||||
public int getHopCooldown() {
|
||||
return hopCooldown;
|
||||
}
|
||||
|
||||
public void setHopCooldown(int hopCooldown) {
|
||||
this.hopCooldown = hopCooldown;
|
||||
}
|
||||
|
||||
public boolean isHopStartupPending() {
|
||||
return hopStartupPending;
|
||||
}
|
||||
|
||||
public void setHopStartupPending(boolean hopStartupPending) {
|
||||
this.hopStartupPending = hopStartupPending;
|
||||
}
|
||||
|
||||
public int getHopStartupTicks() {
|
||||
return hopStartupTicks;
|
||||
}
|
||||
|
||||
public void setHopStartupTicks(int hopStartupTicks) {
|
||||
this.hopStartupTicks = hopStartupTicks;
|
||||
}
|
||||
|
||||
public int getHopNotMovingTicks() {
|
||||
return hopNotMovingTicks;
|
||||
}
|
||||
|
||||
public void setHopNotMovingTicks(int hopNotMovingTicks) {
|
||||
this.hopNotMovingTicks = hopNotMovingTicks;
|
||||
}
|
||||
|
||||
// --- Crawl ---
|
||||
|
||||
public boolean isPendingPoseRestore() {
|
||||
return pendingPoseRestore;
|
||||
}
|
||||
|
||||
public void setPendingPoseRestore(boolean pendingPoseRestore) {
|
||||
this.pendingPoseRestore = pendingPoseRestore;
|
||||
}
|
||||
|
||||
// --- Position tracking ---
|
||||
|
||||
public double getLastX() {
|
||||
return lastX;
|
||||
}
|
||||
|
||||
public void setLastX(double lastX) {
|
||||
this.lastX = lastX;
|
||||
}
|
||||
|
||||
public double getLastY() {
|
||||
return lastY;
|
||||
}
|
||||
|
||||
public void setLastY(double lastY) {
|
||||
this.lastY = lastY;
|
||||
}
|
||||
|
||||
public double getLastZ() {
|
||||
return lastZ;
|
||||
}
|
||||
|
||||
public void setLastZ(double lastZ) {
|
||||
this.lastZ = lastZ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all movement style state to defaults.
|
||||
* Called on death, logout, and dimension change to ensure clean re-activation.
|
||||
* Note: lastX/Y/Z are NOT reset — they track position, not style state.
|
||||
*/
|
||||
public void clear() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -42,10 +42,9 @@ public class PlayerStruggle {
|
||||
* Entry point for the Struggle logic (Key R).
|
||||
* Distributes effort between Binds and Collar.
|
||||
*
|
||||
* Thread Safety: Synchronized to prevent lost updates when multiple struggle
|
||||
* packets arrive simultaneously (e.g., from macro/rapid keypresses).
|
||||
* Thread Safety: Must be called from the server thread (packet handlers use enqueueWork).
|
||||
*/
|
||||
public synchronized void struggle() {
|
||||
public void struggle() {
|
||||
if (struggleBindState != null) struggleBindState.struggle(state);
|
||||
if (struggleCollarState != null) struggleCollarState.struggle(state);
|
||||
}
|
||||
@@ -53,9 +52,9 @@ public class PlayerStruggle {
|
||||
/**
|
||||
* Restores resistance to base values when a master tightens the ties.
|
||||
*
|
||||
* Thread Safety: Synchronized to prevent race with struggle operations.
|
||||
* Thread Safety: Must be called from the server thread.
|
||||
*/
|
||||
public synchronized void tighten(Player tightener) {
|
||||
public void tighten(Player tightener) {
|
||||
if (struggleBindState != null) struggleBindState.tighten(
|
||||
tightener,
|
||||
state
|
||||
|
||||
Reference in New Issue
Block a user