Add Pip-Boy state to transform sync

Adds optional `isPipboyOpen` to transform packets, remote player state, proxy animation logging, and fake client/player samples. Updates protocol and architecture docs to describe the new Pip-Boy open/raising/closing state and related movement-state handling.
This commit is contained in:
2026-06-30 16:00:07 +12:00
parent 4b8f5246c4
commit 4ed671464e
18 changed files with 196 additions and 19 deletions
+5
View File
@@ -10,6 +10,7 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
## [Unreleased]
### 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.
- 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.
@@ -28,7 +29,11 @@ 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.
- `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
- 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.
- 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.
+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:
1. The Fallout 4 plugin reads local player transform data.
2. The plugin derives basic movement state and captures visible equipped
2. The plugin derives basic movement/menu state and captures visible equipped
apparel and player appearance snapshots on the game-thread polling path, then
sends them as optional data on transform packets.
3. The plugin sends transform packets to the Python relay server. The default
+112
View File
@@ -9,6 +9,118 @@ 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
### Summary
+2
View File
@@ -26,6 +26,7 @@ This document describes the Phase 2 protocol extensions to support actor state f
"isSneaking": false,
"isJumping": false,
"weaponDrawn": false,
"isPipboyOpen": false,
"movementSpeed": 180.5,
"clientTime": 1717424000.123,
"serverTime": 1717424000.456,
@@ -66,6 +67,7 @@ This document describes the Phase 2 protocol extensions to support actor state f
| `isSneaking` | Boolean | NO | Local player is sneaking (default: false) |
| `isJumping` | Boolean | NO | Local player is jumping (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) |
| `clientTime` | Number (double) | NO | Client-side timestamp (optional) |
| `serverTime` | Number (double) | NO | Server timestamp for diagnostics (optional) |
+4 -3
View File
@@ -60,7 +60,8 @@ 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.
`transform` packets carry position, movement state, optional `equippedItems`
apparel snapshots, and optional `appearance` snapshots for best-effort proxy
body/face visuals. The canonical field list and compatibility rules live in
`transform` packets carry position, movement/action state such as locomotion,
weapon, and Pip-Boy flags, optional `equippedItems` apparel snapshots, and
optional `appearance` snapshots for best-effort proxy body/face visuals. The
canonical field list and compatibility rules live in
[`protocol/packets.md`](../protocol/packets.md).
+1
View File
@@ -30,6 +30,7 @@ namespace F4T::Networking
bool a_isJumping = false,
bool a_isCrouching = false,
bool a_weaponDrawn = false,
bool a_isPipboyOpen = false,
float a_movementSpeed = 0.0F,
float a_animationGraphSpeed = -1.0F,
std::span<const F4T::RemotePlayerState::RemoteEquippedItem> a_equippedItems = {},
+1
View File
@@ -68,6 +68,7 @@ namespace F4T::ProxyAnimationSync
bool lastIsJumping{ false };
bool lastIsCrouching{ false };
bool lastWeaponDrawn{ false };
bool lastIsPipboyOpen{ false };
float lastGraphSpeed{ 0.0F };
float lastDirection{ 0.0F };
std::optional<LocomotionTier> lastLocomotionTier{};
+1
View File
@@ -80,6 +80,7 @@ namespace F4T::RemotePlayerState
bool isJumping{ false };
bool isCrouching{ false };
bool weaponDrawn{ false };
bool isPipboyOpen{ false };
float movementSpeed{ 0.0F };
float animationGraphSpeed{ -1.0F };
bool hasEquipmentUpdate{ false };
+5 -1
View File
@@ -723,6 +723,7 @@ namespace
ReadBool(a_packet, "isJumping").value_or(false),
ReadBool(a_packet, "isCrouching").value_or(false),
ReadBool(a_packet, "weaponDrawn").value_or(false),
ReadBool(a_packet, "isPipboyOpen").value_or(false),
static_cast<float>(movementSpeed),
static_cast<float>(animationGraphSpeed),
hasEquipmentUpdate,
@@ -755,7 +756,7 @@ namespace
if (ShouldLogRemoteMovementState(*playerId)) {
LogInfoWithLocalPlayerPrefix(std::format(
"Remote player {} movement state: moving={}, sprinting={}, sneaking={}, jumping={}, crouching={}, weaponDrawn={}, speed={:.1f}, flags1={:08X}, flags2={:08X}",
"Remote player {} movement state: moving={}, sprinting={}, sneaking={}, jumping={}, crouching={}, weaponDrawn={}, pipboyOpen={}, speed={:.1f}, flags1={:08X}, flags2={:08X}",
remoteState.playerId,
remoteState.isMoving,
remoteState.isSprinting,
@@ -763,6 +764,7 @@ namespace
remoteState.isJumping,
remoteState.isCrouching,
remoteState.weaponDrawn,
remoteState.isPipboyOpen,
remoteState.movementSpeed,
remoteState.actorStateFlags1,
remoteState.actorStateFlags2));
@@ -1083,6 +1085,7 @@ namespace F4T::Networking
bool a_isJumping,
bool a_isCrouching,
bool a_weaponDrawn,
bool a_isPipboyOpen,
float a_movementSpeed,
float a_animationGraphSpeed,
std::span<const F4T::RemotePlayerState::RemoteEquippedItem> a_equippedItems,
@@ -1109,6 +1112,7 @@ namespace F4T::Networking
{ "isJumping", a_isJumping },
{ "isCrouching", a_isCrouching },
{ "weaponDrawn", a_weaponDrawn },
{ "isPipboyOpen", a_isPipboyOpen },
{ "movementSpeed", a_movementSpeed }
};
+6 -1
View File
@@ -360,6 +360,7 @@ namespace
std::optional<bool> lastObservedSneakingState;
std::optional<bool> lastObservedJumpingState;
std::optional<bool> lastObservedWeaponDrawnState;
std::optional<bool> lastObservedPipboyOpenState;
std::optional<std::string> lastObservedMovementType;
std::optional<std::string> lastObservedRemoteCellId;
std::optional<ProxyAnimationSpeedBucket> lastObservedMovementSpeedBucket;
@@ -6052,6 +6053,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
a_slot.lastObservedSneakingState.reset();
a_slot.lastObservedJumpingState.reset();
a_slot.lastObservedWeaponDrawnState.reset();
a_slot.lastObservedPipboyOpenState.reset();
a_slot.lastObservedMovementType.reset();
a_slot.lastObservedMovementSpeedBucket.reset();
ResetSlotVisualSneakTracking(a_slot);
@@ -6462,12 +6464,13 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
!a_slot.lastObservedSneakingState || *a_slot.lastObservedSneakingState != a_remotePlayer.isSneaking ||
!a_slot.lastObservedJumpingState || *a_slot.lastObservedJumpingState != a_remotePlayer.isJumping ||
!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.lastObservedMovementSpeedBucket || *a_slot.lastObservedMovementSpeedBucket != speedBucket;
if (shouldLogInitial || changed) {
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy animation state {} for remote player {}: actor={:08X}, holdingIndex={}, moving={}, speed={:.1f}, speedBucket={}, sprinting={}, sneaking={}, jumping={}, weaponDrawn={}, movementType={}.",
"Runtime proxy animation state {} for remote player {}: actor={:08X}, holdingIndex={}, moving={}, speed={:.1f}, speedBucket={}, sprinting={}, sneaking={}, jumping={}, weaponDrawn={}, pipboyOpen={}, movementType={}.",
shouldLogInitial ? "initial" : "changed",
a_remotePlayer.playerId,
a_proxy.GetFormID(),
@@ -6479,6 +6482,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
a_remotePlayer.isSneaking,
a_remotePlayer.isJumping,
a_remotePlayer.weaponDrawn,
a_remotePlayer.isPipboyOpen,
a_remotePlayer.movementType));
}
@@ -6488,6 +6492,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
a_slot.lastObservedSneakingState = a_remotePlayer.isSneaking;
a_slot.lastObservedJumpingState = a_remotePlayer.isJumping;
a_slot.lastObservedWeaponDrawnState = a_remotePlayer.weaponDrawn;
a_slot.lastObservedPipboyOpenState = a_remotePlayer.isPipboyOpen;
a_slot.lastObservedMovementType = a_remotePlayer.movementType;
a_slot.lastObservedMovementSpeedBucket = speedBucket;
+13 -4
View File
@@ -12,7 +12,6 @@
#include "RE/T/TESObjectREFR.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
@@ -257,6 +256,7 @@ namespace
bool isJumping{ false };
bool isCrouching{ false };
bool weaponDrawn{ false };
bool isPipboyOpen{ false };
float graphSpeed{ 0.0F };
float direction{ 0.0F };
};
@@ -273,6 +273,7 @@ namespace
desired.isJumping = a_remotePlayer.isJumping;
desired.isCrouching = a_remotePlayer.isCrouching;
desired.weaponDrawn = a_remotePlayer.weaponDrawn;
desired.isPipboyOpen = a_remotePlayer.isPipboyOpen;
desired.graphSpeed = ComputeGraphSpeed(a_remotePlayer);
desired.direction = a_remotePlayer.angleZ;
return desired;
@@ -333,13 +334,18 @@ namespace
const auto currentTier =
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 ||
a_state.lastIsSprinting != a_desired.isSprinting ||
a_state.lastIsSneaking != a_desired.isSneaking ||
a_state.lastIsJumping != a_desired.isJumping ||
a_state.lastIsCrouching != a_desired.isCrouching ||
a_state.lastWeaponDrawn != a_desired.weaponDrawn ||
!a_state.lastLocomotionTier || *a_state.lastLocomotionTier != currentTier ||
a_state.lastIsPipboyOpen != a_desired.isPipboyOpen ||
locomotionTierChanged ||
AreFloatsDifferent(a_state.lastGraphSpeed, a_desired.graphSpeed) ||
AreFloatsDifferent(a_state.lastDirection, a_desired.direction);
}
@@ -630,7 +636,7 @@ namespace
}
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy animation sync {} for remote player {}: actor={:08X}, moving={}, tier={}, sprinting={}, sneaking={}, jumping={}, crouching={}, weaponDrawn={}, graphSpeed={:.1f}, direction={:.3f}.",
"Runtime proxy animation sync {} for remote player {}: actor={:08X}, moving={}, tier={}, sprinting={}, sneaking={}, jumping={}, crouching={}, weaponDrawn={}, pipboyOpen={}, graphSpeed={:.1f}, direction={:.3f}.",
a_initial ? "initial" : "changed",
a_remotePlayerId,
a_actorId,
@@ -641,6 +647,7 @@ namespace
a_desired.isJumping,
a_desired.isCrouching,
a_desired.weaponDrawn,
a_desired.isPipboyOpen,
a_desired.graphSpeed,
a_desired.direction));
}
@@ -735,11 +742,12 @@ namespace F4T::ProxyAnimationSync
const auto haveSpeedSmoothed =
diagHolder->GetGraphVariableImplFloat(RE::BSFixedString{ F4T::ProxyAnimationSync::GraphVar::kSpeedSmoothed }, readSpeedSmoothed);
LogInfoWithLocalPlayerPrefix(std::format(
"Proxy anim diag p{} actor={:08X}: recvMoving={}, recvSprint={}, recvMovementSpeed={:.1f}, recvAnimGraphSpeed={:.1f}, computedGraphSpeed={:.1f}, graphSpeedReadBack={}({:.1f}), speedSmoothedReadBack={}({:.1f}).",
"Proxy anim diag p{} actor={:08X}: recvMoving={}, recvSprint={}, recvPipboyOpen={}, recvMovementSpeed={:.1f}, recvAnimGraphSpeed={:.1f}, computedGraphSpeed={:.1f}, graphSpeedReadBack={}({:.1f}), speedSmoothedReadBack={}({:.1f}).",
a_remotePlayer.playerId,
actorId,
a_remotePlayer.isMoving,
a_remotePlayer.isSprinting,
a_remotePlayer.isPipboyOpen,
a_remotePlayer.movementSpeed,
a_remotePlayer.animationGraphSpeed,
desired.graphSpeed,
@@ -780,6 +788,7 @@ namespace F4T::ProxyAnimationSync
a_state.lastIsJumping = desired.isJumping;
a_state.lastIsCrouching = desired.isCrouching;
a_state.lastWeaponDrawn = desired.weaponDrawn;
a_state.lastIsPipboyOpen = desired.isPipboyOpen;
a_state.lastGraphSpeed = desired.graphSpeed;
a_state.lastDirection = desired.direction;
if (desired.isJumping) {
+30 -1
View File
@@ -14,6 +14,9 @@
#include "RE/B/BipedAnim.h"
#include "RE/B/BSFixedString.h"
#include "RE/I/IAnimationGraphManagerHolder.h"
#include "RE/P/PipboyManager.h"
#include "RE/P/PipboyMenu.h"
#include "RE/U/UI.h"
#include <array>
#include <cmath>
@@ -51,6 +54,7 @@ namespace
bool isJumping;
bool isCrouching;
bool weaponDrawn;
bool isPipboyOpen;
float movementSpeed;
};
@@ -422,6 +426,22 @@ namespace
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(
const SneakDetectionState& a_sneakDetection,
std::chrono::steady_clock::time_point a_currentTime)
@@ -512,6 +532,7 @@ namespace
heldJumping,
GetCrouchState(a_player),
a_player.GetWeaponMagicDrawn(),
IsLocalPipboyOpen(),
a_movementSpeed
};
}
@@ -545,7 +566,8 @@ namespace
a_previous.isSneaking != a_current.isSneaking ||
a_previous.isJumping != a_current.isJumping ||
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)
@@ -726,6 +748,7 @@ namespace
debugMovementState.isJumping,
debugMovementState.isCrouching,
debugMovementState.weaponDrawn,
debugMovementState.isPipboyOpen,
debugMovementState.movementSpeed,
-1.0F,
currentEquipmentState,
@@ -818,6 +841,7 @@ namespace
networkMovementState.isJumping,
networkMovementState.isCrouching,
networkMovementState.weaponDrawn,
networkMovementState.isPipboyOpen,
networkMovementState.movementSpeed,
animationGraphSpeed,
currentEquipmentState,
@@ -839,6 +863,11 @@ namespace
} else if (hasAppearanceChanged) {
LogInfoWithLocalPlayerPrefix("Detected local appearance change. Sending immediate transform update.");
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) {
LogPlayerTransform(currentTransform);
lastLogTime = currentTime;
+2
View File
@@ -68,6 +68,7 @@ Example after server processing:
"isSneaking": false,
"isJumping": false,
"weaponDrawn": false,
"isPipboyOpen": false,
"movementSpeed": 186.4,
"equippedItems": [
{ "slot": "body", "formId": "0001F66A" },
@@ -106,6 +107,7 @@ isSprinting 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.
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.
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.
+4 -3
View File
@@ -67,6 +67,7 @@ isSneaking
isJumping
isCrouching
weaponDrawn
isPipboyOpen
movementSpeed
animationGraphSpeed
equippedItems
@@ -78,8 +79,8 @@ Required transform packet fields are `type`, `playerId`, `x`, `y`, `z`,
`angleZ`, and `cellId`. `worldspaceId`, `movementType`, `clientTime`, and
`serverTime` are optional; interior test cells may omit `worldspaceId`.
Movement state fields are also optional and default to not moving, not
sprinting, not sneaking, not jumping, weapon holstered, and speed `0.0` when
missing.
sprinting, not sneaking, not jumping, weapon holstered, Pip-Boy closed, and
speed `0.0` when missing.
`equippedItems` is also optional. New clients send it as a complete snapshot of
tracked visible apparel slots with `{ slot, formId }` entries; empty `formId`
@@ -125,7 +126,7 @@ player exists.
local test cell, then snaps back to a valid same-cell remote player before
resuming smoothing.
- Transform packets include movement state data (`isMoving`, `movementSpeed`,
`isSprinting`, `isSneaking`, `isJumping`, `weaponDrawn`).
`isSprinting`, `isSneaking`, `isJumping`, `weaponDrawn`, `isPipboyOpen`).
- Transform packets can include optional `equippedItems` snapshots for visible
clothing, armor, hats, and eyewear slots. The game-thread proxy controller
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.
Transform packets may include optional movement-state fields such as
`isMoving`, `isSprinting`, `isSneaking`, `isJumping`, `isCrouching`, `weaponDrawn`, and
`movementSpeed`. The server preserves these fields automatically because it
broadcasts the original transform packet after adding server-owned fields.
`isMoving`, `isSprinting`, `isSneaking`, `isJumping`, `isCrouching`, `weaponDrawn`,
`isPipboyOpen`, and `movementSpeed`. The server preserves these fields automatically
because it broadcasts the original transform packet after adding server-owned fields.
This is still a local prototype. It does not make Fallout 4 multiplayer
playable yet.
+5 -2
View File
@@ -212,6 +212,7 @@ def update_remote_player_state(packet: dict[str, Any]) -> None:
"isJumping": get_optional_bool(packet, "isJumping"),
"isCrouching": get_optional_bool(packet, "isCrouching"),
"weaponDrawn": get_optional_bool(packet, "weaponDrawn"),
"isPipboyOpen": get_optional_bool(packet, "isPipboyOpen"),
"movementSpeed": get_optional_float(packet, "movementSpeed"),
"actorStateFlags1": get_optional_uint32(packet, "actorStateFlags1"),
"actorStateFlags2": get_optional_uint32(packet, "actorStateFlags2"),
@@ -254,7 +255,8 @@ def print_remote_player(player: dict[str, Any]) -> None:
"Movement State: "
f"moving={player['isMoving']}, sprinting={player['isSprinting']}, "
f"sneaking={player['isSneaking']}, jumping={player['isJumping']}, "
f"crouching={player['isCrouching']}, weaponDrawn={player['weaponDrawn']}, speed={player['movementSpeed']:.1f}"
f"crouching={player['isCrouching']}, weaponDrawn={player['weaponDrawn']}, "
f"pipboyOpen={player['isPipboyOpen']}, speed={player['movementSpeed']:.1f}"
),
f"Actor State: flags1={player['actorStateFlags1']:08X}, flags2={player['actorStateFlags2']:08X}",
f"Equipment: {equipment_text}",
@@ -294,7 +296,8 @@ def print_remote_players() -> None:
f"angleZ={player['angleZ']:.2f}, movementType={player['movementType']}, "
f"moving={player['isMoving']}, sprinting={player['isSprinting']}, "
f"sneaking={player['isSneaking']}, jumping={player['isJumping']}, "
f"crouching={player['isCrouching']}, weaponDrawn={player['weaponDrawn']}, speed={player['movementSpeed']:.1f}, "
f"crouching={player['isCrouching']}, weaponDrawn={player['weaponDrawn']}, "
f"pipboyOpen={player['isPipboyOpen']}, speed={player['movementSpeed']:.1f}, "
f"flags1={player['actorStateFlags1']:08X}, flags2={player['actorStateFlags2']:08X}, "
f"equipment={equipment_text}, "
f"appearance={appearance_text}, "
+1
View File
@@ -577,6 +577,7 @@ class FakePlayerClient:
"isJumping": jumping,
"isCrouching": crouching,
"weaponDrawn": False,
"isPipboyOpen": False,
"movementSpeed": movement_speed,
"appearance": DEFAULT_APPEARANCE,
}