Throttle transform sends and smooth proxy movement
Separate normal transform send cadence from movement logging and improve proxy smoothing. Key changes: - Polling/send logic (plugin/src/main.cpp): introduced distinct send vs log intervals (100ms send, 1s log), lowered send thresholds (≈3.0 units position, 0.02 rad rotation), renamed/clarified functions, and added special movement types (worldspace_change, cell_change, teleport) that bypass the send gate and are logged immediately. Normal movement targets roughly 10 Hz while moving to avoid packet-per-frame traffic. - Proxy controller (plugin/src/F4TProxyActorController.cpp): tuned proxy position lerp alpha to 0.15 for smoother motion, renamed movement-interval variables for clarity, and removed the generic per-update movement gate so normal proxy visual updates are applied each game update while only the safety-offset path is throttled. - Documentation updates (docs/architecture.md, docs/dev-log.md, protocol/player-sync.md): describe the new send/log separation, cadence and thresholds, smoothing behavior, and recorded dev-log about smoothing tests and results. These changes reduce network noise, produce smoother remote visuals, and keep readable local movement logs while preserving immediate sends for large/categorical movement changes.
This commit is contained in:
+10
-4
@@ -13,7 +13,8 @@ both send and receive paths:
|
|||||||
|
|
||||||
1. The Fallout 4 plugin reads local player transform data.
|
1. The Fallout 4 plugin reads local player transform data.
|
||||||
2. The plugin sends transform packets to the local Python server on
|
2. The plugin sends transform packets to the local Python server on
|
||||||
`127.0.0.1:7777`.
|
`127.0.0.1:7777`. Normal movement sends are throttled separately from
|
||||||
|
movement logs, targeting roughly 10 Hz while the player is moving.
|
||||||
3. The server assigns `playerId` values and sends `welcome` packets.
|
3. The server assigns `playerId` values and sends `welcome` packets.
|
||||||
4. The plugin receives its `welcome` packet and stores its assigned `playerId`.
|
4. The plugin receives its `welcome` packet and stores its assigned `playerId`.
|
||||||
5. The server adds `playerId` and `serverTime` to transform packets.
|
5. The server adds `playerId` and `serverTime` to transform packets.
|
||||||
@@ -23,9 +24,10 @@ both send and receive paths:
|
|||||||
9. The plugin and fake client remove disconnected players from their remote
|
9. The plugin and fake client remove disconnected players from their remote
|
||||||
player tables.
|
player tables.
|
||||||
10. On the game-thread update path, the plugin reads a copied remote-player
|
10. On the game-thread update path, the plugin reads a copied remote-player
|
||||||
snapshot and moves the single placed proxy actor in `F4TTestCell01`,
|
snapshot and moves the single placed proxy actor in `F4TTestCell01`.
|
||||||
smoothing normal movement and snapping cell changes, worldspace changes, and
|
Proxy visual updates are independent from local movement log throttling:
|
||||||
teleports.
|
normal movement is smoothed toward the latest target, while cell changes,
|
||||||
|
worldspace changes, and teleports snap directly.
|
||||||
|
|
||||||
The Fallout 4 plugin still does not dynamically spawn remote actors.
|
The Fallout 4 plugin still does not dynamically spawn remote actors.
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@ The native plugin is responsible for:
|
|||||||
- Loading into Fallout 4
|
- Loading into Fallout 4
|
||||||
- Reading local player state
|
- Reading local player state
|
||||||
- Sending local player data to the server
|
- Sending local player data to the server
|
||||||
|
- Throttling normal transform sends separately from readable local movement logs
|
||||||
- Receiving server packets on a background thread
|
- Receiving server packets on a background thread
|
||||||
- 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
|
||||||
@@ -89,6 +92,9 @@ Networking should run separately from game update logic.
|
|||||||
Current approach:
|
Current approach:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
Game update reads local player transform
|
||||||
|
Game update sends thresholded transform packets around 10 Hz while moving
|
||||||
|
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
|
||||||
|
|||||||
+93
-31
@@ -544,35 +544,7 @@ Player position: X=2048.00, Y=2048.00, Z=0.00, AngleZ=0.00
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Entry Template
|
## 2026-05-31 - Placed Proxy Actor Control
|
||||||
|
|
||||||
Use this format for future updates:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## YYYY-MM-DD - Milestone Title
|
|
||||||
|
|
||||||
### What Changed
|
|
||||||
|
|
||||||
- ...
|
|
||||||
|
|
||||||
### What Worked
|
|
||||||
|
|
||||||
- ...
|
|
||||||
|
|
||||||
### What Broke
|
|
||||||
|
|
||||||
- ...
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
|
|
||||||
- ...
|
|
||||||
|
|
||||||
### Next Steps
|
|
||||||
|
|
||||||
- ...
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2026-05-31
|
|
||||||
|
|
||||||
### What Changed
|
### What Changed
|
||||||
|
|
||||||
@@ -608,7 +580,9 @@ Use this format for future updates:
|
|||||||
- Snap movement first.
|
- Snap movement first.
|
||||||
- Add smoothing/interpolation later.
|
- Add smoothing/interpolation later.
|
||||||
|
|
||||||
## 2026-05-31
|
---
|
||||||
|
|
||||||
|
## 2026-05-31 - Remote-State Proxy Movement
|
||||||
|
|
||||||
### What Changed
|
### What Changed
|
||||||
|
|
||||||
@@ -644,7 +618,9 @@ Use this format for future updates:
|
|||||||
- Build the plugin with `xmake build`.
|
- Build the plugin with `xmake build`.
|
||||||
- Run the single-client, two-client, and disconnect tests in `F4TTestCell01`.
|
- Run the single-client, two-client, and disconnect tests in `F4TTestCell01`.
|
||||||
|
|
||||||
## 2026-06-02
|
---
|
||||||
|
|
||||||
|
## 2026-06-02 - Remote Proxy Smoothing
|
||||||
|
|
||||||
### What Changed
|
### What Changed
|
||||||
|
|
||||||
@@ -682,3 +658,89 @@ Use this format for future updates:
|
|||||||
- Run the single-client, two-client, special-movement, disconnect, and cell-leave
|
- Run the single-client, two-client, special-movement, disconnect, and cell-leave
|
||||||
tests in `F4TTestCell01`.
|
tests in `F4TTestCell01`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-06-02 - Smooth Remote Proxy Movement Tested
|
||||||
|
|
||||||
|
### What Changed
|
||||||
|
|
||||||
|
- Separated normal local transform send cadence from local movement log cadence.
|
||||||
|
- Lowered normal transform send thresholds to support smoother remote motion:
|
||||||
|
about 3 game units of position change or 0.02 radians of rotation change.
|
||||||
|
- Targeted normal transform sends at roughly 10 Hz while moving, without sending
|
||||||
|
every frame.
|
||||||
|
- Kept local movement logs readable at the slower debug interval.
|
||||||
|
- Kept `cell_change`, `worldspace_change`, and `teleport` sends immediate and
|
||||||
|
baseline-updating.
|
||||||
|
- Removed the 200 ms throttle from remote-player proxy visual movement on the
|
||||||
|
game-thread update path.
|
||||||
|
- Tuned normal proxy position smoothing to a per-update lerp alpha near 0.15.
|
||||||
|
- Preserved direct snapping for special remote movement types and kept rotation
|
||||||
|
snapping through the existing safe heading path.
|
||||||
|
|
||||||
|
### What Worked
|
||||||
|
|
||||||
|
- The networking receive thread remains actor-free and still only updates plain
|
||||||
|
remote-player state.
|
||||||
|
- The single placed proxy actor remains the only remote visual representation;
|
||||||
|
dynamic spawning and multiple proxy actors are still out of scope.
|
||||||
|
- Two Fallout 4 clients can still connect to the local server.
|
||||||
|
- Server transform relay still works.
|
||||||
|
- Remote player state still updates correctly.
|
||||||
|
- The proxy actor now moves smoothly compared to the previous snapping/jittery
|
||||||
|
version.
|
||||||
|
- The current visual result is close to feeling correct, with animation sync now
|
||||||
|
being the obvious missing piece.
|
||||||
|
|
||||||
|
### What Broke
|
||||||
|
|
||||||
|
- Nothing recorded.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- `server/server.py` and `server/fake_client.py` were intentionally left
|
||||||
|
unchanged because the transform packet protocol did not need to change.
|
||||||
|
- This is the first smooth visible multiplayer prototype for Fallout 4
|
||||||
|
Together.
|
||||||
|
- The proxy actor still lacks synced animations.
|
||||||
|
- Only one proxy actor is supported.
|
||||||
|
- Dynamic spawning is not implemented.
|
||||||
|
- Combat, inventory, quest, settlement, interaction, and animation sync remain
|
||||||
|
out of scope for this milestone.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- Add basic movement state sync.
|
||||||
|
- Start with simple animation-related states like idle, walking, running,
|
||||||
|
sprinting, crouching, jumping, and weapon drawn.
|
||||||
|
- Keep animation sync separate from combat and inventory sync.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entry Template
|
||||||
|
|
||||||
|
Use this format for future updates:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## YYYY-MM-DD - Milestone Title
|
||||||
|
|
||||||
|
### What Changed
|
||||||
|
|
||||||
|
- ...
|
||||||
|
|
||||||
|
### What Worked
|
||||||
|
|
||||||
|
- ...
|
||||||
|
|
||||||
|
### What Broke
|
||||||
|
|
||||||
|
- ...
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- ...
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- ...
|
||||||
|
```
|
||||||
@@ -23,8 +23,8 @@ namespace
|
|||||||
constexpr auto kTestPluginName = "Fallout4Together_Test.esp";
|
constexpr auto kTestPluginName = "Fallout4Together_Test.esp";
|
||||||
constexpr RE::TESFormID kProxyFallbackLocalFormId = 0x0020A2;
|
constexpr RE::TESFormID kProxyFallbackLocalFormId = 0x0020A2;
|
||||||
constexpr auto kProxyOffsetX = 150.0F;
|
constexpr auto kProxyOffsetX = 150.0F;
|
||||||
constexpr float kProxyPositionLerpAlpha = 0.25F;
|
constexpr float kProxyPositionLerpAlpha = 0.15F;
|
||||||
constexpr auto kMovementInterval = 200ms;
|
constexpr auto kSafetyOffsetMovementInterval = 200ms;
|
||||||
constexpr auto kWarningLogInterval = 5s;
|
constexpr auto kWarningLogInterval = 5s;
|
||||||
constexpr auto kNotInTestCellLogInterval = 30s;
|
constexpr auto kNotInTestCellLogInterval = 30s;
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ namespace
|
|||||||
bool g_remoteStateMovedLogged = false;
|
bool g_remoteStateMovedLogged = false;
|
||||||
bool g_proxySmoothingEnabledLogged = false;
|
bool g_proxySmoothingEnabledLogged = false;
|
||||||
bool g_firstSmoothedMovementLogged = false;
|
bool g_firstSmoothedMovementLogged = false;
|
||||||
auto g_lastMovementTime = std::chrono::steady_clock::time_point{};
|
auto g_lastSafetyOffsetMovementTime = std::chrono::steady_clock::time_point{};
|
||||||
std::optional<std::uint32_t> g_representedPlayerId;
|
std::optional<std::uint32_t> g_representedPlayerId;
|
||||||
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;
|
||||||
|
|
||||||
@@ -393,14 +393,15 @@ namespace
|
|||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ShouldMoveProxy()
|
bool ShouldMoveSafetyOffsetProxy()
|
||||||
{
|
{
|
||||||
const auto now = std::chrono::steady_clock::now();
|
const auto now = std::chrono::steady_clock::now();
|
||||||
if (g_lastMovementTime.time_since_epoch().count() != 0 && now - g_lastMovementTime < kMovementInterval) {
|
if (g_lastSafetyOffsetMovementTime.time_since_epoch().count() != 0 &&
|
||||||
|
now - g_lastSafetyOffsetMovementTime < kSafetyOffsetMovementInterval) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
g_lastMovementTime = now;
|
g_lastSafetyOffsetMovementTime = now;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,7 +564,7 @@ namespace F4T::ProxyActorController
|
|||||||
|
|
||||||
if constexpr (kProxyMovementMode == ProxyMovementMode::kSafetyOffsetTest) {
|
if constexpr (kProxyMovementMode == ProxyMovementMode::kSafetyOffsetTest) {
|
||||||
auto* proxy = ResolveProxy(*parentCell, *player);
|
auto* proxy = ResolveProxy(*parentCell, *player);
|
||||||
if (!proxy || !ShouldMoveProxy()) {
|
if (!proxy || !ShouldMoveSafetyOffsetProxy()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,7 +578,7 @@ namespace F4T::ProxyActorController
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto* proxy = ResolveProxy(*parentCell, *player);
|
auto* proxy = ResolveProxy(*parentCell, *player);
|
||||||
if (!proxy || !ShouldMoveProxy()) {
|
if (!proxy) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+62
-34
@@ -24,10 +24,10 @@ namespace
|
|||||||
std::uint32_t worldspaceId;
|
std::uint32_t worldspaceId;
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr auto kPositionLogThreshold = 10.0F;
|
constexpr auto kTransformSendInterval = 100ms;
|
||||||
constexpr auto kRotationLogThresholdDegrees = 1.0F;
|
constexpr auto kTransformLogInterval = 1s;
|
||||||
constexpr auto kRotationLogThresholdRadians = kRotationLogThresholdDegrees * 3.14159265358979323846F / 180.0F;
|
constexpr auto kTransformSendPositionThreshold = 3.0F;
|
||||||
constexpr auto kMinimumLogInterval = 1s;
|
constexpr auto kTransformSendRotationThreshold = 0.02F;
|
||||||
constexpr auto kTeleportDistanceThreshold = 5000.0F;
|
constexpr auto kTeleportDistanceThreshold = 5000.0F;
|
||||||
|
|
||||||
std::string GetLocalPlayerLogMessage(std::string_view a_message)
|
std::string GetLocalPlayerLogMessage(std::string_view a_message)
|
||||||
@@ -99,7 +99,7 @@ namespace
|
|||||||
return (deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ);
|
return (deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HasTransformChanged(const PlayerTransform& a_previous, const PlayerTransform& a_current)
|
bool HasMeaningfulTransformDelta(const PlayerTransform& a_previous, const PlayerTransform& a_current)
|
||||||
{
|
{
|
||||||
auto deltaAngleZ = a_current.angleZ - a_previous.angleZ;
|
auto deltaAngleZ = a_current.angleZ - a_previous.angleZ;
|
||||||
|
|
||||||
@@ -113,10 +113,10 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto squaredPositionDelta = GetSquaredPositionDistance(a_previous, a_current);
|
const auto squaredPositionDelta = GetSquaredPositionDistance(a_previous, a_current);
|
||||||
constexpr auto squaredPositionThreshold = kPositionLogThreshold * kPositionLogThreshold;
|
constexpr auto squaredPositionThreshold = kTransformSendPositionThreshold * kTransformSendPositionThreshold;
|
||||||
|
|
||||||
return squaredPositionDelta > squaredPositionThreshold ||
|
return squaredPositionDelta > squaredPositionThreshold ||
|
||||||
deltaAngleZ > kRotationLogThresholdRadians || deltaAngleZ < -kRotationLogThresholdRadians;
|
deltaAngleZ > kTransformSendRotationThreshold || deltaAngleZ < -kTransformSendRotationThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HasCellChanged(const PlayerLocation& a_previous, const PlayerLocation& a_current)
|
bool HasCellChanged(const PlayerLocation& a_previous, const PlayerLocation& a_current)
|
||||||
@@ -146,7 +146,37 @@ namespace
|
|||||||
a_transform.angleZ));
|
a_transform.angleZ));
|
||||||
}
|
}
|
||||||
|
|
||||||
void CheckAndLogPlayerPositionChange()
|
const char* GetSpecialMovementType(
|
||||||
|
const PlayerLocation& a_previousLocation,
|
||||||
|
const PlayerLocation& a_currentLocation,
|
||||||
|
const PlayerTransform& a_previousTransform,
|
||||||
|
const PlayerTransform& a_currentTransform)
|
||||||
|
{
|
||||||
|
if (HasWorldspaceChanged(a_previousLocation, a_currentLocation)) {
|
||||||
|
return "worldspace_change";
|
||||||
|
}
|
||||||
|
if (HasCellChanged(a_previousLocation, a_currentLocation)) {
|
||||||
|
return "cell_change";
|
||||||
|
}
|
||||||
|
if (HasTeleportDistance(a_previousTransform, a_currentTransform)) {
|
||||||
|
return "teleport";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "normal";
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogSpecialMovement(std::string_view a_movementType)
|
||||||
|
{
|
||||||
|
if (a_movementType == "worldspace_change") {
|
||||||
|
LogInfoWithLocalPlayerPrefix("Detected worldspace change. Sending immediate transform update.");
|
||||||
|
} else if (a_movementType == "cell_change") {
|
||||||
|
LogInfoWithLocalPlayerPrefix("Detected cell change. Sending immediate transform update.");
|
||||||
|
} else if (a_movementType == "teleport") {
|
||||||
|
LogInfoWithLocalPlayerPrefix("Detected teleport/large position jump. Sending immediate transform update.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PollLocalPlayerTransform()
|
||||||
{
|
{
|
||||||
const auto* player = TryGetLocalPlayer();
|
const auto* player = TryGetLocalPlayer();
|
||||||
if (!player) {
|
if (!player) {
|
||||||
@@ -161,42 +191,33 @@ namespace
|
|||||||
static bool hasLastTransform = false;
|
static bool hasLastTransform = false;
|
||||||
static PlayerTransform lastSentTransform{};
|
static PlayerTransform lastSentTransform{};
|
||||||
static PlayerLocation lastKnownLocation{};
|
static PlayerLocation lastKnownLocation{};
|
||||||
static auto lastLogTime = std::chrono::steady_clock::now();
|
static auto lastSendTime = std::chrono::steady_clock::time_point{};
|
||||||
|
static auto lastLogTime = std::chrono::steady_clock::time_point{};
|
||||||
|
|
||||||
const auto currentTransform = GetPlayerTransform(*player);
|
const auto currentTransform = GetPlayerTransform(*player);
|
||||||
const auto currentLocation = GetPlayerLocation(*player);
|
const auto currentLocation = GetPlayerLocation(*player);
|
||||||
|
const auto currentTime = std::chrono::steady_clock::now();
|
||||||
if (!hasLastTransform) {
|
if (!hasLastTransform) {
|
||||||
lastSentTransform = currentTransform;
|
lastSentTransform = currentTransform;
|
||||||
lastKnownLocation = currentLocation;
|
lastKnownLocation = currentLocation;
|
||||||
|
lastSendTime = currentTime;
|
||||||
|
lastLogTime = currentTime;
|
||||||
hasLastTransform = true;
|
hasLastTransform = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* movementType = "normal";
|
const auto* movementType =
|
||||||
if (HasWorldspaceChanged(lastKnownLocation, currentLocation)) {
|
GetSpecialMovementType(lastKnownLocation, currentLocation, lastSentTransform, currentTransform);
|
||||||
LogInfoWithLocalPlayerPrefix("Detected worldspace change. Sending immediate transform update.");
|
|
||||||
movementType = "worldspace_change";
|
|
||||||
} else if (HasCellChanged(lastKnownLocation, currentLocation)) {
|
|
||||||
LogInfoWithLocalPlayerPrefix("Detected cell change. Sending immediate transform update.");
|
|
||||||
movementType = "cell_change";
|
|
||||||
} else if (HasTeleportDistance(lastSentTransform, currentTransform)) {
|
|
||||||
LogInfoWithLocalPlayerPrefix("Detected teleport/large position jump. Sending immediate transform update.");
|
|
||||||
movementType = "teleport";
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto isSpecialMovement = movementType != std::string_view{ "normal" };
|
const auto isSpecialMovement = movementType != std::string_view{ "normal" };
|
||||||
if (!isSpecialMovement && !HasTransformChanged(lastSentTransform, currentTransform)) {
|
if (!isSpecialMovement && !HasMeaningfulTransformDelta(lastSentTransform, currentTransform)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto currentTime = std::chrono::steady_clock::now();
|
if (!isSpecialMovement && currentTime - lastSendTime < kTransformSendInterval) {
|
||||||
if (!isSpecialMovement && currentTime - lastLogTime < kMinimumLogInterval) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Player transforms can change by tiny amounts every frame. Throttling keeps the
|
// Normal movement sends more often than logs, but still avoids packet-per-frame noise.
|
||||||
// readout useful for debugging movement without flooding Fallout4Together.log or
|
|
||||||
// turning this first local networking test into a packet every frame.
|
|
||||||
const auto sentTransform = F4T::Networking::SendTransformPacket(
|
const auto sentTransform = F4T::Networking::SendTransformPacket(
|
||||||
currentTransform.x,
|
currentTransform.x,
|
||||||
currentTransform.y,
|
currentTransform.y,
|
||||||
@@ -208,11 +229,18 @@ namespace
|
|||||||
currentLocation.hasWorldspaceId,
|
currentLocation.hasWorldspaceId,
|
||||||
currentLocation.worldspaceId);
|
currentLocation.worldspaceId);
|
||||||
if (sentTransform) {
|
if (sentTransform) {
|
||||||
LogPlayerTransform(currentTransform);
|
lastSentTransform = currentTransform;
|
||||||
|
lastKnownLocation = currentLocation;
|
||||||
|
lastSendTime = currentTime;
|
||||||
|
|
||||||
|
if (isSpecialMovement) {
|
||||||
|
LogSpecialMovement(movementType);
|
||||||
|
lastLogTime = currentTime;
|
||||||
|
} else if (currentTime - lastLogTime >= kTransformLogInterval) {
|
||||||
|
LogPlayerTransform(currentTransform);
|
||||||
|
lastLogTime = currentTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lastSentTransform = currentTransform;
|
|
||||||
lastKnownLocation = currentLocation;
|
|
||||||
lastLogTime = currentTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void StartPlayerPositionPolling()
|
void StartPlayerPositionPolling()
|
||||||
@@ -224,14 +252,14 @@ namespace
|
|||||||
|
|
||||||
const auto* taskInterface = F4SE::GetTaskInterface();
|
const auto* taskInterface = F4SE::GetTaskInterface();
|
||||||
if (!taskInterface) {
|
if (!taskInterface) {
|
||||||
LogWarningWithLocalPlayerPrefix("Failed to get F4SE task interface; player position changes will not be logged.");
|
LogWarningWithLocalPlayerPrefix("Failed to get F4SE task interface; player position changes will not be sent or logged.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The player reference may not exist at plugin load time. A permanent task lets us
|
// The player reference may not exist at plugin load time. A permanent task lets us
|
||||||
// check on the game thread after load, while the logging gate keeps updates readable.
|
// check on the game thread after load, while separate send and log gates keep updates useful.
|
||||||
taskInterface->AddTaskPermanent([]() {
|
taskInterface->AddTaskPermanent([]() {
|
||||||
CheckAndLogPlayerPositionChange();
|
PollLocalPlayerTransform();
|
||||||
F4T::ProxyActorController::Update();
|
F4T::ProxyActorController::Update();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+23
-3
@@ -12,8 +12,11 @@ Plugin Remote Player State
|
|||||||
```
|
```
|
||||||
|
|
||||||
The Fallout 4 plugin currently reads the local player's transform and sends it
|
The Fallout 4 plugin currently reads the local player's transform and sends it
|
||||||
to the local Python server. The server assigns a `playerId`, adds `serverTime`,
|
to the local Python server. Normal movement sends are throttled separately from
|
||||||
and broadcasts transform packets to other connected clients.
|
local movement logs: while the player is moving, the plugin targets roughly
|
||||||
|
10 Hz and skips sends until position or rotation changes meaningfully. The
|
||||||
|
server assigns a `playerId`, adds `serverTime`, and broadcasts transform packets
|
||||||
|
to other connected clients.
|
||||||
|
|
||||||
The Fallout 4 plugin also receives server packets on a background networking
|
The Fallout 4 plugin also receives server packets on a background networking
|
||||||
thread. It stores its assigned `playerId`, stores remote transform state by
|
thread. It stores its assigned `playerId`, stores remote transform state by
|
||||||
@@ -58,6 +61,8 @@ player exists.
|
|||||||
## Implemented
|
## Implemented
|
||||||
|
|
||||||
- The plugin sends local player transform packets.
|
- The plugin sends local player transform packets.
|
||||||
|
- Normal transform sends are independent from movement logging and are
|
||||||
|
thresholded to avoid packet-per-frame traffic.
|
||||||
- The plugin receives `welcome` packets and stores its assigned `playerId`.
|
- The plugin receives `welcome` packets and stores its assigned `playerId`.
|
||||||
- The plugin receives broadcast transform packets.
|
- The plugin receives broadcast transform packets.
|
||||||
- The plugin ignores transform packets for its own assigned `playerId`.
|
- The plugin ignores transform packets for its own assigned `playerId`.
|
||||||
@@ -72,10 +77,25 @@ player exists.
|
|||||||
- The fake client removes disconnected remote players from its state table.
|
- The fake client removes disconnected remote players from its state table.
|
||||||
- The plugin moves the single placed test proxy actor from a thread-safe
|
- The plugin moves the single placed test proxy actor from a thread-safe
|
||||||
remote-player snapshot on the game-thread update path.
|
remote-player snapshot on the game-thread update path.
|
||||||
- Normal proxy movement is smoothed with a simple position lerp, 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.
|
||||||
|
|
||||||
|
## Transform Cadence
|
||||||
|
|
||||||
|
Normal local movement currently uses three separate rates:
|
||||||
|
|
||||||
|
- Network sends target roughly 10 Hz while the player is moving.
|
||||||
|
- Sends are skipped until position changes by about 3 game units or rotation
|
||||||
|
changes by about 0.02 radians.
|
||||||
|
- Local movement logs stay much slower, around once per second, so
|
||||||
|
`Fallout4Together.log` remains readable.
|
||||||
|
|
||||||
|
`cell_change`, `worldspace_change`, and `teleport` movement types bypass the
|
||||||
|
normal send interval and are sent immediately. The receiving plugin still stores
|
||||||
|
remote transform state on the networking thread and moves actors only from the
|
||||||
|
game-thread proxy controller.
|
||||||
|
|
||||||
No Fallout 4 remote actor is dynamically spawned yet.
|
No Fallout 4 remote actor is dynamically spawned yet.
|
||||||
|
|
||||||
## Planned Later
|
## Planned Later
|
||||||
|
|||||||
Reference in New Issue
Block a user