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:
@@ -30,6 +30,10 @@ both send and receive paths:
|
|||||||
Proxy visual updates are independent from local movement log throttling:
|
Proxy visual updates are independent from local movement log throttling:
|
||||||
normal movement is smoothed toward the latest target, while cell changes,
|
normal movement is smoothed toward the latest target, while cell changes,
|
||||||
worldspace changes, and teleports snap directly.
|
worldspace changes, and teleports snap directly.
|
||||||
|
12. When the selected remote player is missing, disconnected, or not in the same
|
||||||
|
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.
|
||||||
|
|
||||||
The Fallout 4 plugin still does not dynamically spawn remote actors.
|
The Fallout 4 plugin still does not dynamically spawn remote actors.
|
||||||
|
|
||||||
@@ -48,6 +52,8 @@ The native plugin is responsible for:
|
|||||||
- Storing assigned and remote `playerId` state internally
|
- Storing assigned and remote `playerId` state internally
|
||||||
- Moving the single placed test proxy actor from remote-player state on the game
|
- Moving the single placed test proxy actor from remote-player state on the game
|
||||||
thread
|
thread
|
||||||
|
- Holding that proxy actor at a hidden in-cell position when no valid same-cell
|
||||||
|
remote player should be represented
|
||||||
|
|
||||||
Planned later:
|
Planned later:
|
||||||
|
|
||||||
|
|||||||
@@ -837,6 +837,51 @@ Player position: X=2048.00, Y=2048.00, Z=0.00, AngleZ=0.00
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-06-02 - Proxy Lifecycle Holding
|
||||||
|
|
||||||
|
### What Changed
|
||||||
|
|
||||||
|
- Added controller-local lifecycle state for the single placed proxy actor.
|
||||||
|
- Added a temporary hidden holding position inside `F4TTestCell01` for cases
|
||||||
|
where no valid same-cell remote player should be represented.
|
||||||
|
- Moved the proxy to the holding position when the represented remote player
|
||||||
|
disconnects, disappears from remote-player state, or leaves the local test
|
||||||
|
cell.
|
||||||
|
- Restored representation by snapping the proxy to a valid same-cell remote
|
||||||
|
player once before resuming existing smooth movement.
|
||||||
|
- Kept disconnect, no-remote-player, and cell-mismatch logs transition-based or
|
||||||
|
throttled so the log does not spam every update.
|
||||||
|
|
||||||
|
### What Worked
|
||||||
|
|
||||||
|
- Actor/reference access remains inside `F4TProxyActorController` on the
|
||||||
|
game-thread update path.
|
||||||
|
- The networking protocol, Python server, and fake client remain unchanged.
|
||||||
|
- Existing smooth movement, special movement snapping, and sneak-state
|
||||||
|
observation remain in place for valid same-cell remote players.
|
||||||
|
|
||||||
|
### What Broke
|
||||||
|
|
||||||
|
- Nothing recorded during implementation.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- This milestone deliberately does not call `Disable()`, `Enable()`,
|
||||||
|
`SetAlpha()`, invisibility APIs, or animation graph APIs.
|
||||||
|
- Dynamic spawning, multiple proxy actors, combat, inventory, quest, settlement,
|
||||||
|
weapon/projectile, interaction, and animation sync remain out of scope.
|
||||||
|
- The holding position is a prototype-safe fallback until a hide/disable path is
|
||||||
|
validated for the persistent placed proxy reference.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- Test single-client, two-client, disconnect, cell-leave, and return/reconnect
|
||||||
|
flows in `F4TTestCell01`.
|
||||||
|
- If the holding fallback proves stable, separately validate whether a true
|
||||||
|
hide/disable path is safe for the placed proxy reference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Entry Template
|
## Entry Template
|
||||||
|
|
||||||
Use this format for future updates:
|
Use this format for future updates:
|
||||||
|
|||||||
@@ -24,10 +24,16 @@ namespace
|
|||||||
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;
|
||||||
|
// 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 kSafetyOffsetMovementInterval = 200ms;
|
||||||
constexpr auto kWarningLogInterval = 5s;
|
constexpr auto kWarningLogInterval = 5s;
|
||||||
constexpr auto kNotInTestCellLogInterval = 30s;
|
constexpr auto kNotInTestCellLogInterval = 30s;
|
||||||
|
|
||||||
|
using RemotePlayer = F4T::RemotePlayerState::RemotePlayerState;
|
||||||
|
|
||||||
enum class ProxyMovementMode
|
enum class ProxyMovementMode
|
||||||
{
|
{
|
||||||
kSafetyOffsetTest,
|
kSafetyOffsetTest,
|
||||||
@@ -41,6 +47,22 @@ namespace
|
|||||||
kFallbackFormId
|
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;
|
constexpr auto kProxyMovementMode = ProxyMovementMode::kRemotePlayerStateTest;
|
||||||
|
|
||||||
RE::ObjectRefHandle g_proxyHandle;
|
RE::ObjectRefHandle g_proxyHandle;
|
||||||
@@ -51,13 +73,14 @@ namespace
|
|||||||
bool g_proxySmoothingEnabledLogged = false;
|
bool g_proxySmoothingEnabledLogged = false;
|
||||||
bool g_firstSmoothedMovementLogged = false;
|
bool g_firstSmoothedMovementLogged = false;
|
||||||
auto g_lastSafetyOffsetMovementTime = std::chrono::steady_clock::time_point{};
|
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_representedPlayerId;
|
||||||
std::optional<std::uint32_t> g_lastObservedSneakPlayerId;
|
std::optional<std::uint32_t> g_lastObservedSneakPlayerId;
|
||||||
std::optional<bool> g_lastObservedSneakState;
|
std::optional<bool> g_lastObservedSneakState;
|
||||||
std::unordered_map<std::string, std::chrono::steady_clock::time_point> g_lastWarningLogTimes;
|
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)
|
std::string GetLocalPlayerLogMessage(std::string_view a_message)
|
||||||
{
|
{
|
||||||
return F4T::Networking::GetLocalPlayerLogPrefix() + " " + std::string(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)
|
float Lerp(float a_current, float a_target, float a_alpha)
|
||||||
{
|
{
|
||||||
return a_current + ((a_target - a_current) * a_alpha);
|
return a_current + ((a_target - a_current) * a_alpha);
|
||||||
@@ -413,8 +521,10 @@ namespace
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<RemotePlayer> SelectRemotePlayerToRepresent(std::vector<RemotePlayer> a_remotePlayers)
|
RemotePlayerSelection SelectRemotePlayerToRepresent(std::vector<RemotePlayer> a_remotePlayers)
|
||||||
{
|
{
|
||||||
|
RemotePlayerSelection selection;
|
||||||
|
|
||||||
if (g_representedPlayerId) {
|
if (g_representedPlayerId) {
|
||||||
const auto representedPlayerStillExists = std::ranges::any_of(
|
const auto representedPlayerStillExists = std::ranges::any_of(
|
||||||
a_remotePlayers,
|
a_remotePlayers,
|
||||||
@@ -422,9 +532,7 @@ namespace
|
|||||||
return a_remotePlayer.playerId == *g_representedPlayerId;
|
return a_remotePlayer.playerId == *g_representedPlayerId;
|
||||||
});
|
});
|
||||||
if (!representedPlayerStillExists) {
|
if (!representedPlayerStillExists) {
|
||||||
LogInfoWithLocalPlayerPrefix(std::format(
|
selection.disappearedPlayerId = g_representedPlayerId;
|
||||||
"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));
|
|
||||||
g_representedPlayerId.reset();
|
g_representedPlayerId.reset();
|
||||||
g_remoteStateMovedLogged = false;
|
g_remoteStateMovedLogged = false;
|
||||||
g_firstSmoothedMovementLogged = false;
|
g_firstSmoothedMovementLogged = false;
|
||||||
@@ -433,10 +541,7 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (a_remotePlayers.empty()) {
|
if (a_remotePlayers.empty()) {
|
||||||
LogThrottledInfo(
|
return selection;
|
||||||
"no_remote_players",
|
|
||||||
"No suitable remote player is available for proxy movement; leaving proxy actor idle.");
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto selectedPlayer = std::ranges::min_element(
|
auto selectedPlayer = std::ranges::min_element(
|
||||||
@@ -445,7 +550,7 @@ namespace
|
|||||||
return a_left.playerId < a_right.playerId;
|
return a_left.playerId < a_right.playerId;
|
||||||
});
|
});
|
||||||
if (selectedPlayer == a_remotePlayers.end()) {
|
if (selectedPlayer == a_remotePlayers.end()) {
|
||||||
return std::nullopt;
|
return selection;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a_remotePlayers.size() > 1) {
|
if (a_remotePlayers.size() > 1) {
|
||||||
@@ -457,6 +562,7 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (g_representedPlayerId != selectedPlayer->playerId) {
|
if (g_representedPlayerId != selectedPlayer->playerId) {
|
||||||
|
selection.selectedPlayerChanged = true;
|
||||||
g_representedPlayerId = selectedPlayer->playerId;
|
g_representedPlayerId = selectedPlayer->playerId;
|
||||||
g_remoteStateMovedLogged = false;
|
g_remoteStateMovedLogged = false;
|
||||||
g_firstSmoothedMovementLogged = false;
|
g_firstSmoothedMovementLogged = false;
|
||||||
@@ -471,13 +577,10 @@ namespace
|
|||||||
kProxyPositionLerpAlpha));
|
kProxyPositionLerpAlpha));
|
||||||
g_proxySmoothingEnabledLogged = true;
|
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)
|
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)
|
void MoveProxyToRemotePlayer(RE::Actor& a_proxy, const RemotePlayer& a_remotePlayer)
|
||||||
{
|
{
|
||||||
// TODO: Later animation/behavior milestones can read movement state from
|
// TODO: Later animation/behavior milestones can read movement state from
|
||||||
@@ -614,28 +739,47 @@ namespace F4T::ProxyActorController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto selectedRemotePlayer = SelectRemotePlayerToRepresent(F4T::RemotePlayerState::GetRemotePlayerSnapshot());
|
|
||||||
if (!selectedRemotePlayer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* proxy = ResolveProxy(*parentCell, *player);
|
auto* proxy = ResolveProxy(*parentCell, *player);
|
||||||
if (!proxy) {
|
if (!proxy) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsRemotePlayerInSameLocation(*selectedRemotePlayer, *player, *parentCell)) {
|
const auto selection = SelectRemotePlayerToRepresent(F4T::RemotePlayerState::GetRemotePlayerSnapshot());
|
||||||
LogThrottledInfo(
|
if (selection.disappearedPlayerId) {
|
||||||
"proxy_idle_cell_mismatch_" + std::to_string(selectedRemotePlayer->playerId),
|
MoveProxyToHoldingPosition(
|
||||||
std::format(
|
*proxy,
|
||||||
"Proxy actor {} is idle because represented remote player {} is not in the same cell. TODO: hide or disable the proxy actor after cell leave.",
|
ProxyLifecycleState::kRemotePlayerDisconnected,
|
||||||
kProxyEditorId,
|
selection.disappearedPlayerId);
|
||||||
selectedRemotePlayer->playerId));
|
}
|
||||||
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyRemoteMovementStateToProxy(*proxy, *selectedRemotePlayer);
|
if (!IsRemotePlayerInSameLocation(*selection.selectedPlayer, *player, *parentCell)) {
|
||||||
MoveProxyToRemotePlayer(*proxy, *selectedRemotePlayer);
|
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()
|
void UpdateSafetyTest()
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ lifecycle.
|
|||||||
The plugin's game-thread proxy actor controller can now read a copied snapshot
|
The plugin's game-thread proxy actor controller can now read a copied snapshot
|
||||||
of this remote-player state and move the single placed test proxy actor
|
of this remote-player state and move the single placed test proxy actor
|
||||||
`F4TProxyRemotePlayer01REF` in `F4TTestCell01`. The networking thread still does
|
`F4TProxyRemotePlayer01REF` in `F4TTestCell01`. The networking thread still does
|
||||||
not touch Fallout 4 actors or references.
|
not touch Fallout 4 actors or references. If no valid same-cell remote player is
|
||||||
|
available, the controller moves the placed proxy to a hidden holding position
|
||||||
|
inside the same test cell instead of leaving it frozen at the last represented
|
||||||
|
position.
|
||||||
|
|
||||||
## Current Remote Player State Model
|
## Current Remote Player State Model
|
||||||
|
|
||||||
@@ -89,6 +92,10 @@ player exists.
|
|||||||
- Normal proxy movement is smoothed toward the latest received target, while
|
- Normal proxy movement is smoothed toward the latest received target, while
|
||||||
`cell_change`, `worldspace_change`, and `teleport` transforms snap directly to
|
`cell_change`, `worldspace_change`, and `teleport` transforms snap directly to
|
||||||
avoid slow movement across large discontinuities.
|
avoid slow movement across large discontinuities.
|
||||||
|
- The game-thread proxy controller holds the single proxy at a hidden in-cell
|
||||||
|
position when the represented player disconnects, disappears, or leaves the
|
||||||
|
local test cell, then snaps back to a valid same-cell remote player before
|
||||||
|
resuming smoothing.
|
||||||
- Transform packets now include basic movement state data for future animation
|
- Transform packets now include basic movement state data for future animation
|
||||||
work, but the proxy actor does not apply animations from that state yet.
|
work, but the proxy actor does not apply animations from that state yet.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user