Working on the server
This commit is contained in:
1 parent
075750442f
commit
28c6e83eda
30 files changed
+646
-362
No files matched your search
@@ -3,7 +3,6 @@
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <iostream>
|
||||
|
||||
|
||||
namespace FRG {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 };
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
#include "NetworkProtocol.h"
|
||||
#include <exception>
|
||||
|
||||
|
||||
namespace FRG {
|
||||
|
||||
std::string makeStateMessage(const nlohmann::json& state)
|
||||
|
||||
@@ -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
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -29,6 +29,7 @@ public:
|
||||
void load(const nlohmann::json& in) override;
|
||||
|
||||
bool connected() const;
|
||||
bool connectionFailed() const;
|
||||
|
||||
private:
|
||||
friend class RemoteGameServerHost;
|
||||
|
||||
Reference in new issue
Block a user