Hold proxy in hidden position when no remote
Add a temporary in-cell holding fallback for the single placed proxy actor when no valid same-cell remote player is available. Introduces kProxyHiddenHoldingPosition, ProxyLifecycleState, RemotePlayerSelection, MoveProxyToHoldingPosition and RestoreProxyForRemotePlayer, plus selection/state-tracking and throttled logs. The controller now moves the proxy to the hidden position on disconnect, disappearance, or cell-mismatch and snaps it back when a valid same-cell remote appears. Documentation and dev log updated to describe the lifecycle and rationale (docs/architecture.md, docs/dev-log.md, protocol/player-sync.md).
This commit is contained in:
@@ -24,10 +24,16 @@ namespace
|
||||
constexpr RE::TESFormID kProxyFallbackLocalFormId = 0x0020A2;
|
||||
constexpr auto kProxyOffsetX = 150.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
|
||||
// is validated in-game.
|
||||
constexpr RE::NiPoint3 kProxyHiddenHoldingPosition{ -100000.0F, -100000.0F, -100000.0F };
|
||||
constexpr auto kSafetyOffsetMovementInterval = 200ms;
|
||||
constexpr auto kWarningLogInterval = 5s;
|
||||
constexpr auto kNotInTestCellLogInterval = 30s;
|
||||
|
||||
using RemotePlayer = F4T::RemotePlayerState::RemotePlayerState;
|
||||
|
||||
enum class ProxyMovementMode
|
||||
{
|
||||
kSafetyOffsetTest,
|
||||
@@ -41,6 +47,22 @@ namespace
|
||||
kFallbackFormId
|
||||
};
|
||||
|
||||
enum class ProxyLifecycleState
|
||||
{
|
||||
kNoRemotePlayer,
|
||||
kRepresentingRemotePlayer,
|
||||
kRemotePlayerDisconnected,
|
||||
kRemotePlayerLeftCell,
|
||||
kProxyHiddenOrIdle
|
||||
};
|
||||
|
||||
struct RemotePlayerSelection
|
||||
{
|
||||
std::optional<RemotePlayer> selectedPlayer;
|
||||
std::optional<std::uint32_t> disappearedPlayerId;
|
||||
bool selectedPlayerChanged{ false };
|
||||
};
|
||||
|
||||
constexpr auto kProxyMovementMode = ProxyMovementMode::kRemotePlayerStateTest;
|
||||
|
||||
RE::ObjectRefHandle g_proxyHandle;
|
||||
@@ -51,13 +73,14 @@ namespace
|
||||
bool g_proxySmoothingEnabledLogged = false;
|
||||
bool g_firstSmoothedMovementLogged = false;
|
||||
auto g_lastSafetyOffsetMovementTime = std::chrono::steady_clock::time_point{};
|
||||
ProxyLifecycleState g_proxyLifecycleState = ProxyLifecycleState::kNoRemotePlayer;
|
||||
std::optional<ProxyLifecycleState> g_proxyHoldReason;
|
||||
std::optional<std::uint32_t> g_proxyHoldPlayerId;
|
||||
std::optional<std::uint32_t> g_representedPlayerId;
|
||||
std::optional<std::uint32_t> g_lastObservedSneakPlayerId;
|
||||
std::optional<bool> g_lastObservedSneakState;
|
||||
std::unordered_map<std::string, std::chrono::steady_clock::time_point> g_lastWarningLogTimes;
|
||||
|
||||
using RemotePlayer = F4T::RemotePlayerState::RemotePlayerState;
|
||||
|
||||
std::string GetLocalPlayerLogMessage(std::string_view a_message)
|
||||
{
|
||||
return F4T::Networking::GetLocalPlayerLogPrefix() + " " + std::string(a_message);
|
||||
@@ -107,6 +130,91 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
void ResetRemoteMovementStateTracking();
|
||||
|
||||
bool IsProxyHeldFor(ProxyLifecycleState a_reason, std::optional<std::uint32_t> a_playerId)
|
||||
{
|
||||
return g_proxyLifecycleState == ProxyLifecycleState::kProxyHiddenOrIdle &&
|
||||
g_proxyHoldReason == a_reason &&
|
||||
g_proxyHoldPlayerId == a_playerId;
|
||||
}
|
||||
|
||||
void LogProxyStillHeld(ProxyLifecycleState a_reason, std::optional<std::uint32_t> a_playerId)
|
||||
{
|
||||
switch (a_reason) {
|
||||
case ProxyLifecycleState::kNoRemotePlayer:
|
||||
LogThrottledInfo(
|
||||
"proxy_held_no_remote_player",
|
||||
"No suitable remote player is available; proxy actor is already at the holding position.");
|
||||
break;
|
||||
case ProxyLifecycleState::kRemotePlayerDisconnected:
|
||||
if (a_playerId) {
|
||||
LogThrottledInfo(
|
||||
"proxy_held_disconnected_" + std::to_string(*a_playerId),
|
||||
std::format(
|
||||
"Remote player {} is still absent from state; proxy actor remains at the holding position.",
|
||||
*a_playerId));
|
||||
}
|
||||
break;
|
||||
case ProxyLifecycleState::kRemotePlayerLeftCell:
|
||||
if (a_playerId) {
|
||||
LogThrottledInfo(
|
||||
"proxy_held_left_cell_" + std::to_string(*a_playerId),
|
||||
std::format(
|
||||
"Remote player {} is still outside the local test cell; proxy actor remains at the holding position.",
|
||||
*a_playerId));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MoveProxyToHoldingPosition(
|
||||
RE::Actor& a_proxy,
|
||||
ProxyLifecycleState a_reason,
|
||||
std::optional<std::uint32_t> a_playerId = std::nullopt)
|
||||
{
|
||||
if (IsProxyHeldFor(a_reason, a_playerId)) {
|
||||
LogProxyStillHeld(a_reason, a_playerId);
|
||||
return;
|
||||
}
|
||||
|
||||
a_proxy.SetPosition(kProxyHiddenHoldingPosition, true);
|
||||
g_proxyLifecycleState = ProxyLifecycleState::kProxyHiddenOrIdle;
|
||||
g_proxyHoldReason = a_reason;
|
||||
g_proxyHoldPlayerId = a_playerId;
|
||||
g_remoteStateMovedLogged = false;
|
||||
g_firstSmoothedMovementLogged = false;
|
||||
ResetRemoteMovementStateTracking();
|
||||
|
||||
switch (a_reason) {
|
||||
case ProxyLifecycleState::kNoRemotePlayer:
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"No suitable remote player is available; moving proxy actor {} to holding position.",
|
||||
kProxyEditorId));
|
||||
break;
|
||||
case ProxyLifecycleState::kRemotePlayerDisconnected:
|
||||
if (a_playerId) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Remote player {} disappeared; moving proxy actor {} to holding position.",
|
||||
*a_playerId,
|
||||
kProxyEditorId));
|
||||
}
|
||||
break;
|
||||
case ProxyLifecycleState::kRemotePlayerLeftCell:
|
||||
if (a_playerId) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Remote player {} is not in the same cell; moving proxy actor {} to holding position.",
|
||||
*a_playerId,
|
||||
kProxyEditorId));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
float Lerp(float a_current, float a_target, float a_alpha)
|
||||
{
|
||||
return a_current + ((a_target - a_current) * a_alpha);
|
||||
@@ -413,8 +521,10 @@ namespace
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<RemotePlayer> SelectRemotePlayerToRepresent(std::vector<RemotePlayer> a_remotePlayers)
|
||||
RemotePlayerSelection SelectRemotePlayerToRepresent(std::vector<RemotePlayer> a_remotePlayers)
|
||||
{
|
||||
RemotePlayerSelection selection;
|
||||
|
||||
if (g_representedPlayerId) {
|
||||
const auto representedPlayerStillExists = std::ranges::any_of(
|
||||
a_remotePlayers,
|
||||
@@ -422,9 +532,7 @@ namespace
|
||||
return a_remotePlayer.playerId == *g_representedPlayerId;
|
||||
});
|
||||
if (!representedPlayerStillExists) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Represented remote player {} disconnected or disappeared from state; leaving proxy actor idle at its last position. TODO: hide or disable the proxy actor after disconnect.",
|
||||
*g_representedPlayerId));
|
||||
selection.disappearedPlayerId = g_representedPlayerId;
|
||||
g_representedPlayerId.reset();
|
||||
g_remoteStateMovedLogged = false;
|
||||
g_firstSmoothedMovementLogged = false;
|
||||
@@ -433,10 +541,7 @@ namespace
|
||||
}
|
||||
|
||||
if (a_remotePlayers.empty()) {
|
||||
LogThrottledInfo(
|
||||
"no_remote_players",
|
||||
"No suitable remote player is available for proxy movement; leaving proxy actor idle.");
|
||||
return std::nullopt;
|
||||
return selection;
|
||||
}
|
||||
|
||||
auto selectedPlayer = std::ranges::min_element(
|
||||
@@ -445,7 +550,7 @@ namespace
|
||||
return a_left.playerId < a_right.playerId;
|
||||
});
|
||||
if (selectedPlayer == a_remotePlayers.end()) {
|
||||
return std::nullopt;
|
||||
return selection;
|
||||
}
|
||||
|
||||
if (a_remotePlayers.size() > 1) {
|
||||
@@ -457,6 +562,7 @@ namespace
|
||||
}
|
||||
|
||||
if (g_representedPlayerId != selectedPlayer->playerId) {
|
||||
selection.selectedPlayerChanged = true;
|
||||
g_representedPlayerId = selectedPlayer->playerId;
|
||||
g_remoteStateMovedLogged = false;
|
||||
g_firstSmoothedMovementLogged = false;
|
||||
@@ -471,13 +577,10 @@ namespace
|
||||
kProxyPositionLerpAlpha));
|
||||
g_proxySmoothingEnabledLogged = true;
|
||||
}
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Proxy actor {} is representing remote player {}.",
|
||||
kProxyEditorId,
|
||||
selectedPlayer->playerId));
|
||||
}
|
||||
|
||||
return *selectedPlayer;
|
||||
selection.selectedPlayer = *selectedPlayer;
|
||||
return selection;
|
||||
}
|
||||
|
||||
void ApplyRemoteMovementStateToProxy(RE::Actor& a_proxy, const RemotePlayer& a_remotePlayer)
|
||||
@@ -527,6 +630,28 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
void RestoreProxyForRemotePlayer(RE::Actor& a_proxy, const RemotePlayer& a_remotePlayer)
|
||||
{
|
||||
RE::NiPoint3 targetPosition{};
|
||||
targetPosition.x = a_remotePlayer.x;
|
||||
targetPosition.y = a_remotePlayer.y;
|
||||
targetPosition.z = a_remotePlayer.z;
|
||||
|
||||
a_proxy.SetPosition(targetPosition, true);
|
||||
a_proxy.SetHeading(a_remotePlayer.angleZ);
|
||||
g_proxyLifecycleState = ProxyLifecycleState::kRepresentingRemotePlayer;
|
||||
g_proxyHoldReason.reset();
|
||||
g_proxyHoldPlayerId.reset();
|
||||
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Proxy actor {} resumed representing remote player {} at X={:.2f}, Y={:.2f}, Z={:.2f}.",
|
||||
kProxyEditorId,
|
||||
a_remotePlayer.playerId,
|
||||
a_remotePlayer.x,
|
||||
a_remotePlayer.y,
|
||||
a_remotePlayer.z));
|
||||
}
|
||||
|
||||
void MoveProxyToRemotePlayer(RE::Actor& a_proxy, const RemotePlayer& a_remotePlayer)
|
||||
{
|
||||
// TODO: Later animation/behavior milestones can read movement state from
|
||||
@@ -614,28 +739,47 @@ namespace F4T::ProxyActorController
|
||||
return;
|
||||
}
|
||||
|
||||
auto selectedRemotePlayer = SelectRemotePlayerToRepresent(F4T::RemotePlayerState::GetRemotePlayerSnapshot());
|
||||
if (!selectedRemotePlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* proxy = ResolveProxy(*parentCell, *player);
|
||||
if (!proxy) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsRemotePlayerInSameLocation(*selectedRemotePlayer, *player, *parentCell)) {
|
||||
LogThrottledInfo(
|
||||
"proxy_idle_cell_mismatch_" + std::to_string(selectedRemotePlayer->playerId),
|
||||
std::format(
|
||||
"Proxy actor {} is idle because represented remote player {} is not in the same cell. TODO: hide or disable the proxy actor after cell leave.",
|
||||
kProxyEditorId,
|
||||
selectedRemotePlayer->playerId));
|
||||
const auto selection = SelectRemotePlayerToRepresent(F4T::RemotePlayerState::GetRemotePlayerSnapshot());
|
||||
if (selection.disappearedPlayerId) {
|
||||
MoveProxyToHoldingPosition(
|
||||
*proxy,
|
||||
ProxyLifecycleState::kRemotePlayerDisconnected,
|
||||
selection.disappearedPlayerId);
|
||||
}
|
||||
|
||||
if (!selection.selectedPlayer) {
|
||||
if (!selection.disappearedPlayerId) {
|
||||
if (g_proxyLifecycleState == ProxyLifecycleState::kProxyHiddenOrIdle) {
|
||||
LogProxyStillHeld(
|
||||
g_proxyHoldReason.value_or(ProxyLifecycleState::kNoRemotePlayer),
|
||||
g_proxyHoldPlayerId);
|
||||
} else {
|
||||
MoveProxyToHoldingPosition(*proxy, ProxyLifecycleState::kNoRemotePlayer);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyRemoteMovementStateToProxy(*proxy, *selectedRemotePlayer);
|
||||
MoveProxyToRemotePlayer(*proxy, *selectedRemotePlayer);
|
||||
if (!IsRemotePlayerInSameLocation(*selection.selectedPlayer, *player, *parentCell)) {
|
||||
MoveProxyToHoldingPosition(
|
||||
*proxy,
|
||||
ProxyLifecycleState::kRemotePlayerLeftCell,
|
||||
selection.selectedPlayer->playerId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.selectedPlayerChanged || g_proxyLifecycleState != ProxyLifecycleState::kRepresentingRemotePlayer) {
|
||||
RestoreProxyForRemotePlayer(*proxy, *selection.selectedPlayer);
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyRemoteMovementStateToProxy(*proxy, *selection.selectedPlayer);
|
||||
MoveProxyToRemotePlayer(*proxy, *selection.selectedPlayer);
|
||||
}
|
||||
|
||||
void UpdateSafetyTest()
|
||||
|
||||
Reference in New Issue
Block a user