proxy: document visual sneak SetSneaking result
This commit is contained in:
+114
@@ -2375,6 +2375,120 @@ mutating actors by default.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-06-03 - Proxy Visual Sneak Stage 1 SetSneaking Experiment
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Enabled the runtime-proxy-only visual sneak experiment that calls
|
||||||
|
`RE::Actor::SetSneaking(...)` on assigned runtime proxy actors when the matching
|
||||||
|
remote player's `isSneaking` state changes.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
|
||||||
|
- `plugin/src/F4TProxyActorController.cpp`
|
||||||
|
- `docs/dev-log.md`
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
- Set `kEnableProxyVisualSneakSync` to `true` and kept
|
||||||
|
`kEnableProxyVisualSneakExperimentalGraph` set to `false`.
|
||||||
|
- Kept `kEnableProxyAIMovementIntentSuppression` disabled.
|
||||||
|
- Replaced the enabled visual-sneak TODO path with a per-slot transition writer
|
||||||
|
that calls `RE::Actor::SetSneaking(a_remotePlayer.isSneaking)` only when the
|
||||||
|
requested visual sneak state differs from the last applied state, or when no
|
||||||
|
visual sneak state has been applied to that slot yet.
|
||||||
|
- Tracked applied visual sneak state with the existing
|
||||||
|
`hasAppliedVisualSneakingState` and `lastAppliedVisualSneakingState` fields.
|
||||||
|
- Added transition-only logs for applied visual sneak `true`, applied visual
|
||||||
|
sneak `false`, and `SetSneaking(...)` returning `false`.
|
||||||
|
- Added lifecycle clear handling that calls `SetSneaking(false)` for a valid
|
||||||
|
runtime proxy actor before hold/disconnect, reusable transition, or
|
||||||
|
reassignment clears visual sneak tracking.
|
||||||
|
- Preserved existing animation debug logs, runtime proxy spawn/movement/snap
|
||||||
|
behavior, same-cell gating, held/disconnected/reusable lifecycle, combat/AI
|
||||||
|
neutralization, placed fallback availability, and the game-thread proxy
|
||||||
|
controller boundary.
|
||||||
|
- Left networking, protocol, Python server code, fake-client tooling, local
|
||||||
|
player send logic, remote state storage, animation graph events/variables,
|
||||||
|
`PerformAction`, `ActorState` writes, package/pathing/AI movement changes, and
|
||||||
|
vendored CommonLibF4 unchanged.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- Ran `xmake build` from `plugin`; it succeeded and rebuilt
|
||||||
|
`Fallout4Together.dll`.
|
||||||
|
- Manual test placeholder: start the Dev Server GUI, launch Fallout 4 through
|
||||||
|
F4SE, `coc F4TTestCell01`, add fake clients, and toggle sneak on/off.
|
||||||
|
- Expected manual result: only the assigned runtime proxy for the toggled fake
|
||||||
|
client receives visual sneak transition logs and, if `SetSneaking(...)` works
|
||||||
|
visually, crouches/stands with that fake client's `isSneaking` state.
|
||||||
|
- Expected disconnect/reuse result: held or reused runtime proxies are cleared
|
||||||
|
with `SetSneaking(false)` and do not remain stuck crouched for the next remote
|
||||||
|
player.
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
|
||||||
|
- Visual crouch behavior is still experimental. `SetSneaking(...)` may return
|
||||||
|
false or may not visibly crouch this kind of runtime proxy actor, but the
|
||||||
|
transition attempt should be logged without crashing.
|
||||||
|
- Manual in-game validation is still required for visual behavior, multi-client
|
||||||
|
isolation, disconnect cleanup, and reuse cleanup.
|
||||||
|
|
||||||
|
### Rollback
|
||||||
|
|
||||||
|
- If the proxy crashes, stops moving, gets stuck crouched, affects the local
|
||||||
|
player, affects the wrong proxy, starts independent AI walking, or breaks the
|
||||||
|
disconnect/reuse lifecycle, set `kEnableProxyVisualSneakSync` back to `false`
|
||||||
|
and keep the scaffolding/debug logs.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- Run the manual fake-client sneak toggle plan in `F4TTestCell01`.
|
||||||
|
- Record whether `SetSneaking(...)` produces a visible crouch/stand result on
|
||||||
|
runtime proxy actors.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-06-03 - Proxy Visual Sneak Stage 1 Rollback
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Rolled the runtime proxy visual sneak mutation flag back off after Stage 1
|
||||||
|
confirmed `RE::Actor::SetSneaking(bool)` is safe to call but does not visually
|
||||||
|
crouch transform-controlled runtime proxy actors.
|
||||||
|
|
||||||
|
### Files Changed
|
||||||
|
|
||||||
|
- `plugin/src/F4TProxyActorController.cpp`
|
||||||
|
- `docs/dev-log.md`
|
||||||
|
|
||||||
|
### Proxy Visual Sneak Stage 1 Result
|
||||||
|
|
||||||
|
`RE::Actor::SetSneaking(bool)` was tested as the first visual sneak candidate. The call succeeded and did not crash, and per-slot sneak state transitions were detected correctly. However, the runtime proxy did not visually crouch in-game. This suggests `SetSneaking` alone does not drive the actor animation graph for transform-controlled runtime proxy actors.
|
||||||
|
|
||||||
|
The visual mutation flag has been set back to disabled by default. The scaffolding, read-only diagnostics, and transition logging remain in place for future animation investigation.
|
||||||
|
|
||||||
|
Next investigation should focus on read-only animation graph variable discovery before attempting graph writes or animation events.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- Ran `xmake build` from `plugin`; it succeeded and rebuilt
|
||||||
|
`Fallout4Together.dll`.
|
||||||
|
- Pending manual validation: fake-client sneak toggles in `F4TTestCell01`
|
||||||
|
should still produce animation debug `isSneaking` transition logs without
|
||||||
|
applying visual sneak mutation by default.
|
||||||
|
|
||||||
|
### Known Issues
|
||||||
|
|
||||||
|
- Visual crouch behavior remains unresolved; `SetSneaking(...)` alone did not
|
||||||
|
visibly crouch runtime proxy actors.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- Proxy Animation Graph Read-Only Investigation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Entry Template
|
## Entry Template
|
||||||
|
|
||||||
Use this format for future updates:
|
Use this format for future updates:
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ namespace
|
|||||||
constexpr auto kRuntimeProxyVisibilityDiagnosticLogInterval = 2s;
|
constexpr auto kRuntimeProxyVisibilityDiagnosticLogInterval = 2s;
|
||||||
constexpr auto kProxyNeutralizationInterval = 2s;
|
constexpr auto kProxyNeutralizationInterval = 2s;
|
||||||
constexpr bool kEnableProxyAIMovementIntentSuppression = false;
|
constexpr bool kEnableProxyAIMovementIntentSuppression = false;
|
||||||
|
// Stage 1 tested RE::Actor::SetSneaking(bool). It compiled and did not
|
||||||
|
// crash, but did not visually crouch runtime proxy actors. Keep disabled
|
||||||
|
// until animation graph/state investigation identifies a better visual route.
|
||||||
constexpr bool kEnableProxyVisualSneakSync = false;
|
constexpr bool kEnableProxyVisualSneakSync = false;
|
||||||
constexpr bool kEnableProxyVisualSneakExperimentalGraph = false;
|
constexpr bool kEnableProxyVisualSneakExperimentalGraph = false;
|
||||||
constexpr auto kNotInTestCellLogInterval = 30s;
|
constexpr auto kNotInTestCellLogInterval = 30s;
|
||||||
@@ -4073,6 +4076,34 @@ namespace
|
|||||||
a_slot.lastAppliedVisualSneakingState.reset();
|
a_slot.lastAppliedVisualSneakingState.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClearSlotVisualSneakIfApplied(
|
||||||
|
ProxyActorSlot& a_slot,
|
||||||
|
RE::Actor& a_proxy,
|
||||||
|
std::string_view a_reason)
|
||||||
|
{
|
||||||
|
if (!a_slot.hasAppliedVisualSneakingState) {
|
||||||
|
ResetSlotVisualSneakTracking(a_slot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto applied = a_proxy.SetSneaking(false);
|
||||||
|
if (applied) {
|
||||||
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
|
"Runtime proxy visual sneak cleared on {} for remote player {}: actor={:08X}, appliedSneaking=false.",
|
||||||
|
a_reason,
|
||||||
|
a_slot.remotePlayerId,
|
||||||
|
a_proxy.GetFormID()));
|
||||||
|
} else {
|
||||||
|
LogWarningWithLocalPlayerPrefix(std::format(
|
||||||
|
"Runtime proxy visual sneak clear on {} returned false for remote player {}: actor={:08X}, requestedSneaking=false.",
|
||||||
|
a_reason,
|
||||||
|
a_slot.remotePlayerId,
|
||||||
|
a_proxy.GetFormID()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetSlotVisualSneakTracking(a_slot);
|
||||||
|
}
|
||||||
|
|
||||||
void ResetSlotObservationState(ProxyActorSlot& a_slot)
|
void ResetSlotObservationState(ProxyActorSlot& a_slot)
|
||||||
{
|
{
|
||||||
a_slot.hasLoggedInitialAnimationState = false;
|
a_slot.hasLoggedInitialAnimationState = false;
|
||||||
@@ -4143,10 +4174,11 @@ namespace
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClearSlotVisualSneakIfApplied(
|
||||||
|
a_slot,
|
||||||
|
a_proxy,
|
||||||
|
a_reason == ProxyLifecycleState::kHeldDisconnected ? "disconnect hold" : "hold");
|
||||||
a_proxy.SetPosition(GetSlotHoldingPosition(a_slot), true);
|
a_proxy.SetPosition(GetSlotHoldingPosition(a_slot), true);
|
||||||
// TODO: If a later approved visual writer is added, clear any applied
|
|
||||||
// proxy sneak visual here before holding/reusing the actor.
|
|
||||||
ResetSlotVisualSneakTracking(a_slot);
|
|
||||||
if (auto* player = RE::PlayerCharacter::GetSingleton()) {
|
if (auto* player = RE::PlayerCharacter::GetSingleton()) {
|
||||||
NeutralizeProxyActor(a_slot, a_proxy, *player, ProxyNeutralizationReason::kHolding);
|
NeutralizeProxyActor(a_slot, a_proxy, *player, ProxyNeutralizationReason::kHolding);
|
||||||
}
|
}
|
||||||
@@ -4199,7 +4231,9 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MarkReusableSlots()
|
void MarkReusableSlots(
|
||||||
|
RE::TESObjectCELL& a_parentCell,
|
||||||
|
const RE::PlayerCharacter& a_player)
|
||||||
{
|
{
|
||||||
const auto now = std::chrono::steady_clock::now();
|
const auto now = std::chrono::steady_clock::now();
|
||||||
for (auto& [playerId, slot] : g_proxySlots) {
|
for (auto& [playerId, slot] : g_proxySlots) {
|
||||||
@@ -4212,10 +4246,14 @@ namespace
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (auto* proxy = TryResolveSlotProxy(slot, a_parentCell, a_player)) {
|
||||||
|
ClearSlotVisualSneakIfApplied(slot, *proxy, "reuse");
|
||||||
|
} else {
|
||||||
|
ResetSlotVisualSneakTracking(slot);
|
||||||
|
}
|
||||||
slot.lifecycleState = ProxyLifecycleState::kReusable;
|
slot.lifecycleState = ProxyLifecycleState::kReusable;
|
||||||
slot.holdReason.reset();
|
slot.holdReason.reset();
|
||||||
slot.previousRemotePlayerId = playerId;
|
slot.previousRemotePlayerId = playerId;
|
||||||
ResetSlotVisualSneakTracking(slot);
|
|
||||||
if (!slot.reusableLogged) {
|
if (!slot.reusableLogged) {
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Runtime proxy slot for disconnected remote player {} became reusable after {:.1f}s held.",
|
"Runtime proxy slot for disconnected remote player {} became reusable after {:.1f}s held.",
|
||||||
@@ -4306,18 +4344,30 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
if constexpr (kEnableProxyVisualSneakSync) {
|
if constexpr (kEnableProxyVisualSneakSync) {
|
||||||
if (!a_slot.hasLoggedVisualSneakDisabled) {
|
const auto requestedSneaking = a_remotePlayer.isSneaking;
|
||||||
LogWarningWithLocalPlayerPrefix(std::format(
|
if (a_slot.hasAppliedVisualSneakingState &&
|
||||||
"Runtime proxy visual sneak sync flag is enabled for remote player {}, but no actor-mutating sneak writer is implemented in this scaffolding pass. actor={:08X}.",
|
a_slot.lastAppliedVisualSneakingState &&
|
||||||
|
*a_slot.lastAppliedVisualSneakingState == requestedSneaking) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto applied = a_proxy.SetSneaking(requestedSneaking);
|
||||||
|
a_slot.hasAppliedVisualSneakingState = true;
|
||||||
|
a_slot.lastAppliedVisualSneakingState = requestedSneaking;
|
||||||
|
|
||||||
|
if (applied) {
|
||||||
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
|
"Runtime proxy visual sneak applied {} for remote player {}: actor={:08X}.",
|
||||||
|
requestedSneaking,
|
||||||
a_remotePlayer.playerId,
|
a_remotePlayer.playerId,
|
||||||
a_proxy.GetFormID()));
|
a_proxy.GetFormID()));
|
||||||
a_slot.hasLoggedVisualSneakDisabled = true;
|
} else {
|
||||||
|
LogWarningWithLocalPlayerPrefix(std::format(
|
||||||
|
"Runtime proxy visual sneak SetSneaking returned false for remote player {}: actor={:08X}, requestedSneaking={}.",
|
||||||
|
a_remotePlayer.playerId,
|
||||||
|
a_proxy.GetFormID(),
|
||||||
|
requestedSneaking));
|
||||||
}
|
}
|
||||||
// TODO: In a separately approved experiment, consider calling
|
|
||||||
// RE::Actor::SetSneaking(a_remotePlayer.isSneaking) here behind this
|
|
||||||
// flag, with explicit clear handling on false, disconnect, hold,
|
|
||||||
// reassignment, and reuse. Do not add graph writes/events,
|
|
||||||
// PerformAction, ActorState writes, or package/procedure crouch logic.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4479,6 +4529,11 @@ namespace
|
|||||||
|
|
||||||
auto& slotValue = node.mapped();
|
auto& slotValue = node.mapped();
|
||||||
const auto previousRemotePlayerId = slotValue.remotePlayerId;
|
const auto previousRemotePlayerId = slotValue.remotePlayerId;
|
||||||
|
if (auto* proxy = TryResolveSlotProxy(slotValue, a_parentCell, a_player)) {
|
||||||
|
ClearSlotVisualSneakIfApplied(slotValue, *proxy, "reassignment");
|
||||||
|
} else {
|
||||||
|
ResetSlotVisualSneakTracking(slotValue);
|
||||||
|
}
|
||||||
slotValue.previousRemotePlayerId = previousRemotePlayerId;
|
slotValue.previousRemotePlayerId = previousRemotePlayerId;
|
||||||
slotValue.remotePlayerId = a_remotePlayer.playerId;
|
slotValue.remotePlayerId = a_remotePlayer.playerId;
|
||||||
slotValue.lifecycleState = ProxyLifecycleState::kHeldNoRemote;
|
slotValue.lifecycleState = ProxyLifecycleState::kHeldNoRemote;
|
||||||
@@ -4738,7 +4793,7 @@ namespace F4T::ProxyActorController
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto remotePlayers = GetSortedRemotePlayerSnapshot();
|
const auto remotePlayers = GetSortedRemotePlayerSnapshot();
|
||||||
MarkReusableSlots();
|
MarkReusableSlots(*parentCell, *player);
|
||||||
std::unordered_set<std::uint32_t> seenRemotePlayerIds;
|
std::unordered_set<std::uint32_t> seenRemotePlayerIds;
|
||||||
seenRemotePlayerIds.reserve(remotePlayers.size());
|
seenRemotePlayerIds.reserve(remotePlayers.size());
|
||||||
std::optional<RemotePlayer> fallbackCandidate;
|
std::optional<RemotePlayer> fallbackCandidate;
|
||||||
@@ -4795,7 +4850,7 @@ namespace F4T::ProxyActorController
|
|||||||
}
|
}
|
||||||
|
|
||||||
HoldDisconnectedSlots(seenRemotePlayerIds, *parentCell, *player);
|
HoldDisconnectedSlots(seenRemotePlayerIds, *parentCell, *player);
|
||||||
MarkReusableSlots();
|
MarkReusableSlots(*parentCell, *player);
|
||||||
CorrectHeldSlotPositions(*parentCell, *player);
|
CorrectHeldSlotPositions(*parentCell, *player);
|
||||||
HoldFallbackIfRepresentedPlayerDisconnected(seenRemotePlayerIds, *parentCell, *player);
|
HoldFallbackIfRepresentedPlayerDisconnected(seenRemotePlayerIds, *parentCell, *player);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user