Revert "Add Pip-Boy state to transform sync"

This reverts commit 4ed671464e.
This commit is contained in:
2026-06-30 16:59:42 +12:00
parent 683feabb77
commit 8ff1a90fa5
18 changed files with 19 additions and 196 deletions
-5
View File
@@ -10,7 +10,6 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
## [Unreleased] ## [Unreleased]
### Added ### Added
- Optional `isPipboyOpen` transform state so receivers can detect remote Pip-Boy open/close transitions. The state is synced and logged for diagnostics; no proxy Pip-Boy animation is driven (the third-person humanoid behavior graph has no Pip-Boy raise/use animation — it is first-person only), leaving room for a future custom third-person animation.
- Optional `appearance` transform snapshots for best-effort runtime proxy body and face visuals, including height, body morph weight, body tint, hair colors, head-part IDs, and morph slider values while leaving gender switching out. - Optional `appearance` transform snapshots for best-effort runtime proxy body and face visuals, including height, body morph weight, body tint, hair colors, head-part IDs, and morph slider values while leaving gender switching out.
- Game-thread proxy appearance application for supported `appearance` fields after runtime proxy 3D is ready, with race switching deferred for future proxy-base compatibility work. - Game-thread proxy appearance application for supported `appearance` fields after runtime proxy 3D is ready, with race switching deferred for future proxy-base compatibility work.
- `server/fake_client.py` parsing/output and `server/fake_player.py` sample payloads for relayed `appearance` transform data. - `server/fake_client.py` parsing/output and `server/fake_player.py` sample payloads for relayed `appearance` transform data.
@@ -29,11 +28,7 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
- Game-thread proxy equipment application for synced apparel, including explicit tracked-slot unequips and cleanup when runtime proxy slots are held, reused, or reassigned. - Game-thread proxy equipment application for synced apparel, including explicit tracked-slot unequips and cleanup when runtime proxy slots are held, reused, or reassigned.
- `server/fake_client.py` parsing and output for relayed `equippedItems` transform data. - `server/fake_client.py` parsing and output for relayed `equippedItems` transform data.
### Removed
- Proxy Pip-Boy animation triggering (guessed graph notification events, `pipboyUp`/`bBlockPipboy` graph-variable writes, and the `TESIdleForm` playback fallback) after the F4-Animation-Research unpack confirmed the third-person humanoid behavior graph has no Pip-Boy raise/use animation; only the `isPipboyOpen` state sync is retained.
### Fixed ### Fixed
- Idle animation state no longer logs as changed every frame while a proxy is stationary.
- New clients now receive existing players' last transform snapshots immediately after `welcome`, including `appearance`, so late joiners do not start with default proxy visuals while waiting for the next sender heartbeat. - New clients now receive existing players' last transform snapshots immediately after `welcome`, including `appearance`, so late joiners do not start with default proxy visuals while waiting for the next sender heartbeat.
- Runtime proxy candidate isolation now ignores actors flagged as deleted, so a freshly spawned proxy is controlled instead of the slot adopting a still-present stale proxy that was marked for deletion the same frame. - Runtime proxy candidate isolation now ignores actors flagged as deleted, so a freshly spawned proxy is controlled instead of the slot adopting a still-present stale proxy that was marked for deletion the same frame.
- Stale saved runtime `COPlayerProxy` actors are deleted before spawning a fresh PlaceAtMe proxy, preventing duplicate visible proxies when an old uncontrolled proxy persisted in a save. - Stale saved runtime `COPlayerProxy` actors are deleted before spawning a fresh PlaceAtMe proxy, preventing duplicate visible proxies when an old uncontrolled proxy persisted in a save.
+1 -1
View File
@@ -12,7 +12,7 @@ The current system is still a local prototype, but the Fallout 4 plugin now has
both send and receive paths: both send and receive paths:
1. The Fallout 4 plugin reads local player transform data. 1. The Fallout 4 plugin reads local player transform data.
2. The plugin derives basic movement/menu state and captures visible equipped 2. The plugin derives basic movement state and captures visible equipped
apparel and player appearance snapshots on the game-thread polling path, then apparel and player appearance snapshots on the game-thread polling path, then
sends them as optional data on transform packets. sends them as optional data on transform packets.
3. The plugin sends transform packets to the Python relay server. The default 3. The plugin sends transform packets to the Python relay server. The default
-112
View File
@@ -9,118 +9,6 @@ failed experiments, successful tests, and next steps.
--- ---
## 2026-06-30 - Pip-Boy Third-Person Behavior Investigation
### Summary
Traced why no Pip-Boy animation plays on the proxy and established the engine-level root cause from the F4-Animation-Research unpack. Replaced the guessed graph events with the authoritative third-person Pip-Boy graph variables.
### Files Changed
- `plugin/include/F4TProxyAnimationSync.h`
- `plugin/src/F4TProxyAnimationSync.cpp`
- `changelog.md`
- `docs/dev-log.md`
### Details
- Log analysis: `isPipboyOpen=true` reaches the receiver (`recvPipboyOpen=true`), so capture/relay/storage are correct. Every guessed Pip-Boy event (`PipboyOpen`, `PipBoyOpen`, `pipboyOpen`, `Pipboy_Open`) returned `graphAccepted=false`, and the idle scan found `0` Pip-Boy idle forms.
- Searched `F:\Repos\F4-Animation-Research\unpacked`. The Pip-Boy raise/use animation set is **first-person only** (`Character\_1stPerson\Behaviors\Pipboy.hkx`, `Character\_1stPerson\Animations\Pipboy\*`).
- The third-person root graph (`Character\Behaviors\RaiderRootBehavior.xml`, used by the humanoid proxy) exposes only limited Pip-Boy data:
- `PipboyOffsetAnimationGen` (`BSOffsetAnimationGenerator`) blending `Animations\PipboyOffset.hkt`, whose `fOffsetVariable` is bound to variable index 186 = `bIsFemale` (a gender-selected, effectively always-on wrist-clearance additive, not an open/close toggle).
- `pipboyUp` is variable index 289 (`VARIABLE_TYPE_BOOL`) but is **not** in `eventNames` (event index -1) and is bound to no node in this behavior.
- `bBlockPipboy` is variable index 191 (`VARIABLE_TYPE_BOOL`); `bBlockPipboyStart`/`bBlockPipboyStop` are events 785/786 but appear in `0` transitions/triggers here.
- Conclusion: the stock third-person humanoid behavior has no open/close Pip-Boy transition, so neither graph events nor idle forms can reproduce the first-person raise pose. The only third-person Pip-Boy clips are `DynamicAnims\PipboyIdle1/2/3` (wrist-glance idles) and furniture-chair flavor poses, none exposed as a behavior event/state.
- Code change: drive the real `pipboyUp` / `bBlockPipboy` bool graph variables with write + read-back diagnostics; removed the fictional event candidate list; kept `TESIdleForm` playback as a fallback only when the graph does not expose the variable.
### Testing
- Ran `xmake build`; plugin build succeeded (pre-existing C4702 warnings in `F4TProxyActorController.cpp` only).
- In-game re-test still required to confirm whether the proxy graph even exposes `pipboyUp` (`pipboyUpWritten` / `pipboyUpReadBack` in the new log line).
### Decision
- Per project owner: no Pip-Boy animation for now. A custom third-person animation may be authored later when an animator is available.
- Removed the proxy Pip-Boy animation-trigger code (guessed events, `pipboyUp`/`bBlockPipboy` variable writes, and the `TESIdleForm` fallback) and their now-unused helpers/includes. Kept the lightweight `isPipboyOpen` state sync (capture, relay, storage, and diagnostics) so a future custom animation has the signal available. Rebuilt successfully.
### Known Issues
- A faithful third-person "using Pip-Boy" pose is not reachable from the stock behavior graph; achieving it would require custom animation work (porting the first-person raise to a third-person additive/idle plus a behavior hook).
### Next Steps
- Revisit only if a custom third-person Pip-Boy animation is authored; the `isPipboyOpen` flag is already available to drive it.
---
## 2026-06-30 - Pip-Boy Idle Playback Fallback
### Summary
Diagnosed the first in-game Pip-Boy sync attempt and added an idle-form fallback for proxy playback.
### Files Changed
- `plugin/src/F4TProxyAnimationSync.cpp`
- `changelog.md`
- `docs/dev-log.md`
### Details
- `CommonwealthOnline.log` showed `isPipboyOpen=true` reached the receiver, so capture and relay worked.
- The proxy rejected every diagnostic `NotifyAnimationGraph` Pip-Boy event candidate, matching the broader observation that raw locomotion graph events also return `graphAccepted=false` on the current proxy.
- Added a `TESIdleForm` scan for Pip-Boy-related idle records and a fallback that tries `AIProcess::PlayIdle` for open/close matching idles when graph event notification fails.
- Fixed idle-state change detection so missing locomotion tier does not make stationary proxy animation state log as changed every frame.
### Testing
- Ran `build.bat`; plugin build succeeded.
- In-game confirmation is still needed to see whether a matching Pip-Boy idle exists and returns `played=true` on the runtime proxy.
### Known Issues
- The fallback depends on loaded `TESIdleForm` records whose editor ID, anim event, or anim file includes `pip` and `open`/`close`.
- If no playable idle is found, the next step is using extracted HKX/AnimTextData to identify the actual Pip-Boy subgraph or adding a purpose-built proxy idle.
### Next Steps
- Re-test with two clients, open/close Pip-Boy, and check `CommonwealthOnline.log` for `Runtime proxy Pip-Boy idle candidate` and `played=true/false` lines.
---
## 2026-06-30 - Pip-Boy State Animation Sync
### Summary
Added a backward-compatible Pip-Boy state flag to transform sync so remote proxies can detect when another player opens or closes the Pip-Boy and attempt matching graph events.
### Files Changed
- `plugin/include/F4TRemotePlayerState.h`
- `plugin/include/F4TNetworking.h`
- `plugin/include/F4TProxyAnimationSync.h`
- `plugin/src/F4TNetworking.cpp`
- `plugin/src/F4TProxyActorController.cpp`
- `plugin/src/F4TProxyAnimationSync.cpp`
- `plugin/src/main.cpp`
- `server/fake_client.py`
- `server/fake_player.py`
- `protocol/packets.md`
- `protocol/player-sync.md`
- `docs/protocol.md`
- `docs/protocol-phase2-extensions.md`
- `docs/architecture.md`
- `server/README.md`
- `changelog.md`
- `docs/dev-log.md`
### Details
- Added optional `isPipboyOpen` to transform packets, remote-player state, fake-client output, and fake-player payloads.
- Captured local Pip-Boy activity from the C++ polling path using the Pip-Boy menu and `PipboyManager` state, so open/close changes trigger immediate transform sends.
- Added proxy-side edge detection for `isPipboyOpen` and a small diagnostic candidate event list for Pip-Boy open/close graph events.
- Logged graph acceptance for each candidate event so extracted HKX `AnimEventinfo` or Pip-Boy subgraph data can replace the diagnostic list once the real event names are confirmed.
### Testing
- Ran `python -m py_compile server\fake_client.py server\fake_player.py`; syntax check passed.
- Ran `build.bat`; plugin build succeeded.
- Build still reports pre-existing unreachable-code warnings in `F4TLocalAnimationGraphDebug.cpp` and `F4TProxyActorController.cpp`.
- In-game two-client confirmation is still required to verify which Pip-Boy event name, if any, the current proxy graph accepts.
### Known Issues
- The proxy Pip-Boy graph event names are still diagnostic candidates, not confirmed names from extracted HKX data.
- The first pass mirrors state/pose attempts only; it does not replicate the Pip-Boy UI screen on the remote actor.
### Next Steps
- Open and close Pip-Boy with two connected Fallout 4 instances and check `CommonwealthOnline.log` for `isPipboyOpen` send/receive state and `graphAccepted` results.
- Use `F4-Animation-Research/extracted` `AnimTextData` / `AnimEventinfo` or Pip-Boy subgraph data to replace failed candidate event names.
---
## 2026-06-29 - Late-Join Appearance Snapshot Fix ## 2026-06-29 - Late-Join Appearance Snapshot Fix
### Summary ### Summary
-2
View File
@@ -26,7 +26,6 @@ This document describes the Phase 2 protocol extensions to support actor state f
"isSneaking": false, "isSneaking": false,
"isJumping": false, "isJumping": false,
"weaponDrawn": false, "weaponDrawn": false,
"isPipboyOpen": false,
"movementSpeed": 180.5, "movementSpeed": 180.5,
"clientTime": 1717424000.123, "clientTime": 1717424000.123,
"serverTime": 1717424000.456, "serverTime": 1717424000.456,
@@ -67,7 +66,6 @@ This document describes the Phase 2 protocol extensions to support actor state f
| `isSneaking` | Boolean | NO | Local player is sneaking (default: false) | | `isSneaking` | Boolean | NO | Local player is sneaking (default: false) |
| `isJumping` | Boolean | NO | Local player is jumping (default: false) | | `isJumping` | Boolean | NO | Local player is jumping (default: false) |
| `weaponDrawn` | Boolean | NO | Local player has weapon drawn (default: false) | | `weaponDrawn` | Boolean | NO | Local player has weapon drawn (default: false) |
| `isPipboyOpen` | Boolean | NO | Local player has Pip-Boy open, opening, closing, or raising (default: false) |
| `movementSpeed` | Number (float) | NO | Movement speed in game units/frame (default: 0.0) | | `movementSpeed` | Number (float) | NO | Movement speed in game units/frame (default: 0.0) |
| `clientTime` | Number (double) | NO | Client-side timestamp (optional) | | `clientTime` | Number (double) | NO | Client-side timestamp (optional) |
| `serverTime` | Number (double) | NO | Server timestamp for diagnostics (optional) | | `serverTime` | Number (double) | NO | Server timestamp for diagnostics (optional) |
+3 -4
View File
@@ -60,8 +60,7 @@ If UDP discovery is unavailable, the plugin may fall back to probing
The protocol should be testable outside Fallout 4 before it is used inside the F4SE plugin. The protocol should be testable outside Fallout 4 before it is used inside the F4SE plugin.
`transform` packets carry position, movement/action state such as locomotion, `transform` packets carry position, movement state, optional `equippedItems`
weapon, and Pip-Boy flags, optional `equippedItems` apparel snapshots, and apparel snapshots, and optional `appearance` snapshots for best-effort proxy
optional `appearance` snapshots for best-effort proxy body/face visuals. The body/face visuals. The canonical field list and compatibility rules live in
canonical field list and compatibility rules live in
[`protocol/packets.md`](../protocol/packets.md). [`protocol/packets.md`](../protocol/packets.md).
-1
View File
@@ -30,7 +30,6 @@ namespace F4T::Networking
bool a_isJumping = false, bool a_isJumping = false,
bool a_isCrouching = false, bool a_isCrouching = false,
bool a_weaponDrawn = false, bool a_weaponDrawn = false,
bool a_isPipboyOpen = false,
float a_movementSpeed = 0.0F, float a_movementSpeed = 0.0F,
float a_animationGraphSpeed = -1.0F, float a_animationGraphSpeed = -1.0F,
std::span<const F4T::RemotePlayerState::RemoteEquippedItem> a_equippedItems = {}, std::span<const F4T::RemotePlayerState::RemoteEquippedItem> a_equippedItems = {},
-1
View File
@@ -68,7 +68,6 @@ namespace F4T::ProxyAnimationSync
bool lastIsJumping{ false }; bool lastIsJumping{ false };
bool lastIsCrouching{ false }; bool lastIsCrouching{ false };
bool lastWeaponDrawn{ false }; bool lastWeaponDrawn{ false };
bool lastIsPipboyOpen{ false };
float lastGraphSpeed{ 0.0F }; float lastGraphSpeed{ 0.0F };
float lastDirection{ 0.0F }; float lastDirection{ 0.0F };
std::optional<LocomotionTier> lastLocomotionTier{}; std::optional<LocomotionTier> lastLocomotionTier{};
-1
View File
@@ -80,7 +80,6 @@ namespace F4T::RemotePlayerState
bool isJumping{ false }; bool isJumping{ false };
bool isCrouching{ false }; bool isCrouching{ false };
bool weaponDrawn{ false }; bool weaponDrawn{ false };
bool isPipboyOpen{ false };
float movementSpeed{ 0.0F }; float movementSpeed{ 0.0F };
float animationGraphSpeed{ -1.0F }; float animationGraphSpeed{ -1.0F };
bool hasEquipmentUpdate{ false }; bool hasEquipmentUpdate{ false };
+1 -5
View File
@@ -723,7 +723,6 @@ namespace
ReadBool(a_packet, "isJumping").value_or(false), ReadBool(a_packet, "isJumping").value_or(false),
ReadBool(a_packet, "isCrouching").value_or(false), ReadBool(a_packet, "isCrouching").value_or(false),
ReadBool(a_packet, "weaponDrawn").value_or(false), ReadBool(a_packet, "weaponDrawn").value_or(false),
ReadBool(a_packet, "isPipboyOpen").value_or(false),
static_cast<float>(movementSpeed), static_cast<float>(movementSpeed),
static_cast<float>(animationGraphSpeed), static_cast<float>(animationGraphSpeed),
hasEquipmentUpdate, hasEquipmentUpdate,
@@ -756,7 +755,7 @@ namespace
if (ShouldLogRemoteMovementState(*playerId)) { if (ShouldLogRemoteMovementState(*playerId)) {
LogInfoWithLocalPlayerPrefix(std::format( LogInfoWithLocalPlayerPrefix(std::format(
"Remote player {} movement state: moving={}, sprinting={}, sneaking={}, jumping={}, crouching={}, weaponDrawn={}, pipboyOpen={}, 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,
@@ -764,7 +763,6 @@ namespace
remoteState.isJumping, remoteState.isJumping,
remoteState.isCrouching, remoteState.isCrouching,
remoteState.weaponDrawn, remoteState.weaponDrawn,
remoteState.isPipboyOpen,
remoteState.movementSpeed, remoteState.movementSpeed,
remoteState.actorStateFlags1, remoteState.actorStateFlags1,
remoteState.actorStateFlags2)); remoteState.actorStateFlags2));
@@ -1085,7 +1083,6 @@ namespace F4T::Networking
bool a_isJumping, bool a_isJumping,
bool a_isCrouching, bool a_isCrouching,
bool a_weaponDrawn, bool a_weaponDrawn,
bool a_isPipboyOpen,
float a_movementSpeed, float a_movementSpeed,
float a_animationGraphSpeed, float a_animationGraphSpeed,
std::span<const F4T::RemotePlayerState::RemoteEquippedItem> a_equippedItems, std::span<const F4T::RemotePlayerState::RemoteEquippedItem> a_equippedItems,
@@ -1112,7 +1109,6 @@ namespace F4T::Networking
{ "isJumping", a_isJumping }, { "isJumping", a_isJumping },
{ "isCrouching", a_isCrouching }, { "isCrouching", a_isCrouching },
{ "weaponDrawn", a_weaponDrawn }, { "weaponDrawn", a_weaponDrawn },
{ "isPipboyOpen", a_isPipboyOpen },
{ "movementSpeed", a_movementSpeed } { "movementSpeed", a_movementSpeed }
}; };
+1 -6
View File
@@ -360,7 +360,6 @@ namespace
std::optional<bool> lastObservedSneakingState; std::optional<bool> lastObservedSneakingState;
std::optional<bool> lastObservedJumpingState; std::optional<bool> lastObservedJumpingState;
std::optional<bool> lastObservedWeaponDrawnState; std::optional<bool> lastObservedWeaponDrawnState;
std::optional<bool> lastObservedPipboyOpenState;
std::optional<std::string> lastObservedMovementType; std::optional<std::string> lastObservedMovementType;
std::optional<std::string> lastObservedRemoteCellId; std::optional<std::string> lastObservedRemoteCellId;
std::optional<ProxyAnimationSpeedBucket> lastObservedMovementSpeedBucket; std::optional<ProxyAnimationSpeedBucket> lastObservedMovementSpeedBucket;
@@ -6053,7 +6052,6 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
a_slot.lastObservedSneakingState.reset(); a_slot.lastObservedSneakingState.reset();
a_slot.lastObservedJumpingState.reset(); a_slot.lastObservedJumpingState.reset();
a_slot.lastObservedWeaponDrawnState.reset(); a_slot.lastObservedWeaponDrawnState.reset();
a_slot.lastObservedPipboyOpenState.reset();
a_slot.lastObservedMovementType.reset(); a_slot.lastObservedMovementType.reset();
a_slot.lastObservedMovementSpeedBucket.reset(); a_slot.lastObservedMovementSpeedBucket.reset();
ResetSlotVisualSneakTracking(a_slot); ResetSlotVisualSneakTracking(a_slot);
@@ -6464,13 +6462,12 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
!a_slot.lastObservedSneakingState || *a_slot.lastObservedSneakingState != a_remotePlayer.isSneaking || !a_slot.lastObservedSneakingState || *a_slot.lastObservedSneakingState != a_remotePlayer.isSneaking ||
!a_slot.lastObservedJumpingState || *a_slot.lastObservedJumpingState != a_remotePlayer.isJumping || !a_slot.lastObservedJumpingState || *a_slot.lastObservedJumpingState != a_remotePlayer.isJumping ||
!a_slot.lastObservedWeaponDrawnState || *a_slot.lastObservedWeaponDrawnState != a_remotePlayer.weaponDrawn || !a_slot.lastObservedWeaponDrawnState || *a_slot.lastObservedWeaponDrawnState != a_remotePlayer.weaponDrawn ||
!a_slot.lastObservedPipboyOpenState || *a_slot.lastObservedPipboyOpenState != a_remotePlayer.isPipboyOpen ||
!a_slot.lastObservedMovementType || *a_slot.lastObservedMovementType != a_remotePlayer.movementType || !a_slot.lastObservedMovementType || *a_slot.lastObservedMovementType != a_remotePlayer.movementType ||
!a_slot.lastObservedMovementSpeedBucket || *a_slot.lastObservedMovementSpeedBucket != speedBucket; !a_slot.lastObservedMovementSpeedBucket || *a_slot.lastObservedMovementSpeedBucket != speedBucket;
if (shouldLogInitial || changed) { if (shouldLogInitial || changed) {
LogInfoWithLocalPlayerPrefix(std::format( LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy animation state {} for remote player {}: actor={:08X}, holdingIndex={}, moving={}, speed={:.1f}, speedBucket={}, sprinting={}, sneaking={}, jumping={}, weaponDrawn={}, pipboyOpen={}, movementType={}.", "Runtime proxy animation state {} for remote player {}: actor={:08X}, holdingIndex={}, moving={}, speed={:.1f}, speedBucket={}, sprinting={}, sneaking={}, jumping={}, weaponDrawn={}, movementType={}.",
shouldLogInitial ? "initial" : "changed", shouldLogInitial ? "initial" : "changed",
a_remotePlayer.playerId, a_remotePlayer.playerId,
a_proxy.GetFormID(), a_proxy.GetFormID(),
@@ -6482,7 +6479,6 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
a_remotePlayer.isSneaking, a_remotePlayer.isSneaking,
a_remotePlayer.isJumping, a_remotePlayer.isJumping,
a_remotePlayer.weaponDrawn, a_remotePlayer.weaponDrawn,
a_remotePlayer.isPipboyOpen,
a_remotePlayer.movementType)); a_remotePlayer.movementType));
} }
@@ -6492,7 +6488,6 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
a_slot.lastObservedSneakingState = a_remotePlayer.isSneaking; a_slot.lastObservedSneakingState = a_remotePlayer.isSneaking;
a_slot.lastObservedJumpingState = a_remotePlayer.isJumping; a_slot.lastObservedJumpingState = a_remotePlayer.isJumping;
a_slot.lastObservedWeaponDrawnState = a_remotePlayer.weaponDrawn; a_slot.lastObservedWeaponDrawnState = a_remotePlayer.weaponDrawn;
a_slot.lastObservedPipboyOpenState = a_remotePlayer.isPipboyOpen;
a_slot.lastObservedMovementType = a_remotePlayer.movementType; a_slot.lastObservedMovementType = a_remotePlayer.movementType;
a_slot.lastObservedMovementSpeedBucket = speedBucket; a_slot.lastObservedMovementSpeedBucket = speedBucket;
+4 -13
View File
@@ -12,6 +12,7 @@
#include "RE/T/TESObjectREFR.h" #include "RE/T/TESObjectREFR.h"
#include <algorithm> #include <algorithm>
#include <array>
#include <chrono> #include <chrono>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
@@ -256,7 +257,6 @@ namespace
bool isJumping{ false }; bool isJumping{ false };
bool isCrouching{ false }; bool isCrouching{ false };
bool weaponDrawn{ false }; bool weaponDrawn{ false };
bool isPipboyOpen{ false };
float graphSpeed{ 0.0F }; float graphSpeed{ 0.0F };
float direction{ 0.0F }; float direction{ 0.0F };
}; };
@@ -273,7 +273,6 @@ namespace
desired.isJumping = a_remotePlayer.isJumping; desired.isJumping = a_remotePlayer.isJumping;
desired.isCrouching = a_remotePlayer.isCrouching; desired.isCrouching = a_remotePlayer.isCrouching;
desired.weaponDrawn = a_remotePlayer.weaponDrawn; desired.weaponDrawn = a_remotePlayer.weaponDrawn;
desired.isPipboyOpen = a_remotePlayer.isPipboyOpen;
desired.graphSpeed = ComputeGraphSpeed(a_remotePlayer); desired.graphSpeed = ComputeGraphSpeed(a_remotePlayer);
desired.direction = a_remotePlayer.angleZ; desired.direction = a_remotePlayer.angleZ;
return desired; return desired;
@@ -334,18 +333,13 @@ namespace
const auto currentTier = const auto currentTier =
F4T::ProxyAnimationSync::ComputeLocomotionTier(a_desired.graphSpeed, a_desired.isSprinting); F4T::ProxyAnimationSync::ComputeLocomotionTier(a_desired.graphSpeed, a_desired.isSprinting);
const auto locomotionTierChanged =
a_desired.isMoving &&
(!a_state.lastLocomotionTier || *a_state.lastLocomotionTier != currentTier);
return a_state.lastIsMoving != a_desired.isMoving || return a_state.lastIsMoving != a_desired.isMoving ||
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.lastIsCrouching != a_desired.isCrouching ||
a_state.lastWeaponDrawn != a_desired.weaponDrawn || a_state.lastWeaponDrawn != a_desired.weaponDrawn ||
a_state.lastIsPipboyOpen != a_desired.isPipboyOpen || !a_state.lastLocomotionTier || *a_state.lastLocomotionTier != currentTier ||
locomotionTierChanged ||
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);
} }
@@ -636,7 +630,7 @@ namespace
} }
LogInfoWithLocalPlayerPrefix(std::format( LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy animation sync {} for remote player {}: actor={:08X}, moving={}, tier={}, sprinting={}, sneaking={}, jumping={}, crouching={}, weaponDrawn={}, pipboyOpen={}, graphSpeed={:.1f}, direction={:.3f}.", "Runtime proxy animation sync {} for remote player {}: actor={:08X}, moving={}, tier={}, 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,
@@ -647,7 +641,6 @@ namespace
a_desired.isJumping, a_desired.isJumping,
a_desired.isCrouching, a_desired.isCrouching,
a_desired.weaponDrawn, a_desired.weaponDrawn,
a_desired.isPipboyOpen,
a_desired.graphSpeed, a_desired.graphSpeed,
a_desired.direction)); a_desired.direction));
} }
@@ -742,12 +735,11 @@ namespace F4T::ProxyAnimationSync
const auto haveSpeedSmoothed = const auto haveSpeedSmoothed =
diagHolder->GetGraphVariableImplFloat(RE::BSFixedString{ F4T::ProxyAnimationSync::GraphVar::kSpeedSmoothed }, readSpeedSmoothed); diagHolder->GetGraphVariableImplFloat(RE::BSFixedString{ F4T::ProxyAnimationSync::GraphVar::kSpeedSmoothed }, readSpeedSmoothed);
LogInfoWithLocalPlayerPrefix(std::format( LogInfoWithLocalPlayerPrefix(std::format(
"Proxy anim diag p{} actor={:08X}: recvMoving={}, recvSprint={}, recvPipboyOpen={}, recvMovementSpeed={:.1f}, recvAnimGraphSpeed={:.1f}, computedGraphSpeed={:.1f}, graphSpeedReadBack={}({:.1f}), speedSmoothedReadBack={}({:.1f}).", "Proxy anim diag p{} actor={:08X}: recvMoving={}, recvSprint={}, recvMovementSpeed={:.1f}, recvAnimGraphSpeed={:.1f}, computedGraphSpeed={:.1f}, graphSpeedReadBack={}({:.1f}), speedSmoothedReadBack={}({:.1f}).",
a_remotePlayer.playerId, a_remotePlayer.playerId,
actorId, actorId,
a_remotePlayer.isMoving, a_remotePlayer.isMoving,
a_remotePlayer.isSprinting, a_remotePlayer.isSprinting,
a_remotePlayer.isPipboyOpen,
a_remotePlayer.movementSpeed, a_remotePlayer.movementSpeed,
a_remotePlayer.animationGraphSpeed, a_remotePlayer.animationGraphSpeed,
desired.graphSpeed, desired.graphSpeed,
@@ -788,7 +780,6 @@ namespace F4T::ProxyAnimationSync
a_state.lastIsJumping = desired.isJumping; a_state.lastIsJumping = desired.isJumping;
a_state.lastIsCrouching = desired.isCrouching; a_state.lastIsCrouching = desired.isCrouching;
a_state.lastWeaponDrawn = desired.weaponDrawn; a_state.lastWeaponDrawn = desired.weaponDrawn;
a_state.lastIsPipboyOpen = desired.isPipboyOpen;
a_state.lastGraphSpeed = desired.graphSpeed; a_state.lastGraphSpeed = desired.graphSpeed;
a_state.lastDirection = desired.direction; a_state.lastDirection = desired.direction;
if (desired.isJumping) { if (desired.isJumping) {
+1 -30
View File
@@ -14,9 +14,6 @@
#include "RE/B/BipedAnim.h" #include "RE/B/BipedAnim.h"
#include "RE/B/BSFixedString.h" #include "RE/B/BSFixedString.h"
#include "RE/I/IAnimationGraphManagerHolder.h" #include "RE/I/IAnimationGraphManagerHolder.h"
#include "RE/P/PipboyManager.h"
#include "RE/P/PipboyMenu.h"
#include "RE/U/UI.h"
#include <array> #include <array>
#include <cmath> #include <cmath>
@@ -54,7 +51,6 @@ namespace
bool isJumping; bool isJumping;
bool isCrouching; bool isCrouching;
bool weaponDrawn; bool weaponDrawn;
bool isPipboyOpen;
float movementSpeed; float movementSpeed;
}; };
@@ -426,22 +422,6 @@ namespace
return false; return false;
} }
bool IsLocalPipboyOpen()
{
if (const auto* ui = RE::UI::GetSingleton(); ui && ui->GetMenuOpen<RE::PipboyMenu>()) {
return true;
}
if (auto* pipboyManager = RE::PipboyManager::GetSingleton()) {
return pipboyManager->QPipboyActive() ||
pipboyManager->pipboyOpening ||
pipboyManager->pipboyClosing ||
pipboyManager->pipboyRaising;
}
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)
@@ -532,7 +512,6 @@ namespace
heldJumping, heldJumping,
GetCrouchState(a_player), GetCrouchState(a_player),
a_player.GetWeaponMagicDrawn(), a_player.GetWeaponMagicDrawn(),
IsLocalPipboyOpen(),
a_movementSpeed a_movementSpeed
}; };
} }
@@ -566,8 +545,7 @@ namespace
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.isCrouching != a_current.isCrouching ||
a_previous.weaponDrawn != a_current.weaponDrawn || a_previous.weaponDrawn != a_current.weaponDrawn;
a_previous.isPipboyOpen != a_current.isPipboyOpen;
} }
bool HasMeaningfulTransformDelta(const PlayerTransform& a_previous, const PlayerTransform& a_current) bool HasMeaningfulTransformDelta(const PlayerTransform& a_previous, const PlayerTransform& a_current)
@@ -748,7 +726,6 @@ namespace
debugMovementState.isJumping, debugMovementState.isJumping,
debugMovementState.isCrouching, debugMovementState.isCrouching,
debugMovementState.weaponDrawn, debugMovementState.weaponDrawn,
debugMovementState.isPipboyOpen,
debugMovementState.movementSpeed, debugMovementState.movementSpeed,
-1.0F, -1.0F,
currentEquipmentState, currentEquipmentState,
@@ -841,7 +818,6 @@ namespace
networkMovementState.isJumping, networkMovementState.isJumping,
networkMovementState.isCrouching, networkMovementState.isCrouching,
networkMovementState.weaponDrawn, networkMovementState.weaponDrawn,
networkMovementState.isPipboyOpen,
networkMovementState.movementSpeed, networkMovementState.movementSpeed,
animationGraphSpeed, animationGraphSpeed,
currentEquipmentState, currentEquipmentState,
@@ -863,11 +839,6 @@ namespace
} else if (hasAppearanceChanged) { } else if (hasAppearanceChanged) {
LogInfoWithLocalPlayerPrefix("Detected local appearance change. Sending immediate transform update."); LogInfoWithLocalPlayerPrefix("Detected local appearance change. Sending immediate transform update.");
lastLogTime = currentTime; lastLogTime = currentTime;
} else if (hasMovementStateChanged && lastSentMovementState.isPipboyOpen != networkMovementState.isPipboyOpen) {
LogInfoWithLocalPlayerPrefix(std::format(
"Detected local Pip-Boy state change. Sending immediate transform update: isPipboyOpen={}.",
networkMovementState.isPipboyOpen));
lastLogTime = currentTime;
} else if (currentTime - lastLogTime >= kTransformLogInterval) { } else if (currentTime - lastLogTime >= kTransformLogInterval) {
LogPlayerTransform(currentTransform); LogPlayerTransform(currentTransform);
lastLogTime = currentTime; lastLogTime = currentTime;
-2
View File
@@ -68,7 +68,6 @@ Example after server processing:
"isSneaking": false, "isSneaking": false,
"isJumping": false, "isJumping": false,
"weaponDrawn": false, "weaponDrawn": false,
"isPipboyOpen": false,
"movementSpeed": 186.4, "movementSpeed": 186.4,
"equippedItems": [ "equippedItems": [
{ "slot": "body", "formId": "0001F66A" }, { "slot": "body", "formId": "0001F66A" },
@@ -107,7 +106,6 @@ isSprinting Optional movement-state flag; defaults to false when missing.
isSneaking Optional movement-state flag; defaults to false when missing. isSneaking Optional movement-state flag; defaults to false when missing.
isJumping Optional movement-state flag; defaults to false when missing. isJumping Optional movement-state flag; defaults to false when missing.
weaponDrawn Optional weapon drawn state; defaults to false when missing. weaponDrawn Optional weapon drawn state; defaults to false when missing.
isPipboyOpen Optional Pip-Boy open/raising/closing state; defaults to false when missing.
movementSpeed Optional derived movement speed in game units per second; defaults to 0.0 when missing. movementSpeed Optional derived movement speed in game units per second; defaults to 0.0 when missing.
equippedItems Optional full snapshot of tracked visible apparel slots. Each entry has a string `slot` and hex-string `formId`; an empty `formId` means the slot is intentionally unequipped. equippedItems Optional full snapshot of tracked visible apparel slots. Each entry has a string `slot` and hex-string `formId`; an empty `formId` means the slot is intentionally unequipped.
appearance Optional versioned player appearance snapshot for runtime proxy visuals. appearance Optional versioned player appearance snapshot for runtime proxy visuals.
+3 -4
View File
@@ -67,7 +67,6 @@ isSneaking
isJumping isJumping
isCrouching isCrouching
weaponDrawn weaponDrawn
isPipboyOpen
movementSpeed movementSpeed
animationGraphSpeed animationGraphSpeed
equippedItems equippedItems
@@ -79,8 +78,8 @@ Required transform packet fields are `type`, `playerId`, `x`, `y`, `z`,
`angleZ`, and `cellId`. `worldspaceId`, `movementType`, `clientTime`, and `angleZ`, and `cellId`. `worldspaceId`, `movementType`, `clientTime`, and
`serverTime` are optional; interior test cells may omit `worldspaceId`. `serverTime` are optional; interior test cells may omit `worldspaceId`.
Movement state fields are also optional and default to not moving, not Movement state fields are also optional and default to not moving, not
sprinting, not sneaking, not jumping, weapon holstered, Pip-Boy closed, and sprinting, not sneaking, not jumping, weapon holstered, and speed `0.0` when
speed `0.0` when missing. missing.
`equippedItems` is also optional. New clients send it as a complete snapshot of `equippedItems` is also optional. New clients send it as a complete snapshot of
tracked visible apparel slots with `{ slot, formId }` entries; empty `formId` tracked visible apparel slots with `{ slot, formId }` entries; empty `formId`
@@ -126,7 +125,7 @@ player exists.
local test cell, then snaps back to a valid same-cell remote player before local test cell, then snaps back to a valid same-cell remote player before
resuming smoothing. resuming smoothing.
- Transform packets include movement state data (`isMoving`, `movementSpeed`, - Transform packets include movement state data (`isMoving`, `movementSpeed`,
`isSprinting`, `isSneaking`, `isJumping`, `weaponDrawn`, `isPipboyOpen`). `isSprinting`, `isSneaking`, `isJumping`, `weaponDrawn`).
- Transform packets can include optional `equippedItems` snapshots for visible - Transform packets can include optional `equippedItems` snapshots for visible
clothing, armor, hats, and eyewear slots. The game-thread proxy controller clothing, armor, hats, and eyewear slots. The game-thread proxy controller
applies those snapshots after proxy 3D is loaded. applies those snapshots after proxy 3D is loaded.
+3 -3
View File
@@ -14,9 +14,9 @@ can send a `discover` packet (see `docs/protocol.md`) and receive a
`discoverResponse` with the game port and current player count. `discoverResponse` with the game port and current player count.
Transform packets may include optional movement-state fields such as Transform packets may include optional movement-state fields such as
`isMoving`, `isSprinting`, `isSneaking`, `isJumping`, `isCrouching`, `weaponDrawn`, `isMoving`, `isSprinting`, `isSneaking`, `isJumping`, `isCrouching`, `weaponDrawn`, and
`isPipboyOpen`, and `movementSpeed`. The server preserves these fields automatically `movementSpeed`. The server preserves these fields automatically because it
because it broadcasts the original transform packet after adding server-owned fields. broadcasts the original transform packet after adding server-owned fields.
This is still a local prototype. It does not make Fallout 4 multiplayer This is still a local prototype. It does not make Fallout 4 multiplayer
playable yet. playable yet.
+2 -5
View File
@@ -212,7 +212,6 @@ def update_remote_player_state(packet: dict[str, Any]) -> None:
"isJumping": get_optional_bool(packet, "isJumping"), "isJumping": get_optional_bool(packet, "isJumping"),
"isCrouching": get_optional_bool(packet, "isCrouching"), "isCrouching": get_optional_bool(packet, "isCrouching"),
"weaponDrawn": get_optional_bool(packet, "weaponDrawn"), "weaponDrawn": get_optional_bool(packet, "weaponDrawn"),
"isPipboyOpen": get_optional_bool(packet, "isPipboyOpen"),
"movementSpeed": get_optional_float(packet, "movementSpeed"), "movementSpeed": get_optional_float(packet, "movementSpeed"),
"actorStateFlags1": get_optional_uint32(packet, "actorStateFlags1"), "actorStateFlags1": get_optional_uint32(packet, "actorStateFlags1"),
"actorStateFlags2": get_optional_uint32(packet, "actorStateFlags2"), "actorStateFlags2": get_optional_uint32(packet, "actorStateFlags2"),
@@ -255,8 +254,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"crouching={player['isCrouching']}, weaponDrawn={player['weaponDrawn']}, " f"crouching={player['isCrouching']}, weaponDrawn={player['weaponDrawn']}, speed={player['movementSpeed']:.1f}"
f"pipboyOpen={player['isPipboyOpen']}, 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"Equipment: {equipment_text}", f"Equipment: {equipment_text}",
@@ -296,8 +294,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"crouching={player['isCrouching']}, weaponDrawn={player['weaponDrawn']}, " f"crouching={player['isCrouching']}, weaponDrawn={player['weaponDrawn']}, speed={player['movementSpeed']:.1f}, "
f"pipboyOpen={player['isPipboyOpen']}, speed={player['movementSpeed']:.1f}, "
f"flags1={player['actorStateFlags1']:08X}, flags2={player['actorStateFlags2']:08X}, " f"flags1={player['actorStateFlags1']:08X}, flags2={player['actorStateFlags2']:08X}, "
f"equipment={equipment_text}, " f"equipment={equipment_text}, "
f"appearance={appearance_text}, " f"appearance={appearance_text}, "
-1
View File
@@ -577,7 +577,6 @@ class FakePlayerClient:
"isJumping": jumping, "isJumping": jumping,
"isCrouching": crouching, "isCrouching": crouching,
"weaponDrawn": False, "weaponDrawn": False,
"isPipboyOpen": False,
"movementSpeed": movement_speed, "movementSpeed": movement_speed,
"appearance": DEFAULT_APPEARANCE, "appearance": DEFAULT_APPEARANCE,
} }