Working on the server

This commit is contained in:
Vladislav Khorev committed 2026-11-05 20:12:30 +06:00
1 parent 075750442f
commit 28c6e83eda
30 files changed
+646 -362

No files matched your search

-1
View File
@@ -3,7 +3,6 @@
#include "external/nlohmann/json.hpp"
#include <iostream>
namespace FRG {
using json = nlohmann::json;
+2 -2
View File
@@ -47,7 +47,7 @@ void CharacterState::stopInPlace() {
void CharacterState::setTarget(
const Eigen::Vector3f& target,
std::string callbackName,
const LocationState& location,
const LocationNetworkState& location,
const std::string& entityId)
{
const Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z());
@@ -79,7 +79,7 @@ void CharacterState::setTarget(
}
void CharacterState::forceReplan(
const LocationState& location, const std::string& entityId)
const LocationNetworkState& location, const std::string& entityId)
{
const Eigen::Vector3f normalizedTarget(
requestedWalkTarget.x(), 0.f, requestedWalkTarget.z());
+3 -3
View File
@@ -16,7 +16,7 @@ namespace FRG {
struct CharacterUpdateContext;
struct CharacterUpdateEvents;
struct LocationState;
struct LocationNetworkState;
enum class AnimationState {
STAND = 0,
@@ -142,9 +142,9 @@ public:
void stopInPlace();
void setTarget(const Eigen::Vector3f& target,
std::string callbackName,
const LocationState& location,
const LocationNetworkState& location,
const std::string& entityId);
void forceReplan(const LocationState& location,
void forceReplan(const LocationNetworkState& location,
const std::string& entityId);
void stopAndRotateToTarget(std::string characterId);
void resetPlayerAfterDeath();
+39 -23
View File
@@ -552,7 +552,7 @@ namespace FRG
CampaignRegistry::instance().find(campaignId);
if (!campaign) return;
#if defined(NETWORK)
remoteCampaignActive = campaignId == "battle_zone";
remoteCampaignActive = campaignId == "battle_zone_multiplayer";
if (remoteCampaignActive) {
websocketGameClient = std::make_unique<WebsocketGameClient>();
remoteGameServerHost =
@@ -644,12 +644,7 @@ namespace FRG
});
loc->stopCharacter(*loc->player);
}
auto locationState = loc->stateSnapshot();
const float inclination = locationState
? locationState->cameraInclination
: 1.1036f;
server().submit(SetLocationCameraCommand{
destName, destRotY, inclination});
loc->localState().cameraAzimuth = destRotY;
loc->scriptEngine.callLocationEnterCallback();
//Reset taxi
@@ -1163,6 +1158,17 @@ namespace FRG
// The authoritative implementation advances simulation; a future
// network client can use the same call to poll/apply server updates.
gameServerHost.update(delta);
#if defined(NETWORK)
if (remoteCampaignActive && websocketGameClient &&
websocketGameClient->connectionFailed()) {
logger() << "[network] Connection failed; returning to campaign selection"
<< std::endl;
unloadCampaign();
menuManager.showMainMenu();
menuManager.showCampaignScreen();
return;
}
#endif
processServerEvents();
}
@@ -1524,11 +1530,9 @@ namespace FRG
currentLocation()->lastMouseY = eventY;
// Snapshot current angles so we can measure how far the user rotates.
auto state = currentLocation()->stateSnapshot();
if (state) {
dragStartAzimuth = state->cameraAzimuth;
dragStartInclination = state->cameraInclination;
}
const LocationLocalState& state = currentLocation()->localState();
dragStartAzimuth = state.cameraAzimuth;
dragStartInclination = state.cameraInclination;
}
}
@@ -1736,10 +1740,9 @@ namespace FRG
if (cameraDragging
&& game
&& game->tutorialStep == TutorialStep::RotateCamera) {
auto state = currentLocation()->stateSnapshot();
if (!state) return;
float deltaAz = std::abs(state->cameraAzimuth - dragStartAzimuth);
float deltaInc = std::abs(state->cameraInclination - dragStartInclination);
const LocationLocalState& state = currentLocation()->localState();
float deltaAz = std::abs(state.cameraAzimuth - dragStartAzimuth);
float deltaInc = std::abs(state.cameraInclination - dragStartInclination);
if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) {
server().submit(
CompleteTutorialStepCommand{TutorialStep::RotateCamera});
@@ -1894,12 +1897,18 @@ namespace FRG
if (content.empty()) return {};
try {
nlohmann::json root = nlohmann::json::parse(content);
if (root.value("schemaVersion", -1) != GameState::kSchemaVersion) return {};
if (root.value("persistenceVersion", -1) !=
kSinglePlayerPersistenceVersion) return {};
const auto& networkState = root.at("networkState");
if (networkState.value("schemaVersion", -1) !=
GameState::kSchemaVersion) return {};
SaveSlotInfo info;
info.empty = false;
info.campaignId = root.at("campaignId").get<std::string>();
info.locationName = root.at("currentLocationId").get<std::string>();
info.savedAt = root.value("savedAt", "");
info.campaignId =
networkState.at("campaignId").get<std::string>();
info.locationName =
networkState.at("currentLocationId").get<std::string>();
info.savedAt = networkState.value("savedAt", "");
return info;
} catch (...) {
return {};
@@ -1937,12 +1946,19 @@ namespace FRG
return;
}
if (root.value("schemaVersion", -1) != GameState::kSchemaVersion) {
if (root.value("persistenceVersion", -1) !=
kSinglePlayerPersistenceVersion ||
!root.contains("networkState") ||
root["networkState"].value("schemaVersion", -1) !=
GameState::kSchemaVersion) {
std::cerr << "[save] Unsupported save schema in " << path << std::endl;
return;
}
const std::string campaignId = root.value("campaignId", std::string());
const std::string locationId = root.value("currentLocationId", std::string());
const auto& networkState = root["networkState"];
const std::string campaignId =
networkState.value("campaignId", std::string());
const std::string locationId =
networkState.value("currentLocationId", std::string());
const CampaignConfig* campaign = CampaignRegistry::instance().find(campaignId);
if (!campaign || !campaign->findLocation(locationId)) {
std::cerr << "[save] Unknown campaign/location in " << path << std::endl;
+1 -1
View File
@@ -33,7 +33,7 @@ struct StoredChatMessage {
};
struct GameState : public ISaveable {
static constexpr int kSchemaVersion = 1;
static constexpr int kSchemaVersion = 2;
// --- World ---
// Campaign being played (see CampaignRegistry).
+21 -21
View File
@@ -1,4 +1,4 @@
#include "Location.h"
#include "Location.h"
#include "utils/Utils.h"
#include "render/OpenGlExtensions.h"
#include <iostream>
@@ -62,13 +62,13 @@ namespace FRG
{
}
std::shared_ptr<const LocationState> Location::stateSnapshot() const
std::shared_ptr<const LocationNetworkState> Location::stateSnapshot() const
{
auto snapshot = gameServer_.snapshot();
if (!snapshot) return {};
auto location = snapshot->locations.find(locationId_);
if (location == snapshot->locations.end()) return {};
return std::shared_ptr<const LocationState>(
return std::shared_ptr<const LocationNetworkState>(
std::move(snapshot), &location->second.state);
}
@@ -307,7 +307,7 @@ namespace FRG
// ---- Pending-target lookup ----
TeleportZone* Location::getTargetTeleportZone(const LocationState& state)
TeleportZone* Location::getTargetTeleportZone(const LocationNetworkState& state)
{
if (state.targetTeleportZoneId.empty()) return nullptr;
for (auto& zone : teleportZones)
@@ -321,6 +321,8 @@ namespace FRG
{
if (!params.gameState) throw std::runtime_error("Location requires authoritative state binding");
if (!params.definition) throw std::runtime_error("Location requires prepared content definition");
if (!params.localState) throw std::runtime_error("Location requires local state binding");
localState_ = params.localState;
const LocationDefinition& definition = *params.definition;
this->audioPlayer = audioPlayer;
pathVisualization.initialize();
@@ -1006,8 +1008,8 @@ namespace FRG
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
//renderer.TranslateMatrix({ 0, -6.f, 0 });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
const Eigen::Vector3f camTarget =
playerState ? playerState->position : Eigen::Vector3f::Zero();
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
@@ -1200,8 +1202,8 @@ namespace FRG
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
auto playerState = player ? player->stateSnapshot() : nullptr;
const Eigen::Vector3f camTarget =
playerState ? playerState->position : Eigen::Vector3f::Zero();
@@ -1340,8 +1342,8 @@ namespace FRG
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0, 0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
const Eigen::Vector3f camTarget =
playerState ? playerState->position : Eigen::Vector3f::Zero();
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
@@ -1586,8 +1588,8 @@ namespace FRG
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0, 0, -1.0f * Environment::zoom });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
const Eigen::Vector3f camTarget =
playerState ? playerState->position : Eigen::Vector3f::Zero();
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
@@ -1963,8 +1965,8 @@ namespace FRG
float aspect = (float)Environment::width / (float)Environment::height;
float tanHalfFov = tan(CAMERA_FOV_Y * 0.5f);
float cosAzim = cos(locationState->cameraAzimuth), sinAzim = sin(locationState->cameraAzimuth);
float cosIncl = cos(locationState->cameraInclination), sinIncl = sin(locationState->cameraInclination);
float cosAzim = cos(localState_->cameraAzimuth), sinAzim = sin(localState_->cameraAzimuth);
float cosIncl = cos(localState_->cameraInclination), sinIncl = sin(localState_->cameraInclination);
Eigen::Vector3f camRight(cosAzim, 0.f, sinAzim);
Eigen::Vector3f camForward(sinAzim * cosIncl, -sinIncl, -cosAzim * cosIncl);
@@ -2168,16 +2170,14 @@ namespace FRG
lastMouseY = eventY;
const float sensitivity = 0.005f;
auto state = stateSnapshot();
if (!state) return;
const float azimuth = state->cameraAzimuth + dx * sensitivity;
float inclination = state->cameraInclination + dy * sensitivity;
localState_->cameraAzimuth += dx * sensitivity;
float inclination =
localState_->cameraInclination + dy * sensitivity;
const float minInclination = M_PI * 30.f / 180.f;
const float maxInclination = M_PI * 0.5f;
inclination = max(minInclination, min(maxInclination, inclination));
gameServer_.submit(SetLocationCameraCommand{
locationId_, azimuth, inclination});
localState_->cameraInclination =
max(minInclination, min(maxInclination, inclination));
}
}
+8 -4
View File
@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "render/Renderer.h"
#include "Environment.h"
@@ -48,7 +48,7 @@ namespace FRG
float radius = 1.5f;
float hysteresis = 0.3f; // exit fires at radius + hysteresis to prevent flickering
bool enabled = true;
bool playerInside = false; // runtime tracking state, not serialized (mirrored in LocationState)
bool playerInside = false; // runtime tracking state, not serialized (mirrored in LocationNetworkState)
};
struct LocationSetup
@@ -68,6 +68,7 @@ namespace FRG
// setup() creates a private one when this is null.
std::shared_ptr<Character> player;
LocationGameState* gameState = nullptr;
std::shared_ptr<LocationLocalState> localState;
};
class Location
@@ -78,7 +79,9 @@ namespace FRG
// Detaches the (shared, longer-lived) player from this location.
~Location();
const std::string& locationId() const { return locationId_; }
std::shared_ptr<const LocationState> stateSnapshot() const;
std::shared_ptr<const LocationNetworkState> stateSnapshot() const;
LocationLocalState& localState() { return *localState_; }
const LocationLocalState& localState() const { return *localState_; }
bool isDarklands() const;
bool isNight() const;
bool isDawn() const;
@@ -234,13 +237,14 @@ namespace FRG
private:
IGameServer& gameServer_;
std::string locationId_;
std::shared_ptr<LocationLocalState> localState_;
std::unordered_map<std::string, std::shared_ptr<Character>> remotePlayers_;
void synchronizeRemotePlayers();
void drawRemotePlayerNames(
const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix);
// ---- Pointer-to-index helpers ----
TeleportZone* getTargetTeleportZone(const LocationState& state);
TeleportZone* getTargetTeleportZone(const LocationNetworkState& state);
enum class InstancedTexture { None, Color, Darklands };
+2 -2
View File
@@ -14,7 +14,7 @@ bool LocationGameState::setup(
LocationDefinition content, const std::string& zipFile)
{
const bool initializeRuntimeFields = !hasPersistentState;
if (!location) location = std::make_shared<LocationState>();
if (!location) location = std::make_shared<LocationNetworkState>();
if (location->navigationMaps.empty())
location->initNavigation(content.config.navigationJsonPaths, zipFile);
lockInteractiveObjectsDuringTutorial =
@@ -377,7 +377,7 @@ void LocationGameState::save(nlohmann::json& out) const
void LocationGameState::load(const nlohmann::json& in)
{
hasPersistentState = true;
if (!location) location = std::make_shared<LocationState>();
if (!location) location = std::make_shared<LocationNetworkState>();
location->load(in.at("state"));
const auto& npcValues = in.at("npcs");
+1 -1
View File
@@ -49,7 +49,7 @@ struct LocationSimulationEvents {
// simulation objects stable addresses; runtime renderers and Location pointers
// are deliberately absent, and public clients receive value-only snapshots.
struct LocationGameState {
std::shared_ptr<LocationState> location = std::make_shared<LocationState>();
std::shared_ptr<LocationNetworkState> location = std::make_shared<LocationNetworkState>();
std::unordered_map<std::string, std::shared_ptr<CharacterState>> npcs;
std::unordered_map<std::string, std::shared_ptr<InteractiveObjectState>> interactiveObjects;
bool lockInteractiveObjectsDuringTutorial = false;
+82 -12
View File
@@ -1,7 +1,85 @@
#include "LocationState.h"
#include <stdexcept>
namespace FRG {
std::shared_ptr<LocationLocalState> ClientLocationState::getOrCreate(
const std::string& locationId)
{
auto& state = locations[locationId];
if (!state) state = std::make_shared<LocationLocalState>();
return state;
}
void ClientLocationState::clear()
{
locations.clear();
}
void LocationLocalState::save(nlohmann::json& out) const
{
out = {
{"cameraAzimuth", cameraAzimuth},
{"cameraInclination", cameraInclination}
};
}
void LocationLocalState::load(const nlohmann::json& in)
{
cameraAzimuth = in.at("cameraAzimuth").get<float>();
cameraInclination = in.at("cameraInclination").get<float>();
}
void ClientLocationState::save(nlohmann::json& out) const
{
out["locations"] = nlohmann::json::object();
for (const auto& [id, state] : locations) {
if (!state) continue;
nlohmann::json value;
state->save(value);
out["locations"][id] = std::move(value);
}
}
void ClientLocationState::load(const nlohmann::json& in)
{
const auto& values = in.at("locations");
if (!values.is_object())
throw std::runtime_error("Client location state locations must be an object");
for (auto& [id, state] : locations)
if (state) *state = LocationLocalState{};
for (const auto& [id, value] : values.items())
getOrCreate(id)->load(value);
}
void saveSinglePlayerPersistence(
const nlohmann::json& networkState,
const ClientLocationState& localState,
nlohmann::json& out)
{
nlohmann::json savedLocalState;
localState.save(savedLocalState);
out = {
{"persistenceVersion", kSinglePlayerPersistenceVersion},
{"networkState", networkState},
{"localState", std::move(savedLocalState)}
};
}
void loadSinglePlayerPersistence(
const nlohmann::json& in,
nlohmann::json& networkState,
ClientLocationState& localState)
{
const int version = in.value("persistenceVersion", -1);
if (version != kSinglePlayerPersistenceVersion)
throw std::runtime_error(
"Unsupported single-player persistence version: " +
std::to_string(version));
networkState = in.at("networkState");
localState.load(in.at("localState"));
}
namespace {
nlohmann::json saveCar(const NpcCar& car)
{
@@ -44,12 +122,12 @@ namespace FRG {
}
std::vector<Eigen::Vector3f> LocationState::planPathExceptMyself(const Eigen::Vector3f& start, const Eigen::Vector3f& end, const std::string& entityId) const
std::vector<Eigen::Vector3f> LocationNetworkState::planPathExceptMyself(const Eigen::Vector3f& start, const Eigen::Vector3f& end, const std::string& entityId) const
{
return navigationMaps[activeNavigationIndex].findPathToNearest(start, end, {});
}
void LocationState::initNavigation(
void LocationNetworkState::initNavigation(
const std::vector<std::string>& paths, const std::string& zipFile)
{
const int savedActiveNavigationIndex = activeNavigationIndex;
@@ -68,11 +146,8 @@ namespace FRG {
: 0;
}
void LocationState::save(nlohmann::json& out) const
void LocationNetworkState::save(nlohmann::json& out) const
{
out["cameraAzimuth"] = cameraAzimuth;
out["cameraInclination"] = cameraInclination;
out["activeNavigationIndex"] = activeNavigationIndex;
out["targetInteractiveObjectId"] = targetInteractiveObjectId;
@@ -85,13 +160,8 @@ void LocationState::save(nlohmann::json& out) const
};
}
void LocationState::load(const nlohmann::json& in)
void LocationNetworkState::load(const nlohmann::json& in)
{
//logger() << "LocationState::load step 1" << std::endl;
cameraAzimuth = in.value("cameraAzimuth", cameraAzimuth);
cameraInclination = in.value("cameraInclination", cameraInclination);
activeNavigationIndex = in.value("activeNavigationIndex", activeNavigationIndex);
targetInteractiveObjectId = in.value("targetInteractiveObjectId", std::string());
+31 -5
View File
@@ -3,11 +3,41 @@
#include "ISaveable.h"
#include "NpcCar.h"
#include "navigation/PathFinder.h"
#include <memory>
#include <string>
#include <unordered_map>
namespace FRG {
struct LocationState : public ISaveable {
inline constexpr int kSinglePlayerPersistenceVersion = 1;
struct LocationLocalState : public ISaveable {
float cameraAzimuth = -2.35f;
float cameraInclination = 1.1036f;
void save(nlohmann::json& out) const override;
void load(const nlohmann::json& in) override;
};
struct ClientLocationState : public ISaveable {
std::unordered_map<std::string, std::shared_ptr<LocationLocalState>> locations;
std::shared_ptr<LocationLocalState> getOrCreate(const std::string& locationId);
void clear();
void save(nlohmann::json& out) const override;
void load(const nlohmann::json& in) override;
};
void saveSinglePlayerPersistence(
const nlohmann::json& networkState,
const ClientLocationState& localState,
nlohmann::json& out);
void loadSinglePlayerPersistence(
const nlohmann::json& in,
nlohmann::json& networkState,
ClientLocationState& localState);
struct LocationNetworkState : public ISaveable {
//This value is populated from the JSON configs and should not be changed during the regular gameplay.
//It might be changed only in the editor mode during development.
std::vector<PathFinder> navigationMaps;
@@ -16,10 +46,6 @@ struct LocationState : public ISaveable {
const std::vector<std::string>& paths,
const std::string& zipFile = std::string());
// ---- Camera ----
float cameraAzimuth = -2.35f;
float cameraInclination = 1.1036f;
// ---- Navigation ----
int activeNavigationIndex = 0;
+2 -2
View File
@@ -57,6 +57,8 @@ namespace FRG {
void showMainMenu();
void showMenuAfterGameCompleted();
// Returns to the main menu with the campaign list already open.
void showCampaignScreen();
bool isMainMenuOpen() const { return uiState_ == GameUiState::MainMenu; }
void openPhoneScreen();
@@ -223,8 +225,6 @@ namespace FRG {
void showLanguageScreen();
void showLoadGameScreen();
void showSaveGameScreen();
// Campaign list opened by "Start game".
void showCampaignScreen();
// Text of a load/save slot button.
std::string slotLabel(const SaveSlotInfo& info);
+1 -7
View File
@@ -30,7 +30,7 @@ struct InteractiveObjectSnapshot {
struct LocationSnapshot {
std::string id;
LocationState state;
LocationNetworkState state;
std::unordered_map<std::string, CharacterSnapshot> npcs;
std::unordered_map<std::string, InteractiveObjectSnapshot> interactiveObjects;
std::unordered_map<std::string, TriggerZoneGameState> triggerZones;
@@ -95,11 +95,6 @@ struct AppendChatMessageCommand {
struct SetTaxiCalledCommand { bool called; };
struct StartTaxiCommand { std::string locationId; };
struct ResetTaxiCommand { std::string locationId; };
struct SetLocationCameraCommand {
std::string locationId;
float azimuth;
float inclination;
};
struct SetLocationTargetsCommand {
std::string locationId;
std::string interactiveObjectId;
@@ -232,7 +227,6 @@ using GameCommand = std::variant<
SetTaxiCalledCommand,
StartTaxiCommand,
ResetTaxiCommand,
SetLocationCameraCommand,
SetLocationTargetsCommand,
SwitchLocationNavigationCommand,
SetNavigationAreaAvailableCommand,
+3 -9
View File
@@ -305,12 +305,6 @@ void InternalGameServer::submit(const GameCommand& command)
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end())
location->second.resetTaxi();
} else if constexpr (std::is_same_v<T, SetLocationCameraCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) {
location->second.location->cameraAzimuth = value.azimuth;
location->second.location->cameraInclination = value.inclination;
}
} else if constexpr (std::is_same_v<T, SetLocationTargetsCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) {
@@ -326,7 +320,7 @@ void InternalGameServer::submit(const GameCommand& command)
value.index >= 0 &&
value.index < static_cast<int>(
location->second.location->navigationMaps.size())) {
LocationState& locationState = *location->second.location;
LocationNetworkState& locationState = *location->second.location;
locationState.activeNavigationIndex = value.index;
for (const auto& [id, player] : state_.players)
if (player) player->forceReplan(locationState, id);
@@ -336,7 +330,7 @@ void InternalGameServer::submit(const GameCommand& command)
} else if constexpr (std::is_same_v<T, SetNavigationAreaAvailableCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) {
LocationState& locationState = *location->second.location;
LocationNetworkState& locationState = *location->second.location;
const int index = locationState.activeNavigationIndex;
if (index >= 0 &&
index < static_cast<int>(locationState.navigationMaps.size())) {
@@ -347,7 +341,7 @@ void InternalGameServer::submit(const GameCommand& command)
} else if constexpr (std::is_same_v<T, AddNavigationObstacleCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) {
LocationState& locationState = *location->second.location;
LocationNetworkState& locationState = *location->second.location;
const int index = locationState.activeNavigationIndex;
if (index >= 0 &&
index < static_cast<int>(locationState.navigationMaps.size())) {
+14 -2
View File
@@ -15,18 +15,21 @@ void LocalGameServerHost::initializeCampaign(
const std::string& campaignId, const std::string& questsPath,
const std::string& zipFile)
{
localState_.clear();
server_.initializeCampaign(campaignId, questsPath, zipFile);
}
void LocalGameServerHost::clearCampaign()
{
runtimeLocations_.clear();
localState_.clear();
server_.clearCampaign();
}
void LocalGameServerHost::resetCampaign(
const std::string& questsPath, const std::string& zipFile)
{
localState_.clear();
server_.resetCampaign(questsPath, zipFile);
}
@@ -44,6 +47,7 @@ void LocalGameServerHost::setupRuntimeLocation(
throw std::runtime_error("Location state was not prepared");
params.definition = stored->second.definition;
params.gameState = &stored->second;
params.localState = localState_.getOrCreate(id);
location->setup(params, audioPlayer);
server_.publishMutation();
}
@@ -122,12 +126,20 @@ void LocalGameServerHost::update(std::int64_t deltaMs)
void LocalGameServerHost::save(nlohmann::json& out)
{
synchronizeRuntimeAuxiliaryState();
server_.save(out);
nlohmann::json networkState;
server_.save(networkState);
saveSinglePlayerPersistence(networkState, localState_, out);
}
void LocalGameServerHost::load(const nlohmann::json& in)
{
server_.load(in);
nlohmann::json networkState;
ClientLocationState loadedLocalState;
loadSinglePlayerPersistence(in, networkState, loadedLocalState);
server_.load(networkState);
nlohmann::json localStateJson;
loadedLocalState.save(localStateJson);
localState_.load(localStateJson);
applyStateToRuntime();
}
+2
View File
@@ -1,6 +1,7 @@
#pragma once
#include "IGameServerHost.h"
#include "../LocationState.h"
#include <unordered_map>
namespace FRG {
@@ -39,6 +40,7 @@ private:
void applyStateToRuntime();
InternalGameServer& server_;
ClientLocationState localState_;
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_;
};
+1
View File
@@ -1,6 +1,7 @@
#include "NetworkProtocol.h"
#include <exception>
namespace FRG {
std::string makeStateMessage(const nlohmann::json& state)
+12 -2
View File
@@ -15,6 +15,7 @@ void RemoteGameServerHost::initializeCampaign(
const std::string& campaignId, const std::string&,
const std::string& zipFile)
{
localState_.clear();
const CampaignConfig* campaign =
CampaignRegistry::instance().find(campaignId);
if (!campaign)
@@ -25,6 +26,7 @@ void RemoteGameServerHost::initializeCampaign(
void RemoteGameServerHost::clearCampaign()
{
runtimeLocations_.clear();
localState_.clear();
client_.replica_.clearCampaign();
}
@@ -48,6 +50,7 @@ void RemoteGameServerHost::setupRuntimeLocation(
throw std::runtime_error("Remote location state was not prepared");
params.definition = stored->second.definition;
params.gameState = &stored->second;
params.localState = localState_.getOrCreate(id);
location->setup(params, audioPlayer);
}
@@ -101,9 +104,16 @@ void RemoteGameServerHost::save(nlohmann::json&)
// Persistence remains server-side.
}
void RemoteGameServerHost::load(const nlohmann::json&)
void RemoteGameServerHost::load(const nlohmann::json& in)
{
// Persistence remains server-side.
// The authoritative half is loaded by the standalone server. Each client
// restores only its local presentation state from the shared start state.
nlohmann::json ignoredNetworkState;
ClientLocationState loadedLocalState;
loadSinglePlayerPersistence(in, ignoredNetworkState, loadedLocalState);
nlohmann::json localStateJson;
loadedLocalState.save(localStateJson);
localState_.load(localStateJson);
}
} // namespace FRG
+2
View File
@@ -1,6 +1,7 @@
#pragma once
#include "IGameServerHost.h"
#include "../LocationState.h"
#include <unordered_map>
namespace FRG {
@@ -33,6 +34,7 @@ public:
private:
WebsocketGameClient& client_;
ClientLocationState localState_;
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_;
};
+8
View File
@@ -47,6 +47,7 @@ public:
}
bool connected() const { return connected_.load(); }
bool connectionFailed() const { return connectionFailed_.load(); }
private:
void resolve()
@@ -101,6 +102,7 @@ private:
void fail(const char* operation, beast::error_code error)
{
connected_.store(false);
connectionFailed_.store(true);
std::cerr << "[network] WebSocket " << operation << " failed: "
<< error.message() << '\n';
}
@@ -114,6 +116,7 @@ private:
std::string port_;
std::string path_;
std::atomic_bool connected_{false};
std::atomic_bool connectionFailed_{false};
std::mutex messagesMutex_;
std::vector<std::string> messages_;
};
@@ -186,4 +189,9 @@ bool WebsocketGameClient::connected() const
return impl_->connected();
}
bool WebsocketGameClient::connectionFailed() const
{
return impl_->connectionFailed();
}
} // namespace FRG
+1
View File
@@ -29,6 +29,7 @@ public:
void load(const nlohmann::json& in) override;
bool connected() const;
bool connectionFailed() const;
private:
friend class RemoteGameServerHost;