Include movement/cell/worldspace in transforms
Extend transform packets to carry movement metadata and optional cell/worldspace IDs. Public API updated (SendTransformPacket signature) and implementation now formats movementType, cellId and worldspaceId when present (buffer size increased and formatting safety checks added). Player tracking in main.cpp now computes PlayerLocation, detects cell/worldspace changes and large teleports (threshold 5000), sends immediate updates with movementType="cell_change"/"worldspace_change"/"teleport", and preserves last-sent state. Server logging updated to print movementType and extra fields. Dev log updated with an entry describing these changes.
This commit is contained in:
@@ -242,3 +242,39 @@ Player position: X=2048.00, Y=2048.00, Z=0.00, AngleZ=0.00
|
|||||||
- Add timestamps to transform packets.
|
- Add timestamps to transform packets.
|
||||||
- Have the server echo/broadcast transform packets to connected clients.
|
- Have the server echo/broadcast transform packets to connected clients.
|
||||||
- Add a fake test client before attempting a second Fallout 4 client.
|
- Add a fake test client before attempting a second Fallout 4 client.
|
||||||
|
|
||||||
|
## 2026-05-31
|
||||||
|
|
||||||
|
### What Changed
|
||||||
|
|
||||||
|
- Added transform metadata for movement type, cell ID, and worldspace ID.
|
||||||
|
- Updated transform packets to include `movementType`.
|
||||||
|
- Added detection for cell changes and worldspace changes.
|
||||||
|
- Updated the server to print the additional transform fields.
|
||||||
|
|
||||||
|
### What Worked
|
||||||
|
|
||||||
|
- Normal movement packets still send correctly.
|
||||||
|
- Cell changes are detected and sent as `movementType=cell_change`.
|
||||||
|
- Worldspace changes are detected and sent as `movementType=worldspace_change`.
|
||||||
|
- Transform packets now include `cellId` and `worldspaceId`.
|
||||||
|
- The server remains compatible with the expanded packet format.
|
||||||
|
- The server handled client disconnects safely.
|
||||||
|
|
||||||
|
### What Broke
|
||||||
|
|
||||||
|
- Nothing recorded.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- Example normal movement packet included `cellId=0000DD5F` and `worldspaceId=0000003C`.
|
||||||
|
- Example cell change packet included `movementType=cell_change`.
|
||||||
|
- Interior cell behavior should be verified later, especially whether `worldspaceId` should be null or inherited.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- Add `playerId` to transform packets.
|
||||||
|
- Add packet timestamps.
|
||||||
|
- Add server-side client IDs.
|
||||||
|
- Make the server broadcast transform packets to other connected clients.
|
||||||
|
- Create a fake client to receive broadcast packets.
|
||||||
@@ -1,9 +1,20 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
namespace F4T::Networking
|
namespace F4T::Networking
|
||||||
{
|
{
|
||||||
bool ConnectToLocalServer();
|
bool ConnectToLocalServer();
|
||||||
void DisconnectFromLocalServer();
|
void DisconnectFromLocalServer();
|
||||||
bool IsConnectedToServer();
|
bool IsConnectedToServer();
|
||||||
bool SendTransformPacket(float a_x, float a_y, float a_z, float a_angleZ);
|
bool SendTransformPacket(
|
||||||
|
float a_x,
|
||||||
|
float a_y,
|
||||||
|
float a_z,
|
||||||
|
float a_angleZ,
|
||||||
|
const char* a_movementType,
|
||||||
|
bool a_hasCellId = false,
|
||||||
|
std::uint32_t a_cellId = 0,
|
||||||
|
bool a_hasWorldspaceId = false,
|
||||||
|
std::uint32_t a_worldspaceId = 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,27 +166,78 @@ namespace F4T::Networking
|
|||||||
return g_socket != INVALID_SOCKET;
|
return g_socket != INVALID_SOCKET;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SendTransformPacket(float a_x, float a_y, float a_z, float a_angleZ)
|
bool SendTransformPacket(
|
||||||
|
float a_x,
|
||||||
|
float a_y,
|
||||||
|
float a_z,
|
||||||
|
float a_angleZ,
|
||||||
|
const char* a_movementType,
|
||||||
|
bool a_hasCellId,
|
||||||
|
std::uint32_t a_cellId,
|
||||||
|
bool a_hasWorldspaceId,
|
||||||
|
std::uint32_t a_worldspaceId)
|
||||||
{
|
{
|
||||||
if (!IsConnectedToServer() && !ConnectToLocalServer()) {
|
if (!IsConnectedToServer() && !ConnectToLocalServer()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::array<char, 192> packet{};
|
std::array<char, 320> packet{};
|
||||||
const auto packetSize = std::snprintf(
|
auto packetSize = std::snprintf(
|
||||||
packet.data(),
|
packet.data(),
|
||||||
packet.size(),
|
packet.size(),
|
||||||
"{\"type\":\"transform\",\"x\":%.2f,\"y\":%.2f,\"z\":%.2f,\"angleZ\":%.2f}\n",
|
"{\"type\":\"transform\",\"x\":%.2f,\"y\":%.2f,\"z\":%.2f,\"angleZ\":%.2f,\"movementType\":\"%s\"",
|
||||||
a_x,
|
a_x,
|
||||||
a_y,
|
a_y,
|
||||||
a_z,
|
a_z,
|
||||||
a_angleZ);
|
a_angleZ,
|
||||||
|
a_movementType ? a_movementType : "normal");
|
||||||
|
|
||||||
if (packetSize <= 0 || static_cast<std::size_t>(packetSize) >= packet.size()) {
|
if (packetSize <= 0 || static_cast<std::size_t>(packetSize) >= packet.size()) {
|
||||||
REX::WARN("Could not format Fallout 4 Together transform packet.");
|
REX::WARN("Could not format Fallout 4 Together transform packet.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (a_hasCellId) {
|
||||||
|
const auto remaining = packet.size() - static_cast<std::size_t>(packetSize);
|
||||||
|
const auto appended = std::snprintf(
|
||||||
|
packet.data() + packetSize,
|
||||||
|
remaining,
|
||||||
|
",\"cellId\":\"%08X\"",
|
||||||
|
a_cellId);
|
||||||
|
|
||||||
|
if (appended <= 0 || static_cast<std::size_t>(appended) >= remaining) {
|
||||||
|
REX::WARN("Could not format Fallout 4 Together transform packet cell ID.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
packetSize += appended;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a_hasWorldspaceId) {
|
||||||
|
const auto remaining = packet.size() - static_cast<std::size_t>(packetSize);
|
||||||
|
const auto appended = std::snprintf(
|
||||||
|
packet.data() + packetSize,
|
||||||
|
remaining,
|
||||||
|
",\"worldspaceId\":\"%08X\"",
|
||||||
|
a_worldspaceId);
|
||||||
|
|
||||||
|
if (appended <= 0 || static_cast<std::size_t>(appended) >= remaining) {
|
||||||
|
REX::WARN("Could not format Fallout 4 Together transform packet worldspace ID.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
packetSize += appended;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto remaining = packet.size() - static_cast<std::size_t>(packetSize);
|
||||||
|
const auto appended = std::snprintf(packet.data() + packetSize, remaining, "}\n");
|
||||||
|
if (appended <= 0 || static_cast<std::size_t>(appended) >= remaining) {
|
||||||
|
REX::WARN("Could not finish Fallout 4 Together transform packet.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
packetSize += appended;
|
||||||
|
|
||||||
const auto bytesSent = send(g_socket, packet.data(), packetSize, 0);
|
const auto bytesSent = send(g_socket, packet.data(), packetSize, 0);
|
||||||
if (bytesSent == SOCKET_ERROR) {
|
if (bytesSent == SOCKET_ERROR) {
|
||||||
const auto error = WSAGetLastError();
|
const auto error = WSAGetLastError();
|
||||||
|
|||||||
+91
-8
@@ -1,6 +1,8 @@
|
|||||||
#include "F4TNetworking.h"
|
#include "F4TNetworking.h"
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -12,10 +14,19 @@ namespace
|
|||||||
float angleZ;
|
float angleZ;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct PlayerLocation
|
||||||
|
{
|
||||||
|
bool hasCellId;
|
||||||
|
std::uint32_t cellId;
|
||||||
|
bool hasWorldspaceId;
|
||||||
|
std::uint32_t worldspaceId;
|
||||||
|
};
|
||||||
|
|
||||||
constexpr auto kPositionLogThreshold = 10.0F;
|
constexpr auto kPositionLogThreshold = 10.0F;
|
||||||
constexpr auto kRotationLogThresholdDegrees = 1.0F;
|
constexpr auto kRotationLogThresholdDegrees = 1.0F;
|
||||||
constexpr auto kRotationLogThresholdRadians = kRotationLogThresholdDegrees * 3.14159265358979323846F / 180.0F;
|
constexpr auto kRotationLogThresholdRadians = kRotationLogThresholdDegrees * 3.14159265358979323846F / 180.0F;
|
||||||
constexpr auto kMinimumLogInterval = 1s;
|
constexpr auto kMinimumLogInterval = 1s;
|
||||||
|
constexpr auto kTeleportDistanceThreshold = 5000.0F;
|
||||||
|
|
||||||
RE::PlayerCharacter* TryGetLocalPlayer()
|
RE::PlayerCharacter* TryGetLocalPlayer()
|
||||||
{
|
{
|
||||||
@@ -34,11 +45,43 @@ namespace
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HasTransformChanged(const PlayerTransform& a_previous, const PlayerTransform& a_current)
|
PlayerLocation GetPlayerLocation(const RE::PlayerCharacter& a_player)
|
||||||
|
{
|
||||||
|
PlayerLocation location{
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto* parentCell = a_player.GetParentCell();
|
||||||
|
if (parentCell) {
|
||||||
|
location.hasCellId = true;
|
||||||
|
location.cellId = parentCell->GetFormID();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a_player.cachedWorldspace) {
|
||||||
|
location.hasWorldspaceId = true;
|
||||||
|
location.worldspaceId = a_player.cachedWorldspace->GetFormID();
|
||||||
|
} else if (parentCell && parentCell->IsExterior() && parentCell->worldSpace) {
|
||||||
|
location.hasWorldspaceId = true;
|
||||||
|
location.worldspaceId = parentCell->worldSpace->GetFormID();
|
||||||
|
}
|
||||||
|
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
float GetSquaredPositionDistance(const PlayerTransform& a_previous, const PlayerTransform& a_current)
|
||||||
{
|
{
|
||||||
const auto deltaX = a_current.x - a_previous.x;
|
const auto deltaX = a_current.x - a_previous.x;
|
||||||
const auto deltaY = a_current.y - a_previous.y;
|
const auto deltaY = a_current.y - a_previous.y;
|
||||||
const auto deltaZ = a_current.z - a_previous.z;
|
const auto deltaZ = a_current.z - a_previous.z;
|
||||||
|
|
||||||
|
return (deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasTransformChanged(const PlayerTransform& a_previous, const PlayerTransform& a_current)
|
||||||
|
{
|
||||||
auto deltaAngleZ = a_current.angleZ - a_previous.angleZ;
|
auto deltaAngleZ = a_current.angleZ - a_previous.angleZ;
|
||||||
|
|
||||||
constexpr auto fullRotationRadians = 360.0F * 3.14159265358979323846F / 180.0F;
|
constexpr auto fullRotationRadians = 360.0F * 3.14159265358979323846F / 180.0F;
|
||||||
@@ -50,13 +93,31 @@ namespace
|
|||||||
deltaAngleZ += fullRotationRadians;
|
deltaAngleZ += fullRotationRadians;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto squaredPositionDelta = (deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ);
|
const auto squaredPositionDelta = GetSquaredPositionDistance(a_previous, a_current);
|
||||||
constexpr auto squaredPositionThreshold = kPositionLogThreshold * kPositionLogThreshold;
|
constexpr auto squaredPositionThreshold = kPositionLogThreshold * kPositionLogThreshold;
|
||||||
|
|
||||||
return squaredPositionDelta > squaredPositionThreshold ||
|
return squaredPositionDelta > squaredPositionThreshold ||
|
||||||
deltaAngleZ > kRotationLogThresholdRadians || deltaAngleZ < -kRotationLogThresholdRadians;
|
deltaAngleZ > kRotationLogThresholdRadians || deltaAngleZ < -kRotationLogThresholdRadians;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool HasCellChanged(const PlayerLocation& a_previous, const PlayerLocation& a_current)
|
||||||
|
{
|
||||||
|
return a_previous.hasCellId != a_current.hasCellId ||
|
||||||
|
(a_previous.hasCellId && a_previous.cellId != a_current.cellId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasWorldspaceChanged(const PlayerLocation& a_previous, const PlayerLocation& a_current)
|
||||||
|
{
|
||||||
|
return a_previous.hasWorldspaceId != a_current.hasWorldspaceId ||
|
||||||
|
(a_previous.hasWorldspaceId && a_previous.worldspaceId != a_current.worldspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasTeleportDistance(const PlayerTransform& a_previous, const PlayerTransform& a_current)
|
||||||
|
{
|
||||||
|
constexpr auto squaredTeleportThreshold = kTeleportDistanceThreshold * kTeleportDistanceThreshold;
|
||||||
|
return GetSquaredPositionDistance(a_previous, a_current) > squaredTeleportThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
void LogPlayerTransform(const PlayerTransform& a_transform)
|
void LogPlayerTransform(const PlayerTransform& a_transform)
|
||||||
{
|
{
|
||||||
REX::INFO("Player position: X={:.2f}, Y={:.2f}, Z={:.2f}, AngleZ={:.2f}",
|
REX::INFO("Player position: X={:.2f}, Y={:.2f}, Z={:.2f}, AngleZ={:.2f}",
|
||||||
@@ -79,22 +140,38 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool hasLastTransform = false;
|
static bool hasLastTransform = false;
|
||||||
static PlayerTransform lastTransform{};
|
static PlayerTransform lastSentTransform{};
|
||||||
|
static PlayerLocation lastKnownLocation{};
|
||||||
static auto lastLogTime = std::chrono::steady_clock::now();
|
static auto lastLogTime = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
const auto currentTransform = GetPlayerTransform(*player);
|
const auto currentTransform = GetPlayerTransform(*player);
|
||||||
|
const auto currentLocation = GetPlayerLocation(*player);
|
||||||
if (!hasLastTransform) {
|
if (!hasLastTransform) {
|
||||||
lastTransform = currentTransform;
|
lastSentTransform = currentTransform;
|
||||||
|
lastKnownLocation = currentLocation;
|
||||||
hasLastTransform = true;
|
hasLastTransform = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!HasTransformChanged(lastTransform, currentTransform)) {
|
const char* movementType = "normal";
|
||||||
|
if (HasWorldspaceChanged(lastKnownLocation, currentLocation)) {
|
||||||
|
REX::INFO("Detected worldspace change. Sending immediate transform update.");
|
||||||
|
movementType = "worldspace_change";
|
||||||
|
} else if (HasCellChanged(lastKnownLocation, currentLocation)) {
|
||||||
|
REX::INFO("Detected cell change. Sending immediate transform update.");
|
||||||
|
movementType = "cell_change";
|
||||||
|
} else if (HasTeleportDistance(lastSentTransform, currentTransform)) {
|
||||||
|
REX::INFO("Detected teleport/large position jump. Sending immediate transform update.");
|
||||||
|
movementType = "teleport";
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto isSpecialMovement = movementType != std::string_view{ "normal" };
|
||||||
|
if (!isSpecialMovement && !HasTransformChanged(lastSentTransform, currentTransform)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto currentTime = std::chrono::steady_clock::now();
|
const auto currentTime = std::chrono::steady_clock::now();
|
||||||
if (currentTime - lastLogTime < kMinimumLogInterval) {
|
if (!isSpecialMovement && currentTime - lastLogTime < kMinimumLogInterval) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,8 +183,14 @@ namespace
|
|||||||
currentTransform.x,
|
currentTransform.x,
|
||||||
currentTransform.y,
|
currentTransform.y,
|
||||||
currentTransform.z,
|
currentTransform.z,
|
||||||
currentTransform.angleZ);
|
currentTransform.angleZ,
|
||||||
lastTransform = currentTransform;
|
movementType,
|
||||||
|
currentLocation.hasCellId,
|
||||||
|
currentLocation.cellId,
|
||||||
|
currentLocation.hasWorldspaceId,
|
||||||
|
currentLocation.worldspaceId);
|
||||||
|
lastSentTransform = currentTransform;
|
||||||
|
lastKnownLocation = currentLocation;
|
||||||
lastLogTime = currentTime;
|
lastLogTime = currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -65,9 +65,20 @@ def print_packet(client: str, packet: dict[str, Any]) -> None:
|
|||||||
log(f"Malformed transform packet from {client}: {packet}")
|
log(f"Malformed transform packet from {client}: {packet}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
movement_type = packet.get("movementType", "normal")
|
||||||
|
extra_fields = []
|
||||||
|
for field_name in ("cellId", "worldspaceId"):
|
||||||
|
if field_name in packet:
|
||||||
|
extra_fields.append(f"{field_name}={packet[field_name]}")
|
||||||
|
|
||||||
|
extra_details = ""
|
||||||
|
if extra_fields:
|
||||||
|
extra_details = ", " + ", ".join(extra_fields)
|
||||||
|
|
||||||
log(
|
log(
|
||||||
"Transform from "
|
"Transform from "
|
||||||
f"{client}: x={x:.2f}, y={y:.2f}, z={z:.2f}, angleZ={angle_z:.2f}"
|
f"{client}: x={x:.2f}, y={y:.2f}, z={z:.2f}, angleZ={angle_z:.2f}, "
|
||||||
|
f"movementType={movement_type}{extra_details}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user