Neutralize runtime proxy actors and docs

Add a safe neutralization pass for Stage 4 runtime proxy actors and document the change. Introduces NeutralizeProxyActor and supporting helpers (ProxyNeutralizationReason, GetProxyNeutralizationReasonLabel, FormatPosition, IsProxyMovementPackageType, IsProxyFleeOrAlarmPackageType) plus kProxyNeutralizationInterval and a compile-time gate kEnableProxyAIMovementIntentSuppression (default=false). Adds SuppressProxyAIMovementIntent (default-disabled due to a crash observed during testing) which attempts safe package/process interruption using only confirmed CommonLibF4 APIs (EndInterruptPackage, InitiateDoNothingPackage, SetAvoidanceDisabled, command/process handle clears) and extensive throttled logs and TODOs for further unsafe changes. Neutralization is invoked after spawn promotion, on slot reassignment, during active maintenance, and while holding. Updates plugin logic to call neutralization at these points. Updates docs/architecture.md and docs/dev-log.md with the design, testing notes, and rationale (including the safety gate and next steps). Builds succeed; manual in-game validations are still pending.
This commit is contained in:
2026-06-03 11:24:45 +12:00
parent 5394250ef2
commit 3d1abc66ad
3 changed files with 544 additions and 11 deletions
+31 -11
View File
@@ -41,59 +41,69 @@ both send and receive paths:
player's current valid transform before spawning and moves the isolated
runtime actor to that transform immediately, so normal remote representation
does not visually start at the local player offset.
14. Proxy visual updates are independent per slot. Normal movement is smoothed
14. Runtime proxies receive a small game-thread neutralization pass when a
PlaceAtMe-backed actor is promoted, when a reusable slot is reassigned, while
active on a throttle, and when moved or kept in holding. The pass only uses
confirmed CommonLibF4 actor APIs/fields to stop combat, clear combat target
handles, reset safe hostile/attack state, suppress combat-oriented process
flags, clear exposed AI process movement targets, and attempt safe
package/pathing interruption with a do-nothing package when pathing, flee,
alarm, bump, or package movement intent is observed. It does not disable,
hide, delete, despawn, alpha-fade, or force animation graph events on the
actor.
15. Proxy visual updates are independent per slot. Normal movement is smoothed
toward that player’s latest target, while `cell_change`, `worldspace_change`,
and `teleport` snap directly. Sneak state is still observed and logged only;
no visual crouch/sneak application is attempted.
15. When a remote player leaves the cell or disconnects, only that player’s
16. When a remote player leaves the cell or disconnects, only that player’s
runtime proxy is moved to the hidden holding position inside
`F4TTestCell01`. Runtime actors are not disabled or deleted. Connected
players who leave the cell keep their slot reserved, while disconnected
held slots become reusable only after the configured grace period. Runtime
slots use per-slot holding positions with spacing and a game-thread
correction check so held actors do not stack or drift back into view.
16. The placed fallback reference `F4TProxyRemotePlayer01REF` remains a single
17. The placed fallback reference `F4TProxyRemotePlayer01REF` remains a single
backup representation. If runtime spawning fails for one deterministic
selected remote player, the fallback can represent that one player only; it
is not used for multiple remote players.
17. As part of the staged runtime proxy actor manager work, the same game-thread
18. As part of the staged runtime proxy actor manager work, the same game-thread
proxy controller also logs whether `Fallout4Together_Test.esp` is loaded and
whether the actor base `F4T_RemotePlayerProxy` resolves by editor ID.
18. Stage 3.1 adds a temporary diagnostic layer around the single runtime proxy:
19. Stage 3.1 adds a temporary diagnostic layer around the single runtime proxy:
post-spawn visibility data, loaded-3D pointer checks where safe, console
helper logs, active source logs, and throttled post-movement diagnostics.
The diagnostic mode can force the runtime proxy near the local player to
prove whether the created actor is visibly renderable.
19. Stage 3.2 adds a temporary near-player visibility hold for that same single
20. Stage 3.2 adds a temporary near-player visibility hold for that same single
runtime proxy. During the hold, remote state is still selected and validated,
but runtime proxy restore and remote movement are deferred long enough to
test whether a runtime-created actor ref can load visible 3D while kept near
the local player.
20. Stage 3.3 adds a temporary vanilla actor-base diagnostic layer. It attempts
21. Stage 3.3 adds a temporary vanilla actor-base diagnostic layer. It attempts
to resolve Codsworth as a visible vanilla `TESNPC` candidate, then passes
that actor base into the same runtime spawn, handle validation, near-player
hold, and visibility diagnostic path. This isolates custom actor-base issues
from runtime-spawn/init issues without changing the final architecture.
21. Stage 3.4 adds a temporary absolute FormID and existing-reference diagnostic
22. Stage 3.4 adds a temporary absolute FormID and existing-reference diagnostic
layer. It compares Codsworth base-form runtime spawning against moving the
existing placed Codsworth reference near the local player as a visibility
control. This separates runtime actor creation/init problems from vanilla
actor-base or placed-reference movement problems and does not change the
final architecture.
22. Stage 3.5 adds a temporary PlaceAtMe-equivalent diagnostic after manual
23. Stage 3.5 adds a temporary PlaceAtMe-equivalent diagnostic after manual
console validation proved `player.placeatme 000179FF 1` can spawn visible
Codsworth in `F4TTestCell01`. The plugin executes that console placement path
from the local player, resolves the newly created Codsworth actor near the
player, and holds/logs it separately from the `CreateReferenceAtLocation`
runtime proxy path.
23. Stage 3.6 refines that temporary PlaceAtMe diagnostic layer. It snapshots
24. Stage 3.6 refines that temporary PlaceAtMe diagnostic layer. It snapshots
known Codsworth refs before executing `player.placeatme 000179FF 1`, accepts
only a newly isolated Codsworth ref for the primary diagnostic, and preserves
that actor at the original console-spawned location during a short settle
window. This tests whether the actor becomes visible naturally or requires a
render/process refresh such as the manually observed pause-menu or alt-tab
behavior. Stage 3.6 does not change the final architecture.
24. Stage 3.7 promotes the working PlaceAtMe-style spawn pattern into the
25. Stage 3.7 promotes the working PlaceAtMe-style spawn pattern into the
preferred single runtime proxy spawn path. The proxy controller uses the
resolved runtime FormID for `F4T_RemotePlayerProxy` when available, falls
back to Codsworth only for diagnostics, defers active proxy selection during
@@ -139,6 +149,14 @@ The native plugin is responsible for:
`F4T_RemotePlayerProxy`
- Capturing the remote player's current valid transform before Stage 4 runtime
spawn and applying it as soon as the PlaceAtMe-backed actor is isolated
- Neutralizing only resolved runtime proxy actors on the game-thread controller
path after spawn promotion, during reusable-slot reassignment, periodically
while active, and while held so they remain passive visual puppets rather than
independent combat actors
- Suppressing exposed AI movement intent for runtime proxies by clearing safe
process target handles and using validated package interruption/do-nothing
calls when combat, pathing, flee/alarm, bump, or package movement state is
observed
- Logging temporary Stage 3.1 visibility and movement diagnostics for the single
runtime proxy, including active proxy source, runtime FormID, local distance,
and safe loaded-3D checks
@@ -219,6 +237,8 @@ Game update attempts at most one PlaceAtMe-backed runtime actor spawn at a time
Game update snapshots matching candidates, isolates the new actor, and waits for the settle window
Game update moves isolated Stage 4 runtime spawn candidates to the remote player's current transform
Game update stores promoted runtime actors as ObjectRefHandle values in slots
Game update neutralizes resolved runtime proxy actors after spawn promotion, reuse, active throttled checks, and holding
Game update suppresses exposed AI movement/pathing intent without forcing animation graph events
Game update moves each valid proxy from its assigned remote player state
Game update holds only proxies whose assigned remote player left the cell or disappeared
Game update corrects held proxy slots back to their per-slot hidden positions
+179
View File
@@ -1969,6 +1969,185 @@ proxy cap forever.
---
## 2026-06-03 - Proxy AI Movement Intent Crash-Safety Gate
### Summary
Default-disabled the experimental AI movement/pathing/package suppression path
after manual testing showed a crash during runtime proxy spawn/settle with a fake
client connected.
### Files Changed
- `plugin/src/F4TProxyActorController.cpp`
- `docs/dev-log.md`
### Details
- Added `kEnableProxyAIMovementIntentSuppression = false` so the risky follow-up
suppression path is compiled behind an explicit safety flag.
- Gated the `SuppressProxyAIMovementIntent(...)` calls from
`NeutralizeProxyActor(...)` so the default runtime path no longer executes
pathing status checks, package interruption, do-nothing package calls,
command clearing, or AI process movement target writes.
- Preserved the earlier neutralization behavior that was working better:
`StopCombat()` only after `IsInCombat()`, combat target reset, attack/hostile
flag reset, passive actor values, combat-oriented process flags, and throttled
logs.
- Left runtime proxy spawning, per-`playerId` slots, spawn-at-remote-position,
movement smoothing/snapping, held/reusable lifecycle, placed fallback behavior,
networking, protocol, Python server, and fake-client tooling unchanged.
- Added a TODO in code noting that AI movement intent suppression caused a crash
and must be re-investigated in smaller isolated steps before it is enabled
again.
### Testing
- Ran `xmake build` from `plugin`; it succeeded and rebuilt
`Fallout4Together.dll`.
- Pending manual validation: start server, add fake client, connect Fallout 4,
and confirm runtime proxy spawn/settle no longer crashes.
- Pending manual validation: re-test multiple clients, movement, held/reusable
lifecycle, and attack/bump behavior.
### Known Issues
- Attacking or bumping a proxy may still briefly trigger walk-away animation
intent because the experimental movement/pathing/package suppression is now
disabled by default for crash safety.
- Runtime actors are still held instead of safely despawned or deleted.
### Next Steps
- Re-test the fake-client spawn path immediately.
- Revisit AI movement intent suppression later with a diagnostics-only pass first,
then re-enable one write/call at a time if each step is stable in-game.
---
## 2026-06-03 - Proxy AI Movement Intent Suppression Patch
### Summary
Reduced the remaining autonomous walk-away/flee animation intent that could play
after a runtime proxy was attacked or bumped, while keeping transform authority
driven by `RemotePlayerState`.
### Files Changed
- `plugin/src/F4TProxyActorController.cpp`
- `docs/architecture.md`
- `docs/dev-log.md`
### Details
- Added `SuppressProxyAIMovementIntent(...)` and call it through the existing
`NeutralizeProxyActor(...)` path after spawn promotion, reusable slot
reassignment, throttled active maintenance, combat neutralization, combat
target clearing, and holding.
- The helper uses only exposed CommonLibF4 APIs/fields: pathing status checks,
`SetAvoidanceDisabled(true)`, `EndInterruptPackage(false)`,
`InitiateDoNothingPackage()`, command clearing, and safe `AIProcess` target
handle resets.
- Detects flee/alarm/search/avoid/bump/travel/follow/patrol-style package types
when exposed through the current package and logs throttled suppression events.
- Leaves transform smoothing, snapping, spawn-at-remote-position, held slots,
reusable slots, placed fallback behavior, networking, protocol, Python server,
fake-client tooling, gameplay sync, and animation sync unchanged.
- Added TODOs for movement-controller, actor-mover, procedure-index, preferred
speed, and animation-layer resets because no clearly safe direct path-cancel
or animation reset API has been validated yet.
### Testing
- Ran `xmake build` from `plugin`; it succeeded and rebuilt
`Fallout4Together.dll`.
- Pending manual validation: attack a runtime proxy and confirm walk-away
animation intent is gone or reduced.
- Pending manual validation: bump a runtime proxy and confirm no persistent
autonomous walk/flee animation overrides remote-state transform control.
- Pending manual validation: re-test multiple clients, spawn-at-remote-position,
smoothing, snap movement types, held/disconnected/reusable slots, and Leave
Cell / Return To Cell.
### Known Issues
- Runtime actors are still held instead of safely despawned or deleted.
- Direct movement-controller/path-destination clearing is still not implemented
because the local CommonLibF4 headers do not expose a validated safe API for it.
- If the engine keeps an animation-layer response active after package
suppression, a future patch may need a narrowly validated animation reset that
does not block remote-driven animation sync.
### Next Steps
- Manually validate attack and bump behavior in `F4TTestCell01`.
- If walk-away animation still persists, inspect runtime logs for pathing,
package type, process target, and no-safe-API messages before adding deeper AI
or animation control.
---
## 2026-06-03 - Runtime Proxy Neutralization Pass
### Summary
Added a safe runtime proxy actor neutralization pass so Stage 4 runtime proxies
remain passive visual representations controlled by remote player state instead
of entering persistent NPC combat or pursuit behavior.
### Files Changed
- `plugin/src/F4TProxyActorController.cpp`
- `docs/architecture.md`
- `docs/dev-log.md`
### Details
- Added `NeutralizeProxyActor(...)` on the game-thread proxy controller path and
only call it on resolved runtime proxy slot actors, never the local player.
- Neutralization now runs after runtime proxy spawn promotion, when reusable
slots are reassigned, periodically while active, and when proxies are moved or
kept in holding.
- The helper uses confirmed available CommonLibF4 actor APIs/fields to stop
combat, clear the current combat target handle, reset safe attack/hostility
flags, set passive aggression/confidence/assistance actor values, and suppress
combat-oriented AI process flags.
- Kept runtime proxies renderable and reusable. No `Disable()`, `Enable()`,
alpha/invisibility, `SetWantsDelete`, deletion, protocol, networking, Python
server, fake-client, gameplay sync, animation sync, combat sync, inventory
sync, quest sync, settlement sync, or placed fallback changes were made.
- Added TODOs for package/procedure clearing and collision/stagger suppression
because no clearly safe CommonLibF4 API has been validated for those yet.
### Testing
- Ran `xmake build` from `plugin`; it succeeded and rebuilt
`Fallout4Together.dll`.
- Pending manual validation: attack a runtime proxy and confirm it does not chase
or attack the local player.
- Pending manual validation: bump a runtime proxy and confirm no persistent AI
behavior overrides remote-state transform control.
- Pending manual validation: re-test multiple clients, spawn-at-remote-position,
smoothing, `cell_change`/`worldspace_change`/`teleport` snapping,
held/disconnected/reusable slots, and Leave Cell / Return To Cell.
### Known Issues
- Runtime actors are still held instead of safely despawned or deleted.
- Package/procedure clearing and collision/stagger suppression need further
investigation before adding more invasive AI control.
- The placed fallback remains single-player only by design.
### Next Steps
- Run the manual attack/bump validation in `F4TTestCell01`.
- If proxies can still enter transient stagger animations, investigate a
separate collision/stagger-safe puppet-control pass that does not block future
animation sync.
---
## 2026-06-03 - Held Runtime Proxy Stability Patch
### Summary
+334
View File
@@ -5,7 +5,10 @@
#include "F4TNetworking.h"
#include "F4TRemotePlayerState.h"
#include "RE/A/ActorValue.h"
#include "RE/C/COMMAND_TYPE.h"
#include "RE/C/Console.h"
#include "RE/P/PTYPE.h"
#include <algorithm>
#include <charconv>
@@ -66,6 +69,8 @@ namespace
constexpr auto kWarningLogInterval = 5s;
constexpr auto kProxyMovementDiagnosticLogInterval = 2s;
constexpr auto kRuntimeProxyVisibilityDiagnosticLogInterval = 2s;
constexpr auto kProxyNeutralizationInterval = 2s;
constexpr bool kEnableProxyAIMovementIntentSuppression = false;
constexpr auto kNotInTestCellLogInterval = 30s;
using RemotePlayer = F4T::RemotePlayerState::RemotePlayerState;
@@ -128,6 +133,14 @@ namespace
kSnap
};
enum class ProxyNeutralizationReason
{
kSpawnPromoted,
kSlotReassigned,
kActiveMaintenance,
kHolding
};
struct RemotePlayerSelection
{
std::optional<RemotePlayer> selectedPlayer;
@@ -349,6 +362,22 @@ namespace
void ResetRemoteMovementStateTracking();
const char* GetProxyNeutralizationReasonLabel(ProxyNeutralizationReason a_reason)
{
switch (a_reason) {
case ProxyNeutralizationReason::kSpawnPromoted:
return "spawn_promoted";
case ProxyNeutralizationReason::kSlotReassigned:
return "slot_reassigned";
case ProxyNeutralizationReason::kActiveMaintenance:
return "active_maintenance";
case ProxyNeutralizationReason::kHolding:
return "holding";
default:
return "unknown";
}
}
std::string FormatPosition(const RE::NiPoint3& a_position)
{
return std::format("({:.2f}, {:.2f}, {:.2f})", a_position.x, a_position.y, a_position.z);
@@ -362,6 +391,295 @@ namespace
return std::sqrt((deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ));
}
bool IsProxyMovementPackageType(RE::PTYPE a_type)
{
switch (a_type) {
case RE::PTYPE::kFleeNonCombat:
case RE::PTYPE::kAlarm:
case RE::PTYPE::kFlee:
case RE::PTYPE::kSearchForAttacker:
case RE::PTYPE::kAvoidReference:
case RE::PTYPE::kBumpReaction:
case RE::PTYPE::kMovementBlocked:
case RE::PTYPE::kFollow:
case RE::PTYPE::kTravel:
case RE::PTYPE::kPatrol:
return true;
default:
return false;
}
}
bool IsProxyFleeOrAlarmPackageType(RE::PTYPE a_type)
{
return a_type == RE::PTYPE::kFleeNonCombat ||
a_type == RE::PTYPE::kAlarm ||
a_type == RE::PTYPE::kFlee ||
a_type == RE::PTYPE::kSearchForAttacker ||
a_type == RE::PTYPE::kAvoidReference ||
a_type == RE::PTYPE::kBumpReaction;
}
void SuppressProxyAIMovementIntent(
ProxyActorSlot& a_slot,
RE::Actor& a_proxy,
const RE::PlayerCharacter& a_player,
ProxyNeutralizationReason a_reason,
bool a_forcePackageClear)
{
if constexpr (!kEnableProxyAIMovementIntentSuppression) {
(void)a_slot;
(void)a_proxy;
(void)a_player;
(void)a_reason;
(void)a_forcePackageClear;
return;
}
if (std::addressof(a_proxy) == std::addressof(a_player)) {
return;
}
const auto reason = GetProxyNeutralizationReasonLabel(a_reason);
const auto actorId = a_proxy.GetFormID();
const auto playerId = a_slot.remotePlayerId;
const auto pathingActive = a_proxy.IsPathing();
const auto pathValid = a_proxy.IsPathValid();
const auto pathComplete = a_proxy.IsPathingComplete();
const auto pathingIntentObserved = pathingActive || (pathValid && !pathComplete);
auto processTargetsCleared = false;
auto commandCleared = false;
auto movementPackageObserved = false;
auto fleeOrAlarmPackageObserved = false;
auto packageTypeForLog = -1;
if (auto* process = a_proxy.currentProcess) {
auto clearHandle = [&](auto& a_handle) {
if (!a_handle) {
return false;
}
a_handle.reset();
return true;
};
processTargetsCleared |= clearHandle(process->target);
processTargetsCleared |= clearHandle(process->followTarget);
processTargetsCleared |= clearHandle(process->genericLocation);
processTargetsCleared |= clearHandle(process->genericSecondLocation);
processTargetsCleared |= clearHandle(process->patrolLocation);
processTargetsCleared |= clearHandle(process->currentPackage.target);
if (process->acquireObject) {
process->acquireObject = nullptr;
processTargetsCleared = true;
}
if (process->savedAcquireObject) {
process->savedAcquireObject = nullptr;
processTargetsCleared = true;
}
const auto commandType = process->GetCommandType();
if (commandType != RE::COMMAND_TYPE::kNone) {
process->SetCommandType(RE::COMMAND_TYPE::kNone);
commandCleared = true;
}
if (const auto* currentPackage = process->currentPackage.package) {
const auto packageType = static_cast<RE::PTYPE>(currentPackage->data.packType);
packageTypeForLog = static_cast<int>(currentPackage->data.packType);
movementPackageObserved = IsProxyMovementPackageType(packageType);
fleeOrAlarmPackageObserved = IsProxyFleeOrAlarmPackageType(packageType);
}
}
const auto shouldClearPackage =
a_forcePackageClear ||
pathingIntentObserved ||
processTargetsCleared ||
commandCleared ||
movementPackageObserved;
if (!shouldClearPackage) {
return;
}
a_proxy.SetAvoidanceDisabled(true);
a_proxy.EndInterruptPackage(false);
a_proxy.InitiateDoNothingPackage();
a_proxy.ClearAttackStates();
LogThrottledInfo(
"runtime_proxy_ai_movement_intent_suppressed_" + std::to_string(playerId) + "_" + std::to_string(actorId),
std::format(
"Runtime proxy actor {:08X} for remote player {} had AI movement/pathing intent suppressed. reason={}, pathingActive={}, pathValid={}, pathComplete={}, processTargetsCleared={}, commandCleared={}, movementPackageObserved={}, packageType={}.",
actorId,
playerId,
reason,
pathingActive,
pathValid,
pathComplete,
processTargetsCleared,
commandCleared,
movementPackageObserved,
packageTypeForLog));
LogThrottledInfo(
"runtime_proxy_package_clear_attempted_" + std::to_string(playerId) + "_" + std::to_string(actorId),
std::format(
"Runtime proxy actor {:08X} for remote player {} attempted safe package/pathing clear via EndInterruptPackage(false) and InitiateDoNothingPackage(). reason={}.",
actorId,
playerId,
reason));
if (fleeOrAlarmPackageObserved) {
LogThrottledInfo(
"runtime_proxy_flee_alarm_suppressed_" + std::to_string(playerId) + "_" + std::to_string(actorId),
std::format(
"Runtime proxy actor {:08X} for remote player {} had flee/alarm/bump package movement suppressed. reason={}, packageType={}.",
actorId,
playerId,
reason,
packageTypeForLog));
}
if ((a_proxy.movementController || a_proxy.actorMover) &&
ShouldLog("runtime_proxy_no_safe_movement_controller_clear_" + std::to_string(playerId) + "_" + std::to_string(actorId), kNotInTestCellLogInterval)) {
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy actor {:08X} for remote player {} has movement controller/actor mover state, but no safe CommonLibF4 API is exposed here to clear a path destination directly. Using package/process suppression only.",
actorId,
playerId));
}
// TODO: Keep this default-disabled until the crash introduced by the first
// AI movement intent suppression patch is isolated. Re-enable pieces one at
// a time in-game, starting with read-only diagnostics, before writing
// currentProcess fields or interrupting packages again.
// TODO: Avoid direct writes to currentPackage.currentProcedureIndex,
// preferredSpeed, movementController internals, or actorMover internals until
// a safe Fallout 4/CommonLibF4 path-cancel API is validated.
// TODO: If walk-away animation persists, investigate a future animation-layer
// reset that preserves remote-driven idle/locomotion/sneak/jump/weapon sync.
}
void NeutralizeProxyActor(
ProxyActorSlot& a_slot,
RE::Actor& a_proxy,
const RE::PlayerCharacter& a_player,
ProxyNeutralizationReason a_reason)
{
if (std::addressof(a_proxy) == std::addressof(a_player)) {
return;
}
const auto reason = GetProxyNeutralizationReasonLabel(a_reason);
const auto actorId = a_proxy.GetFormID();
const auto playerId = a_slot.remotePlayerId;
auto combatObserved = false;
if (a_proxy.IsInCombat()) {
combatObserved = true;
a_proxy.StopCombat();
a_proxy.ClearAttackStates();
LogThrottledInfo(
"runtime_proxy_combat_stopped_" + std::to_string(playerId) + "_" + std::to_string(actorId),
std::format(
"Runtime proxy actor {:08X} for remote player {} entered combat and was neutralized. reason={}.",
actorId,
playerId,
reason));
}
if constexpr (kEnableProxyAIMovementIntentSuppression) {
SuppressProxyAIMovementIntent(a_slot, a_proxy, a_player, a_reason, combatObserved);
}
if (a_proxy.currentCombatTarget) {
const auto targetHandle = a_proxy.currentCombatTarget.get_handle();
a_proxy.currentCombatTarget.reset();
a_proxy.ClearAttackStates();
if constexpr (kEnableProxyAIMovementIntentSuppression) {
SuppressProxyAIMovementIntent(a_slot, a_proxy, a_player, a_reason, true);
}
LogThrottledInfo(
"runtime_proxy_combat_target_cleared_" + std::to_string(playerId) + "_" + std::to_string(actorId),
std::format(
"Runtime proxy actor {:08X} for remote player {} had combat target handle {:08X} cleared. reason={}.",
actorId,
playerId,
targetHandle,
reason));
}
auto hostileStateReset = false;
if (a_proxy.HasBeenAttacked()) {
a_proxy.SetBeenAttacked(false);
hostileStateReset = true;
}
if (a_proxy.boolFlags.any(RE::Actor::BOOL_FLAGS::kAngryWithPlayer, RE::Actor::BOOL_FLAGS::kAttackOnSight)) {
a_proxy.boolFlags.reset(RE::Actor::BOOL_FLAGS::kAngryWithPlayer, RE::Actor::BOOL_FLAGS::kAttackOnSight);
hostileStateReset = true;
}
if (a_proxy.boolFlags.none(RE::Actor::BOOL_FLAGS::kAttackingDisabled)) {
a_proxy.boolFlags.set(RE::Actor::BOOL_FLAGS::kAttackingDisabled);
hostileStateReset = true;
}
if (auto* actorValues = RE::ActorValue::GetSingleton()) {
auto resetActorValue = [&](RE::ActorValueInfo* a_valueInfo, float a_value) {
if (!a_valueInfo) {
return false;
}
if (a_proxy.GetActorValue(*a_valueInfo) == a_value) {
return false;
}
a_proxy.SetActorValue(*a_valueInfo, a_value);
return true;
};
hostileStateReset |= resetActorValue(actorValues->aggression, 0.0F);
hostileStateReset |= resetActorValue(actorValues->confidence, 0.0F);
hostileStateReset |= resetActorValue(actorValues->assistance, 0.0F);
}
if (hostileStateReset) {
LogThrottledInfo(
"runtime_proxy_hostility_reset_" + std::to_string(playerId) + "_" + std::to_string(actorId),
std::format(
"Runtime proxy actor {:08X} for remote player {} had hostility/aggression state reset. reason={}.",
actorId,
playerId,
reason));
}
if (auto* process = a_proxy.currentProcess) {
auto processSuppressed = false;
if (!process->ignoringCombat) {
process->ignoringCombat = true;
processSuppressed = true;
}
if (!process->endAlarmOnActor) {
process->endAlarmOnActor = true;
processSuppressed = true;
}
if (processSuppressed) {
LogThrottledInfo(
"runtime_proxy_ai_process_suppressed_" + std::to_string(playerId) + "_" + std::to_string(actorId),
std::format(
"Runtime proxy actor {:08X} for remote player {} had safe AI/process combat intervention suppressed. reason={}.",
actorId,
playerId,
reason));
}
}
// TODO: Investigate collision/stagger suppression separately. This pass only
// clears persistent combat/hostility state and avoids hide/delete/disable APIs.
}
const char* GetActiveProxySourceForLog(ActiveProxyKind a_kind)
{
switch (a_kind) {
@@ -2827,6 +3145,9 @@ namespace
g_runtimeProxySpawnAttemptPlayerId.reset();
}
if (auto* player = RE::PlayerCharacter::GetSingleton()) {
NeutralizeProxyActor(a_slot, a_actor, *player, ProxyNeutralizationReason::kSpawnPromoted);
}
LogInfoWithLocalPlayerPrefix(std::format(
"Stage 4 runtime proxy spawned for remote player {}: actor={:08X}, handle={:08X}, source={}.",
a_slot.remotePlayerId,
@@ -3731,11 +4052,19 @@ namespace
{
if (a_slot.lifecycleState == a_reason && a_slot.holdReason == a_reason) {
CorrectHeldSlotPositionIfNeeded(a_slot, a_proxy);
if (ShouldLog("runtime_proxy_holding_neutralization_check_" + std::to_string(a_slot.remotePlayerId), kProxyNeutralizationInterval)) {
if (auto* player = RE::PlayerCharacter::GetSingleton()) {
NeutralizeProxyActor(a_slot, a_proxy, *player, ProxyNeutralizationReason::kHolding);
}
}
LogProxyStillHeld(a_reason, a_slot.remotePlayerId);
return;
}
a_proxy.SetPosition(GetSlotHoldingPosition(a_slot), true);
if (auto* player = RE::PlayerCharacter::GetSingleton()) {
NeutralizeProxyActor(a_slot, a_proxy, *player, ProxyNeutralizationReason::kHolding);
}
a_slot.lifecycleState = a_reason;
a_slot.holdReason = a_reason;
ResetSlotMovementState(a_slot);
@@ -4010,6 +4339,7 @@ namespace
a_remotePlayer.playerId));
if (auto* proxy = TryResolveSlotProxy(slot, a_parentCell, a_player)) {
NeutralizeProxyActor(slot, *proxy, a_player, ProxyNeutralizationReason::kSlotReassigned);
RestoreSlotProxyForRemotePlayer(slot, *proxy, a_remotePlayer, a_player, a_sameCellPassed);
}
@@ -4075,6 +4405,10 @@ namespace
}
}
if (ShouldLog("runtime_proxy_active_neutralization_check_" + std::to_string(a_slot.remotePlayerId), kProxyNeutralizationInterval)) {
NeutralizeProxyActor(a_slot, *proxy, a_player, ProxyNeutralizationReason::kActiveMaintenance);
}
if (!a_slot.restoredForCurrentState ||
a_slot.lifecycleState != ProxyLifecycleState::kActive ||
ShouldSnapRemoteMovement(a_remotePlayer.movementType)) {