Separating state out of the location
This commit is contained in:
1 parent
5e9a663812
commit
297bd7377f
45 files changed
+2111
-775
No files matched your search
+57
-31
@@ -157,23 +157,30 @@ Loading and persistence restoration clear pending events and remain silent.
|
||||
`InternalGameServer` is the current `IGameServer` implementation. It:
|
||||
|
||||
- Exclusively owns the aggregate `GameState`.
|
||||
- Privately stores loaded runtime `Location` objects.
|
||||
- Executes typed commands against authoritative state.
|
||||
- Advances the active location simulation through `update(deltaMs)`.
|
||||
- Synchronizes non-bound auxiliary state such as dialogue, Lua globals, triggers, and teleports.
|
||||
- Builds and caches immutable snapshots.
|
||||
- Delegates save/load serialization to the aggregate state model.
|
||||
|
||||
`Game` remains a privileged lifecycle host, but it no longer holds or receives a
|
||||
mutable `GameState` reference. It uses private, focused internal-server operations
|
||||
for campaign loading and location construction. Those operations publish revisions
|
||||
themselves and are not exposed by `IGameServer`.
|
||||
`LocalGameServerHost` is the graphical client's integration adapter. It owns loaded
|
||||
runtime `Location` objects, forwards simulation events to them, and synchronizes
|
||||
dialogue, Lua, trigger, and teleport auxiliary state. `Game` uses the explicit
|
||||
`IGameServerHost` lifecycle interface and is no longer a friend of
|
||||
`InternalGameServer`. The standalone server does not compile this adapter.
|
||||
|
||||
Location loading has two phases. `LocationContentLoader` parses renderer-free
|
||||
`LocationDefinition` data, and `LocationGameState::setup()` creates/reconciles
|
||||
authoritative NPC, interactive-object, navigation, taxi, trigger, teleport, and
|
||||
animation state. The graphical `Location::setup()` runs afterward and only builds
|
||||
presentation objects from that definition and the prepared state. Lua and dialogue
|
||||
runtime behavior remains client-side.
|
||||
|
||||
### Animation and combat
|
||||
|
||||
Logical animation frame clocks live in `CharacterState`. Animation frame counts are
|
||||
copied from the loaded clip metadata during runtime construction, after which state
|
||||
transitions and frame-19 combat impacts are deterministic authoritative simulation.
|
||||
Logical animation frame clocks live in `CharacterState`. The renderer-free content
|
||||
loader reads animation frame metadata before authoritative entities are created.
|
||||
Runtime `Character` construction only loads models for presentation and does not
|
||||
submit metadata mutations back to the server.
|
||||
|
||||
`CharacterState::update()` returns value-only events containing arrival Lua function
|
||||
names, attack impacts, navigation requests, and death-animation completion. `LocationGameState` applies
|
||||
@@ -224,9 +231,9 @@ editor obstacles, and taxi reset. No whole-location commit command is exposed.
|
||||
Navigation target selection, replanning, stopping, facing, death reset, health
|
||||
assignment, and attack-combo clock changes are implemented by `CharacterState`.
|
||||
`Character` no longer exposes gameplay mutation helpers or a generic state mutation
|
||||
callback. Its only state-facing setup responsibility is reporting frame ranges from
|
||||
loaded animation assets through `SetCharacterAnimationMetadataCommand`; otherwise it
|
||||
focuses on model posing, rendering, audio, particles, and runtime callbacks.
|
||||
callback. Character construction is presentation-only; frame ranges are already
|
||||
present in `CharacterState` after content setup. It focuses on model posing,
|
||||
rendering, audio, particles, and runtime callbacks.
|
||||
|
||||
Death-animation and health-change callbacks remain runtime-only on `Character`.
|
||||
Arrival behavior is represented solely by the serialized Lua function name in
|
||||
@@ -242,10 +249,10 @@ by `Game`.
|
||||
|
||||
`GameObjectLoader` now:
|
||||
|
||||
1. Loads static creation data from location configuration.
|
||||
2. Creates or reuses server-owned state objects.
|
||||
3. Applies saved runtime fields over the configuration defaults.
|
||||
4. Constructs visual objects using stable IDs and state-backed creation data.
|
||||
1. Receives immutable object/NPC definitions parsed by `LocationContentLoader`.
|
||||
2. Resolves already-prepared authoritative entities by stable ID.
|
||||
3. Constructs meshes, textures, VBOs, `InteractiveObject`, and `Character`
|
||||
presentation wrappers without creating or overwriting authoritative state.
|
||||
|
||||
Configuration remains the source of visual assets, animation paths, meshes, textures, and fixed tuning data.
|
||||
|
||||
@@ -256,7 +263,8 @@ The normal frame flow is:
|
||||
1. SDL input is processed.
|
||||
2. UI code submits semantic commands through `IGameServer`.
|
||||
3. The internal server freezes location character observations and calls
|
||||
`CharacterState::update()` for the player and each NPC.
|
||||
`CharacterState::update()` for every present player and NPC. An empty player
|
||||
collection is valid.
|
||||
4. Attack impacts are queued, then damage is applied after all characters advance.
|
||||
5. The runtime location consumes value-only events. Navigation requests are sent
|
||||
back as semantic destination commands, whose server handler calls
|
||||
@@ -283,21 +291,28 @@ When a campaign starts:
|
||||
|
||||
- Existing campaign state and runtime locations are cleared.
|
||||
- Quest definitions are loaded.
|
||||
- A campaign-level player state is created under the `"player"` ID.
|
||||
- The visual player is created with its stable ID and resolves that state through snapshots.
|
||||
- The graphical single-player host requests a campaign-level `"player"` state and
|
||||
creates its visual wrapper.
|
||||
- A standalone host may deliberately create no players; locations continue to tick.
|
||||
- The battle-zone test bot is an explicit host option rather than an unconditional
|
||||
campaign side effect.
|
||||
|
||||
### Location loading
|
||||
|
||||
When a location is loaded:
|
||||
|
||||
- A `LocationGameState` record is created or reused.
|
||||
- Static configuration is loaded.
|
||||
- NPC and interactive-object visuals are created against state owned by the location record.
|
||||
- `LocationContentLoader` parses a renderer-free `LocationDefinition`.
|
||||
- `LocationGameState::setup()` creates or reconciles navigation, taxi, NPC,
|
||||
interactive-object, trigger, teleport, and animation state.
|
||||
- Saved mutable fields are overlaid on configuration defaults by stable ID.
|
||||
- `Location::setup()` builds static and entity visuals from the prepared definition
|
||||
without mutating authoritative state.
|
||||
- The shared campaign player is attached.
|
||||
- Runtime visuals are created for other player IDs currently present in the
|
||||
snapshot and are subsequently spawned or removed as snapshots change.
|
||||
- Dialogue, Lua, navigation, trigger, teleport, and presentation callbacks are initialized.
|
||||
- The runtime `Location` is registered privately with `InternalGameServer`.
|
||||
- Dialogue, Lua, trigger/teleport visuals, and presentation callbacks are initialized.
|
||||
- The runtime `Location` is owned and registered by `LocalGameServerHost`;
|
||||
`InternalGameServer` remains headless.
|
||||
|
||||
### Location unloading
|
||||
|
||||
@@ -387,7 +402,6 @@ Still required for real multiplayer:
|
||||
|
||||
- Network transport and command serialization.
|
||||
- Authentication and player/session assignment.
|
||||
- Server tick scheduling independent of rendering.
|
||||
- Snapshot delta generation or other replication strategy.
|
||||
- Latency handling, interpolation, reconciliation, and prediction.
|
||||
- Command validation, authorization, and anti-cheat rules.
|
||||
@@ -407,6 +421,12 @@ The new state and server files are registered in:
|
||||
|
||||
The refactor uses portable C++17 and introduces no Windows-only gameplay code.
|
||||
|
||||
The standalone target lives under `server/`. It links the authoritative state,
|
||||
campaign, content-definition, and portable resource sources only; SDL, OpenGL,
|
||||
audio, Lua, dialogue presentation, and rendering sources are absent. It can prepare
|
||||
a selected campaign/location with zero players and run the 50 ms fixed-step loop.
|
||||
Networking remains outside this milestone.
|
||||
|
||||
The Windows Debug target was verified with:
|
||||
|
||||
```powershell
|
||||
@@ -422,18 +442,24 @@ cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -
|
||||
location-local NPC ID).
|
||||
- Some top-level values remain mirrors for compatibility, notably player health used by the HUD and scripts.
|
||||
- Dialogue and Lua are synchronized as auxiliary location state rather than fully networked subsystems.
|
||||
- `Game` uses focused privileged internal-server APIs for local content
|
||||
construction, but cannot access the aggregate or its state maps directly. The
|
||||
private location-construction path still supplies `LocationGameState` to the
|
||||
local object loader. A remote client implementation will need a client-side
|
||||
runtime-location factory driven by configuration and snapshots.
|
||||
- `IGameServerLifecycle` is the graphics-free campaign/location administration
|
||||
boundary. `LocalGameServerHost` adds client-only runtime construction and
|
||||
auxiliary-state synchronization without making `Game` a server friend.
|
||||
- Empty player maps are valid. Player-specific interactions remain inactive until
|
||||
a player entity is added.
|
||||
- A remote client still needs a client-side runtime-location factory driven by
|
||||
configuration/definitions and replicated snapshots.
|
||||
|
||||
## Main Files
|
||||
|
||||
- `src/GameState.h` / `src/GameState.cpp` — aggregate state and top-level persistence.
|
||||
- `src/LocationGameState.h` / `src/LocationGameState.cpp` — typed location state, simulation, and ID-keyed persistence.
|
||||
- `src/content/LocationDefinition.h` / `.cpp` — renderer-free content definitions,
|
||||
parsing, and animation metadata loading.
|
||||
- `src/game_server/IGameServer.h` — public snapshot and command contract.
|
||||
- `src/game_server/IGameServerLifecycle.h` — graphics-free campaign/location lifecycle.
|
||||
- `src/game_server/InternalGameServer.h` / `.cpp` — in-process authoritative implementation.
|
||||
- `src/game_server/LocalGameServerHost.h` / `.cpp` — graphical-client runtime adapter.
|
||||
- `src/Game.h` / `src/Game.cpp` — application host, campaign lifecycle, and runtime wiring.
|
||||
- `src/Location.h` / `src/Location.cpp` — loaded location runtime, command routing,
|
||||
interactions, collisions, scripts, and presentation-event consumption.
|
||||
@@ -442,5 +468,5 @@ cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -
|
||||
- `src/CharacterState.h` / `src/CharacterState.cpp` — deterministic character
|
||||
simulation, navigation operations, combat state, and logical animation clock.
|
||||
- `src/items/InteractiveObject.h` / `.cpp` — visual object runtime bound to central object state.
|
||||
- `src/items/GameObjectLoader.h` / `.cpp` — configuration/state overlay and runtime construction.
|
||||
- `src/items/GameObjectLoader.h` / `.cpp` — presentation builders for prepared definitions.
|
||||
- `src/MenuManager.h` / `src/MenuManager.cpp` — snapshot-driven UI and command submission.
|
||||
@@ -106,9 +106,11 @@ add_library(main SHARED
|
||||
../../../../src/GameConstants.cpp
|
||||
../../../../src/GameState.h
|
||||
../../../../src/GameState.cpp
|
||||
../../../../src/content/LocationDefinition.cpp
|
||||
../../../../src/game_server/IGameServer.h
|
||||
../../../../src/game_server/InternalGameServer.h
|
||||
../../../../src/game_server/InternalGameServer.cpp
|
||||
../../../../src/game_server/LocalGameServerHost.cpp
|
||||
../../../../src/ScriptEngine.h
|
||||
../../../../src/ScriptEngine.cpp
|
||||
../../../../src/navigation/PathFinder.h
|
||||
@@ -117,11 +119,13 @@ add_library(main SHARED
|
||||
../../../../src/items/GameObjectLoader.cpp
|
||||
../../../../src/items/Item.h
|
||||
../../../../src/items/Item.cpp
|
||||
../../../../src/items/Inventory.cpp
|
||||
../../../../src/items/ItemRegistry.h
|
||||
../../../../src/items/ItemRegistry.cpp
|
||||
../../../../src/Campaign.cpp
|
||||
../../../../src/items/InteractiveObject.h
|
||||
../../../../src/items/InteractiveObject.cpp
|
||||
../../../../src/items/InteractiveObjectState.cpp
|
||||
../../../../src/items/InteractiveObjectState.h
|
||||
../../../../src/dialogue/DialogueTypes.h
|
||||
../../../../src/dialogue/DialogueDatabase.h
|
||||
|
||||
@@ -131,9 +131,15 @@ add_executable(ShadowOverBishkekDemo
|
||||
../src/GameConstants.cpp
|
||||
../src/GameState.h
|
||||
../src/GameState.cpp
|
||||
../src/content/LocationDefinition.h
|
||||
../src/content/LocationDefinition.cpp
|
||||
../src/game_server/IGameServer.h
|
||||
../src/game_server/IGameServerLifecycle.h
|
||||
../src/game_server/IGameServerHost.h
|
||||
../src/game_server/InternalGameServer.h
|
||||
../src/game_server/InternalGameServer.cpp
|
||||
../src/game_server/LocalGameServerHost.h
|
||||
../src/game_server/LocalGameServerHost.cpp
|
||||
../src/ScriptEngine.h
|
||||
../src/ScriptEngine.cpp
|
||||
../src/navigation/PathFinder.h
|
||||
@@ -142,12 +148,14 @@ add_executable(ShadowOverBishkekDemo
|
||||
../src/items/GameObjectLoader.cpp
|
||||
../src/items/Item.h
|
||||
../src/items/Item.cpp
|
||||
../src/items/Inventory.cpp
|
||||
../src/items/ItemRegistry.h
|
||||
../src/items/ItemRegistry.cpp
|
||||
../src/Campaign.cpp
|
||||
../src/items/InteractiveObject.h
|
||||
../src/items/InteractiveObject.cpp
|
||||
../src/items/InteractiveObjectState.h
|
||||
../src/items/InteractiveObjectState.cpp
|
||||
../src/dialogue/DialogueTypes.h
|
||||
../src/dialogue/DialogueDatabase.h
|
||||
../src/dialogue/DialogueDatabase.cpp
|
||||
|
||||
@@ -125,9 +125,11 @@ set(SOURCES
|
||||
../src/GameConstants.cpp
|
||||
../src/GameState.h
|
||||
../src/GameState.cpp
|
||||
../src/content/LocationDefinition.cpp
|
||||
../src/game_server/IGameServer.h
|
||||
../src/game_server/InternalGameServer.h
|
||||
../src/game_server/InternalGameServer.cpp
|
||||
../src/game_server/LocalGameServerHost.cpp
|
||||
../src/ScriptEngine.h
|
||||
../src/ScriptEngine.cpp
|
||||
../src/navigation/PathFinder.h
|
||||
@@ -136,11 +138,13 @@ set(SOURCES
|
||||
../src/items/GameObjectLoader.cpp
|
||||
../src/items/Item.h
|
||||
../src/items/Item.cpp
|
||||
../src/items/Inventory.cpp
|
||||
../src/items/ItemRegistry.h
|
||||
../src/items/ItemRegistry.cpp
|
||||
../src/Campaign.cpp
|
||||
../src/items/InteractiveObject.h
|
||||
../src/items/InteractiveObject.cpp
|
||||
../src/items/InteractiveObjectState.cpp
|
||||
../src/items/InteractiveObjectState.h
|
||||
../src/dialogue/DialogueTypes.h
|
||||
../src/dialogue/DialogueDatabase.h
|
||||
|
||||
@@ -73,9 +73,15 @@ add_executable(ShadowOverBishkekDemo WIN32
|
||||
../src/GameConstants.cpp
|
||||
../src/GameState.h
|
||||
../src/GameState.cpp
|
||||
../src/content/LocationDefinition.h
|
||||
../src/content/LocationDefinition.cpp
|
||||
../src/game_server/IGameServer.h
|
||||
../src/game_server/IGameServerLifecycle.h
|
||||
../src/game_server/IGameServerHost.h
|
||||
../src/game_server/InternalGameServer.h
|
||||
../src/game_server/InternalGameServer.cpp
|
||||
../src/game_server/LocalGameServerHost.h
|
||||
../src/game_server/LocalGameServerHost.cpp
|
||||
../src/ScriptEngine.h
|
||||
../src/ScriptEngine.cpp
|
||||
../src/navigation/PathFinder.h
|
||||
@@ -84,12 +90,14 @@ add_executable(ShadowOverBishkekDemo WIN32
|
||||
../src/items/GameObjectLoader.cpp
|
||||
../src/items/Item.h
|
||||
../src/items/Item.cpp
|
||||
../src/items/Inventory.cpp
|
||||
../src/items/ItemRegistry.h
|
||||
../src/items/ItemRegistry.cpp
|
||||
../src/Campaign.cpp
|
||||
../src/items/InteractiveObject.h
|
||||
../src/items/InteractiveObject.cpp
|
||||
../src/items/InteractiveObjectState.h
|
||||
../src/items/InteractiveObjectState.cpp
|
||||
../src/dialogue/DialogueTypes.h
|
||||
../src/dialogue/DialogueDatabase.h
|
||||
../src/dialogue/DialogueDatabase.cpp
|
||||
|
||||
@@ -414,7 +414,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 1.1,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.9695000052452087,
|
||||
"positionZ": 10.0,
|
||||
"approachRadius": 0.89,
|
||||
"boundsMaxX": 0.01552116870880127,
|
||||
@@ -439,7 +439,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -1.1,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.9695000052452087,
|
||||
"positionZ": 10.0,
|
||||
"approachRadius": 0.8,
|
||||
"boundsMaxX": 0.10440409183502197,
|
||||
@@ -464,7 +464,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 1.1,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.9695000052452087,
|
||||
"positionZ": -10.0,
|
||||
"approachRadius": 0.8,
|
||||
"boundsMaxX": 0.05766594409942627,
|
||||
@@ -489,7 +489,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -1.1,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.9695000052452087,
|
||||
"positionZ": -10.0,
|
||||
"approachRadius": 0.8,
|
||||
"boundsMaxX": 0.10906755924224854,
|
||||
@@ -514,7 +514,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 1.1,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.9695000052452087,
|
||||
"positionZ": 15.7,
|
||||
"approachRadius": 0.8,
|
||||
"boundsMaxX": 0.046350955963134766,
|
||||
@@ -539,7 +539,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -1.1,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.9695000052452087,
|
||||
"positionZ": 15.7,
|
||||
"approachRadius": 0.800000011920929,
|
||||
"boundsMaxX": 0.12420153617858887,
|
||||
@@ -564,7 +564,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 1.1,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.9695000052452087,
|
||||
"positionZ": 19.95,
|
||||
"approachRadius": 0.8,
|
||||
"boundsMaxX": -0.050987482070922852,
|
||||
@@ -589,7 +589,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -1.1,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.9695000052452087,
|
||||
"positionZ": 19.95,
|
||||
"approachRadius": 0.8,
|
||||
"boundsMaxX": 0.073349356651306152,
|
||||
@@ -614,7 +614,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 1.1,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.9695000052452087,
|
||||
"positionZ": -19.95,
|
||||
"pivotX": 0.0,
|
||||
"pivotY": 0.0,
|
||||
@@ -631,7 +631,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -1.1,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.9695000052452087,
|
||||
"positionZ": -19.95,
|
||||
"pivotX": 0.0,
|
||||
"pivotY": 0.0,
|
||||
@@ -647,7 +647,7 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 6.6644,
|
||||
"positionX": 6.6644004991889,
|
||||
"positionY": 0.9,
|
||||
"positionZ": -12.5262,
|
||||
"approachRadius": 0.6,
|
||||
@@ -674,7 +674,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 5.84296,
|
||||
"positionY": 0.9,
|
||||
"positionY": 0.9284919999539852,
|
||||
"positionZ": -12.4661,
|
||||
"approachRadius": 0.6,
|
||||
"boundsMaxX": 0.17081403732299805,
|
||||
@@ -726,9 +726,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 5.10067,
|
||||
"positionY": 0.91385,
|
||||
"positionZ": -11.9566,
|
||||
"positionX": 5.1006695008111,
|
||||
"positionY": 1.0181855017900467,
|
||||
"positionZ": -11.958324999397993,
|
||||
"approachRadius": 0.6,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.3,
|
||||
@@ -743,7 +743,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 2.825,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.9695000052452087,
|
||||
"positionZ": -14.3,
|
||||
"pivotX": -0.6,
|
||||
"pivotY": 0.0,
|
||||
@@ -771,7 +771,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -2.825,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.9695000052452087,
|
||||
"positionZ": -14.3,
|
||||
"approachRadius": 0.8,
|
||||
"boundsMaxX": 0.55439233779907227,
|
||||
@@ -796,7 +796,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 1.1,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.9695000052452087,
|
||||
"positionZ": -15.7,
|
||||
"pivotX": 0.0,
|
||||
"pivotY": 0.0,
|
||||
@@ -824,7 +824,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -1.1,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.9695000052452087,
|
||||
"positionZ": -15.7,
|
||||
"approachRadius": 0.8,
|
||||
"boundsMaxX": 0.13942182064056396,
|
||||
@@ -852,9 +852,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": -0.1,
|
||||
"positionZ": 0.0,
|
||||
"positionX": 2.8249999284744263,
|
||||
"positionY": 2.8500000476837157,
|
||||
"positionZ": -12.849999904632568,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
@@ -867,9 +867,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": -0.1,
|
||||
"positionZ": 0.0,
|
||||
"positionX": 4.949999928474426,
|
||||
"positionY": 2.8500000476837157,
|
||||
"positionZ": -14.25,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
@@ -882,9 +882,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": -0.1,
|
||||
"positionZ": 0.0,
|
||||
"positionX": -4.949999928474426,
|
||||
"positionY": 2.8500000476837157,
|
||||
"positionZ": -14.25,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
@@ -898,7 +898,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": -0.1,
|
||||
"positionY": 2.8500000476837157,
|
||||
"positionZ": 0.0,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
@@ -913,8 +913,8 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": -0.1,
|
||||
"positionZ": 0.0,
|
||||
"positionY": 2.8500000476837157,
|
||||
"positionZ": 4.250000476837158,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
@@ -927,9 +927,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": -0.1,
|
||||
"positionZ": 0.0,
|
||||
"positionX": 4.949999928474426,
|
||||
"positionY": 2.8500000476837157,
|
||||
"positionZ": -19.950000762939453,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
@@ -942,9 +942,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": -0.1,
|
||||
"positionZ": 0.0,
|
||||
"positionX": -4.949999928474426,
|
||||
"positionY": 2.8500000476837157,
|
||||
"positionZ": -19.950000762939453,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
@@ -960,7 +960,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": 0.1,
|
||||
"positionY": 3.2050000190734864,
|
||||
"positionZ": 0.0,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -26.8049,
|
||||
"positionY": 0.0,
|
||||
"positionZ": 9.11524,
|
||||
"positionX": -27.003390023612976,
|
||||
"positionY": 0.32889999728649855,
|
||||
"positionZ": 8.820399544944763,
|
||||
"approachRadius": 1.5,
|
||||
"boundsMaxX": 2.9518299102783203,
|
||||
"boundsMaxY": 0.5,
|
||||
@@ -33,9 +33,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 90.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -10.5,
|
||||
"positionY": 0.55,
|
||||
"positionZ": -14.4195,
|
||||
"positionX": -10.503284412622452,
|
||||
"positionY": 0.9012498408555984,
|
||||
"positionZ": -14.861633367061614,
|
||||
"approachRadius": 0.5,
|
||||
"boundsMaxX": 0.8858480453491211,
|
||||
"boundsMaxY": 0.7,
|
||||
@@ -59,8 +59,8 @@
|
||||
"rotationY": 90.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -10.5,
|
||||
"positionY": 0.0,
|
||||
"positionZ": -14.4195,
|
||||
"positionY": 0.3359999924898147,
|
||||
"positionZ": -14.839499933242797,
|
||||
"approachRadius": 0.5,
|
||||
"boundsMaxX": 0,
|
||||
"boundsMaxY": 0.0,
|
||||
|
||||
@@ -116,9 +116,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0,
|
||||
"positionY": -5.0,
|
||||
"positionZ": 0,
|
||||
"positionX": -5.365539073944092,
|
||||
"positionY": 0.9888315200805664,
|
||||
"positionZ": 2.468366503715515,
|
||||
"approachRadius": 0.6,
|
||||
"boundsMaxX": 0.19605731964111328,
|
||||
"boundsMaxY": 0.4,
|
||||
@@ -144,9 +144,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 5.0916,
|
||||
"positionY": 1.1534,
|
||||
"positionZ": 0.751629,
|
||||
"positionX": 5.01753252658844,
|
||||
"positionY": 1.353411484324932,
|
||||
"positionZ": 0.6829135021486282,
|
||||
"approachRadius": 1.2,
|
||||
"boundsMaxX": 0.6253643035888672,
|
||||
"boundsMaxY": 0.5,
|
||||
@@ -168,9 +168,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -4.8327,
|
||||
"positionX": -4.959765496635623,
|
||||
"positionY": 0.93551,
|
||||
"positionZ": 2.2418,
|
||||
"positionZ": 2.369899506953731,
|
||||
"approachRadius": 1.4,
|
||||
"boundsMaxX": 0.30867958068847656,
|
||||
"boundsMaxY": 0.4,
|
||||
@@ -192,7 +192,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 1.84795,
|
||||
"positionY": 0.6595,
|
||||
"positionY": 0.6809705001562834,
|
||||
"positionZ": 2.90552,
|
||||
"approachRadius": 0.6,
|
||||
"boundsMaxX": 0.2350165843963623,
|
||||
@@ -294,9 +294,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 90.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -10.5,
|
||||
"positionY": -5.0,
|
||||
"positionZ": -14.4195,
|
||||
"positionX": -10.503284412622452,
|
||||
"positionY": -4.648750159144401,
|
||||
"positionZ": -14.861633367061614,
|
||||
"approachRadius": 0.6,
|
||||
"boundsMaxX": 0.0,
|
||||
"boundsMaxY": 0.0,
|
||||
@@ -319,8 +319,8 @@
|
||||
"rotationY": 90.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -10.5,
|
||||
"positionY": -5.0,
|
||||
"positionZ": -14.4195,
|
||||
"positionY": -4.664000007510185,
|
||||
"positionZ": -14.839499933242797,
|
||||
"approachRadius": 0.6,
|
||||
"boundsMaxX": 0.0,
|
||||
"boundsMaxY": 0.0,
|
||||
@@ -344,7 +344,7 @@
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 1.5,
|
||||
"positionY": 0.975,
|
||||
"positionZ": 6.965,
|
||||
"positionZ": 6.400000002384186,
|
||||
"approachRadius": 0.8,
|
||||
"boundsMaxX": 0.06785714626312256,
|
||||
"boundsMaxY": 1.0,
|
||||
@@ -372,7 +372,7 @@
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 1.5,
|
||||
"positionY": 0.975,
|
||||
"positionZ": -1.035,
|
||||
"positionZ": -1.5999999976158141,
|
||||
"approachRadius": 0.8,
|
||||
"boundsMaxX": 0.10772454738616943,
|
||||
"boundsMaxY": 1.0,
|
||||
@@ -400,7 +400,7 @@
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -1.5,
|
||||
"positionY": 0.975,
|
||||
"positionZ": -1.035,
|
||||
"positionZ": -1.5999999976158141,
|
||||
"approachRadius": 0.8,
|
||||
"boundsMaxX": 0.12115073204040527,
|
||||
"boundsMaxY": 1.0,
|
||||
@@ -428,7 +428,7 @@
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -1.5,
|
||||
"positionY": 0.975,
|
||||
"positionZ": 6.965,
|
||||
"positionZ": 6.400000002384186,
|
||||
"approachRadius": 0.8,
|
||||
"boundsMaxX": 0.09747374057769775,
|
||||
"boundsMaxY": 1.0,
|
||||
@@ -454,9 +454,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.565,
|
||||
"positionX": 0.0,
|
||||
"positionY": 0.975,
|
||||
"positionZ": 8,
|
||||
"positionZ": 8.000001000240445,
|
||||
"approachRadius": 0.9,
|
||||
"boundsMaxX": 0.6243956089019775,
|
||||
"boundsMaxY": 1.0,
|
||||
@@ -484,7 +484,7 @@
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -1.5,
|
||||
"positionY": 0.975,
|
||||
"positionZ": -9.035,
|
||||
"positionZ": -9.599999020778657,
|
||||
"approachRadius": 0.8,
|
||||
"boundsMaxX": 0.10914039611816406,
|
||||
"boundsMaxY": 1.0,
|
||||
@@ -511,9 +511,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": -5.0,
|
||||
"positionZ": 0.0,
|
||||
"positionX": 2.2500000596046448,
|
||||
"positionY": 2.4499998092651367,
|
||||
"positionZ": -3.999999761581421,
|
||||
"scale": 1.0,
|
||||
"pivotX": 0.0,
|
||||
"pivotY": 0.0,
|
||||
@@ -530,8 +530,8 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": -5.0,
|
||||
"positionZ": 0.0,
|
||||
"positionY": 2.4499998092651367,
|
||||
"positionZ": 12.0,
|
||||
"scale": 1.0,
|
||||
"pivotX": 0.0,
|
||||
"pivotY": 0.0,
|
||||
@@ -547,9 +547,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": -5.0,
|
||||
"positionZ": 0.0,
|
||||
"positionX": -3.7500000596046448,
|
||||
"positionY": 2.4499998092651367,
|
||||
"positionZ": -12.0,
|
||||
"scale": 1.0,
|
||||
"pivotX": 0.0,
|
||||
"pivotY": 0.0,
|
||||
@@ -565,9 +565,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": -5.0,
|
||||
"positionZ": 0.0,
|
||||
"positionX": 3.7500000596046448,
|
||||
"positionY": 2.4499998092651367,
|
||||
"positionZ": -4.000000048428774,
|
||||
"scale": 1.0,
|
||||
"pivotX": 0.0,
|
||||
"pivotY": 0.0,
|
||||
@@ -583,9 +583,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": -5.0,
|
||||
"positionZ": 0.0,
|
||||
"positionX": -3.7500000596046448,
|
||||
"positionY": 2.4499998092651367,
|
||||
"positionZ": -4.000000048428774,
|
||||
"scale": 1.0,
|
||||
"pivotX": 0.0,
|
||||
"pivotY": 0.0,
|
||||
@@ -601,9 +601,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": -5.0,
|
||||
"positionZ": 0.0,
|
||||
"positionX": 3.7500000596046448,
|
||||
"positionY": 2.4499998092651367,
|
||||
"positionZ": 4.000000048428774,
|
||||
"scale": 1.0,
|
||||
"pivotX": 0.0,
|
||||
"pivotY": 0.0,
|
||||
@@ -619,9 +619,9 @@
|
||||
"rotationX": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": -5.0,
|
||||
"positionZ": 0.0,
|
||||
"positionX": -3.7500000596046448,
|
||||
"positionY": 2.4499998092651367,
|
||||
"positionZ": 4.000000048428774,
|
||||
"scale": 1.0,
|
||||
"pivotX": 0.0,
|
||||
"pivotY": 0.0,
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 0.0,
|
||||
"positionY": 3.1999998092651367,
|
||||
"positionY": 3.200000047683716,
|
||||
"positionZ": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
@@ -148,6 +148,43 @@
|
||||
"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": "",
|
||||
@@ -195,7 +232,7 @@
|
||||
"questJournal": {
|
||||
"quests": []
|
||||
},
|
||||
"savedAt": "2026-10-28 23:24",
|
||||
"savedAt": "2026-10-30 14:50",
|
||||
"schemaVersion": 1,
|
||||
"taxiIsCalled": false,
|
||||
"tutorialJournalScreenOpened": false,
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
"animTask": {
|
||||
"durationMs": 10.0,
|
||||
"elapsedMs": 0.0,
|
||||
"onCompleteName": "",
|
||||
"startPosX": -1.100000023841858,
|
||||
"startPosY": 0.9695000052452087,
|
||||
"startPosZ": -19.950000762939453,
|
||||
@@ -184,6 +185,7 @@
|
||||
"animTask": {
|
||||
"durationMs": 10.0,
|
||||
"elapsedMs": 0.0,
|
||||
"onCompleteName": "",
|
||||
"startPosX": 1.100000023841858,
|
||||
"startPosY": 0.9695000052452087,
|
||||
"startPosZ": -19.950000762939453,
|
||||
@@ -259,7 +261,7 @@
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 5.842959880828857,
|
||||
"positionY": 0.9284919500350952,
|
||||
"positionY": 0.92849200963974,
|
||||
"positionZ": -12.466099739074707,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
@@ -763,7 +765,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"savedAt": "2026-10-28 23:16",
|
||||
"savedAt": "2026-10-30 14:48",
|
||||
"schemaVersion": 1,
|
||||
"taxiIsCalled": false,
|
||||
"tutorialJournalScreenOpened": false,
|
||||
@@ -9,7 +9,7 @@
|
||||
"rotationY": -180.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.6,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.85,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
@@ -23,7 +23,7 @@
|
||||
"rotationY": -180.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 1.4,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.85,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
@@ -37,7 +37,7 @@
|
||||
"rotationY": -180.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 2.2,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.85,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
@@ -51,7 +51,7 @@
|
||||
"rotationY": -180.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 3.0,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.85,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
@@ -65,7 +65,7 @@
|
||||
"rotationY": 0.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": 0.0,
|
||||
"positionY": 3.2,
|
||||
"positionZ": 0.0,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
@@ -79,7 +79,7 @@
|
||||
"rotationY": -90.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": -1.0,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.915,
|
||||
"positionZ": -3.9,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
@@ -93,7 +93,7 @@
|
||||
"rotationY": -90.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 0.0,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.915,
|
||||
"positionZ": -3.9,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
@@ -107,7 +107,7 @@
|
||||
"rotationY": -90.0,
|
||||
"rotationZ": 0.0,
|
||||
"positionX": 1.0,
|
||||
"positionY": 0.0,
|
||||
"positionY": 0.915,
|
||||
"positionZ": -3.9,
|
||||
"scale": 1.0,
|
||||
"interactionRadius": 0.0
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(GameServer LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
set(GAME_ROOT "${CMAKE_CURRENT_LIST_DIR}/..")
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(Eigen3 3.3 CONFIG QUIET)
|
||||
if(NOT TARGET Eigen3::Eigen)
|
||||
set(EIGEN_BUNDLED_DIR "${GAME_ROOT}/thirdparty/eigen-5.0.0")
|
||||
if(NOT EXISTS "${EIGEN_BUNDLED_DIR}/Eigen/Core")
|
||||
message(FATAL_ERROR
|
||||
"Eigen3 was not found. Install Eigen3 or configure the graphical "
|
||||
"project once to populate thirdparty/eigen-5.0.0.")
|
||||
endif()
|
||||
add_library(game_server_eigen INTERFACE)
|
||||
target_include_directories(game_server_eigen INTERFACE "${EIGEN_BUNDLED_DIR}")
|
||||
add_library(Eigen3::Eigen ALIAS game_server_eigen)
|
||||
endif()
|
||||
|
||||
add_library(game_server_core STATIC
|
||||
"${GAME_ROOT}/src/Campaign.cpp"
|
||||
"${GAME_ROOT}/src/Campaign.h"
|
||||
"${GAME_ROOT}/src/CharacterState.cpp"
|
||||
"${GAME_ROOT}/src/CharacterState.h"
|
||||
"${GAME_ROOT}/src/GameState.cpp"
|
||||
"${GAME_ROOT}/src/GameState.h"
|
||||
"${GAME_ROOT}/src/ISaveable.h"
|
||||
"${GAME_ROOT}/src/Localization.cpp"
|
||||
"${GAME_ROOT}/src/Localization.h"
|
||||
"${GAME_ROOT}/src/LocationGameState.cpp"
|
||||
"${GAME_ROOT}/src/LocationGameState.h"
|
||||
"${GAME_ROOT}/src/LocationState.cpp"
|
||||
"${GAME_ROOT}/src/LocationState.h"
|
||||
"${GAME_ROOT}/src/NpcCar.cpp"
|
||||
"${GAME_ROOT}/src/NpcCar.h"
|
||||
"${GAME_ROOT}/src/content/LocationDefinition.cpp"
|
||||
"${GAME_ROOT}/src/content/LocationDefinition.h"
|
||||
"${GAME_ROOT}/src/game_server/IGameServer.h"
|
||||
"${GAME_ROOT}/src/game_server/IGameServerLifecycle.h"
|
||||
"${GAME_ROOT}/src/game_server/InternalGameServer.cpp"
|
||||
"${GAME_ROOT}/src/game_server/InternalGameServer.h"
|
||||
"${GAME_ROOT}/src/items/InteractiveObjectState.cpp"
|
||||
"${GAME_ROOT}/src/items/InteractiveObjectState.h"
|
||||
"${GAME_ROOT}/src/items/Inventory.cpp"
|
||||
"${GAME_ROOT}/src/items/Item.h"
|
||||
"${GAME_ROOT}/src/navigation/PathFinder.cpp"
|
||||
"${GAME_ROOT}/src/navigation/PathFinder.h"
|
||||
"${GAME_ROOT}/src/quest/QuestJournal.cpp"
|
||||
"${GAME_ROOT}/src/quest/QuestJournal.h"
|
||||
"${GAME_ROOT}/src/quest/QuestTypes.h"
|
||||
"${GAME_ROOT}/src/utils/HeadlessUtils.cpp"
|
||||
"${GAME_ROOT}/src/utils/Utils.h"
|
||||
)
|
||||
|
||||
source_group(TREE "${GAME_ROOT}/src" PREFIX "Source" FILES
|
||||
"${GAME_ROOT}/src/Campaign.cpp"
|
||||
"${GAME_ROOT}/src/Campaign.h"
|
||||
"${GAME_ROOT}/src/CharacterState.cpp"
|
||||
"${GAME_ROOT}/src/CharacterState.h"
|
||||
"${GAME_ROOT}/src/GameState.cpp"
|
||||
"${GAME_ROOT}/src/GameState.h"
|
||||
"${GAME_ROOT}/src/ISaveable.h"
|
||||
"${GAME_ROOT}/src/Localization.cpp"
|
||||
"${GAME_ROOT}/src/Localization.h"
|
||||
"${GAME_ROOT}/src/LocationGameState.cpp"
|
||||
"${GAME_ROOT}/src/LocationGameState.h"
|
||||
"${GAME_ROOT}/src/LocationState.cpp"
|
||||
"${GAME_ROOT}/src/LocationState.h"
|
||||
"${GAME_ROOT}/src/NpcCar.cpp"
|
||||
"${GAME_ROOT}/src/NpcCar.h"
|
||||
"${GAME_ROOT}/src/content/LocationDefinition.cpp"
|
||||
"${GAME_ROOT}/src/content/LocationDefinition.h"
|
||||
"${GAME_ROOT}/src/game_server/IGameServer.h"
|
||||
"${GAME_ROOT}/src/game_server/IGameServerLifecycle.h"
|
||||
"${GAME_ROOT}/src/game_server/InternalGameServer.cpp"
|
||||
"${GAME_ROOT}/src/game_server/InternalGameServer.h"
|
||||
"${GAME_ROOT}/src/items/InteractiveObjectState.cpp"
|
||||
"${GAME_ROOT}/src/items/InteractiveObjectState.h"
|
||||
"${GAME_ROOT}/src/items/Inventory.cpp"
|
||||
"${GAME_ROOT}/src/items/Item.h"
|
||||
"${GAME_ROOT}/src/navigation/PathFinder.cpp"
|
||||
"${GAME_ROOT}/src/navigation/PathFinder.h"
|
||||
"${GAME_ROOT}/src/quest/QuestJournal.cpp"
|
||||
"${GAME_ROOT}/src/quest/QuestJournal.h"
|
||||
"${GAME_ROOT}/src/quest/QuestTypes.h"
|
||||
"${GAME_ROOT}/src/utils/HeadlessUtils.cpp"
|
||||
"${GAME_ROOT}/src/utils/Utils.h"
|
||||
)
|
||||
|
||||
target_include_directories(game_server_core PUBLIC "${GAME_ROOT}/src")
|
||||
target_link_libraries(game_server_core PUBLIC Eigen3::Eigen Threads::Threads)
|
||||
target_compile_definitions(game_server_core PRIVATE FRG_HEADLESS_SERVER=1)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(game_server_core PRIVATE /W4 /permissive-)
|
||||
else()
|
||||
target_compile_options(game_server_core PRIVATE -Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
|
||||
add_executable(GameServer src/main.cpp README.md)
|
||||
source_group("Server" FILES src/main.cpp README.md)
|
||||
target_link_libraries(GameServer PRIVATE game_server_core)
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
add_executable(
|
||||
GameServerStateTests tests/LocationGameStateTests.cpp)
|
||||
target_link_libraries(GameServerStateTests PRIVATE game_server_core)
|
||||
source_group(
|
||||
"Server\\Tests" FILES tests/LocationGameStateTests.cpp)
|
||||
add_test(
|
||||
NAME GameServer.LocationState
|
||||
COMMAND GameServerStateTests)
|
||||
add_test(
|
||||
NAME GameServer.IdleSmoke
|
||||
COMMAND GameServer --ticks 2 --no-sleep)
|
||||
add_test(
|
||||
NAME GameServer.BattleZoneZeroPlayers
|
||||
COMMAND GameServer --campaign battle_zone --location uni_exterior
|
||||
--ticks 2 --no-sleep)
|
||||
set_tests_properties(
|
||||
GameServer.BattleZoneZeroPlayers
|
||||
PROPERTIES WORKING_DIRECTORY "${GAME_ROOT}")
|
||||
endif()
|
||||
@@ -0,0 +1,77 @@
|
||||
# Headless GameServer
|
||||
|
||||
This target runs the authoritative `InternalGameServer` without SDL, OpenGL,
|
||||
audio, Lua, or rendering dependencies. It can prepare campaign/location data
|
||||
headlessly; networking is not implemented yet.
|
||||
|
||||
## Build
|
||||
|
||||
Windows:
|
||||
|
||||
```powershell
|
||||
cmake -S server -B server/build
|
||||
cmake --build server/build --config Release --target GameServer
|
||||
ctest --test-dir server/build -C Release --output-on-failure
|
||||
```
|
||||
|
||||
Linux:
|
||||
|
||||
```bash
|
||||
cmake -S server -B server/build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build server/build --target GameServer --parallel
|
||||
ctest --test-dir server/build --output-on-failure
|
||||
```
|
||||
|
||||
Eigen3 must either be installed or be available at
|
||||
`thirdparty/eigen-5.0.0`. The headless target reads ordinary filesystem resources
|
||||
only; ZIP-backed campaign loading belongs to the later content-loading
|
||||
milestone.
|
||||
|
||||
Run until interrupted:
|
||||
|
||||
```bash
|
||||
./server/build/GameServer
|
||||
```
|
||||
|
||||
For a multi-configuration Windows build, the executable is normally
|
||||
`server/build/Release/GameServer.exe`.
|
||||
|
||||
For smoke tests, `--ticks N` limits execution and `--no-sleep` skips the
|
||||
real-time delay.
|
||||
|
||||
Load a location with no players:
|
||||
|
||||
```bash
|
||||
./server/build/GameServer --campaign battle_zone --location uni_exterior
|
||||
```
|
||||
|
||||
If `--location` is omitted, the campaign's configured start location is used.
|
||||
`--campaigns PATH` selects a different campaign registry. Content paths are
|
||||
resolved from the process working directory, so run from the repository root
|
||||
when using the bundled `resources/` tree.
|
||||
|
||||
Available startup options:
|
||||
|
||||
```text
|
||||
--campaign ID
|
||||
--location ID
|
||||
--campaigns PATH
|
||||
--tick-ms N
|
||||
--ticks N
|
||||
--no-sleep
|
||||
```
|
||||
|
||||
The graphical client executes Lua/dialogue behavior after building its runtime
|
||||
`Location`; the headless content pass loads authoritative navigation, NPC,
|
||||
interactive-object, trigger, teleport, taxi, and animation metadata only.
|
||||
The standalone lifecycle intentionally creates neither the local `"player"` nor
|
||||
the battle-zone test bot. Those are opt-ins used by the graphical host.
|
||||
|
||||
## Tests
|
||||
|
||||
CTest covers:
|
||||
|
||||
- repeated `LocationGameState::setup()` preserving mutable runtime state;
|
||||
- ticking a prepared location with zero players;
|
||||
- idle server startup;
|
||||
- headless loading of `battle_zone/uni_exterior`.
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "game_server/InternalGameServer.h"
|
||||
#include "Campaign.h"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic_bool stopRequested{false};
|
||||
|
||||
void requestStop(int)
|
||||
{
|
||||
stopRequested.store(true);
|
||||
}
|
||||
|
||||
std::int64_t parsePositiveNumber(const char* value, const char* option)
|
||||
{
|
||||
try {
|
||||
const std::string text(value);
|
||||
std::size_t consumed = 0;
|
||||
const long long parsed = std::stoll(text, &consumed);
|
||||
if (consumed != text.size() || parsed <= 0)
|
||||
throw std::invalid_argument("not positive");
|
||||
return static_cast<std::int64_t>(parsed);
|
||||
} catch (const std::exception&) {
|
||||
throw std::runtime_error(
|
||||
std::string("Invalid value for ") + option + ": " + value);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
std::int64_t tickMs = 50;
|
||||
std::int64_t maxTicks = (std::numeric_limits<std::int64_t>::max)();
|
||||
bool sleepBetweenTicks = true;
|
||||
std::string campaignId;
|
||||
std::string locationId;
|
||||
std::string campaignsPath = "resources/config/campaigns.json";
|
||||
|
||||
try {
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
const std::string option(argv[index]);
|
||||
if (option == "--ticks" && index + 1 < argc) {
|
||||
maxTicks = parsePositiveNumber(argv[++index], "--ticks");
|
||||
} else if (option == "--tick-ms" && index + 1 < argc) {
|
||||
tickMs = parsePositiveNumber(argv[++index], "--tick-ms");
|
||||
} else if (option == "--no-sleep") {
|
||||
sleepBetweenTicks = false;
|
||||
} else if (option == "--campaign" && index + 1 < argc) {
|
||||
campaignId = argv[++index];
|
||||
} else if (option == "--location" && index + 1 < argc) {
|
||||
locationId = argv[++index];
|
||||
} else if (option == "--campaigns" && index + 1 < argc) {
|
||||
campaignsPath = argv[++index];
|
||||
} else if (option == "--help") {
|
||||
std::cout
|
||||
<< "Usage: GameServer [--ticks N] [--tick-ms N] "
|
||||
"[--no-sleep] [--campaign ID] [--location ID] "
|
||||
"[--campaigns PATH]\n";
|
||||
return EXIT_SUCCESS;
|
||||
} else {
|
||||
throw std::runtime_error("Unknown or incomplete option: " + option);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
std::signal(SIGINT, requestStop);
|
||||
std::signal(SIGTERM, requestStop);
|
||||
|
||||
FRG::InternalGameServer server;
|
||||
if (!campaignId.empty()) {
|
||||
try {
|
||||
FRG::CampaignRegistry::instance().loadFromJson(campaignsPath);
|
||||
const FRG::CampaignConfig* campaign =
|
||||
FRG::CampaignRegistry::instance().find(campaignId);
|
||||
if (!campaign)
|
||||
throw std::runtime_error(
|
||||
"Unknown campaign '" + campaignId + "'");
|
||||
const std::string selectedLocation = locationId.empty()
|
||||
? campaign->startLocation : locationId;
|
||||
const FRG::LocationConfig* location =
|
||||
campaign->findLocation(selectedLocation);
|
||||
if (!location)
|
||||
throw std::runtime_error(
|
||||
"Campaign '" + campaignId +
|
||||
"' has no location '" + selectedLocation + "'");
|
||||
server.startCampaign(*campaign, std::string(), false, false);
|
||||
server.prepareLocation(*location);
|
||||
server.activateLocation(selectedLocation);
|
||||
std::cout << "Loaded campaign '" << campaignId
|
||||
<< "', location '" << selectedLocation
|
||||
<< "' with zero players\n";
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "Server content setup failed: "
|
||||
<< error.what() << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
const auto tickDuration = std::chrono::milliseconds(tickMs);
|
||||
auto nextTick = std::chrono::steady_clock::now();
|
||||
|
||||
std::cout << "GameServer started (idle headless mode, "
|
||||
<< tickMs << " ms tick)\n";
|
||||
for (std::int64_t tick = 0;
|
||||
tick < maxTicks && !stopRequested.load(); ++tick) {
|
||||
nextTick += tickDuration;
|
||||
server.update(tickMs);
|
||||
if (sleepBetweenTicks) std::this_thread::sleep_until(nextTick);
|
||||
}
|
||||
std::cout << "GameServer stopped\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "LocationGameState.h"
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
int fail(const char* message)
|
||||
{
|
||||
std::cerr << message << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
FRG::LocationDefinition definition;
|
||||
definition.config.id = "test_location";
|
||||
|
||||
FRG::NpcData npc;
|
||||
npc.id = "npc";
|
||||
npc.name = "Test NPC";
|
||||
npc.hp = 75.f;
|
||||
definition.npcs.push_back(npc);
|
||||
|
||||
FRG::InteractiveObjectData object;
|
||||
object.id = "object";
|
||||
object.base.name = "Test Object";
|
||||
object.base.meshPath = "unused.mesh";
|
||||
object.isActive = true;
|
||||
definition.interactiveObjects.push_back(object);
|
||||
|
||||
FRG::TriggerZoneDefinition trigger;
|
||||
trigger.id = "trigger";
|
||||
trigger.enabled = true;
|
||||
definition.triggerZones.push_back(trigger);
|
||||
|
||||
FRG::TeleportDefinition teleport;
|
||||
teleport.id = "teleport";
|
||||
teleport.active = false;
|
||||
definition.teleports.push_back(teleport);
|
||||
|
||||
FRG::LocationGameState state;
|
||||
if (!state.setup(definition, {}))
|
||||
return fail("First setup must initialize runtime fields");
|
||||
if (state.npcs.size() != 1 || state.interactiveObjects.size() != 1)
|
||||
return fail("Definitions were not applied");
|
||||
|
||||
state.npcs.at("npc")->hp = 12.f;
|
||||
state.interactiveObjects.at("object")->isActive = false;
|
||||
state.triggerZones.at("trigger").enabled = false;
|
||||
state.teleportZones.at("teleport") = true;
|
||||
state.hasPersistentState = true;
|
||||
|
||||
if (state.setup(definition, {}))
|
||||
return fail("Persistent setup must not reinitialize runtime fields");
|
||||
if (state.npcs.at("npc")->hp != 12.f ||
|
||||
state.interactiveObjects.at("object")->isActive ||
|
||||
state.triggerZones.at("trigger").enabled ||
|
||||
!state.teleportZones.at("teleport")) {
|
||||
return fail("Repeated setup did not preserve runtime state");
|
||||
}
|
||||
|
||||
FRG::PlayerStateMap noPlayers;
|
||||
state.update(50, noPlayers, false);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <iostream>
|
||||
|
||||
|
||||
namespace FRG {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
@@ -65,8 +65,6 @@ void Character::loadAnimation(AnimationState animState, const std::string& filen
|
||||
} else {
|
||||
data.totalFrames = 1;
|
||||
}
|
||||
gameServer_.submit(SetCharacterAnimationMetadataCommand{
|
||||
locationId_, entityId_, animState, data.model.startingFrame, data.totalFrames});
|
||||
}
|
||||
|
||||
void Character::loadBinaryAnimation(AnimationState animState, const std::string& filename, const std::string& zipFile) {
|
||||
@@ -77,8 +75,6 @@ void Character::loadBinaryAnimation(AnimationState animState, const std::string&
|
||||
} else {
|
||||
data.totalFrames = 1;
|
||||
}
|
||||
gameServer_.submit(SetCharacterAnimationMetadataCommand{
|
||||
locationId_, entityId_, animState, data.model.startingFrame, data.totalFrames});
|
||||
}
|
||||
|
||||
std::unique_ptr<Character> Character::createFromState(const CharacterState& st, IGameServer& server,
|
||||
|
||||
@@ -114,7 +114,6 @@ private:
|
||||
friend class ScriptEngine;
|
||||
friend class LocationEditor;
|
||||
friend class PathVisualization;
|
||||
friend class InternalGameServer;
|
||||
IGameServer& gameServer_;
|
||||
std::string locationId_;
|
||||
std::string entityId_;
|
||||
|
||||
+22
-19
@@ -79,12 +79,13 @@ namespace FRG
|
||||
Location* Game::currentLocation() const {
|
||||
const auto state = stateSnapshot();
|
||||
if (!state || state->currentLocationId.empty()) return nullptr;
|
||||
return internalGameServer.runtimeLocation(state->currentLocationId);
|
||||
return gameServerHost.runtimeLocation(state->currentLocationId);
|
||||
}
|
||||
|
||||
Game::Game()
|
||||
: audioPlayer(std::make_unique<AudioPlayerAsync>())
|
||||
, internalGameServer()
|
||||
, gameServerHost(internalGameServer)
|
||||
, menuManager(renderer, internalGameServer, *audioPlayer)
|
||||
, newTickCount(0)
|
||||
, lastTickCount(0)
|
||||
@@ -505,7 +506,7 @@ namespace FRG
|
||||
|
||||
bool Game::isLocationLoaded(const std::string& campaignId, const std::string& locationId) const {
|
||||
return player && loadedCampaignId == campaignId
|
||||
&& internalGameServer.hasRuntimeLocation(locationId);
|
||||
&& gameServerHost.hasRuntimeLocation(locationId);
|
||||
}
|
||||
|
||||
void Game::enterLocationThen(const std::string& requestedCampaignId, const std::string& requestedLocationId,
|
||||
@@ -548,7 +549,7 @@ namespace FRG
|
||||
const CampaignConfig* campaign =
|
||||
CampaignRegistry::instance().find(campaignId);
|
||||
if (!campaign) return;
|
||||
internalGameServer.initializeCampaign(
|
||||
gameServerHost.initializeCampaign(
|
||||
campaignId, campaign->questsPath, CONST_ZIP_FILE);
|
||||
// The player stays loaded for the whole campaign; locations come and go around him.
|
||||
player = Location::createPlayer(
|
||||
@@ -562,7 +563,7 @@ namespace FRG
|
||||
unloadLocation(false);
|
||||
player.reset();
|
||||
loadedCampaignId.clear();
|
||||
internalGameServer.clearCampaign();
|
||||
gameServerHost.clearCampaign();
|
||||
renderer.textureManager.UnloadUnused(); // the player's textures
|
||||
}
|
||||
|
||||
@@ -575,18 +576,19 @@ namespace FRG
|
||||
<< locationId << "'" << std::endl;
|
||||
return;
|
||||
}
|
||||
const bool initializeLocation = internalGameServer.prepareLocation(*config);
|
||||
SDL_PumpEvents();
|
||||
const bool initializeLocation = gameServerHost.prepareLocation(*config);
|
||||
std::shared_ptr<Location> loc = createLocation(*config);
|
||||
wireLocation(loc.get());
|
||||
if (shadowMap) loc->shadowMap = shadowMap;
|
||||
if (initializeLocation)
|
||||
loc->scriptEngine.callLocationInitCallback();
|
||||
// Back to how the player left it (or to the start state's snapshot of it).
|
||||
internalGameServer.bindRuntimeLocation(locationId, loc);
|
||||
gameServerHost.bindRuntimeLocation(locationId, loc);
|
||||
}
|
||||
|
||||
void Game::unloadLocation(bool storeState) {
|
||||
internalGameServer.clearRuntimeLocations(storeState);
|
||||
gameServerHost.clearRuntimeLocations(storeState);
|
||||
// Textures only the location's models and characters used are freed now.
|
||||
renderer.textureManager.UnloadUnused();
|
||||
}
|
||||
@@ -639,8 +641,8 @@ namespace FRG
|
||||
// Rebuild the loaded location from its config; the others have no stored state
|
||||
// any more, so they are built fresh when the player first goes there.
|
||||
std::vector<std::string> loadedIds =
|
||||
internalGameServer.loadedRuntimeLocationIds();
|
||||
internalGameServer.resetCampaign(
|
||||
gameServerHost.loadedRuntimeLocationIds();
|
||||
gameServerHost.resetCampaign(
|
||||
activeCampaign().questsPath, CONST_ZIP_FILE);
|
||||
unloadLocation(false);
|
||||
for (const std::string& id : loadedIds) loadLocation(id);
|
||||
@@ -649,6 +651,7 @@ namespace FRG
|
||||
// from this freshly-rebuilt state after changing gameplay/content JSON or Lua.
|
||||
// Re-comment before shipping.
|
||||
//saveInitialState();
|
||||
|
||||
}
|
||||
|
||||
void Game::performResetToInitialState() {
|
||||
@@ -671,7 +674,7 @@ namespace FRG
|
||||
if (!content.empty()) {
|
||||
try {
|
||||
nlohmann::json root = nlohmann::json::parse(content);
|
||||
internalGameServer.load(root); // applies to the loaded location in place and stores
|
||||
gameServerHost.load(root); // applies to the loaded location in place and stores
|
||||
// the other locations' states for when they load.
|
||||
return;
|
||||
} catch (const std::exception& e) {
|
||||
@@ -696,7 +699,7 @@ namespace FRG
|
||||
|
||||
void Game::saveInitialState() {
|
||||
nlohmann::json root;
|
||||
internalGameServer.save(root);
|
||||
gameServerHost.save(root);
|
||||
// Written next to the executable; copy it over the campaign's start-state file.
|
||||
const std::string startStatePath = activeCampaign().startStatePath;
|
||||
const std::string path = startStatePath.substr(startStatePath.find_last_of('/') + 1);
|
||||
@@ -729,7 +732,7 @@ namespace FRG
|
||||
|
||||
auto loc = std::make_shared<Location>(
|
||||
renderer, internalGameServer, config.id);
|
||||
internalGameServer.setupRuntimeLocation(
|
||||
gameServerHost.setupRuntimeLocation(
|
||||
config.id, loc, std::move(params), audioPlayer.get());
|
||||
loc->requestNightDayTransition = [this](bool isNight, bool isDawn) {
|
||||
const auto state = stateSnapshot();
|
||||
@@ -861,8 +864,8 @@ namespace FRG
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
logger() << "Shadow map initialized" << std::endl;
|
||||
|
||||
for (const std::string& id : internalGameServer.loadedRuntimeLocationIds())
|
||||
if (Location* loc = internalGameServer.runtimeLocation(id))
|
||||
for (const std::string& id : gameServerHost.loadedRuntimeLocationIds())
|
||||
if (Location* loc = gameServerHost.runtimeLocation(id))
|
||||
loc->shadowMap = shadowMap;
|
||||
|
||||
}
|
||||
@@ -873,8 +876,8 @@ namespace FRG
|
||||
shadowMap.reset();
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
|
||||
for (const std::string& id : internalGameServer.loadedRuntimeLocationIds())
|
||||
if (Location* loc = internalGameServer.runtimeLocation(id))
|
||||
for (const std::string& id : gameServerHost.loadedRuntimeLocationIds())
|
||||
if (Location* loc = gameServerHost.runtimeLocation(id))
|
||||
loc->shadowMap.reset();
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
|
||||
@@ -1135,7 +1138,7 @@ namespace FRG
|
||||
{
|
||||
// The authoritative implementation advances simulation; a future
|
||||
// network client can use the same call to poll/apply server updates.
|
||||
internalGameServer.update(delta);
|
||||
gameServerHost.update(delta);
|
||||
processServerEvents();
|
||||
}
|
||||
|
||||
@@ -1882,7 +1885,7 @@ namespace FRG
|
||||
void Game::saveGame(int slot)
|
||||
{
|
||||
nlohmann::json root;
|
||||
internalGameServer.save(root);
|
||||
gameServerHost.save(root);
|
||||
std::string path = "save_slot" + std::to_string(slot) + ".json";
|
||||
|
||||
/*std::ofstream file(path);
|
||||
@@ -1922,7 +1925,7 @@ namespace FRG
|
||||
return;
|
||||
}
|
||||
enterLocationThen(campaignId, locationId, [this, root]() {
|
||||
internalGameServer.load(root);
|
||||
gameServerHost.load(root);
|
||||
playMusicForLoadedGame();
|
||||
menuManager.enterGameplay(true);
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "Location.h"
|
||||
#include "AudioPlayerAsync.h"
|
||||
#include "game_server/InternalGameServer.h"
|
||||
#include "game_server/LocalGameServerHost.h"
|
||||
namespace FRG {
|
||||
|
||||
struct LocationConfig;
|
||||
@@ -83,6 +84,7 @@ namespace FRG {
|
||||
|
||||
private:
|
||||
InternalGameServer internalGameServer;
|
||||
LocalGameServerHost gameServerHost;
|
||||
|
||||
public:
|
||||
IGameServer& server() { return internalGameServer; }
|
||||
|
||||
+6
-3
@@ -1,4 +1,5 @@
|
||||
#include "GameState.h"
|
||||
#include "utils/Utils.h"
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
@@ -209,8 +210,8 @@ void GameState::load(const nlohmann::json& in)
|
||||
|
||||
// --- Players ---
|
||||
const auto& playerValues = in.at("players");
|
||||
if (!playerValues.is_object() || !playerValues.contains("player"))
|
||||
throw std::runtime_error("Game state requires players.player");
|
||||
if (!playerValues.is_object())
|
||||
throw std::runtime_error("Game state players must be an object");
|
||||
for (const auto& [id, value] : playerValues.items()) {
|
||||
auto& player = players[id];
|
||||
if (!player) player = std::make_shared<CharacterState>();
|
||||
@@ -224,7 +225,9 @@ void GameState::load(const nlohmann::json& in)
|
||||
|
||||
// --- Locations ---
|
||||
const auto& locationValues = in.at("locations");
|
||||
if (!locationValues.is_object() ||
|
||||
if (!locationValues.is_object())
|
||||
throw std::runtime_error("Game state locations must be an object");
|
||||
if (!currentLocationName.empty() &&
|
||||
!locationValues.contains(currentLocationName))
|
||||
throw std::runtime_error(
|
||||
"Game state does not contain current location '" +
|
||||
|
||||
+63
-13
@@ -320,6 +320,8 @@ namespace FRG
|
||||
void Location::setup(const LocationSetup& params, AudioPlayerAsync* audioPlayer)
|
||||
{
|
||||
if (!params.gameState) throw std::runtime_error("Location requires authoritative state binding");
|
||||
if (!params.definition) throw std::runtime_error("Location requires prepared content definition");
|
||||
const LocationDefinition& definition = *params.definition;
|
||||
this->audioPlayer = audioPlayer;
|
||||
pathVisualization.initialize();
|
||||
|
||||
@@ -330,16 +332,18 @@ namespace FRG
|
||||
|
||||
// Load static game objects
|
||||
{
|
||||
StaticObjectSet staticObjects = GameObjectLoader::loadAndCreateGameObjects(params.gameObjectsJsonPath, renderer, CONST_ZIP_FILE);
|
||||
StaticObjectSet staticObjects =
|
||||
GameObjectLoader::createGameObjects(
|
||||
definition.staticObjects, renderer, CONST_ZIP_FILE);
|
||||
gameObjects = std::move(staticObjects.single);
|
||||
instancedBatches = std::move(staticObjects.batches);
|
||||
}
|
||||
SDL_PumpEvents();
|
||||
|
||||
// Load interactive objects
|
||||
interactiveObjects = GameObjectLoader::loadAndCreateInteractiveObjects(
|
||||
params.interactiveObjectsJsonPath, renderer, params.gameState->interactiveObjects,
|
||||
CONST_ZIP_FILE);
|
||||
interactiveObjects = GameObjectLoader::createInteractiveObjects(
|
||||
definition.interactiveObjects, renderer,
|
||||
params.gameState->interactiveObjects, CONST_ZIP_FILE);
|
||||
SDL_PumpEvents();
|
||||
|
||||
auto sparkTexture = renderer.textureManager.LoadFromPng("resources/w/spark.png", CONST_ZIP_FILE);
|
||||
@@ -347,10 +351,6 @@ namespace FRG
|
||||
if (!params.player) throw std::runtime_error("Location requires game-level player binding");
|
||||
player = params.player;
|
||||
player->bindToLocation(locationId_);
|
||||
updateCharacterState(*player, [¶ms](CharacterState& workingState) {
|
||||
workingState.position = params.playerPosition;
|
||||
});
|
||||
setCharacterTarget(*player, params.playerPosition);
|
||||
synchronizeRemotePlayers();
|
||||
logger() << "Load resurces step 9" << std::endl;
|
||||
SDL_PumpEvents();
|
||||
@@ -358,8 +358,9 @@ namespace FRG
|
||||
// Load NPCs from JSON. Aggressive (canAttack) NPCs need a player target
|
||||
// and hit sparks; peaceful NPCs don't take damage from the player so
|
||||
// they don't need either.
|
||||
npcs = GameObjectLoader::loadAndCreate_Npcs(
|
||||
params.npcsJsonPath, renderer, params.gameState->npcs, gameServer_, locationId_, CONST_ZIP_FILE);
|
||||
npcs = GameObjectLoader::createNpcs(
|
||||
definition.npcs, renderer, params.gameState->npcs,
|
||||
gameServer_, locationId_, CONST_ZIP_FILE);
|
||||
for (auto& npc : npcs) {
|
||||
if (!npc) continue;
|
||||
auto state = npc->stateSnapshot();
|
||||
@@ -371,9 +372,58 @@ namespace FRG
|
||||
|
||||
SDL_PumpEvents();
|
||||
|
||||
loadTeleportZones(params.teleportsJsonPath, CONST_ZIP_FILE);
|
||||
loadTriggerZones(params.triggerZonesJsonPath, CONST_ZIP_FILE);
|
||||
loadPointLights(params.lightsJsonPath, CONST_ZIP_FILE);
|
||||
auto activeTeleportTexture =
|
||||
renderer.textureManager.LoadFromPng(
|
||||
"resources/w/star.png", CONST_ZIP_FILE);
|
||||
auto inactiveTeleportTexture =
|
||||
renderer.textureManager.LoadFromPng(
|
||||
"resources/w/star_red.png", CONST_ZIP_FILE);
|
||||
for (const TeleportDefinition& data : definition.teleports) {
|
||||
TeleportZone zone;
|
||||
zone.id = data.id;
|
||||
zone.position = data.position;
|
||||
zone.radius = data.radius;
|
||||
zone.destinationLocation = data.destinationLocation;
|
||||
zone.destinationPosition = data.destinationPosition;
|
||||
zone.destinationRotationY = data.destinationRotationY;
|
||||
zone.automatic = data.automatic;
|
||||
zone.automaticRadius = data.automaticRadius;
|
||||
auto active = params.gameState->teleportZones.find(data.id);
|
||||
zone.active = active != params.gameState->teleportZones.end()
|
||||
? active->second : data.active;
|
||||
zone.initSparks(
|
||||
activeTeleportTexture, inactiveTeleportTexture);
|
||||
teleportZones.push_back(std::move(zone));
|
||||
}
|
||||
for (const TriggerZoneDefinition& data : definition.triggerZones) {
|
||||
TriggerZone zone;
|
||||
zone.id = data.id;
|
||||
zone.position = data.position;
|
||||
zone.radius = data.radius;
|
||||
zone.hysteresis = data.hysteresis;
|
||||
auto state = params.gameState->triggerZones.find(data.id);
|
||||
if (state != params.gameState->triggerZones.end()) {
|
||||
zone.enabled = state->second.enabled;
|
||||
zone.playerInside = state->second.playerInside;
|
||||
} else {
|
||||
zone.enabled = data.enabled;
|
||||
}
|
||||
triggerZones.push_back(std::move(zone));
|
||||
}
|
||||
for (const PointLightDefinition& data : definition.pointLights) {
|
||||
PointLight light;
|
||||
light.id = data.id;
|
||||
light.position = data.position;
|
||||
light.direction = data.direction;
|
||||
light.color = data.color;
|
||||
light.autoLight = data.autoLight;
|
||||
light.autoLightDistance = data.autoLightDistance;
|
||||
light.limitX = data.limitX;
|
||||
light.limitY = data.limitY;
|
||||
light.autoLightLimitX = data.autoLightLimitX;
|
||||
light.autoLightLimitY = data.autoLightLimitY;
|
||||
pointLights.push_back(std::move(light));
|
||||
}
|
||||
|
||||
SDL_PumpEvents();
|
||||
|
||||
|
||||
+2
-1
@@ -53,6 +53,7 @@ namespace FRG
|
||||
|
||||
struct LocationSetup
|
||||
{
|
||||
std::shared_ptr<const LocationDefinition> definition;
|
||||
std::string gameObjectsJsonPath;
|
||||
std::string interactiveObjectsJsonPath;
|
||||
std::string npcsJsonPath;
|
||||
@@ -225,7 +226,7 @@ namespace FRG
|
||||
friend class LocationEditor;
|
||||
friend class Game;
|
||||
friend class ScriptEngine;
|
||||
friend class InternalGameServer;
|
||||
friend class LocalGameServerHost;
|
||||
Renderer& renderer;
|
||||
AudioPlayerAsync* audioPlayer;
|
||||
|
||||
|
||||
@@ -10,6 +10,100 @@ constexpr float PI = 3.14159265358979323846f;
|
||||
constexpr float NPC_TALK_DISTANCE = 1.35f;
|
||||
}
|
||||
|
||||
bool LocationGameState::setup(
|
||||
LocationDefinition content, const std::string& zipFile)
|
||||
{
|
||||
const bool initializeRuntimeFields = !hasPersistentState;
|
||||
if (!location) location = std::make_shared<LocationState>();
|
||||
if (location->navigationMaps.empty())
|
||||
location->initNavigation(content.config.navigationJsonPaths, zipFile);
|
||||
lockInteractiveObjectsDuringTutorial =
|
||||
content.config.lockInteractiveObjectsDuringTutorial;
|
||||
|
||||
if (content.config.taxi) {
|
||||
const LocationConfig::Taxi& taxi = *content.config.taxi;
|
||||
location->taxiCarTriggerPosition = taxi.triggerPosition;
|
||||
location->taxiCarWaypoints = taxi.waypoints;
|
||||
location->taxiCarInitialState.position = taxi.initialPosition;
|
||||
location->taxiCarInitialState.rotation = taxi.initialRotation;
|
||||
location->taxiCarInitialState.mode = NpcCar::Mode::NONE;
|
||||
location->taxiCarInitialState.velocity = 0.f;
|
||||
location->taxiCarInitialState.steeringAngle = 0.f;
|
||||
location->taxiCarInitialState.waypointReachRadius =
|
||||
taxi.waypointReachRadius;
|
||||
location->taxiCar.waypointReachRadius = taxi.waypointReachRadius;
|
||||
if (initializeRuntimeFields) resetTaxi();
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, bool> configuredNpcs;
|
||||
for (const NpcData& data : content.npcs) {
|
||||
configuredNpcs.emplace(data.id, true);
|
||||
CharacterState defaults = data.toCharacterState();
|
||||
loadCharacterAnimationMetadata(defaults, zipFile);
|
||||
auto& state = npcs[data.id];
|
||||
if (!state) {
|
||||
state = std::make_shared<CharacterState>(std::move(defaults));
|
||||
} else {
|
||||
nlohmann::json runtime;
|
||||
state->save(runtime);
|
||||
*state = std::move(defaults);
|
||||
state->load(runtime);
|
||||
}
|
||||
}
|
||||
for (const auto& [id, state] : npcs)
|
||||
if (configuredNpcs.count(id) == 0)
|
||||
throw std::runtime_error(
|
||||
"Saved location contains unknown NPC '" + id + "'");
|
||||
|
||||
std::unordered_map<std::string, bool> configuredObjects;
|
||||
for (const InteractiveObjectData& data : content.interactiveObjects) {
|
||||
configuredObjects.emplace(data.id, true);
|
||||
InteractiveObjectState defaults = data.toInteractiveObjectState();
|
||||
auto& state = interactiveObjects[data.id];
|
||||
if (!state) {
|
||||
state =
|
||||
std::make_shared<InteractiveObjectState>(std::move(defaults));
|
||||
} else {
|
||||
nlohmann::json runtime;
|
||||
state->save(runtime);
|
||||
*state = std::move(defaults);
|
||||
state->load(runtime);
|
||||
}
|
||||
}
|
||||
for (const auto& [id, state] : interactiveObjects)
|
||||
if (configuredObjects.count(id) == 0)
|
||||
throw std::runtime_error(
|
||||
"Saved location contains unknown interactive object '" +
|
||||
id + "'");
|
||||
|
||||
std::unordered_map<std::string, TriggerZoneGameState> newTriggers;
|
||||
for (const TriggerZoneDefinition& data : content.triggerZones) {
|
||||
auto existing = triggerZones.find(data.id);
|
||||
newTriggers[data.id] = existing != triggerZones.end()
|
||||
? existing->second
|
||||
: TriggerZoneGameState{data.enabled, false};
|
||||
}
|
||||
if (hasPersistentState && newTriggers.size() != triggerZones.size())
|
||||
throw std::runtime_error(
|
||||
"Saved trigger-zone IDs do not match location content");
|
||||
triggerZones = std::move(newTriggers);
|
||||
|
||||
std::unordered_map<std::string, bool> newTeleports;
|
||||
for (const TeleportDefinition& data : content.teleports) {
|
||||
auto existing = teleportZones.find(data.id);
|
||||
newTeleports[data.id] =
|
||||
existing != teleportZones.end() ? existing->second : data.active;
|
||||
}
|
||||
if (hasPersistentState && newTeleports.size() != teleportZones.size())
|
||||
throw std::runtime_error(
|
||||
"Saved teleport IDs do not match location content");
|
||||
teleportZones = std::move(newTeleports);
|
||||
|
||||
definition =
|
||||
std::make_shared<const LocationDefinition>(std::move(content));
|
||||
return initializeRuntimeFields;
|
||||
}
|
||||
|
||||
CharacterState* LocationGameState::findCharacter(
|
||||
const std::string& id, PlayerStateMap& players)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "CharacterState.h"
|
||||
#include "LocationState.h"
|
||||
#include "items/InteractiveObjectState.h"
|
||||
#include "content/LocationDefinition.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -57,7 +58,9 @@ struct LocationGameState {
|
||||
std::unordered_map<std::string, bool> teleportZones;
|
||||
nlohmann::json dialogue = nlohmann::json::object();
|
||||
nlohmann::json scriptGlobals = nlohmann::json::object();
|
||||
std::shared_ptr<const LocationDefinition> definition;
|
||||
|
||||
bool setup(LocationDefinition content, const std::string& zipFile);
|
||||
CharacterState* findCharacter(const std::string& id, PlayerStateMap& players);
|
||||
const CharacterState* findCharacter(
|
||||
const std::string& id, const PlayerStateMap& players) const;
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace FRG {
|
||||
|
||||
extern const char* CONST_ZIP_FILE;
|
||||
|
||||
namespace {
|
||||
nlohmann::json saveCar(const NpcCar& car)
|
||||
{
|
||||
@@ -51,7 +49,8 @@ namespace FRG {
|
||||
return navigationMaps[activeNavigationIndex].findPathToNearest(start, end, {});
|
||||
}
|
||||
|
||||
void LocationState::initNavigation(const std::vector<std::string>& paths)
|
||||
void LocationState::initNavigation(
|
||||
const std::vector<std::string>& paths, const std::string& zipFile)
|
||||
{
|
||||
const int savedActiveNavigationIndex = activeNavigationIndex;
|
||||
//navigationMapPaths = paths;
|
||||
@@ -59,10 +58,7 @@ namespace FRG {
|
||||
navigationMaps.resize(paths.size());
|
||||
|
||||
for (size_t i = 0; i < paths.size(); ++i) {
|
||||
// Service the OS message queue between builds so the window doesn't
|
||||
// get flagged as "not responding" while loading many navigation grids.
|
||||
SDL_PumpEvents();
|
||||
navigationMaps[i].build(paths[i], CONST_ZIP_FILE);
|
||||
navigationMaps[i].build(paths[i], zipFile);
|
||||
}
|
||||
|
||||
activeNavigationIndex =
|
||||
|
||||
+5
-3
@@ -12,7 +12,9 @@ struct LocationState : public ISaveable {
|
||||
//It might be changed only in the editor mode during development.
|
||||
std::vector<PathFinder> navigationMaps;
|
||||
std::vector<Eigen::Vector3f> planPathExceptMyself(const Eigen::Vector3f& start, const Eigen::Vector3f& end, const std::string& entityId) const;
|
||||
void initNavigation(const std::vector<std::string>& paths);
|
||||
void initNavigation(
|
||||
const std::vector<std::string>& paths,
|
||||
const std::string& zipFile = std::string());
|
||||
|
||||
// ---- Camera ----
|
||||
float cameraAzimuth = -2.35f;
|
||||
@@ -29,8 +31,8 @@ struct LocationState : public ISaveable {
|
||||
NpcCar taxiCarInitialState;
|
||||
NpcCar taxiCar;
|
||||
bool taxiArrivedCallbackCalled = false;
|
||||
Vector3f taxiCarTriggerPosition = Vector3f(0,0,0);
|
||||
std::vector<Vector3f> taxiCarWaypoints;
|
||||
Eigen::Vector3f taxiCarTriggerPosition = Eigen::Vector3f(0,0,0);
|
||||
std::vector<Eigen::Vector3f> taxiCarWaypoints;
|
||||
|
||||
// (trigger/teleport zone runtime flags are serialised by id directly in Location::save/load)
|
||||
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "render/Renderer.h"
|
||||
#include "Environment.h"
|
||||
#include <Eigen/Core>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
struct NpcCar
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
#include "LocationDefinition.h"
|
||||
#include "../external/nlohmann/json.hpp"
|
||||
#include "../utils/Utils.h"
|
||||
#include <Eigen/Geometry>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace FRG {
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::json;
|
||||
constexpr float kPi = 3.14159265358979323846f;
|
||||
|
||||
json loadJson(const std::string& path, const std::string& zipFile)
|
||||
{
|
||||
if (path.empty()) return json::object();
|
||||
std::string content;
|
||||
if (zipFile.empty()) {
|
||||
content = readTextFile(path);
|
||||
} else {
|
||||
const auto bytes = readFileFromZIP(path, zipFile);
|
||||
content.assign(bytes.begin(), bytes.end());
|
||||
}
|
||||
if (content.empty()) throw std::runtime_error("Failed to read " + path);
|
||||
try {
|
||||
return json::parse(content);
|
||||
} catch (const std::exception& error) {
|
||||
throw std::runtime_error(
|
||||
"JSON parse error in " + path + ": " + error.what());
|
||||
}
|
||||
}
|
||||
|
||||
GameObjectData parseGameObject(const json& item)
|
||||
{
|
||||
GameObjectData data;
|
||||
data.name = item.value("name", "Unknown");
|
||||
data.texturePath = item.value("texturePath", "");
|
||||
data.textureDarkandsPath = item.value("textureDarkandsPath", "");
|
||||
data.meshPath = item.value("meshPath", "");
|
||||
data.rotationX = item.value("rotationX", 0.f);
|
||||
data.rotationY = item.value("rotationY", 0.f);
|
||||
data.rotationZ = item.value("rotationZ", 0.f);
|
||||
data.positionX = item.value("positionX", 0.f);
|
||||
data.positionY = item.value("positionY", 0.f);
|
||||
data.positionZ = item.value("positionZ", 0.f);
|
||||
data.scale = item.value("scale", 1.f);
|
||||
data.instanced = item.value("instanced", true);
|
||||
return data;
|
||||
}
|
||||
|
||||
Eigen::Vector3f vector(
|
||||
const json& item, const char* x, const char* y, const char* z,
|
||||
const Eigen::Vector3f& fallback = Eigen::Vector3f::Zero())
|
||||
{
|
||||
return {item.value(x, fallback.x()), item.value(y, fallback.y()),
|
||||
item.value(z, fallback.z())};
|
||||
}
|
||||
|
||||
std::pair<int, int> readBinaryAnimationMetadata(
|
||||
const std::string& path, const std::string& zipFile)
|
||||
{
|
||||
const std::vector<char> bytes = zipFile.empty()
|
||||
? readFile(path) : readFileFromZIP(path, zipFile);
|
||||
if (bytes.size() < 8)
|
||||
throw std::runtime_error("Invalid animation file " + path);
|
||||
const char* cursor = bytes.data();
|
||||
const char* end = cursor + bytes.size();
|
||||
auto skip = [&](std::size_t size) {
|
||||
if (size > static_cast<std::size_t>(end - cursor))
|
||||
throw std::runtime_error("Unexpected EOF in " + path);
|
||||
cursor += size;
|
||||
};
|
||||
auto readU32 = [&]() {
|
||||
std::uint32_t value = 0;
|
||||
if (sizeof(value) > static_cast<std::size_t>(end - cursor))
|
||||
throw std::runtime_error("Unexpected EOF in " + path);
|
||||
std::memcpy(&value, cursor, sizeof(value));
|
||||
cursor += sizeof(value);
|
||||
return value;
|
||||
};
|
||||
auto readI32 = [&]() {
|
||||
std::int32_t value = 0;
|
||||
if (sizeof(value) > static_cast<std::size_t>(end - cursor))
|
||||
throw std::runtime_error("Unexpected EOF in " + path);
|
||||
std::memcpy(&value, cursor, sizeof(value));
|
||||
cursor += sizeof(value);
|
||||
return value;
|
||||
};
|
||||
auto skipString = [&]() { skip(readU32()); };
|
||||
|
||||
if (std::memcmp(cursor, "BSMF", 4) != 0)
|
||||
throw std::runtime_error("Invalid animation file " + path);
|
||||
skip(4);
|
||||
if (readU32() != 2)
|
||||
throw std::runtime_error("Unsupported animation version in " + path);
|
||||
skip(16 * sizeof(float));
|
||||
const std::uint32_t boneCount = readU32();
|
||||
for (std::uint32_t bone = 0; bone < boneCount; ++bone) {
|
||||
skip(3 * sizeof(float) + sizeof(float) + 9 * sizeof(float));
|
||||
skip(sizeof(std::int32_t));
|
||||
skip(static_cast<std::size_t>(readU32()) * sizeof(std::int32_t));
|
||||
}
|
||||
for (std::uint32_t bone = 0; bone < boneCount; ++bone) skipString();
|
||||
|
||||
const std::uint32_t meshCount = readU32();
|
||||
for (std::uint32_t mesh = 0; mesh < meshCount; ++mesh) {
|
||||
skipString();
|
||||
const std::uint32_t vertexCount = readU32();
|
||||
skip(static_cast<std::size_t>(vertexCount) * 3 * sizeof(float));
|
||||
const std::uint32_t faceCount = readU32();
|
||||
skip(static_cast<std::size_t>(faceCount) * 3 * 2 * sizeof(float));
|
||||
skip(static_cast<std::size_t>(vertexCount) * 3 * sizeof(float));
|
||||
const std::uint32_t triangleCount = readU32();
|
||||
skip(static_cast<std::size_t>(triangleCount) * 3 * sizeof(std::int32_t));
|
||||
for (std::uint32_t vertex = 0; vertex < vertexCount; ++vertex)
|
||||
skip(static_cast<std::size_t>(readU32()) *
|
||||
(sizeof(std::int32_t) + sizeof(float)));
|
||||
}
|
||||
|
||||
const std::uint32_t keyframeCount = readU32();
|
||||
int lastFrame = 0;
|
||||
for (std::uint32_t frame = 0; frame < keyframeCount; ++frame) {
|
||||
lastFrame = readI32();
|
||||
skip(static_cast<std::size_t>(boneCount) *
|
||||
(3 * sizeof(float) + 16 * sizeof(float)));
|
||||
}
|
||||
return {0, keyframeCount == 0 ? 1 : lastFrame + 1};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LoadedGameObjectState GameObjectData::toLoadedGameObjectState() const
|
||||
{
|
||||
LoadedGameObjectState state;
|
||||
state.texturePath = texturePath;
|
||||
state.textureDarkandsPath = textureDarkandsPath;
|
||||
state.meshPath = meshPath;
|
||||
state.meshRotationX = rotationX;
|
||||
state.meshRotationY = rotationY;
|
||||
state.meshRotationZ = rotationZ;
|
||||
state.meshScale = scale;
|
||||
return state;
|
||||
}
|
||||
|
||||
InteractiveObjectState InteractiveObjectData::toInteractiveObjectState() const
|
||||
{
|
||||
InteractiveObjectState state;
|
||||
state.objectInfo = base.toLoadedGameObjectState();
|
||||
state.interactionRadius = interactionRadius;
|
||||
state.approachRadius = approachRadius;
|
||||
state.castShadow = castShadow;
|
||||
state.castShadowNight = castShadowNight;
|
||||
state.isActive = isActive;
|
||||
state.activateFunctionName = activateFunctionName;
|
||||
state.pivot = pivot;
|
||||
state.boundsMin = boundsMin;
|
||||
state.boundsMax = boundsMax;
|
||||
state.hasInteractionPosition = hasInteractionPosition;
|
||||
state.interactionPosition = interactionPosition;
|
||||
state.jsonPositionX = base.positionX;
|
||||
state.jsonPositionY = base.positionY;
|
||||
state.jsonPositionZ = base.positionZ;
|
||||
state.position =
|
||||
Eigen::Vector3f(base.positionX, base.positionY, base.positionZ);
|
||||
return state;
|
||||
}
|
||||
|
||||
CharacterState NpcData::toCharacterState() const
|
||||
{
|
||||
CharacterState state;
|
||||
state.npcName = name;
|
||||
state.position = {positionX, positionY, positionZ};
|
||||
state.homePosition = {positionX, 0.f, positionZ};
|
||||
state.stopInPlace();
|
||||
state.facingAngle = facingAngle;
|
||||
state.walkSpeed = walkSpeed;
|
||||
state.rotationSpeed = rotationSpeed;
|
||||
state.modelScale = modelScale;
|
||||
state.setHp(hp);
|
||||
state.initialHp = hp;
|
||||
state.visibleRange = visibleRange;
|
||||
state.canAttack = canAttack;
|
||||
if (canAttack) state.attackTargetId = "player";
|
||||
state.enabled = enabled;
|
||||
state.interactionRadius = interactionRadius;
|
||||
|
||||
Eigen::Quaternionf correction = Eigen::Quaternionf::Identity();
|
||||
if (modelCorrectionRotX != 0.f)
|
||||
correction = Eigen::Quaternionf(Eigen::AngleAxisf(
|
||||
modelCorrectionRotX, Eigen::Vector3f::UnitX())) * correction;
|
||||
if (modelCorrectionRotY != 0.f)
|
||||
correction = Eigen::Quaternionf(Eigen::AngleAxisf(
|
||||
modelCorrectionRotY, Eigen::Vector3f::UnitY())) * correction;
|
||||
if (modelCorrectionRotZ != 0.f)
|
||||
correction = Eigen::Quaternionf(Eigen::AngleAxisf(
|
||||
modelCorrectionRotZ, Eigen::Vector3f::UnitZ())) * correction;
|
||||
state.modelCorrectionRotation = correction;
|
||||
|
||||
auto addAnimation = [&](AnimationState animation, const std::string& path) {
|
||||
if (!path.empty())
|
||||
state.creationInfo.animationPaths.emplace_back(animation, path);
|
||||
};
|
||||
addAnimation(AnimationState::STAND, animationIdlePath);
|
||||
addAnimation(AnimationState::WALK, animationWalkPath);
|
||||
addAnimation(AnimationState::ACTION_IDLE, animationActionIdlePath);
|
||||
addAnimation(AnimationState::ACTION_ATTACK, animationActionAttackPath);
|
||||
addAnimation(AnimationState::STAND_TO_ACTION, animationStandToActionPath);
|
||||
addAnimation(AnimationState::ACTION_TO_STAND, animationActionToStandPath);
|
||||
addAnimation(AnimationState::ACTION_TO_DEATH, animationActionToDeathPath);
|
||||
addAnimation(AnimationState::DEATH_IDLE, animationDeathIdlePath);
|
||||
state.creationInfo.meshTexturePaths = {
|
||||
meshTextures.begin(), meshTextures.end()};
|
||||
state.creationInfo.singleTexturePath = texturePath;
|
||||
return state;
|
||||
}
|
||||
|
||||
void loadCharacterAnimationMetadata(
|
||||
CharacterState& state, const std::string& zipFile)
|
||||
{
|
||||
for (const auto& [animation, path] : state.creationInfo.animationPaths) {
|
||||
if (path.size() < 5 || path.substr(path.size() - 5) != ".anim")
|
||||
continue;
|
||||
try {
|
||||
const auto [startFrame, frameCount] =
|
||||
readBinaryAnimationMetadata(path, zipFile);
|
||||
state.setAnimationMetadata(animation, startFrame, frameCount);
|
||||
} catch (const std::exception& error) {
|
||||
logger() << "[content] Animation metadata failed for " << path
|
||||
<< ": " << error.what() << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LocationDefinition LocationContentLoader::load(
|
||||
const LocationConfig& config, const std::string& zipFile)
|
||||
{
|
||||
LocationDefinition result;
|
||||
result.config = config;
|
||||
|
||||
if (!config.gameObjectsJsonPath.empty()) {
|
||||
const json root = loadJson(config.gameObjectsJsonPath, zipFile);
|
||||
for (const auto& item : root.value("objects", json::array())) {
|
||||
GameObjectData data = parseGameObject(item);
|
||||
if (!data.meshPath.empty())
|
||||
result.staticObjects.push_back(std::move(data));
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.interactiveObjectsJsonPath.empty()) {
|
||||
const json root = loadJson(config.interactiveObjectsJsonPath, zipFile);
|
||||
std::unordered_set<std::string> ids;
|
||||
for (const auto& item : root.value("objects", json::array())) {
|
||||
InteractiveObjectData data;
|
||||
data.id = item.at("id").get<std::string>();
|
||||
if (data.id.empty() || !ids.insert(data.id).second)
|
||||
throw std::runtime_error(
|
||||
"Missing or duplicate interactive-object id in " +
|
||||
config.interactiveObjectsJsonPath);
|
||||
data.base = parseGameObject(item);
|
||||
data.interactionRadius = item.value("interactionRadius", 2.f);
|
||||
data.approachRadius =
|
||||
item.value("approachRadius", data.interactionRadius);
|
||||
data.activateFunctionName =
|
||||
item.value("activateFunction", "");
|
||||
data.pivot = vector(item, "pivotX", "pivotY", "pivotZ");
|
||||
data.boundsMin =
|
||||
vector(item, "boundsMinX", "boundsMinY", "boundsMinZ");
|
||||
data.boundsMax =
|
||||
vector(item, "boundsMaxX", "boundsMaxY", "boundsMaxZ");
|
||||
data.hasInteractionPosition =
|
||||
item.contains("interactionPositionX");
|
||||
if (data.hasInteractionPosition) {
|
||||
data.interactionPosition = vector(
|
||||
item, "interactionPositionX", "interactionPositionY",
|
||||
"interactionPositionZ");
|
||||
}
|
||||
data.castShadow = item.value("castShadow", true);
|
||||
data.castShadowNight = item.value("castShadowNight", true);
|
||||
data.isActive = item.value("isActive", true);
|
||||
result.interactiveObjects.push_back(std::move(data));
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.npcsJsonPath.empty()) {
|
||||
const json root = loadJson(config.npcsJsonPath, zipFile);
|
||||
std::unordered_set<std::string> ids;
|
||||
for (const auto& item : root.value("npcs", json::array())) {
|
||||
NpcData data;
|
||||
data.id = item.at("id").get<std::string>();
|
||||
if (data.id.empty() || data.id == "player" ||
|
||||
!ids.insert(data.id).second) {
|
||||
throw std::runtime_error(
|
||||
"Missing, duplicate, or reserved NPC id in " +
|
||||
config.npcsJsonPath);
|
||||
}
|
||||
data.name = item.value("name", "Unknown NPC");
|
||||
data.texturePath = item.value("texturePath", "");
|
||||
if (item.contains("meshTextures") &&
|
||||
item["meshTextures"].is_object()) {
|
||||
for (const auto& [name, path] :
|
||||
item["meshTextures"].items()) {
|
||||
if (path.is_string())
|
||||
data.meshTextures[name] = path.get<std::string>();
|
||||
}
|
||||
}
|
||||
data.interactionRadius =
|
||||
item.value("interactionRadius", 0.f);
|
||||
data.animationIdlePath =
|
||||
item.value("animationIdlePath", "");
|
||||
data.animationWalkPath =
|
||||
item.value("animationWalkPath", "");
|
||||
data.animationActionIdlePath =
|
||||
item.value("animationActionIdlePath", "");
|
||||
data.animationActionAttackPath =
|
||||
item.value("animationActionAttackPath", "");
|
||||
data.animationStandToActionPath =
|
||||
item.value("animationStandToActionPath", "");
|
||||
data.animationActionToStandPath =
|
||||
item.value("animationActionToStandPath", "");
|
||||
data.animationActionToDeathPath =
|
||||
item.value("animationActionToDeathPath", "");
|
||||
data.animationDeathIdlePath =
|
||||
item.value("animationDeathIdlePath", "");
|
||||
data.positionX = item.value("positionX", 0.f);
|
||||
data.positionY = item.value("positionY", 0.f);
|
||||
data.positionZ = item.value("positionZ", 0.f);
|
||||
data.walkSpeed = item.value("walkSpeed", 1.5f);
|
||||
data.rotationSpeed = item.value("rotationSpeed", 8.f);
|
||||
data.modelScale = item.value("modelScale", 0.01f);
|
||||
const float degrees = kPi / 180.f;
|
||||
data.modelCorrectionRotX =
|
||||
item.value("modelCorrectionRotX", 0.f) * degrees;
|
||||
data.modelCorrectionRotY =
|
||||
item.value("modelCorrectionRotY", 0.f) * degrees;
|
||||
data.modelCorrectionRotZ =
|
||||
item.value("modelCorrectionRotZ", 0.f) * degrees;
|
||||
data.facingAngle =
|
||||
item.value("facingAngle", 0.f) * degrees;
|
||||
data.hp = item.value("hp", 100.f);
|
||||
data.visibleRange =
|
||||
item.value("visibleRange", VISIBLE_RANGE);
|
||||
data.canAttack = item.value("canAttack", false);
|
||||
data.enabled = item.value("enabled", true);
|
||||
result.npcs.push_back(std::move(data));
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.teleportsJsonPath.empty()) {
|
||||
const json root = loadJson(config.teleportsJsonPath, zipFile);
|
||||
std::unordered_set<std::string> ids;
|
||||
for (const auto& item : root.value("teleports", json::array())) {
|
||||
TeleportDefinition data;
|
||||
data.id = item.at("id").get<std::string>();
|
||||
if (data.id.empty() || !ids.insert(data.id).second)
|
||||
throw std::runtime_error(
|
||||
"Missing or duplicate teleport id in " +
|
||||
config.teleportsJsonPath);
|
||||
data.active = item.value("active", false);
|
||||
data.position =
|
||||
vector(item, "positionX", "positionY", "positionZ");
|
||||
data.radius = item.value("radius", 0.f);
|
||||
data.automatic = item.value("automatic", false);
|
||||
data.automaticRadius =
|
||||
item.value("automaticRadius", data.radius);
|
||||
data.destinationLocation =
|
||||
item.value("destinationLocation", "");
|
||||
data.destinationPosition = vector(
|
||||
item, "destinationPositionX", "destinationPositionY",
|
||||
"destinationPositionZ");
|
||||
data.destinationRotationY =
|
||||
item.value("destinationRotationY", 0.f);
|
||||
result.teleports.push_back(std::move(data));
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.triggerZonesJsonPath.empty()) {
|
||||
const json root = loadJson(config.triggerZonesJsonPath, zipFile);
|
||||
std::unordered_set<std::string> ids;
|
||||
for (const auto& item :
|
||||
root.value("trigger_zones", json::array())) {
|
||||
TriggerZoneDefinition data;
|
||||
data.id = item.at("id").get<std::string>();
|
||||
if (data.id.empty() || !ids.insert(data.id).second)
|
||||
throw std::runtime_error(
|
||||
"Missing or duplicate trigger-zone id in " +
|
||||
config.triggerZonesJsonPath);
|
||||
data.enabled = item.value("enabled", true);
|
||||
data.radius = item.value("radius", 1.5f);
|
||||
data.hysteresis = item.value("hysteresis", 0.3f);
|
||||
data.position =
|
||||
vector(item, "positionX", "positionY", "positionZ");
|
||||
result.triggerZones.push_back(std::move(data));
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.lightsJsonPath.empty()) {
|
||||
const json root = loadJson(config.lightsJsonPath, zipFile);
|
||||
for (const auto& item : root.value("lights", json::array())) {
|
||||
PointLightDefinition data;
|
||||
data.id = item.value("id", "");
|
||||
data.position =
|
||||
vector(item, "positionX", "positionY", "positionZ");
|
||||
data.direction = vector(
|
||||
item, "directionX", "directionY", "directionZ",
|
||||
Eigen::Vector3f(0.f, -1.f, 0.f));
|
||||
data.color = vector(
|
||||
item, "colorR", "colorG", "colorB",
|
||||
Eigen::Vector3f::Ones());
|
||||
data.autoLight = item.value("autoLight", false);
|
||||
data.autoLightDistance =
|
||||
item.value("autoLightDistance", -1.f);
|
||||
data.limitX = item.value("limitX", 99999.f);
|
||||
data.limitY = item.value("limitY", 99999.f);
|
||||
data.autoLightLimitX =
|
||||
item.value("autoLightLimitX", -1.f);
|
||||
data.autoLightLimitY =
|
||||
item.value("autoLightLimitY", -1.f);
|
||||
result.pointLights.push_back(std::move(data));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,132 @@
|
||||
#pragma once
|
||||
|
||||
#include "../Campaign.h"
|
||||
#include "../CharacterState.h"
|
||||
#include "../items/InteractiveObjectState.h"
|
||||
#include <Eigen/Core>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
struct GameObjectData {
|
||||
std::string name;
|
||||
std::string texturePath;
|
||||
std::string textureDarkandsPath;
|
||||
std::string meshPath;
|
||||
float rotationX = 0.f;
|
||||
float rotationY = 0.f;
|
||||
float rotationZ = 0.f;
|
||||
float positionX = 0.f;
|
||||
float positionY = 0.f;
|
||||
float positionZ = 0.f;
|
||||
float scale = 1.f;
|
||||
bool instanced = true;
|
||||
|
||||
LoadedGameObjectState toLoadedGameObjectState() const;
|
||||
};
|
||||
|
||||
struct InteractiveObjectData {
|
||||
std::string id;
|
||||
GameObjectData base;
|
||||
float interactionRadius = 2.f;
|
||||
float approachRadius = 2.f;
|
||||
std::string activateFunctionName;
|
||||
Eigen::Vector3f pivot = Eigen::Vector3f::Zero();
|
||||
Eigen::Vector3f boundsMin = Eigen::Vector3f::Zero();
|
||||
Eigen::Vector3f boundsMax = Eigen::Vector3f::Zero();
|
||||
bool hasInteractionPosition = false;
|
||||
Eigen::Vector3f interactionPosition = Eigen::Vector3f::Zero();
|
||||
bool castShadow = true;
|
||||
bool castShadowNight = true;
|
||||
bool isActive = true;
|
||||
|
||||
InteractiveObjectState toInteractiveObjectState() const;
|
||||
};
|
||||
|
||||
struct NpcData {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string texturePath;
|
||||
std::map<std::string, std::string> meshTextures;
|
||||
std::string animationIdlePath;
|
||||
std::string animationWalkPath;
|
||||
std::string animationActionIdlePath;
|
||||
std::string animationActionAttackPath;
|
||||
std::string animationStandToActionPath;
|
||||
std::string animationActionToStandPath;
|
||||
std::string animationActionToDeathPath;
|
||||
std::string animationDeathIdlePath;
|
||||
float positionX = 0.f;
|
||||
float positionY = 0.f;
|
||||
float positionZ = 0.f;
|
||||
float walkSpeed = 1.5f;
|
||||
float rotationSpeed = 8.f;
|
||||
float modelScale = 0.01f;
|
||||
float modelCorrectionRotX = 0.f;
|
||||
float modelCorrectionRotY = 0.f;
|
||||
float modelCorrectionRotZ = 0.f;
|
||||
float facingAngle = 0.f;
|
||||
float hp = 100.f;
|
||||
float visibleRange = VISIBLE_RANGE;
|
||||
bool canAttack = false;
|
||||
bool enabled = true;
|
||||
float interactionRadius = 0.f;
|
||||
|
||||
CharacterState toCharacterState() const;
|
||||
};
|
||||
|
||||
struct TeleportDefinition {
|
||||
std::string id;
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
float radius = 0.f;
|
||||
bool active = false;
|
||||
std::string destinationLocation;
|
||||
Eigen::Vector3f destinationPosition = Eigen::Vector3f::Zero();
|
||||
float destinationRotationY = 0.f;
|
||||
bool automatic = false;
|
||||
float automaticRadius = 0.f;
|
||||
};
|
||||
|
||||
struct TriggerZoneDefinition {
|
||||
std::string id;
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
float radius = 1.5f;
|
||||
float hysteresis = 0.3f;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
struct PointLightDefinition {
|
||||
std::string id;
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
Eigen::Vector3f direction = Eigen::Vector3f(0.f, -1.f, 0.f);
|
||||
Eigen::Vector3f color = Eigen::Vector3f::Ones();
|
||||
bool autoLight = false;
|
||||
float autoLightDistance = -1.f;
|
||||
float limitX = 99999.f;
|
||||
float limitY = 99999.f;
|
||||
float autoLightLimitX = -1.f;
|
||||
float autoLightLimitY = -1.f;
|
||||
};
|
||||
|
||||
struct LocationDefinition {
|
||||
LocationConfig config;
|
||||
std::vector<GameObjectData> staticObjects;
|
||||
std::vector<InteractiveObjectData> interactiveObjects;
|
||||
std::vector<NpcData> npcs;
|
||||
std::vector<TeleportDefinition> teleports;
|
||||
std::vector<TriggerZoneDefinition> triggerZones;
|
||||
std::vector<PointLightDefinition> pointLights;
|
||||
};
|
||||
|
||||
class LocationContentLoader {
|
||||
public:
|
||||
static LocationDefinition load(
|
||||
const LocationConfig& config, const std::string& zipFile = {});
|
||||
};
|
||||
|
||||
void loadCharacterAnimationMetadata(
|
||||
CharacterState& state, const std::string& zipFile = {});
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServer.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class AudioPlayerAsync;
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class Location;
|
||||
struct LocationConfig;
|
||||
struct LocationSetup;
|
||||
|
||||
// Client-only lifecycle and presentation integration for the in-process server.
|
||||
// The public gameplay contract remains IGameServer and is graphics-free.
|
||||
class IGameServerHost {
|
||||
public:
|
||||
virtual ~IGameServerHost() = default;
|
||||
|
||||
virtual void initializeCampaign(
|
||||
const std::string& campaignId, const std::string& questsPath,
|
||||
const std::string& zipFile) = 0;
|
||||
virtual void clearCampaign() = 0;
|
||||
virtual void resetCampaign(
|
||||
const std::string& questsPath, const std::string& zipFile) = 0;
|
||||
virtual bool prepareLocation(const LocationConfig& config) = 0;
|
||||
|
||||
virtual void setupRuntimeLocation(
|
||||
const std::string& id, const std::shared_ptr<Location>& location,
|
||||
LocationSetup params, AudioPlayerAsync* audioPlayer) = 0;
|
||||
virtual Location* runtimeLocation(const std::string& id) const = 0;
|
||||
virtual bool hasRuntimeLocation(const std::string& id) const = 0;
|
||||
virtual std::vector<std::string> loadedRuntimeLocationIds() const = 0;
|
||||
virtual void clearRuntimeLocations(bool retainState) = 0;
|
||||
virtual void bindRuntimeLocation(
|
||||
const std::string& id, std::shared_ptr<Location> location) = 0;
|
||||
|
||||
virtual void update(std::int64_t deltaMs) = 0;
|
||||
virtual void save(nlohmann::json& out) = 0;
|
||||
virtual void load(const nlohmann::json& in) = 0;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
struct CampaignConfig;
|
||||
struct LocationConfig;
|
||||
|
||||
// Graphics-free administrative lifecycle used by local and standalone hosts.
|
||||
class IGameServerLifecycle {
|
||||
public:
|
||||
virtual ~IGameServerLifecycle() = default;
|
||||
|
||||
virtual void startCampaign(
|
||||
const CampaignConfig& campaign, const std::string& zipFile,
|
||||
bool createLocalPlayer, bool enableTestBot) = 0;
|
||||
virtual bool prepareLocation(const LocationConfig& config) = 0;
|
||||
virtual void activateLocation(const std::string& locationId) = 0;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "InternalGameServer.h"
|
||||
#include "../Campaign.h"
|
||||
#include "../Location.h"
|
||||
#include "../content/LocationDefinition.h"
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <type_traits>
|
||||
@@ -8,8 +8,6 @@
|
||||
namespace FRG {
|
||||
|
||||
namespace {
|
||||
// Set this to false to disable the battle-zone remote-player test bot.
|
||||
constexpr bool kEnableBattleZoneTestBot = true;
|
||||
constexpr const char* kBattleZoneCampaignId = "battle_zone";
|
||||
constexpr const char* kBattleZoneTestBotId = "battle_zone_test_bot";
|
||||
constexpr std::int64_t kBattleZoneTestBotIntervalMs = 5000;
|
||||
@@ -20,7 +18,7 @@ InternalGameServer::InternalGameServer() = default;
|
||||
void InternalGameServer::ensureBattleZoneTestBot()
|
||||
{
|
||||
if (state_.campaignId != kBattleZoneCampaignId) return;
|
||||
if (!kEnableBattleZoneTestBot) {
|
||||
if (!battleZoneTestBotEnabled_) {
|
||||
state_.players.erase(kBattleZoneTestBotId);
|
||||
return;
|
||||
}
|
||||
@@ -33,7 +31,7 @@ void InternalGameServer::ensureBattleZoneTestBot()
|
||||
|
||||
void InternalGameServer::updateBattleZoneTestBot(std::int64_t deltaMs)
|
||||
{
|
||||
if (!kEnableBattleZoneTestBot ||
|
||||
if (!battleZoneTestBotEnabled_ ||
|
||||
state_.campaignId != kBattleZoneCampaignId ||
|
||||
state_.currentLocationName.empty() ||
|
||||
state_.players.find(kBattleZoneTestBotId) == state_.players.end()) {
|
||||
@@ -57,24 +55,66 @@ void InternalGameServer::initializeCampaign(
|
||||
const std::string& campaignId, const std::string& questsPath,
|
||||
const std::string& zipFile)
|
||||
{
|
||||
state_.campaignId = campaignId;
|
||||
CampaignConfig campaign;
|
||||
campaign.id = campaignId;
|
||||
campaign.questsPath = questsPath;
|
||||
startCampaign(campaign, zipFile, true, true);
|
||||
}
|
||||
|
||||
void InternalGameServer::startCampaign(
|
||||
const CampaignConfig& campaign, const std::string& zipFile,
|
||||
bool createLocalPlayer, bool enableTestBot)
|
||||
{
|
||||
resourceZipFile_ = zipFile;
|
||||
battleZoneTestBotEnabled_ = enableTestBot;
|
||||
state_.campaignId = campaign.id;
|
||||
state_.currentLocationName.clear();
|
||||
state_.locations.clear();
|
||||
state_.players.clear();
|
||||
state_.questJournal.loadFromFile(questsPath, zipFile);
|
||||
state_.questJournal.loadFromFile(campaign.questsPath, zipFile);
|
||||
state_.globalFloats["player_hp"] = 200.f;
|
||||
|
||||
if (createLocalPlayer) {
|
||||
auto player = std::make_shared<CharacterState>();
|
||||
player->configureAsPlayer();
|
||||
player->creationInfo.animationPaths = {
|
||||
{AnimationState::STAND,
|
||||
"resources/w/gg/new2/gg_stand_idle001_small.anim"},
|
||||
{AnimationState::WALK,
|
||||
"resources/w/gg/new2/gg_walk001_small.anim"},
|
||||
{AnimationState::STAND_TO_ACTION,
|
||||
"resources/w/gg/new2/gg_stand_to_action001_small.anim"},
|
||||
{AnimationState::ACTION_ATTACK,
|
||||
"resources/w/gg/new2/gg_action_chop001_small.anim"},
|
||||
{AnimationState::ACTION_ATTACK_2,
|
||||
"resources/w/gg/new2/gg_action_stab001_small.anim"},
|
||||
{AnimationState::ACTION_IDLE,
|
||||
"resources/w/gg/new2/gg_action_idle002_small.anim"},
|
||||
{AnimationState::ACTION_TO_STAND,
|
||||
"resources/w/gg/new2/gg_action_to_stand001_small.anim"},
|
||||
{AnimationState::ACTION_TO_DEATH,
|
||||
"resources/w/gg/new2/gg_die001_small.anim"},
|
||||
{AnimationState::DEATH_IDLE,
|
||||
"resources/w/gg/new2/gg_die_idle001_small.anim"}};
|
||||
loadCharacterAnimationMetadata(*player, zipFile);
|
||||
state_.players.emplace(localPlayerId_, std::move(player));
|
||||
}
|
||||
battleZoneTestBotElapsedMs_ = 0;
|
||||
ensureBattleZoneTestBot();
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::activateLocation(const std::string& locationId)
|
||||
{
|
||||
if (state_.locations.count(locationId) == 0)
|
||||
throw std::runtime_error(
|
||||
"Cannot activate unprepared location '" + locationId + "'");
|
||||
state_.currentLocationName = locationId;
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::clearCampaign()
|
||||
{
|
||||
runtimeLocations_.clear();
|
||||
state_.currentLocationName.clear();
|
||||
state_.players.clear();
|
||||
state_.locations.clear();
|
||||
@@ -117,76 +157,20 @@ void InternalGameServer::resetCampaign(
|
||||
bool InternalGameServer::prepareLocation(const LocationConfig& config)
|
||||
{
|
||||
LocationGameState& authoritative = state_.locations[config.id];
|
||||
const bool initializeLocation = !authoritative.hasPersistentState;
|
||||
if (!authoritative.location)
|
||||
authoritative.location = std::make_shared<LocationState>();
|
||||
if (authoritative.location->navigationMaps.empty())
|
||||
authoritative.location->initNavigation(config.navigationJsonPaths);
|
||||
|
||||
authoritative.lockInteractiveObjectsDuringTutorial =
|
||||
config.lockInteractiveObjectsDuringTutorial;
|
||||
if (config.taxi) {
|
||||
const LocationConfig::Taxi& taxi = *config.taxi;
|
||||
LocationState& locationState = *authoritative.location;
|
||||
locationState.taxiCarTriggerPosition = taxi.triggerPosition;
|
||||
locationState.taxiCarWaypoints = taxi.waypoints;
|
||||
locationState.taxiCarInitialState.position = taxi.initialPosition;
|
||||
locationState.taxiCarInitialState.rotation = taxi.initialRotation;
|
||||
locationState.taxiCarInitialState.mode = NpcCar::Mode::NONE;
|
||||
locationState.taxiCarInitialState.velocity = 0.f;
|
||||
locationState.taxiCarInitialState.steeringAngle = 0.f;
|
||||
locationState.taxiCarInitialState.waypointReachRadius =
|
||||
taxi.waypointReachRadius;
|
||||
locationState.taxiCar.waypointReachRadius = taxi.waypointReachRadius;
|
||||
if (initializeLocation) authoritative.resetTaxi();
|
||||
const bool initializeLocation = authoritative.setup(
|
||||
LocationContentLoader::load(config, resourceZipFile_),
|
||||
resourceZipFile_);
|
||||
auto player = state_.players.find(localPlayerId_);
|
||||
if (player != state_.players.end() && player->second) {
|
||||
player->second->position = config.playerPosition;
|
||||
player->second->setTarget(
|
||||
config.playerPosition, std::string(), *authoritative.location,
|
||||
localPlayerId_);
|
||||
}
|
||||
publishMutation();
|
||||
return initializeLocation;
|
||||
}
|
||||
|
||||
void InternalGameServer::setupRuntimeLocation(
|
||||
const std::string& id, const std::shared_ptr<Location>& location,
|
||||
LocationSetup params, AudioPlayerAsync* audioPlayer)
|
||||
{
|
||||
auto stored = state_.locations.find(id);
|
||||
if (stored == state_.locations.end())
|
||||
throw std::runtime_error("Location state was not prepared");
|
||||
params.gameState = &stored->second;
|
||||
location->setup(params, audioPlayer);
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
Location* InternalGameServer::runtimeLocation(const std::string& id) const
|
||||
{
|
||||
auto it = runtimeLocations_.find(id);
|
||||
return it != runtimeLocations_.end() ? it->second.get() : nullptr;
|
||||
}
|
||||
|
||||
bool InternalGameServer::hasRuntimeLocation(const std::string& id) const
|
||||
{
|
||||
return runtimeLocations_.count(id) > 0;
|
||||
}
|
||||
|
||||
std::vector<std::string> InternalGameServer::loadedRuntimeLocationIds() const
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
result.reserve(runtimeLocations_.size());
|
||||
for (const auto& [id, location] : runtimeLocations_) result.push_back(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
void InternalGameServer::clearRuntimeLocations(bool retainState)
|
||||
{
|
||||
if (retainState) {
|
||||
synchronizeRuntimeAuxiliaryState();
|
||||
} else {
|
||||
for (const auto& [id, location] : runtimeLocations_)
|
||||
state_.locations.erase(id);
|
||||
publishMutation();
|
||||
}
|
||||
runtimeLocations_.clear();
|
||||
}
|
||||
|
||||
GameSnapshot InternalGameServer::buildSnapshot() const
|
||||
{
|
||||
GameSnapshot result;
|
||||
@@ -472,11 +456,8 @@ void InternalGameServer::submit(const GameCommand& command)
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end()) {
|
||||
location->second.triggerZones[value.zoneId].enabled = value.enabled;
|
||||
auto runtime = runtimeLocations_.find(value.locationId);
|
||||
if (runtime != runtimeLocations_.end() && runtime->second) {
|
||||
for (auto& zone : runtime->second->triggerZones)
|
||||
if (zone.id == value.zoneId) zone.enabled = value.enabled;
|
||||
}
|
||||
pendingTriggerZoneUpdates_.emplace_back(
|
||||
value.locationId, value.zoneId);
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, ReloadQuestDefinitionsCommand>) {
|
||||
state_.questJournal.reloadDefinitionsPreservingState(value.path, value.zipFile);
|
||||
@@ -494,26 +475,22 @@ std::vector<GameServerEvent> InternalGameServer::drainEvents()
|
||||
|
||||
void InternalGameServer::update(std::int64_t deltaMs)
|
||||
{
|
||||
auto runtime = runtimeLocations_.find(state_.currentLocationName);
|
||||
if (runtime == runtimeLocations_.end() || !runtime->second) return;
|
||||
pendingSimulationLocationId_.clear();
|
||||
pendingSimulationEvents_ = {};
|
||||
auto stored = state_.locations.find(state_.currentLocationName);
|
||||
if (stored == state_.locations.end()) return;
|
||||
updateBattleZoneTestBot(deltaMs);
|
||||
const bool interactiveObjectsLocked =
|
||||
stored->second.lockInteractiveObjectsDuringTutorial &&
|
||||
state_.tutorialStep < TutorialStep::PickUpItem;
|
||||
LocationSimulationEvents events =
|
||||
pendingSimulationLocationId_ = state_.currentLocationName;
|
||||
pendingSimulationEvents_ =
|
||||
stored->second.update(deltaMs, state_.players, interactiveObjectsLocked);
|
||||
// Runtime presentation reads through snapshots, so publish the simulation
|
||||
// tick before forwarding its value-only events to the loaded location.
|
||||
publishMutation();
|
||||
runtime->second->update(deltaMs, events);
|
||||
synchronizeRuntimeAuxiliaryState();
|
||||
}
|
||||
|
||||
void InternalGameServer::save(nlohmann::json& out) const
|
||||
{
|
||||
const_cast<InternalGameServer*>(this)->synchronizeRuntimeAuxiliaryState();
|
||||
state_.save(out);
|
||||
}
|
||||
|
||||
@@ -523,40 +500,9 @@ void InternalGameServer::load(const nlohmann::json& in)
|
||||
state_.load(in);
|
||||
battleZoneTestBotElapsedMs_ = 0;
|
||||
ensureBattleZoneTestBot();
|
||||
applyStateToRuntime();
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::bindRuntimeLocation(const std::string& id, std::shared_ptr<Location> location)
|
||||
{
|
||||
runtimeLocations_[id] = std::move(location);
|
||||
auto stored = state_.locations.find(id);
|
||||
if (stored != state_.locations.end() && stored->second.hasPersistentState) {
|
||||
runtimeLocations_[id]->applyAuxiliaryState(stored->second);
|
||||
} else {
|
||||
runtimeLocations_[id]->captureAuxiliaryState(state_.locations[id]);
|
||||
}
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::synchronizeRuntimeAuxiliaryState()
|
||||
{
|
||||
for (const auto& [id, location] : runtimeLocations_) {
|
||||
if (!location) continue;
|
||||
location->captureAuxiliaryState(state_.locations[id]);
|
||||
}
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::applyStateToRuntime()
|
||||
{
|
||||
for (const auto& [id, stored] : state_.locations) {
|
||||
auto runtime = runtimeLocations_.find(id);
|
||||
if (runtime == runtimeLocations_.end() || !runtime->second) continue;
|
||||
runtime->second->applyAuxiliaryState(stored);
|
||||
}
|
||||
}
|
||||
|
||||
void InternalGameServer::publishMutation()
|
||||
{
|
||||
++revision_;
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServer.h"
|
||||
|
||||
class AudioPlayerAsync;
|
||||
#include "IGameServerLifecycle.h"
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class Game;
|
||||
class Character;
|
||||
class Location;
|
||||
class LocalGameServerHost;
|
||||
struct LocationConfig;
|
||||
struct LocationSetup;
|
||||
|
||||
// In-process implementation used by single-player. GameState exists exactly
|
||||
// once, here; clients see immutable snapshots and mutate it with typed commands.
|
||||
class InternalGameServer final : public IGameServer {
|
||||
class InternalGameServer final
|
||||
: public IGameServer, public IGameServerLifecycle {
|
||||
public:
|
||||
InternalGameServer();
|
||||
|
||||
@@ -25,9 +22,14 @@ public:
|
||||
void update(std::int64_t deltaMs) override;
|
||||
void save(nlohmann::json& out) const override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
void startCampaign(
|
||||
const CampaignConfig& campaign, const std::string& zipFile,
|
||||
bool createLocalPlayer, bool enableTestBot) override;
|
||||
bool prepareLocation(const LocationConfig& config) override;
|
||||
void activateLocation(const std::string& locationId) override;
|
||||
|
||||
private:
|
||||
friend class Game;
|
||||
friend class LocalGameServerHost;
|
||||
|
||||
// Host-only lifecycle operations. They never expose authoritative state and
|
||||
// are intentionally absent from IGameServer.
|
||||
@@ -36,17 +38,6 @@ private:
|
||||
const std::string& zipFile);
|
||||
void clearCampaign();
|
||||
void resetCampaign(const std::string& questsPath, const std::string& zipFile);
|
||||
bool prepareLocation(const LocationConfig& config);
|
||||
void setupRuntimeLocation(
|
||||
const std::string& id, const std::shared_ptr<Location>& location,
|
||||
LocationSetup params, AudioPlayerAsync* audioPlayer);
|
||||
Location* runtimeLocation(const std::string& id) const;
|
||||
bool hasRuntimeLocation(const std::string& id) const;
|
||||
std::vector<std::string> loadedRuntimeLocationIds() const;
|
||||
void clearRuntimeLocations(bool retainState);
|
||||
void bindRuntimeLocation(const std::string& id, std::shared_ptr<Location> location);
|
||||
void synchronizeRuntimeAuxiliaryState();
|
||||
void applyStateToRuntime();
|
||||
void publishMutation();
|
||||
void ensureBattleZoneTestBot();
|
||||
void updateBattleZoneTestBot(std::int64_t deltaMs);
|
||||
@@ -55,8 +46,12 @@ private:
|
||||
|
||||
GameState state_;
|
||||
std::string localPlayerId_ = "player";
|
||||
std::string resourceZipFile_;
|
||||
std::int64_t battleZoneTestBotElapsedMs_ = 0;
|
||||
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_;
|
||||
bool battleZoneTestBotEnabled_ = false;
|
||||
std::string pendingSimulationLocationId_;
|
||||
LocationSimulationEvents pendingSimulationEvents_;
|
||||
std::vector<std::pair<std::string, std::string>> pendingTriggerZoneUpdates_;
|
||||
std::uint64_t revision_ = 1;
|
||||
mutable std::uint64_t cachedRevision_ = 0;
|
||||
mutable std::shared_ptr<const GameSnapshot> cachedSnapshot_;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#include "LocalGameServerHost.h"
|
||||
#include "InternalGameServer.h"
|
||||
#include "../Campaign.h"
|
||||
#include "../Location.h"
|
||||
#include <stdexcept>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
LocalGameServerHost::LocalGameServerHost(InternalGameServer& server)
|
||||
: server_(server)
|
||||
{
|
||||
}
|
||||
|
||||
void LocalGameServerHost::initializeCampaign(
|
||||
const std::string& campaignId, const std::string& questsPath,
|
||||
const std::string& zipFile)
|
||||
{
|
||||
server_.initializeCampaign(campaignId, questsPath, zipFile);
|
||||
}
|
||||
|
||||
void LocalGameServerHost::clearCampaign()
|
||||
{
|
||||
runtimeLocations_.clear();
|
||||
server_.clearCampaign();
|
||||
}
|
||||
|
||||
void LocalGameServerHost::resetCampaign(
|
||||
const std::string& questsPath, const std::string& zipFile)
|
||||
{
|
||||
server_.resetCampaign(questsPath, zipFile);
|
||||
}
|
||||
|
||||
bool LocalGameServerHost::prepareLocation(const LocationConfig& config)
|
||||
{
|
||||
return server_.prepareLocation(config);
|
||||
}
|
||||
|
||||
void LocalGameServerHost::setupRuntimeLocation(
|
||||
const std::string& id, const std::shared_ptr<Location>& location,
|
||||
LocationSetup params, AudioPlayerAsync* audioPlayer)
|
||||
{
|
||||
auto stored = server_.state_.locations.find(id);
|
||||
if (stored == server_.state_.locations.end())
|
||||
throw std::runtime_error("Location state was not prepared");
|
||||
params.definition = stored->second.definition;
|
||||
params.gameState = &stored->second;
|
||||
location->setup(params, audioPlayer);
|
||||
server_.publishMutation();
|
||||
}
|
||||
|
||||
Location* LocalGameServerHost::runtimeLocation(const std::string& id) const
|
||||
{
|
||||
auto it = runtimeLocations_.find(id);
|
||||
return it != runtimeLocations_.end() ? it->second.get() : nullptr;
|
||||
}
|
||||
|
||||
bool LocalGameServerHost::hasRuntimeLocation(const std::string& id) const
|
||||
{
|
||||
return runtimeLocations_.count(id) > 0;
|
||||
}
|
||||
|
||||
std::vector<std::string> LocalGameServerHost::loadedRuntimeLocationIds() const
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
result.reserve(runtimeLocations_.size());
|
||||
for (const auto& [id, location] : runtimeLocations_) result.push_back(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
void LocalGameServerHost::clearRuntimeLocations(bool retainState)
|
||||
{
|
||||
if (retainState) {
|
||||
synchronizeRuntimeAuxiliaryState();
|
||||
} else {
|
||||
for (const auto& [id, location] : runtimeLocations_)
|
||||
server_.state_.locations.erase(id);
|
||||
server_.publishMutation();
|
||||
}
|
||||
runtimeLocations_.clear();
|
||||
}
|
||||
|
||||
void LocalGameServerHost::bindRuntimeLocation(
|
||||
const std::string& id, std::shared_ptr<Location> location)
|
||||
{
|
||||
runtimeLocations_[id] = std::move(location);
|
||||
auto stored = server_.state_.locations.find(id);
|
||||
if (stored != server_.state_.locations.end() &&
|
||||
stored->second.hasPersistentState) {
|
||||
runtimeLocations_[id]->applyAuxiliaryState(stored->second);
|
||||
} else {
|
||||
runtimeLocations_[id]->captureAuxiliaryState(server_.state_.locations[id]);
|
||||
}
|
||||
server_.publishMutation();
|
||||
}
|
||||
|
||||
void LocalGameServerHost::update(std::int64_t deltaMs)
|
||||
{
|
||||
server_.update(deltaMs);
|
||||
|
||||
for (const auto& [locationId, zoneId] : server_.pendingTriggerZoneUpdates_) {
|
||||
auto runtime = runtimeLocations_.find(locationId);
|
||||
auto stored = server_.state_.locations.find(locationId);
|
||||
if (runtime == runtimeLocations_.end() || !runtime->second ||
|
||||
stored == server_.state_.locations.end()) {
|
||||
continue;
|
||||
}
|
||||
auto zoneState = stored->second.triggerZones.find(zoneId);
|
||||
if (zoneState == stored->second.triggerZones.end()) continue;
|
||||
for (auto& zone : runtime->second->triggerZones)
|
||||
if (zone.id == zoneId) zone.enabled = zoneState->second.enabled;
|
||||
}
|
||||
server_.pendingTriggerZoneUpdates_.clear();
|
||||
|
||||
if (server_.pendingSimulationLocationId_.empty()) return;
|
||||
auto runtime =
|
||||
runtimeLocations_.find(server_.pendingSimulationLocationId_);
|
||||
if (runtime == runtimeLocations_.end() || !runtime->second) return;
|
||||
runtime->second->update(deltaMs, server_.pendingSimulationEvents_);
|
||||
synchronizeRuntimeAuxiliaryState();
|
||||
}
|
||||
|
||||
void LocalGameServerHost::save(nlohmann::json& out)
|
||||
{
|
||||
synchronizeRuntimeAuxiliaryState();
|
||||
server_.save(out);
|
||||
}
|
||||
|
||||
void LocalGameServerHost::load(const nlohmann::json& in)
|
||||
{
|
||||
server_.load(in);
|
||||
applyStateToRuntime();
|
||||
}
|
||||
|
||||
void LocalGameServerHost::synchronizeRuntimeAuxiliaryState()
|
||||
{
|
||||
for (const auto& [id, location] : runtimeLocations_) {
|
||||
if (!location) continue;
|
||||
location->captureAuxiliaryState(server_.state_.locations[id]);
|
||||
}
|
||||
server_.publishMutation();
|
||||
}
|
||||
|
||||
void LocalGameServerHost::applyStateToRuntime()
|
||||
{
|
||||
for (const auto& [id, stored] : server_.state_.locations) {
|
||||
auto runtime = runtimeLocations_.find(id);
|
||||
if (runtime == runtimeLocations_.end() || !runtime->second) continue;
|
||||
runtime->second->applyAuxiliaryState(stored);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServerHost.h"
|
||||
#include <unordered_map>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class InternalGameServer;
|
||||
|
||||
// Adapts the headless authoritative server to the graphical in-process client.
|
||||
class LocalGameServerHost final : public IGameServerHost {
|
||||
public:
|
||||
explicit LocalGameServerHost(InternalGameServer& server);
|
||||
|
||||
void initializeCampaign(
|
||||
const std::string& campaignId, const std::string& questsPath,
|
||||
const std::string& zipFile) override;
|
||||
void clearCampaign() override;
|
||||
void resetCampaign(
|
||||
const std::string& questsPath, const std::string& zipFile) override;
|
||||
bool prepareLocation(const LocationConfig& config) override;
|
||||
|
||||
void setupRuntimeLocation(
|
||||
const std::string& id, const std::shared_ptr<Location>& location,
|
||||
LocationSetup params, AudioPlayerAsync* audioPlayer) override;
|
||||
Location* runtimeLocation(const std::string& id) const override;
|
||||
bool hasRuntimeLocation(const std::string& id) const override;
|
||||
std::vector<std::string> loadedRuntimeLocationIds() const override;
|
||||
void clearRuntimeLocations(bool retainState) override;
|
||||
void bindRuntimeLocation(
|
||||
const std::string& id, std::shared_ptr<Location> location) override;
|
||||
|
||||
void update(std::int64_t deltaMs) override;
|
||||
void save(nlohmann::json& out) override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
|
||||
private:
|
||||
void synchronizeRuntimeAuxiliaryState();
|
||||
void applyStateToRuntime();
|
||||
|
||||
InternalGameServer& server_;
|
||||
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
+68
-158
@@ -102,20 +102,21 @@ namespace FRG {
|
||||
data.castShadowNight = item.value("castShadowNight", true);
|
||||
data.isActive = item.value("isActive", true);
|
||||
data.activateFunctionName = item.value("activateFunction", "");
|
||||
data.pivotX = item.value("pivotX", 0.0f);
|
||||
data.pivotY = item.value("pivotY", 0.0f);
|
||||
data.pivotZ = item.value("pivotZ", 0.0f);
|
||||
data.boundsMinX = item.value("boundsMinX", 0.0f);
|
||||
data.boundsMinY = item.value("boundsMinY", 0.0f);
|
||||
data.boundsMinZ = item.value("boundsMinZ", 0.0f);
|
||||
data.boundsMaxX = item.value("boundsMaxX", 0.0f);
|
||||
data.boundsMaxY = item.value("boundsMaxY", 0.0f);
|
||||
data.boundsMaxZ = item.value("boundsMaxZ", 0.0f);
|
||||
data.pivot = {
|
||||
item.value("pivotX", 0.0f), item.value("pivotY", 0.0f),
|
||||
item.value("pivotZ", 0.0f)};
|
||||
data.boundsMin = {
|
||||
item.value("boundsMinX", 0.0f), item.value("boundsMinY", 0.0f),
|
||||
item.value("boundsMinZ", 0.0f)};
|
||||
data.boundsMax = {
|
||||
item.value("boundsMaxX", 0.0f), item.value("boundsMaxY", 0.0f),
|
||||
item.value("boundsMaxZ", 0.0f)};
|
||||
if (item.contains("interactionPositionX")) {
|
||||
data.hasInteractionPosition = true;
|
||||
data.interactionPositionX = item.value("interactionPositionX", 0.0f);
|
||||
data.interactionPositionY = item.value("interactionPositionY", 0.0f);
|
||||
data.interactionPositionZ = item.value("interactionPositionZ", 0.0f);
|
||||
data.interactionPosition = {
|
||||
item.value("interactionPositionX", 0.0f),
|
||||
item.value("interactionPositionY", 0.0f),
|
||||
item.value("interactionPositionZ", 0.0f)};
|
||||
}
|
||||
if (!data.base.meshPath.empty())
|
||||
objects.push_back(std::move(data));
|
||||
@@ -125,40 +126,6 @@ namespace FRG {
|
||||
return objects;
|
||||
}
|
||||
|
||||
LoadedGameObjectState GameObjectData::toLoadedGameObjectState() const {
|
||||
LoadedGameObjectState st;
|
||||
st.texturePath = texturePath;
|
||||
st.textureDarkandsPath = textureDarkandsPath;
|
||||
st.meshPath = meshPath;
|
||||
st.meshRotationX = rotationX;
|
||||
st.meshRotationY = rotationY;
|
||||
st.meshRotationZ = rotationZ;
|
||||
st.meshScale = scale;
|
||||
return st;
|
||||
}
|
||||
|
||||
InteractiveObjectState InteractiveObjectData::toInteractiveObjectState() const {
|
||||
InteractiveObjectState st;
|
||||
st.objectInfo = base.toLoadedGameObjectState();
|
||||
st.interactionRadius = interactionRadius;
|
||||
st.approachRadius = approachRadius;
|
||||
st.castShadow = castShadow;
|
||||
st.castShadowNight = castShadowNight;
|
||||
st.isActive = isActive;
|
||||
st.activateFunctionName = activateFunctionName;
|
||||
st.pivot = Eigen::Vector3f(pivotX, pivotY, pivotZ);
|
||||
st.boundsMin = Eigen::Vector3f(boundsMinX, boundsMinY, boundsMinZ);
|
||||
st.boundsMax = Eigen::Vector3f(boundsMaxX, boundsMaxY, boundsMaxZ);
|
||||
st.hasInteractionPosition = hasInteractionPosition;
|
||||
if (hasInteractionPosition)
|
||||
st.interactionPosition = Eigen::Vector3f(interactionPositionX, interactionPositionY, interactionPositionZ);
|
||||
st.jsonPositionX = base.positionX;
|
||||
st.jsonPositionY = base.positionY;
|
||||
st.jsonPositionZ = base.positionZ;
|
||||
// state.position is set by InteractiveObject::createFromState() after mesh-center computation
|
||||
return st;
|
||||
}
|
||||
|
||||
static VertexDataStruct loadMeshData(const std::string& meshPath, const std::string& zipPath)
|
||||
{
|
||||
if (meshPath.size() > 4 && meshPath.compare(meshPath.size() - 4, 4, ".bin") == 0)
|
||||
@@ -213,20 +180,18 @@ namespace FRG {
|
||||
// Public API
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
StaticObjectSet GameObjectLoader::loadAndCreateGameObjects(
|
||||
const std::string& jsonPath,
|
||||
StaticObjectSet GameObjectLoader::createGameObjects(
|
||||
const std::vector<GameObjectData>& definitions,
|
||||
Renderer& renderer,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
StaticObjectSet result;
|
||||
|
||||
std::vector<GameObjectData> allData = loadFromJson(jsonPath, zipPath);
|
||||
|
||||
// Group objects sharing mesh + textures (in file order) so they can be drawn instanced.
|
||||
std::vector<std::vector<const GameObjectData*>> groups;
|
||||
std::unordered_map<std::string, size_t> groupIndexByKey;
|
||||
|
||||
for (const auto& data : allData) {
|
||||
for (const auto& data : definitions) {
|
||||
if (!data.instanced) {
|
||||
groups.push_back({ &data });
|
||||
continue;
|
||||
@@ -294,45 +259,32 @@ namespace FRG {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<InteractiveObject> GameObjectLoader::loadAndCreateInteractiveObjects(
|
||||
const std::string& jsonPath,
|
||||
StaticObjectSet GameObjectLoader::loadAndCreateGameObjects(
|
||||
const std::string& jsonPath, Renderer& renderer,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
return createGameObjects(
|
||||
loadFromJson(jsonPath, zipPath), renderer, zipPath);
|
||||
}
|
||||
|
||||
std::vector<InteractiveObject> GameObjectLoader::createInteractiveObjects(
|
||||
const std::vector<InteractiveObjectData>& definitions,
|
||||
Renderer& renderer,
|
||||
std::unordered_map<std::string, std::shared_ptr<InteractiveObjectState>>& states,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
std::vector<InteractiveObject> interactiveObjects;
|
||||
|
||||
if (jsonPath.empty())
|
||||
return interactiveObjects;
|
||||
|
||||
const auto definitions = loadInteractiveFromJson(jsonPath, zipPath);
|
||||
std::unordered_set<std::string> configuredIds;
|
||||
for (const auto& data : definitions) {
|
||||
if (!configuredIds.insert(data.id).second)
|
||||
throw std::runtime_error(
|
||||
"GameObjectLoader: duplicate interactive object id '" +
|
||||
data.id + "' in " + jsonPath);
|
||||
}
|
||||
for (const auto& [id, state] : states)
|
||||
if (configuredIds.count(id) == 0)
|
||||
throw std::runtime_error(
|
||||
"GameObjectLoader: saved interactive object id '" + id +
|
||||
"' is not present in " + jsonPath);
|
||||
for (const auto& data : definitions) {
|
||||
SDL_PumpEvents();
|
||||
logger() << "Loading interactive object: " << data.base.name << std::endl;
|
||||
try {
|
||||
InteractiveObjectState definition = data.toInteractiveObjectState();
|
||||
auto& state = states[data.id];
|
||||
if (!state) state = std::make_shared<InteractiveObjectState>(definition);
|
||||
else {
|
||||
nlohmann::json runtime;
|
||||
state->save(runtime);
|
||||
*state = std::move(definition);
|
||||
state->load(runtime);
|
||||
}
|
||||
auto state = states.find(data.id);
|
||||
if (state == states.end() || !state->second)
|
||||
throw std::runtime_error(
|
||||
"Authoritative state missing for '" + data.id + "'");
|
||||
InteractiveObject intObj =
|
||||
InteractiveObject::createFromState(*state, renderer, zipPath);
|
||||
InteractiveObject::createFromState(
|
||||
*state->second, renderer, zipPath);
|
||||
intObj.id = data.id;
|
||||
intObj.loadedObject.name = data.base.name;
|
||||
|
||||
@@ -352,6 +304,18 @@ namespace FRG {
|
||||
return interactiveObjects;
|
||||
}
|
||||
|
||||
std::vector<InteractiveObject>
|
||||
GameObjectLoader::loadAndCreateInteractiveObjects(
|
||||
const std::string& jsonPath, Renderer& renderer,
|
||||
std::unordered_map<std::string,
|
||||
std::shared_ptr<InteractiveObjectState>>& states,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
return createInteractiveObjects(
|
||||
loadInteractiveFromJson(jsonPath, zipPath), renderer, states,
|
||||
zipPath);
|
||||
}
|
||||
|
||||
std::vector<NpcData> GameObjectLoader::loadNpcsFromJson(const std::string& jsonPath, const std::string& zipPath)
|
||||
{
|
||||
std::vector<NpcData> npcs;
|
||||
@@ -412,96 +376,29 @@ namespace FRG {
|
||||
return npcs;
|
||||
}
|
||||
|
||||
CharacterState NpcData::toCharacterState() const {
|
||||
CharacterState st;
|
||||
|
||||
// Identity + game state
|
||||
st.npcName = name;
|
||||
st.position = Eigen::Vector3f(positionX, positionY, positionZ);
|
||||
st.homePosition = Eigen::Vector3f(positionX, 0.f, positionZ);
|
||||
st.stopInPlace();
|
||||
st.facingAngle = facingAngle;
|
||||
st.walkSpeed = walkSpeed;
|
||||
st.rotationSpeed = rotationSpeed;
|
||||
st.modelScale = modelScale;
|
||||
st.setHp(hp);
|
||||
st.initialHp = hp;
|
||||
st.visibleRange = visibleRange;
|
||||
st.canAttack = canAttack;
|
||||
if (canAttack) st.attackTargetId = "player";
|
||||
st.enabled = enabled;
|
||||
st.interactionRadius = interactionRadius;
|
||||
|
||||
Eigen::Quaternionf corrRot = Eigen::Quaternionf::Identity();
|
||||
if (modelCorrectionRotX != 0.0f)
|
||||
corrRot = Eigen::Quaternionf(Eigen::AngleAxisf(modelCorrectionRotX, Eigen::Vector3f::UnitX())) * corrRot;
|
||||
if (modelCorrectionRotY != 0.0f)
|
||||
corrRot = Eigen::Quaternionf(Eigen::AngleAxisf(modelCorrectionRotY, Eigen::Vector3f::UnitY())) * corrRot;
|
||||
if (modelCorrectionRotZ != 0.0f)
|
||||
corrRot = Eigen::Quaternionf(Eigen::AngleAxisf(modelCorrectionRotZ, Eigen::Vector3f::UnitZ())) * corrRot;
|
||||
st.modelCorrectionRotation = corrRot;
|
||||
|
||||
// Creation info: animation paths
|
||||
CharacterCreationInfo& info = st.creationInfo;
|
||||
auto addAnim = [&](AnimationState animState, const std::string& path) {
|
||||
if (!path.empty())
|
||||
info.animationPaths.emplace_back(animState, path);
|
||||
};
|
||||
addAnim(AnimationState::STAND, animationIdlePath);
|
||||
addAnim(AnimationState::WALK, animationWalkPath);
|
||||
addAnim(AnimationState::ACTION_IDLE, animationActionIdlePath);
|
||||
addAnim(AnimationState::ACTION_ATTACK, animationActionAttackPath);
|
||||
addAnim(AnimationState::STAND_TO_ACTION, animationStandToActionPath);
|
||||
addAnim(AnimationState::ACTION_TO_STAND, animationActionToStandPath);
|
||||
addAnim(AnimationState::ACTION_TO_DEATH, animationActionToDeathPath);
|
||||
addAnim(AnimationState::DEATH_IDLE, animationDeathIdlePath);
|
||||
|
||||
// Creation info: textures
|
||||
info.meshTexturePaths = std::unordered_map<std::string, std::string>(meshTextures.begin(), meshTextures.end());
|
||||
info.singleTexturePath = texturePath;
|
||||
|
||||
return st;
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<Character>> GameObjectLoader::loadAndCreate_Npcs(
|
||||
const std::string& jsonPath,
|
||||
std::vector<std::unique_ptr<Character>> GameObjectLoader::createNpcs(
|
||||
const std::vector<NpcData>& definitions,
|
||||
Renderer& renderer,
|
||||
std::unordered_map<std::string, std::shared_ptr<CharacterState>>& states,
|
||||
const std::unordered_map<std::string,
|
||||
std::shared_ptr<CharacterState>>& states,
|
||||
IGameServer& server,
|
||||
const std::string& locationId,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
std::vector<std::unique_ptr<Character>> npcs;
|
||||
|
||||
const auto definitions = loadNpcsFromJson(jsonPath, zipPath);
|
||||
std::unordered_set<std::string> seenIds;
|
||||
for (const auto& definition : definitions) {
|
||||
const std::string& id = definition.id;
|
||||
if (id == "player" || !seenIds.insert(id).second)
|
||||
throw std::runtime_error(
|
||||
"GameObjectLoader: duplicate or reserved NPC id '" + id +
|
||||
"' in " + jsonPath);
|
||||
}
|
||||
for (const auto& [id, state] : states)
|
||||
if (seenIds.count(id) == 0)
|
||||
throw std::runtime_error(
|
||||
"GameObjectLoader: saved NPC id '" + id +
|
||||
"' is not present in " + jsonPath);
|
||||
for (const auto& npcData : definitions) {
|
||||
SDL_PumpEvents();
|
||||
logger() << "Loading NPC: " << npcData.name << std::endl;
|
||||
try {
|
||||
CharacterState definition = npcData.toCharacterState();
|
||||
auto& state = states[npcData.id];
|
||||
if (!state) state = std::make_shared<CharacterState>(definition);
|
||||
else {
|
||||
nlohmann::json runtime;
|
||||
state->save(runtime);
|
||||
*state = std::move(definition);
|
||||
state->load(runtime);
|
||||
}
|
||||
auto state = states.find(npcData.id);
|
||||
if (state == states.end() || !state->second)
|
||||
throw std::runtime_error(
|
||||
"Authoritative state missing for '" +
|
||||
npcData.id + "'");
|
||||
auto npc = Character::createFromState(
|
||||
*state, server, locationId, npcData.id, renderer, zipPath.c_str());
|
||||
*state->second, server, locationId, npcData.id,
|
||||
renderer, zipPath.c_str());
|
||||
if (npc) {
|
||||
logger() << "Successfully loaded NPC: " << npcData.name << " at ("
|
||||
<< npcData.positionX << ", " << npcData.positionY << ", " << npcData.positionZ << ")" << std::endl;
|
||||
@@ -515,4 +412,17 @@ namespace FRG {
|
||||
return npcs;
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<Character>>
|
||||
GameObjectLoader::loadAndCreate_Npcs(
|
||||
const std::string& jsonPath, Renderer& renderer,
|
||||
std::unordered_map<std::string,
|
||||
std::shared_ptr<CharacterState>>& states,
|
||||
IGameServer& server, const std::string& locationId,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
return createNpcs(
|
||||
loadNpcsFromJson(jsonPath, zipPath), renderer, states, server,
|
||||
locationId, zipPath);
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -9,31 +9,13 @@
|
||||
#include "InteractiveObject.h"
|
||||
#include "InteractiveObjectState.h"
|
||||
#include "../CharacterState.h"
|
||||
#include "../content/LocationDefinition.h"
|
||||
|
||||
namespace FRG {
|
||||
class IGameServer;
|
||||
|
||||
class Character;
|
||||
|
||||
struct GameObjectData {
|
||||
std::string name;
|
||||
std::string texturePath;
|
||||
std::string textureDarkandsPath;
|
||||
std::string meshPath;
|
||||
float rotationX = 0.0f;
|
||||
float rotationY = 0.0f;
|
||||
float rotationZ = 0.0f;
|
||||
float positionX = 0.0f;
|
||||
float positionY = 0.0f;
|
||||
float positionZ = 0.0f;
|
||||
float scale = 1.0f;
|
||||
// Objects sharing mesh and textures are drawn with one instanced call.
|
||||
// Set "instanced": false in the JSON to keep an object as a standalone mesh.
|
||||
bool instanced = true;
|
||||
|
||||
LoadedGameObjectState toLoadedGameObjectState() const;
|
||||
};
|
||||
|
||||
// Result of loading a static object list: unique objects keep their own mesh,
|
||||
// repeated ones are merged into instanced batches.
|
||||
struct StaticObjectSet {
|
||||
@@ -41,63 +23,25 @@ namespace FRG {
|
||||
std::vector<InstancedBatch> batches;
|
||||
};
|
||||
|
||||
struct InteractiveObjectData {
|
||||
std::string id;
|
||||
GameObjectData base;
|
||||
float interactionRadius = 2.0f;
|
||||
float approachRadius = 2.0f;
|
||||
std::string activateFunctionName;
|
||||
float pivotX = 0.0f;
|
||||
float pivotY = 0.0f;
|
||||
float pivotZ = 0.0f;
|
||||
float boundsMinX = 0.0f, boundsMinY = 0.0f, boundsMinZ = 0.0f;
|
||||
float boundsMaxX = 0.0f, boundsMaxY = 0.0f, boundsMaxZ = 0.0f;
|
||||
bool hasInteractionPosition = false;
|
||||
float interactionPositionX = 0.0f, interactionPositionY = 0.0f, interactionPositionZ = 0.0f;
|
||||
bool castShadow = true;
|
||||
bool castShadowNight = true;
|
||||
bool isActive = true;
|
||||
|
||||
InteractiveObjectState toInteractiveObjectState() const;
|
||||
};
|
||||
|
||||
struct NpcData {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string texturePath;
|
||||
std::map<std::string, std::string> meshTextures;
|
||||
std::string animationIdlePath;
|
||||
std::string animationWalkPath;
|
||||
// Optional combat / death animations. Empty path = slot not loaded.
|
||||
std::string animationActionIdlePath;
|
||||
std::string animationActionAttackPath;
|
||||
std::string animationStandToActionPath;
|
||||
std::string animationActionToStandPath;
|
||||
std::string animationActionToDeathPath;
|
||||
std::string animationDeathIdlePath;
|
||||
float positionX = 0.0f;
|
||||
float positionY = 0.0f;
|
||||
float positionZ = 0.0f;
|
||||
float walkSpeed = 1.5f;
|
||||
float rotationSpeed = 8.0f;
|
||||
float modelScale = 0.01f;
|
||||
float modelCorrectionRotX = 0.0f;
|
||||
float modelCorrectionRotY = 0.0f;
|
||||
float modelCorrectionRotZ = 0.0f;
|
||||
float facingAngle = 0.0f;
|
||||
float hp = 100.0f;
|
||||
float visibleRange = VISIBLE_RANGE;
|
||||
bool canAttack = false;
|
||||
bool enabled = true;
|
||||
float interactionRadius = 0.0f;
|
||||
|
||||
// Converts this JSON-parsed descriptor into a fully-populated CharacterState
|
||||
// (including creationInfo) ready for Character::createFromState().
|
||||
CharacterState toCharacterState() const;
|
||||
};
|
||||
|
||||
class GameObjectLoader {
|
||||
public:
|
||||
static StaticObjectSet createGameObjects(
|
||||
const std::vector<GameObjectData>& definitions,
|
||||
Renderer& renderer, const std::string& zipPath = "");
|
||||
static std::vector<InteractiveObject> createInteractiveObjects(
|
||||
const std::vector<InteractiveObjectData>& definitions,
|
||||
Renderer& renderer,
|
||||
std::unordered_map<std::string,
|
||||
std::shared_ptr<InteractiveObjectState>>& states,
|
||||
const std::string& zipPath = "");
|
||||
static std::vector<std::unique_ptr<FRG::Character>> createNpcs(
|
||||
const std::vector<NpcData>& definitions,
|
||||
Renderer& renderer,
|
||||
const std::unordered_map<std::string,
|
||||
std::shared_ptr<CharacterState>>& states,
|
||||
IGameServer& server, const std::string& locationId,
|
||||
const std::string& zipPath);
|
||||
|
||||
static StaticObjectSet loadAndCreateGameObjects(
|
||||
const std::string& jsonPath,
|
||||
Renderer& renderer,
|
||||
|
||||
@@ -51,7 +51,8 @@ namespace FRG {
|
||||
|
||||
obj.loadedObject.mesh.RefreshVBO();
|
||||
|
||||
// Center mesh at origin; adjust world position by the centering offset.
|
||||
// Interactive-object definitions store the centered world position.
|
||||
// Center the presentation mesh at the origin without changing state.
|
||||
if (!obj.loadedObject.mesh.data.PositionData.empty()) {
|
||||
Eigen::Vector3f meshMin = obj.loadedObject.mesh.data.PositionData[0];
|
||||
Eigen::Vector3f meshMax = obj.loadedObject.mesh.data.PositionData[0];
|
||||
@@ -62,129 +63,11 @@ namespace FRG {
|
||||
const Eigen::Vector3f meshCenter = (meshMin + meshMax) * 0.5f;
|
||||
obj.loadedObject.mesh.data.Move(-meshCenter);
|
||||
obj.loadedObject.mesh.RefreshVBO();
|
||||
obj.state.position = Eigen::Vector3f(obj.state.jsonPositionX,
|
||||
obj.state.jsonPositionY,
|
||||
obj.state.jsonPositionZ) + meshCenter;
|
||||
} else {
|
||||
obj.state.position = Eigen::Vector3f(obj.state.jsonPositionX,
|
||||
obj.state.jsonPositionY,
|
||||
obj.state.jsonPositionZ);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
void InteractiveObjectState::moveTo(
|
||||
const Eigen::Vector3f& target, float durationSec, std::string onCompleteName) {
|
||||
if (isAnimating) return;
|
||||
AnimTask task;
|
||||
task.type = AnimTask::Type::Move;
|
||||
task.startPos = position;
|
||||
task.startRotY = rotationY;
|
||||
task.startScale = scale;
|
||||
task.targetPos = target;
|
||||
task.targetRotY = rotationY;
|
||||
task.targetScale = scale;
|
||||
task.durationMs = durationSec * 1000.f;
|
||||
task.elapsedMs = 0.f;
|
||||
task.onCompleteName = std::move(onCompleteName);
|
||||
animTask = std::move(task);
|
||||
isAnimating = true;
|
||||
}
|
||||
|
||||
void InteractiveObjectState::rotateTo(
|
||||
float targetRotY, float durationSec, std::string onCompleteName) {
|
||||
if (isAnimating) return;
|
||||
AnimTask task;
|
||||
task.type = AnimTask::Type::Rotate;
|
||||
task.startPos = position;
|
||||
task.startRotY = rotationY;
|
||||
task.startScale = scale;
|
||||
task.targetPos = position;
|
||||
task.targetRotY = targetRotY;
|
||||
task.targetScale = scale;
|
||||
task.durationMs = durationSec * 1000.f;
|
||||
task.elapsedMs = 0.f;
|
||||
task.onCompleteName = std::move(onCompleteName);
|
||||
animTask = std::move(task);
|
||||
isAnimating = true;
|
||||
}
|
||||
|
||||
void InteractiveObjectState::scaleTo(
|
||||
float targetScale, float durationSec, std::string onCompleteName) {
|
||||
if (isAnimating) return;
|
||||
AnimTask task;
|
||||
task.type = AnimTask::Type::Scale;
|
||||
task.startPos = position;
|
||||
task.startRotY = rotationY;
|
||||
task.startScale = scale;
|
||||
task.targetPos = position;
|
||||
task.targetRotY = rotationY;
|
||||
task.targetScale = targetScale;
|
||||
task.durationMs = durationSec * 1000.f;
|
||||
task.elapsedMs = 0.f;
|
||||
task.onCompleteName = std::move(onCompleteName);
|
||||
animTask = std::move(task);
|
||||
isAnimating = true;
|
||||
}
|
||||
|
||||
void InteractiveObjectState::fadeTo(
|
||||
float targetAlpha, float durationSec, std::string onCompleteName) {
|
||||
if (isAnimating) return;
|
||||
AnimTask task;
|
||||
task.type = AnimTask::Type::Fade;
|
||||
task.startPos = position;
|
||||
task.startRotY = rotationY;
|
||||
task.startScale = alpha; // reuse startScale to hold start alpha
|
||||
task.targetPos = position;
|
||||
task.targetRotY = rotationY;
|
||||
task.targetScale = targetAlpha; // reuse targetScale to hold target alpha
|
||||
task.durationMs = durationSec * 1000.f;
|
||||
task.elapsedMs = 0.f;
|
||||
task.onCompleteName = std::move(onCompleteName);
|
||||
animTask = std::move(task);
|
||||
isAnimating = true;
|
||||
}
|
||||
|
||||
InteractiveObjectUpdateEvents InteractiveObjectState::update(std::int64_t deltaMs) {
|
||||
InteractiveObjectUpdateEvents events;
|
||||
if (!isAnimating || !animTask) return events;
|
||||
|
||||
AnimTask& task = *animTask;
|
||||
task.elapsedMs += static_cast<float>(deltaMs);
|
||||
|
||||
const float t = (std::min)(task.elapsedMs / task.durationMs, 1.0f);
|
||||
|
||||
switch (task.type) {
|
||||
case AnimTask::Type::Move:
|
||||
position = task.startPos + (task.targetPos - task.startPos) * t;
|
||||
break;
|
||||
case AnimTask::Type::Rotate:
|
||||
rotationY = task.startRotY + (task.targetRotY - task.startRotY) * t;
|
||||
break;
|
||||
case AnimTask::Type::Scale:
|
||||
scale = task.startScale + (task.targetScale - task.startScale) * t;
|
||||
break;
|
||||
case AnimTask::Type::Fade:
|
||||
alpha = task.startScale + (task.targetScale - task.startScale) * t;
|
||||
break;
|
||||
}
|
||||
|
||||
if (t >= 1.0f) {
|
||||
position = task.targetPos;
|
||||
rotationY = task.targetRotY;
|
||||
if (task.type == AnimTask::Type::Fade)
|
||||
alpha = task.targetScale;
|
||||
else
|
||||
scale = task.targetScale;
|
||||
|
||||
events.onCompleteName = std::move(task.onCompleteName);
|
||||
animTask.reset();
|
||||
isAnimating = false;
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
void InteractiveObject::drawDarklands(Renderer& renderer) const {
|
||||
if (!state.isActive || !loadedObject.textureDarklands) return;
|
||||
|
||||
@@ -237,65 +120,4 @@ namespace FRG {
|
||||
renderer.RenderUniform1f("uAlpha", 1.0f);
|
||||
}
|
||||
|
||||
void InteractiveObjectState::save(nlohmann::json& out) const
|
||||
{
|
||||
out["positionX"] = position.x();
|
||||
out["positionY"] = position.y();
|
||||
out["positionZ"] = position.z();
|
||||
out["rotationY"] = rotationY;
|
||||
out["scale"] = scale;
|
||||
out["alpha"] = alpha;
|
||||
out["isActive"] = isActive;
|
||||
out["isAnimating"] = isAnimating;
|
||||
|
||||
if (isAnimating && animTask.has_value()) {
|
||||
nlohmann::json at;
|
||||
at["type"] = static_cast<int>(animTask->type);
|
||||
at["startPosX"] = animTask->startPos.x();
|
||||
at["startPosY"] = animTask->startPos.y();
|
||||
at["startPosZ"] = animTask->startPos.z();
|
||||
at["startRotY"] = animTask->startRotY;
|
||||
at["startScale"] = animTask->startScale;
|
||||
at["targetPosX"] = animTask->targetPos.x();
|
||||
at["targetPosY"] = animTask->targetPos.y();
|
||||
at["targetPosZ"] = animTask->targetPos.z();
|
||||
at["targetRotY"] = animTask->targetRotY;
|
||||
at["targetScale"] = animTask->targetScale;
|
||||
at["durationMs"] = animTask->durationMs;
|
||||
at["elapsedMs"] = animTask->elapsedMs;
|
||||
if (!animTask->onCompleteName.empty())
|
||||
at["onCompleteName"] = animTask->onCompleteName;
|
||||
out["animTask"] = std::move(at);
|
||||
}
|
||||
}
|
||||
|
||||
void InteractiveObjectState::load(const nlohmann::json& in)
|
||||
{
|
||||
position.x() = in.value("positionX", position.x());
|
||||
position.y() = in.value("positionY", position.y());
|
||||
position.z() = in.value("positionZ", position.z());
|
||||
rotationY = in.value("rotationY", rotationY);
|
||||
scale = in.value("scale", scale);
|
||||
alpha = in.value("alpha", alpha);
|
||||
isActive = in.value("isActive", isActive);
|
||||
isAnimating = in.value("isAnimating", false);
|
||||
animTask.reset();
|
||||
|
||||
if (isAnimating && in.contains("animTask") && in["animTask"].is_object()) {
|
||||
const auto& at = in["animTask"];
|
||||
AnimTask task;
|
||||
task.type = static_cast<AnimTask::Type>(at.value("type", 0));
|
||||
task.startPos = { at.value("startPosX", 0.f), at.value("startPosY", 0.f), at.value("startPosZ", 0.f) };
|
||||
task.startRotY = at.value("startRotY", 0.f);
|
||||
task.startScale = at.value("startScale", 1.f);
|
||||
task.targetPos = { at.value("targetPosX", 0.f), at.value("targetPosY", 0.f), at.value("targetPosZ", 0.f) };
|
||||
task.targetRotY = at.value("targetRotY", 0.f);
|
||||
task.targetScale = at.value("targetScale", 1.f);
|
||||
task.durationMs = at.value("durationMs", 1000.f);
|
||||
task.elapsedMs = at.value("elapsedMs", 0.f);
|
||||
task.onCompleteName = at.value("onCompleteName", std::string());
|
||||
animTask = std::move(task);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -23,8 +23,7 @@ namespace FRG {
|
||||
LoadedGameObject loadedObject; // rendering assets
|
||||
|
||||
// Factory: creates a fully-loaded InteractiveObject from a state descriptor.
|
||||
// Loads textures and mesh from state.objectInfo, computes mesh centering,
|
||||
// and sets state.position = jsonPosition + meshCenter.
|
||||
// Loads textures and centers the mesh around the state position.
|
||||
static InteractiveObject createFromState(InteractiveObjectState& state,
|
||||
Renderer& renderer,
|
||||
const std::string& zipPath = "");
|
||||
@@ -38,7 +37,6 @@ namespace FRG {
|
||||
friend class Location;
|
||||
friend class LocationEditor;
|
||||
friend class ScriptEngine;
|
||||
friend class InternalGameServer;
|
||||
InteractiveObjectState& state; // non-owning binding to LocationGameState
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
#include "InteractiveObjectState.h"
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
void InteractiveObjectState::moveTo(
|
||||
const Eigen::Vector3f& target, float durationSec, std::string onCompleteName)
|
||||
{
|
||||
if (isAnimating) return;
|
||||
AnimTask task;
|
||||
task.type = AnimTask::Type::Move;
|
||||
task.startPos = position;
|
||||
task.startRotY = rotationY;
|
||||
task.startScale = scale;
|
||||
task.targetPos = target;
|
||||
task.targetRotY = rotationY;
|
||||
task.targetScale = scale;
|
||||
task.durationMs = durationSec * 1000.f;
|
||||
task.onCompleteName = std::move(onCompleteName);
|
||||
animTask = std::move(task);
|
||||
isAnimating = true;
|
||||
}
|
||||
|
||||
void InteractiveObjectState::rotateTo(
|
||||
float targetRotY, float durationSec, std::string onCompleteName)
|
||||
{
|
||||
if (isAnimating) return;
|
||||
AnimTask task;
|
||||
task.type = AnimTask::Type::Rotate;
|
||||
task.startPos = position;
|
||||
task.startRotY = rotationY;
|
||||
task.startScale = scale;
|
||||
task.targetPos = position;
|
||||
task.targetRotY = targetRotY;
|
||||
task.targetScale = scale;
|
||||
task.durationMs = durationSec * 1000.f;
|
||||
task.onCompleteName = std::move(onCompleteName);
|
||||
animTask = std::move(task);
|
||||
isAnimating = true;
|
||||
}
|
||||
|
||||
void InteractiveObjectState::scaleTo(
|
||||
float targetScale, float durationSec, std::string onCompleteName)
|
||||
{
|
||||
if (isAnimating) return;
|
||||
AnimTask task;
|
||||
task.type = AnimTask::Type::Scale;
|
||||
task.startPos = position;
|
||||
task.startRotY = rotationY;
|
||||
task.startScale = scale;
|
||||
task.targetPos = position;
|
||||
task.targetRotY = rotationY;
|
||||
task.targetScale = targetScale;
|
||||
task.durationMs = durationSec * 1000.f;
|
||||
task.onCompleteName = std::move(onCompleteName);
|
||||
animTask = std::move(task);
|
||||
isAnimating = true;
|
||||
}
|
||||
|
||||
void InteractiveObjectState::fadeTo(
|
||||
float targetAlpha, float durationSec, std::string onCompleteName)
|
||||
{
|
||||
if (isAnimating) return;
|
||||
AnimTask task;
|
||||
task.type = AnimTask::Type::Fade;
|
||||
task.startPos = position;
|
||||
task.startRotY = rotationY;
|
||||
task.startScale = alpha;
|
||||
task.targetPos = position;
|
||||
task.targetRotY = rotationY;
|
||||
task.targetScale = targetAlpha;
|
||||
task.durationMs = durationSec * 1000.f;
|
||||
task.onCompleteName = std::move(onCompleteName);
|
||||
animTask = std::move(task);
|
||||
isAnimating = true;
|
||||
}
|
||||
|
||||
InteractiveObjectUpdateEvents InteractiveObjectState::update(std::int64_t deltaMs)
|
||||
{
|
||||
InteractiveObjectUpdateEvents events;
|
||||
if (!isAnimating || !animTask) return events;
|
||||
|
||||
AnimTask& task = *animTask;
|
||||
task.elapsedMs += static_cast<float>(deltaMs);
|
||||
const float duration = (std::max)(task.durationMs, 0.001f);
|
||||
const float t = (std::min)(task.elapsedMs / duration, 1.0f);
|
||||
|
||||
switch (task.type) {
|
||||
case AnimTask::Type::Move:
|
||||
position = task.startPos + (task.targetPos - task.startPos) * t;
|
||||
break;
|
||||
case AnimTask::Type::Rotate:
|
||||
rotationY = task.startRotY + (task.targetRotY - task.startRotY) * t;
|
||||
break;
|
||||
case AnimTask::Type::Scale:
|
||||
scale = task.startScale + (task.targetScale - task.startScale) * t;
|
||||
break;
|
||||
case AnimTask::Type::Fade:
|
||||
alpha = task.startScale + (task.targetScale - task.startScale) * t;
|
||||
break;
|
||||
}
|
||||
|
||||
if (t < 1.0f) return events;
|
||||
position = task.targetPos;
|
||||
rotationY = task.targetRotY;
|
||||
if (task.type == AnimTask::Type::Fade)
|
||||
alpha = task.targetScale;
|
||||
else
|
||||
scale = task.targetScale;
|
||||
events.onCompleteName = std::move(task.onCompleteName);
|
||||
animTask.reset();
|
||||
isAnimating = false;
|
||||
return events;
|
||||
}
|
||||
|
||||
void InteractiveObjectState::save(nlohmann::json& out) const
|
||||
{
|
||||
out["positionX"] = position.x();
|
||||
out["positionY"] = position.y();
|
||||
out["positionZ"] = position.z();
|
||||
out["rotationY"] = rotationY;
|
||||
out["scale"] = scale;
|
||||
out["alpha"] = alpha;
|
||||
out["isActive"] = isActive;
|
||||
out["isAnimating"] = isAnimating;
|
||||
|
||||
if (isAnimating && animTask) {
|
||||
out["animTask"] = {
|
||||
{"type", static_cast<int>(animTask->type)},
|
||||
{"startPosX", animTask->startPos.x()},
|
||||
{"startPosY", animTask->startPos.y()},
|
||||
{"startPosZ", animTask->startPos.z()},
|
||||
{"startRotY", animTask->startRotY},
|
||||
{"startScale", animTask->startScale},
|
||||
{"targetPosX", animTask->targetPos.x()},
|
||||
{"targetPosY", animTask->targetPos.y()},
|
||||
{"targetPosZ", animTask->targetPos.z()},
|
||||
{"targetRotY", animTask->targetRotY},
|
||||
{"targetScale", animTask->targetScale},
|
||||
{"durationMs", animTask->durationMs},
|
||||
{"elapsedMs", animTask->elapsedMs},
|
||||
{"onCompleteName", animTask->onCompleteName}};
|
||||
}
|
||||
}
|
||||
|
||||
void InteractiveObjectState::load(const nlohmann::json& in)
|
||||
{
|
||||
position.x() = in.value("positionX", position.x());
|
||||
position.y() = in.value("positionY", position.y());
|
||||
position.z() = in.value("positionZ", position.z());
|
||||
rotationY = in.value("rotationY", rotationY);
|
||||
scale = in.value("scale", scale);
|
||||
alpha = in.value("alpha", alpha);
|
||||
isActive = in.value("isActive", isActive);
|
||||
isAnimating = in.value("isAnimating", false);
|
||||
animTask.reset();
|
||||
|
||||
if (!isAnimating || !in.contains("animTask") ||
|
||||
!in["animTask"].is_object()) {
|
||||
return;
|
||||
}
|
||||
const auto& value = in["animTask"];
|
||||
AnimTask task;
|
||||
task.type = static_cast<AnimTask::Type>(value.value("type", 0));
|
||||
task.startPos = {value.value("startPosX", 0.f),
|
||||
value.value("startPosY", 0.f), value.value("startPosZ", 0.f)};
|
||||
task.startRotY = value.value("startRotY", 0.f);
|
||||
task.startScale = value.value("startScale", 1.f);
|
||||
task.targetPos = {value.value("targetPosX", 0.f),
|
||||
value.value("targetPosY", 0.f), value.value("targetPosZ", 0.f)};
|
||||
task.targetRotY = value.value("targetRotY", 0.f);
|
||||
task.targetScale = value.value("targetScale", 1.f);
|
||||
task.durationMs = value.value("durationMs", 1000.f);
|
||||
task.elapsedMs = value.value("elapsedMs", 0.f);
|
||||
task.onCompleteName =
|
||||
value.value("onCompleteName", std::string());
|
||||
animTask = std::move(task);
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "Item.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
bool Inventory::addItem(const std::string& itemId)
|
||||
{
|
||||
itemIds.push_back(itemId);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Inventory::removeItem(const std::string& itemId)
|
||||
{
|
||||
auto it = std::find(itemIds.begin(), itemIds.end(), itemId);
|
||||
if (it == itemIds.end()) return false;
|
||||
itemIds.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Inventory::hasItem(const std::string& itemId) const
|
||||
{
|
||||
return std::find(itemIds.begin(), itemIds.end(), itemId) != itemIds.end();
|
||||
}
|
||||
|
||||
void Inventory::save(nlohmann::json& out) const
|
||||
{
|
||||
out["items"] = itemIds;
|
||||
}
|
||||
|
||||
void Inventory::load(const nlohmann::json& in)
|
||||
{
|
||||
itemIds.clear();
|
||||
if (!in.contains("items") || !in["items"].is_array()) return;
|
||||
for (const auto& item : in["items"]) {
|
||||
if (!item.is_string())
|
||||
throw std::runtime_error("Inventory item IDs must be strings");
|
||||
itemIds.push_back(item.get<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -1,33 +1,8 @@
|
||||
#include "Item.h"
|
||||
#include "ItemRegistry.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include "../utils/Utils.h"
|
||||
|
||||
namespace FRG {
|
||||
|
||||
bool Inventory::addItem(const std::string& itemId) {
|
||||
itemIds.push_back(itemId);
|
||||
logger() << "Item added to inventory: " << itemId << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Inventory::removeItem(const std::string& itemId) {
|
||||
auto it = std::find(itemIds.begin(), itemIds.end(), itemId);
|
||||
if (it != itemIds.end()) {
|
||||
logger() << "Item removed from inventory: " << itemId << std::endl;
|
||||
itemIds.erase(it);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Inventory::hasItem(const std::string& itemId) const {
|
||||
return std::find(itemIds.begin(), itemIds.end(), itemId) != itemIds.end();
|
||||
}
|
||||
|
||||
std::vector<Item> Inventory::getItems() const {
|
||||
std::vector<Item> result;
|
||||
result.reserve(itemIds.size());
|
||||
@@ -38,18 +13,4 @@ namespace FRG {
|
||||
return result;
|
||||
}
|
||||
|
||||
void Inventory::save(nlohmann::json& out) const {
|
||||
out["items"] = itemIds;
|
||||
}
|
||||
|
||||
void Inventory::load(const nlohmann::json& in) {
|
||||
itemIds.clear();
|
||||
if (!in.contains("items") || !in["items"].is_array()) return;
|
||||
for (const auto& j : in["items"]) {
|
||||
if (!j.is_string())
|
||||
throw std::runtime_error("Inventory item IDs must be strings");
|
||||
itemIds.push_back(j.get<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "render/Renderer.h"
|
||||
#include <Eigen/Core>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "Utils.h"
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
std::string readTextFile(const std::string& filename)
|
||||
{
|
||||
std::ifstream file(filename, std::ios::binary);
|
||||
if (!file) return {};
|
||||
return std::string(
|
||||
std::istreambuf_iterator<char>(file),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
std::vector<char> readFile(const std::string& filename)
|
||||
{
|
||||
std::ifstream file(filename, std::ios::binary | std::ios::ate);
|
||||
if (!file) return {};
|
||||
const std::streamsize size = file.tellg();
|
||||
if (size <= 0) return {};
|
||||
file.seekg(0, std::ios::beg);
|
||||
std::vector<char> data(static_cast<std::size_t>(size));
|
||||
if (!file.read(data.data(), size)) return {};
|
||||
return data;
|
||||
}
|
||||
|
||||
std::vector<char> readFileFromZIP(
|
||||
const std::string& filename, const std::string& zipfilename)
|
||||
{
|
||||
if (zipfilename.empty()) return readFile(filename);
|
||||
std::cerr << "ZIP resources are not enabled in the idle headless server: "
|
||||
<< zipfilename << " (" << filename << ")\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
bool zipFileExists(const std::string& zipfilename)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::path getSaveDirectory()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
const char* base = std::getenv("APPDATA");
|
||||
#else
|
||||
const char* base = std::getenv("XDG_CONFIG_HOME");
|
||||
if (!base || !*base) base = std::getenv("HOME");
|
||||
#endif
|
||||
const std::filesystem::path root =
|
||||
base && *base ? std::filesystem::path(base)
|
||||
: std::filesystem::current_path();
|
||||
return root / "Fish Run Games" / "Shadow Over Bishkek Demo";
|
||||
}
|
||||
|
||||
std::string readSavedTextFile(const std::string& filename)
|
||||
{
|
||||
return readTextFile((getSaveDirectory() / filename).string());
|
||||
}
|
||||
|
||||
bool saveJsonToFile(
|
||||
const nlohmann::json& root, const std::string& filename)
|
||||
{
|
||||
std::ofstream file(filename);
|
||||
if (!file) return false;
|
||||
file << root.dump(2);
|
||||
return file.good();
|
||||
}
|
||||
|
||||
std::ostream& logger()
|
||||
{
|
||||
return std::cout;
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
Reference in new issue
Block a user