Stabilize proxy visibility and smooth movement
Fix proxy orbiting and improve motion/visibility handling. - Capture a per-slot visibleDebugAnchorPosition (reset on reassignment) to keep pre-placed proxies visually stable and avoid orbiting when the local player moves. - Remove the debug +20Z offset so pre-placed actors remain grounded. - Add GetRuntimeProxyVisibleTargetPosition to compute a stable visible target (anchored base + scaled remote delta). - Replace teleport-heavy updates with smooth movement: add InjectProxyMovementVelocity to publish velocity into bhkCharacterController, call Actor::Move() per-frame and inject velocity for animation blending. - Prefer reusing pre-placed pool proxies before attempting dynamic spawn. - Implement dynamic spawn via NEW_REFR_DATA / TESDataHandler::CreateReferenceAtLocation, apply debug visibility to spawned actors, and store handles in the dynamic pool. - Harden debug visibility setup: explicitly enable/disable 3D, alpha, display geometry, set 3D update flags and queue a TaskQueueInterface 3D update; expand debug logging. - Remove unsafe currentProcess target-clearing code and other minor cleanups. - Add dev-log entries describing Phase 6 debugging and fixes.
This commit is contained in:
@@ -4329,3 +4329,73 @@ The compilation errors were mostly API surface mismatches between the codebase a
|
||||
This is a good indication that the phased, iterative approach is working - we can disable unverified Phase 4 code and proceed with Phase 6 testing.
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-03 - Phase 6 Proxy Orbit Fix
|
||||
|
||||
### Observation
|
||||
|
||||
In-game testing showed the pre-placed proxy now appears with an idle animation, but it moves relative to the local player and can appear to orbit as the local player moves or turns.
|
||||
|
||||
### Cause
|
||||
|
||||
The debug visibility mapper recalculated the proxy's visible base position from the local player's current position/heading every update. A stable remote target could therefore map to different visible positions whenever the local player moved or rotated.
|
||||
|
||||
### Fix
|
||||
|
||||
- Added a per-slot `visibleDebugAnchorPosition` captured once when the remote player is assigned.
|
||||
- Reset that visible anchor when a slot is reassigned to a different remote player.
|
||||
- Removed the debug `+20Z` offset so the pre-placed actor stays grounded.
|
||||
- Enabled the per-frame smooth movement pass from `Update()` so proxies receive continuous `Move()` calls toward their current target.
|
||||
|
||||
### Build Status
|
||||
|
||||
✅ **SUCCESS** - Plugin rebuild succeeded and the DLL was installed to the F4SE folder for the next in-game test.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-03 - Phase 6 Proxy AI/Locomotion Iteration
|
||||
|
||||
### Observation
|
||||
|
||||
The pre-placed proxy now has collision and a stable independent position, but vanilla NPC AI still tries to walk away while the plugin holds the proxy at the remote client's position. Locomotion animations still do not visibly play when remote walk commands arrive.
|
||||
|
||||
### Cause
|
||||
|
||||
Active movement updates were still snapping the proxy to each network target with `SetPosition()`. That preserved sync, but collapsed most movement into teleports and left little continuous controller motion for the animation graph. The proxy's vanilla package was also still free to choose its own movement destination.
|
||||
|
||||
### Fix
|
||||
|
||||
- Disabled the temporary "preserve all AI for animation testing" flag.
|
||||
- Re-enabled AI movement intent suppression, but kept it to higher-level APIs only: `SetCommandType(kNone)`, `EndInterruptPackage(false)`, `InitiateDoNothingPackage()`, and `ClearAttackStates()`.
|
||||
- Removed the risky direct `currentProcess` handle/target clearing from the suppression path.
|
||||
- Changed active proxy packet updates to store a target instead of teleporting every update.
|
||||
- Kept snapping only for first activation, explicit snap movement, or large corrections.
|
||||
- Updated the per-frame mover to call `Actor::Move()` toward the target and inject the same velocity into `bhkCharacterController::SetLinearVelocityImpl()`.
|
||||
|
||||
### Build Status
|
||||
|
||||
✅ **SUCCESS** - Plugin rebuild succeeded and the DLL was installed to the F4SE folder for the next test.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-03 - Phase 6 AI Suppression Rollback
|
||||
|
||||
### Observation
|
||||
|
||||
The game crashed immediately after adding a fake client. The log stopped after assigning the pre-placed proxy from the pool and before the normal transform/animation diagnostics.
|
||||
|
||||
### Cause
|
||||
|
||||
The crash correlated with disabling `g_preserveProxyAIForAnimationTesting` and re-entering the active `NeutralizeProxyActor()` path on slot assignment. Even after removing direct `currentProcess` target writes, the safe-looking combat/package calls in that path are still not safe enough to run during proxy assignment.
|
||||
|
||||
### Fix
|
||||
|
||||
- Restored `g_preserveProxyAIForAnimationTesting = true`.
|
||||
- Disabled `kEnableProxyAIMovementIntentSuppression` again.
|
||||
- Kept the fixed debug anchor and non-teleport target movement changes for continued testing.
|
||||
|
||||
### Build Status
|
||||
|
||||
✅ **SUCCESS** - Rollback build succeeded and the DLL was installed to the F4SE folder.
|
||||
@@ -13,7 +13,11 @@
|
||||
#include "RE/C/COMMAND_TYPE.h"
|
||||
#include "RE/C/Console.h"
|
||||
#include "RE/H/hkVector4.h"
|
||||
#include "RE/N/NEW_REFR_DATA.h"
|
||||
#include "RE/P/PTYPE.h"
|
||||
#include "RE/R/RESET_3D_FLAGS.h"
|
||||
#include "RE/T/TESDataHandler.h"
|
||||
#include "RE/T/TaskQueueInterface.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
@@ -75,7 +79,7 @@ namespace
|
||||
constexpr bool kDebugForceRuntimeProxyNearLocalPlayer = true;
|
||||
constexpr bool kDebugHoldRuntimeProxyNearLocalPlayer = true;
|
||||
constexpr float kDebugRuntimeProxyHoldSeconds = 15.0F;
|
||||
constexpr auto kDebugRuntimeProxyOffsetZ = 20.0F;
|
||||
constexpr auto kDebugRuntimeProxyOffsetZ = 0.0F;
|
||||
constexpr auto kDebugRuntimeProxyScale = 1.50F;
|
||||
constexpr float kProxyPositionLerpAlpha = 0.15F;
|
||||
constexpr std::size_t kMaxRuntimeProxyActors = 4;
|
||||
@@ -240,6 +244,7 @@ namespace
|
||||
std::uint32_t spawnBaseFormId{ 0 };
|
||||
std::unordered_set<std::uint32_t> preexistingCandidateRefs;
|
||||
std::optional<RE::NiPoint3> initialRemoteSpawnPosition;
|
||||
std::optional<RE::NiPoint3> visibleDebugAnchorPosition;
|
||||
std::chrono::steady_clock::time_point candidateSearchStartTime{};
|
||||
std::chrono::steady_clock::time_point settleStartTime{};
|
||||
std::chrono::steady_clock::time_point heldSinceTime{};
|
||||
@@ -488,6 +493,32 @@ namespace
|
||||
return std::sqrt((deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ));
|
||||
}
|
||||
|
||||
void InjectProxyMovementVelocity(RE::bhkCharacterController& a_charController, const RE::NiPoint3& a_deltaPos, float a_deltaSeconds)
|
||||
{
|
||||
if (a_deltaSeconds <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto distance = a_deltaPos.Length();
|
||||
if (distance <= 0.001F) {
|
||||
const RE::hkVector4f stoppedVelocity{ 0.0F, 0.0F, 0.0F, 0.0F };
|
||||
a_charController.SetLinearVelocityImpl(stoppedVelocity);
|
||||
return;
|
||||
}
|
||||
|
||||
constexpr float kMaxInjectedSpeed = 400.0F;
|
||||
const auto unclampedSpeed = distance / a_deltaSeconds;
|
||||
const auto clampedSpeed = unclampedSpeed > kMaxInjectedSpeed ? kMaxInjectedSpeed : unclampedSpeed;
|
||||
const auto direction = a_deltaPos / distance;
|
||||
const RE::hkVector4f velocity{
|
||||
direction.x * clampedSpeed,
|
||||
direction.y * clampedSpeed,
|
||||
direction.z * clampedSpeed,
|
||||
0.0F
|
||||
};
|
||||
a_charController.SetLinearVelocityImpl(velocity);
|
||||
}
|
||||
|
||||
// Hybrid movement strategy for AI-driven animations
|
||||
// Phase 1: Let the proxy's natural AI/character controller drive movement
|
||||
// This allows Move() to be called, which updates velocity and plays animations
|
||||
@@ -567,18 +598,7 @@ namespace
|
||||
if (distanceDrift > 0.001F) {
|
||||
auto* charController = a_proxy.Move(0.016F, deltaPos, false);
|
||||
if (charController) {
|
||||
// Scale delta to units-per-second so the controller sees walking velocity
|
||||
const float speed = distanceDrift / 0.016F;
|
||||
constexpr float MAX_INJECT_SPEED = 400.0F;
|
||||
const float clampedSpeed = speed > MAX_INJECT_SPEED ? MAX_INJECT_SPEED : speed;
|
||||
const RE::NiPoint3 dir = distanceDrift > 0.0F ? (deltaPos / distanceDrift) : RE::NiPoint3{};
|
||||
const RE::hkVector4f vel{
|
||||
dir.x * clampedSpeed,
|
||||
dir.y * clampedSpeed,
|
||||
dir.z * clampedSpeed,
|
||||
0.0F
|
||||
};
|
||||
charController->SetLinearVelocityImpl(vel);
|
||||
InjectProxyMovementVelocity(*charController, deltaPos, 0.016F);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -712,30 +732,6 @@ namespace
|
||||
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);
|
||||
@@ -1657,15 +1653,47 @@ namespace
|
||||
}
|
||||
|
||||
const auto debugPosition = GetRuntimeProxyDebugPosition(a_player);
|
||||
const auto wasDisabled = a_proxy.IsDisabled();
|
||||
const auto had3D = a_proxy.Get3D() != nullptr || a_proxy.GetFullyLoaded3D() != nullptr || a_proxy.GetCurrent3D() != nullptr;
|
||||
const auto wasVisible = a_proxy.IsVisible();
|
||||
|
||||
if (wasDisabled) {
|
||||
a_proxy.Enable(false);
|
||||
}
|
||||
a_proxy.SetPosition(debugPosition, true);
|
||||
a_proxy.SetHeading(a_player.data.angle.z);
|
||||
a_proxy.SetScale(kDebugRuntimeProxyScale);
|
||||
a_proxy.SetAlpha(1.0F);
|
||||
a_proxy.UpdateAlpha();
|
||||
a_proxy.SetDisplayGeometry(true);
|
||||
a_proxy.Update3DPosition(true);
|
||||
a_proxy.MoveHavok(true);
|
||||
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kModel);
|
||||
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kSkin);
|
||||
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kSkeleton);
|
||||
a_proxy.Set3DUpdateFlag(RE::RESET_3D_FLAGS::kInitDefault);
|
||||
if (auto* taskQueue = RE::TaskQueueInterface::GetSingleton()) {
|
||||
const auto updateFlags =
|
||||
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kModel) |
|
||||
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kSkin) |
|
||||
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kSkeleton) |
|
||||
static_cast<std::uint32_t>(RE::RESET_3D_FLAGS::kInitDefault);
|
||||
taskQueue->QueueUpdate3D(std::addressof(a_proxy), updateFlags);
|
||||
}
|
||||
|
||||
const auto has3D = a_proxy.Get3D() != nullptr || a_proxy.GetFullyLoaded3D() != nullptr || a_proxy.GetCurrent3D() != nullptr;
|
||||
const auto isVisible = a_proxy.IsVisible();
|
||||
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy debug visibility mode active: forcing runtime proxy near local player. debugPosition={}, scale={:.2f}.",
|
||||
"Runtime proxy debug visibility mode active: forcing runtime proxy near local player. debugPosition={}, scale={:.2f}, wasDisabled={}, had3D={}, wasVisible={}, nowDisabled={}, has3D={}, isVisible={}.",
|
||||
FormatPosition(debugPosition),
|
||||
kDebugRuntimeProxyScale));
|
||||
kDebugRuntimeProxyScale,
|
||||
wasDisabled,
|
||||
had3D,
|
||||
wasVisible,
|
||||
a_proxy.IsDisabled(),
|
||||
has3D,
|
||||
isVisible));
|
||||
}
|
||||
|
||||
struct RuntimeProxyVisibilityHoldTiming
|
||||
@@ -2203,32 +2231,19 @@ namespace
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Phase 4: Dynamic spawning implementation
|
||||
// NOTE: Disabled for Phase 6 testing - API verification needed
|
||||
// TODO: Phase 7 - Fix CommonLibF4 API calls for dynamic spawning
|
||||
//
|
||||
// Original code disabled - API calls need verification
|
||||
/*
|
||||
RE::TESNPC* proxyBase = nullptr;
|
||||
|
||||
// First try to find custom proxy base in ESP
|
||||
const auto* dataHandler = RE::TESDataHandler::GetSingleton();
|
||||
auto* dataHandler = RE::TESDataHandler::GetSingleton();
|
||||
if (!dataHandler) {
|
||||
LogWarningWithLocalPlayerPrefix(
|
||||
"SpawnDynamicProxyActor: Cannot spawn proxy; data handler unavailable.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Try to load custom proxy base (F4T_RemotePlayerProxy)
|
||||
const auto customFile = dataHandler->LookupLoadedModByName(kTestPluginName);
|
||||
if (customFile && customFile->compileIndex != 0xFF) {
|
||||
const auto customBaseFormId = (customFile->compileIndex << 24) | kProxyBaseActorLocalFormId;
|
||||
proxyBase = RE::TESForm::LookupByID<RE::TESNPC>(customBaseFormId);
|
||||
}
|
||||
|
||||
// Fallback to Codsworth if custom proxy not available
|
||||
auto* proxyBase = ResolveProxyActorBase();
|
||||
if (!proxyBase) {
|
||||
proxyBase = RE::TESForm::LookupByID<RE::TESNPC>(kDebugPlaceAtMeActorBaseFormId);
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: Custom proxy base unavailable for remote player {}; trying Codsworth diagnostic base.",
|
||||
a_remotePlayerId));
|
||||
proxyBase = ResolveCodsworthBaseByAbsoluteFormId();
|
||||
}
|
||||
|
||||
if (!proxyBase) {
|
||||
@@ -2237,8 +2252,76 @@ namespace
|
||||
a_remotePlayerId));
|
||||
return nullptr;
|
||||
}
|
||||
*/
|
||||
return nullptr; // Phase 6: Disabled - API verification needed for Phase 7
|
||||
|
||||
auto* boundObject = proxyBase->As<RE::TESBoundObject>();
|
||||
if (!boundObject) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: Cannot spawn proxy for remote player {}; actor base {:08X} is not a TESBoundObject.",
|
||||
a_remotePlayerId,
|
||||
proxyBase->GetFormID()));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* parentCell = a_player.GetParentCell();
|
||||
if (!parentCell) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: Cannot spawn proxy for remote player {}; player parent cell unavailable.",
|
||||
a_remotePlayerId));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto spawnPosition = GetRuntimeProxyDebugPosition(a_player);
|
||||
RE::NiPoint3 spawnDirection{};
|
||||
spawnDirection.z = a_player.data.angle.z;
|
||||
|
||||
RE::NEW_REFR_DATA spawnData;
|
||||
spawnData.object = boundObject;
|
||||
spawnData.location = spawnPosition;
|
||||
spawnData.direction = spawnDirection;
|
||||
spawnData.interior = parentCell;
|
||||
spawnData.world = nullptr;
|
||||
spawnData.reference = nullptr;
|
||||
spawnData.forcePersist = false;
|
||||
spawnData.initializeScripts = true;
|
||||
spawnData.initiallyDisabled = false;
|
||||
|
||||
const auto spawnedHandle = dataHandler->CreateReferenceAtLocation(spawnData);
|
||||
if (!spawnedHandle) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: CreateReferenceAtLocation failed for remote player {} with base {:08X}.",
|
||||
a_remotePlayerId,
|
||||
proxyBase->GetFormID()));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto spawnedRef = spawnedHandle.get();
|
||||
auto* spawnedActor = spawnedRef ? spawnedRef->As<RE::Actor>() : nullptr;
|
||||
if (!spawnedActor) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: Created reference for remote player {} did not resolve as actor.",
|
||||
a_remotePlayerId));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (std::addressof(*spawnedActor) == std::addressof(a_player)) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: Created reference {:08X} unexpectedly resolved as the local player.",
|
||||
spawnedActor->GetFormID()));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ApplyRuntimeProxyDebugVisibility(*spawnedActor, a_player);
|
||||
|
||||
g_dynamicProxyPool[a_remotePlayerId] = spawnedHandle;
|
||||
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: Spawned dynamic proxy actor {:08X} for remote player {} with base {:08X} at {}.",
|
||||
spawnedActor->GetFormID(),
|
||||
a_remotePlayerId,
|
||||
proxyBase->GetFormID(),
|
||||
FormatPosition(spawnPosition)));
|
||||
|
||||
return spawnedActor;
|
||||
}
|
||||
|
||||
// Phase 4.1: Retrieve or spawn proxy for a remote player
|
||||
@@ -3279,6 +3362,41 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
};
|
||||
}
|
||||
|
||||
RE::NiPoint3 GetRuntimeProxyVisibleTargetPosition(
|
||||
ProxyActorSlot& a_slot,
|
||||
const RemotePlayer& a_remotePlayer,
|
||||
const RE::PlayerCharacter& a_player)
|
||||
{
|
||||
if constexpr (!kDebugForceRuntimeProxyNearLocalPlayer) {
|
||||
return GetRemotePlayerPosition(a_remotePlayer);
|
||||
}
|
||||
|
||||
if (!a_slot.visibleDebugAnchorPosition) {
|
||||
a_slot.visibleDebugAnchorPosition = GetRuntimeProxyDebugPosition(a_player);
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy visible debug anchor captured for remote player {}: anchor={}.",
|
||||
a_remotePlayer.playerId,
|
||||
FormatPosition(*a_slot.visibleDebugAnchorPosition)));
|
||||
}
|
||||
const auto debugBasePosition = *a_slot.visibleDebugAnchorPosition;
|
||||
if (!a_slot.initialRemoteSpawnPosition) {
|
||||
a_slot.initialRemoteSpawnPosition = GetRemotePlayerPosition(a_remotePlayer);
|
||||
a_slot.initialRemoteSpawnHeading = a_remotePlayer.angleZ;
|
||||
return debugBasePosition;
|
||||
}
|
||||
|
||||
auto remoteDelta = GetRemotePlayerPosition(a_remotePlayer) - *a_slot.initialRemoteSpawnPosition;
|
||||
const auto remoteDeltaLength = remoteDelta.Length();
|
||||
if (remoteDeltaLength > 0.001F) {
|
||||
constexpr auto kVisibleRemoteDeltaScale = 0.35F;
|
||||
constexpr auto kVisibleRemoteDeltaMax = 250.0F;
|
||||
const auto scaledLength = std::min(remoteDeltaLength * kVisibleRemoteDeltaScale, kVisibleRemoteDeltaMax);
|
||||
remoteDelta = (remoteDelta / remoteDeltaLength) * scaledLength;
|
||||
}
|
||||
|
||||
return debugBasePosition + remoteDelta;
|
||||
}
|
||||
|
||||
void SetSlotInitialRemoteSpawnTransform(ProxyActorSlot& a_slot, const RemotePlayer& a_remotePlayer)
|
||||
{
|
||||
a_slot.initialRemoteSpawnPosition = GetRemotePlayerPosition(a_remotePlayer);
|
||||
@@ -4387,7 +4505,15 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
const RE::PlayerCharacter& a_player)
|
||||
{
|
||||
if (!a_slot.proxyHandle) {
|
||||
// Phase 4: Try dynamic spawn first (new model)
|
||||
// Phase 6 validation: prefer pre-placed actors because Creation Kit refs
|
||||
// already have loaded 3D, collision, AI process, and animation state.
|
||||
if (auto* pooledProxy = TryGetProxyFromPool()) {
|
||||
a_slot.proxyHandle = pooledProxy->GetHandle();
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Assigned proxy from pre-placed pool {:08X} to remote player {}",
|
||||
pooledProxy->GetFormID(),
|
||||
a_slot.remotePlayerId));
|
||||
} else
|
||||
if (g_dynamicProxyPoolEnabled) {
|
||||
if (auto* dynamicProxy = GetOrSpawnDynamicProxy(const_cast<RE::PlayerCharacter&>(a_player), a_slot.remotePlayerId)) {
|
||||
a_slot.proxyHandle = dynamicProxy->GetHandle();
|
||||
@@ -4397,15 +4523,6 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
a_slot.remotePlayerId));
|
||||
return dynamicProxy;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Try to grab a proxy from the pre-placed pool
|
||||
if (auto* pooledProxy = TryGetProxyFromPool()) {
|
||||
a_slot.proxyHandle = pooledProxy->GetHandle();
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Assigned proxy from pre-placed pool {:08X} to remote player {}",
|
||||
pooledProxy->GetFormID(),
|
||||
a_slot.remotePlayerId));
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -4670,10 +4787,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
const RE::PlayerCharacter& a_player,
|
||||
bool a_sameCellPassed)
|
||||
{
|
||||
RE::NiPoint3 targetPosition{};
|
||||
targetPosition.x = a_remotePlayer.x;
|
||||
targetPosition.y = a_remotePlayer.y;
|
||||
targetPosition.z = a_remotePlayer.z;
|
||||
const auto targetPosition = GetRuntimeProxyVisibleTargetPosition(a_slot, a_remotePlayer, a_player);
|
||||
|
||||
ApplyRuntimeProxyTransform(a_proxy, targetPosition, a_remotePlayer.angleZ, a_remotePlayer, true, &a_slot);
|
||||
a_slot.lifecycleState = ProxyLifecycleState::kActive;
|
||||
@@ -4685,9 +4799,12 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
a_slot.remoteStateMovedLogged = false;
|
||||
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy actor {:08X} restored/reused for remote player {} at X={:.2f}, Y={:.2f}, Z={:.2f}.",
|
||||
"Runtime proxy actor {:08X} restored/reused for remote player {} at visible X={:.2f}, Y={:.2f}, Z={:.2f} from remote X={:.2f}, Y={:.2f}, Z={:.2f}.",
|
||||
a_proxy.GetFormID(),
|
||||
a_remotePlayer.playerId,
|
||||
targetPosition.x,
|
||||
targetPosition.y,
|
||||
targetPosition.z,
|
||||
a_remotePlayer.x,
|
||||
a_remotePlayer.y,
|
||||
a_remotePlayer.z));
|
||||
@@ -4831,10 +4948,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
a_proxy.GetFormID()));
|
||||
}
|
||||
|
||||
RE::NiPoint3 targetPosition;
|
||||
targetPosition.x = a_remotePlayer.x;
|
||||
targetPosition.y = a_remotePlayer.y;
|
||||
targetPosition.z = a_remotePlayer.z;
|
||||
const auto targetPosition = GetRuntimeProxyVisibleTargetPosition(a_slot, a_remotePlayer, a_player);
|
||||
|
||||
// Store latest target for smooth frame movement
|
||||
a_slot.targetPosition = targetPosition;
|
||||
@@ -4953,6 +5067,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
}
|
||||
slotValue.previousRemotePlayerId = previousRemotePlayerId;
|
||||
slotValue.remotePlayerId = a_remotePlayer.playerId;
|
||||
slotValue.visibleDebugAnchorPosition.reset();
|
||||
slotValue.lifecycleState = ProxyLifecycleState::kHeldNoRemote;
|
||||
slotValue.holdReason.reset();
|
||||
slotValue.heldSinceTime = {};
|
||||
@@ -5194,17 +5309,23 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
continue;
|
||||
}
|
||||
|
||||
proxy->SetHeading(slot.targetHeading);
|
||||
|
||||
// Use Move() to smoothly approach target via character controller
|
||||
// This lets the AI/character controller drive animations naturally
|
||||
constexpr float MAX_FRAME_DELTA = 100.0F; // Max movement per frame
|
||||
constexpr float kFrameDeltaSeconds = 0.016F;
|
||||
constexpr float MAX_FRAME_DELTA = 12.0F; // Max movement per frame
|
||||
RE::NiPoint3 frameDelta = deltaToTarget;
|
||||
if (distanceToTarget > MAX_FRAME_DELTA) {
|
||||
frameDelta = (deltaToTarget / distanceToTarget) * MAX_FRAME_DELTA;
|
||||
}
|
||||
|
||||
// Call Move() with deferred=true to integrate movement naturally
|
||||
// Call Move() through the character controller and publish a velocity
|
||||
// sample so the graph can blend into locomotion.
|
||||
// This should update velocity in the character controller for animations
|
||||
proxy->Move(0.016F, frameDelta, true);
|
||||
if (auto* charController = proxy->Move(kFrameDeltaSeconds, frameDelta, false)) {
|
||||
InjectProxyMovementVelocity(*charController, frameDelta, kFrameDeltaSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5325,6 +5446,8 @@ namespace F4T::ProxyActorController
|
||||
*player,
|
||||
fallbackCandidateSameCellPassed);
|
||||
}
|
||||
|
||||
ApplySmoothFrameMovement();
|
||||
}
|
||||
|
||||
void UpdateSafetyTest()
|
||||
|
||||
Reference in New Issue
Block a user