Add proxy positioning API and CK-placed pool
Add a ProxyGameAPI (F4TProxyGameAPI.h/.cpp) exposing PositionRemoteActor which centralizes positioning logic, play-space resolution, MoveRefrToPosition-style moves, and 20Hz throttling bypass for forced moves. Replace ad-hoc SetPosition/SetHeading calls across the proxy controller to use the new API, ensuring correct cell/worldspace migration, logging, and move-rate throttling. Introduce support for Creation Kit placed proxy refs: detect CK-placed proxies by editor ID prefix or base form, populate a per-cell proxy pool, and prefer assigning CK-placed proxies for runtime remotes (with fallbacks). Update proxy spawn/holding logic (skip smooth frame once, handle settle states, preserve/reuse CK pool entries) and improve worldspace/cell mismatch checks to avoid incorrect movement across play spaces. Other changes: add CommonwealthOnline.esp resource file, update setup.md with the new local FormID (0x002666) for COPlayerProxy, enable animation sync gating to require loaded 3D, and small logging/message improvements.
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RE/N/NiPoint3.h"
|
||||||
|
|
||||||
|
namespace RE
|
||||||
|
{
|
||||||
|
class Actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace F4T::ProxyGameAPI
|
||||||
|
{
|
||||||
|
// Move a remote proxy into the local player's loaded play space at world coordinates.
|
||||||
|
// Uses the engine MoveRefrToPosition native (same path as Papyrus MoveTo) so parent
|
||||||
|
// cell/worldspace follow the local player across interior/exterior transitions.
|
||||||
|
// When true, bypass the 20 Hz MoveRefrToPosition throttle (snap/restore/holding moves).
|
||||||
|
void PositionRemoteActor(
|
||||||
|
RE::Actor& a_proxy,
|
||||||
|
const RE::NiPoint3& a_pos,
|
||||||
|
float a_headingZ,
|
||||||
|
bool a_force = false);
|
||||||
|
|
||||||
|
void PositionRemoteActor(
|
||||||
|
RE::Actor& a_proxy,
|
||||||
|
const RE::NiPoint3& a_pos,
|
||||||
|
const RE::NiPoint3& a_rot,
|
||||||
|
bool a_force = false);
|
||||||
|
}
|
||||||
+1
-1
@@ -115,7 +115,7 @@ Use this checklist for the first plugin test:
|
|||||||
* [ ] Plugin log shows `[LocalPlayerId=N] Assigned server playerId: N`
|
* [ ] Plugin log shows `[LocalPlayerId=N] Assigned server playerId: N`
|
||||||
* [ ] Plugin logs prefixed remote player updates from another connected client
|
* [ ] Plugin logs prefixed remote player updates from another connected client
|
||||||
* [ ] Plugin logs prefixed remote player removal after a disconnect
|
* [ ] Plugin logs prefixed remote player removal after a disconnect
|
||||||
* [ ] `CommonwealthOnline.esp` is enabled (provides `COPlayerProxy` actor base, local FormID `001733`)
|
* [ ] `CommonwealthOnline.esp` is enabled (provides `COPlayerProxy` actor base, local FormID `002666`)
|
||||||
* [ ] With two Fallout 4 instances in the same cell, each instance spawns a dynamic
|
* [ ] With two Fallout 4 instances in the same cell, each instance spawns a dynamic
|
||||||
proxy at the other player's networked position
|
proxy at the other player's networked position
|
||||||
* [ ] Optional regression: `coc F4TTestCell01` with `kEnablePrePlacedProxyPool` enabled
|
* [ ] Optional regression: `coc F4TTestCell01` with `kEnablePrePlacedProxyPool` enabled
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "F4TRemotePlayerState.h"
|
#include "F4TRemotePlayerState.h"
|
||||||
#include "F4RemoteActionComponent.h"
|
#include "F4RemoteActionComponent.h"
|
||||||
#include "F4TInterpolationSystem.h"
|
#include "F4TInterpolationSystem.h"
|
||||||
|
#include "F4TProxyGameAPI.h"
|
||||||
|
|
||||||
#include "RE/A/ACTOR_STANCE.h"
|
#include "RE/A/ACTOR_STANCE.h"
|
||||||
#include "RE/A/ActorValue.h"
|
#include "RE/A/ActorValue.h"
|
||||||
@@ -43,14 +44,13 @@ namespace
|
|||||||
constexpr auto kProxyBaseActorEditorId = "COPlayerProxy";
|
constexpr auto kProxyBaseActorEditorId = "COPlayerProxy";
|
||||||
constexpr auto kTestPluginName = "CommonwealthOnline.esp";
|
constexpr auto kTestPluginName = "CommonwealthOnline.esp";
|
||||||
constexpr auto kBaseGamePluginName = "Fallout4.esm";
|
constexpr auto kBaseGamePluginName = "Fallout4.esm";
|
||||||
constexpr RE::TESFormID kProxyBaseActorLocalFormId = 0x001733;
|
constexpr RE::TESFormID kProxyBaseActorLocalFormId = 0x002666;
|
||||||
constexpr RE::TESFormID kProxyFallbackLocalFormId = 0x0020A2;
|
constexpr RE::TESFormID kProxyFallbackLocalFormId = 0x0020A2;
|
||||||
constexpr auto kProxyOffsetX = 150.0F;
|
constexpr auto kProxyOffsetX = 150.0F;
|
||||||
|
|
||||||
// Runtime mode (default: global dynamic spawn)
|
// Runtime mode (default: global dynamic PlaceAtMe spawn + MoveRefrToPosition puppeting)
|
||||||
constexpr bool kEnableGlobalProxyController = true;
|
constexpr bool kEnableGlobalProxyController = true;
|
||||||
// Disabled while PlaceAtMe-backed spawn is primary; direct CreateReferenceAtLocation
|
// Dynamic spawn at the local player, positioned via MoveRefrToPosition (DoxyCoSync path).
|
||||||
// refs persist as invisible actors (Get3D=false) and block PlaceAtMe fallback.
|
|
||||||
constexpr bool kEnableDynamicProxySpawn = false;
|
constexpr bool kEnableDynamicProxySpawn = false;
|
||||||
constexpr bool kEnablePrePlacedProxyPool = false;
|
constexpr bool kEnablePrePlacedProxyPool = false;
|
||||||
constexpr bool kEnablePlacedFallbackProxy = false;
|
constexpr bool kEnablePlacedFallbackProxy = false;
|
||||||
@@ -67,9 +67,9 @@ namespace
|
|||||||
constexpr bool kDebugUsePlaceAtMeCandidateIsolation = kEnableStage3SpawnDiagnostics;
|
constexpr bool kDebugUsePlaceAtMeCandidateIsolation = kEnableStage3SpawnDiagnostics;
|
||||||
constexpr float kDebugPlaceAtMePostSpawnSettleSeconds = 2.0F;
|
constexpr float kDebugPlaceAtMePostSpawnSettleSeconds = 2.0F;
|
||||||
constexpr float kDebugPlaceAtMeCandidateSearchSeconds = 5.0F;
|
constexpr float kDebugPlaceAtMeCandidateSearchSeconds = 5.0F;
|
||||||
// CreateReferenceAtLocation spawns often fail to load visible 3D in exterior cells.
|
|
||||||
// PlaceAtMe (player.placeatme) is the proven runtime spawn path for visible proxies.
|
|
||||||
constexpr bool kUsePlaceAtMeBackedRuntimeProxySpawn = true;
|
constexpr bool kUsePlaceAtMeBackedRuntimeProxySpawn = true;
|
||||||
|
// Legacy test-cell ref editor ID; also accepts COProxyRemotePlayer* refs in any cell.
|
||||||
|
constexpr auto kCkPlacedProxyEditorIdPrefix = "COProxyRemotePlayer";
|
||||||
|
|
||||||
// Pre-placed proxies discovered in the current cell (regression only when kEnablePrePlacedProxyPool)
|
// Pre-placed proxies discovered in the current cell (regression only when kEnablePrePlacedProxyPool)
|
||||||
static std::vector<RE::ObjectRefHandle> g_availableProxyPool;
|
static std::vector<RE::ObjectRefHandle> g_availableProxyPool;
|
||||||
@@ -94,8 +94,9 @@ namespace
|
|||||||
std::uint32_t a_oldCellId,
|
std::uint32_t a_oldCellId,
|
||||||
RE::TESObjectCELL& a_currentCell,
|
RE::TESObjectCELL& a_currentCell,
|
||||||
const RE::PlayerCharacter& a_player);
|
const RE::PlayerCharacter& a_player);
|
||||||
|
bool IsProxyPoolActorAssigned(const RE::Actor& a_actor);
|
||||||
void ApplySlotInitialRemoteSpawnTransform(ProxyActorSlot& a_slot, RE::Actor& a_actor);
|
void ApplySlotInitialRemoteSpawnTransform(ProxyActorSlot& a_slot, RE::Actor& a_actor);
|
||||||
// COPlayerProxy in CommonwealthOnline.esp (local FormID 001733, runtime 01001733 at LO 01).
|
// COPlayerProxy in CommonwealthOnline.esp (local FormID 002666, runtime 01002666 at LO 01).
|
||||||
constexpr bool kDebugPlaceAtMeSpawnUseCustomProxyBase = true;
|
constexpr bool kDebugPlaceAtMeSpawnUseCustomProxyBase = true;
|
||||||
constexpr bool kDebugPlaceAtMeSpawnFallbackToCodsworth = true;
|
constexpr bool kDebugPlaceAtMeSpawnFallbackToCodsworth = true;
|
||||||
constexpr float kPlaceAtMeProxySettleSeconds = 2.0F;
|
constexpr float kPlaceAtMeProxySettleSeconds = 2.0F;
|
||||||
@@ -299,6 +300,7 @@ namespace
|
|||||||
bool directRemoteSpawnLogged{ false };
|
bool directRemoteSpawnLogged{ false };
|
||||||
bool visibilityRefreshPending{ true };
|
bool visibilityRefreshPending{ true };
|
||||||
bool visibilityRefreshLogged{ false };
|
bool visibilityRefreshLogged{ false };
|
||||||
|
bool skipSmoothFrameOnce{ false };
|
||||||
std::optional<std::uint32_t> cachedProxyFormId;
|
std::optional<std::uint32_t> cachedProxyFormId;
|
||||||
RE::NiPointer<RE::Actor> cachedProxyActor;
|
RE::NiPointer<RE::Actor> cachedProxyActor;
|
||||||
bool hasLoggedInitialAnimationState{ false };
|
bool hasLoggedInitialAnimationState{ false };
|
||||||
@@ -674,11 +676,11 @@ namespace
|
|||||||
a_optSlot->interpolationComponent.Clear();
|
a_optSlot->interpolationComponent.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float headingToApply = a_heading;
|
||||||
// Slot-backed proxies get per-frame rotation smoothing in ApplySmoothFrameMovement.
|
// Slot-backed proxies get per-frame rotation smoothing in ApplySmoothFrameMovement.
|
||||||
if (!a_optSlot) {
|
if (!a_optSlot) {
|
||||||
const bool shouldSnapHeading =
|
const bool shouldSnapHeading =
|
||||||
ShouldSnapRemoteMovement(a_remotePlayer.movementType) || distanceDrift >= kLargeDriftSnapDistance;
|
ShouldSnapRemoteMovement(a_remotePlayer.movementType) || distanceDrift >= kLargeDriftSnapDistance;
|
||||||
float headingToApply = a_heading;
|
|
||||||
if (!shouldSnapHeading) {
|
if (!shouldSnapHeading) {
|
||||||
const float rotationAlpha = a_shouldSnap ?
|
const float rotationAlpha = a_shouldSnap ?
|
||||||
kProxyRotationLerpAlpha :
|
kProxyRotationLerpAlpha :
|
||||||
@@ -691,11 +693,15 @@ namespace
|
|||||||
a_heading,
|
a_heading,
|
||||||
rotationAlpha);
|
rotationAlpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
a_proxy.SetHeading(headingToApply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a_proxy.SetPosition(positionToApply, true);
|
const bool shouldForcePosition =
|
||||||
|
a_shouldSnap || distanceDrift >= kLargeDriftSnapDistance;
|
||||||
|
F4T::ProxyGameAPI::PositionRemoteActor(
|
||||||
|
a_proxy,
|
||||||
|
positionToApply,
|
||||||
|
headingToApply,
|
||||||
|
shouldForcePosition);
|
||||||
|
|
||||||
// Character controller velocity is injected once per frame in ApplySmoothFrameMovement
|
// Character controller velocity is injected once per frame in ApplySmoothFrameMovement
|
||||||
// (derived from the actual applied displacement). Injecting it here too, at packet rate,
|
// (derived from the actual applied displacement). Injecting it here too, at packet rate,
|
||||||
@@ -1189,7 +1195,11 @@ namespace
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
a_proxy.SetPosition(kProxyHiddenHoldingPosition, true);
|
F4T::ProxyGameAPI::PositionRemoteActor(
|
||||||
|
a_proxy,
|
||||||
|
kProxyHiddenHoldingPosition,
|
||||||
|
a_proxy.GetHeading(),
|
||||||
|
true);
|
||||||
g_proxyLifecycleState = a_reason;
|
g_proxyLifecycleState = a_reason;
|
||||||
g_proxyHoldReason = a_reason;
|
g_proxyHoldReason = a_reason;
|
||||||
g_proxyHoldPlayerId = a_playerId;
|
g_proxyHoldPlayerId = a_playerId;
|
||||||
@@ -1256,6 +1266,45 @@ namespace
|
|||||||
return editorId && a_editorId == editorId;
|
return editorId && a_editorId == editorId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool HasEditorIdPrefix(const RE::TESForm& a_form, std::string_view a_prefix)
|
||||||
|
{
|
||||||
|
if (a_prefix.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* editorId = a_form.GetFormEditorID();
|
||||||
|
if (!editorId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto id = std::string_view(editorId);
|
||||||
|
return id.size() >= a_prefix.size() && id.substr(0, a_prefix.size()) == a_prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsCkPlacedProxyReference(
|
||||||
|
const RE::TESObjectREFR& a_ref,
|
||||||
|
const RE::Actor& a_actor,
|
||||||
|
std::optional<RE::TESFormID> a_proxyBaseFormId)
|
||||||
|
{
|
||||||
|
if (HasEditorId(a_ref, kProxyEditorId) || HasEditorIdPrefix(a_ref, kCkPlacedProxyEditorIdPrefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto refLocalFormId = a_ref.GetFormID() & 0x00FFFFFF;
|
||||||
|
if (refLocalFormId == kProxyFallbackLocalFormId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a_proxyBaseFormId) {
|
||||||
|
const auto* baseActor = a_actor.GetNPC();
|
||||||
|
if (baseActor && baseActor->GetFormID() == *a_proxyBaseFormId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool IsTestPluginLoaded()
|
bool IsTestPluginLoaded()
|
||||||
{
|
{
|
||||||
auto* dataHandler = RE::TESDataHandler::GetSingleton();
|
auto* dataHandler = RE::TESDataHandler::GetSingleton();
|
||||||
@@ -1638,25 +1687,10 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto localCellId = a_parentCell.GetFormID();
|
const auto localCellId = a_parentCell.GetFormID();
|
||||||
if (localCellId != *remoteCellId) {
|
if (localCellId == *remoteCellId) {
|
||||||
LogThrottledInfo(
|
|
||||||
"remote_cell_mismatch_" + std::to_string(a_remotePlayer.playerId),
|
|
||||||
std::format(
|
|
||||||
"Remote player {} is in a different cell: local={:08X}, remote={}. Proxy movement is skipped.",
|
|
||||||
a_remotePlayer.playerId,
|
|
||||||
localCellId,
|
|
||||||
a_remotePlayer.cellId));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto remoteWorldspaceId = ParseHexFormId(a_remotePlayer.worldspaceId);
|
const auto remoteWorldspaceId = ParseHexFormId(a_remotePlayer.worldspaceId);
|
||||||
const auto localWorldspaceId = GetLocalWorldspaceId(a_player, a_parentCell);
|
const auto localWorldspaceId = GetLocalWorldspaceId(a_player, a_parentCell);
|
||||||
if (!localWorldspaceId && !remoteWorldspaceId) {
|
if (!localWorldspaceId && !remoteWorldspaceId) {
|
||||||
LogThrottledInfo(
|
|
||||||
"remote_same_cell_" + std::to_string(a_remotePlayer.playerId),
|
|
||||||
std::format(
|
|
||||||
"Remote player {} is in the same cell/worldspace; moving proxy from remote state.",
|
|
||||||
a_remotePlayer.playerId));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1674,6 +1708,32 @@ namespace
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exterior Commonwealth: same worldspace is enough; adjacent cells load together.
|
||||||
|
if (a_parentCell.IsExterior()) {
|
||||||
|
const auto remoteWorldspaceId = ParseHexFormId(a_remotePlayer.worldspaceId);
|
||||||
|
const auto localWorldspaceId = GetLocalWorldspaceId(a_player, a_parentCell);
|
||||||
|
if (localWorldspaceId && remoteWorldspaceId && *localWorldspaceId == *remoteWorldspaceId) {
|
||||||
|
LogThrottledInfo(
|
||||||
|
"remote_same_worldspace_" + std::to_string(a_remotePlayer.playerId),
|
||||||
|
std::format(
|
||||||
|
"Remote player {} is in a different exterior cell but same worldspace; proxy movement continues. localCell={:08X}, remoteCell={}.",
|
||||||
|
a_remotePlayer.playerId,
|
||||||
|
localCellId,
|
||||||
|
a_remotePlayer.cellId));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LogThrottledInfo(
|
||||||
|
"remote_cell_mismatch_" + std::to_string(a_remotePlayer.playerId),
|
||||||
|
std::format(
|
||||||
|
"Remote player {} is in a different location: local={:08X}, remote={}. Proxy movement is skipped.",
|
||||||
|
a_remotePlayer.playerId,
|
||||||
|
localCellId,
|
||||||
|
a_remotePlayer.cellId));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void FillSpawnDataForCell(
|
void FillSpawnDataForCell(
|
||||||
RE::NEW_REFR_DATA& a_spawnData,
|
RE::NEW_REFR_DATA& a_spawnData,
|
||||||
RE::TESObjectCELL& a_parentCell,
|
RE::TESObjectCELL& a_parentCell,
|
||||||
@@ -2403,9 +2463,15 @@ namespace
|
|||||||
|
|
||||||
g_availableProxyPool.clear();
|
g_availableProxyPool.clear();
|
||||||
|
|
||||||
// Search current cell for the known pre-placed proxy reference.
|
std::optional<RE::TESFormID> proxyBaseFormId;
|
||||||
|
if (auto* proxyBase = ResolveProxyActorBaseByPluginLocalFormId()) {
|
||||||
|
proxyBaseFormId = proxyBase->GetFormID();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search current cell for CK-placed proxy references (refs placed in the editor, not
|
||||||
|
// spawned at runtime). These actors load with full skeleton, head, and behavior graph.
|
||||||
a_cell.ForEachReference([&](RE::TESObjectREFR* a_ref) {
|
a_cell.ForEachReference([&](RE::TESObjectREFR* a_ref) {
|
||||||
if (!a_ref) {
|
if (!a_ref || IsRuntimeProxyRef(*a_ref)) {
|
||||||
return RE::BSContainer::ForEachResult::kContinue;
|
return RE::BSContainer::ForEachResult::kContinue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2414,21 +2480,20 @@ namespace
|
|||||||
return RE::BSContainer::ForEachResult::kContinue;
|
return RE::BSContainer::ForEachResult::kContinue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto refFormId = actor->GetFormID();
|
if (!IsCkPlacedProxyReference(*a_ref, *actor, proxyBaseFormId)) {
|
||||||
const auto refLocalFormId = refFormId & 0x00FFFFFF;
|
return RE::BSContainer::ForEachResult::kContinue;
|
||||||
const auto isKnownProxyRef =
|
}
|
||||||
HasEditorId(*a_ref, kProxyEditorId) ||
|
|
||||||
refLocalFormId == kProxyFallbackLocalFormId;
|
if (IsProxyPoolActorAssigned(*actor)) {
|
||||||
if (!isKnownProxyRef) {
|
|
||||||
return RE::BSContainer::ForEachResult::kContinue;
|
return RE::BSContainer::ForEachResult::kContinue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto handle = actor->GetHandle();
|
auto handle = actor->GetHandle();
|
||||||
g_availableProxyPool.push_back(handle);
|
g_availableProxyPool.push_back(handle);
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Proxy pool: Found pre-placed proxy {:08X} (localForm={:06X}, editorId={}), total proxies: {}",
|
"Proxy pool: Found CK-placed proxy {:08X} (localForm={:06X}, editorId={}), total proxies: {}",
|
||||||
actor->GetFormID(),
|
actor->GetFormID(),
|
||||||
refLocalFormId,
|
actor->GetFormID() & 0x00FFFFFF,
|
||||||
GetEditorIdForLog(*a_ref),
|
GetEditorIdForLog(*a_ref),
|
||||||
g_availableProxyPool.size()));
|
g_availableProxyPool.size()));
|
||||||
|
|
||||||
@@ -2438,13 +2503,17 @@ namespace
|
|||||||
g_proxyPoolInitialized = true;
|
g_proxyPoolInitialized = true;
|
||||||
|
|
||||||
if (g_availableProxyPool.empty()) {
|
if (g_availableProxyPool.empty()) {
|
||||||
LogWarningWithLocalPlayerPrefix(
|
LogWarningWithLocalPlayerPrefix(std::format(
|
||||||
"Proxy pool initialized but NO pre-placed proxies found in cell! "
|
"Proxy pool initialized but NO CK-placed proxies found in cell {:08X}. "
|
||||||
"Expected F4TProxyRemotePlayer01REF or ESP-local form 0x0020A2 in the current cell.");
|
"Place Actor refs in Creation Kit using COPlayerProxy base and editor IDs like "
|
||||||
|
"COProxyRemotePlayer01REF (legacy: {}).",
|
||||||
|
a_cell.GetFormID(),
|
||||||
|
kProxyEditorId));
|
||||||
} else {
|
} else {
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Proxy pool ready: {} pre-placed proxies available. Max concurrent players: {}",
|
"Proxy pool ready: {} CK-placed proxies available in cell {:08X}. Max concurrent remotes in this cell: {}.",
|
||||||
g_availableProxyPool.size(),
|
g_availableProxyPool.size(),
|
||||||
|
a_cell.GetFormID(),
|
||||||
g_availableProxyPool.size()));
|
g_availableProxyPool.size()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4706,8 +4775,11 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
targetPosition.y = a_remotePlayer.y;
|
targetPosition.y = a_remotePlayer.y;
|
||||||
targetPosition.z = a_remotePlayer.z;
|
targetPosition.z = a_remotePlayer.z;
|
||||||
|
|
||||||
a_proxy.SetPosition(targetPosition, true);
|
F4T::ProxyGameAPI::PositionRemoteActor(
|
||||||
a_proxy.SetHeading(a_remotePlayer.angleZ);
|
a_proxy,
|
||||||
|
targetPosition,
|
||||||
|
a_remotePlayer.angleZ,
|
||||||
|
true);
|
||||||
g_proxyLifecycleState = ProxyLifecycleState::kActive;
|
g_proxyLifecycleState = ProxyLifecycleState::kActive;
|
||||||
g_proxyHoldReason.reset();
|
g_proxyHoldReason.reset();
|
||||||
g_proxyHoldPlayerId.reset();
|
g_proxyHoldPlayerId.reset();
|
||||||
@@ -4803,6 +4875,14 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
const RE::TESObjectCELL& a_currentCell,
|
const RE::TESObjectCELL& a_currentCell,
|
||||||
const RE::PlayerCharacter& a_player)
|
const RE::PlayerCharacter& a_player)
|
||||||
{
|
{
|
||||||
|
// Keep the proxy at the local spawn feet during settle; movement/animation before
|
||||||
|
// 3D finishes loading causes headless A-poses (especially on PlaceAtMe spawns).
|
||||||
|
if (a_slot.spawnState == PlaceAtMeBackedProxySpawnState::kSettling ||
|
||||||
|
a_slot.spawnState == PlaceAtMeBackedProxySpawnState::kCustomProxyAttempting ||
|
||||||
|
a_slot.spawnState == PlaceAtMeBackedProxySpawnState::kCodsworthFallbackAttempting) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
if (!a_slot.cachedProxyActor) {
|
if (!a_slot.cachedProxyActor) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -4940,8 +5020,13 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
if (auto* pooledProxy = TryGetProxyFromPool()) {
|
if (auto* pooledProxy = TryGetProxyFromPool()) {
|
||||||
a_slot.proxyHandle = GetProxyActorHandle(*pooledProxy);
|
a_slot.proxyHandle = GetProxyActorHandle(*pooledProxy);
|
||||||
a_slot.cachedProxyFormId = pooledProxy->GetFormID();
|
a_slot.cachedProxyFormId = pooledProxy->GetFormID();
|
||||||
|
a_slot.cachedProxyActor = pooledProxy;
|
||||||
|
a_slot.spawnState = PlaceAtMeBackedProxySpawnState::kPromoted;
|
||||||
|
a_slot.spawnSucceeded = true;
|
||||||
|
a_slot.spawnAttemptInProgress = false;
|
||||||
|
a_slot.spawnFailed = false;
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Assigned proxy from pre-placed pool {:08X} to remote player {}",
|
"Assigned CK-placed proxy {:08X} to remote player {} from cell pool.",
|
||||||
pooledProxy->GetFormID(),
|
pooledProxy->GetFormID(),
|
||||||
a_slot.remotePlayerId));
|
a_slot.remotePlayerId));
|
||||||
return pooledProxy;
|
return pooledProxy;
|
||||||
@@ -5126,7 +5211,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
a_proxy.SetPosition(holdingPosition, true);
|
F4T::ProxyGameAPI::PositionRemoteActor(a_proxy, holdingPosition, a_proxy.GetHeading(), true);
|
||||||
LogThrottledInfo(
|
LogThrottledInfo(
|
||||||
"held_runtime_proxy_position_corrected_" + std::to_string(a_slot.remotePlayerId),
|
"held_runtime_proxy_position_corrected_" + std::to_string(a_slot.remotePlayerId),
|
||||||
std::format(
|
std::format(
|
||||||
@@ -5165,7 +5250,15 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
ClearSlotVisualSneakIfApplied(slot, *proxy, "local cell change");
|
ClearSlotVisualSneakIfApplied(slot, *proxy, "local cell change");
|
||||||
ClearSlotProxyAnimationIfApplied(slot, *proxy, "local cell change");
|
ClearSlotProxyAnimationIfApplied(slot, *proxy, "local cell change");
|
||||||
NeutralizeProxyActor(slot, *proxy, a_player, ProxyNeutralizationReason::kHolding);
|
NeutralizeProxyActor(slot, *proxy, a_player, ProxyNeutralizationReason::kHolding);
|
||||||
|
if constexpr (kEnablePrePlacedProxyPool) {
|
||||||
|
MoveProxyToHoldingPosition(
|
||||||
|
*proxy,
|
||||||
|
ProxyLifecycleState::kHeldLeftCell,
|
||||||
|
slot.remotePlayerId,
|
||||||
|
ActiveProxyKind::kPlacedFallback);
|
||||||
|
} else {
|
||||||
proxy->MarkAsDeleted();
|
proxy->MarkAsDeleted();
|
||||||
|
}
|
||||||
|
|
||||||
slot.proxyHandle.reset();
|
slot.proxyHandle.reset();
|
||||||
slot.cachedProxyActor = nullptr;
|
slot.cachedProxyActor = nullptr;
|
||||||
@@ -5209,7 +5302,11 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
a_slot,
|
a_slot,
|
||||||
a_proxy,
|
a_proxy,
|
||||||
a_reason == ProxyLifecycleState::kHeldDisconnected ? "disconnect hold" : "hold");
|
a_reason == ProxyLifecycleState::kHeldDisconnected ? "disconnect hold" : "hold");
|
||||||
a_proxy.SetPosition(GetSlotHoldingPosition(a_slot), true);
|
F4T::ProxyGameAPI::PositionRemoteActor(
|
||||||
|
a_proxy,
|
||||||
|
GetSlotHoldingPosition(a_slot),
|
||||||
|
a_proxy.GetHeading(),
|
||||||
|
true);
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -5306,7 +5403,31 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
{
|
{
|
||||||
const auto targetPosition = GetRuntimeProxyVisibleTargetPosition(a_slot, a_remotePlayer, a_player);
|
const auto targetPosition = GetRuntimeProxyVisibleTargetPosition(a_slot, a_remotePlayer, a_player);
|
||||||
|
|
||||||
ApplyRuntimeProxyTransform(a_proxy, targetPosition, a_remotePlayer.angleZ, a_remotePlayer, true, &a_slot);
|
a_slot.targetPosition = targetPosition;
|
||||||
|
a_slot.targetHeading = a_remotePlayer.angleZ;
|
||||||
|
a_slot.puppetDirection = a_remotePlayer.angleZ;
|
||||||
|
a_slot.lastRemotePlayerSnapshot = a_remotePlayer;
|
||||||
|
a_slot.puppetGraphSpeed = -1.0F;
|
||||||
|
if (a_remotePlayer.isMoving && a_remotePlayer.movementSpeed >= 1.0F) {
|
||||||
|
a_slot.puppetGraphSpeed = F4T::ProxyAnimationSync::ResolveLocomotionGraphSpeed(
|
||||||
|
a_remotePlayer.movementSpeed,
|
||||||
|
a_remotePlayer.animationGraphSpeed,
|
||||||
|
a_remotePlayer.isMoving,
|
||||||
|
a_remotePlayer.isSprinting);
|
||||||
|
}
|
||||||
|
|
||||||
|
F4T::ProxyPuppet::SetProxyLocomotionState(
|
||||||
|
a_proxy.GetFormID(),
|
||||||
|
a_slot.puppetGraphSpeed,
|
||||||
|
a_slot.puppetDirection);
|
||||||
|
|
||||||
|
F4T::ProxyGameAPI::PositionRemoteActor(
|
||||||
|
a_proxy,
|
||||||
|
targetPosition,
|
||||||
|
a_remotePlayer.angleZ,
|
||||||
|
true);
|
||||||
|
a_slot.skipSmoothFrameOnce = true;
|
||||||
|
|
||||||
a_slot.lifecycleState = ProxyLifecycleState::kActive;
|
a_slot.lifecycleState = ProxyLifecycleState::kActive;
|
||||||
a_slot.holdReason.reset();
|
a_slot.holdReason.reset();
|
||||||
a_slot.heldSinceTime = {};
|
a_slot.heldSinceTime = {};
|
||||||
@@ -5706,6 +5827,9 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
auto* proxy = TryResolveSlotProxy(a_slot, a_parentCell, a_player);
|
auto* proxy = TryResolveSlotProxy(a_slot, a_parentCell, a_player);
|
||||||
if (!proxy) {
|
if (!proxy) {
|
||||||
MaintainSlotPlaceAtMeSpawn(a_slot, a_parentCell, a_player);
|
MaintainSlotPlaceAtMeSpawn(a_slot, a_parentCell, a_player);
|
||||||
|
if (a_slot.spawnState == PlaceAtMeBackedProxySpawnState::kSettling) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
proxy = TryResolveSlotProxy(a_slot, a_parentCell, a_player);
|
proxy = TryResolveSlotProxy(a_slot, a_parentCell, a_player);
|
||||||
if (!proxy) {
|
if (!proxy) {
|
||||||
return false;
|
return false;
|
||||||
@@ -5893,6 +6017,11 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (slot.skipSmoothFrameOnce) {
|
||||||
|
slot.skipSmoothFrameOnce = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
auto* proxy = TryResolveSlotProxy(slot, *player->GetParentCell(), *player);
|
auto* proxy = TryResolveSlotProxy(slot, *player->GetParentCell(), *player);
|
||||||
if (!proxy || !proxy->currentProcess) {
|
if (!proxy || !proxy->currentProcess) {
|
||||||
continue;
|
continue;
|
||||||
@@ -5932,7 +6061,11 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
// Animation follows network state, not interpolation remainder. When the remote
|
// Animation follows network state, not interpolation remainder. When the remote
|
||||||
// stops, pin position immediately so we don't jog-in-place while lerping.
|
// stops, pin position immediately so we don't jog-in-place while lerping.
|
||||||
if (!remoteLocomoting || distanceToTarget < 0.1F) {
|
if (!remoteLocomoting || distanceToTarget < 0.1F) {
|
||||||
proxy->SetPosition(slot.targetPosition, true);
|
F4T::ProxyGameAPI::PositionRemoteActor(
|
||||||
|
*proxy,
|
||||||
|
slot.targetPosition,
|
||||||
|
headingToApply,
|
||||||
|
true);
|
||||||
slot.puppetGraphSpeed = -1.0F;
|
slot.puppetGraphSpeed = -1.0F;
|
||||||
F4T::ProxyPuppet::SetProxyLocomotionState(
|
F4T::ProxyPuppet::SetProxyLocomotionState(
|
||||||
proxy->GetFormID(),
|
proxy->GetFormID(),
|
||||||
@@ -5964,9 +6097,15 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Move through the character controller so FO4's motion-feedback path sees
|
// Move through the character controller so FO4's motion-feedback path sees
|
||||||
// real velocity (within-state walk cadence). SetPosition alone leaves ~0
|
// real velocity (within-state walk cadence). PositionRemoteActor uses
|
||||||
// controller velocity, so every sub-run pace used the same authored walk.
|
// SetPosition(..., true) so the Havok body follows the lerp each frame.
|
||||||
proxy->Move(frameDeltaSeconds, frameDelta, true);
|
// Per-frame lerp must bypass the 20 Hz MoveRefrToPosition throttle; otherwise
|
||||||
|
// puppet motion feedback runs every frame but the actor stays at PlaceAtMe spawn.
|
||||||
|
F4T::ProxyGameAPI::PositionRemoteActor(
|
||||||
|
*proxy,
|
||||||
|
nextPosition,
|
||||||
|
headingToApply,
|
||||||
|
true);
|
||||||
F4T::ProxyPuppet::ApplyProxyLocomotionFrame(
|
F4T::ProxyPuppet::ApplyProxyLocomotionFrame(
|
||||||
*proxy,
|
*proxy,
|
||||||
frameDelta,
|
frameDelta,
|
||||||
@@ -6108,7 +6247,9 @@ namespace F4T::ProxyActorController
|
|||||||
|
|
||||||
if (!g_stage4StartedLogged) {
|
if (!g_stage4StartedLogged) {
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
LogInfoWithLocalPlayerPrefix(std::format(
|
||||||
"Runtime proxy actor manager active: one runtime proxy slot per remote playerId, max runtime proxies={}.",
|
"Proxy actor manager active (PlaceAtMe={}, MoveRefrToPosition positioning, CK pool={}), max slots={}.",
|
||||||
|
kUsePlaceAtMeBackedRuntimeProxySpawn,
|
||||||
|
kEnablePrePlacedProxyPool,
|
||||||
kMaxRuntimeProxyActors));
|
kMaxRuntimeProxyActors));
|
||||||
g_stage4StartedLogged = true;
|
g_stage4StartedLogged = true;
|
||||||
}
|
}
|
||||||
@@ -6116,7 +6257,7 @@ namespace F4T::ProxyActorController
|
|||||||
const auto remotePlayers = GetSortedRemotePlayerSnapshot();
|
const auto remotePlayers = GetSortedRemotePlayerSnapshot();
|
||||||
|
|
||||||
if constexpr (kEnablePrePlacedProxyPool) {
|
if constexpr (kEnablePrePlacedProxyPool) {
|
||||||
if (!g_proxyPoolInitialized && IsTestCell(*parentCell)) {
|
if (!g_proxyPoolInitialized) {
|
||||||
InitializeProxyPool(*parentCell, *player);
|
InitializeProxyPool(*parentCell, *player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,8 @@
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
// Master gate; sub-gates allow incremental rollback.
|
// Master gate; sub-gates allow incremental rollback.
|
||||||
// Enabled now that proxies use CK-placed actors with active AI (g_preserveProxyAIForAnimationTesting).
|
// Enabled for dynamic PlaceAtMe proxies positioned via MoveRefrToPosition; puppet hook
|
||||||
// The old reason for disabling (suppressed AI) no longer applies.
|
// drives graph Speed/Direction because controller velocity stays near zero.
|
||||||
constexpr bool kEnableProxyVisualAnimationSync = true;
|
constexpr bool kEnableProxyVisualAnimationSync = true;
|
||||||
constexpr bool kEnableProxyLocomotionSync = true;
|
constexpr bool kEnableProxyLocomotionSync = true;
|
||||||
// Enabled now that the authoritative FO4 graph name is known: sneak is the INT32
|
// Enabled now that the authoritative FO4 graph name is known: sneak is the INT32
|
||||||
@@ -90,8 +90,19 @@ namespace
|
|||||||
return std::fabs(a_lhs - a_rhs) > kSpeedEpsilon;
|
return std::fabs(a_lhs - a_rhs) > kSpeedEpsilon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ProxyHasLoaded3D(const RE::Actor& a_proxy)
|
||||||
|
{
|
||||||
|
return a_proxy.Get3D() != nullptr ||
|
||||||
|
a_proxy.GetFullyLoaded3D() != nullptr ||
|
||||||
|
a_proxy.GetCurrent3D() != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
bool CanWriteProxyGraph(RE::Actor& a_proxy, std::uint32_t a_remotePlayerId, std::uint32_t a_actorId)
|
bool CanWriteProxyGraph(RE::Actor& a_proxy, std::uint32_t a_remotePlayerId, std::uint32_t a_actorId)
|
||||||
{
|
{
|
||||||
|
if (!ProxyHasLoaded3D(a_proxy)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy);
|
auto* graphHolder = static_cast<RE::IAnimationGraphManagerHolder*>(&a_proxy);
|
||||||
RE::BSTSmartPointer<RE::BSAnimationGraphManager> graphManager;
|
RE::BSTSmartPointer<RE::BSAnimationGraphManager> graphManager;
|
||||||
if (!graphHolder->GetAnimationGraphManagerImpl(graphManager) || !graphManager) {
|
if (!graphHolder->GetAnimationGraphManagerImpl(graphManager) || !graphManager) {
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
#include "pch.h"
|
||||||
|
|
||||||
|
#include "F4TProxyGameAPI.h"
|
||||||
|
|
||||||
|
#include "F4TNetworking.h"
|
||||||
|
|
||||||
|
#include "RE/P/PlayerCharacter.h"
|
||||||
|
#include "RE/T/TESObjectCELL.h"
|
||||||
|
#include "RE/T/TESWorldSpace.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <format>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct ActorMoveState
|
||||||
|
{
|
||||||
|
std::uint32_t callCount{ 0 };
|
||||||
|
std::chrono::steady_clock::time_point lastTime{};
|
||||||
|
float lastX{ 0.0F };
|
||||||
|
float lastY{ 0.0F };
|
||||||
|
float lastZ{ 0.0F };
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::unordered_map<RE::Actor*, ActorMoveState> g_moveState;
|
||||||
|
|
||||||
|
constexpr double kMoveIntervalSeconds = 1.0 / 20.0;
|
||||||
|
constexpr float kMoveDeltaSq = 0.25F;
|
||||||
|
constexpr std::uint32_t kForcedMoveCalls = 5;
|
||||||
|
|
||||||
|
std::string GetLocalPlayerLogMessage(std::string_view a_message)
|
||||||
|
{
|
||||||
|
return F4T::Networking::GetLocalPlayerLogPrefix() + " " + std::string(a_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogThrottledCellFix(
|
||||||
|
RE::Actor& a_proxy,
|
||||||
|
const RE::NiPoint3& a_pos,
|
||||||
|
RE::TESObjectCELL* a_cell,
|
||||||
|
RE::TESWorldSpace* a_worldSpace,
|
||||||
|
std::uint32_t a_callCount,
|
||||||
|
bool a_migratedPlaySpace)
|
||||||
|
{
|
||||||
|
if (a_callCount > kForcedMoveCalls && a_callCount % 300 != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto cellId = a_cell ? a_cell->GetFormID() : 0U;
|
||||||
|
const auto worldId = a_worldSpace ? a_worldSpace->GetFormID() : 0U;
|
||||||
|
REX::INFO(std::string_view{ GetLocalPlayerLogMessage(std::format(
|
||||||
|
"PositionRemoteActor proxy={:08X} call#{} pos=({:.1f},{:.1f},{:.1f}) mode={} ws={:08X} cell={:08X} migrated={}.",
|
||||||
|
a_proxy.GetFormID(),
|
||||||
|
a_callCount,
|
||||||
|
a_pos.x,
|
||||||
|
a_pos.y,
|
||||||
|
a_pos.z,
|
||||||
|
a_worldSpace ? "exterior" : (a_cell ? "interior" : "no-anchor"),
|
||||||
|
worldId,
|
||||||
|
cellId,
|
||||||
|
a_migratedPlaySpace)) });
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResolveLocalPlaySpace(
|
||||||
|
RE::TESObjectCELL*& a_outCell,
|
||||||
|
RE::TESWorldSpace*& a_outWorldSpace)
|
||||||
|
{
|
||||||
|
a_outCell = nullptr;
|
||||||
|
a_outWorldSpace = nullptr;
|
||||||
|
|
||||||
|
const auto* player = RE::PlayerCharacter::GetSingleton();
|
||||||
|
if (!player) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* playerCell = player->GetParentCell();
|
||||||
|
if (!playerCell) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerCell->IsExterior()) {
|
||||||
|
a_outWorldSpace = playerCell->worldSpace;
|
||||||
|
if (!a_outWorldSpace) {
|
||||||
|
a_outWorldSpace = player->cachedWorldspace;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
a_outCell = const_cast<RE::TESObjectCELL*>(playerCell);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsFinitePosition(const RE::NiPoint3& a_pos)
|
||||||
|
{
|
||||||
|
return std::isfinite(a_pos.x) && std::isfinite(a_pos.y) && std::isfinite(a_pos.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProxyInLocalPlaySpace(
|
||||||
|
const RE::Actor& a_proxy,
|
||||||
|
RE::TESObjectCELL* a_localCell,
|
||||||
|
RE::TESWorldSpace* a_localWorldSpace)
|
||||||
|
{
|
||||||
|
const auto* proxyCell = a_proxy.GetParentCell();
|
||||||
|
if (!proxyCell) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a_localWorldSpace) {
|
||||||
|
return proxyCell->IsExterior() && proxyCell->worldSpace == a_localWorldSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a_localCell && proxyCell->GetFormID() == a_localCell->GetFormID();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyPositionInPlaySpace(
|
||||||
|
RE::Actor& a_proxy,
|
||||||
|
const RE::NiPoint3& a_pos,
|
||||||
|
const RE::NiPoint3& a_rot,
|
||||||
|
RE::TESObjectCELL* a_cell,
|
||||||
|
RE::TESWorldSpace* a_worldSpace,
|
||||||
|
std::uint32_t a_callCount)
|
||||||
|
{
|
||||||
|
const bool migrated =
|
||||||
|
!ProxyInLocalPlaySpace(a_proxy, a_cell, a_worldSpace);
|
||||||
|
if (migrated) {
|
||||||
|
a_proxy.MoveRefToNewSpace(a_cell, a_worldSpace);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLocationOnReference only updates TESObjectREFR::pos; it does not sync the
|
||||||
|
// Havok character controller. PlaceAtMe proxies snap back to spawn on the next
|
||||||
|
// AI tick while GetPosition() briefly reports the teleported coords.
|
||||||
|
a_proxy.SetPosition(a_pos, true);
|
||||||
|
a_proxy.SetAngleOnReference(a_rot);
|
||||||
|
a_proxy.SetHeading(a_rot.z);
|
||||||
|
|
||||||
|
LogThrottledCellFix(a_proxy, a_pos, a_cell, a_worldSpace, a_callCount, migrated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace F4T::ProxyGameAPI
|
||||||
|
{
|
||||||
|
void PositionRemoteActor(RE::Actor& a_proxy, const RE::NiPoint3& a_pos, float a_headingZ, bool a_force)
|
||||||
|
{
|
||||||
|
PositionRemoteActor(a_proxy, a_pos, RE::NiPoint3{ 0.0F, 0.0F, a_headingZ }, a_force);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PositionRemoteActor(
|
||||||
|
RE::Actor& a_proxy,
|
||||||
|
const RE::NiPoint3& a_pos,
|
||||||
|
const RE::NiPoint3& a_rot,
|
||||||
|
bool a_force)
|
||||||
|
{
|
||||||
|
if (!IsFinitePosition(a_pos)) {
|
||||||
|
REX::WARN(std::string_view{ GetLocalPlayerLogMessage(std::format(
|
||||||
|
"PositionRemoteActor skipped for proxy {:08X}: non-finite target position.",
|
||||||
|
a_proxy.GetFormID())) });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RE::TESObjectCELL* cell = nullptr;
|
||||||
|
RE::TESWorldSpace* worldSpace = nullptr;
|
||||||
|
ResolveLocalPlaySpace(cell, worldSpace);
|
||||||
|
if (!cell && !worldSpace) {
|
||||||
|
REX::WARN(std::string_view{ GetLocalPlayerLogMessage(std::format(
|
||||||
|
"PositionRemoteActor skipped for proxy {:08X}: local play space unavailable.",
|
||||||
|
a_proxy.GetFormID())) });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& moveState = g_moveState[&a_proxy];
|
||||||
|
++moveState.callCount;
|
||||||
|
|
||||||
|
const float dx = a_pos.x - moveState.lastX;
|
||||||
|
const float dy = a_pos.y - moveState.lastY;
|
||||||
|
const float dz = a_pos.z - moveState.lastZ;
|
||||||
|
const float deltaSq = (dx * dx) + (dy * dy) + (dz * dz);
|
||||||
|
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
const bool forceMove = a_force || moveState.callCount <= kForcedMoveCalls;
|
||||||
|
const bool timedOut = moveState.lastTime.time_since_epoch().count() == 0 ||
|
||||||
|
std::chrono::duration<double>(now - moveState.lastTime).count() >= kMoveIntervalSeconds;
|
||||||
|
const bool movedEnough = deltaSq >= kMoveDeltaSq;
|
||||||
|
|
||||||
|
if (!forceMove && !(timedOut && movedEnough)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
moveState.lastX = a_pos.x;
|
||||||
|
moveState.lastY = a_pos.y;
|
||||||
|
moveState.lastZ = a_pos.z;
|
||||||
|
moveState.lastTime = now;
|
||||||
|
|
||||||
|
ApplyPositionInPlaySpace(a_proxy, a_pos, a_rot, cell, worldSpace, moveState.callCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user