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
+12 -3
View File
@@ -34,8 +34,13 @@ both send and receive paths:
test cell, the game-thread proxy controller moves the single placed proxy to test cell, the game-thread proxy controller moves the single placed proxy to
a hidden holding position inside `F4TTestCell01` until a valid same-cell a hidden holding position inside `F4TTestCell01` until a valid same-cell
remote player is available again. remote player is available again.
13. As the first runtime proxy actor manager stage, the same game-thread proxy
controller also logs whether `Fallout4Together_Test.esp` is loaded and
whether the actor base `F4T_RemotePlayerProxy` resolves by editor ID.
The Fallout 4 plugin still does not dynamically spawn remote actors. The Fallout 4 plugin still does not dynamically spawn remote actors. The actor
base lookup is diagnostic-only and keeps the placed proxy reference as the
visual representation path.
## Main Components ## Main Components
@@ -54,10 +59,13 @@ The native plugin is responsible for:
thread thread
- Holding that proxy actor at a hidden in-cell position when no valid same-cell - Holding that proxy actor at a hidden in-cell position when no valid same-cell
remote player should be represented remote player should be represented
- Logging staged runtime proxy actor base lookup diagnostics on the same
game-thread controller path
Planned later: Planned later:
- Spawning or moving remote actors - Runtime spawning one proxy actor from `F4T_RemotePlayerProxy`
- Supporting one proxy actor per represented remote `playerId`
### External Server ### External Server
@@ -108,6 +116,7 @@ Game update logs local movement around 1 Hz
Networking thread receives packets Networking thread receives packets
Networking thread stores remote player state Networking thread stores remote player state
Game update reads a copied remote player snapshot Game update reads a copied remote player snapshot
Game update resolves/logs proxy actor base diagnostics
Game update moves the placed test proxy actor Game update moves the placed test proxy actor
``` ```
@@ -118,7 +127,7 @@ update path.
## Out Of Scope For Current Prototype ## Out Of Scope For Current Prototype
- Remote actor spawning - Runtime actor spawning
- Multiple proxy actors - Multiple proxy actors
- Animation graph sync or animation application - Animation graph sync or animation application
- Combat sync - Combat sync
+71
View File
@@ -1161,6 +1161,77 @@ snapping without launching multiple Fallout 4 instances.
--- ---
## 2026-06-02 - Runtime Proxy Actor Manager Stage 1
### Summary
Added lookup-only diagnostics for the future runtime proxy actor path. The
plugin now confirms whether `Fallout4Together_Test.esp` is loaded and whether
the actor base `F4T_RemotePlayerProxy` resolves by editor ID or by its confirmed
plugin-local form ID on the existing game-thread proxy controller path.
### Files Changed
- `plugin/src/F4TProxyActorController.cpp`
- `docs/dev-log.md`
- `docs/architecture.md`
### Details
- Added `ResolveProxyActorBase()` as a Stage 1 diagnostic function.
- Added a test ESP loaded check before actor-base lookup diagnostics.
- Tightened the diagnostic call site so it runs in `F4TTestCell01` before placed
proxy fallback lookup and before remote-player selection.
- Made runtime proxy actor-base log messages explicit for diagnostic searches:
plugin loaded, lookup running, lookup resolved, or lookup failed.
- Uses `LookupModByName` as a fallback for the ESP loaded check, matching the
mod lookup path already used by the placed-reference form ID fallback.
- Added editor-ID lookup for `F4T_RemotePlayerProxy` as a `RE::TESNPC` actor
base and validation that the resolved form still has the expected editor ID.
- Added confirmed plugin-local FormID fallback lookup:
`LookupForm<RE::TESNPC>(0x0020A1, "Fallout4Together_Test.esp")`.
- Kept the actor-base local ID `0x0020A1` separate from the placed reference
local ID `0x0020A2`.
- Validates the resolved actor base is usable as the future
`NEW_REFR_DATA::object` source before reporting success.
- Accepts the confirmed `LookupForm<RE::TESNPC>` result even if runtime editor
ID extraction is unavailable or mismatched; editor ID mismatch is logged as a
warning instead of rejecting the actor base.
- Logs fallback diagnostics including runtime form ID, local form ID, form type,
`TESNPC` status, editor ID if available, display name if available, and
whether the form was accepted.
- Logs whether actor-base lookup succeeded through editor ID lookup or
plugin-local FormID fallback.
- Added clear one-shot or throttled logs for test ESP load status, actor-base
lookup success, missing editor ID, or wrong form type.
- Did not call `CreateReferenceAtLocation` and did not spawn runtime actors.
- Left placed proxy lookup, smoothing, special movement snapping, holding
lifecycle, and sneak-state observation unchanged.
- Left networking protocol, Python server code, and fake-client tooling
unchanged.
### Testing
- Ran `xmake build` from `plugin`; it succeeded.
- Pending: launch Fallout 4 through F4SE, load `Fallout4Together_Test.esp`, and
`coc F4TTestCell01`.
- Pending: confirm `Fallout4Together.log` reports the test ESP load status and
`F4T_RemotePlayerProxy` lookup result.
- Pending: confirm the existing placed proxy fallback still represents one fake
client and still holds/restores correctly.
### Known Issues
- Runtime actor spawning is intentionally not implemented in this stage.
### Next Steps
- Run the build and in-game lookup diagnostics.
- After lookup is proven stable, proceed to Stage 2: one game-thread-only runtime
spawn diagnostic in `F4TTestCell01` while keeping the placed reference fallback.
---
## Entry Template ## Entry Template
Use this format for future updates: Use this format for future updates:
+191
View File
@@ -21,6 +21,7 @@ namespace
constexpr auto kProxyEditorId = "F4TProxyRemotePlayer01REF"; constexpr auto kProxyEditorId = "F4TProxyRemotePlayer01REF";
constexpr auto kProxyBaseActorEditorId = "F4T_RemotePlayerProxy"; constexpr auto kProxyBaseActorEditorId = "F4T_RemotePlayerProxy";
constexpr auto kTestPluginName = "Fallout4Together_Test.esp"; constexpr auto kTestPluginName = "Fallout4Together_Test.esp";
constexpr RE::TESFormID kProxyBaseActorLocalFormId = 0x0020A1;
constexpr RE::TESFormID kProxyFallbackLocalFormId = 0x0020A2; constexpr RE::TESFormID kProxyFallbackLocalFormId = 0x0020A2;
constexpr auto kProxyOffsetX = 150.0F; constexpr auto kProxyOffsetX = 150.0F;
constexpr float kProxyPositionLerpAlpha = 0.15F; constexpr float kProxyPositionLerpAlpha = 0.15F;
@@ -67,6 +68,9 @@ namespace
RE::ObjectRefHandle g_proxyHandle; RE::ObjectRefHandle g_proxyHandle;
bool g_proxyResolvedLogged = false; bool g_proxyResolvedLogged = false;
bool g_proxyActorBaseDiagnosticStartedLogged = false;
bool g_testPluginLoadedLogged = false;
bool g_proxyActorBaseResolvedLogged = false;
bool g_safetyOffsetMovedLogged = false; bool g_safetyOffsetMovedLogged = false;
bool g_remoteStateMovementStartedLogged = false; bool g_remoteStateMovementStartedLogged = false;
bool g_remoteStateMovedLogged = false; bool g_remoteStateMovedLogged = false;
@@ -246,6 +250,189 @@ namespace
return editorId && a_editorId == editorId; 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) bool IsTestCell(const RE::TESObjectCELL& a_cell)
{ {
const auto* testCell = RE::TESForm::GetFormByEditorID<RE::TESObjectCELL>(RE::BSFixedString(kTestCellEditorId)); const auto* testCell = RE::TESForm::GetFormByEditorID<RE::TESObjectCELL>(RE::BSFixedString(kTestCellEditorId));
@@ -729,6 +916,10 @@ namespace F4T::ProxyActorController
return; 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) { if constexpr (kProxyMovementMode == ProxyMovementMode::kSafetyOffsetTest) {
auto* proxy = ResolveProxy(*parentCell, *player); auto* proxy = ResolveProxy(*parentCell, *player);
if (!proxy || !ShouldMoveSafetyOffsetProxy()) { if (!proxy || !ShouldMoveSafetyOffsetProxy()) {