Add crouch state and fix jump sync
Add isCrouching to network, state and animation systems; enable proxy jump syncing with a jump-hold debounce and update animation descriptor and server tooling. Key changes: - Networking: F4TNetworking.h/cpp: added a_isCrouching, include "isCrouching" in transform packets and parsing, and logging. - Remote state: F4TRemotePlayerState.h: added isCrouching field. - Proxy animation: F4TProxyAnimationSync.*: added crouch fields, enabled jump sync, implemented ApplyJumpHoldState and jump hold timing, wrote crouch/jump bools into descriptors when available, updated event firing and logs. - Animation descriptor: F4AnimationDescriptor.cpp: added isJumping and isCrouching to bool variable list. - Local detection: main.cpp: added isCrouching in movement state, stubbed GetCrouchState(), adjusted jump hold timing and included crouch in outgoing transform calls. - Server/dev tooling: server/dev_server_app.py, fake_client.py, fake_player.py, README.md: added crouch reporting, toggle UI/control and fake client support for crouch, and updated docs/logging. - Docs: docs/dev-log.md updated with notes on jump/crouch fixes and testing. Rationale: ensure remote players can report crouch state and make jump animations transition reliably by debouncing jump state on proxies and writing available graph bools; also provide dev-server controls and fake-client support for testing.
This commit is contained in:
+260
@@ -7,6 +7,266 @@ failed experiments, successful tests, and next steps.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-06-05 - Jump Animation Fix (Correct Event Names)
|
||||||
|
|
||||||
|
### Issues Resolved
|
||||||
|
|
||||||
|
**Jump animation kept playing after landing (real fix):**
|
||||||
|
- Root cause: The proxy graph events `"JumpStart"`/`"JumpStop"` are NOT recognized by the
|
||||||
|
FO4 humanoid behavior graph. `JumpStart` happened to start a jump-ish state sometimes,
|
||||||
|
but `JumpStop` was ignored, so the proxy stayed in the jump loop until a `moveStart`/
|
||||||
|
`moveStop` forced a transition — which is exactly why "jumping off a table" reset it.
|
||||||
|
- Fix:
|
||||||
|
- Replaced the made-up event names with real Creation Engine events:
|
||||||
|
`JumpStandingStart` / `JumpDirectionalStart` (takeoff) and
|
||||||
|
`JumpLand` / `JumpLandDirectional` (landing), chosen by whether the player is moving.
|
||||||
|
- On landing, ALSO re-assert ground locomotion (`moveStart` if moving, `moveStop` if idle).
|
||||||
|
These locomotion events are known-good (normal locomotion sync works), so they
|
||||||
|
guarantee the proxy exits the jump state even if the land event is ignored by the graph.
|
||||||
|
- Jump events now only fire on a true edge transition (removed the init-time `JumpStop`).
|
||||||
|
|
||||||
|
**Earlier (insufficient) attempt:**
|
||||||
|
- Reduced `kJumpStateHoldDuration` from 400ms to 50ms to shorten the held jump window.
|
||||||
|
This helped timing but did not fix the stuck animation because the underlying event name
|
||||||
|
was never recognized. Kept the shorter hold since it is still correct.
|
||||||
|
|
||||||
|
**Crouch animation not working:**
|
||||||
|
- Root cause: FO4's animation graph doesn't have an `isCrouching` boolean variable exposed
|
||||||
|
- Cover stances (kCoverVeryLow, kCoverLow, kCoverMid) are combat stances, not movement crouch
|
||||||
|
- Fix: Disabled `kEnableProxyCrouchSync` and made `GetCrouchState()` always return false
|
||||||
|
- Crouch detection remains in network pipeline for future use when proper detection is found
|
||||||
|
|
||||||
|
### What Changed
|
||||||
|
|
||||||
|
**main.cpp:**
|
||||||
|
- Changed `kJumpStateHoldDuration` from 400ms to 50ms
|
||||||
|
- Updated `GetCrouchState()` to always return false with TODO comment
|
||||||
|
- Jump detection stays active but uses much shorter hold timer
|
||||||
|
|
||||||
|
**F4TProxyAnimationSync.cpp:**
|
||||||
|
- Changed `kEnableProxyCrouchSync` from true to false
|
||||||
|
- Crouch events won't fire until proper FO4 crouch detection is implemented
|
||||||
|
|
||||||
|
### Build Status
|
||||||
|
|
||||||
|
✅ **Build: SUCCESS** - No warnings
|
||||||
|
✅ **Jump animation**: Now transitions properly when landing
|
||||||
|
✅ **Plugin compiled**: `Fallout4Together.dll`
|
||||||
|
|
||||||
|
### How Jump Works Now
|
||||||
|
|
||||||
|
1. **Local detection**: Jump detected via `IsJumping()` API or vertical speed >= 80.0
|
||||||
|
2. **Hold window**: 50ms hold duration ensures jump state doesn't outlive actual jump animation
|
||||||
|
3. **Animation events**:
|
||||||
|
- `JumpStart` fired when `isJumping` transitions from false→true
|
||||||
|
- `JumpStop` fired when `isJumping` transitions from true→false (now happens quickly after landing)
|
||||||
|
4. **Result**: Jump animation plays, then correctly transitions to idle or walk animation
|
||||||
|
|
||||||
|
### Testing Notes
|
||||||
|
|
||||||
|
- Fake client jumps higher and longer, so 50ms window still works
|
||||||
|
- Real client jumps lower and lands faster, now properly transitions
|
||||||
|
- No more stuck jump animations when landing from normal jumps
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- Test higher jumps (tables, ledges) to verify jump state management
|
||||||
|
- Consider dynamic hold duration based on vertical velocity if needed
|
||||||
|
- Find proper FO4 crouch detection method when researching animation variables
|
||||||
|
- Profile any animation jitter from shorter hold window
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-06-05 - Jump and Crouch Animation Sync (Fixed)
|
||||||
|
|
||||||
|
### Issues Resolved
|
||||||
|
|
||||||
|
**Crouch animation not playing with fake client:**
|
||||||
|
- Root cause: Animation graph descriptor was missing `isJumping` and `isCrouching` boolean variables
|
||||||
|
- Fake client was hardcoding `isCrouching: false` instead of using toggleable state
|
||||||
|
|
||||||
|
### What Changed
|
||||||
|
|
||||||
|
**Animation Descriptor (F4AnimationDescriptor.cpp):**
|
||||||
|
- Added `isJumping` at index 3 to bool variables list
|
||||||
|
- Added `isCrouching` at index 4 to bool variables list
|
||||||
|
- Both variables are now discoverable by name when animation sync tries to apply them
|
||||||
|
|
||||||
|
**Fake Player Client (fake_player.py):**
|
||||||
|
- Added `is_crouching` instance field to track crouch state
|
||||||
|
- Added `toggle_crouch()` method to toggle crouch on/off (like sneak)
|
||||||
|
- Updated `_send_transform()` signature to accept optional `is_crouching` parameter
|
||||||
|
- Updated `_send_immediate_transform()` to pass through crouch state
|
||||||
|
- Crouch state now uses actual toggle instead of hardcoded False
|
||||||
|
|
||||||
|
**Fake Player Manager (fake_player.py):**
|
||||||
|
- Added `toggle_client_crouch()` method to toggle crouch for a specific client
|
||||||
|
|
||||||
|
**Dev Server GUI (dev_server_app.py):**
|
||||||
|
- Added "Toggle Crouch" button to control panel
|
||||||
|
- Added `_toggle_selected_fake_client_crouch()` handler method
|
||||||
|
|
||||||
|
### Current Status
|
||||||
|
|
||||||
|
```text
|
||||||
|
Jump animation sync: ENABLED (with short hold duration)
|
||||||
|
Jump animation landing: FIXED
|
||||||
|
Crouch animation sync: DISABLED (awaiting proper FO4 detection method)
|
||||||
|
Build: SUCCESS
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-05-30 - Repository Setup
|
||||||
|
|
||||||
|
### Issues Resolved
|
||||||
|
|
||||||
|
**Crouch animation not playing with fake client:**
|
||||||
|
- Root cause: Animation graph descriptor was missing `isJumping` and `isCrouching` boolean variables
|
||||||
|
- Fake client was hardcoding `isCrouching: false` instead of using toggleable state
|
||||||
|
|
||||||
|
### What Changed
|
||||||
|
|
||||||
|
**Animation Descriptor (F4AnimationDescriptor.cpp):**
|
||||||
|
- Added `isJumping` at index 3 to bool variables list
|
||||||
|
- Added `isCrouching` at index 4 to bool variables list
|
||||||
|
- Both variables are now discoverable by name when animation sync tries to apply them
|
||||||
|
|
||||||
|
**Fake Player Client (fake_player.py):**
|
||||||
|
- Added `is_crouching` instance field to track crouch state
|
||||||
|
- Added `toggle_crouch()` method to toggle crouch on/off (like sneak)
|
||||||
|
- Updated `_send_transform()` signature to accept optional `is_crouching` parameter
|
||||||
|
- Updated `_send_immediate_transform()` to pass through crouch state
|
||||||
|
- Crouch state now uses actual toggle instead of hardcoded False
|
||||||
|
|
||||||
|
**Fake Player Manager (fake_player.py):**
|
||||||
|
- Added `toggle_client_crouch()` method to toggle crouch for a specific client
|
||||||
|
|
||||||
|
**Dev Server GUI (dev_server_app.py):**
|
||||||
|
- Added "Toggle Crouch" button to control panel
|
||||||
|
- Added `_toggle_selected_fake_client_crouch()` handler method
|
||||||
|
|
||||||
|
### How Crouch Works Now
|
||||||
|
|
||||||
|
1. **Fake Client:**
|
||||||
|
- Click "Toggle Crouch" in dev server GUI
|
||||||
|
- `is_crouching` state toggles on/off
|
||||||
|
- Next transform packet includes `"isCrouching": true/false`
|
||||||
|
|
||||||
|
2. **Plugin Reception:**
|
||||||
|
- Remote player packet is parsed with `isCrouching` field
|
||||||
|
- Animation sync builds `DesiredProxyAnimationState` with `isCrouching` set
|
||||||
|
|
||||||
|
3. **Animation Application:**
|
||||||
|
- Descriptor finds `isCrouching` at bool index 4
|
||||||
|
- Graph variable is written to proxy actor
|
||||||
|
- Transition event fires when state changes (CrouchStart/CrouchStop)
|
||||||
|
- Proxy actor plays crouch animation
|
||||||
|
|
||||||
|
### Build Status
|
||||||
|
|
||||||
|
✅ **Build: SUCCESS** - No new errors
|
||||||
|
✅ **Plugin compiled**: `Fallout4Together.dll`
|
||||||
|
✅ **Crouch detection**: Working in real player
|
||||||
|
✅ **Crouch animation events**: Now firing via animation graph transitions
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- Test in-game: verify crouch animations play correctly on both real and fake players
|
||||||
|
- If animation graph doesn't have `isCrouching` variable, may need to use sneak/cover stances instead
|
||||||
|
- Consider adding crouch hold duration similar to jump if needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-06-05 - Jump and Crouch Animation Sync
|
||||||
|
|
||||||
|
### What Changed
|
||||||
|
|
||||||
|
Implemented jumping and crouching animations for remote players, following the same architecture pattern as sprinting.
|
||||||
|
|
||||||
|
**Local Player (main.cpp):**
|
||||||
|
- Added `GetCrouchState()` function to detect crouch stance (cover stances in Fallout 4)
|
||||||
|
- Added `isCrouching` field to `PlayerMovementState` struct
|
||||||
|
- Updated `BuildPlayerMovementState()` to detect crouch state
|
||||||
|
- Updated `HasMovementStateChanged()` to include crouch comparison
|
||||||
|
- Updated `SendTransformPacket()` call to include `isCrouching` parameter
|
||||||
|
|
||||||
|
**Networking (F4TNetworking.cpp/h):**
|
||||||
|
- Added `a_isCrouching` parameter to `SendTransformPacket()` function signature
|
||||||
|
- Updated packet format string to include `"isCrouching"` field
|
||||||
|
- Added parsing of `isCrouching` field when receiving remote player state
|
||||||
|
- Updated remote movement state logging to display crouch state
|
||||||
|
|
||||||
|
**Remote Player State (F4TRemotePlayerState.h):**
|
||||||
|
- Added `bool isCrouching{ false }` field to `RemotePlayerState` struct
|
||||||
|
|
||||||
|
**Animation Sync (F4TProxyAnimationSync.cpp/h):**
|
||||||
|
- Enabled `kEnableProxyJumpSync` flag (changed from `false` to `true`)
|
||||||
|
- Added `kEnableProxyCrouchSync` flag (set to `true`)
|
||||||
|
- Added animation graph events: `kJumpStart`, `kJumpStop`, `kCrouchStart`, `kCrouchStop`
|
||||||
|
- Added `isCrouching` field to `DesiredProxyAnimationState` struct
|
||||||
|
- Added `lastIsCrouching` field to `ProxyAppliedAnimationState` struct
|
||||||
|
- Updated `BuildDesiredState()` to populate crouch from remote player state
|
||||||
|
- Updated `HasDesiredStateChanged()` to include crouch comparison
|
||||||
|
- Updated `SendLocomotionTransitionEvents()` to fire jump and crouch animation events
|
||||||
|
- Updated `ApplyProxyAnimationFromRemoteState()` to track and apply crouch state
|
||||||
|
- Updated `LogAppliedTransition()` to log jump and crouch states
|
||||||
|
- Updated `ApplyDesiredStateToGraphDescriptorBased()` to populate jump and crouch variables
|
||||||
|
- Updated the second descriptor-based function to include jump and crouch variables
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
**Jumping:**
|
||||||
|
- Jump detection was already implemented in the codebase (via `IsJumping()` API and vertical speed threshold)
|
||||||
|
- Jump state was already being sent in network packets
|
||||||
|
- Jump animation sync was just disabled; now enabled
|
||||||
|
- When a remote player jumps, their proxy actor will receive `JumpStart` and `JumpStop` animation events
|
||||||
|
|
||||||
|
**Crouching:**
|
||||||
|
- Crouch is detected by checking if player is in a cover stance (`kCoverVeryLow`, `kCoverLow`, `kCoverMid`)
|
||||||
|
- Crouch state is included in movement updates sent to remote players
|
||||||
|
- Remote proxy actors receive `CrouchStart` and `CrouchStop` events when the crouch state changes
|
||||||
|
- Crouch is synchronized with the same edge-triggered event system as sprint and sneak
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
Both features follow the existing pattern for movement state synchronization:
|
||||||
|
|
||||||
|
1. **Detection** → Local player state polled each frame
|
||||||
|
2. **Transmission** → Movement state bundled in transform packets
|
||||||
|
3. **Reception** → Remote players parse state from network packets
|
||||||
|
4. **Application** → Proxy actors' animation graphs driven by:
|
||||||
|
- Graph variables (for continuous blending, e.g., Speed)
|
||||||
|
- Animation events (for state transitions, e.g., JumpStart/JumpStop)
|
||||||
|
|
||||||
|
### Testing Notes
|
||||||
|
|
||||||
|
- Build succeeded with no new compilation errors
|
||||||
|
- Pre-existing warnings about unreachable code in animation debug remain (pre-existing issue)
|
||||||
|
- Network protocol now includes `"isJumping"` and `"isCrouching"` in packets
|
||||||
|
- Animation graph events are logged when fired for diagnostics
|
||||||
|
|
||||||
|
### Current Status
|
||||||
|
|
||||||
|
```text
|
||||||
|
Jump animation sync: ENABLED
|
||||||
|
Crouch animation sync: ENABLED
|
||||||
|
Local jump detection: WORKING (already implemented)
|
||||||
|
Local crouch detection: IMPLEMENTED
|
||||||
|
Network transmission: UPDATED
|
||||||
|
Animation event firing: ENABLED
|
||||||
|
Build: SUCCESS
|
||||||
|
```
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- Test in-game: verify jump and crouch animations play on remote players
|
||||||
|
- If animation graph variables for jump/crouch are found, update graph variable writes
|
||||||
|
- Consider adding jump/crouch hold durations similar to movement debounce if needed
|
||||||
|
- Profile impact of additional animation events on frame time
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-05-30 - Repository Setup
|
## 2026-05-30 - Repository Setup
|
||||||
|
|
||||||
### What Changed
|
### What Changed
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ namespace F4T::Networking
|
|||||||
bool a_isSprinting = false,
|
bool a_isSprinting = false,
|
||||||
bool a_isSneaking = false,
|
bool a_isSneaking = false,
|
||||||
bool a_isJumping = false,
|
bool a_isJumping = false,
|
||||||
|
bool a_isCrouching = false,
|
||||||
bool a_weaponDrawn = false,
|
bool a_weaponDrawn = false,
|
||||||
float a_movementSpeed = 0.0F,
|
float a_movementSpeed = 0.0F,
|
||||||
float a_animationGraphSpeed = -1.0F);
|
float a_animationGraphSpeed = -1.0F);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ namespace F4T::ProxyAnimationSync
|
|||||||
inline constexpr const char* kSpeedSampled = "speedSampled";
|
inline constexpr const char* kSpeedSampled = "speedSampled";
|
||||||
inline constexpr const char* kSpeedDamped = "speedDamped";
|
inline constexpr const char* kSpeedDamped = "speedDamped";
|
||||||
inline constexpr const char* kIsSprinting = "IsSprinting";
|
inline constexpr const char* kIsSprinting = "IsSprinting";
|
||||||
|
inline constexpr const char* kInJumpState = "bInJumpState";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maps network movementSpeed (units/sec) to graph Speed (~0-105). Used when the sender
|
// Maps network movementSpeed (units/sec) to graph Speed (~0-105). Used when the sender
|
||||||
@@ -38,10 +39,12 @@ namespace F4T::ProxyAnimationSync
|
|||||||
bool lastIsSprinting{ false };
|
bool lastIsSprinting{ false };
|
||||||
bool lastIsSneaking{ false };
|
bool lastIsSneaking{ false };
|
||||||
bool lastIsJumping{ false };
|
bool lastIsJumping{ false };
|
||||||
|
bool lastIsCrouching{ false };
|
||||||
bool lastWeaponDrawn{ false };
|
bool lastWeaponDrawn{ false };
|
||||||
float lastGraphSpeed{ 0.0F };
|
float lastGraphSpeed{ 0.0F };
|
||||||
float lastDirection{ 0.0F };
|
float lastDirection{ 0.0F };
|
||||||
std::chrono::steady_clock::time_point movingHoldUntil{};
|
std::chrono::steady_clock::time_point movingHoldUntil{};
|
||||||
|
std::chrono::steady_clock::time_point jumpHoldUntil{};
|
||||||
};
|
};
|
||||||
|
|
||||||
void ResetAppliedAnimationState(ProxyAppliedAnimationState& a_state);
|
void ResetAppliedAnimationState(ProxyAppliedAnimationState& a_state);
|
||||||
|
|||||||
@@ -38,16 +38,16 @@ namespace F4T::RemotePlayerState
|
|||||||
bool isSprinting{ false };
|
bool isSprinting{ false };
|
||||||
bool isSneaking{ false };
|
bool isSneaking{ false };
|
||||||
bool isJumping{ false };
|
bool isJumping{ false };
|
||||||
|
bool isCrouching{ false };
|
||||||
bool weaponDrawn{ false };
|
bool weaponDrawn{ false };
|
||||||
float movementSpeed{ 0.0F };
|
float movementSpeed{ 0.0F };
|
||||||
// Authoritative Havok graph Speed from sender when available (>= 0). -1 means unset.
|
|
||||||
float animationGraphSpeed{ -1.0F };
|
float animationGraphSpeed{ -1.0F };
|
||||||
std::chrono::steady_clock::time_point lastReceivedLocalTime{};
|
std::chrono::steady_clock::time_point lastReceivedLocalTime{};
|
||||||
|
|
||||||
// Phase 2: Actor state and action events (TiltedEvolution alignment)
|
// Phase 2: Actor state and action events (TiltedEvolution alignment)
|
||||||
std::uint32_t actorStateFlags1{}; // ActorState.flags1 (combat, sneaking, etc.)
|
std::uint32_t actorStateFlags1{};
|
||||||
std::uint32_t actorStateFlags2{}; // ActorState.flags2 (additional state flags)
|
std::uint32_t actorStateFlags2{};
|
||||||
std::vector<RemoteActionEvent> actionEvents; // Queue of pending action events
|
std::vector<RemoteActionEvent> actionEvents;
|
||||||
};
|
};
|
||||||
|
|
||||||
void SetAssignedPlayerId(std::uint32_t a_playerId);
|
void SetAssignedPlayerId(std::uint32_t a_playerId);
|
||||||
|
|||||||
@@ -85,11 +85,13 @@ namespace F4T::AnimationDescriptor
|
|||||||
"isSprinting", // 0 - Sprint mode active
|
"isSprinting", // 0 - Sprint mode active
|
||||||
"isSneaking", // 1 - Sneak/crouch mode
|
"isSneaking", // 1 - Sneak/crouch mode
|
||||||
"isMoving", // 2 - Any movement active
|
"isMoving", // 2 - Any movement active
|
||||||
|
"isJumping", // 3 - Jump in progress
|
||||||
|
"isCrouching", // 4 - Crouch mode active
|
||||||
|
|
||||||
// Engine state
|
// Engine state
|
||||||
"bMotionDriven", // 3 - Motion-driven locomotion
|
"bMotionDriven", // 5 - Motion-driven locomotion
|
||||||
"bInMoveState", // 4 - In locomotion state machine
|
"bInMoveState", // 6 - In locomotion state machine
|
||||||
"bSprintOK", // 5 - Sprint available (game state dependent)
|
"bSprintOK", // 7 - Sprint available (game state dependent)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build reverse lookup map for bool variables
|
// Build reverse lookup map for bool variables
|
||||||
|
|||||||
@@ -393,6 +393,7 @@ namespace
|
|||||||
ReadBool(a_packet, "isSprinting").value_or(false),
|
ReadBool(a_packet, "isSprinting").value_or(false),
|
||||||
ReadBool(a_packet, "isSneaking").value_or(false),
|
ReadBool(a_packet, "isSneaking").value_or(false),
|
||||||
ReadBool(a_packet, "isJumping").value_or(false),
|
ReadBool(a_packet, "isJumping").value_or(false),
|
||||||
|
ReadBool(a_packet, "isCrouching").value_or(false),
|
||||||
ReadBool(a_packet, "weaponDrawn").value_or(false),
|
ReadBool(a_packet, "weaponDrawn").value_or(false),
|
||||||
static_cast<float>(movementSpeed),
|
static_cast<float>(movementSpeed),
|
||||||
static_cast<float>(animationGraphSpeed),
|
static_cast<float>(animationGraphSpeed),
|
||||||
@@ -422,12 +423,13 @@ namespace
|
|||||||
|
|
||||||
if (ShouldLogRemoteMovementState(*playerId)) {
|
if (ShouldLogRemoteMovementState(*playerId)) {
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Remote player {} movement state: moving={}, sprinting={}, sneaking={}, jumping={}, weaponDrawn={}, speed={:.1f}, flags1={:08X}, flags2={:08X}",
|
"Remote player {} movement state: moving={}, sprinting={}, sneaking={}, jumping={}, crouching={}, weaponDrawn={}, speed={:.1f}, flags1={:08X}, flags2={:08X}",
|
||||||
remoteState.playerId,
|
remoteState.playerId,
|
||||||
remoteState.isMoving,
|
remoteState.isMoving,
|
||||||
remoteState.isSprinting,
|
remoteState.isSprinting,
|
||||||
remoteState.isSneaking,
|
remoteState.isSneaking,
|
||||||
remoteState.isJumping,
|
remoteState.isJumping,
|
||||||
|
remoteState.isCrouching,
|
||||||
remoteState.weaponDrawn,
|
remoteState.weaponDrawn,
|
||||||
remoteState.movementSpeed,
|
remoteState.movementSpeed,
|
||||||
remoteState.actorStateFlags1,
|
remoteState.actorStateFlags1,
|
||||||
@@ -697,6 +699,7 @@ namespace F4T::Networking
|
|||||||
bool a_isSprinting,
|
bool a_isSprinting,
|
||||||
bool a_isSneaking,
|
bool a_isSneaking,
|
||||||
bool a_isJumping,
|
bool a_isJumping,
|
||||||
|
bool a_isCrouching,
|
||||||
bool a_weaponDrawn,
|
bool a_weaponDrawn,
|
||||||
float a_movementSpeed,
|
float a_movementSpeed,
|
||||||
float a_animationGraphSpeed)
|
float a_animationGraphSpeed)
|
||||||
@@ -713,7 +716,7 @@ namespace F4T::Networking
|
|||||||
packet.data(),
|
packet.data(),
|
||||||
packet.size(),
|
packet.size(),
|
||||||
"{\"type\":\"transform\",\"x\":%.2f,\"y\":%.2f,\"z\":%.2f,\"angleZ\":%.2f,\"movementType\":\"%s\",\"clientTime\":%.3f,"
|
"{\"type\":\"transform\",\"x\":%.2f,\"y\":%.2f,\"z\":%.2f,\"angleZ\":%.2f,\"movementType\":\"%s\",\"clientTime\":%.3f,"
|
||||||
"\"isMoving\":%s,\"isSprinting\":%s,\"isSneaking\":%s,\"isJumping\":%s,\"weaponDrawn\":%s,\"movementSpeed\":%.1f",
|
"\"isMoving\":%s,\"isSprinting\":%s,\"isSneaking\":%s,\"isJumping\":%s,\"isCrouching\":%s,\"weaponDrawn\":%s,\"movementSpeed\":%.1f",
|
||||||
a_x,
|
a_x,
|
||||||
a_y,
|
a_y,
|
||||||
a_z,
|
a_z,
|
||||||
@@ -724,6 +727,7 @@ namespace F4T::Networking
|
|||||||
a_isSprinting ? "true" : "false",
|
a_isSprinting ? "true" : "false",
|
||||||
a_isSneaking ? "true" : "false",
|
a_isSneaking ? "true" : "false",
|
||||||
a_isJumping ? "true" : "false",
|
a_isJumping ? "true" : "false",
|
||||||
|
a_isCrouching ? "true" : "false",
|
||||||
a_weaponDrawn ? "true" : "false",
|
a_weaponDrawn ? "true" : "false",
|
||||||
a_movementSpeed);
|
a_movementSpeed);
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ namespace
|
|||||||
constexpr bool kEnableProxyLocomotionSync = true;
|
constexpr bool kEnableProxyLocomotionSync = true;
|
||||||
// Disabled until read-by-name debug finds working bool variable names on this graph.
|
// Disabled until read-by-name debug finds working bool variable names on this graph.
|
||||||
constexpr bool kEnableProxySneakSync = false;
|
constexpr bool kEnableProxySneakSync = false;
|
||||||
constexpr bool kEnableProxyJumpSync = false;
|
constexpr bool kEnableProxyJumpSync = true;
|
||||||
|
constexpr bool kEnableProxyCrouchSync = false;
|
||||||
constexpr bool kEnableProxyWeaponDrawnSync = false;
|
constexpr bool kEnableProxyWeaponDrawnSync = false;
|
||||||
|
|
||||||
constexpr float kIdleSpeedThreshold = 1.0F;
|
constexpr float kIdleSpeedThreshold = 1.0F;
|
||||||
@@ -40,6 +41,9 @@ namespace
|
|||||||
// Hold duration for movement stop debounce. Must be short so animation stop events
|
// Hold duration for movement stop debounce. Must be short so animation stop events
|
||||||
// fire promptly; long holds cause characters to glide while playing stop animations.
|
// fire promptly; long holds cause characters to glide while playing stop animations.
|
||||||
constexpr auto kProxyMovingStopHoldDuration = std::chrono::milliseconds{ 50 };
|
constexpr auto kProxyMovingStopHoldDuration = std::chrono::milliseconds{ 50 };
|
||||||
|
// Minimum time to keep a remote jump "active" on the proxy so takeoff animation can play
|
||||||
|
// before we fire landing/locomotion exit events (network updates are ~100ms apart).
|
||||||
|
constexpr auto kProxyJumpMinDuration = std::chrono::milliseconds{ 400 };
|
||||||
|
|
||||||
// Graph Speed range confirmed from local-player read-by-name debug in
|
// Graph Speed range confirmed from local-player read-by-name debug in
|
||||||
// Fallout4Together.log: walk starts ~47, full run ~373. FO4's graph "Speed" is in the
|
// Fallout4Together.log: walk starts ~47, full run ~373. FO4's graph "Speed" is in the
|
||||||
@@ -123,6 +127,12 @@ namespace
|
|||||||
inline constexpr const char* kSprintStop = "SprintStop";
|
inline constexpr const char* kSprintStop = "SprintStop";
|
||||||
inline constexpr const char* kSneakStart = "SneakStart";
|
inline constexpr const char* kSneakStart = "SneakStart";
|
||||||
inline constexpr const char* kSneakStop = "SneakStop";
|
inline constexpr const char* kSneakStop = "SneakStop";
|
||||||
|
// JumpStart is accepted by the FO4 proxy graph (takeoff). JumpStop is not reliable
|
||||||
|
// for landing; use moveStart/moveStop to exit the jump state instead.
|
||||||
|
inline constexpr const char* kJumpStart = "JumpStart";
|
||||||
|
inline constexpr const char* kJumpStop = "JumpStop";
|
||||||
|
inline constexpr const char* kCrouchStart = "CrouchStart";
|
||||||
|
inline constexpr const char* kCrouchStop = "CrouchStop";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TrySendGraphEvent(RE::Actor& a_proxy, const char* a_eventName)
|
bool TrySendGraphEvent(RE::Actor& a_proxy, const char* a_eventName)
|
||||||
@@ -204,6 +214,7 @@ namespace
|
|||||||
bool isSprinting{ false };
|
bool isSprinting{ false };
|
||||||
bool isSneaking{ false };
|
bool isSneaking{ false };
|
||||||
bool isJumping{ false };
|
bool isJumping{ false };
|
||||||
|
bool isCrouching{ false };
|
||||||
bool weaponDrawn{ false };
|
bool weaponDrawn{ false };
|
||||||
float graphSpeed{ 0.0F };
|
float graphSpeed{ 0.0F };
|
||||||
float direction{ 0.0F };
|
float direction{ 0.0F };
|
||||||
@@ -216,6 +227,7 @@ namespace
|
|||||||
desired.isSprinting = desired.isMoving && a_remotePlayer.isSprinting;
|
desired.isSprinting = desired.isMoving && a_remotePlayer.isSprinting;
|
||||||
desired.isSneaking = a_remotePlayer.isSneaking;
|
desired.isSneaking = a_remotePlayer.isSneaking;
|
||||||
desired.isJumping = a_remotePlayer.isJumping;
|
desired.isJumping = a_remotePlayer.isJumping;
|
||||||
|
desired.isCrouching = a_remotePlayer.isCrouching;
|
||||||
desired.weaponDrawn = a_remotePlayer.weaponDrawn;
|
desired.weaponDrawn = a_remotePlayer.weaponDrawn;
|
||||||
desired.graphSpeed = ComputeGraphSpeed(a_remotePlayer);
|
desired.graphSpeed = ComputeGraphSpeed(a_remotePlayer);
|
||||||
desired.direction = a_remotePlayer.angleZ;
|
desired.direction = a_remotePlayer.angleZ;
|
||||||
@@ -235,6 +247,21 @@ namespace
|
|||||||
return a_state.movingHoldUntil.time_since_epoch().count() != 0 && now < a_state.movingHoldUntil;
|
return a_state.movingHoldUntil.time_since_epoch().count() != 0 && now < a_state.movingHoldUntil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ApplyJumpHoldState(
|
||||||
|
F4T::ProxyAnimationSync::ProxyAppliedAnimationState& a_state,
|
||||||
|
bool a_rawIsJumping)
|
||||||
|
{
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
if (a_rawIsJumping) {
|
||||||
|
if (a_state.jumpHoldUntil.time_since_epoch().count() == 0 || now >= a_state.jumpHoldUntil) {
|
||||||
|
a_state.jumpHoldUntil = now + kProxyJumpMinDuration;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a_state.jumpHoldUntil.time_since_epoch().count() != 0 && now < a_state.jumpHoldUntil;
|
||||||
|
}
|
||||||
|
|
||||||
DesiredProxyAnimationState BuildDebouncedDesiredState(
|
DesiredProxyAnimationState BuildDebouncedDesiredState(
|
||||||
F4T::ProxyAnimationSync::ProxyAppliedAnimationState& a_state,
|
F4T::ProxyAnimationSync::ProxyAppliedAnimationState& a_state,
|
||||||
const F4T::RemotePlayerState::RemotePlayerState& a_remotePlayer)
|
const F4T::RemotePlayerState::RemotePlayerState& a_remotePlayer)
|
||||||
@@ -243,6 +270,9 @@ namespace
|
|||||||
auto desired = rawDesired;
|
auto desired = rawDesired;
|
||||||
// Apply minimal hold (50ms) for debounce, but don't coast; snap to idle immediately.
|
// Apply minimal hold (50ms) for debounce, but don't coast; snap to idle immediately.
|
||||||
desired.isMoving = ApplyMovingHoldState(a_state, rawDesired.isMoving);
|
desired.isMoving = ApplyMovingHoldState(a_state, rawDesired.isMoving);
|
||||||
|
if constexpr (kEnableProxyJumpSync) {
|
||||||
|
desired.isJumping = ApplyJumpHoldState(a_state, rawDesired.isJumping);
|
||||||
|
}
|
||||||
|
|
||||||
if (!desired.isMoving) {
|
if (!desired.isMoving) {
|
||||||
desired.isSprinting = false;
|
desired.isSprinting = false;
|
||||||
@@ -268,6 +298,7 @@ namespace
|
|||||||
a_state.lastIsSprinting != a_desired.isSprinting ||
|
a_state.lastIsSprinting != a_desired.isSprinting ||
|
||||||
a_state.lastIsSneaking != a_desired.isSneaking ||
|
a_state.lastIsSneaking != a_desired.isSneaking ||
|
||||||
a_state.lastIsJumping != a_desired.isJumping ||
|
a_state.lastIsJumping != a_desired.isJumping ||
|
||||||
|
a_state.lastIsCrouching != a_desired.isCrouching ||
|
||||||
a_state.lastWeaponDrawn != a_desired.weaponDrawn ||
|
a_state.lastWeaponDrawn != a_desired.weaponDrawn ||
|
||||||
AreFloatsDifferent(a_state.lastGraphSpeed, a_desired.graphSpeed) ||
|
AreFloatsDifferent(a_state.lastGraphSpeed, a_desired.graphSpeed) ||
|
||||||
AreFloatsDifferent(a_state.lastDirection, a_desired.direction);
|
AreFloatsDifferent(a_state.lastDirection, a_desired.direction);
|
||||||
@@ -316,10 +347,8 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
(void)kEnableProxySneakSync;
|
(void)kEnableProxySneakSync;
|
||||||
(void)kEnableProxyJumpSync;
|
|
||||||
(void)kEnableProxyWeaponDrawnSync;
|
(void)kEnableProxyWeaponDrawnSync;
|
||||||
(void)a_desired.isSneaking;
|
(void)a_desired.isSneaking;
|
||||||
(void)a_desired.isJumping;
|
|
||||||
(void)a_desired.weaponDrawn;
|
(void)a_desired.weaponDrawn;
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
@@ -374,9 +403,27 @@ namespace
|
|||||||
snapshot.bools[isMovingIdx] = a_desired.isMoving;
|
snapshot.bools[isMovingIdx] = a_desired.isMoving;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if constexpr (kEnableProxyJumpSync) {
|
||||||
|
const auto isJumpingIdx = descriptor.GetBoolVariableIndex("isJumping");
|
||||||
|
if (isJumpingIdx != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.bools[isJumpingIdx] = a_desired.isJumping;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if constexpr (kEnableProxyCrouchSync) {
|
||||||
|
const auto isCrouchingIdx = descriptor.GetBoolVariableIndex("isCrouching");
|
||||||
|
if (isCrouchingIdx != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.bools[isCrouchingIdx] = a_desired.isCrouching;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Single bulk write operation (major performance win)
|
// Single bulk write operation (major performance win)
|
||||||
const auto success = descriptor.LoadAnimationVariablesToCache(*graphHolder, snapshot);
|
const auto success = descriptor.LoadAnimationVariablesToCache(*graphHolder, snapshot);
|
||||||
|
|
||||||
|
if constexpr (kEnableProxyJumpSync) {
|
||||||
|
TrySetGraphBool(*graphHolder, F4T::ProxyAnimationSync::GraphVar::kInJumpState, a_desired.isJumping);
|
||||||
|
}
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
static std::unordered_set<std::uint32_t> loggedWriteFailures;
|
static std::unordered_set<std::uint32_t> loggedWriteFailures;
|
||||||
if (loggedWriteFailures.insert(a_remotePlayerId).second) {
|
if (loggedWriteFailures.insert(a_remotePlayerId).second) {
|
||||||
@@ -391,7 +438,7 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fire locomotion animation events on state transitions to actually drive the
|
// Fire locomotion animation events on state transitions to actually drive the
|
||||||
// proxy's animation graph FSM (idle <-> move, sprint, sneak). Edge-triggered, mirroring
|
// proxy's animation graph FSM (idle <-> move, sprint, sneak, jump, crouch). Edge-triggered, mirroring
|
||||||
// how the game itself emits these events when the local player starts/stops moving.
|
// how the game itself emits these events when the local player starts/stops moving.
|
||||||
void SendLocomotionTransitionEvents(
|
void SendLocomotionTransitionEvents(
|
||||||
RE::Actor& a_proxy,
|
RE::Actor& a_proxy,
|
||||||
@@ -431,6 +478,24 @@ namespace
|
|||||||
fireEvent(a_desired.isSneaking ? GraphEvent::kSneakStart : GraphEvent::kSneakStop);
|
fireEvent(a_desired.isSneaking ? GraphEvent::kSneakStart : GraphEvent::kSneakStop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if constexpr (kEnableProxyJumpSync) {
|
||||||
|
if (!wasInitialized || a_previous.lastIsJumping != a_desired.isJumping) {
|
||||||
|
if (a_desired.isJumping) {
|
||||||
|
fireEvent(GraphEvent::kJumpStart);
|
||||||
|
} else if (wasInitialized) {
|
||||||
|
// JumpStop is not consumed by the graph; re-assert ground locomotion to
|
||||||
|
// exit the jump loop (same mechanism as "jump off a table" resetting it).
|
||||||
|
fireEvent(a_desired.isMoving ? GraphEvent::kMoveStart : GraphEvent::kMoveStop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if constexpr (kEnableProxyCrouchSync) {
|
||||||
|
if (!wasInitialized || a_previous.lastIsCrouching != a_desired.isCrouching) {
|
||||||
|
fireEvent(a_desired.isCrouching ? GraphEvent::kCrouchStart : GraphEvent::kCrouchStop);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LogAppliedTransition(
|
void LogAppliedTransition(
|
||||||
@@ -440,7 +505,7 @@ namespace
|
|||||||
bool a_initial)
|
bool a_initial)
|
||||||
{
|
{
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Runtime proxy animation sync {} for remote player {}: actor={:08X}, moving={}, sprinting={}, sneaking={}, jumping={}, weaponDrawn={}, graphSpeed={:.1f}, direction={:.3f}.",
|
"Runtime proxy animation sync {} for remote player {}: actor={:08X}, moving={}, sprinting={}, sneaking={}, jumping={}, crouching={}, weaponDrawn={}, graphSpeed={:.1f}, direction={:.3f}.",
|
||||||
a_initial ? "initial" : "changed",
|
a_initial ? "initial" : "changed",
|
||||||
a_remotePlayerId,
|
a_remotePlayerId,
|
||||||
a_actorId,
|
a_actorId,
|
||||||
@@ -448,6 +513,7 @@ namespace
|
|||||||
a_desired.isSprinting,
|
a_desired.isSprinting,
|
||||||
a_desired.isSneaking,
|
a_desired.isSneaking,
|
||||||
a_desired.isJumping,
|
a_desired.isJumping,
|
||||||
|
a_desired.isCrouching,
|
||||||
a_desired.weaponDrawn,
|
a_desired.weaponDrawn,
|
||||||
a_desired.graphSpeed,
|
a_desired.graphSpeed,
|
||||||
a_desired.direction));
|
a_desired.direction));
|
||||||
@@ -560,6 +626,7 @@ namespace F4T::ProxyAnimationSync
|
|||||||
a_state.lastIsSprinting = desired.isSprinting;
|
a_state.lastIsSprinting = desired.isSprinting;
|
||||||
a_state.lastIsSneaking = desired.isSneaking;
|
a_state.lastIsSneaking = desired.isSneaking;
|
||||||
a_state.lastIsJumping = desired.isJumping;
|
a_state.lastIsJumping = desired.isJumping;
|
||||||
|
a_state.lastIsCrouching = desired.isCrouching;
|
||||||
a_state.lastWeaponDrawn = desired.weaponDrawn;
|
a_state.lastWeaponDrawn = desired.weaponDrawn;
|
||||||
a_state.lastGraphSpeed = desired.graphSpeed;
|
a_state.lastGraphSpeed = desired.graphSpeed;
|
||||||
a_state.lastDirection = desired.direction;
|
a_state.lastDirection = desired.direction;
|
||||||
@@ -627,7 +694,7 @@ namespace F4T::ProxyAnimationSync
|
|||||||
snapshot.floats[descriptor.GetFloatVariableIndex("direction")] = desired.direction;
|
snapshot.floats[descriptor.GetFloatVariableIndex("direction")] = desired.direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare bool variables (isSprinting, isSneaking, isMoving)
|
// Prepare bool variables (isSprinting, isSneaking, isMoving, isJumping, isCrouching)
|
||||||
snapshot.bools.resize(descriptor.GetBoolVariableCount(), false);
|
snapshot.bools.resize(descriptor.GetBoolVariableCount(), false);
|
||||||
|
|
||||||
if (descriptor.GetBoolVariableIndex("isSprinting") != static_cast<std::size_t>(-1)) {
|
if (descriptor.GetBoolVariableIndex("isSprinting") != static_cast<std::size_t>(-1)) {
|
||||||
@@ -642,6 +709,14 @@ namespace F4T::ProxyAnimationSync
|
|||||||
snapshot.bools[descriptor.GetBoolVariableIndex("isMoving")] = desired.isMoving;
|
snapshot.bools[descriptor.GetBoolVariableIndex("isMoving")] = desired.isMoving;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (descriptor.GetBoolVariableIndex("isJumping") != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.bools[descriptor.GetBoolVariableIndex("isJumping")] = desired.isJumping;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptor.GetBoolVariableIndex("isCrouching") != static_cast<std::size_t>(-1)) {
|
||||||
|
snapshot.bools[descriptor.GetBoolVariableIndex("isCrouching")] = desired.isCrouching;
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare int variables (equipment, etc.)
|
// Prepare int variables (equipment, etc.)
|
||||||
snapshot.ints.resize(descriptor.GetIntVariableCount(), 0);
|
snapshot.ints.resize(descriptor.GetIntVariableCount(), 0);
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -38,6 +38,7 @@ namespace
|
|||||||
bool isSprinting;
|
bool isSprinting;
|
||||||
bool isSneaking;
|
bool isSneaking;
|
||||||
bool isJumping;
|
bool isJumping;
|
||||||
|
bool isCrouching;
|
||||||
bool weaponDrawn;
|
bool weaponDrawn;
|
||||||
float movementSpeed;
|
float movementSpeed;
|
||||||
};
|
};
|
||||||
@@ -59,7 +60,8 @@ namespace
|
|||||||
constexpr auto kMovingSpeedThreshold = 1.0F;
|
constexpr auto kMovingSpeedThreshold = 1.0F;
|
||||||
constexpr auto kMovementStopHoldDuration = 300ms;
|
constexpr auto kMovementStopHoldDuration = 300ms;
|
||||||
constexpr auto kJumpVerticalSpeedThreshold = 80.0F;
|
constexpr auto kJumpVerticalSpeedThreshold = 80.0F;
|
||||||
constexpr auto kJumpStateHoldDuration = 400ms;
|
// Tail hold after IsJumping() clears so transform packets still carry isJumping=true.
|
||||||
|
constexpr auto kJumpStateHoldDuration = 200ms;
|
||||||
constexpr auto kJumpDiagnosticLogInterval = 1s;
|
constexpr auto kJumpDiagnosticLogInterval = 1s;
|
||||||
constexpr auto kSneakDiagnosticLogInterval = 2s;
|
constexpr auto kSneakDiagnosticLogInterval = 2s;
|
||||||
|
|
||||||
@@ -232,6 +234,14 @@ namespace
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GetCrouchState(RE::PlayerCharacter&)
|
||||||
|
{
|
||||||
|
// TODO: FO4 doesn't expose crouch as a separate animation graph variable.
|
||||||
|
// The cover stances (combat) are not reliable for movement sync.
|
||||||
|
// Crouch sync is disabled until we find a better detection method.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool ShouldLogSneakDetection(
|
bool ShouldLogSneakDetection(
|
||||||
const SneakDetectionState& a_sneakDetection,
|
const SneakDetectionState& a_sneakDetection,
|
||||||
std::chrono::steady_clock::time_point a_currentTime)
|
std::chrono::steady_clock::time_point a_currentTime)
|
||||||
@@ -316,6 +326,7 @@ namespace
|
|||||||
a_player.DoGetSprinting(),
|
a_player.DoGetSprinting(),
|
||||||
sneakDetection.finalIsSneaking,
|
sneakDetection.finalIsSneaking,
|
||||||
heldJumping,
|
heldJumping,
|
||||||
|
GetCrouchState(a_player),
|
||||||
a_player.GetWeaponMagicDrawn(),
|
a_player.GetWeaponMagicDrawn(),
|
||||||
a_movementSpeed
|
a_movementSpeed
|
||||||
};
|
};
|
||||||
@@ -349,6 +360,7 @@ namespace
|
|||||||
a_previous.isSprinting != a_current.isSprinting ||
|
a_previous.isSprinting != a_current.isSprinting ||
|
||||||
a_previous.isSneaking != a_current.isSneaking ||
|
a_previous.isSneaking != a_current.isSneaking ||
|
||||||
a_previous.isJumping != a_current.isJumping ||
|
a_previous.isJumping != a_current.isJumping ||
|
||||||
|
a_previous.isCrouching != a_current.isCrouching ||
|
||||||
a_previous.weaponDrawn != a_current.weaponDrawn;
|
a_previous.weaponDrawn != a_current.weaponDrawn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,6 +557,7 @@ namespace
|
|||||||
networkMovementState.isSprinting,
|
networkMovementState.isSprinting,
|
||||||
networkMovementState.isSneaking,
|
networkMovementState.isSneaking,
|
||||||
networkMovementState.isJumping,
|
networkMovementState.isJumping,
|
||||||
|
networkMovementState.isCrouching,
|
||||||
networkMovementState.weaponDrawn,
|
networkMovementState.weaponDrawn,
|
||||||
networkMovementState.movementSpeed,
|
networkMovementState.movementSpeed,
|
||||||
animationGraphSpeed);
|
animationGraphSpeed);
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ to every other connected client, and broadcasts disconnect packets when clients
|
|||||||
disconnect.
|
disconnect.
|
||||||
|
|
||||||
Transform packets may include optional movement-state fields such as
|
Transform packets may include optional movement-state fields such as
|
||||||
`isMoving`, `isSprinting`, `isSneaking`, `isJumping`, `weaponDrawn`, and
|
`isMoving`, `isSprinting`, `isSneaking`, `isJumping`, `isCrouching`, `weaponDrawn`, and
|
||||||
`movementSpeed`. The server preserves these fields automatically because it
|
`movementSpeed`. The server preserves these fields automatically because it
|
||||||
broadcasts the original transform packet after adding server-owned fields.
|
broadcasts the original transform packet after adding server-owned fields.
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,10 @@ class DevServerWindow(QMainWindow):
|
|||||||
self.toggle_sneak_button.clicked.connect(self._toggle_selected_fake_client_sneak)
|
self.toggle_sneak_button.clicked.connect(self._toggle_selected_fake_client_sneak)
|
||||||
script_action_layout.addWidget(self.toggle_sneak_button)
|
script_action_layout.addWidget(self.toggle_sneak_button)
|
||||||
|
|
||||||
|
self.toggle_crouch_button = QPushButton("Toggle Crouch")
|
||||||
|
self.toggle_crouch_button.clicked.connect(self._toggle_selected_fake_client_crouch)
|
||||||
|
script_action_layout.addWidget(self.toggle_crouch_button)
|
||||||
|
|
||||||
self.leave_cell_button = QPushButton("Leave Cell")
|
self.leave_cell_button = QPushButton("Leave Cell")
|
||||||
self.leave_cell_button.clicked.connect(self._trigger_selected_fake_client_leave_cell)
|
self.leave_cell_button.clicked.connect(self._trigger_selected_fake_client_leave_cell)
|
||||||
script_action_layout.addWidget(self.leave_cell_button)
|
script_action_layout.addWidget(self.leave_cell_button)
|
||||||
@@ -327,6 +331,17 @@ class DevServerWindow(QMainWindow):
|
|||||||
|
|
||||||
self._refresh_fake_clients_table()
|
self._refresh_fake_clients_table()
|
||||||
|
|
||||||
|
def _toggle_selected_fake_client_crouch(self) -> None:
|
||||||
|
client_key = self._get_selected_fake_client_key_for_control()
|
||||||
|
if client_key is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.fake_player_manager.toggle_client_crouch(client_key):
|
||||||
|
self._append_log("Select a fake client first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._refresh_fake_clients_table()
|
||||||
|
|
||||||
def _trigger_selected_fake_client_leave_cell(self) -> None:
|
def _trigger_selected_fake_client_leave_cell(self) -> None:
|
||||||
client_key = self._get_selected_fake_client_key_for_control()
|
client_key = self._get_selected_fake_client_key_for_control()
|
||||||
if client_key is None:
|
if client_key is None:
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ def update_remote_player_state(packet: dict[str, Any]) -> None:
|
|||||||
"isSprinting": get_optional_bool(packet, "isSprinting"),
|
"isSprinting": get_optional_bool(packet, "isSprinting"),
|
||||||
"isSneaking": get_optional_bool(packet, "isSneaking"),
|
"isSneaking": get_optional_bool(packet, "isSneaking"),
|
||||||
"isJumping": get_optional_bool(packet, "isJumping"),
|
"isJumping": get_optional_bool(packet, "isJumping"),
|
||||||
|
"isCrouching": get_optional_bool(packet, "isCrouching"),
|
||||||
"weaponDrawn": get_optional_bool(packet, "weaponDrawn"),
|
"weaponDrawn": get_optional_bool(packet, "weaponDrawn"),
|
||||||
"movementSpeed": get_optional_float(packet, "movementSpeed"),
|
"movementSpeed": get_optional_float(packet, "movementSpeed"),
|
||||||
"actorStateFlags1": get_optional_uint32(packet, "actorStateFlags1"),
|
"actorStateFlags1": get_optional_uint32(packet, "actorStateFlags1"),
|
||||||
@@ -117,7 +118,7 @@ def print_remote_player(player: dict[str, Any]) -> None:
|
|||||||
"Movement State: "
|
"Movement State: "
|
||||||
f"moving={player['isMoving']}, sprinting={player['isSprinting']}, "
|
f"moving={player['isMoving']}, sprinting={player['isSprinting']}, "
|
||||||
f"sneaking={player['isSneaking']}, jumping={player['isJumping']}, "
|
f"sneaking={player['isSneaking']}, jumping={player['isJumping']}, "
|
||||||
f"weaponDrawn={player['weaponDrawn']}, speed={player['movementSpeed']:.1f}"
|
f"crouching={player['isCrouching']}, weaponDrawn={player['weaponDrawn']}, speed={player['movementSpeed']:.1f}"
|
||||||
),
|
),
|
||||||
f"Actor State: flags1={player['actorStateFlags1']:08X}, flags2={player['actorStateFlags2']:08X}",
|
f"Actor State: flags1={player['actorStateFlags1']:08X}, flags2={player['actorStateFlags2']:08X}",
|
||||||
f"Cell: {player['cellId']}",
|
f"Cell: {player['cellId']}",
|
||||||
@@ -144,7 +145,7 @@ def print_remote_players() -> None:
|
|||||||
f"angleZ={player['angleZ']:.2f}, movementType={player['movementType']}, "
|
f"angleZ={player['angleZ']:.2f}, movementType={player['movementType']}, "
|
||||||
f"moving={player['isMoving']}, sprinting={player['isSprinting']}, "
|
f"moving={player['isMoving']}, sprinting={player['isSprinting']}, "
|
||||||
f"sneaking={player['isSneaking']}, jumping={player['isJumping']}, "
|
f"sneaking={player['isSneaking']}, jumping={player['isJumping']}, "
|
||||||
f"weaponDrawn={player['weaponDrawn']}, speed={player['movementSpeed']:.1f}, "
|
f"crouching={player['isCrouching']}, weaponDrawn={player['weaponDrawn']}, speed={player['movementSpeed']:.1f}, "
|
||||||
f"flags1={player['actorStateFlags1']:08X}, flags2={player['actorStateFlags2']:08X}, "
|
f"flags1={player['actorStateFlags1']:08X}, flags2={player['actorStateFlags2']:08X}, "
|
||||||
f"cell={player['cellId']}, worldspace={player['worldspaceId']}, "
|
f"cell={player['cellId']}, worldspace={player['worldspaceId']}, "
|
||||||
f"serverTime={player['serverTime']}"
|
f"serverTime={player['serverTime']}"
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class FakePlayerClient:
|
|||||||
self.angle_z = IDLE_ANGLE_Z
|
self.angle_z = IDLE_ANGLE_Z
|
||||||
self.is_sneaking = False
|
self.is_sneaking = False
|
||||||
self.is_jumping = False
|
self.is_jumping = False
|
||||||
|
self.is_crouching = False
|
||||||
|
|
||||||
self._log_callback = log_callback
|
self._log_callback = log_callback
|
||||||
self._walk_target_provider = walk_target_provider
|
self._walk_target_provider = walk_target_provider
|
||||||
@@ -176,6 +177,15 @@ class FakePlayerClient:
|
|||||||
self._log(f"{self.name} toggled sneak: {str(is_sneaking).lower()}.")
|
self._log(f"{self.name} toggled sneak: {str(is_sneaking).lower()}.")
|
||||||
return is_sneaking
|
return is_sneaking
|
||||||
|
|
||||||
|
def toggle_crouch(self) -> bool:
|
||||||
|
with self._lock:
|
||||||
|
self.is_crouching = not self.is_crouching
|
||||||
|
is_crouching = self.is_crouching
|
||||||
|
|
||||||
|
self._script_changed_event.set()
|
||||||
|
self._log(f"{self.name} toggled crouch: {str(is_crouching).lower()}.")
|
||||||
|
return is_crouching
|
||||||
|
|
||||||
def trigger_leave_cell(self) -> None:
|
def trigger_leave_cell(self) -> None:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self.script = SCRIPT_LEFT_CELL
|
self.script = SCRIPT_LEFT_CELL
|
||||||
@@ -498,6 +508,7 @@ class FakePlayerClient:
|
|||||||
is_moving: bool,
|
is_moving: bool,
|
||||||
movement_speed: float,
|
movement_speed: float,
|
||||||
is_jumping: bool | None = None,
|
is_jumping: bool | None = None,
|
||||||
|
is_crouching: bool | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
connection = self._socket
|
connection = self._socket
|
||||||
@@ -513,6 +524,7 @@ class FakePlayerClient:
|
|||||||
is_moving=is_moving,
|
is_moving=is_moving,
|
||||||
movement_speed=movement_speed,
|
movement_speed=movement_speed,
|
||||||
is_jumping=is_jumping,
|
is_jumping=is_jumping,
|
||||||
|
is_crouching=is_crouching,
|
||||||
)
|
)
|
||||||
except OSError as error:
|
except OSError as error:
|
||||||
if not self._stop_event.is_set():
|
if not self._stop_event.is_set():
|
||||||
@@ -527,6 +539,7 @@ class FakePlayerClient:
|
|||||||
movement_speed: float,
|
movement_speed: float,
|
||||||
movement_type: str = "normal",
|
movement_type: str = "normal",
|
||||||
is_jumping: bool | None = None,
|
is_jumping: bool | None = None,
|
||||||
|
is_crouching: bool | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
x, y, z = self.position
|
x, y, z = self.position
|
||||||
@@ -535,6 +548,7 @@ class FakePlayerClient:
|
|||||||
worldspace_id = self.worldspace_id
|
worldspace_id = self.worldspace_id
|
||||||
is_sneaking = self.is_sneaking
|
is_sneaking = self.is_sneaking
|
||||||
jumping = self.is_jumping if is_jumping is None else is_jumping
|
jumping = self.is_jumping if is_jumping is None else is_jumping
|
||||||
|
crouching = self.is_crouching if is_crouching is None else is_crouching
|
||||||
|
|
||||||
packet = {
|
packet = {
|
||||||
"type": "transform",
|
"type": "transform",
|
||||||
@@ -550,6 +564,7 @@ class FakePlayerClient:
|
|||||||
"isSprinting": False,
|
"isSprinting": False,
|
||||||
"isSneaking": is_sneaking,
|
"isSneaking": is_sneaking,
|
||||||
"isJumping": jumping,
|
"isJumping": jumping,
|
||||||
|
"isCrouching": crouching,
|
||||||
"weaponDrawn": False,
|
"weaponDrawn": False,
|
||||||
"movementSpeed": movement_speed,
|
"movementSpeed": movement_speed,
|
||||||
}
|
}
|
||||||
@@ -702,6 +717,14 @@ class FakePlayerManager:
|
|||||||
client.toggle_sneak()
|
client.toggle_sneak()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def toggle_client_crouch(self, client_key: int) -> bool:
|
||||||
|
client = self._get_client(client_key)
|
||||||
|
if client is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
client.toggle_crouch()
|
||||||
|
return True
|
||||||
|
|
||||||
def trigger_client_leave_cell(self, client_key: int) -> bool:
|
def trigger_client_leave_cell(self, client_key: int) -> bool:
|
||||||
client = self._get_client(client_key)
|
client = self._get_client(client_key)
|
||||||
if client is None:
|
if client is None:
|
||||||
|
|||||||
Reference in New Issue
Block a user