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:
+12
-3
@@ -34,8 +34,13 @@ both send and receive paths:
|
||||
test cell, the game-thread proxy controller moves the single placed proxy to
|
||||
a hidden holding position inside `F4TTestCell01` until a valid same-cell
|
||||
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
|
||||
|
||||
@@ -54,10 +59,13 @@ The native plugin is responsible for:
|
||||
thread
|
||||
- Holding that proxy actor at a hidden in-cell position when no valid same-cell
|
||||
remote player should be represented
|
||||
- Logging staged runtime proxy actor base lookup diagnostics on the same
|
||||
game-thread controller path
|
||||
|
||||
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
|
||||
|
||||
@@ -108,6 +116,7 @@ Game update logs local movement around 1 Hz
|
||||
Networking thread receives packets
|
||||
Networking thread stores remote player state
|
||||
Game update reads a copied remote player snapshot
|
||||
Game update resolves/logs proxy actor base diagnostics
|
||||
Game update moves the placed test proxy actor
|
||||
```
|
||||
|
||||
@@ -118,7 +127,7 @@ update path.
|
||||
|
||||
## Out Of Scope For Current Prototype
|
||||
|
||||
- Remote actor spawning
|
||||
- Runtime actor spawning
|
||||
- Multiple proxy actors
|
||||
- Animation graph sync or animation application
|
||||
- Combat sync
|
||||
|
||||
@@ -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
|
||||
|
||||
Use this format for future updates:
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user