Working on the server
This commit is contained in:
1 parent
075750442f
commit
28c6e83eda
30 files changed
+646
-362
No files matched your search
+22
-13
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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.
@@ -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
@@ -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
@@ -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";
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 };
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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())) {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user