Add runtime proxy actor lookup diagnostics

Introduce Stage 1 runtime lookup diagnostics for the proxy actor base: adds ResolveProxyActorBase and ResolveProxyActorBaseByPluginLocalFormId along with helper checks (IsTestPluginLoaded, IsUsableProxyActorBase, editor/fullname helpers). The code logs plugin load status, editor-ID resolution, and a plugin-local FormID fallback (0x0020A1) while refusing to spawn actors (runtime spawning remains disabled). Adds throttled and one-shot log flags to avoid spam and invokes the diagnostic early on the game-thread before placed-proxy fallback and remote-player selection. Documentation updated (docs/architecture.md and docs/dev-log.md) to record the new diagnostic behavior and next steps.
This commit is contained in:
2026-06-02 16:27:39 +12:00
parent 0e67209ab1
commit df5b0c32cc
3 changed files with 274 additions and 3 deletions
+191
View File
@@ -21,6 +21,7 @@ namespace
constexpr auto kProxyEditorId = "F4TProxyRemotePlayer01REF";
constexpr auto kProxyBaseActorEditorId = "F4T_RemotePlayerProxy";
constexpr auto kTestPluginName = "Fallout4Together_Test.esp";
constexpr RE::TESFormID kProxyBaseActorLocalFormId = 0x0020A1;
constexpr RE::TESFormID kProxyFallbackLocalFormId = 0x0020A2;
constexpr auto kProxyOffsetX = 150.0F;
constexpr float kProxyPositionLerpAlpha = 0.15F;
@@ -67,6 +68,9 @@ namespace
RE::ObjectRefHandle g_proxyHandle;
bool g_proxyResolvedLogged = false;
bool g_proxyActorBaseDiagnosticStartedLogged = false;
bool g_testPluginLoadedLogged = false;
bool g_proxyActorBaseResolvedLogged = false;
bool g_safetyOffsetMovedLogged = false;
bool g_remoteStateMovementStartedLogged = false;
bool g_remoteStateMovedLogged = false;
@@ -246,6 +250,189 @@ namespace
return editorId && a_editorId == editorId;
}
bool IsTestPluginLoaded()
{
auto* dataHandler = RE::TESDataHandler::GetSingleton();
if (!dataHandler) {
LogThrottledWarning(
"test_plugin_data_handler_missing",
std::format(
"Runtime proxy actor base lookup failed: TESDataHandler is unavailable; cannot check {} or resolve {}.",
kTestPluginName,
kProxyBaseActorEditorId));
return false;
}
const auto* loadedMod = dataHandler->LookupLoadedModByName(kTestPluginName);
const auto* mod = loadedMod ? loadedMod : dataHandler->LookupModByName(kTestPluginName);
if (!mod) {
LogThrottledWarning(
"test_plugin_missing",
std::format(
"Runtime proxy actor base lookup failed: {} is not loaded; cannot resolve {}.",
kTestPluginName,
kProxyBaseActorEditorId));
return false;
}
if (!g_testPluginLoadedLogged) {
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy actor base lookup confirmed plugin loaded: {}.",
kTestPluginName));
g_testPluginLoadedLogged = true;
}
return true;
}
bool IsUsableProxyActorBase(const RE::TESNPC& a_baseActor)
{
return a_baseActor.As<RE::TESBoundObject>() != nullptr;
}
std::string GetEditorIdForLog(const RE::TESForm& a_form)
{
const auto* editorId = a_form.GetFormEditorID();
if (!editorId || std::string_view(editorId).empty()) {
return "unavailable";
}
return editorId;
}
std::string GetFullNameForLog(const RE::TESForm& a_form)
{
const auto fullName = RE::TESFullName::GetFullName(a_form);
if (fullName.empty()) {
return "unavailable";
}
return std::string(fullName);
}
RE::TESNPC* ResolveProxyActorBaseByPluginLocalFormId()
{
auto* dataHandler = RE::TESDataHandler::GetSingleton();
if (!dataHandler) {
LogThrottledWarning(
"proxy_actor_base_form_id_data_handler_missing",
std::format(
"Runtime proxy actor base plugin-local FormID fallback failed: TESDataHandler is unavailable for {} local form {:06X}.",
kTestPluginName,
kProxyBaseActorLocalFormId));
return nullptr;
}
auto* baseActor = dataHandler->LookupForm<RE::TESNPC>(kProxyBaseActorLocalFormId, kTestPluginName);
if (!baseActor) {
LogThrottledWarning(
"proxy_actor_base_form_id_missing",
std::format(
"Runtime proxy actor base plugin-local FormID fallback failed: {} local form {:06X} did not resolve as a TESNPC.",
kTestPluginName,
kProxyBaseActorLocalFormId));
return nullptr;
}
if (!IsUsableProxyActorBase(*baseActor)) {
LogThrottledWarning(
"proxy_actor_base_form_id_not_bound_object",
std::format(
"Runtime proxy actor base plugin-local FormID fallback failed: {} local form {:06X} is not usable as NEW_REFR_DATA::object.",
kTestPluginName,
kProxyBaseActorLocalFormId));
return nullptr;
}
const auto editorId = GetEditorIdForLog(*baseActor);
const auto fullName = GetFullNameForLog(*baseActor);
const auto* formType = RE::TESForm::GetFormTypeString(baseActor->GetFormType());
const auto editorIdMatches = editorId == kProxyBaseActorEditorId;
if (!editorIdMatches) {
LogThrottledWarning(
"proxy_actor_base_form_id_editor_id_mismatch_accepted",
std::format(
"Runtime proxy actor base plugin-local FormID fallback editor ID check did not match: expected {}, got {}. Accepting confirmed {} local form {:06X} because it resolved as TESNPC form {:08X}.",
kProxyBaseActorEditorId,
editorId,
kTestPluginName,
kProxyBaseActorLocalFormId,
baseActor->GetFormID()));
}
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.",
kProxyBaseActorEditorId,
kTestPluginName,
kProxyBaseActorLocalFormId,
baseActor->GetFormID(),
formType ? formType : "unavailable",
editorId,
fullName));
g_proxyActorBaseResolvedLogged = true;
}
return baseActor;
}
RE::TESNPC* ResolveProxyActorBase()
{
if (!g_proxyActorBaseDiagnosticStartedLogged) {
LogInfoWithLocalPlayerPrefix(std::format(
"Runtime proxy actor base lookup diagnostic running for {}.",
kProxyBaseActorEditorId));
g_proxyActorBaseDiagnosticStartedLogged = true;
}
if (!IsTestPluginLoaded()) {
return nullptr;
}
auto* baseActor = RE::TESForm::GetFormByEditorID<RE::TESNPC>(RE::BSFixedString(kProxyBaseActorEditorId));
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.",
kProxyBaseActorEditorId,
baseActor->GetFormID()));
g_proxyActorBaseResolvedLogged = true;
}
return baseActor;
}
auto* form = RE::TESForm::GetFormByEditorID(RE::BSFixedString(kProxyBaseActorEditorId));
if (form) {
LogThrottledWarning(
"proxy_actor_base_wrong_type",
std::format(
"Runtime proxy actor base editor ID lookup failed: {} resolved as form {:08X}, but it is not a usable TESNPC actor base; trying plugin-local FormID fallback.",
kProxyBaseActorEditorId,
form->GetFormID()));
} else {
LogThrottledWarning(
"proxy_actor_base_editor_id_missing",
std::format(
"Runtime proxy actor base editor ID lookup failed: {} could not be resolved; trying plugin-local FormID fallback.",
kProxyBaseActorEditorId));
}
baseActor = ResolveProxyActorBaseByPluginLocalFormId();
if (baseActor) {
return baseActor;
}
LogThrottledWarning(
"proxy_actor_base_missing",
std::format(
"Runtime proxy actor base lookup failed: {} was not resolved by editor ID or {} plugin-local FormID {:06X}; runtime spawning remains disabled.",
kProxyBaseActorEditorId,
kTestPluginName,
kProxyBaseActorLocalFormId));
return nullptr;
}
bool IsTestCell(const RE::TESObjectCELL& a_cell)
{
const auto* testCell = RE::TESForm::GetFormByEditorID<RE::TESObjectCELL>(RE::BSFixedString(kTestCellEditorId));
@@ -729,6 +916,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();
if constexpr (kProxyMovementMode == ProxyMovementMode::kSafetyOffsetTest) {
auto* proxy = ResolveProxy(*parentCell, *player);
if (!proxy || !ShouldMoveSafetyOffsetProxy()) {