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:
2026-05-31 18:59:22 +12:00
parent b24ce17751
commit 4d43ecfc6c
5 changed files with 208 additions and 16 deletions
+56 -5
View File
@@ -166,27 +166,78 @@ namespace F4T::Networking
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()) {
return false;
}
std::array<char, 192> packet{};
const auto packetSize = std::snprintf(
std::array<char, 320> packet{};
auto packetSize = std::snprintf(
packet.data(),
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_y,
a_z,
a_angleZ);
a_angleZ,
a_movementType ? a_movementType : "normal");
if (packetSize <= 0 || static_cast<std::size_t>(packetSize) >= packet.size()) {
REX::WARN("Could not format Fallout 4 Together transform packet.");
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);
if (bytesSent == SOCKET_ERROR) {
const auto error = WSAGetLastError();
+91 -8
View File
@@ -1,6 +1,8 @@
#include "F4TNetworking.h"
#include <cstdlib>
#include <cstdint>
#include <string_view>
namespace
{
@@ -12,10 +14,19 @@ namespace
float angleZ;
};
struct PlayerLocation
{
bool hasCellId;
std::uint32_t cellId;
bool hasWorldspaceId;
std::uint32_t worldspaceId;
};
constexpr auto kPositionLogThreshold = 10.0F;
constexpr auto kRotationLogThresholdDegrees = 1.0F;
constexpr auto kRotationLogThresholdRadians = kRotationLogThresholdDegrees * 3.14159265358979323846F / 180.0F;
constexpr auto kMinimumLogInterval = 1s;
constexpr auto kTeleportDistanceThreshold = 5000.0F;
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 deltaY = a_current.y - a_previous.y;
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;
constexpr auto fullRotationRadians = 360.0F * 3.14159265358979323846F / 180.0F;
@@ -50,13 +93,31 @@ namespace
deltaAngleZ += fullRotationRadians;
}
const auto squaredPositionDelta = (deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ);
const auto squaredPositionDelta = GetSquaredPositionDistance(a_previous, a_current);
constexpr auto squaredPositionThreshold = kPositionLogThreshold * kPositionLogThreshold;
return squaredPositionDelta > squaredPositionThreshold ||
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)
{
REX::INFO("Player position: X={:.2f}, Y={:.2f}, Z={:.2f}, AngleZ={:.2f}",
@@ -79,22 +140,38 @@ namespace
}
static bool hasLastTransform = false;
static PlayerTransform lastTransform{};
static PlayerTransform lastSentTransform{};
static PlayerLocation lastKnownLocation{};
static auto lastLogTime = std::chrono::steady_clock::now();
const auto currentTransform = GetPlayerTransform(*player);
const auto currentLocation = GetPlayerLocation(*player);
if (!hasLastTransform) {
lastTransform = currentTransform;
lastSentTransform = currentTransform;
lastKnownLocation = currentLocation;
hasLastTransform = true;
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;
}
const auto currentTime = std::chrono::steady_clock::now();
if (currentTime - lastLogTime < kMinimumLogInterval) {
if (!isSpecialMovement && currentTime - lastLogTime < kMinimumLogInterval) {
return;
}
@@ -106,8 +183,14 @@ namespace
currentTransform.x,
currentTransform.y,
currentTransform.z,
currentTransform.angleZ);
lastTransform = currentTransform;
currentTransform.angleZ,
movementType,
currentLocation.hasCellId,
currentLocation.cellId,
currentLocation.hasWorldspaceId,
currentLocation.worldspaceId);
lastSentTransform = currentTransform;
lastKnownLocation = currentLocation;
lastLogTime = currentTime;
}