Add plugin receive loop & remote player state

Add a networking receive loop and in-plugin remote-player storage. Introduces F4TRemotePlayerState (header + implementation) to track assigned playerId and remote player snapshots with thread-safe access. Expands F4TNetworking to start/stop a background receive thread, parse newline-separated JSON packets (welcome, transform, disconnect), validate fields, update remote state, throttle logs, and handle socket/thread synchronization. Add nlohmann_json to xmake and plugin build. Update docs and dev log to reflect the new receive behavior and current prototype scope.
This commit is contained in:
2026-05-31 20:10:41 +12:00
parent d724d7404c
commit 62635f4226
10 changed files with 581 additions and 70 deletions
+19 -20
View File
@@ -3,27 +3,27 @@
## Current Architecture ## Current Architecture
```text ```text
Fallout 4 Plugin Fallout 4 Plugin ↔ Local Python Server ↔ Other Clients
Local Python Server Plugin Remote Player State
Fake Client Remote Player State
``` ```
The current system is one-way from the Fallout 4 plugin to the fake client: The current system is still a local prototype, but the Fallout 4 plugin now has
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`.
3. The server assigns `playerId` values and sends `welcome` packets. 3. The server assigns `playerId` values and sends `welcome` packets.
4. The server adds `playerId` and `serverTime` to transform packets. 4. The plugin receives its `welcome` packet and stores its assigned `playerId`.
5. The server broadcasts transform packets to other connected clients. 5. The server adds `playerId` and `serverTime` to transform packets.
6. `server/fake_client.py` stores remote player state by `playerId`. 6. The server broadcasts transform packets to other connected clients.
7. The server broadcasts `disconnect` packets when clients disconnect. 7. The plugin and `server/fake_client.py` store remote player state by `playerId`.
8. The fake client removes disconnected players from its remote player table. 8. The server broadcasts `disconnect` packets when clients disconnect.
9. The plugin and fake client remove disconnected players from their remote
player tables.
The Fallout 4 plugin does not yet receive remote transforms from the server. The Fallout 4 plugin still does not spawn or move remote actors.
There is no plugin receive loop and no remote actor spawning yet.
## Main Components ## Main Components
@@ -34,11 +34,11 @@ 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
- Receiving server packets on a background thread
- Storing assigned and remote `playerId` state internally
Planned later: Planned later:
- Receiving remote player data
- Storing remote player state inside the plugin
- Applying remote player updates safely - Applying remote player updates safely
- Spawning or moving remote actors - Spawning or moving remote actors
@@ -79,24 +79,23 @@ The Creation Kit side is planned later and may be responsible for:
## Threading Model ## Threading Model
This is planned for the Fallout 4 receiving client. It is not implemented yet.
Networking should run separately from game update logic. Networking should run separately from game update logic.
Recommended approach: Current approach:
```text ```text
Networking thread receives packets Networking thread receives packets
Networking thread stores remote player state Networking thread stores remote player state
Game update reads remote player state Game update reads remote player state
Game update applies actor movement Future game update applies actor movement
``` ```
The networking thread should avoid directly modifying Fallout 4 game objects unless the operation is known to be safe. The networking receive thread does not directly modify Fallout 4 actors or game
objects. It parses newline-separated JSON, updates plain C++ remote-player
state, and leaves actor spawning or movement for a later game-thread milestone.
## Out Of Scope For Current Prototype ## Out Of Scope For Current Prototype
- Fallout 4 receive-loop processing
- Remote actor spawning - Remote actor spawning
- Combat sync - Combat sync
- Quest sync - Quest sync
+33
View File
@@ -483,6 +483,39 @@ Player position: X=2048.00, Y=2048.00, Z=0.00, AngleZ=0.00
--- ---
## 2026-05-31 - Plugin Receive State Implemented
### What Changed
- Added plugin-side receiving for server `welcome`, `transform`, and `disconnect` packets.
- Added internal remote player state storage keyed by server-assigned `playerId`.
- Added defensive packet parsing so malformed or unknown packets are ignored without crashing the plugin.
- Kept remote actor spawning and movement out of scope.
### What Worked
- The plugin now stores its assigned server `playerId`.
- Remote transforms can be stored internally without touching Fallout 4 actors or game objects.
- Disconnect packets remove remote player state.
- `xmake build` succeeds.
### What Broke
- Nothing recorded.
### Notes
- The receive loop runs on a background networking thread.
- Remote transform logging is throttled so `Fallout4Together.log` remains readable.
- Actor spawning, actor movement, animation sync, combat sync, inventory sync, quest sync, and settlement sync remain planned later.
### Next Steps
- Test with Fallout 4 launched through F4SE against `server/server.py`.
- Add a controlled game-thread reader for remote player snapshots before any actor spawning work.
---
## Entry Template ## Entry Template
Use this format for future updates: Use this format for future updates:
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <chrono>
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace F4T::RemotePlayerState
{
struct RemotePlayerState
{
std::uint32_t playerId{};
float x{};
float y{};
float z{};
float angleZ{};
std::string movementType;
std::string cellId;
std::string worldspaceId;
std::optional<double> clientTime;
std::optional<double> serverTime;
std::chrono::steady_clock::time_point lastReceivedLocalTime{};
};
void SetAssignedPlayerId(std::uint32_t a_playerId);
std::optional<std::uint32_t> GetAssignedPlayerId();
bool IsAssignedPlayerId(std::uint32_t a_playerId);
void ClearAssignedPlayerId();
void UpdateRemotePlayer(RemotePlayerState a_state);
bool RemoveRemotePlayer(std::uint32_t a_playerId);
void ClearRemotePlayers();
std::vector<RemotePlayerState> GetRemotePlayerSnapshot();
}
+13 -7
View File
@@ -21,7 +21,8 @@ Expected Install Path: Data/F4SE/Plugins/Fallout4Together.dll
## Current Goal ## Current Goal
The current goal is to build an empty plugin and confirm it loads through F4SE. The current goal is to build the F4SE plugin, confirm it loads through F4SE, and
verify the local networking prototype against `server/server.py`.
This means the plugin should be able to: This means the plugin should be able to:
@@ -30,16 +31,17 @@ This means the plugin should be able to:
* Be copied into `Data/F4SE/Plugins/` * Be copied into `Data/F4SE/Plugins/`
* Load when Fallout 4 is launched through F4SE * Load when Fallout 4 is launched through F4SE
* Create or write to a `Fallout4Together.log` file * Create or write to a `Fallout4Together.log` file
* Connect to the local server on `127.0.0.1:7777`
* Send local transform packets
* Receive `welcome`, `transform`, and `disconnect` packets
* Store remote player state internally without spawning actors
## Not Included Yet ## Not Included Yet
The plugin does not currently include: The plugin does not currently include:
* Networking
* Actor sync * Actor sync
* Player transform sync
* Creation Kit test cell integration * Creation Kit test cell integration
* Server connection
* Remote player spawning * Remote player spawning
* Combat sync * Combat sync
* Quest sync * Quest sync
@@ -103,9 +105,13 @@ Use this checklist for the first plugin test:
* [ ] Game reaches the main menu without crashing * [ ] Game reaches the main menu without crashing
* [ ] Plugin log file is created * [ ] Plugin log file is created
* [ ] Plugin log confirms that the plugin loaded * [ ] Plugin log confirms that the plugin loaded
* [ ] Local server is running before gameplay networking tests
* [ ] Plugin log shows `Assigned server playerId: N`
* [ ] Plugin logs remote player updates from another connected client
* [ ] Plugin logs remote player removal after a disconnect
## Notes ## Notes
Do not start multiplayer networking work until the empty plugin can build and load consistently. Remote actor spawning is intentionally not part of the current networking
prototype. The receive loop stores plain remote-player state first so later
The first real technical milestone is not player sync. The first milestone is simply proving that Fallout 4 can load `Fallout4Together.dll` through F4SE. actor work can be added from a stable data source.
+368 -5
View File
@@ -1,15 +1,26 @@
#include "pch.h" #include "pch.h"
#include "F4TNetworking.h" #include "F4TNetworking.h"
#include "F4TRemotePlayerState.h"
#include <WinSock2.h> #include <WinSock2.h>
#include <WS2tcpip.h> #include <WS2tcpip.h>
#include <array> #include <array>
#include <atomic>
#include <charconv>
#include <chrono> #include <chrono>
#include <cstddef> #include <cstddef>
#include <cstdio> #include <cstdio>
#include <limits>
#include <memory> #include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <unordered_map>
#include <nlohmann/json.hpp>
namespace namespace
{ {
@@ -17,19 +28,37 @@ namespace
constexpr auto kLocalServerPort = 7777; constexpr auto kLocalServerPort = 7777;
constexpr auto kReconnectCooldown = 10s; constexpr auto kReconnectCooldown = 10s;
constexpr auto kConnectTimeoutMilliseconds = 100; constexpr auto kConnectTimeoutMilliseconds = 100;
constexpr auto kReceiveIdleSleep = 25ms;
constexpr auto kRemoteUpdateLogInterval = 1s;
constexpr auto kWarningLogInterval = 2s;
constexpr auto kMaximumReceiveBufferSize = 64uz * 1024uz;
SOCKET g_socket = INVALID_SOCKET; SOCKET g_socket = INVALID_SOCKET;
std::mutex g_socketMutex;
bool g_winsockInitialized = false; bool g_winsockInitialized = false;
auto g_lastConnectAttempt = std::chrono::steady_clock::time_point{}; auto g_lastConnectAttempt = std::chrono::steady_clock::time_point{};
std::atomic_bool g_receiveThreadRunning{ false };
std::thread g_receiveThread;
std::unordered_map<std::uint32_t, std::chrono::steady_clock::time_point> g_lastRemoteUpdateLogTimes;
std::unordered_map<std::string, std::chrono::steady_clock::time_point> g_lastWarningLogTimes;
using Json = nlohmann::json;
void CloseSocket() void CloseSocket()
{ {
const std::scoped_lock lock(g_socketMutex);
if (g_socket != INVALID_SOCKET) { if (g_socket != INVALID_SOCKET) {
closesocket(g_socket); closesocket(g_socket);
g_socket = INVALID_SOCKET; g_socket = INVALID_SOCKET;
} }
} }
SOCKET GetSocket()
{
const std::scoped_lock lock(g_socketMutex);
return g_socket;
}
bool EnsureWinsockInitialized() bool EnsureWinsockInitialized()
{ {
if (g_winsockInitialized) { if (g_winsockInitialized) {
@@ -92,6 +121,321 @@ namespace
g_lastConnectAttempt = now; g_lastConnectAttempt = now;
return true; return true;
} }
bool ShouldLogWarning(std::string a_key)
{
const auto now = std::chrono::steady_clock::now();
const auto lastLog = g_lastWarningLogTimes.find(a_key);
if (lastLog != g_lastWarningLogTimes.end() && now - lastLog->second < kWarningLogInterval) {
return false;
}
g_lastWarningLogTimes[std::move(a_key)] = now;
return true;
}
void LogThrottledWarning(const std::string& a_key, const char* a_message)
{
if (ShouldLogWarning(a_key)) {
REX::WARN("{}", a_message);
}
}
void TrimPacketLine(std::string& a_line)
{
while (!a_line.empty() && (a_line.back() == '\r' || a_line.back() == ' ' || a_line.back() == '\t')) {
a_line.pop_back();
}
}
std::optional<std::uint32_t> ReadPlayerId(const Json& a_packet)
{
const auto playerId = a_packet.find("playerId");
if (playerId == a_packet.end()) {
return std::nullopt;
}
try {
if (playerId->is_number_integer()) {
const auto value = playerId->get<std::int64_t>();
if (value >= 0 && value <= (std::numeric_limits<std::uint32_t>::max)()) {
return static_cast<std::uint32_t>(value);
}
}
if (playerId->is_string()) {
const auto text = playerId->get<std::string>();
std::uint32_t value = 0;
const auto* begin = text.data();
const auto* end = begin + text.size();
const auto result = std::from_chars(begin, end, value);
if (result.ec == std::errc{} && result.ptr == end) {
return value;
}
}
} catch (const std::exception&) {
return std::nullopt;
}
return std::nullopt;
}
std::optional<double> ReadDouble(const Json& a_packet, const char* a_fieldName)
{
const auto field = a_packet.find(a_fieldName);
if (field == a_packet.end()) {
return std::nullopt;
}
try {
if (field->is_number()) {
return field->get<double>();
}
if (field->is_string()) {
const auto text = field->get<std::string>();
std::size_t parsedCharacters = 0;
const auto value = std::stod(text, std::addressof(parsedCharacters));
if (parsedCharacters == text.size()) {
return value;
}
}
} catch (const std::exception&) {
return std::nullopt;
}
return std::nullopt;
}
std::optional<std::string> ReadString(const Json& a_packet, const char* a_fieldName)
{
const auto field = a_packet.find(a_fieldName);
if (field == a_packet.end() || !field->is_string()) {
return std::nullopt;
}
return field->get<std::string>();
}
bool ShouldLogRemoteUpdate(std::uint32_t a_playerId)
{
const auto now = std::chrono::steady_clock::now();
const auto lastLog = g_lastRemoteUpdateLogTimes.find(a_playerId);
if (lastLog != g_lastRemoteUpdateLogTimes.end() && now - lastLog->second < kRemoteUpdateLogInterval) {
return false;
}
g_lastRemoteUpdateLogTimes[a_playerId] = now;
return true;
}
void HandleWelcomePacket(const Json& a_packet)
{
const auto playerId = ReadPlayerId(a_packet);
if (!playerId) {
LogThrottledWarning("welcome_playerId", "Ignoring welcome packet without a valid playerId.");
return;
}
F4T::RemotePlayerState::SetAssignedPlayerId(*playerId);
REX::INFO("Assigned server playerId: {}", *playerId);
}
void HandleTransformPacket(const Json& a_packet)
{
const auto playerId = ReadPlayerId(a_packet);
const auto x = ReadDouble(a_packet, "x");
const auto y = ReadDouble(a_packet, "y");
const auto z = ReadDouble(a_packet, "z");
const auto angleZ = ReadDouble(a_packet, "angleZ");
const auto movementType = ReadString(a_packet, "movementType");
const auto cellId = ReadString(a_packet, "cellId");
const auto worldspaceId = ReadString(a_packet, "worldspaceId");
if (!playerId || !x || !y || !z || !angleZ || !movementType || !cellId || !worldspaceId) {
LogThrottledWarning("transform_invalid", "Ignoring transform packet with missing or invalid required fields.");
return;
}
if (F4T::RemotePlayerState::IsAssignedPlayerId(*playerId)) {
return;
}
F4T::RemotePlayerState::RemotePlayerState remoteState{
*playerId,
static_cast<float>(*x),
static_cast<float>(*y),
static_cast<float>(*z),
static_cast<float>(*angleZ),
*movementType,
*cellId,
*worldspaceId,
ReadDouble(a_packet, "clientTime"),
ReadDouble(a_packet, "serverTime"),
std::chrono::steady_clock::now()
};
F4T::RemotePlayerState::UpdateRemotePlayer(remoteState);
// TODO: A later milestone can read this plain state from the game thread and
// spawn or move remote actors. The networking thread must not touch actors.
if (ShouldLogRemoteUpdate(*playerId)) {
REX::INFO(
"Remote player {} updated: X={:.2f}, Y={:.2f}, Z={:.2f}, AngleZ={:.2f}, Cell={}, Worldspace={}",
remoteState.playerId,
remoteState.x,
remoteState.y,
remoteState.z,
remoteState.angleZ,
remoteState.cellId,
remoteState.worldspaceId);
}
}
void HandleDisconnectPacket(const Json& a_packet)
{
const auto playerId = ReadPlayerId(a_packet);
if (!playerId) {
LogThrottledWarning("disconnect_playerId", "Ignoring disconnect packet without a valid playerId.");
return;
}
g_lastRemoteUpdateLogTimes.erase(*playerId);
if (F4T::RemotePlayerState::RemoveRemotePlayer(*playerId)) {
REX::INFO("Remote player {} disconnected and was removed.", *playerId);
} else {
REX::INFO("Received disconnect for unknown remote player {}.", *playerId);
}
}
void HandleUnknownPacket(const Json& a_packet)
{
const auto type = ReadString(a_packet, "type").value_or("<missing>");
if (ShouldLogWarning("unknown_" + type)) {
REX::WARN("Ignoring unknown server packet type: {}", type);
}
}
void HandlePacketLine(const std::string& a_line)
{
if (a_line.empty()) {
return;
}
Json packet;
try {
packet = Json::parse(a_line);
} catch (const std::exception& a_error) {
if (ShouldLogWarning("json_parse")) {
REX::WARN("Ignoring invalid JSON packet from server: {}", a_error.what());
}
return;
}
if (!packet.is_object()) {
LogThrottledWarning("json_object", "Ignoring server packet that was not a JSON object.");
return;
}
const auto packetType = ReadString(packet, "type");
if (!packetType) {
LogThrottledWarning("packet_type", "Ignoring server packet without a string type field.");
return;
}
if (*packetType == "welcome") {
HandleWelcomePacket(packet);
} else if (*packetType == "transform") {
HandleTransformPacket(packet);
} else if (*packetType == "disconnect") {
HandleDisconnectPacket(packet);
} else {
HandleUnknownPacket(packet);
}
}
void ProcessReceiveBuffer(std::string& a_receiveBuffer)
{
std::size_t newlinePosition = 0;
while ((newlinePosition = a_receiveBuffer.find('\n')) != std::string::npos) {
auto line = a_receiveBuffer.substr(0, newlinePosition);
a_receiveBuffer.erase(0, newlinePosition + 1);
TrimPacketLine(line);
HandlePacketLine(line);
}
if (a_receiveBuffer.size() > kMaximumReceiveBufferSize) {
a_receiveBuffer.clear();
LogThrottledWarning("receive_buffer", "Cleared oversized server receive buffer.");
}
}
void ReceiveLoop()
{
std::array<char, 4096> receiveChunk{};
std::string receiveBuffer;
while (g_receiveThreadRunning.load()) {
int bytesReceived = 0;
{
const std::scoped_lock lock(g_socketMutex);
if (g_socket == INVALID_SOCKET) {
break;
}
bytesReceived = recv(g_socket, receiveChunk.data(), static_cast<int>(receiveChunk.size()), 0);
}
if (bytesReceived > 0) {
receiveBuffer.append(receiveChunk.data(), static_cast<std::size_t>(bytesReceived));
ProcessReceiveBuffer(receiveBuffer);
continue;
}
if (bytesReceived == 0) {
REX::WARN("Fallout 4 Together local server closed the connection.");
CloseSocket();
break;
}
const auto error = WSAGetLastError();
if (error == WSAEWOULDBLOCK) {
std::this_thread::sleep_for(kReceiveIdleSleep);
continue;
}
if (g_receiveThreadRunning.load()) {
REX::WARN("Lost connection to Fallout 4 Together local server while receiving.");
}
CloseSocket();
break;
}
g_receiveThreadRunning.store(false);
}
void StopReceiveThread()
{
g_receiveThreadRunning.store(false);
CloseSocket();
if (g_receiveThread.joinable() && g_receiveThread.get_id() != std::this_thread::get_id()) {
g_receiveThread.join();
}
}
void StartReceiveThread()
{
if (g_receiveThread.joinable()) {
g_receiveThread.join();
}
// Receiving server packets is deliberately isolated from Fallout 4 objects.
// This thread only parses network data and updates plain remote-player state.
g_receiveThreadRunning.store(true);
g_receiveThread = std::thread(ReceiveLoop);
}
} }
namespace F4T::Networking namespace F4T::Networking
@@ -146,16 +490,26 @@ namespace F4T::Networking
return false; return false;
} }
{
const std::scoped_lock lock(g_socketMutex);
g_socket = localSocket; g_socket = localSocket;
}
F4T::RemotePlayerState::ClearAssignedPlayerId();
F4T::RemotePlayerState::ClearRemotePlayers();
g_lastRemoteUpdateLogTimes.clear();
StartReceiveThread();
REX::INFO("Connected to Fallout 4 Together local server."); REX::INFO("Connected to Fallout 4 Together local server.");
// TODO: Read and log the server welcome packet once the plugin has a small
// non-blocking receive path. For now the server owns player IDs entirely.
return true; return true;
} }
void DisconnectFromLocalServer() void DisconnectFromLocalServer()
{ {
CloseSocket(); StopReceiveThread();
F4T::RemotePlayerState::ClearAssignedPlayerId();
F4T::RemotePlayerState::ClearRemotePlayers();
g_lastRemoteUpdateLogTimes.clear();
if (g_winsockInitialized) { if (g_winsockInitialized) {
WSACleanup(); WSACleanup();
@@ -165,7 +519,7 @@ namespace F4T::Networking
bool IsConnectedToServer() bool IsConnectedToServer()
{ {
return g_socket != INVALID_SOCKET; return GetSocket() != INVALID_SOCKET;
} }
bool SendTransformPacket( bool SendTransformPacket(
@@ -244,7 +598,16 @@ namespace F4T::Networking
packetSize += appended; packetSize += appended;
const auto bytesSent = send(g_socket, packet.data(), packetSize, 0); int bytesSent = 0;
{
const std::scoped_lock lock(g_socketMutex);
if (g_socket == INVALID_SOCKET) {
return false;
}
bytesSent = send(g_socket, packet.data(), packetSize, 0);
}
if (bytesSent == SOCKET_ERROR) { if (bytesSent == SOCKET_ERROR) {
const auto error = WSAGetLastError(); const auto error = WSAGetLastError();
if (error != WSAEWOULDBLOCK) { if (error != WSAEWOULDBLOCK) {
+73
View File
@@ -0,0 +1,73 @@
#include "pch.h"
#include "F4TRemotePlayerState.h"
#include <mutex>
#include <optional>
#include <unordered_map>
#include <vector>
namespace
{
std::mutex g_stateMutex;
std::optional<std::uint32_t> g_assignedPlayerId;
std::unordered_map<std::uint32_t, F4T::RemotePlayerState::RemotePlayerState> g_remotePlayers;
}
namespace F4T::RemotePlayerState
{
void SetAssignedPlayerId(std::uint32_t a_playerId)
{
const std::scoped_lock lock(g_stateMutex);
g_assignedPlayerId = a_playerId;
}
std::optional<std::uint32_t> GetAssignedPlayerId()
{
const std::scoped_lock lock(g_stateMutex);
return g_assignedPlayerId;
}
bool IsAssignedPlayerId(std::uint32_t a_playerId)
{
const std::scoped_lock lock(g_stateMutex);
return g_assignedPlayerId.has_value() && *g_assignedPlayerId == a_playerId;
}
void ClearAssignedPlayerId()
{
const std::scoped_lock lock(g_stateMutex);
g_assignedPlayerId.reset();
}
void UpdateRemotePlayer(RemotePlayerState a_state)
{
const std::scoped_lock lock(g_stateMutex);
g_remotePlayers[a_state.playerId] = std::move(a_state);
}
bool RemoveRemotePlayer(std::uint32_t a_playerId)
{
const std::scoped_lock lock(g_stateMutex);
return g_remotePlayers.erase(a_playerId) > 0;
}
void ClearRemotePlayers()
{
const std::scoped_lock lock(g_stateMutex);
g_remotePlayers.clear();
}
std::vector<RemotePlayerState> GetRemotePlayerSnapshot()
{
const std::scoped_lock lock(g_stateMutex);
std::vector<RemotePlayerState> snapshot;
snapshot.reserve(g_remotePlayers.size());
for (const auto& [_, player] : g_remotePlayers) {
snapshot.push_back(player);
}
return snapshot;
}
}
+2
View File
@@ -11,6 +11,7 @@ set_warnings("allextra")
-- add common rules -- add common rules
add_rules("mode.debug", "mode.releasedbg") add_rules("mode.debug", "mode.releasedbg")
add_rules("plugin.vsxmake.autoupdate") add_rules("plugin.vsxmake.autoupdate")
add_requires("nlohmann_json")
-- define targets -- define targets
target("Fallout4Together") target("Fallout4Together")
@@ -26,5 +27,6 @@ target("Fallout4Together")
add_headerfiles("include/**.h") add_headerfiles("include/**.h")
add_includedirs("src") add_includedirs("src")
add_includedirs("include") add_includedirs("include")
add_packages("nlohmann_json")
add_syslinks("ws2_32") add_syslinks("ws2_32")
set_pcxxheader("src/pch.h") set_pcxxheader("src/pch.h")
+7 -6
View File
@@ -20,12 +20,13 @@ Current behavior:
broadcast to other connected clients. broadcast to other connected clients.
- `disconnect` is sent by the server when a client disconnects. - `disconnect` is sent by the server when a client disconnects.
The Fallout 4 plugin currently sends transform packets only. It does not yet The Fallout 4 plugin sends transform packets and receives `welcome`,
receive or process server packets. `transform`, and `disconnect` packets. Received transforms are stored as plain
remote-player state only.
## Working In Fake Client Only ## Current Receivers
`server/fake_client.py` currently receives: The Fallout 4 plugin and `server/fake_client.py` currently receive:
```text ```text
welcome welcome
@@ -33,8 +34,8 @@ transform
disconnect disconnect
``` ```
The fake client stores broadcast transform packets in an in-memory remote player The plugin and fake client store broadcast transform packets in in-memory remote
state table and removes entries when disconnect packets arrive. player state tables and remove entries when disconnect packets arrive.
## Planned Later ## Planned Later
+16 -12
View File
@@ -40,8 +40,8 @@ playerId Server-assigned player ID for this connection.
serverTime Server timestamp when the welcome packet was created. serverTime Server timestamp when the welcome packet was created.
``` ```
The Fallout 4 plugin does not yet read this packet. `server/fake_client.py` The Fallout 4 plugin and `server/fake_client.py` receive this packet. The plugin
currently receives and prints it. stores the assigned `playerId` for filtering its own future transform echoes.
### Transform Packet ### Transform Packet
@@ -117,20 +117,24 @@ playerId Server-assigned player ID that disconnected.
serverTime Server timestamp when the disconnect packet was created. serverTime Server timestamp when the disconnect packet was created.
``` ```
`server/fake_client.py` currently uses this packet to remove remote players from The Fallout 4 plugin and `server/fake_client.py` use this packet to remove
its in-memory state table. remote players from their in-memory state tables.
## Working In Fake Client Only ## Receiver Behavior
- Receiving `welcome` packets. The Fallout 4 plugin and fake client both receive:
- Receiving broadcast `transform` packets from other clients.
- Storing remote player state by `playerId`. ```text
- Removing remote player state when `disconnect` packets arrive. welcome
transform
disconnect
```
The plugin receive loop runs on a background networking thread. It stores plain
remote-player state only and does not spawn actors or touch Fallout 4 game
objects.
## Planned Later ## Planned Later
- Fallout 4 plugin receive loop.
- Plugin-side parsing of `welcome`, `transform`, and `disconnect` packets.
- Plugin-side remote player state storage.
- Remote actor spawning. - Remote actor spawning.
- Gameplay synchronization. - Gameplay synchronization.
+14 -19
View File
@@ -6,23 +6,24 @@ The current implementation is still a local networking prototype.
## Current Implemented Flow ## Current Implemented Flow
```text ```text
Fallout 4 Plugin Fallout 4 Plugin ↔ Local Python Server ↔ Other Clients
Local Python Server Plugin Remote Player State
Fake Client 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. The server assigns a `playerId`, adds `serverTime`,
and broadcasts transform packets to other connected clients. and broadcasts transform packets to other connected clients.
The Fallout 4 plugin does not yet receive remote transforms from the server. The Fallout 4 plugin also receives server packets on a background networking
The current receiver is `server/fake_client.py`. thread. It stores its assigned `playerId`, stores remote transform state by
remote `playerId`, and removes remote state when disconnect packets arrive.
`server/fake_client.py` remains a lightweight test receiver for the same packet
lifecycle.
## Current Remote Player State Model ## Current Remote Player State Model
The fake client stores remote players by `playerId`. The plugin and fake client store remote players by `playerId`.
Each remote player state entry tracks: Each remote player state entry tracks:
@@ -40,13 +41,16 @@ serverTime
lastReceivedLocalTime lastReceivedLocalTime
``` ```
This model is a prototype for the eventual Fallout 4 receiving client. The goal This model proves the data shape and lifecycle before spawning remote actors.
is to prove the data shape and lifecycle before adding a plugin receive loop or
spawning remote actors.
## Implemented ## Implemented
- The plugin sends local player transform packets. - The plugin sends local player transform packets.
- The plugin receives `welcome` packets and stores its assigned `playerId`.
- The plugin receives broadcast transform packets.
- The plugin ignores transform packets for its own assigned `playerId`.
- The plugin stores remote player state by `playerId`.
- The plugin receives `disconnect` packets and removes remote player state.
- The server assigns incrementing `playerId` values. - The server assigns incrementing `playerId` values.
- The server sends `welcome` packets. - The server sends `welcome` packets.
- The server adds `playerId` and `serverTime` to transform packets. - The server adds `playerId` and `serverTime` to transform packets.
@@ -55,19 +59,10 @@ spawning remote actors.
- The server broadcasts `disconnect` packets. - The server broadcasts `disconnect` packets.
- The fake client removes disconnected remote players from its state table. - The fake client removes disconnected remote players from its state table.
## Working In Fake Client Only
- Receiving broadcast transform packets.
- Tracking remote player state.
- Handling disconnect cleanup.
No Fallout 4 remote actor is spawned yet. No Fallout 4 remote actor is spawned yet.
## Planned Later ## Planned Later
- Add a Fallout 4 plugin receive loop.
- Parse `welcome`, `transform`, and `disconnect` packets in the plugin.
- Store remote player state inside the plugin.
- Add interpolation or smoothing after plugin-side state exists. - Add interpolation or smoothing after plugin-side state exists.
- Spawn and move remote actors only after the receive loop is stable. - Spawn and move remote actors only after the receive loop is stable.