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
+418 -134

No files matched your search

+22 -13
View File
@@ -8,12 +8,13 @@ This refactor does **not** add multiplayer networking. It establishes the state
## Architecture Overview
The architecture has four layers:
The architecture has five layers:
1. `GameState` is the aggregate root for persistent, authoritative state.
2. `InternalGameServer` owns `GameState`, advances the local simulation, handles commands, and produces immutable snapshots.
3. Runtime objects hold rendering resources. `Character` and `Location` read immutable server snapshots by stable ID, while `InteractiveObject` still uses a transitional non-owning binding to server-owned state. `Location` also translates local input, scripts, and collision results into semantic server commands.
4. Client-facing systems such as `MenuManager` read immutable snapshots and submit typed commands through `IGameServer`.
3. `ClientLocationState` owns per-client `LocationLocalState` records, including camera orientation. Graphical hosts own this state; it is never replicated.
4. Runtime objects hold rendering resources. `Character` and `Location` read immutable server snapshots by stable ID, while `InteractiveObject` still uses a transitional non-owning binding to server-owned state. `Location` also translates local input, scripts, and collision results into semantic server commands.
5. Client-facing systems such as `MenuManager` read immutable snapshots and submit typed commands through `IGameServer`.
```text
Input / UI
@@ -69,7 +70,7 @@ lookup and simulation to treat every campaign player by stable ID.
Each location record owns:
- One `LocationState`.
- One `LocationNetworkState`.
- NPC `CharacterState` objects keyed by configured ID.
- `InteractiveObjectState` objects keyed by configured ID.
- Trigger-zone enabled/inside flags.
@@ -193,7 +194,7 @@ Gameplay operations mutate the authoritative `CharacterState`, not the runtime
`Character`:
- `setTarget()` normalizes the destination, avoids redundant requests, stores the
optional Lua arrival function name, and asks the authoritative `LocationState` to
optional Lua arrival function name, and asks the authoritative `LocationNetworkState` to
call `planPathExceptMyself()`.
- `forceReplan()` recalculates the current requested destination using the same
location navigation state.
@@ -210,7 +211,10 @@ collision handling, and presentation-event processing.
Runtime visual objects do not own their authoritative state objects.
- `Location` keeps only its `IGameServer` reference and stable location ID. It resolves its immutable `LocationState` view from `IGameServer::snapshot()`.
- `Location` keeps its `IGameServer` reference, stable location ID, and a shared
`LocationLocalState` supplied by the graphical host. It resolves its immutable
`LocationNetworkState` view from `IGameServer::snapshot()`, while rendering and
camera input access the local state directly.
- `Character` keeps only its `IGameServer` reference plus stable entity and location IDs. It resolves the player-first character view from `IGameServer::snapshot()`.
- `InteractiveObject` keeps a private non-owning reference to its `InteractiveObjectState`; its configured object name is its stable ID.
@@ -224,9 +228,10 @@ and presentation or input work is skipped.
The authoritative state objects are owned by `GameState` or `LocationGameState`.
The internal server advances them before publishing a new snapshot revision.
Runtime input, scripts, and collision handling submit semantic commands that invoke
authoritative character and location operations. Location commands cover camera
orientation, pending interaction targets, navigation switching and area changes,
editor obstacles, and taxi reset. No whole-location commit command is exposed.
authoritative character and location operations. Location commands cover pending
interaction targets, navigation switching and area changes, editor obstacles, and
taxi reset. Camera orientation is deliberately not a server command. No
whole-location commit command is exposed.
Navigation target selection, replanning, stopping, facing, death reset, health
assignment, and attack-combo clock changes are implemented by `CharacterState`.
@@ -268,7 +273,7 @@ The normal frame flow is:
4. Attack impacts are queued, then damage is applied after all characters advance.
5. The runtime location consumes value-only events. Navigation requests are sent
back as semantic destination commands, whose server handler calls
`CharacterState::setTarget()` with the authoritative `LocationState`; arrival
`CharacterState::setTarget()` with the authoritative `LocationNetworkState`; arrival
function names are resolved through `ScriptEngine`.
6. The server invalidates its cached snapshot and advances the revision so runtime
presentation can observe the completed simulation tick.
@@ -326,8 +331,11 @@ The player state remains campaign-level and survives location changes.
## Save and Load Schema
Persistence uses one strict, versioned schema. `schemaVersion` is required and
unsupported versions are rejected rather than migrated.
Authoritative persistence uses one strict, versioned schema. `schemaVersion` is
required and unsupported versions are rejected rather than migrated. A
single-player save is a separate versioned envelope containing `networkState`
(the authoritative `GameState` JSON) and `localState` (the client-local location
map). WebSocket replication sends only the authoritative document.
- Campaign players are stored once in an ID-keyed `players` object.
- Locations are keyed by location ID.
@@ -453,7 +461,8 @@ cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -
## Main Files
- `src/GameState.h` / `src/GameState.cpp` — aggregate state and top-level persistence.
- `src/LocationGameState.h` / `src/LocationGameState.cpp` — typed location state, simulation, and ID-keyed persistence.
- `src/LocationState.h` / `src/LocationState.cpp` — separate local camera and authoritative network location records, plus single-player persistence composition.
- `src/LocationGameState.h` / `src/LocationGameState.cpp` — authoritative typed location state, simulation, and ID-keyed persistence.
- `src/content/LocationDefinition.h` / `.cpp` — renderer-free content definitions,
parsing, and animation metadata loading.
- `src/game_server/IGameServer.h` — public snapshot and command contract.
+25 -11
View File
@@ -35,6 +35,8 @@ not enable the battle-zone test bot.
thread from `update()`.
- It maintains an internal `InternalGameServer` replica so existing snapshot
consumers and transitional runtime-state bindings continue to work.
- Camera orientation lives in host-owned `LocationLocalState`, so incoming
authoritative snapshots cannot overwrite camera movement.
- `submit()` intentionally discards all commands.
- `drainEvents()` returns no events.
- Client-side save and load calls are ignored because persistence remains
@@ -51,11 +53,21 @@ The graphical game uses stable router objects for `IGameServer` and
When `NETWORK` is defined:
- `battle_zone` uses `WebsocketGameClient` and `RemoteGameServerHost`.
- Every other campaign uses `InternalGameServer` and `LocalGameServerHost`.
- `battle_zone_multiplayer` uses `WebsocketGameClient` and `RemoteGameServerHost`.
- `main`, `battle_zone`, and every other campaign use `InternalGameServer` and
`LocalGameServerHost`.
The WebSocket client is created only when `battle_zone` is selected and is
destroyed when that campaign is unloaded.
The WebSocket client is created only when `battle_zone_multiplayer` is selected
and is destroyed when that campaign is unloaded. The single-player and
multiplayer Battle Zone entries share location content and the start-state path,
so the state can be regenerated through the single-player entry. At startup, the
standalone server loads the authoritative half of that shared save and rewrites
its campaign ID to `battle_zone_multiplayer`; each remote client restores only
the local presentation half.
If DNS resolution, TCP connection, the WebSocket handshake, or the active socket
fails, the client unloads the remote campaign and returns to the campaign
selection screen.
## JSON Protocol
@@ -66,20 +78,22 @@ Messages use a versioned JSON envelope:
"protocolVersion": 1,
"type": "state",
"payload": {
"schemaVersion": 1
"schemaVersion": 2
}
}
```
The payload temporarily uses the existing `GameState::save()` representation.
The payload uses the authoritative `GameState::save()` representation and excludes
the client-local location map, including camera orientation.
The client applies it through `InternalGameServer::load()`.
Malformed JSON, unsupported protocol versions, unsupported message types, and
invalid state payloads are logged and ignored.
This temporary full-state format is expected to be replaced or separated from
the persistence schema as command handling and production replication are
implemented.
Single-player persistence uses a separate envelope that combines this authoritative
document with client-local location state. The temporary full-state network format
is still expected to be replaced by a dedicated replication schema or deltas as
command handling and production replication are implemented.
## Build Configuration
@@ -106,7 +120,7 @@ cmake --build server/build --config Debug --target GameServer
Run it from the repository root:
```powershell
.\server\build\Debug\GameServer.exe --campaign battle_zone --location uni_exterior
.\server\build\Debug\GameServer.exe --campaign battle_zone_multiplayer --location uni_exterior
```
The listener defaults to `0.0.0.0:8080`. It can be changed with:
@@ -116,7 +130,7 @@ The listener defaults to `0.0.0.0:8080`. It can be changed with:
--port N
```
Launch the Windows game and select `battle_zone`. The client connects to
Launch the Windows game and select `battle_zone_multiplayer`. The client connects to
`127.0.0.1:8080`. Selecting another campaign continues to use the local server.
## Verification
+14 -1
View File
@@ -17,7 +17,20 @@
},
{
"id": "battle_zone",
"title": { "ru": "Зона боя", "en": "Battle Zone" },
"title": { "ru": "Зона боя (одиночная)", "en": "Battle Zone (Single Player)" },
"locations": [
"resources/config/locations/battle_zone_uni_exterior.json"
],
"startLocation": "uni_exterior",
"startMusic": "music/bishkek univer day.ogg",
"startState": "resources/config/start_state_battle_zone.json",
"quests": "resources/config/quests_battle_zone.json",
"tutorial": false,
"loadingScreen": "resources/w/ui/screen_loading.json"
},
{
"id": "battle_zone_multiplayer",
"title": { "ru": "Зона боя (сетевая)", "en": "Battle Zone (Multiplayer)" },
"locations": [
"resources/config/locations/battle_zone_uni_exterior.json"
],
File renamed without changes.
+13 -4
View File
@@ -1,4 +1,13 @@
{
"localState": {
"locations": {
"uni_exterior": {
"cameraAzimuth": -2.3499999046325684,
"cameraInclination": 1.103600025177002
}
}
},
"networkState": {
"campaignId": "battle_zone",
"chatHistory": [
[],
@@ -117,8 +126,6 @@
"scriptGlobals": {},
"state": {
"activeNavigationIndex": 0,
"cameraAzimuth": -2.3499999046325684,
"cameraInclination": 1.103600025177002,
"targetInteractNpcId": "",
"targetInteractiveObjectId": "",
"targetTeleportZoneId": "",
@@ -232,12 +239,14 @@
"questJournal": {
"quests": []
},
"savedAt": "2026-10-30 14:50",
"schemaVersion": 1,
"savedAt": "2026-11-05 17:43",
"schemaVersion": 2,
"taxiIsCalled": false,
"tutorialJournalScreenOpened": false,
"tutorialNeedOpenTaxiScreen": false,
"tutorialPhoneChatScreenOpened": false,
"tutorialStep": 0,
"tutorialTaxiHintDone": false
},
"persistenceVersion": 1
}
+3 -3
View File
@@ -42,11 +42,11 @@ real-time delay.
Start the first multiplayer transport iteration:
```powershell
.\server\build\Release\GameServer.exe --campaign battle_zone --location uni_exterior
.\server\build\Release\GameServer.exe --campaign battle_zone_multiplayer --location uni_exterior
```
The listener defaults to `ws://0.0.0.0:8080`. Launch the Windows graphical game
afterward and select `battle_zone`; that campaign connects to
afterward and select `battle_zone_multiplayer`; that campaign connects to
`ws://127.0.0.1:8080`. Other campaigns continue to use the in-process server.
The server broadcasts an initial full state and then one every five seconds.
Client commands are intentionally discarded in this iteration.
@@ -83,4 +83,4 @@ CTest covers:
- repeated `LocationGameState::setup()` preserving mutable runtime state;
- ticking a prepared location with zero players;
- idle server startup;
- headless loading of `battle_zone/uni_exterior`.
- headless loading of `battle_zone_multiplayer/uni_exterior`.
+29 -1
View File
@@ -3,6 +3,8 @@
#include "game_server/WebsocketGameServer.h"
#endif
#include "Campaign.h"
#include "LocationState.h"
#include "utils/Utils.h"
#include <atomic>
#include <chrono>
#include <csignal>
@@ -88,7 +90,7 @@ int main(int argc, char** argv)
}
//Temporary override:
campaignId = "battle_zone";
campaignId = "battle_zone_multiplayer";
locationId = "uni_exterior";
std::signal(SIGINT, requestStop);
@@ -98,6 +100,7 @@ int main(int argc, char** argv)
if (!campaignId.empty()) {
try {
FRG::CampaignRegistry::instance().loadFromJson(campaignsPath);
std::cout << "[server] Loading...\n" << std::flush;
const FRG::CampaignConfig* campaign =
FRG::CampaignRegistry::instance().find(campaignId);
if (!campaign)
@@ -113,7 +116,32 @@ int main(int argc, char** argv)
"' has no location '" + selectedLocation + "'");
server.startCampaign(*campaign, std::string(), true, false);
server.prepareLocation(*location);
const std::string startState =
FRG::readTextFile(campaign->startStatePath);
if (!startState.empty()) {
try {
nlohmann::json persisted =
nlohmann::json::parse(startState);
nlohmann::json networkState;
FRG::ClientLocationState ignoredLocalState;
FRG::loadSinglePlayerPersistence(
persisted, networkState, ignoredLocalState);
networkState["campaignId"] = campaignId;
networkState["currentLocationId"] = selectedLocation;
server.load(networkState);
std::cout << "Loaded initial state '"
<< campaign->startStatePath << "'\n";
} catch (const std::exception& error) {
std::cerr << "Ignoring invalid initial state '"
<< campaign->startStatePath << "': "
<< error.what() << '\n';
}
}
server.activateLocation(selectedLocation);
if (!campaign->tutorial) {
server.submit(FRG::SetTutorialStepCommand{
FRG::TutorialStep::Completed});
}
std::cout << "Loaded campaign '" << campaignId
<< "', location '" << selectedLocation
<< "' with shared player '" << server.localPlayerId() << "'\n";
+64 -1
View File
@@ -16,10 +16,69 @@ int fail(const char* message)
int main()
{
FRG::ClientLocationState localState;
auto camera = localState.getOrCreate("test_location");
camera->cameraAzimuth = 0.75f;
camera->cameraInclination = 1.25f;
nlohmann::json localJson;
localState.save(localJson);
FRG::ClientLocationState restoredLocalState;
restoredLocalState.load(localJson);
const auto restoredCamera =
restoredLocalState.getOrCreate("test_location");
if (restoredCamera->cameraAzimuth != 0.75f ||
restoredCamera->cameraInclination != 1.25f) {
return fail("Local camera state did not round trip");
}
FRG::LocationNetworkState networkLocationState;
nlohmann::json networkLocationJson;
networkLocationState.save(networkLocationJson);
if (networkLocationJson.contains("cameraAzimuth") ||
networkLocationJson.contains("cameraInclination")) {
return fail("Network location state contains local camera fields");
}
const nlohmann::json authoritativeJson{
{"schemaVersion", FRG::GameState::kSchemaVersion},
{"campaignId", "test"}
};
nlohmann::json persistenceJson;
FRG::saveSinglePlayerPersistence(
authoritativeJson, localState, persistenceJson);
nlohmann::json restoredAuthoritativeJson;
FRG::ClientLocationState restoredPersistenceLocalState;
FRG::loadSinglePlayerPersistence(
persistenceJson, restoredAuthoritativeJson,
restoredPersistenceLocalState);
if (restoredAuthoritativeJson != authoritativeJson ||
restoredPersistenceLocalState.getOrCreate("test_location")
->cameraAzimuth != 0.75f) {
return fail("Composed single-player state did not round trip");
}
if (persistenceJson.at("networkState").contains("localState")) {
return fail("Local state leaked into authoritative state");
}
const std::filesystem::path repositoryRoot =
std::filesystem::path(__FILE__).parent_path().parent_path().parent_path();
std::filesystem::current_path(repositoryRoot);
FRG::CampaignRegistry::instance().loadFromJson(
"resources/config/campaigns.json");
const FRG::CampaignConfig* singlePlayerBattleZone =
FRG::CampaignRegistry::instance().find("battle_zone");
const FRG::CampaignConfig* multiplayerBattleZoneConfig =
FRG::CampaignRegistry::instance().find("battle_zone_multiplayer");
if (FRG::CampaignRegistry::instance().all().size() != 3 ||
!singlePlayerBattleZone || !multiplayerBattleZoneConfig) {
return fail("Expected three campaigns including both Battle Zone modes");
}
if (singlePlayerBattleZone->startStatePath !=
multiplayerBattleZoneConfig->startStatePath) {
return fail("Battle Zone modes do not share their initial state");
}
FRG::LocationDefinition definition;
definition.config.id = "test_location";
@@ -87,8 +146,12 @@ int main()
}
FRG::InternalGameServer networkServer;
networkServer.startCampaign(battleZone, {}, true, false);
FRG::CampaignConfig multiplayerBattleZone = battleZone;
multiplayerBattleZone.id = "battle_zone_multiplayer";
networkServer.startCampaign(multiplayerBattleZone, {}, true, false);
const auto networkSnapshot = networkServer.snapshot();
if (networkSnapshot->campaignId != "battle_zone_multiplayer")
return fail("Network campaign ID was not retained");
if (networkSnapshot->players.count("player") != 1)
return fail("Network campaign shared player was not created");
if (networkSnapshot->players.count("battle_zone_test_bot") != 0)
+12 -3
View File
@@ -1,4 +1,5 @@
#include "game_server/NetworkProtocol.h"
#include "LocationState.h"
#include <cstdlib>
#include <iostream>
#include <string>
@@ -16,10 +17,14 @@ void require(bool condition, const char* message)
int main()
{
FRG::LocationNetworkState locationState;
nlohmann::json savedLocationState;
locationState.save(savedLocationState);
const nlohmann::json original{
{"schemaVersion", 1},
{"campaignId", "battle_zone"},
{"players", {{"player", {{"hp", 200.0}}}}}
{"schemaVersion", 2},
{"campaignId", "battle_zone_multiplayer"},
{"players", {{"player", {{"hp", 200.0}}}}},
{"locations", {{"uni_exterior", {{"state", savedLocationState}}}}}
};
const std::string message = FRG::makeStateMessage(original);
nlohmann::json parsed;
@@ -28,6 +33,10 @@ int main()
FRG::parseStateMessage(message, parsed, error),
"valid state envelope was rejected");
require(parsed == original, "state payload did not round trip");
require(
!parsed["locations"]["uni_exterior"]["state"].contains("cameraAzimuth") &&
!parsed["locations"]["uni_exterior"]["state"].contains("cameraInclination"),
"local camera state leaked into the network payload");
require(
!FRG::parseStateMessage(
-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;