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

+22 -13
View File
@@ -8,12 +8,13 @@ This refactor does **not** add multiplayer networking. It establishes the state
## Architecture Overview ## Architecture Overview
The architecture has four layers: The architecture has five layers:
1. `GameState` is the aggregate root for persistent, authoritative state. 1. `GameState` is the aggregate root for persistent, authoritative state.
2. `InternalGameServer` owns `GameState`, advances the local simulation, handles commands, and produces immutable snapshots. 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. 3. `ClientLocationState` owns per-client `LocationLocalState` records, including camera orientation. Graphical hosts own this state; it is never replicated.
4. Client-facing systems such as `MenuManager` read immutable snapshots and submit typed commands through `IGameServer`. 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 ```text
Input / UI Input / UI
@@ -69,7 +70,7 @@ lookup and simulation to treat every campaign player by stable ID.
Each location record owns: Each location record owns:
- One `LocationState`. - One `LocationNetworkState`.
- NPC `CharacterState` objects keyed by configured ID. - NPC `CharacterState` objects keyed by configured ID.
- `InteractiveObjectState` objects keyed by configured ID. - `InteractiveObjectState` objects keyed by configured ID.
- Trigger-zone enabled/inside flags. - Trigger-zone enabled/inside flags.
@@ -193,7 +194,7 @@ Gameplay operations mutate the authoritative `CharacterState`, not the runtime
`Character`: `Character`:
- `setTarget()` normalizes the destination, avoids redundant requests, stores the - `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()`. call `planPathExceptMyself()`.
- `forceReplan()` recalculates the current requested destination using the same - `forceReplan()` recalculates the current requested destination using the same
location navigation state. location navigation state.
@@ -210,7 +211,10 @@ collision handling, and presentation-event processing.
Runtime visual objects do not own their authoritative state objects. 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()`. - `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. - `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 authoritative state objects are owned by `GameState` or `LocationGameState`.
The internal server advances them before publishing a new snapshot revision. The internal server advances them before publishing a new snapshot revision.
Runtime input, scripts, and collision handling submit semantic commands that invoke Runtime input, scripts, and collision handling submit semantic commands that invoke
authoritative character and location operations. Location commands cover camera authoritative character and location operations. Location commands cover pending
orientation, pending interaction targets, navigation switching and area changes, interaction targets, navigation switching and area changes, editor obstacles, and
editor obstacles, and taxi reset. No whole-location commit command is exposed. 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 Navigation target selection, replanning, stopping, facing, death reset, health
assignment, and attack-combo clock changes are implemented by `CharacterState`. 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. 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 5. The runtime location consumes value-only events. Navigation requests are sent
back as semantic destination commands, whose server handler calls 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`. function names are resolved through `ScriptEngine`.
6. The server invalidates its cached snapshot and advances the revision so runtime 6. The server invalidates its cached snapshot and advances the revision so runtime
presentation can observe the completed simulation tick. 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 ## Save and Load Schema
Persistence uses one strict, versioned schema. `schemaVersion` is required and Authoritative persistence uses one strict, versioned schema. `schemaVersion` is
unsupported versions are rejected rather than migrated. 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. - Campaign players are stored once in an ID-keyed `players` object.
- Locations are keyed by location ID. - Locations are keyed by location ID.
@@ -453,7 +461,8 @@ cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -
## Main Files ## Main Files
- `src/GameState.h` / `src/GameState.cpp` — aggregate state and top-level persistence. - `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, - `src/content/LocationDefinition.h` / `.cpp` — renderer-free content definitions,
parsing, and animation metadata loading. parsing, and animation metadata loading.
- `src/game_server/IGameServer.h` — public snapshot and command contract. - `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()`. thread from `update()`.
- It maintains an internal `InternalGameServer` replica so existing snapshot - It maintains an internal `InternalGameServer` replica so existing snapshot
consumers and transitional runtime-state bindings continue to work. 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. - `submit()` intentionally discards all commands.
- `drainEvents()` returns no events. - `drainEvents()` returns no events.
- Client-side save and load calls are ignored because persistence remains - 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: When `NETWORK` is defined:
- `battle_zone` uses `WebsocketGameClient` and `RemoteGameServerHost`. - `battle_zone_multiplayer` uses `WebsocketGameClient` and `RemoteGameServerHost`.
- Every other campaign uses `InternalGameServer` and `LocalGameServerHost`. - `main`, `battle_zone`, and every other campaign use `InternalGameServer` and
`LocalGameServerHost`.
The WebSocket client is created only when `battle_zone` is selected and is The WebSocket client is created only when `battle_zone_multiplayer` is selected
destroyed when that campaign is unloaded. 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 ## JSON Protocol
@@ -66,20 +78,22 @@ Messages use a versioned JSON envelope:
"protocolVersion": 1, "protocolVersion": 1,
"type": "state", "type": "state",
"payload": { "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()`. The client applies it through `InternalGameServer::load()`.
Malformed JSON, unsupported protocol versions, unsupported message types, and Malformed JSON, unsupported protocol versions, unsupported message types, and
invalid state payloads are logged and ignored. invalid state payloads are logged and ignored.
This temporary full-state format is expected to be replaced or separated from Single-player persistence uses a separate envelope that combines this authoritative
the persistence schema as command handling and production replication are document with client-local location state. The temporary full-state network format
implemented. is still expected to be replaced by a dedicated replication schema or deltas as
command handling and production replication are implemented.
## Build Configuration ## Build Configuration
@@ -106,7 +120,7 @@ cmake --build server/build --config Debug --target GameServer
Run it from the repository root: Run it from the repository root:
```powershell ```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: 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 --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. `127.0.0.1:8080`. Selecting another campaign continues to use the local server.
## Verification ## Verification
+14 -1
View File
@@ -17,7 +17,20 @@
}, },
{ {
"id": "battle_zone", "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": [ "locations": [
"resources/config/locations/battle_zone_uni_exterior.json" "resources/config/locations/battle_zone_uni_exterior.json"
], ],
File renamed without changes.
+241 -232
View File
@@ -1,243 +1,252 @@
{ {
"campaignId": "battle_zone", "localState": {
"chatHistory": [ "locations": {
[], "uni_exterior": {
[],
[]
],
"chatPreviewMsg": [
"",
"",
""
],
"chatUnread": [
true,
true,
true
],
"currentLocationId": "",
"globalFloats": {
"player_hp": 200.0
},
"globalInts": {},
"inventory": {
"items": []
},
"isDarklands": false,
"isDawn": false,
"isNight": false,
"locations": {
"uni_exterior": {
"dialogue": {
"active": false
},
"interactiveObjects": {
"intBin_Cans": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 0.6000000238418579,
"positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intBin_Plastic": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 1.399999976158142,
"positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intBin_Waste_A": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 2.200000047683716,
"positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intBin_Waste_B": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 3.0,
"positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intRoof": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 0.0,
"positionY": 3.200000047683716,
"positionZ": 0.0,
"rotationY": 0.0,
"scale": 1.0
},
"intVending.00": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": -1.0,
"positionY": 0.9150000214576721,
"positionZ": -3.9000000953674316,
"rotationY": 0.0,
"scale": 1.0
},
"intVending.01": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 0.0,
"positionY": 0.9150000214576721,
"positionZ": -3.9000000953674316,
"rotationY": 0.0,
"scale": 1.0
},
"intVending.02": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 1.0,
"positionY": 0.9150000214576721,
"positionZ": -3.9000000953674316,
"rotationY": 0.0,
"scale": 1.0
}
},
"npcs": {},
"scriptGlobals": {},
"state": {
"activeNavigationIndex": 0,
"cameraAzimuth": -2.3499999046325684, "cameraAzimuth": -2.3499999046325684,
"cameraInclination": 1.103600025177002, "cameraInclination": 1.103600025177002
"targetInteractNpcId": "", }
"targetInteractiveObjectId": "",
"targetTeleportZoneId": "",
"taxi": {
"arrivedCallbackCalled": false,
"car": {
"currentWaypoint": 0,
"mode": 1,
"position": [
-100.0,
-100.0,
-100.0
],
"rotation": 0.0,
"steeringAngle": 0.0,
"velocity": 0.0,
"waypoints": [],
"wheelAngle": 0.0
}
}
},
"teleportZones": {},
"triggerZones": {}
} }
}, },
"money": 5500, "networkState": {
"playerHp": 200.0, "campaignId": "battle_zone",
"playerMaxHp": 200.0, "chatHistory": [
"players": { [],
"battle_zone_test_bot": { [],
"animationFrame": 0.0, []
"attackTargetId": "", ],
"attack_cooldown": 0.0, "chatPreviewMsg": [
"battle_state": 0, "",
"currentState": 0, "",
"currentWaypointIndex": 0, ""
"enabled": true, ],
"faceTargetId": "", "chatUnread": [
"facingAngle": 0.0, true,
"homeDriftCheckTimer": 5.0, true,
"homePosition": [ true
0.0, ],
0.0, "currentLocationId": "",
0.0 "globalFloats": {
], "player_hp": 200.0
"hp": 200.0,
"pathWaypoints": [],
"position": [
0.0,
0.0,
0.0
],
"requestedWalkTarget": [
0.0,
0.0,
0.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.0,
"walkTarget": [
0.0,
0.0,
0.0
]
}, },
"player": { "globalInts": {},
"animationFrame": 0.0, "inventory": {
"attackTargetId": "", "items": []
"attack_cooldown": 0.0, },
"battle_state": 0, "isDarklands": false,
"currentState": 0, "isDawn": false,
"currentWaypointIndex": 0, "isNight": false,
"enabled": true, "locations": {
"faceTargetId": "", "uni_exterior": {
"facingAngle": 0.0, "dialogue": {
"homeDriftCheckTimer": 5.0, "active": false
"homePosition": [ },
0.0, "interactiveObjects": {
0.0, "intBin_Cans": {
0.0 "alpha": 1.0,
], "isActive": true,
"hp": 200.0, "isAnimating": false,
"pathWaypoints": [ "positionX": 0.6000000238418579,
[ "positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intBin_Plastic": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 1.399999976158142,
"positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intBin_Waste_A": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 2.200000047683716,
"positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intBin_Waste_B": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 3.0,
"positionY": 0.45736101269721985,
"positionZ": 3.8499999046325684,
"rotationY": 0.0,
"scale": 1.0
},
"intRoof": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 0.0,
"positionY": 3.200000047683716,
"positionZ": 0.0,
"rotationY": 0.0,
"scale": 1.0
},
"intVending.00": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": -1.0,
"positionY": 0.9150000214576721,
"positionZ": -3.9000000953674316,
"rotationY": 0.0,
"scale": 1.0
},
"intVending.01": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 0.0,
"positionY": 0.9150000214576721,
"positionZ": -3.9000000953674316,
"rotationY": 0.0,
"scale": 1.0
},
"intVending.02": {
"alpha": 1.0,
"isActive": true,
"isAnimating": false,
"positionX": 1.0,
"positionY": 0.9150000214576721,
"positionZ": -3.9000000953674316,
"rotationY": 0.0,
"scale": 1.0
}
},
"npcs": {},
"scriptGlobals": {},
"state": {
"activeNavigationIndex": 0,
"targetInteractNpcId": "",
"targetInteractiveObjectId": "",
"targetTeleportZoneId": "",
"taxi": {
"arrivedCallbackCalled": false,
"car": {
"currentWaypoint": 0,
"mode": 1,
"position": [
-100.0,
-100.0,
-100.0
],
"rotation": 0.0,
"steeringAngle": 0.0,
"velocity": 0.0,
"waypoints": [],
"wheelAngle": 0.0
}
}
},
"teleportZones": {},
"triggerZones": {}
}
},
"money": 5500,
"playerHp": 200.0,
"playerMaxHp": 200.0,
"players": {
"battle_zone_test_bot": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
0.0,
0.0,
0.0
],
"hp": 200.0,
"pathWaypoints": [],
"position": [
0.0,
0.0,
0.0
],
"requestedWalkTarget": [
0.0,
0.0,
0.0
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.0,
"walkTarget": [
0.0,
0.0,
0.0
]
},
"player": {
"animationFrame": 0.0,
"attackTargetId": "",
"attack_cooldown": 0.0,
"battle_state": 0,
"currentState": 0,
"currentWaypointIndex": 0,
"enabled": true,
"faceTargetId": "",
"facingAngle": 0.0,
"homeDriftCheckTimer": 5.0,
"homePosition": [
0.0,
0.0,
0.0
],
"hp": 200.0,
"pathWaypoints": [
[
-6.025001525878906,
0.0,
12.775001525878906
]
],
"position": [
-6.0,
0.0,
12.800000190734863
],
"requestedWalkTarget": [
-6.0,
0.0,
12.800000190734863
],
"showWeapon": true,
"targetFacingAngle": 0.0,
"visibleRange": 6.0,
"walkTarget": [
-6.025001525878906, -6.025001525878906,
0.0, 0.0,
12.775001525878906 12.775001525878906
] ]
], }
"position": [ },
-6.0, "questJournal": {
0.0, "quests": []
12.800000190734863 },
], "savedAt": "2026-11-05 17:43",
"requestedWalkTarget": [ "schemaVersion": 2,
-6.0, "taxiIsCalled": false,
0.0, "tutorialJournalScreenOpened": false,
12.800000190734863 "tutorialNeedOpenTaxiScreen": false,
], "tutorialPhoneChatScreenOpened": false,
"showWeapon": true, "tutorialStep": 0,
"targetFacingAngle": 0.0, "tutorialTaxiHintDone": false
"visibleRange": 6.0,
"walkTarget": [
-6.025001525878906,
0.0,
12.775001525878906
]
}
}, },
"questJournal": { "persistenceVersion": 1
"quests": []
},
"savedAt": "2026-10-30 14:50",
"schemaVersion": 1,
"taxiIsCalled": false,
"tutorialJournalScreenOpened": false,
"tutorialNeedOpenTaxiScreen": false,
"tutorialPhoneChatScreenOpened": false,
"tutorialStep": 0,
"tutorialTaxiHintDone": false
} }
+3 -3
View File
@@ -42,11 +42,11 @@ real-time delay.
Start the first multiplayer transport iteration: Start the first multiplayer transport iteration:
```powershell ```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 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. `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. The server broadcasts an initial full state and then one every five seconds.
Client commands are intentionally discarded in this iteration. Client commands are intentionally discarded in this iteration.
@@ -83,4 +83,4 @@ CTest covers:
- repeated `LocationGameState::setup()` preserving mutable runtime state; - repeated `LocationGameState::setup()` preserving mutable runtime state;
- ticking a prepared location with zero players; - ticking a prepared location with zero players;
- idle server startup; - 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" #include "game_server/WebsocketGameServer.h"
#endif #endif
#include "Campaign.h" #include "Campaign.h"
#include "LocationState.h"
#include "utils/Utils.h"
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <csignal> #include <csignal>
@@ -88,7 +90,7 @@ int main(int argc, char** argv)
} }
//Temporary override: //Temporary override:
campaignId = "battle_zone"; campaignId = "battle_zone_multiplayer";
locationId = "uni_exterior"; locationId = "uni_exterior";
std::signal(SIGINT, requestStop); std::signal(SIGINT, requestStop);
@@ -98,6 +100,7 @@ int main(int argc, char** argv)
if (!campaignId.empty()) { if (!campaignId.empty()) {
try { try {
FRG::CampaignRegistry::instance().loadFromJson(campaignsPath); FRG::CampaignRegistry::instance().loadFromJson(campaignsPath);
std::cout << "[server] Loading...\n" << std::flush;
const FRG::CampaignConfig* campaign = const FRG::CampaignConfig* campaign =
FRG::CampaignRegistry::instance().find(campaignId); FRG::CampaignRegistry::instance().find(campaignId);
if (!campaign) if (!campaign)
@@ -113,7 +116,32 @@ int main(int argc, char** argv)
"' has no location '" + selectedLocation + "'"); "' has no location '" + selectedLocation + "'");
server.startCampaign(*campaign, std::string(), true, false); server.startCampaign(*campaign, std::string(), true, false);
server.prepareLocation(*location); 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); server.activateLocation(selectedLocation);
if (!campaign->tutorial) {
server.submit(FRG::SetTutorialStepCommand{
FRG::TutorialStep::Completed});
}
std::cout << "Loaded campaign '" << campaignId std::cout << "Loaded campaign '" << campaignId
<< "', location '" << selectedLocation << "', location '" << selectedLocation
<< "' with shared player '" << server.localPlayerId() << "'\n"; << "' with shared player '" << server.localPlayerId() << "'\n";
+64 -1
View File
@@ -16,10 +16,69 @@ int fail(const char* message)
int main() 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 = const std::filesystem::path repositoryRoot =
std::filesystem::path(__FILE__).parent_path().parent_path().parent_path(); std::filesystem::path(__FILE__).parent_path().parent_path().parent_path();
std::filesystem::current_path(repositoryRoot); 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; FRG::LocationDefinition definition;
definition.config.id = "test_location"; definition.config.id = "test_location";
@@ -87,8 +146,12 @@ int main()
} }
FRG::InternalGameServer networkServer; 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(); 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) if (networkSnapshot->players.count("player") != 1)
return fail("Network campaign shared player was not created"); return fail("Network campaign shared player was not created");
if (networkSnapshot->players.count("battle_zone_test_bot") != 0) if (networkSnapshot->players.count("battle_zone_test_bot") != 0)
+12 -3
View File
@@ -1,4 +1,5 @@
#include "game_server/NetworkProtocol.h" #include "game_server/NetworkProtocol.h"
#include "LocationState.h"
#include <cstdlib> #include <cstdlib>
#include <iostream> #include <iostream>
#include <string> #include <string>
@@ -16,10 +17,14 @@ void require(bool condition, const char* message)
int main() int main()
{ {
FRG::LocationNetworkState locationState;
nlohmann::json savedLocationState;
locationState.save(savedLocationState);
const nlohmann::json original{ const nlohmann::json original{
{"schemaVersion", 1}, {"schemaVersion", 2},
{"campaignId", "battle_zone"}, {"campaignId", "battle_zone_multiplayer"},
{"players", {{"player", {{"hp", 200.0}}}}} {"players", {{"player", {{"hp", 200.0}}}}},
{"locations", {{"uni_exterior", {{"state", savedLocationState}}}}}
}; };
const std::string message = FRG::makeStateMessage(original); const std::string message = FRG::makeStateMessage(original);
nlohmann::json parsed; nlohmann::json parsed;
@@ -28,6 +33,10 @@ int main()
FRG::parseStateMessage(message, parsed, error), FRG::parseStateMessage(message, parsed, error),
"valid state envelope was rejected"); "valid state envelope was rejected");
require(parsed == original, "state payload did not round trip"); 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( require(
!FRG::parseStateMessage( !FRG::parseStateMessage(
-1
View File
@@ -3,7 +3,6 @@
#include "external/nlohmann/json.hpp" #include "external/nlohmann/json.hpp"
#include <iostream> #include <iostream>
namespace FRG { namespace FRG {
using json = nlohmann::json; using json = nlohmann::json;
+2 -2
View File
@@ -47,7 +47,7 @@ void CharacterState::stopInPlace() {
void CharacterState::setTarget( void CharacterState::setTarget(
const Eigen::Vector3f& target, const Eigen::Vector3f& target,
std::string callbackName, std::string callbackName,
const LocationState& location, const LocationNetworkState& location,
const std::string& entityId) const std::string& entityId)
{ {
const Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z()); const Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z());
@@ -79,7 +79,7 @@ void CharacterState::setTarget(
} }
void CharacterState::forceReplan( void CharacterState::forceReplan(
const LocationState& location, const std::string& entityId) const LocationNetworkState& location, const std::string& entityId)
{ {
const Eigen::Vector3f normalizedTarget( const Eigen::Vector3f normalizedTarget(
requestedWalkTarget.x(), 0.f, requestedWalkTarget.z()); requestedWalkTarget.x(), 0.f, requestedWalkTarget.z());
+3 -3
View File
@@ -16,7 +16,7 @@ namespace FRG {
struct CharacterUpdateContext; struct CharacterUpdateContext;
struct CharacterUpdateEvents; struct CharacterUpdateEvents;
struct LocationState; struct LocationNetworkState;
enum class AnimationState { enum class AnimationState {
STAND = 0, STAND = 0,
@@ -142,9 +142,9 @@ public:
void stopInPlace(); void stopInPlace();
void setTarget(const Eigen::Vector3f& target, void setTarget(const Eigen::Vector3f& target,
std::string callbackName, std::string callbackName,
const LocationState& location, const LocationNetworkState& location,
const std::string& entityId); const std::string& entityId);
void forceReplan(const LocationState& location, void forceReplan(const LocationNetworkState& location,
const std::string& entityId); const std::string& entityId);
void stopAndRotateToTarget(std::string characterId); void stopAndRotateToTarget(std::string characterId);
void resetPlayerAfterDeath(); void resetPlayerAfterDeath();
+39 -23
View File
@@ -552,7 +552,7 @@ namespace FRG
CampaignRegistry::instance().find(campaignId); CampaignRegistry::instance().find(campaignId);
if (!campaign) return; if (!campaign) return;
#if defined(NETWORK) #if defined(NETWORK)
remoteCampaignActive = campaignId == "battle_zone"; remoteCampaignActive = campaignId == "battle_zone_multiplayer";
if (remoteCampaignActive) { if (remoteCampaignActive) {
websocketGameClient = std::make_unique<WebsocketGameClient>(); websocketGameClient = std::make_unique<WebsocketGameClient>();
remoteGameServerHost = remoteGameServerHost =
@@ -644,12 +644,7 @@ namespace FRG
}); });
loc->stopCharacter(*loc->player); loc->stopCharacter(*loc->player);
} }
auto locationState = loc->stateSnapshot(); loc->localState().cameraAzimuth = destRotY;
const float inclination = locationState
? locationState->cameraInclination
: 1.1036f;
server().submit(SetLocationCameraCommand{
destName, destRotY, inclination});
loc->scriptEngine.callLocationEnterCallback(); loc->scriptEngine.callLocationEnterCallback();
//Reset taxi //Reset taxi
@@ -1163,6 +1158,17 @@ namespace FRG
// The authoritative implementation advances simulation; a future // The authoritative implementation advances simulation; a future
// network client can use the same call to poll/apply server updates. // network client can use the same call to poll/apply server updates.
gameServerHost.update(delta); 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(); processServerEvents();
} }
@@ -1524,11 +1530,9 @@ namespace FRG
currentLocation()->lastMouseY = eventY; currentLocation()->lastMouseY = eventY;
// Snapshot current angles so we can measure how far the user rotates. // Snapshot current angles so we can measure how far the user rotates.
auto state = currentLocation()->stateSnapshot(); const LocationLocalState& state = currentLocation()->localState();
if (state) { dragStartAzimuth = state.cameraAzimuth;
dragStartAzimuth = state->cameraAzimuth; dragStartInclination = state.cameraInclination;
dragStartInclination = state->cameraInclination;
}
} }
} }
@@ -1736,10 +1740,9 @@ namespace FRG
if (cameraDragging if (cameraDragging
&& game && game
&& game->tutorialStep == TutorialStep::RotateCamera) { && game->tutorialStep == TutorialStep::RotateCamera) {
auto state = currentLocation()->stateSnapshot(); const LocationLocalState& state = currentLocation()->localState();
if (!state) return; float deltaAz = std::abs(state.cameraAzimuth - dragStartAzimuth);
float deltaAz = std::abs(state->cameraAzimuth - dragStartAzimuth); float deltaInc = std::abs(state.cameraInclination - dragStartInclination);
float deltaInc = std::abs(state->cameraInclination - dragStartInclination);
if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) { if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) {
server().submit( server().submit(
CompleteTutorialStepCommand{TutorialStep::RotateCamera}); CompleteTutorialStepCommand{TutorialStep::RotateCamera});
@@ -1894,12 +1897,18 @@ namespace FRG
if (content.empty()) return {}; if (content.empty()) return {};
try { try {
nlohmann::json root = nlohmann::json::parse(content); 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; SaveSlotInfo info;
info.empty = false; info.empty = false;
info.campaignId = root.at("campaignId").get<std::string>(); info.campaignId =
info.locationName = root.at("currentLocationId").get<std::string>(); networkState.at("campaignId").get<std::string>();
info.savedAt = root.value("savedAt", ""); info.locationName =
networkState.at("currentLocationId").get<std::string>();
info.savedAt = networkState.value("savedAt", "");
return info; return info;
} catch (...) { } catch (...) {
return {}; return {};
@@ -1937,12 +1946,19 @@ namespace FRG
return; 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; std::cerr << "[save] Unsupported save schema in " << path << std::endl;
return; return;
} }
const std::string campaignId = root.value("campaignId", std::string()); const auto& networkState = root["networkState"];
const std::string locationId = root.value("currentLocationId", std::string()); 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); const CampaignConfig* campaign = CampaignRegistry::instance().find(campaignId);
if (!campaign || !campaign->findLocation(locationId)) { if (!campaign || !campaign->findLocation(locationId)) {
std::cerr << "[save] Unknown campaign/location in " << path << std::endl; std::cerr << "[save] Unknown campaign/location in " << path << std::endl;
+1 -1
View File
@@ -33,7 +33,7 @@ struct StoredChatMessage {
}; };
struct GameState : public ISaveable { struct GameState : public ISaveable {
static constexpr int kSchemaVersion = 1; static constexpr int kSchemaVersion = 2;
// --- World --- // --- World ---
// Campaign being played (see CampaignRegistry). // Campaign being played (see CampaignRegistry).
+21 -21
View File
@@ -1,4 +1,4 @@
#include "Location.h" #include "Location.h"
#include "utils/Utils.h" #include "utils/Utils.h"
#include "render/OpenGlExtensions.h" #include "render/OpenGlExtensions.h"
#include <iostream> #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(); auto snapshot = gameServer_.snapshot();
if (!snapshot) return {}; if (!snapshot) return {};
auto location = snapshot->locations.find(locationId_); auto location = snapshot->locations.find(locationId_);
if (location == snapshot->locations.end()) return {}; if (location == snapshot->locations.end()) return {};
return std::shared_ptr<const LocationState>( return std::shared_ptr<const LocationNetworkState>(
std::move(snapshot), &location->second.state); std::move(snapshot), &location->second.state);
} }
@@ -307,7 +307,7 @@ namespace FRG
// ---- Pending-target lookup ---- // ---- Pending-target lookup ----
TeleportZone* Location::getTargetTeleportZone(const LocationState& state) TeleportZone* Location::getTargetTeleportZone(const LocationNetworkState& state)
{ {
if (state.targetTeleportZoneId.empty()) return nullptr; if (state.targetTeleportZoneId.empty()) return nullptr;
for (auto& zone : teleportZones) for (auto& zone : teleportZones)
@@ -321,6 +321,8 @@ namespace FRG
{ {
if (!params.gameState) throw std::runtime_error("Location requires authoritative state binding"); 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.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; const LocationDefinition& definition = *params.definition;
this->audioPlayer = audioPlayer; this->audioPlayer = audioPlayer;
pathVisualization.initialize(); pathVisualization.initialize();
@@ -1006,8 +1008,8 @@ namespace FRG
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom }); renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
//renderer.TranslateMatrix({ 0, -6.f, 0 }); //renderer.TranslateMatrix({ 0, -6.f, 0 });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
const Eigen::Vector3f camTarget = const Eigen::Vector3f camTarget =
playerState ? playerState->position : Eigen::Vector3f::Zero(); playerState ? playerState->position : Eigen::Vector3f::Zero();
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() }); renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
@@ -1200,8 +1202,8 @@ namespace FRG
renderer.LoadIdentity(); renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom }); 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(localState_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
auto playerState = player ? player->stateSnapshot() : nullptr; auto playerState = player ? player->stateSnapshot() : nullptr;
const Eigen::Vector3f camTarget = const Eigen::Vector3f camTarget =
playerState ? playerState->position : Eigen::Vector3f::Zero(); playerState ? playerState->position : Eigen::Vector3f::Zero();
@@ -1340,8 +1342,8 @@ namespace FRG
renderer.LoadIdentity(); renderer.LoadIdentity();
renderer.TranslateMatrix({ 0, 0, -1.0f * Environment::zoom }); 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(localState_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
const Eigen::Vector3f camTarget = const Eigen::Vector3f camTarget =
playerState ? playerState->position : Eigen::Vector3f::Zero(); playerState ? playerState->position : Eigen::Vector3f::Zero();
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() }); renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
@@ -1586,8 +1588,8 @@ namespace FRG
renderer.LoadIdentity(); renderer.LoadIdentity();
renderer.TranslateMatrix({ 0, 0, -1.0f * Environment::zoom }); 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(localState_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
const Eigen::Vector3f camTarget = const Eigen::Vector3f camTarget =
playerState ? playerState->position : Eigen::Vector3f::Zero(); playerState ? playerState->position : Eigen::Vector3f::Zero();
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() }); renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
@@ -1963,8 +1965,8 @@ namespace FRG
float aspect = (float)Environment::width / (float)Environment::height; float aspect = (float)Environment::width / (float)Environment::height;
float tanHalfFov = tan(CAMERA_FOV_Y * 0.5f); float tanHalfFov = tan(CAMERA_FOV_Y * 0.5f);
float cosAzim = cos(locationState->cameraAzimuth), sinAzim = sin(locationState->cameraAzimuth); float cosAzim = cos(localState_->cameraAzimuth), sinAzim = sin(localState_->cameraAzimuth);
float cosIncl = cos(locationState->cameraInclination), sinIncl = sin(locationState->cameraInclination); float cosIncl = cos(localState_->cameraInclination), sinIncl = sin(localState_->cameraInclination);
Eigen::Vector3f camRight(cosAzim, 0.f, sinAzim); Eigen::Vector3f camRight(cosAzim, 0.f, sinAzim);
Eigen::Vector3f camForward(sinAzim * cosIncl, -sinIncl, -cosAzim * cosIncl); Eigen::Vector3f camForward(sinAzim * cosIncl, -sinIncl, -cosAzim * cosIncl);
@@ -2168,16 +2170,14 @@ namespace FRG
lastMouseY = eventY; lastMouseY = eventY;
const float sensitivity = 0.005f; const float sensitivity = 0.005f;
auto state = stateSnapshot(); localState_->cameraAzimuth += dx * sensitivity;
if (!state) return; float inclination =
const float azimuth = state->cameraAzimuth + dx * sensitivity; localState_->cameraInclination + dy * sensitivity;
float inclination = state->cameraInclination + dy * sensitivity;
const float minInclination = M_PI * 30.f / 180.f; const float minInclination = M_PI * 30.f / 180.f;
const float maxInclination = M_PI * 0.5f; const float maxInclination = M_PI * 0.5f;
inclination = max(minInclination, min(maxInclination, inclination)); localState_->cameraInclination =
gameServer_.submit(SetLocationCameraCommand{ max(minInclination, min(maxInclination, inclination));
locationId_, azimuth, inclination});
} }
} }
+8 -4
View File
@@ -1,4 +1,4 @@
#pragma once #pragma once
#include "render/Renderer.h" #include "render/Renderer.h"
#include "Environment.h" #include "Environment.h"
@@ -48,7 +48,7 @@ namespace FRG
float radius = 1.5f; float radius = 1.5f;
float hysteresis = 0.3f; // exit fires at radius + hysteresis to prevent flickering float hysteresis = 0.3f; // exit fires at radius + hysteresis to prevent flickering
bool enabled = true; 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 struct LocationSetup
@@ -68,6 +68,7 @@ namespace FRG
// setup() creates a private one when this is null. // setup() creates a private one when this is null.
std::shared_ptr<Character> player; std::shared_ptr<Character> player;
LocationGameState* gameState = nullptr; LocationGameState* gameState = nullptr;
std::shared_ptr<LocationLocalState> localState;
}; };
class Location class Location
@@ -78,7 +79,9 @@ namespace FRG
// Detaches the (shared, longer-lived) player from this location. // Detaches the (shared, longer-lived) player from this location.
~Location(); ~Location();
const std::string& locationId() const { return locationId_; } 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 isDarklands() const;
bool isNight() const; bool isNight() const;
bool isDawn() const; bool isDawn() const;
@@ -234,13 +237,14 @@ namespace FRG
private: private:
IGameServer& gameServer_; IGameServer& gameServer_;
std::string locationId_; std::string locationId_;
std::shared_ptr<LocationLocalState> localState_;
std::unordered_map<std::string, std::shared_ptr<Character>> remotePlayers_; std::unordered_map<std::string, std::shared_ptr<Character>> remotePlayers_;
void synchronizeRemotePlayers(); void synchronizeRemotePlayers();
void drawRemotePlayerNames( void drawRemotePlayerNames(
const Eigen::Matrix4f& cameraViewMatrix, const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix); const Eigen::Matrix4f& projectionMatrix);
// ---- Pointer-to-index helpers ---- // ---- Pointer-to-index helpers ----
TeleportZone* getTargetTeleportZone(const LocationState& state); TeleportZone* getTargetTeleportZone(const LocationNetworkState& state);
enum class InstancedTexture { None, Color, Darklands }; enum class InstancedTexture { None, Color, Darklands };
+2 -2
View File
@@ -14,7 +14,7 @@ bool LocationGameState::setup(
LocationDefinition content, const std::string& zipFile) LocationDefinition content, const std::string& zipFile)
{ {
const bool initializeRuntimeFields = !hasPersistentState; const bool initializeRuntimeFields = !hasPersistentState;
if (!location) location = std::make_shared<LocationState>(); if (!location) location = std::make_shared<LocationNetworkState>();
if (location->navigationMaps.empty()) if (location->navigationMaps.empty())
location->initNavigation(content.config.navigationJsonPaths, zipFile); location->initNavigation(content.config.navigationJsonPaths, zipFile);
lockInteractiveObjectsDuringTutorial = lockInteractiveObjectsDuringTutorial =
@@ -377,7 +377,7 @@ void LocationGameState::save(nlohmann::json& out) const
void LocationGameState::load(const nlohmann::json& in) void LocationGameState::load(const nlohmann::json& in)
{ {
hasPersistentState = true; hasPersistentState = true;
if (!location) location = std::make_shared<LocationState>(); if (!location) location = std::make_shared<LocationNetworkState>();
location->load(in.at("state")); location->load(in.at("state"));
const auto& npcValues = in.at("npcs"); 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 // simulation objects stable addresses; runtime renderers and Location pointers
// are deliberately absent, and public clients receive value-only snapshots. // are deliberately absent, and public clients receive value-only snapshots.
struct LocationGameState { 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<CharacterState>> npcs;
std::unordered_map<std::string, std::shared_ptr<InteractiveObjectState>> interactiveObjects; std::unordered_map<std::string, std::shared_ptr<InteractiveObjectState>> interactiveObjects;
bool lockInteractiveObjectsDuringTutorial = false; bool lockInteractiveObjectsDuringTutorial = false;
+82 -12
View File
@@ -1,7 +1,85 @@
#include "LocationState.h" #include "LocationState.h"
#include <stdexcept>
namespace FRG { 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 { namespace {
nlohmann::json saveCar(const NpcCar& car) 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, {}); return navigationMaps[activeNavigationIndex].findPathToNearest(start, end, {});
} }
void LocationState::initNavigation( void LocationNetworkState::initNavigation(
const std::vector<std::string>& paths, const std::string& zipFile) const std::vector<std::string>& paths, const std::string& zipFile)
{ {
const int savedActiveNavigationIndex = activeNavigationIndex; const int savedActiveNavigationIndex = activeNavigationIndex;
@@ -68,11 +146,8 @@ namespace FRG {
: 0; : 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["activeNavigationIndex"] = activeNavigationIndex;
out["targetInteractiveObjectId"] = targetInteractiveObjectId; 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); activeNavigationIndex = in.value("activeNavigationIndex", activeNavigationIndex);
targetInteractiveObjectId = in.value("targetInteractiveObjectId", std::string()); targetInteractiveObjectId = in.value("targetInteractiveObjectId", std::string());
+31 -5
View File
@@ -3,11 +3,41 @@
#include "ISaveable.h" #include "ISaveable.h"
#include "NpcCar.h" #include "NpcCar.h"
#include "navigation/PathFinder.h" #include "navigation/PathFinder.h"
#include <memory>
#include <string> #include <string>
#include <unordered_map>
namespace FRG { 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. //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. //It might be changed only in the editor mode during development.
std::vector<PathFinder> navigationMaps; std::vector<PathFinder> navigationMaps;
@@ -16,10 +46,6 @@ struct LocationState : public ISaveable {
const std::vector<std::string>& paths, const std::vector<std::string>& paths,
const std::string& zipFile = std::string()); const std::string& zipFile = std::string());
// ---- Camera ----
float cameraAzimuth = -2.35f;
float cameraInclination = 1.1036f;
// ---- Navigation ---- // ---- Navigation ----
int activeNavigationIndex = 0; int activeNavigationIndex = 0;
+2 -2
View File
@@ -57,6 +57,8 @@ namespace FRG {
void showMainMenu(); void showMainMenu();
void showMenuAfterGameCompleted(); void showMenuAfterGameCompleted();
// Returns to the main menu with the campaign list already open.
void showCampaignScreen();
bool isMainMenuOpen() const { return uiState_ == GameUiState::MainMenu; } bool isMainMenuOpen() const { return uiState_ == GameUiState::MainMenu; }
void openPhoneScreen(); void openPhoneScreen();
@@ -223,8 +225,6 @@ namespace FRG {
void showLanguageScreen(); void showLanguageScreen();
void showLoadGameScreen(); void showLoadGameScreen();
void showSaveGameScreen(); void showSaveGameScreen();
// Campaign list opened by "Start game".
void showCampaignScreen();
// Text of a load/save slot button. // Text of a load/save slot button.
std::string slotLabel(const SaveSlotInfo& info); std::string slotLabel(const SaveSlotInfo& info);
+1 -7
View File
@@ -30,7 +30,7 @@ struct InteractiveObjectSnapshot {
struct LocationSnapshot { struct LocationSnapshot {
std::string id; std::string id;
LocationState state; LocationNetworkState state;
std::unordered_map<std::string, CharacterSnapshot> npcs; std::unordered_map<std::string, CharacterSnapshot> npcs;
std::unordered_map<std::string, InteractiveObjectSnapshot> interactiveObjects; std::unordered_map<std::string, InteractiveObjectSnapshot> interactiveObjects;
std::unordered_map<std::string, TriggerZoneGameState> triggerZones; std::unordered_map<std::string, TriggerZoneGameState> triggerZones;
@@ -95,11 +95,6 @@ struct AppendChatMessageCommand {
struct SetTaxiCalledCommand { bool called; }; struct SetTaxiCalledCommand { bool called; };
struct StartTaxiCommand { std::string locationId; }; struct StartTaxiCommand { std::string locationId; };
struct ResetTaxiCommand { std::string locationId; }; struct ResetTaxiCommand { std::string locationId; };
struct SetLocationCameraCommand {
std::string locationId;
float azimuth;
float inclination;
};
struct SetLocationTargetsCommand { struct SetLocationTargetsCommand {
std::string locationId; std::string locationId;
std::string interactiveObjectId; std::string interactiveObjectId;
@@ -232,7 +227,6 @@ using GameCommand = std::variant<
SetTaxiCalledCommand, SetTaxiCalledCommand,
StartTaxiCommand, StartTaxiCommand,
ResetTaxiCommand, ResetTaxiCommand,
SetLocationCameraCommand,
SetLocationTargetsCommand, SetLocationTargetsCommand,
SwitchLocationNavigationCommand, SwitchLocationNavigationCommand,
SetNavigationAreaAvailableCommand, SetNavigationAreaAvailableCommand,
+3 -9
View File
@@ -305,12 +305,6 @@ void InternalGameServer::submit(const GameCommand& command)
auto location = state_.locations.find(value.locationId); auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end()) if (location != state_.locations.end())
location->second.resetTaxi(); 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>) { } else if constexpr (std::is_same_v<T, SetLocationTargetsCommand>) {
auto location = state_.locations.find(value.locationId); auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) { if (location != state_.locations.end() && location->second.location) {
@@ -326,7 +320,7 @@ void InternalGameServer::submit(const GameCommand& command)
value.index >= 0 && value.index >= 0 &&
value.index < static_cast<int>( value.index < static_cast<int>(
location->second.location->navigationMaps.size())) { location->second.location->navigationMaps.size())) {
LocationState& locationState = *location->second.location; LocationNetworkState& locationState = *location->second.location;
locationState.activeNavigationIndex = value.index; locationState.activeNavigationIndex = value.index;
for (const auto& [id, player] : state_.players) for (const auto& [id, player] : state_.players)
if (player) player->forceReplan(locationState, id); 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>) { } else if constexpr (std::is_same_v<T, SetNavigationAreaAvailableCommand>) {
auto location = state_.locations.find(value.locationId); auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) { if (location != state_.locations.end() && location->second.location) {
LocationState& locationState = *location->second.location; LocationNetworkState& locationState = *location->second.location;
const int index = locationState.activeNavigationIndex; const int index = locationState.activeNavigationIndex;
if (index >= 0 && if (index >= 0 &&
index < static_cast<int>(locationState.navigationMaps.size())) { 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>) { } else if constexpr (std::is_same_v<T, AddNavigationObstacleCommand>) {
auto location = state_.locations.find(value.locationId); auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) { if (location != state_.locations.end() && location->second.location) {
LocationState& locationState = *location->second.location; LocationNetworkState& locationState = *location->second.location;
const int index = locationState.activeNavigationIndex; const int index = locationState.activeNavigationIndex;
if (index >= 0 && if (index >= 0 &&
index < static_cast<int>(locationState.navigationMaps.size())) { 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& campaignId, const std::string& questsPath,
const std::string& zipFile) const std::string& zipFile)
{ {
localState_.clear();
server_.initializeCampaign(campaignId, questsPath, zipFile); server_.initializeCampaign(campaignId, questsPath, zipFile);
} }
void LocalGameServerHost::clearCampaign() void LocalGameServerHost::clearCampaign()
{ {
runtimeLocations_.clear(); runtimeLocations_.clear();
localState_.clear();
server_.clearCampaign(); server_.clearCampaign();
} }
void LocalGameServerHost::resetCampaign( void LocalGameServerHost::resetCampaign(
const std::string& questsPath, const std::string& zipFile) const std::string& questsPath, const std::string& zipFile)
{ {
localState_.clear();
server_.resetCampaign(questsPath, zipFile); server_.resetCampaign(questsPath, zipFile);
} }
@@ -44,6 +47,7 @@ void LocalGameServerHost::setupRuntimeLocation(
throw std::runtime_error("Location state was not prepared"); throw std::runtime_error("Location state was not prepared");
params.definition = stored->second.definition; params.definition = stored->second.definition;
params.gameState = &stored->second; params.gameState = &stored->second;
params.localState = localState_.getOrCreate(id);
location->setup(params, audioPlayer); location->setup(params, audioPlayer);
server_.publishMutation(); server_.publishMutation();
} }
@@ -122,12 +126,20 @@ void LocalGameServerHost::update(std::int64_t deltaMs)
void LocalGameServerHost::save(nlohmann::json& out) void LocalGameServerHost::save(nlohmann::json& out)
{ {
synchronizeRuntimeAuxiliaryState(); synchronizeRuntimeAuxiliaryState();
server_.save(out); nlohmann::json networkState;
server_.save(networkState);
saveSinglePlayerPersistence(networkState, localState_, out);
} }
void LocalGameServerHost::load(const nlohmann::json& in) 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(); applyStateToRuntime();
} }
+2
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "IGameServerHost.h" #include "IGameServerHost.h"
#include "../LocationState.h"
#include <unordered_map> #include <unordered_map>
namespace FRG { namespace FRG {
@@ -39,6 +40,7 @@ private:
void applyStateToRuntime(); void applyStateToRuntime();
InternalGameServer& server_; InternalGameServer& server_;
ClientLocationState localState_;
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_; std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_;
}; };
+1
View File
@@ -1,6 +1,7 @@
#include "NetworkProtocol.h" #include "NetworkProtocol.h"
#include <exception> #include <exception>
namespace FRG { namespace FRG {
std::string makeStateMessage(const nlohmann::json& state) 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& campaignId, const std::string&,
const std::string& zipFile) const std::string& zipFile)
{ {
localState_.clear();
const CampaignConfig* campaign = const CampaignConfig* campaign =
CampaignRegistry::instance().find(campaignId); CampaignRegistry::instance().find(campaignId);
if (!campaign) if (!campaign)
@@ -25,6 +26,7 @@ void RemoteGameServerHost::initializeCampaign(
void RemoteGameServerHost::clearCampaign() void RemoteGameServerHost::clearCampaign()
{ {
runtimeLocations_.clear(); runtimeLocations_.clear();
localState_.clear();
client_.replica_.clearCampaign(); client_.replica_.clearCampaign();
} }
@@ -48,6 +50,7 @@ void RemoteGameServerHost::setupRuntimeLocation(
throw std::runtime_error("Remote location state was not prepared"); throw std::runtime_error("Remote location state was not prepared");
params.definition = stored->second.definition; params.definition = stored->second.definition;
params.gameState = &stored->second; params.gameState = &stored->second;
params.localState = localState_.getOrCreate(id);
location->setup(params, audioPlayer); location->setup(params, audioPlayer);
} }
@@ -101,9 +104,16 @@ void RemoteGameServerHost::save(nlohmann::json&)
// Persistence remains server-side. // 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 } // namespace FRG
+2
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "IGameServerHost.h" #include "IGameServerHost.h"
#include "../LocationState.h"
#include <unordered_map> #include <unordered_map>
namespace FRG { namespace FRG {
@@ -33,6 +34,7 @@ public:
private: private:
WebsocketGameClient& client_; WebsocketGameClient& client_;
ClientLocationState localState_;
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_; 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 connected() const { return connected_.load(); }
bool connectionFailed() const { return connectionFailed_.load(); }
private: private:
void resolve() void resolve()
@@ -101,6 +102,7 @@ private:
void fail(const char* operation, beast::error_code error) void fail(const char* operation, beast::error_code error)
{ {
connected_.store(false); connected_.store(false);
connectionFailed_.store(true);
std::cerr << "[network] WebSocket " << operation << " failed: " std::cerr << "[network] WebSocket " << operation << " failed: "
<< error.message() << '\n'; << error.message() << '\n';
} }
@@ -114,6 +116,7 @@ private:
std::string port_; std::string port_;
std::string path_; std::string path_;
std::atomic_bool connected_{false}; std::atomic_bool connected_{false};
std::atomic_bool connectionFailed_{false};
std::mutex messagesMutex_; std::mutex messagesMutex_;
std::vector<std::string> messages_; std::vector<std::string> messages_;
}; };
@@ -186,4 +189,9 @@ bool WebsocketGameClient::connected() const
return impl_->connected(); return impl_->connected();
} }
bool WebsocketGameClient::connectionFailed() const
{
return impl_->connectionFailed();
}
} // namespace FRG } // namespace FRG
+1
View File
@@ -29,6 +29,7 @@ public:
void load(const nlohmann::json& in) override; void load(const nlohmann::json& in) override;
bool connected() const; bool connected() const;
bool connectionFailed() const;
private: private:
friend class RemoteGameServerHost; friend class RemoteGameServerHost;