Add one-shot runtime proxy spawn diagnostic
Introduce a Stage 2 diagnostic that attempts a single runtime spawn of F4T_RemotePlayerProxy in F4TTestCell01 from the game-thread proxy controller. Changes include a new enable flag (kEnableRuntimeProxySpawnDiagnostic), diagnostic offset constant, new diagnostic ObjectRefHandle and boolean state flags, and a TrySpawnRuntimeProxyDiagnostic function that builds NEW_REFR_DATA, calls TESDataHandler::CreateReferenceAtLocation, validates the returned handle/actor/cell, logs a single attempt and success/failure outcome, and intentionally leaves the spawned actor in place. The diagnostic spawn is run before placed-proxy lookup and the spawned reference is skipped during placed proxy scanning so the placed proxy remains the active visual fallback. Updated header comment and documentation (architecture.md and dev-log.md) to describe the new diagnostic behavior and testing notes.
This commit is contained in:
@@ -24,6 +24,8 @@ namespace
|
||||
constexpr RE::TESFormID kProxyBaseActorLocalFormId = 0x0020A1;
|
||||
constexpr RE::TESFormID kProxyFallbackLocalFormId = 0x0020A2;
|
||||
constexpr auto kProxyOffsetX = 150.0F;
|
||||
constexpr bool kEnableRuntimeProxySpawnDiagnostic = true;
|
||||
constexpr auto kRuntimeProxySpawnDiagnosticOffsetX = 250.0F;
|
||||
constexpr float kProxyPositionLerpAlpha = 0.15F;
|
||||
// Temporary single-proxy lifecycle fallback. Keep the placed proxy inside the
|
||||
// loaded test cell instead of disabling a persistent reference before that path
|
||||
@@ -67,10 +69,15 @@ namespace
|
||||
constexpr auto kProxyMovementMode = ProxyMovementMode::kRemotePlayerStateTest;
|
||||
|
||||
RE::ObjectRefHandle g_proxyHandle;
|
||||
RE::ObjectRefHandle g_runtimeProxySpawnDiagnosticHandle;
|
||||
bool g_proxyResolvedLogged = false;
|
||||
bool g_proxyActorBaseDiagnosticStartedLogged = false;
|
||||
bool g_testPluginLoadedLogged = false;
|
||||
bool g_proxyActorBaseResolvedLogged = false;
|
||||
bool g_runtimeProxySpawnAttempted = false;
|
||||
bool g_runtimeProxySpawnAttemptInProgress = false;
|
||||
bool g_runtimeProxySpawnSucceeded = false;
|
||||
bool g_runtimeProxySpawnFailed = false;
|
||||
bool g_safetyOffsetMovedLogged = false;
|
||||
bool g_remoteStateMovementStartedLogged = false;
|
||||
bool g_remoteStateMovedLogged = false;
|
||||
@@ -362,7 +369,7 @@ namespace
|
||||
|
||||
if (!g_proxyActorBaseResolvedLogged) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy actor base resolved: {} by plugin-local FormID fallback. plugin={}, localFormId={:06X}, runtimeFormId={:08X}, formType={}, isTESNPC=true, editorId={}, displayName={}, accepted=true. Runtime spawning remains disabled for Stage 1.",
|
||||
"Runtime proxy actor base resolved: {} by plugin-local FormID fallback. plugin={}, localFormId={:06X}, runtimeFormId={:08X}, formType={}, isTESNPC=true, editorId={}, displayName={}, accepted=true.",
|
||||
kProxyBaseActorEditorId,
|
||||
kTestPluginName,
|
||||
kProxyBaseActorLocalFormId,
|
||||
@@ -393,7 +400,7 @@ namespace
|
||||
if (baseActor && HasEditorId(*baseActor, kProxyBaseActorEditorId) && IsUsableProxyActorBase(*baseActor)) {
|
||||
if (!g_proxyActorBaseResolvedLogged) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy actor base resolved: {} by editor ID as form {:08X}. Runtime spawning remains disabled for Stage 1.",
|
||||
"Runtime proxy actor base resolved: {} by editor ID as form {:08X}.",
|
||||
kProxyBaseActorEditorId,
|
||||
baseActor->GetFormID()));
|
||||
g_proxyActorBaseResolvedLogged = true;
|
||||
@@ -546,6 +553,153 @@ namespace
|
||||
return std::addressof(a_proxy) != std::addressof(a_player) && a_proxy.GetParentCell() == std::addressof(a_expectedCell);
|
||||
}
|
||||
|
||||
bool IsRuntimeProxySpawnDiagnosticRef(const RE::TESObjectREFR& a_ref)
|
||||
{
|
||||
if (!g_runtimeProxySpawnDiagnosticHandle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto spawnedRef = g_runtimeProxySpawnDiagnosticHandle.get();
|
||||
return spawnedRef && spawnedRef.get() == std::addressof(a_ref);
|
||||
}
|
||||
|
||||
void MarkRuntimeProxySpawnDiagnosticFailed(std::string_view a_reason)
|
||||
{
|
||||
g_runtimeProxySpawnFailed = true;
|
||||
g_runtimeProxySpawnAttemptInProgress = false;
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy spawn diagnostic failed: {}",
|
||||
a_reason));
|
||||
}
|
||||
|
||||
void TrySpawnRuntimeProxyDiagnostic(
|
||||
RE::TESNPC* a_baseActor,
|
||||
RE::TESObjectCELL& a_parentCell,
|
||||
const RE::PlayerCharacter& a_player)
|
||||
{
|
||||
if constexpr (!kEnableRuntimeProxySpawnDiagnostic) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_runtimeProxySpawnAttempted ||
|
||||
g_runtimeProxySpawnSucceeded ||
|
||||
g_runtimeProxySpawnAttemptInProgress ||
|
||||
g_runtimeProxySpawnFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!a_baseActor) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* dataHandler = RE::TESDataHandler::GetSingleton();
|
||||
if (!dataHandler) {
|
||||
g_runtimeProxySpawnAttempted = true;
|
||||
MarkRuntimeProxySpawnDiagnosticFailed("TESDataHandler is unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
auto* boundObject = a_baseActor->As<RE::TESBoundObject>();
|
||||
if (!boundObject) {
|
||||
g_runtimeProxySpawnAttempted = true;
|
||||
MarkRuntimeProxySpawnDiagnosticFailed(std::format(
|
||||
"actor base {:08X} is not usable as NEW_REFR_DATA::object.",
|
||||
a_baseActor->GetFormID()));
|
||||
return;
|
||||
}
|
||||
|
||||
g_runtimeProxySpawnAttempted = true;
|
||||
g_runtimeProxySpawnAttemptInProgress = true;
|
||||
|
||||
auto spawnPosition = a_player.GetPosition();
|
||||
spawnPosition.x += kRuntimeProxySpawnDiagnosticOffsetX;
|
||||
|
||||
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 = std::addressof(a_parentCell);
|
||||
spawnData.world = nullptr;
|
||||
spawnData.reference = nullptr;
|
||||
spawnData.forcePersist = false;
|
||||
spawnData.initializeScripts = true;
|
||||
spawnData.initiallyDisabled = false;
|
||||
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy spawn diagnostic attempting: actorBase={:08X}, cell={:08X}, position=({:.2f}, {:.2f}, {:.2f}), heading={:.2f}.",
|
||||
a_baseActor->GetFormID(),
|
||||
a_parentCell.GetFormID(),
|
||||
spawnPosition.x,
|
||||
spawnPosition.y,
|
||||
spawnPosition.z,
|
||||
spawnDirection.z));
|
||||
|
||||
g_runtimeProxySpawnDiagnosticHandle = dataHandler->CreateReferenceAtLocation(spawnData);
|
||||
if (!g_runtimeProxySpawnDiagnosticHandle) {
|
||||
MarkRuntimeProxySpawnDiagnosticFailed(std::format(
|
||||
"CreateReferenceAtLocation returned an invalid handle for actorBase={:08X}, cell={:08X}, position=({:.2f}, {:.2f}, {:.2f}).",
|
||||
a_baseActor->GetFormID(),
|
||||
a_parentCell.GetFormID(),
|
||||
spawnPosition.x,
|
||||
spawnPosition.y,
|
||||
spawnPosition.z));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto spawnedRef = g_runtimeProxySpawnDiagnosticHandle.get();
|
||||
if (!spawnedRef) {
|
||||
MarkRuntimeProxySpawnDiagnosticFailed(std::format(
|
||||
"created handle {:08X} could not resolve to a reference.",
|
||||
g_runtimeProxySpawnDiagnosticHandle.get_handle()));
|
||||
return;
|
||||
}
|
||||
|
||||
auto* spawnedActor = spawnedRef->As<RE::Actor>();
|
||||
if (!spawnedActor) {
|
||||
MarkRuntimeProxySpawnDiagnosticFailed(std::format(
|
||||
"created reference {:08X} resolved but was not an actor.",
|
||||
spawnedRef->GetFormID()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (std::addressof(*spawnedActor) == std::addressof(a_player)) {
|
||||
MarkRuntimeProxySpawnDiagnosticFailed(std::format(
|
||||
"created reference {:08X} unexpectedly resolved as the local player.",
|
||||
spawnedRef->GetFormID()));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* spawnedCell = spawnedActor->GetParentCell();
|
||||
if (spawnedCell != std::addressof(a_parentCell)) {
|
||||
MarkRuntimeProxySpawnDiagnosticFailed(std::format(
|
||||
"created actor {:08X} parent cell validation failed: expected={:08X}, actual={}.",
|
||||
spawnedRef->GetFormID(),
|
||||
a_parentCell.GetFormID(),
|
||||
spawnedCell ? std::format("{:08X}", spawnedCell->GetFormID()) : "unavailable"));
|
||||
return;
|
||||
}
|
||||
|
||||
spawnedActor->SetPosition(spawnPosition, true);
|
||||
spawnedActor->SetHeading(spawnDirection.z);
|
||||
|
||||
// TODO: Add safe cleanup/despawn/hold behavior once runtime proxy lifecycle
|
||||
// ownership is designed. Stage 2 intentionally leaves the actor in place.
|
||||
g_runtimeProxySpawnSucceeded = true;
|
||||
g_runtimeProxySpawnAttemptInProgress = false;
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Runtime proxy spawn diagnostic succeeded: actorBase={:08X}, handle={:08X}, createdRef={:08X}, resolvedAsActor=true, parentCell={:08X}, position=({:.2f}, {:.2f}, {:.2f}). The placed proxy fallback remains the active remote representation. TODO: add safe runtime proxy cleanup/despawn/hold behavior in a later stage.",
|
||||
a_baseActor->GetFormID(),
|
||||
g_runtimeProxySpawnDiagnosticHandle.get_handle(),
|
||||
spawnedRef->GetFormID(),
|
||||
spawnedCell->GetFormID(),
|
||||
spawnPosition.x,
|
||||
spawnPosition.y,
|
||||
spawnPosition.z));
|
||||
}
|
||||
|
||||
bool HasProxyBaseActorEditorId(const RE::Actor& a_actor)
|
||||
{
|
||||
const auto* baseActor = a_actor.GetNPC();
|
||||
@@ -600,6 +754,10 @@ namespace
|
||||
return RE::BSContainer::ForEachResult::kContinue;
|
||||
}
|
||||
|
||||
if (IsRuntimeProxySpawnDiagnosticRef(*a_ref)) {
|
||||
return RE::BSContainer::ForEachResult::kContinue;
|
||||
}
|
||||
|
||||
auto* actor = a_ref->As<RE::Actor>();
|
||||
if (!actor || !IsValidProxyActor(*actor, a_currentCell, a_player)) {
|
||||
return RE::BSContainer::ForEachResult::kContinue;
|
||||
@@ -916,9 +1074,10 @@ namespace F4T::ProxyActorController
|
||||
return;
|
||||
}
|
||||
|
||||
// Stage 1 runtime-spawn diagnostic only. Run before placed-proxy lookup and
|
||||
// before remote-player selection so it is not hidden behind visual fallback state.
|
||||
(void)ResolveProxyActorBase();
|
||||
// Runtime spawn diagnostics run before placed-proxy lookup and before
|
||||
// remote-player selection so failures are visible even when the fallback works.
|
||||
auto* runtimeProxyActorBase = ResolveProxyActorBase();
|
||||
TrySpawnRuntimeProxyDiagnostic(runtimeProxyActorBase, *parentCell, *player);
|
||||
|
||||
if constexpr (kProxyMovementMode == ProxyMovementMode::kSafetyOffsetTest) {
|
||||
auto* proxy = ResolveProxy(*parentCell, *player);
|
||||
|
||||
Reference in New Issue
Block a user