Add host-authoritative world-state sync
Implement host-authoritative time/weather sync across server and clients. Adds a new worldState packet (and worldStateHost for host assignment) and server-side host tracking/reassignment to the lowest remaining playerId when the host disconnects. Plugin-side changes introduce F4T::WorldStateSync (new header + implementation), polling/apply hooks in the game-thread main loop, and Networking support to send/receive/parse worldState/worldStateHost packets. The server validates host-only sends, relays worldState to peers, broadcasts worldStateHost on host handoff, and augments logging/stats. Protocol and documentation files updated (protocol/world-state.md, docs/*) and server/fake_client.py extended to track/print host world-state snapshots.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "F4TNetworking.h"
|
||||
#include "F4TRemotePlayerState.h"
|
||||
#include "F4TWorldStateSync.h"
|
||||
|
||||
#include <WinSock2.h>
|
||||
#include <WS2tcpip.h>
|
||||
@@ -169,9 +170,9 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::uint32_t> ReadPlayerId(const Json& a_packet)
|
||||
std::optional<std::uint32_t> ReadPlayerId(const Json& a_packet, const char* a_fieldName = "playerId")
|
||||
{
|
||||
const auto playerId = a_packet.find("playerId");
|
||||
const auto playerId = a_packet.find(a_fieldName);
|
||||
if (playerId == a_packet.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -306,7 +307,109 @@ namespace
|
||||
}
|
||||
|
||||
F4T::RemotePlayerState::SetAssignedPlayerId(*playerId);
|
||||
LogInfoWithLocalPlayerPrefix(std::format("Assigned server playerId: {}", *playerId));
|
||||
|
||||
const auto worldStateHostPlayerId = ReadPlayerId(a_packet, "worldStateHostPlayerId");
|
||||
if (worldStateHostPlayerId) {
|
||||
F4T::WorldStateSync::SetWorldStateHostPlayerId(*worldStateHostPlayerId);
|
||||
} else {
|
||||
F4T::WorldStateSync::SetWorldStateHostPlayerId(1);
|
||||
}
|
||||
|
||||
if (F4T::WorldStateSync::IsWorldStateHost()) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Assigned server playerId: {} (world-state host).",
|
||||
*playerId));
|
||||
} else if (const auto hostPlayerId = F4T::WorldStateSync::GetWorldStateHostPlayerId()) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Assigned server playerId: {} (world-state host is player {}).",
|
||||
*playerId,
|
||||
*hostPlayerId));
|
||||
} else {
|
||||
LogInfoWithLocalPlayerPrefix(std::format("Assigned server playerId: {}", *playerId));
|
||||
}
|
||||
}
|
||||
|
||||
void HandleWorldStateHostPacket(const Json& a_packet)
|
||||
{
|
||||
const auto hostPlayerId = ReadPlayerId(a_packet, "worldStateHostPlayerId");
|
||||
if (!hostPlayerId) {
|
||||
LogThrottledWarning(
|
||||
"world_state_host_missing",
|
||||
"Ignoring worldStateHost packet without a valid worldStateHostPlayerId.");
|
||||
return;
|
||||
}
|
||||
|
||||
F4T::WorldStateSync::SetWorldStateHostPlayerId(*hostPlayerId);
|
||||
}
|
||||
|
||||
std::optional<std::uint32_t> ParseHexFormId(std::string_view a_hex)
|
||||
{
|
||||
if (a_hex.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::uint32_t value = 0;
|
||||
const auto* begin = a_hex.data();
|
||||
const auto* end = begin + a_hex.size();
|
||||
const auto result = std::from_chars(begin, end, value, 16);
|
||||
if (result.ec != std::errc{} || result.ptr != end) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
void HandleWorldStatePacket(const Json& a_packet)
|
||||
{
|
||||
const auto gameHour = ReadDouble(a_packet, "gameHour");
|
||||
const auto gameDaysPassed = ReadDouble(a_packet, "gameDaysPassed");
|
||||
if (!gameHour || !gameDaysPassed) {
|
||||
LogThrottledWarning(
|
||||
"world_state_missing_fields",
|
||||
"Ignoring worldState packet missing required gameHour or gameDaysPassed.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!std::isfinite(*gameHour) || !std::isfinite(*gameDaysPassed)) {
|
||||
LogThrottledWarning("world_state_invalid_numbers", "Ignoring worldState packet with non-finite time values.");
|
||||
return;
|
||||
}
|
||||
|
||||
F4T::WorldStateSync::PendingWorldState pendingState{
|
||||
static_cast<float>(*gameHour),
|
||||
static_cast<float>(*gameDaysPassed),
|
||||
std::nullopt,
|
||||
false
|
||||
};
|
||||
|
||||
const auto weatherFormIdText = ReadString(a_packet, "weatherFormId");
|
||||
if (weatherFormIdText && !weatherFormIdText->empty()) {
|
||||
if (const auto weatherFormId = ParseHexFormId(*weatherFormIdText)) {
|
||||
pendingState.weatherFormId = *weatherFormId;
|
||||
pendingState.hasWeatherFormId = true;
|
||||
} else if (ShouldLogWarning("world_state_weather_parse")) {
|
||||
LogWarningWithLocalPlayerPrefix(std::format(
|
||||
"Ignoring invalid weatherFormId '{}' in worldState packet.",
|
||||
*weatherFormIdText));
|
||||
}
|
||||
}
|
||||
|
||||
if (ShouldLogRemoteUpdate(1)) {
|
||||
if (pendingState.hasWeatherFormId && pendingState.weatherFormId) {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Queued host worldState: gameHour={:.2f}, gameDaysPassed={:.3f}, weatherFormId=0x{:08X}.",
|
||||
pendingState.gameHour,
|
||||
pendingState.gameDaysPassed,
|
||||
*pendingState.weatherFormId));
|
||||
} else {
|
||||
LogInfoWithLocalPlayerPrefix(std::format(
|
||||
"Queued host worldState: gameHour={:.2f}, gameDaysPassed={:.3f}, weatherFormId=<none>.",
|
||||
pendingState.gameHour,
|
||||
pendingState.gameDaysPassed));
|
||||
}
|
||||
}
|
||||
|
||||
F4T::WorldStateSync::SetPendingWorldState(std::move(pendingState));
|
||||
}
|
||||
|
||||
void HandleTransformPacket(const Json& a_packet)
|
||||
@@ -494,6 +597,10 @@ namespace
|
||||
HandleWelcomePacket(packet);
|
||||
} else if (*packetType == "transform") {
|
||||
HandleTransformPacket(packet);
|
||||
} else if (*packetType == "worldState") {
|
||||
HandleWorldStatePacket(packet);
|
||||
} else if (*packetType == "worldStateHost") {
|
||||
HandleWorldStateHostPacket(packet);
|
||||
} else if (*packetType == "disconnect") {
|
||||
HandleDisconnectPacket(packet);
|
||||
} else {
|
||||
@@ -657,6 +764,7 @@ namespace F4T::Networking
|
||||
|
||||
F4T::RemotePlayerState::ClearAssignedPlayerId();
|
||||
F4T::RemotePlayerState::ClearRemotePlayers();
|
||||
F4T::WorldStateSync::Reset();
|
||||
g_lastRemoteUpdateLogTimes.clear();
|
||||
g_lastRemoteMovementStateLogTimes.clear();
|
||||
StartReceiveThread();
|
||||
@@ -713,6 +821,7 @@ namespace F4T::Networking
|
||||
StopReceiveThread();
|
||||
F4T::RemotePlayerState::ClearAssignedPlayerId();
|
||||
F4T::RemotePlayerState::ClearRemotePlayers();
|
||||
F4T::WorldStateSync::Reset();
|
||||
g_lastRemoteUpdateLogTimes.clear();
|
||||
g_lastRemoteMovementStateLogTimes.clear();
|
||||
|
||||
@@ -860,4 +969,76 @@ namespace F4T::Networking
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SendWorldStatePacket(
|
||||
float a_gameHour,
|
||||
float a_gameDaysPassed,
|
||||
bool a_hasWeatherFormId,
|
||||
std::uint32_t a_weatherFormId)
|
||||
{
|
||||
if (!IsConnectedToServer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!std::isfinite(a_gameHour) || !std::isfinite(a_gameDaysPassed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto clientTime =
|
||||
std::chrono::duration<double>(std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
|
||||
std::array<char, 256> packet{};
|
||||
int packetSize = 0;
|
||||
if (a_hasWeatherFormId) {
|
||||
packetSize = std::snprintf(
|
||||
packet.data(),
|
||||
packet.size(),
|
||||
"{\"type\":\"worldState\",\"gameHour\":%.3f,\"gameDaysPassed\":%.3f,\"weatherFormId\":\"%08X\",\"clientTime\":%.3f}\n",
|
||||
a_gameHour,
|
||||
a_gameDaysPassed,
|
||||
a_weatherFormId,
|
||||
clientTime);
|
||||
} else {
|
||||
packetSize = std::snprintf(
|
||||
packet.data(),
|
||||
packet.size(),
|
||||
"{\"type\":\"worldState\",\"gameHour\":%.3f,\"gameDaysPassed\":%.3f,\"clientTime\":%.3f}\n",
|
||||
a_gameHour,
|
||||
a_gameDaysPassed,
|
||||
clientTime);
|
||||
}
|
||||
|
||||
if (packetSize <= 0 || static_cast<std::size_t>(packetSize) >= packet.size()) {
|
||||
LogWarningWithLocalPlayerPrefix("Could not format Commonwealth Online worldState packet.");
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
const auto error = WSAGetLastError();
|
||||
if (error != WSAEWOULDBLOCK) {
|
||||
LogWarningWithLocalPlayerPrefix("Lost connection to Commonwealth Online local server.");
|
||||
CloseSocket();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bytesSent != packetSize) {
|
||||
LogWarningWithLocalPlayerPrefix("Could not send full Commonwealth Online worldState packet.");
|
||||
CloseSocket();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
#include "pch.h"
|
||||
|
||||
#include "F4TWorldStateSync.h"
|
||||
|
||||
#include "F4TNetworking.h"
|
||||
#include "F4TRemotePlayerState.h"
|
||||
|
||||
#include "RE/C/Calendar.h"
|
||||
#include "RE/P/PlayerCharacter.h"
|
||||
#include "RE/S/Sky.h"
|
||||
#include "RE/T/TESDataHandler.h"
|
||||
#include "RE/T/TESForm.h"
|
||||
#include "RE/T/TESObjectCELL.h"
|
||||
#include "RE/T/TESWeather.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr auto kWorldStateHeartbeatInterval = 1s;
|
||||
constexpr float kGameHourSendThreshold = 0.05F;
|
||||
constexpr float kGameDaysPassedSendThreshold = 0.001F;
|
||||
constexpr float kGameHourApplyThreshold = 0.05F;
|
||||
constexpr float kGameDaysPassedApplyThreshold = 0.001F;
|
||||
constexpr auto kWorldStateLogInterval = 2s;
|
||||
|
||||
std::mutex g_pendingMutex;
|
||||
bool g_hasPendingWorldState{ false };
|
||||
F4T::WorldStateSync::PendingWorldState g_pendingWorldState{};
|
||||
|
||||
struct LastSentWorldState
|
||||
{
|
||||
float gameHour{ -1.0F };
|
||||
float gameDaysPassed{ -1.0F };
|
||||
std::optional<std::uint32_t> weatherFormId;
|
||||
bool hasWeatherFormId{ false };
|
||||
bool initialized{ false };
|
||||
};
|
||||
|
||||
struct LastAppliedWorldState
|
||||
{
|
||||
float gameHour{ -1.0F };
|
||||
float gameDaysPassed{ -1.0F };
|
||||
std::optional<std::uint32_t> weatherFormId;
|
||||
bool hasWeatherFormId{ false };
|
||||
bool initialized{ false };
|
||||
};
|
||||
|
||||
LastSentWorldState g_lastSentWorldState{};
|
||||
LastAppliedWorldState g_lastAppliedWorldState{};
|
||||
auto g_lastHostSendTime = std::chrono::steady_clock::time_point{};
|
||||
std::optional<std::uint32_t> g_worldStateHostPlayerId{};
|
||||
std::unordered_map<std::string, std::chrono::steady_clock::time_point> g_lastLogTimes;
|
||||
|
||||
std::string GetLocalPlayerLogMessage(std::string_view a_message)
|
||||
{
|
||||
return F4T::Networking::GetLocalPlayerLogPrefix() + " " + std::string(a_message);
|
||||
}
|
||||
|
||||
void LogInfo(std::string_view a_message)
|
||||
{
|
||||
const auto message = GetLocalPlayerLogMessage(a_message);
|
||||
REX::INFO(std::string_view{ message });
|
||||
}
|
||||
|
||||
void LogWarning(std::string_view a_message)
|
||||
{
|
||||
const auto message = GetLocalPlayerLogMessage(a_message);
|
||||
REX::WARN(std::string_view{ message });
|
||||
}
|
||||
|
||||
bool ShouldLog(const std::string& a_key)
|
||||
{
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto lastLog = g_lastLogTimes.find(a_key);
|
||||
if (lastLog != g_lastLogTimes.end() && now - lastLog->second < kWorldStateLogInterval) {
|
||||
return false;
|
||||
}
|
||||
|
||||
g_lastLogTimes[a_key] = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsLocalPlayerExterior()
|
||||
{
|
||||
const auto* player = RE::PlayerCharacter::GetSingleton();
|
||||
if (!player) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto* playerCell = player->GetParentCell();
|
||||
return playerCell && playerCell->IsExterior();
|
||||
}
|
||||
|
||||
bool TryReadCalendarValues(float& a_outGameHour, float& a_outGameDaysPassed)
|
||||
{
|
||||
const auto* calendar = RE::Calendar::GetSingleton();
|
||||
if (!calendar || !calendar->gameHour || !calendar->gameDaysPassed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
a_outGameHour = calendar->gameHour->GetValue();
|
||||
a_outGameDaysPassed = calendar->gameDaysPassed->GetValue();
|
||||
return std::isfinite(a_outGameHour) && std::isfinite(a_outGameDaysPassed);
|
||||
}
|
||||
|
||||
std::optional<std::uint32_t> TryReadExteriorWeatherFormId()
|
||||
{
|
||||
const auto* sky = RE::Sky::GetSingleton();
|
||||
if (!sky || sky->mode == RE::Sky::Mode::kInterior) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto* currentWeather = sky->currentWeather;
|
||||
if (!currentWeather) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return currentWeather->GetFormID();
|
||||
}
|
||||
|
||||
bool ShouldSendWorldState(
|
||||
float a_gameHour,
|
||||
float a_gameDaysPassed,
|
||||
bool a_hasWeatherFormId,
|
||||
std::optional<std::uint32_t> a_weatherFormId,
|
||||
const std::chrono::steady_clock::time_point& a_now)
|
||||
{
|
||||
if (!g_lastSentWorldState.initialized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (std::abs(a_gameHour - g_lastSentWorldState.gameHour) > kGameHourSendThreshold ||
|
||||
std::abs(a_gameDaysPassed - g_lastSentWorldState.gameDaysPassed) > kGameDaysPassedSendThreshold) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a_hasWeatherFormId != g_lastSentWorldState.hasWeatherFormId ||
|
||||
a_weatherFormId != g_lastSentWorldState.weatherFormId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return g_lastHostSendTime.time_since_epoch().count() == 0 ||
|
||||
a_now - g_lastHostSendTime >= kWorldStateHeartbeatInterval;
|
||||
}
|
||||
|
||||
void ApplyTimeState(float a_gameHour, float a_gameDaysPassed)
|
||||
{
|
||||
auto* calendar = RE::Calendar::GetSingleton();
|
||||
if (!calendar || !calendar->gameHour || !calendar->gameDaysPassed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto localGameHour = calendar->gameHour->GetValue();
|
||||
const auto localGameDaysPassed = calendar->gameDaysPassed->GetValue();
|
||||
if (std::abs(localGameHour - a_gameHour) <= kGameHourApplyThreshold &&
|
||||
std::abs(localGameDaysPassed - a_gameDaysPassed) <= kGameDaysPassedApplyThreshold) {
|
||||
return;
|
||||
}
|
||||
|
||||
calendar->gameHour->value = a_gameHour;
|
||||
calendar->gameDaysPassed->value = a_gameDaysPassed;
|
||||
|
||||
if (auto* sky = RE::Sky::GetSingleton()) {
|
||||
sky->currentGameHour = a_gameHour;
|
||||
}
|
||||
|
||||
if (ShouldLog("world_state_time_apply")) {
|
||||
LogInfo(std::format(
|
||||
"Applied host world time: gameHour={:.2f}, gameDaysPassed={:.3f}.",
|
||||
a_gameHour,
|
||||
a_gameDaysPassed));
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyWeatherState(bool a_hasWeatherFormId, std::optional<std::uint32_t> a_weatherFormId)
|
||||
{
|
||||
if (!IsLocalPlayerExterior()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* sky = RE::Sky::GetSingleton();
|
||||
if (!sky) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!a_hasWeatherFormId || !a_weatherFormId) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* weather = RE::TESForm::GetFormByID<RE::TESWeather>(*a_weatherFormId);
|
||||
if (!weather) {
|
||||
if (ShouldLog("world_state_weather_lookup_failed")) {
|
||||
LogWarning(std::format(
|
||||
"Could not resolve host weather form 0x{:08X} for world-state apply.",
|
||||
*a_weatherFormId));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_lastAppliedWorldState.initialized &&
|
||||
g_lastAppliedWorldState.hasWeatherFormId &&
|
||||
g_lastAppliedWorldState.weatherFormId == a_weatherFormId &&
|
||||
sky->overrideWeather == weather) {
|
||||
return;
|
||||
}
|
||||
|
||||
sky->ForceWeather(weather, true);
|
||||
|
||||
if (ShouldLog("world_state_weather_apply")) {
|
||||
LogInfo(std::format("Applied host weather form 0x{:08X}.", *a_weatherFormId));
|
||||
}
|
||||
}
|
||||
|
||||
void SeedHostCaptureFromLocalState()
|
||||
{
|
||||
float gameHour = 0.0F;
|
||||
float gameDaysPassed = 0.0F;
|
||||
if (!TryReadCalendarValues(gameHour, gameDaysPassed)) {
|
||||
g_lastSentWorldState = {};
|
||||
g_lastHostSendTime = std::chrono::steady_clock::now();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto weatherFormId = TryReadExteriorWeatherFormId();
|
||||
g_lastSentWorldState.gameHour = gameHour;
|
||||
g_lastSentWorldState.gameDaysPassed = gameDaysPassed;
|
||||
g_lastSentWorldState.hasWeatherFormId = weatherFormId.has_value();
|
||||
g_lastSentWorldState.weatherFormId = weatherFormId;
|
||||
g_lastSentWorldState.initialized = true;
|
||||
g_lastHostSendTime = std::chrono::steady_clock::now();
|
||||
}
|
||||
}
|
||||
|
||||
namespace F4T::WorldStateSync
|
||||
{
|
||||
bool IsWorldStateHost()
|
||||
{
|
||||
const auto assignedPlayerId = RemotePlayerState::GetAssignedPlayerId();
|
||||
return assignedPlayerId && g_worldStateHostPlayerId && *assignedPlayerId == *g_worldStateHostPlayerId;
|
||||
}
|
||||
|
||||
std::optional<std::uint32_t> GetWorldStateHostPlayerId()
|
||||
{
|
||||
return g_worldStateHostPlayerId;
|
||||
}
|
||||
|
||||
void SetWorldStateHostPlayerId(std::uint32_t a_hostPlayerId)
|
||||
{
|
||||
const auto assignedPlayerId = RemotePlayerState::GetAssignedPlayerId();
|
||||
const bool wasHost =
|
||||
assignedPlayerId && g_worldStateHostPlayerId && *assignedPlayerId == *g_worldStateHostPlayerId;
|
||||
const bool hostChanged = !g_worldStateHostPlayerId || *g_worldStateHostPlayerId != a_hostPlayerId;
|
||||
g_worldStateHostPlayerId = a_hostPlayerId;
|
||||
|
||||
const bool isNowHost = assignedPlayerId && *assignedPlayerId == a_hostPlayerId;
|
||||
if (isNowHost && (!wasHost || hostChanged)) {
|
||||
{
|
||||
const std::scoped_lock lock(g_pendingMutex);
|
||||
g_hasPendingWorldState = false;
|
||||
g_pendingWorldState = {};
|
||||
}
|
||||
|
||||
SeedHostCaptureFromLocalState();
|
||||
|
||||
if (ShouldLog("world_state_host_promoted")) {
|
||||
LogInfo(std::format(
|
||||
"Promoted to world-state host (hostPlayerId={}). Continuing with current local time and weather.",
|
||||
a_hostPlayerId));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isNowHost && wasHost) {
|
||||
g_lastSentWorldState = {};
|
||||
g_lastHostSendTime = {};
|
||||
if (ShouldLog("world_state_host_demoted")) {
|
||||
LogInfo(std::format("No longer world-state host; new host is player {}.", a_hostPlayerId));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isNowHost && hostChanged && ShouldLog("world_state_host_changed")) {
|
||||
LogInfo(std::format("World-state host is now player {}.", a_hostPlayerId));
|
||||
}
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
{
|
||||
const std::scoped_lock lock(g_pendingMutex);
|
||||
g_hasPendingWorldState = false;
|
||||
g_pendingWorldState = {};
|
||||
}
|
||||
|
||||
g_lastSentWorldState = {};
|
||||
g_lastAppliedWorldState = {};
|
||||
g_lastHostSendTime = {};
|
||||
g_worldStateHostPlayerId = std::nullopt;
|
||||
g_lastLogTimes.clear();
|
||||
}
|
||||
|
||||
void SetPendingWorldState(PendingWorldState a_state)
|
||||
{
|
||||
if (IsWorldStateHost()) {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
const std::scoped_lock lock(g_pendingMutex);
|
||||
g_pendingWorldState = std::move(a_state);
|
||||
g_hasPendingWorldState = true;
|
||||
}
|
||||
}
|
||||
|
||||
void PollHostCapture()
|
||||
{
|
||||
if (!Networking::IsConnectedToServer() || !IsWorldStateHost()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float gameHour = 0.0F;
|
||||
float gameDaysPassed = 0.0F;
|
||||
if (!TryReadCalendarValues(gameHour, gameDaysPassed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto weatherFormId = TryReadExteriorWeatherFormId();
|
||||
const bool hasWeatherFormId = weatherFormId.has_value();
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (!ShouldSendWorldState(gameHour, gameDaysPassed, hasWeatherFormId, weatherFormId, now)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Networking::SendWorldStatePacket(
|
||||
gameHour,
|
||||
gameDaysPassed,
|
||||
hasWeatherFormId,
|
||||
hasWeatherFormId ? *weatherFormId : 0U)) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_lastSentWorldState.gameHour = gameHour;
|
||||
g_lastSentWorldState.gameDaysPassed = gameDaysPassed;
|
||||
g_lastSentWorldState.hasWeatherFormId = hasWeatherFormId;
|
||||
g_lastSentWorldState.weatherFormId = weatherFormId;
|
||||
g_lastSentWorldState.initialized = true;
|
||||
g_lastHostSendTime = now;
|
||||
|
||||
if (ShouldLog("world_state_host_send")) {
|
||||
if (hasWeatherFormId) {
|
||||
LogInfo(std::format(
|
||||
"Sent worldState: gameHour={:.2f}, gameDaysPassed={:.3f}, weatherFormId=0x{:08X}.",
|
||||
gameHour,
|
||||
gameDaysPassed,
|
||||
*weatherFormId));
|
||||
} else {
|
||||
LogInfo(std::format(
|
||||
"Sent worldState: gameHour={:.2f}, gameDaysPassed={:.3f}, weatherFormId=<none>.",
|
||||
gameHour,
|
||||
gameDaysPassed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyPendingState()
|
||||
{
|
||||
if (!Networking::IsConnectedToServer() || IsWorldStateHost()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PendingWorldState pendingState{};
|
||||
{
|
||||
const std::scoped_lock lock(g_pendingMutex);
|
||||
if (!g_hasPendingWorldState) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingState = g_pendingWorldState;
|
||||
g_hasPendingWorldState = false;
|
||||
}
|
||||
|
||||
ApplyTimeState(pendingState.gameHour, pendingState.gameDaysPassed);
|
||||
ApplyWeatherState(pendingState.hasWeatherFormId, pendingState.weatherFormId);
|
||||
|
||||
g_lastAppliedWorldState.gameHour = pendingState.gameHour;
|
||||
g_lastAppliedWorldState.gameDaysPassed = pendingState.gameDaysPassed;
|
||||
g_lastAppliedWorldState.hasWeatherFormId = pendingState.hasWeatherFormId;
|
||||
g_lastAppliedWorldState.weatherFormId = pendingState.weatherFormId;
|
||||
g_lastAppliedWorldState.initialized = true;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "F4TProxyActorController.h"
|
||||
#include "F4TProxyAnimationSync.h"
|
||||
#include "F4TProxyPuppet.h"
|
||||
#include "F4TWorldStateSync.h"
|
||||
|
||||
#include "RE/A/ACTOR_STANCE.h"
|
||||
#include "RE/B/BSFixedString.h"
|
||||
@@ -654,6 +655,8 @@ namespace
|
||||
// check on the game thread after load, while separate send and log gates keep updates useful.
|
||||
taskInterface->AddTaskPermanent([]() {
|
||||
PollLocalPlayerTransform();
|
||||
F4T::WorldStateSync::PollHostCapture();
|
||||
F4T::WorldStateSync::ApplyPendingState();
|
||||
F4T::ProxyActorController::Update();
|
||||
F4T::SessionLaunch::Update();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user