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

View File

@@ -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;
}
}

View File

@@ -42,10 +42,9 @@ public class PlayerStruggle {
* Entry point for the Struggle logic (Key R). * Entry point for the Struggle logic (Key R).
* Distributes effort between Binds and Collar. * Distributes effort between Binds and Collar.
* *
* Thread Safety: Synchronized to prevent lost updates when multiple struggle * Thread Safety: Must be called from the server thread (packet handlers use enqueueWork).
* packets arrive simultaneously (e.g., from macro/rapid keypresses).
*/ */
public synchronized void struggle() { public void struggle() {
if (struggleBindState != null) struggleBindState.struggle(state); if (struggleBindState != null) struggleBindState.struggle(state);
if (struggleCollarState != null) struggleCollarState.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. * 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( if (struggleBindState != null) struggleBindState.tighten(
tightener, tightener,
state state

View File

@@ -129,14 +129,14 @@ public class MovementStyleManager {
player.isDeadOrDying() || player.isDeadOrDying() ||
state.isStruggling() state.isStruggling()
) { ) {
state.lastX = player.getX(); state.getMovement().setLastX(player.getX());
state.lastY = player.getY(); state.getMovement().setLastY(player.getY());
state.lastZ = player.getZ(); state.getMovement().setLastZ(player.getZ());
return; return;
} }
// --- Pending pose restore (crawl deactivated but can't stand) --- // --- Pending pose restore (crawl deactivated but can't stand) ---
if (state.pendingPoseRestore) { if (state.getMovement().isPendingPoseRestore()) {
tryRestoreStandingPose(player, state); tryRestoreStandingPose(player, state);
} }
@@ -153,7 +153,7 @@ public class MovementStyleManager {
// --- Compare with current active style --- // --- Compare with current active style ---
MovementStyle newStyle = resolved.style(); MovementStyle newStyle = resolved.style();
MovementStyle oldStyle = state.getActiveMovementStyle(); MovementStyle oldStyle = state.getMovement().getActiveMovementStyle();
if (newStyle != oldStyle) { if (newStyle != oldStyle) {
// Style changed: deactivate old, activate new // Style changed: deactivate old, activate new
@@ -161,14 +161,14 @@ public class MovementStyleManager {
onDeactivate(player, state, oldStyle); onDeactivate(player, state, oldStyle);
} }
if (newStyle != null) { if (newStyle != null) {
state.setResolvedMovementSpeed(resolved.speedMultiplier()); state.getMovement().setResolvedMovementSpeed(resolved.speedMultiplier());
state.setResolvedJumpDisabled(resolved.jumpDisabled()); state.getMovement().setResolvedJumpDisabled(resolved.jumpDisabled());
onActivate(player, state, newStyle); onActivate(player, state, newStyle);
} else { } else {
state.setResolvedMovementSpeed(1.0f); state.getMovement().setResolvedMovementSpeed(1.0f);
state.setResolvedJumpDisabled(false); state.getMovement().setResolvedJumpDisabled(false);
} }
state.setActiveMovementStyle(newStyle); state.getMovement().setActiveMovementStyle(newStyle);
// Sync to all tracking clients (animation + crawl pose) // Sync to all tracking clients (animation + crawl pose)
ModNetwork.sendToAllTrackingAndSelf( ModNetwork.sendToAllTrackingAndSelf(
@@ -178,13 +178,13 @@ public class MovementStyleManager {
} }
// --- Per-style tick --- // --- Per-style tick ---
if (state.getActiveMovementStyle() != null) { if (state.getMovement().getActiveMovementStyle() != null) {
// Ladder suspension: skip style tick when on ladder // Ladder suspension: skip style tick when on ladder
// (ladder movement is controlled by BondageItemRestrictionHandler) // (ladder movement is controlled by BondageItemRestrictionHandler)
if (player.onClimbable()) { if (player.onClimbable()) {
state.lastX = player.getX(); state.getMovement().setLastX(player.getX());
state.lastY = player.getY(); state.getMovement().setLastY(player.getY());
state.lastZ = player.getZ(); state.getMovement().setLastZ(player.getZ());
return; return;
} }
@@ -192,9 +192,9 @@ public class MovementStyleManager {
} }
// Update last position for next tick's movement detection // Update last position for next tick's movement detection
state.lastX = player.getX(); state.getMovement().setLastX(player.getX());
state.lastY = player.getY(); state.getMovement().setLastY(player.getY());
state.lastZ = player.getZ(); state.getMovement().setLastZ(player.getZ());
} }
// ==================== Jump Suppression ==================== // ==================== Jump Suppression ====================
@@ -216,7 +216,7 @@ public class MovementStyleManager {
} }
PlayerBindState state = PlayerBindState.getInstance(player); PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null || !state.isResolvedJumpDisabled()) { if (state == null || !state.getMovement().isResolvedJumpDisabled()) {
return; return;
} }
@@ -274,7 +274,7 @@ public class MovementStyleManager {
} }
private static void tickStyle(ServerPlayer player, PlayerBindState state) { private static void tickStyle(ServerPlayer player, PlayerBindState state) {
switch (state.getActiveMovementStyle()) { switch (state.getMovement().getActiveMovementStyle()) {
case WADDLE -> tickWaddle(player, state); case WADDLE -> tickWaddle(player, state);
case SHUFFLE -> tickShuffle(player, state); case SHUFFLE -> tickShuffle(player, state);
case HOP -> tickHop(player, state); case HOP -> tickHop(player, state);
@@ -292,7 +292,7 @@ public class MovementStyleManager {
player, player,
WADDLE_SPEED_UUID, WADDLE_SPEED_UUID,
"tiedup.waddle_speed", "tiedup.waddle_speed",
state.getResolvedMovementSpeed() state.getMovement().getResolvedMovementSpeed()
); );
} }
@@ -318,7 +318,7 @@ public class MovementStyleManager {
player, player,
SHUFFLE_SPEED_UUID, SHUFFLE_SPEED_UUID,
"tiedup.shuffle_speed", "tiedup.shuffle_speed",
state.getResolvedMovementSpeed() state.getMovement().getResolvedMovementSpeed()
); );
} }
@@ -347,11 +347,11 @@ public class MovementStyleManager {
player, player,
HOP_SPEED_UUID, HOP_SPEED_UUID,
"tiedup.hop_speed", "tiedup.hop_speed",
state.getResolvedMovementSpeed() state.getMovement().getResolvedMovementSpeed()
); );
state.hopCooldown = 0; state.getMovement().setHopCooldown(0);
state.hopStartupPending = true; state.getMovement().setHopStartupPending(true);
state.hopStartupTicks = HOP_STARTUP_DELAY_TICKS; state.getMovement().setHopStartupTicks(HOP_STARTUP_DELAY_TICKS);
} }
private static void deactivateHop( private static void deactivateHop(
@@ -359,10 +359,10 @@ public class MovementStyleManager {
PlayerBindState state PlayerBindState state
) { ) {
removeSpeedModifier(player, HOP_SPEED_UUID); removeSpeedModifier(player, HOP_SPEED_UUID);
state.hopCooldown = 0; state.getMovement().setHopCooldown(0);
state.hopStartupPending = false; state.getMovement().setHopStartupPending(false);
state.hopStartupTicks = 0; state.getMovement().setHopStartupTicks(0);
state.hopNotMovingTicks = 0; state.getMovement().setHopNotMovingTicks(0);
} }
/** /**
@@ -375,43 +375,44 @@ public class MovementStyleManager {
* </ul> * </ul>
*/ */
private static void tickHop(ServerPlayer player, PlayerBindState state) { private static void tickHop(ServerPlayer player, PlayerBindState state) {
var mov = state.getMovement();
boolean isMoving = boolean isMoving =
player.distanceToSqr(state.lastX, state.lastY, state.lastZ) > player.distanceToSqr(mov.getLastX(), mov.getLastY(), mov.getLastZ()) >
MOVEMENT_THRESHOLD_SQ; MOVEMENT_THRESHOLD_SQ;
// Decrement cooldown // Decrement cooldown
if (state.hopCooldown > 0) { if (mov.getHopCooldown() > 0) {
state.hopCooldown--; mov.setHopCooldown(mov.getHopCooldown() - 1);
} }
if (isMoving && player.onGround() && state.hopCooldown <= 0) { if (isMoving && player.onGround() && mov.getHopCooldown() <= 0) {
if (state.hopStartupPending) { if (mov.isHopStartupPending()) {
// Startup delay: decrement and wait (latched: completes even if // Startup delay: decrement and wait (latched: completes even if
// player briefly releases input during these 4 ticks) // player briefly releases input during these 4 ticks)
state.hopStartupTicks--; mov.setHopStartupTicks(mov.getHopStartupTicks() - 1);
if (state.hopStartupTicks <= 0) { if (mov.getHopStartupTicks() <= 0) {
// Startup complete: execute first hop // Startup complete: execute first hop
state.hopStartupPending = false; mov.setHopStartupPending(false);
executeHop(player, state); executeHop(player, state);
} }
} else { } else {
// Normal hop // Normal hop
executeHop(player, state); executeHop(player, state);
} }
state.hopNotMovingTicks = 0; mov.setHopNotMovingTicks(0);
} else if (!isMoving) { } else if (!isMoving) {
state.hopNotMovingTicks++; mov.setHopNotMovingTicks(mov.getHopNotMovingTicks() + 1);
// Reset startup if not moving for >= 2 consecutive ticks // Reset startup if not moving for >= 2 consecutive ticks
if ( if (
state.hopNotMovingTicks >= HOP_STARTUP_RESET_TICKS && mov.getHopNotMovingTicks() >= HOP_STARTUP_RESET_TICKS &&
!state.hopStartupPending !mov.isHopStartupPending()
) { ) {
state.hopStartupPending = true; mov.setHopStartupPending(true);
state.hopStartupTicks = HOP_STARTUP_DELAY_TICKS; mov.setHopStartupTicks(HOP_STARTUP_DELAY_TICKS);
} }
} else { } else {
// Moving but not on ground or cooldown active — reset not-moving counter // Moving but not on ground or cooldown active — reset not-moving counter
state.hopNotMovingTicks = 0; mov.setHopNotMovingTicks(0);
} }
} }
@@ -431,7 +432,7 @@ public class MovementStyleManager {
currentMotion.z + forward.z * HOP_FORWARD_IMPULSE currentMotion.z + forward.z * HOP_FORWARD_IMPULSE
); );
state.hopCooldown = HOP_COOLDOWN_TICKS; state.getMovement().setHopCooldown(HOP_COOLDOWN_TICKS);
// Sync velocity to client to prevent rubber-banding // Sync velocity to client to prevent rubber-banding
player.connection.send(new ClientboundSetEntityMotionPacket(player)); player.connection.send(new ClientboundSetEntityMotionPacket(player));
@@ -447,11 +448,11 @@ public class MovementStyleManager {
player, player,
CRAWL_SPEED_UUID, CRAWL_SPEED_UUID,
"tiedup.crawl_speed", "tiedup.crawl_speed",
state.getResolvedMovementSpeed() state.getMovement().getResolvedMovementSpeed()
); );
player.setForcedPose(Pose.SWIMMING); player.setForcedPose(Pose.SWIMMING);
player.refreshDimensions(); player.refreshDimensions();
state.pendingPoseRestore = false; state.getMovement().setPendingPoseRestore(false);
} }
private static void deactivateCrawl( private static void deactivateCrawl(
@@ -470,7 +471,7 @@ public class MovementStyleManager {
player.refreshDimensions(); player.refreshDimensions();
} else { } else {
// Can't stand yet -- flag for periodic retry in tick flow (step 2) // Can't stand yet -- flag for periodic retry in tick flow (step 2)
state.pendingPoseRestore = true; state.getMovement().setPendingPoseRestore(true);
} }
} }
@@ -501,7 +502,7 @@ public class MovementStyleManager {
if (canStand) { if (canStand) {
player.setForcedPose(null); player.setForcedPose(null);
player.refreshDimensions(); player.refreshDimensions();
state.pendingPoseRestore = false; state.getMovement().setPendingPoseRestore(false);
LOGGER.debug( LOGGER.debug(
"Restored standing pose for {} (pending pose restore cleared)", "Restored standing pose for {} (pending pose restore cleared)",
player.getName().getString() player.getName().getString()