Dynamic proxy spawning, velocity & state sync
Implements Phase 4 dynamic proxy spawning and actor state synchronization. Adds a dynamic proxy pool (g_dynamicProxyPool), SpawnDynamicProxyActor and GetOrSpawnDynamicProxy to spawn/reuse actors via Player::PlaceAtMe, and updates slot resolution to prefer dynamic proxies with a pre-placed pool fallback. Verifies/integrates existing character-controller velocity injection and clamps speed, and applies remote actor state flags to proxies before animation sync. Enhanced logging for spawn, velocity and state transitions. Changes in plugin/src/F4TProxyActorController.cpp and updated dev log (docs/dev-log.md).
This commit is contained in:
@@ -53,10 +53,14 @@ namespace
|
||||
// This ensures proxies have proper AI, character controllers, and animation systems.
|
||||
// Max players = number of pre-placed proxies in the world.
|
||||
constexpr bool kUsePlaceAtMeBackedRuntimeProxySpawn = false; // Use pre-placed proxies
|
||||
|
||||
|
||||
// Pre-placed proxies we've discovered and can reuse
|
||||
static std::vector<RE::ObjectRefHandle> g_availableProxyPool;
|
||||
static bool g_proxyPoolInitialized = false;
|
||||
|
||||
// Phase 4: Dynamic spawn pools (maps playerId → actor handle)
|
||||
static std::unordered_map<std::uint32_t, RE::ObjectRefHandle> g_dynamicProxyPool;
|
||||
static bool g_dynamicProxyPoolEnabled = true; // Enable dynamic spawning for Phase 4 testing
|
||||
|
||||
// Forward declaration
|
||||
bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::PlayerCharacter& a_player);
|
||||
@@ -2185,6 +2189,100 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4.1: Dynamic proxy spawning (replacement for pre-placed pool model)
|
||||
// Spawns a new actor instance and manages its lifecycle
|
||||
RE::Actor* SpawnDynamicProxyActor(
|
||||
const RE::PlayerCharacter& a_player,
|
||||
std::uint32_t a_remotePlayerId)
|
||||
{
|
||||
if (!g_dynamicProxyPoolEnabled) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Get proxy base actor (use custom proxy or Codsworth for testing)
|
||||
RE::TESNPC* proxyBase = nullptr;
|
||||
|
||||
// First try to find custom proxy base in ESP
|
||||
const 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
|
||||
if (!proxyBase) {
|
||||
proxyBase = RE::TESForm::LookupByID<RE::TESNPC>(kDebugPlaceAtMeActorBaseFormId);
|
||||
}
|
||||
|
||||
if (!proxyBase) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: Cannot spawn proxy for remote player {}; proxy base actor not found.",
|
||||
a_remotePlayerId));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Spawn the actor via PlaceAtMe
|
||||
const auto spawnedRef = a_player.PlaceAtMe(proxyBase, 1, false, true);
|
||||
if (!spawnedRef) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: PlaceAtMe failed for remote player {} with base {:08X}.",
|
||||
a_remotePlayerId,
|
||||
proxyBase->GetFormID()));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* spawnedActor = spawnedRef->As<RE::Actor>();
|
||||
if (!spawnedActor) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: PlaceAtMe returned non-actor ref for remote player {}.",
|
||||
a_remotePlayerId));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Store in dynamic pool for reuse
|
||||
g_dynamicProxyPool[a_remotePlayerId] = spawnedActor->GetHandle();
|
||||
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"SpawnDynamicProxyActor: Spawned proxy actor {:08X} for remote player {} with base {:08X}.",
|
||||
spawnedActor->GetFormID(),
|
||||
a_remotePlayerId,
|
||||
proxyBase->GetFormID()));
|
||||
|
||||
return spawnedActor;
|
||||
}
|
||||
|
||||
// Phase 4.1: Retrieve or spawn proxy for a remote player
|
||||
// First tries dynamic pool, then spawns if needed
|
||||
RE::Actor* GetOrSpawnDynamicProxy(
|
||||
const RE::PlayerCharacter& a_player,
|
||||
std::uint32_t a_remotePlayerId)
|
||||
{
|
||||
// Check if already have a proxy in pool for this remote player
|
||||
const auto it = g_dynamicProxyPool.find(a_remotePlayerId);
|
||||
if (it != g_dynamicProxyPool.end()) {
|
||||
const auto proxyRef = it->second.get();
|
||||
if (auto actor = proxyRef ? proxyRef->As<RE::Actor>() : nullptr) {
|
||||
// Verify proxy is still valid
|
||||
if (actor->GetNPC() && !actor->IsDead()) {
|
||||
return actor;
|
||||
}
|
||||
}
|
||||
// Remove dead/invalid handle from pool
|
||||
g_dynamicProxyPool.erase(it);
|
||||
}
|
||||
|
||||
// Spawn new proxy if not available
|
||||
return SpawnDynamicProxyActor(a_player, a_remotePlayerId);
|
||||
}
|
||||
|
||||
bool IsProxyPoolActorAssigned(const RE::Actor& a_actor)
|
||||
{
|
||||
for (const auto& [_, slot] : g_proxySlots) {
|
||||
@@ -4306,11 +4404,23 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
const RE::PlayerCharacter& a_player)
|
||||
{
|
||||
if (!a_slot.proxyHandle) {
|
||||
// Try to grab a proxy from the pool
|
||||
// Phase 4: Try dynamic spawn first (new model)
|
||||
if (g_dynamicProxyPoolEnabled) {
|
||||
if (auto* dynamicProxy = GetOrSpawnDynamicProxy(const_cast<RE::PlayerCharacter&>(a_player), a_slot.remotePlayerId)) {
|
||||
a_slot.proxyHandle = dynamicProxy->GetHandle();
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Assigned dynamically spawned proxy {:08X} to remote player {}",
|
||||
dynamicProxy->GetFormID(),
|
||||
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 pool {:08X} to remote player {}",
|
||||
"Assigned proxy from pre-placed pool {:08X} to remote player {}",
|
||||
pooledProxy->GetFormID(),
|
||||
a_slot.remotePlayerId));
|
||||
} else {
|
||||
@@ -4750,6 +4860,13 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
const auto shouldSnap = ShouldSnapRemoteMovement(a_remotePlayer.movementType);
|
||||
ApplyRuntimeProxyTransform(a_proxy, targetPosition, a_remotePlayer.angleZ, a_remotePlayer, shouldSnap, &a_slot);
|
||||
|
||||
// Phase 4.3: Apply actor state flags before animation sync
|
||||
// This signals the animation graph FSM about combat, sneaking, etc.
|
||||
if (a_remotePlayer.actorStateFlags1 != 0 || a_remotePlayer.actorStateFlags2 != 0) {
|
||||
a_proxy.actorState.flags1 = a_remotePlayer.actorStateFlags1;
|
||||
a_proxy.actorState.flags2 = a_remotePlayer.actorStateFlags2;
|
||||
}
|
||||
|
||||
a_slot.lifecycleState = ProxyLifecycleState::kActive;
|
||||
a_slot.holdReason.reset();
|
||||
a_slot.heldSinceTime = {};
|
||||
@@ -4778,13 +4895,15 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
|
||||
|
||||
if (!a_slot.remoteStateMovedLogged) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Moved runtime proxy actor {:08X} from remote player {} state for the first time: X={:.2f}, Y={:.2f}, Z={:.2f}, AngleZ={:.2f}.",
|
||||
"Moved runtime proxy actor {:08X} from remote player {} state for the first time: X={:.2f}, Y={:.2f}, Z={:.2f}, AngleZ={:.2f}, flags1={:08X}, flags2={:08X}.",
|
||||
a_proxy.GetFormID(),
|
||||
a_remotePlayer.playerId,
|
||||
a_remotePlayer.x,
|
||||
a_remotePlayer.y,
|
||||
a_remotePlayer.z,
|
||||
a_remotePlayer.angleZ));
|
||||
a_remotePlayer.angleZ,
|
||||
a_remotePlayer.actorStateFlags1,
|
||||
a_remotePlayer.actorStateFlags2));
|
||||
a_slot.remoteStateMovedLogged = true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user