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:
|
`InternalGameServer` is the current `IGameServer` implementation. It:
|
||||||
|
|
||||||
- Exclusively owns the aggregate `GameState`.
|
- Exclusively owns the aggregate `GameState`.
|
||||||
- Privately stores loaded runtime `Location` objects.
|
|
||||||
- Executes typed commands against authoritative state.
|
- Executes typed commands against authoritative state.
|
||||||
- Advances the active location simulation through `update(deltaMs)`.
|
- 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.
|
- Builds and caches immutable snapshots.
|
||||||
- Delegates save/load serialization to the aggregate state model.
|
- Delegates save/load serialization to the aggregate state model.
|
||||||
|
|
||||||
`Game` remains a privileged lifecycle host, but it no longer holds or receives a
|
`LocalGameServerHost` is the graphical client's integration adapter. It owns loaded
|
||||||
mutable `GameState` reference. It uses private, focused internal-server operations
|
runtime `Location` objects, forwards simulation events to them, and synchronizes
|
||||||
for campaign loading and location construction. Those operations publish revisions
|
dialogue, Lua, trigger, and teleport auxiliary state. `Game` uses the explicit
|
||||||
themselves and are not exposed by `IGameServer`.
|
`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
|
### Animation and combat
|
||||||
|
|
||||||
Logical animation frame clocks live in `CharacterState`. Animation frame counts are
|
Logical animation frame clocks live in `CharacterState`. The renderer-free content
|
||||||
copied from the loaded clip metadata during runtime construction, after which state
|
loader reads animation frame metadata before authoritative entities are created.
|
||||||
transitions and frame-19 combat impacts are deterministic authoritative simulation.
|
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
|
`CharacterState::update()` returns value-only events containing arrival Lua function
|
||||||
names, attack impacts, navigation requests, and death-animation completion. `LocationGameState` applies
|
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
|
Navigation target selection, replanning, stopping, facing, death reset, health
|
||||||
assignment, and attack-combo clock changes are implemented by `CharacterState`.
|
assignment, and attack-combo clock changes are implemented by `CharacterState`.
|
||||||
`Character` no longer exposes gameplay mutation helpers or a generic state mutation
|
`Character` no longer exposes gameplay mutation helpers or a generic state mutation
|
||||||
callback. Its only state-facing setup responsibility is reporting frame ranges from
|
callback. Character construction is presentation-only; frame ranges are already
|
||||||
loaded animation assets through `SetCharacterAnimationMetadataCommand`; otherwise it
|
present in `CharacterState` after content setup. It focuses on model posing,
|
||||||
focuses on model posing, rendering, audio, particles, and runtime callbacks.
|
rendering, audio, particles, and runtime callbacks.
|
||||||
|
|
||||||
Death-animation and health-change callbacks remain runtime-only on `Character`.
|
Death-animation and health-change callbacks remain runtime-only on `Character`.
|
||||||
Arrival behavior is represented solely by the serialized Lua function name in
|
Arrival behavior is represented solely by the serialized Lua function name in
|
||||||
@@ -242,10 +249,10 @@ by `Game`.
|
|||||||
|
|
||||||
`GameObjectLoader` now:
|
`GameObjectLoader` now:
|
||||||
|
|
||||||
1. Loads static creation data from location configuration.
|
1. Receives immutable object/NPC definitions parsed by `LocationContentLoader`.
|
||||||
2. Creates or reuses server-owned state objects.
|
2. Resolves already-prepared authoritative entities by stable ID.
|
||||||
3. Applies saved runtime fields over the configuration defaults.
|
3. Constructs meshes, textures, VBOs, `InteractiveObject`, and `Character`
|
||||||
4. Constructs visual objects using stable IDs and state-backed creation data.
|
presentation wrappers without creating or overwriting authoritative state.
|
||||||
|
|
||||||
Configuration remains the source of visual assets, animation paths, meshes, textures, and fixed tuning data.
|
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.
|
1. SDL input is processed.
|
||||||
2. UI code submits semantic commands through `IGameServer`.
|
2. UI code submits semantic commands through `IGameServer`.
|
||||||
3. The internal server freezes location character observations and calls
|
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.
|
4. Attack impacts are queued, then damage is applied after all characters advance.
|
||||||
5. The runtime location consumes value-only events. Navigation requests are sent
|
5. The runtime location consumes value-only events. Navigation requests are sent
|
||||||
back as semantic destination commands, whose server handler calls
|
back as semantic destination commands, whose server handler calls
|
||||||
@@ -283,21 +291,28 @@ When a campaign starts:
|
|||||||
|
|
||||||
- Existing campaign state and runtime locations are cleared.
|
- Existing campaign state and runtime locations are cleared.
|
||||||
- Quest definitions are loaded.
|
- Quest definitions are loaded.
|
||||||
- A campaign-level player state is created under the `"player"` ID.
|
- The graphical single-player host requests a campaign-level `"player"` state and
|
||||||
- The visual player is created with its stable ID and resolves that state through snapshots.
|
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
|
### Location loading
|
||||||
|
|
||||||
When a location is loaded:
|
When a location is loaded:
|
||||||
|
|
||||||
- A `LocationGameState` record is created or reused.
|
- `LocationContentLoader` parses a renderer-free `LocationDefinition`.
|
||||||
- Static configuration is loaded.
|
- `LocationGameState::setup()` creates or reconciles navigation, taxi, NPC,
|
||||||
- NPC and interactive-object visuals are created against state owned by the location record.
|
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.
|
- The shared campaign player is attached.
|
||||||
- Runtime visuals are created for other player IDs currently present in the
|
- Runtime visuals are created for other player IDs currently present in the
|
||||||
snapshot and are subsequently spawned or removed as snapshots change.
|
snapshot and are subsequently spawned or removed as snapshots change.
|
||||||
- Dialogue, Lua, navigation, trigger, teleport, and presentation callbacks are initialized.
|
- Dialogue, Lua, trigger/teleport visuals, and presentation callbacks are initialized.
|
||||||
- The runtime `Location` is registered privately with `InternalGameServer`.
|
- The runtime `Location` is owned and registered by `LocalGameServerHost`;
|
||||||
|
`InternalGameServer` remains headless.
|
||||||
|
|
||||||
### Location unloading
|
### Location unloading
|
||||||
|
|
||||||
@@ -387,7 +402,6 @@ Still required for real multiplayer:
|
|||||||
|
|
||||||
- Network transport and command serialization.
|
- Network transport and command serialization.
|
||||||
- Authentication and player/session assignment.
|
- Authentication and player/session assignment.
|
||||||
- Server tick scheduling independent of rendering.
|
|
||||||
- Snapshot delta generation or other replication strategy.
|
- Snapshot delta generation or other replication strategy.
|
||||||
- Latency handling, interpolation, reconciliation, and prediction.
|
- Latency handling, interpolation, reconciliation, and prediction.
|
||||||
- Command validation, authorization, and anti-cheat rules.
|
- 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 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:
|
The Windows Debug target was verified with:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -422,18 +442,24 @@ cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -
|
|||||||
location-local NPC ID).
|
location-local NPC ID).
|
||||||
- Some top-level values remain mirrors for compatibility, notably player health used by the HUD and scripts.
|
- 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.
|
- Dialogue and Lua are synchronized as auxiliary location state rather than fully networked subsystems.
|
||||||
- `Game` uses focused privileged internal-server APIs for local content
|
- `IGameServerLifecycle` is the graphics-free campaign/location administration
|
||||||
construction, but cannot access the aggregate or its state maps directly. The
|
boundary. `LocalGameServerHost` adds client-only runtime construction and
|
||||||
private location-construction path still supplies `LocationGameState` to the
|
auxiliary-state synchronization without making `Game` a server friend.
|
||||||
local object loader. A remote client implementation will need a client-side
|
- Empty player maps are valid. Player-specific interactions remain inactive until
|
||||||
runtime-location factory driven by configuration and snapshots.
|
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
|
## Main Files
|
||||||
|
|
||||||
- `src/GameState.h` / `src/GameState.cpp` — aggregate state and top-level persistence.
|
- `src/GameState.h` / `src/GameState.cpp` — aggregate state and top-level persistence.
|
||||||
- `src/LocationGameState.h` / `src/LocationGameState.cpp` — typed location state, simulation, and ID-keyed persistence.
|
- `src/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/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/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/Game.h` / `src/Game.cpp` — application host, campaign lifecycle, and runtime wiring.
|
||||||
- `src/Location.h` / `src/Location.cpp` — loaded location runtime, command routing,
|
- `src/Location.h` / `src/Location.cpp` — loaded location runtime, command routing,
|
||||||
interactions, collisions, scripts, and presentation-event consumption.
|
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
|
- `src/CharacterState.h` / `src/CharacterState.cpp` — deterministic character
|
||||||
simulation, navigation operations, combat state, and logical animation clock.
|
simulation, navigation operations, combat state, and logical animation clock.
|
||||||
- `src/items/InteractiveObject.h` / `.cpp` — visual object runtime bound to central object state.
|
- `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.
|
- `src/MenuManager.h` / `src/MenuManager.cpp` — snapshot-driven UI and command submission.
|
||||||
@@ -106,9 +106,11 @@ add_library(main SHARED
|
|||||||
../../../../src/GameConstants.cpp
|
../../../../src/GameConstants.cpp
|
||||||
../../../../src/GameState.h
|
../../../../src/GameState.h
|
||||||
../../../../src/GameState.cpp
|
../../../../src/GameState.cpp
|
||||||
|
../../../../src/content/LocationDefinition.cpp
|
||||||
../../../../src/game_server/IGameServer.h
|
../../../../src/game_server/IGameServer.h
|
||||||
../../../../src/game_server/InternalGameServer.h
|
../../../../src/game_server/InternalGameServer.h
|
||||||
../../../../src/game_server/InternalGameServer.cpp
|
../../../../src/game_server/InternalGameServer.cpp
|
||||||
|
../../../../src/game_server/LocalGameServerHost.cpp
|
||||||
../../../../src/ScriptEngine.h
|
../../../../src/ScriptEngine.h
|
||||||
../../../../src/ScriptEngine.cpp
|
../../../../src/ScriptEngine.cpp
|
||||||
../../../../src/navigation/PathFinder.h
|
../../../../src/navigation/PathFinder.h
|
||||||
@@ -117,11 +119,13 @@ add_library(main SHARED
|
|||||||
../../../../src/items/GameObjectLoader.cpp
|
../../../../src/items/GameObjectLoader.cpp
|
||||||
../../../../src/items/Item.h
|
../../../../src/items/Item.h
|
||||||
../../../../src/items/Item.cpp
|
../../../../src/items/Item.cpp
|
||||||
|
../../../../src/items/Inventory.cpp
|
||||||
../../../../src/items/ItemRegistry.h
|
../../../../src/items/ItemRegistry.h
|
||||||
../../../../src/items/ItemRegistry.cpp
|
../../../../src/items/ItemRegistry.cpp
|
||||||
../../../../src/Campaign.cpp
|
../../../../src/Campaign.cpp
|
||||||
../../../../src/items/InteractiveObject.h
|
../../../../src/items/InteractiveObject.h
|
||||||
../../../../src/items/InteractiveObject.cpp
|
../../../../src/items/InteractiveObject.cpp
|
||||||
|
../../../../src/items/InteractiveObjectState.cpp
|
||||||
../../../../src/items/InteractiveObjectState.h
|
../../../../src/items/InteractiveObjectState.h
|
||||||
../../../../src/dialogue/DialogueTypes.h
|
../../../../src/dialogue/DialogueTypes.h
|
||||||
../../../../src/dialogue/DialogueDatabase.h
|
../../../../src/dialogue/DialogueDatabase.h
|
||||||
|
|||||||
@@ -131,9 +131,15 @@ add_executable(ShadowOverBishkekDemo
|
|||||||
../src/GameConstants.cpp
|
../src/GameConstants.cpp
|
||||||
../src/GameState.h
|
../src/GameState.h
|
||||||
../src/GameState.cpp
|
../src/GameState.cpp
|
||||||
|
../src/content/LocationDefinition.h
|
||||||
|
../src/content/LocationDefinition.cpp
|
||||||
../src/game_server/IGameServer.h
|
../src/game_server/IGameServer.h
|
||||||
|
../src/game_server/IGameServerLifecycle.h
|
||||||
|
../src/game_server/IGameServerHost.h
|
||||||
../src/game_server/InternalGameServer.h
|
../src/game_server/InternalGameServer.h
|
||||||
../src/game_server/InternalGameServer.cpp
|
../src/game_server/InternalGameServer.cpp
|
||||||
|
../src/game_server/LocalGameServerHost.h
|
||||||
|
../src/game_server/LocalGameServerHost.cpp
|
||||||
../src/ScriptEngine.h
|
../src/ScriptEngine.h
|
||||||
../src/ScriptEngine.cpp
|
../src/ScriptEngine.cpp
|
||||||
../src/navigation/PathFinder.h
|
../src/navigation/PathFinder.h
|
||||||
@@ -142,12 +148,14 @@ add_executable(ShadowOverBishkekDemo
|
|||||||
../src/items/GameObjectLoader.cpp
|
../src/items/GameObjectLoader.cpp
|
||||||
../src/items/Item.h
|
../src/items/Item.h
|
||||||
../src/items/Item.cpp
|
../src/items/Item.cpp
|
||||||
|
../src/items/Inventory.cpp
|
||||||
../src/items/ItemRegistry.h
|
../src/items/ItemRegistry.h
|
||||||
../src/items/ItemRegistry.cpp
|
../src/items/ItemRegistry.cpp
|
||||||
../src/Campaign.cpp
|
../src/Campaign.cpp
|
||||||
../src/items/InteractiveObject.h
|
../src/items/InteractiveObject.h
|
||||||
../src/items/InteractiveObject.cpp
|
../src/items/InteractiveObject.cpp
|
||||||
../src/items/InteractiveObjectState.h
|
../src/items/InteractiveObjectState.h
|
||||||
|
../src/items/InteractiveObjectState.cpp
|
||||||
../src/dialogue/DialogueTypes.h
|
../src/dialogue/DialogueTypes.h
|
||||||
../src/dialogue/DialogueDatabase.h
|
../src/dialogue/DialogueDatabase.h
|
||||||
../src/dialogue/DialogueDatabase.cpp
|
../src/dialogue/DialogueDatabase.cpp
|
||||||
|
|||||||
@@ -125,9 +125,11 @@ set(SOURCES
|
|||||||
../src/GameConstants.cpp
|
../src/GameConstants.cpp
|
||||||
../src/GameState.h
|
../src/GameState.h
|
||||||
../src/GameState.cpp
|
../src/GameState.cpp
|
||||||
|
../src/content/LocationDefinition.cpp
|
||||||
../src/game_server/IGameServer.h
|
../src/game_server/IGameServer.h
|
||||||
../src/game_server/InternalGameServer.h
|
../src/game_server/InternalGameServer.h
|
||||||
../src/game_server/InternalGameServer.cpp
|
../src/game_server/InternalGameServer.cpp
|
||||||
|
../src/game_server/LocalGameServerHost.cpp
|
||||||
../src/ScriptEngine.h
|
../src/ScriptEngine.h
|
||||||
../src/ScriptEngine.cpp
|
../src/ScriptEngine.cpp
|
||||||
../src/navigation/PathFinder.h
|
../src/navigation/PathFinder.h
|
||||||
@@ -136,11 +138,13 @@ set(SOURCES
|
|||||||
../src/items/GameObjectLoader.cpp
|
../src/items/GameObjectLoader.cpp
|
||||||
../src/items/Item.h
|
../src/items/Item.h
|
||||||
../src/items/Item.cpp
|
../src/items/Item.cpp
|
||||||
|
../src/items/Inventory.cpp
|
||||||
../src/items/ItemRegistry.h
|
../src/items/ItemRegistry.h
|
||||||
../src/items/ItemRegistry.cpp
|
../src/items/ItemRegistry.cpp
|
||||||
../src/Campaign.cpp
|
../src/Campaign.cpp
|
||||||
../src/items/InteractiveObject.h
|
../src/items/InteractiveObject.h
|
||||||
../src/items/InteractiveObject.cpp
|
../src/items/InteractiveObject.cpp
|
||||||
|
../src/items/InteractiveObjectState.cpp
|
||||||
../src/items/InteractiveObjectState.h
|
../src/items/InteractiveObjectState.h
|
||||||
../src/dialogue/DialogueTypes.h
|
../src/dialogue/DialogueTypes.h
|
||||||
../src/dialogue/DialogueDatabase.h
|
../src/dialogue/DialogueDatabase.h
|
||||||
|
|||||||
@@ -73,9 +73,15 @@ add_executable(ShadowOverBishkekDemo WIN32
|
|||||||
../src/GameConstants.cpp
|
../src/GameConstants.cpp
|
||||||
../src/GameState.h
|
../src/GameState.h
|
||||||
../src/GameState.cpp
|
../src/GameState.cpp
|
||||||
|
../src/content/LocationDefinition.h
|
||||||
|
../src/content/LocationDefinition.cpp
|
||||||
../src/game_server/IGameServer.h
|
../src/game_server/IGameServer.h
|
||||||
|
../src/game_server/IGameServerLifecycle.h
|
||||||
|
../src/game_server/IGameServerHost.h
|
||||||
../src/game_server/InternalGameServer.h
|
../src/game_server/InternalGameServer.h
|
||||||
../src/game_server/InternalGameServer.cpp
|
../src/game_server/InternalGameServer.cpp
|
||||||
|
../src/game_server/LocalGameServerHost.h
|
||||||
|
../src/game_server/LocalGameServerHost.cpp
|
||||||
../src/ScriptEngine.h
|
../src/ScriptEngine.h
|
||||||
../src/ScriptEngine.cpp
|
../src/ScriptEngine.cpp
|
||||||
../src/navigation/PathFinder.h
|
../src/navigation/PathFinder.h
|
||||||
@@ -84,12 +90,14 @@ add_executable(ShadowOverBishkekDemo WIN32
|
|||||||
../src/items/GameObjectLoader.cpp
|
../src/items/GameObjectLoader.cpp
|
||||||
../src/items/Item.h
|
../src/items/Item.h
|
||||||
../src/items/Item.cpp
|
../src/items/Item.cpp
|
||||||
|
../src/items/Inventory.cpp
|
||||||
../src/items/ItemRegistry.h
|
../src/items/ItemRegistry.h
|
||||||
../src/items/ItemRegistry.cpp
|
../src/items/ItemRegistry.cpp
|
||||||
../src/Campaign.cpp
|
../src/Campaign.cpp
|
||||||
../src/items/InteractiveObject.h
|
../src/items/InteractiveObject.h
|
||||||
../src/items/InteractiveObject.cpp
|
../src/items/InteractiveObject.cpp
|
||||||
../src/items/InteractiveObjectState.h
|
../src/items/InteractiveObjectState.h
|
||||||
|
../src/items/InteractiveObjectState.cpp
|
||||||
../src/dialogue/DialogueTypes.h
|
../src/dialogue/DialogueTypes.h
|
||||||
../src/dialogue/DialogueDatabase.h
|
../src/dialogue/DialogueDatabase.h
|
||||||
../src/dialogue/DialogueDatabase.cpp
|
../src/dialogue/DialogueDatabase.cpp
|
||||||
|
|||||||
@@ -414,7 +414,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.1,
|
"positionX": 1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": 10.0,
|
"positionZ": 10.0,
|
||||||
"approachRadius": 0.89,
|
"approachRadius": 0.89,
|
||||||
"boundsMaxX": 0.01552116870880127,
|
"boundsMaxX": 0.01552116870880127,
|
||||||
@@ -439,7 +439,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.1,
|
"positionX": -1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": 10.0,
|
"positionZ": 10.0,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.10440409183502197,
|
"boundsMaxX": 0.10440409183502197,
|
||||||
@@ -464,7 +464,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.1,
|
"positionX": 1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -10.0,
|
"positionZ": -10.0,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.05766594409942627,
|
"boundsMaxX": 0.05766594409942627,
|
||||||
@@ -489,7 +489,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.1,
|
"positionX": -1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -10.0,
|
"positionZ": -10.0,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.10906755924224854,
|
"boundsMaxX": 0.10906755924224854,
|
||||||
@@ -514,7 +514,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.1,
|
"positionX": 1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": 15.7,
|
"positionZ": 15.7,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.046350955963134766,
|
"boundsMaxX": 0.046350955963134766,
|
||||||
@@ -539,7 +539,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.1,
|
"positionX": -1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": 15.7,
|
"positionZ": 15.7,
|
||||||
"approachRadius": 0.800000011920929,
|
"approachRadius": 0.800000011920929,
|
||||||
"boundsMaxX": 0.12420153617858887,
|
"boundsMaxX": 0.12420153617858887,
|
||||||
@@ -564,7 +564,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.1,
|
"positionX": 1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": 19.95,
|
"positionZ": 19.95,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": -0.050987482070922852,
|
"boundsMaxX": -0.050987482070922852,
|
||||||
@@ -589,7 +589,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.1,
|
"positionX": -1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": 19.95,
|
"positionZ": 19.95,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.073349356651306152,
|
"boundsMaxX": 0.073349356651306152,
|
||||||
@@ -614,7 +614,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.1,
|
"positionX": 1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -19.95,
|
"positionZ": -19.95,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -631,7 +631,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.1,
|
"positionX": -1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -19.95,
|
"positionZ": -19.95,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -647,7 +647,7 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 6.6644,
|
"positionX": 6.6644004991889,
|
||||||
"positionY": 0.9,
|
"positionY": 0.9,
|
||||||
"positionZ": -12.5262,
|
"positionZ": -12.5262,
|
||||||
"approachRadius": 0.6,
|
"approachRadius": 0.6,
|
||||||
@@ -674,7 +674,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 5.84296,
|
"positionX": 5.84296,
|
||||||
"positionY": 0.9,
|
"positionY": 0.9284919999539852,
|
||||||
"positionZ": -12.4661,
|
"positionZ": -12.4661,
|
||||||
"approachRadius": 0.6,
|
"approachRadius": 0.6,
|
||||||
"boundsMaxX": 0.17081403732299805,
|
"boundsMaxX": 0.17081403732299805,
|
||||||
@@ -726,9 +726,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 5.10067,
|
"positionX": 5.1006695008111,
|
||||||
"positionY": 0.91385,
|
"positionY": 1.0181855017900467,
|
||||||
"positionZ": -11.9566,
|
"positionZ": -11.958324999397993,
|
||||||
"approachRadius": 0.6,
|
"approachRadius": 0.6,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.3,
|
"interactionRadius": 0.3,
|
||||||
@@ -743,7 +743,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 2.825,
|
"positionX": 2.825,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -14.3,
|
"positionZ": -14.3,
|
||||||
"pivotX": -0.6,
|
"pivotX": -0.6,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -771,7 +771,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -2.825,
|
"positionX": -2.825,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -14.3,
|
"positionZ": -14.3,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.55439233779907227,
|
"boundsMaxX": 0.55439233779907227,
|
||||||
@@ -796,7 +796,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.1,
|
"positionX": 1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -15.7,
|
"positionZ": -15.7,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -824,7 +824,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.1,
|
"positionX": -1.1,
|
||||||
"positionY": 0.0,
|
"positionY": 0.9695000052452087,
|
||||||
"positionZ": -15.7,
|
"positionZ": -15.7,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.13942182064056396,
|
"boundsMaxX": 0.13942182064056396,
|
||||||
@@ -852,9 +852,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 2.8249999284744263,
|
||||||
"positionY": -0.1,
|
"positionY": 2.8500000476837157,
|
||||||
"positionZ": 0.0,
|
"positionZ": -12.849999904632568,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
@@ -867,9 +867,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 4.949999928474426,
|
||||||
"positionY": -0.1,
|
"positionY": 2.8500000476837157,
|
||||||
"positionZ": 0.0,
|
"positionZ": -14.25,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
@@ -882,9 +882,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": -4.949999928474426,
|
||||||
"positionY": -0.1,
|
"positionY": 2.8500000476837157,
|
||||||
"positionZ": 0.0,
|
"positionZ": -14.25,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
@@ -898,7 +898,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": -0.1,
|
"positionY": 2.8500000476837157,
|
||||||
"positionZ": 0.0,
|
"positionZ": 0.0,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
@@ -913,8 +913,8 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": -0.1,
|
"positionY": 2.8500000476837157,
|
||||||
"positionZ": 0.0,
|
"positionZ": 4.250000476837158,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
@@ -927,9 +927,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 4.949999928474426,
|
||||||
"positionY": -0.1,
|
"positionY": 2.8500000476837157,
|
||||||
"positionZ": 0.0,
|
"positionZ": -19.950000762939453,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
@@ -942,9 +942,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": -4.949999928474426,
|
||||||
"positionY": -0.1,
|
"positionY": 2.8500000476837157,
|
||||||
"positionZ": 0.0,
|
"positionZ": -19.950000762939453,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
},
|
},
|
||||||
@@ -960,7 +960,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": 0.1,
|
"positionY": 3.2050000190734864,
|
||||||
"positionZ": 0.0,
|
"positionZ": 0.0,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -26.8049,
|
"positionX": -27.003390023612976,
|
||||||
"positionY": 0.0,
|
"positionY": 0.32889999728649855,
|
||||||
"positionZ": 9.11524,
|
"positionZ": 8.820399544944763,
|
||||||
"approachRadius": 1.5,
|
"approachRadius": 1.5,
|
||||||
"boundsMaxX": 2.9518299102783203,
|
"boundsMaxX": 2.9518299102783203,
|
||||||
"boundsMaxY": 0.5,
|
"boundsMaxY": 0.5,
|
||||||
@@ -33,9 +33,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 90.0,
|
"rotationY": 90.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -10.5,
|
"positionX": -10.503284412622452,
|
||||||
"positionY": 0.55,
|
"positionY": 0.9012498408555984,
|
||||||
"positionZ": -14.4195,
|
"positionZ": -14.861633367061614,
|
||||||
"approachRadius": 0.5,
|
"approachRadius": 0.5,
|
||||||
"boundsMaxX": 0.8858480453491211,
|
"boundsMaxX": 0.8858480453491211,
|
||||||
"boundsMaxY": 0.7,
|
"boundsMaxY": 0.7,
|
||||||
@@ -59,8 +59,8 @@
|
|||||||
"rotationY": 90.0,
|
"rotationY": 90.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -10.5,
|
"positionX": -10.5,
|
||||||
"positionY": 0.0,
|
"positionY": 0.3359999924898147,
|
||||||
"positionZ": -14.4195,
|
"positionZ": -14.839499933242797,
|
||||||
"approachRadius": 0.5,
|
"approachRadius": 0.5,
|
||||||
"boundsMaxX": 0,
|
"boundsMaxX": 0,
|
||||||
"boundsMaxY": 0.0,
|
"boundsMaxY": 0.0,
|
||||||
|
|||||||
@@ -116,9 +116,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0,
|
"positionX": -5.365539073944092,
|
||||||
"positionY": -5.0,
|
"positionY": 0.9888315200805664,
|
||||||
"positionZ": 0,
|
"positionZ": 2.468366503715515,
|
||||||
"approachRadius": 0.6,
|
"approachRadius": 0.6,
|
||||||
"boundsMaxX": 0.19605731964111328,
|
"boundsMaxX": 0.19605731964111328,
|
||||||
"boundsMaxY": 0.4,
|
"boundsMaxY": 0.4,
|
||||||
@@ -144,9 +144,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 5.0916,
|
"positionX": 5.01753252658844,
|
||||||
"positionY": 1.1534,
|
"positionY": 1.353411484324932,
|
||||||
"positionZ": 0.751629,
|
"positionZ": 0.6829135021486282,
|
||||||
"approachRadius": 1.2,
|
"approachRadius": 1.2,
|
||||||
"boundsMaxX": 0.6253643035888672,
|
"boundsMaxX": 0.6253643035888672,
|
||||||
"boundsMaxY": 0.5,
|
"boundsMaxY": 0.5,
|
||||||
@@ -168,9 +168,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -4.8327,
|
"positionX": -4.959765496635623,
|
||||||
"positionY": 0.93551,
|
"positionY": 0.93551,
|
||||||
"positionZ": 2.2418,
|
"positionZ": 2.369899506953731,
|
||||||
"approachRadius": 1.4,
|
"approachRadius": 1.4,
|
||||||
"boundsMaxX": 0.30867958068847656,
|
"boundsMaxX": 0.30867958068847656,
|
||||||
"boundsMaxY": 0.4,
|
"boundsMaxY": 0.4,
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.84795,
|
"positionX": 1.84795,
|
||||||
"positionY": 0.6595,
|
"positionY": 0.6809705001562834,
|
||||||
"positionZ": 2.90552,
|
"positionZ": 2.90552,
|
||||||
"approachRadius": 0.6,
|
"approachRadius": 0.6,
|
||||||
"boundsMaxX": 0.2350165843963623,
|
"boundsMaxX": 0.2350165843963623,
|
||||||
@@ -294,9 +294,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 90.0,
|
"rotationY": 90.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -10.5,
|
"positionX": -10.503284412622452,
|
||||||
"positionY": -5.0,
|
"positionY": -4.648750159144401,
|
||||||
"positionZ": -14.4195,
|
"positionZ": -14.861633367061614,
|
||||||
"approachRadius": 0.6,
|
"approachRadius": 0.6,
|
||||||
"boundsMaxX": 0.0,
|
"boundsMaxX": 0.0,
|
||||||
"boundsMaxY": 0.0,
|
"boundsMaxY": 0.0,
|
||||||
@@ -319,8 +319,8 @@
|
|||||||
"rotationY": 90.0,
|
"rotationY": 90.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -10.5,
|
"positionX": -10.5,
|
||||||
"positionY": -5.0,
|
"positionY": -4.664000007510185,
|
||||||
"positionZ": -14.4195,
|
"positionZ": -14.839499933242797,
|
||||||
"approachRadius": 0.6,
|
"approachRadius": 0.6,
|
||||||
"boundsMaxX": 0.0,
|
"boundsMaxX": 0.0,
|
||||||
"boundsMaxY": 0.0,
|
"boundsMaxY": 0.0,
|
||||||
@@ -344,7 +344,7 @@
|
|||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.5,
|
"positionX": 1.5,
|
||||||
"positionY": 0.975,
|
"positionY": 0.975,
|
||||||
"positionZ": 6.965,
|
"positionZ": 6.400000002384186,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.06785714626312256,
|
"boundsMaxX": 0.06785714626312256,
|
||||||
"boundsMaxY": 1.0,
|
"boundsMaxY": 1.0,
|
||||||
@@ -372,7 +372,7 @@
|
|||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.5,
|
"positionX": 1.5,
|
||||||
"positionY": 0.975,
|
"positionY": 0.975,
|
||||||
"positionZ": -1.035,
|
"positionZ": -1.5999999976158141,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.10772454738616943,
|
"boundsMaxX": 0.10772454738616943,
|
||||||
"boundsMaxY": 1.0,
|
"boundsMaxY": 1.0,
|
||||||
@@ -400,7 +400,7 @@
|
|||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.5,
|
"positionX": -1.5,
|
||||||
"positionY": 0.975,
|
"positionY": 0.975,
|
||||||
"positionZ": -1.035,
|
"positionZ": -1.5999999976158141,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.12115073204040527,
|
"boundsMaxX": 0.12115073204040527,
|
||||||
"boundsMaxY": 1.0,
|
"boundsMaxY": 1.0,
|
||||||
@@ -428,7 +428,7 @@
|
|||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.5,
|
"positionX": -1.5,
|
||||||
"positionY": 0.975,
|
"positionY": 0.975,
|
||||||
"positionZ": 6.965,
|
"positionZ": 6.400000002384186,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.09747374057769775,
|
"boundsMaxX": 0.09747374057769775,
|
||||||
"boundsMaxY": 1.0,
|
"boundsMaxY": 1.0,
|
||||||
@@ -454,9 +454,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.565,
|
"positionX": 0.0,
|
||||||
"positionY": 0.975,
|
"positionY": 0.975,
|
||||||
"positionZ": 8,
|
"positionZ": 8.000001000240445,
|
||||||
"approachRadius": 0.9,
|
"approachRadius": 0.9,
|
||||||
"boundsMaxX": 0.6243956089019775,
|
"boundsMaxX": 0.6243956089019775,
|
||||||
"boundsMaxY": 1.0,
|
"boundsMaxY": 1.0,
|
||||||
@@ -484,7 +484,7 @@
|
|||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.5,
|
"positionX": -1.5,
|
||||||
"positionY": 0.975,
|
"positionY": 0.975,
|
||||||
"positionZ": -9.035,
|
"positionZ": -9.599999020778657,
|
||||||
"approachRadius": 0.8,
|
"approachRadius": 0.8,
|
||||||
"boundsMaxX": 0.10914039611816406,
|
"boundsMaxX": 0.10914039611816406,
|
||||||
"boundsMaxY": 1.0,
|
"boundsMaxY": 1.0,
|
||||||
@@ -511,9 +511,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 2.2500000596046448,
|
||||||
"positionY": -5.0,
|
"positionY": 2.4499998092651367,
|
||||||
"positionZ": 0.0,
|
"positionZ": -3.999999761581421,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -530,8 +530,8 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": -5.0,
|
"positionY": 2.4499998092651367,
|
||||||
"positionZ": 0.0,
|
"positionZ": 12.0,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -547,9 +547,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": -3.7500000596046448,
|
||||||
"positionY": -5.0,
|
"positionY": 2.4499998092651367,
|
||||||
"positionZ": 0.0,
|
"positionZ": -12.0,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -565,9 +565,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 3.7500000596046448,
|
||||||
"positionY": -5.0,
|
"positionY": 2.4499998092651367,
|
||||||
"positionZ": 0.0,
|
"positionZ": -4.000000048428774,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -583,9 +583,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": -3.7500000596046448,
|
||||||
"positionY": -5.0,
|
"positionY": 2.4499998092651367,
|
||||||
"positionZ": 0.0,
|
"positionZ": -4.000000048428774,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -601,9 +601,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 3.7500000596046448,
|
||||||
"positionY": -5.0,
|
"positionY": 2.4499998092651367,
|
||||||
"positionZ": 0.0,
|
"positionZ": 4.000000048428774,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
@@ -619,9 +619,9 @@
|
|||||||
"rotationX": 0.0,
|
"rotationX": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": -3.7500000596046448,
|
||||||
"positionY": -5.0,
|
"positionY": 2.4499998092651367,
|
||||||
"positionZ": 0.0,
|
"positionZ": 4.000000048428774,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"pivotX": 0.0,
|
"pivotX": 0.0,
|
||||||
"pivotY": 0.0,
|
"pivotY": 0.0,
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": 3.1999998092651367,
|
"positionY": 3.200000047683716,
|
||||||
"positionZ": 0.0,
|
"positionZ": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"scale": 1.0
|
"scale": 1.0
|
||||||
@@ -148,6 +148,43 @@
|
|||||||
"playerHp": 200.0,
|
"playerHp": 200.0,
|
||||||
"playerMaxHp": 200.0,
|
"playerMaxHp": 200.0,
|
||||||
"players": {
|
"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": {
|
"player": {
|
||||||
"animationFrame": 0.0,
|
"animationFrame": 0.0,
|
||||||
"attackTargetId": "",
|
"attackTargetId": "",
|
||||||
@@ -195,7 +232,7 @@
|
|||||||
"questJournal": {
|
"questJournal": {
|
||||||
"quests": []
|
"quests": []
|
||||||
},
|
},
|
||||||
"savedAt": "2026-10-28 23:24",
|
"savedAt": "2026-10-30 14:50",
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"taxiIsCalled": false,
|
"taxiIsCalled": false,
|
||||||
"tutorialJournalScreenOpened": false,
|
"tutorialJournalScreenOpened": false,
|
||||||
|
|||||||
@@ -159,6 +159,7 @@
|
|||||||
"animTask": {
|
"animTask": {
|
||||||
"durationMs": 10.0,
|
"durationMs": 10.0,
|
||||||
"elapsedMs": 0.0,
|
"elapsedMs": 0.0,
|
||||||
|
"onCompleteName": "",
|
||||||
"startPosX": -1.100000023841858,
|
"startPosX": -1.100000023841858,
|
||||||
"startPosY": 0.9695000052452087,
|
"startPosY": 0.9695000052452087,
|
||||||
"startPosZ": -19.950000762939453,
|
"startPosZ": -19.950000762939453,
|
||||||
@@ -184,6 +185,7 @@
|
|||||||
"animTask": {
|
"animTask": {
|
||||||
"durationMs": 10.0,
|
"durationMs": 10.0,
|
||||||
"elapsedMs": 0.0,
|
"elapsedMs": 0.0,
|
||||||
|
"onCompleteName": "",
|
||||||
"startPosX": 1.100000023841858,
|
"startPosX": 1.100000023841858,
|
||||||
"startPosY": 0.9695000052452087,
|
"startPosY": 0.9695000052452087,
|
||||||
"startPosZ": -19.950000762939453,
|
"startPosZ": -19.950000762939453,
|
||||||
@@ -259,7 +261,7 @@
|
|||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": 5.842959880828857,
|
"positionX": 5.842959880828857,
|
||||||
"positionY": 0.9284919500350952,
|
"positionY": 0.92849200963974,
|
||||||
"positionZ": -12.466099739074707,
|
"positionZ": -12.466099739074707,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"scale": 1.0
|
"scale": 1.0
|
||||||
@@ -763,7 +765,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"savedAt": "2026-10-28 23:16",
|
"savedAt": "2026-10-30 14:48",
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"taxiIsCalled": false,
|
"taxiIsCalled": false,
|
||||||
"tutorialJournalScreenOpened": false,
|
"tutorialJournalScreenOpened": false,
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"rotationY": -180.0,
|
"rotationY": -180.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.6,
|
"positionX": 0.6,
|
||||||
"positionY": 0.0,
|
"positionY": 0.45736101269721985,
|
||||||
"positionZ": 3.85,
|
"positionZ": 3.85,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"rotationY": -180.0,
|
"rotationY": -180.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.4,
|
"positionX": 1.4,
|
||||||
"positionY": 0.0,
|
"positionY": 0.45736101269721985,
|
||||||
"positionZ": 3.85,
|
"positionZ": 3.85,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"rotationY": -180.0,
|
"rotationY": -180.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 2.2,
|
"positionX": 2.2,
|
||||||
"positionY": 0.0,
|
"positionY": 0.45736101269721985,
|
||||||
"positionZ": 3.85,
|
"positionZ": 3.85,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"rotationY": -180.0,
|
"rotationY": -180.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 3.0,
|
"positionX": 3.0,
|
||||||
"positionY": 0.0,
|
"positionY": 0.45736101269721985,
|
||||||
"positionZ": 3.85,
|
"positionZ": 3.85,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": 0.0,
|
"positionY": 3.2,
|
||||||
"positionZ": 0.0,
|
"positionZ": 0.0,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
"rotationY": -90.0,
|
"rotationY": -90.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": -1.0,
|
"positionX": -1.0,
|
||||||
"positionY": 0.0,
|
"positionY": 0.915,
|
||||||
"positionZ": -3.9,
|
"positionZ": -3.9,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
"rotationY": -90.0,
|
"rotationY": -90.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": 0.0,
|
"positionY": 0.915,
|
||||||
"positionZ": -3.9,
|
"positionZ": -3.9,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.0
|
"interactionRadius": 0.0
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
"rotationY": -90.0,
|
"rotationY": -90.0,
|
||||||
"rotationZ": 0.0,
|
"rotationZ": 0.0,
|
||||||
"positionX": 1.0,
|
"positionX": 1.0,
|
||||||
"positionY": 0.0,
|
"positionY": 0.915,
|
||||||
"positionZ": -3.9,
|
"positionZ": -3.9,
|
||||||
"scale": 1.0,
|
"scale": 1.0,
|
||||||
"interactionRadius": 0.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 "external/nlohmann/json.hpp"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|||||||
@@ -65,8 +65,6 @@ void Character::loadAnimation(AnimationState animState, const std::string& filen
|
|||||||
} else {
|
} else {
|
||||||
data.totalFrames = 1;
|
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) {
|
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 {
|
} else {
|
||||||
data.totalFrames = 1;
|
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,
|
std::unique_ptr<Character> Character::createFromState(const CharacterState& st, IGameServer& server,
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ private:
|
|||||||
friend class ScriptEngine;
|
friend class ScriptEngine;
|
||||||
friend class LocationEditor;
|
friend class LocationEditor;
|
||||||
friend class PathVisualization;
|
friend class PathVisualization;
|
||||||
friend class InternalGameServer;
|
|
||||||
IGameServer& gameServer_;
|
IGameServer& gameServer_;
|
||||||
std::string locationId_;
|
std::string locationId_;
|
||||||
std::string entityId_;
|
std::string entityId_;
|
||||||
|
|||||||
+22
-19
@@ -79,12 +79,13 @@ namespace FRG
|
|||||||
Location* Game::currentLocation() const {
|
Location* Game::currentLocation() const {
|
||||||
const auto state = stateSnapshot();
|
const auto state = stateSnapshot();
|
||||||
if (!state || state->currentLocationId.empty()) return nullptr;
|
if (!state || state->currentLocationId.empty()) return nullptr;
|
||||||
return internalGameServer.runtimeLocation(state->currentLocationId);
|
return gameServerHost.runtimeLocation(state->currentLocationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Game::Game()
|
Game::Game()
|
||||||
: audioPlayer(std::make_unique<AudioPlayerAsync>())
|
: audioPlayer(std::make_unique<AudioPlayerAsync>())
|
||||||
, internalGameServer()
|
, internalGameServer()
|
||||||
|
, gameServerHost(internalGameServer)
|
||||||
, menuManager(renderer, internalGameServer, *audioPlayer)
|
, menuManager(renderer, internalGameServer, *audioPlayer)
|
||||||
, newTickCount(0)
|
, newTickCount(0)
|
||||||
, lastTickCount(0)
|
, lastTickCount(0)
|
||||||
@@ -505,7 +506,7 @@ namespace FRG
|
|||||||
|
|
||||||
bool Game::isLocationLoaded(const std::string& campaignId, const std::string& locationId) const {
|
bool Game::isLocationLoaded(const std::string& campaignId, const std::string& locationId) const {
|
||||||
return player && loadedCampaignId == campaignId
|
return player && loadedCampaignId == campaignId
|
||||||
&& internalGameServer.hasRuntimeLocation(locationId);
|
&& gameServerHost.hasRuntimeLocation(locationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::enterLocationThen(const std::string& requestedCampaignId, const std::string& requestedLocationId,
|
void Game::enterLocationThen(const std::string& requestedCampaignId, const std::string& requestedLocationId,
|
||||||
@@ -548,7 +549,7 @@ namespace FRG
|
|||||||
const CampaignConfig* campaign =
|
const CampaignConfig* campaign =
|
||||||
CampaignRegistry::instance().find(campaignId);
|
CampaignRegistry::instance().find(campaignId);
|
||||||
if (!campaign) return;
|
if (!campaign) return;
|
||||||
internalGameServer.initializeCampaign(
|
gameServerHost.initializeCampaign(
|
||||||
campaignId, campaign->questsPath, CONST_ZIP_FILE);
|
campaignId, campaign->questsPath, CONST_ZIP_FILE);
|
||||||
// The player stays loaded for the whole campaign; locations come and go around him.
|
// The player stays loaded for the whole campaign; locations come and go around him.
|
||||||
player = Location::createPlayer(
|
player = Location::createPlayer(
|
||||||
@@ -562,7 +563,7 @@ namespace FRG
|
|||||||
unloadLocation(false);
|
unloadLocation(false);
|
||||||
player.reset();
|
player.reset();
|
||||||
loadedCampaignId.clear();
|
loadedCampaignId.clear();
|
||||||
internalGameServer.clearCampaign();
|
gameServerHost.clearCampaign();
|
||||||
renderer.textureManager.UnloadUnused(); // the player's textures
|
renderer.textureManager.UnloadUnused(); // the player's textures
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,18 +576,19 @@ namespace FRG
|
|||||||
<< locationId << "'" << std::endl;
|
<< locationId << "'" << std::endl;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bool initializeLocation = internalGameServer.prepareLocation(*config);
|
SDL_PumpEvents();
|
||||||
|
const bool initializeLocation = gameServerHost.prepareLocation(*config);
|
||||||
std::shared_ptr<Location> loc = createLocation(*config);
|
std::shared_ptr<Location> loc = createLocation(*config);
|
||||||
wireLocation(loc.get());
|
wireLocation(loc.get());
|
||||||
if (shadowMap) loc->shadowMap = shadowMap;
|
if (shadowMap) loc->shadowMap = shadowMap;
|
||||||
if (initializeLocation)
|
if (initializeLocation)
|
||||||
loc->scriptEngine.callLocationInitCallback();
|
loc->scriptEngine.callLocationInitCallback();
|
||||||
// Back to how the player left it (or to the start state's snapshot of it).
|
// 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) {
|
void Game::unloadLocation(bool storeState) {
|
||||||
internalGameServer.clearRuntimeLocations(storeState);
|
gameServerHost.clearRuntimeLocations(storeState);
|
||||||
// Textures only the location's models and characters used are freed now.
|
// Textures only the location's models and characters used are freed now.
|
||||||
renderer.textureManager.UnloadUnused();
|
renderer.textureManager.UnloadUnused();
|
||||||
}
|
}
|
||||||
@@ -639,8 +641,8 @@ namespace FRG
|
|||||||
// Rebuild the loaded location from its config; the others have no stored state
|
// 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.
|
// any more, so they are built fresh when the player first goes there.
|
||||||
std::vector<std::string> loadedIds =
|
std::vector<std::string> loadedIds =
|
||||||
internalGameServer.loadedRuntimeLocationIds();
|
gameServerHost.loadedRuntimeLocationIds();
|
||||||
internalGameServer.resetCampaign(
|
gameServerHost.resetCampaign(
|
||||||
activeCampaign().questsPath, CONST_ZIP_FILE);
|
activeCampaign().questsPath, CONST_ZIP_FILE);
|
||||||
unloadLocation(false);
|
unloadLocation(false);
|
||||||
for (const std::string& id : loadedIds) loadLocation(id);
|
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.
|
// from this freshly-rebuilt state after changing gameplay/content JSON or Lua.
|
||||||
// Re-comment before shipping.
|
// Re-comment before shipping.
|
||||||
//saveInitialState();
|
//saveInitialState();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Game::performResetToInitialState() {
|
void Game::performResetToInitialState() {
|
||||||
@@ -671,7 +674,7 @@ namespace FRG
|
|||||||
if (!content.empty()) {
|
if (!content.empty()) {
|
||||||
try {
|
try {
|
||||||
nlohmann::json root = nlohmann::json::parse(content);
|
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.
|
// the other locations' states for when they load.
|
||||||
return;
|
return;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
@@ -696,7 +699,7 @@ namespace FRG
|
|||||||
|
|
||||||
void Game::saveInitialState() {
|
void Game::saveInitialState() {
|
||||||
nlohmann::json root;
|
nlohmann::json root;
|
||||||
internalGameServer.save(root);
|
gameServerHost.save(root);
|
||||||
// Written next to the executable; copy it over the campaign's start-state file.
|
// Written next to the executable; copy it over the campaign's start-state file.
|
||||||
const std::string startStatePath = activeCampaign().startStatePath;
|
const std::string startStatePath = activeCampaign().startStatePath;
|
||||||
const std::string path = startStatePath.substr(startStatePath.find_last_of('/') + 1);
|
const std::string path = startStatePath.substr(startStatePath.find_last_of('/') + 1);
|
||||||
@@ -729,7 +732,7 @@ namespace FRG
|
|||||||
|
|
||||||
auto loc = std::make_shared<Location>(
|
auto loc = std::make_shared<Location>(
|
||||||
renderer, internalGameServer, config.id);
|
renderer, internalGameServer, config.id);
|
||||||
internalGameServer.setupRuntimeLocation(
|
gameServerHost.setupRuntimeLocation(
|
||||||
config.id, loc, std::move(params), audioPlayer.get());
|
config.id, loc, std::move(params), audioPlayer.get());
|
||||||
loc->requestNightDayTransition = [this](bool isNight, bool isDawn) {
|
loc->requestNightDayTransition = [this](bool isNight, bool isDawn) {
|
||||||
const auto state = stateSnapshot();
|
const auto state = stateSnapshot();
|
||||||
@@ -861,8 +864,8 @@ namespace FRG
|
|||||||
CheckGlError(__FILE__, __LINE__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
logger() << "Shadow map initialized" << std::endl;
|
logger() << "Shadow map initialized" << std::endl;
|
||||||
|
|
||||||
for (const std::string& id : internalGameServer.loadedRuntimeLocationIds())
|
for (const std::string& id : gameServerHost.loadedRuntimeLocationIds())
|
||||||
if (Location* loc = internalGameServer.runtimeLocation(id))
|
if (Location* loc = gameServerHost.runtimeLocation(id))
|
||||||
loc->shadowMap = shadowMap;
|
loc->shadowMap = shadowMap;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -873,8 +876,8 @@ namespace FRG
|
|||||||
shadowMap.reset();
|
shadowMap.reset();
|
||||||
CheckGlError(__FILE__, __LINE__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
|
|
||||||
for (const std::string& id : internalGameServer.loadedRuntimeLocationIds())
|
for (const std::string& id : gameServerHost.loadedRuntimeLocationIds())
|
||||||
if (Location* loc = internalGameServer.runtimeLocation(id))
|
if (Location* loc = gameServerHost.runtimeLocation(id))
|
||||||
loc->shadowMap.reset();
|
loc->shadowMap.reset();
|
||||||
CheckGlError(__FILE__, __LINE__);
|
CheckGlError(__FILE__, __LINE__);
|
||||||
|
|
||||||
@@ -1135,7 +1138,7 @@ namespace FRG
|
|||||||
{
|
{
|
||||||
// The authoritative implementation advances simulation; a future
|
// The authoritative implementation advances simulation; a future
|
||||||
// network client can use the same call to poll/apply server updates.
|
// network client can use the same call to poll/apply server updates.
|
||||||
internalGameServer.update(delta);
|
gameServerHost.update(delta);
|
||||||
processServerEvents();
|
processServerEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1882,7 +1885,7 @@ namespace FRG
|
|||||||
void Game::saveGame(int slot)
|
void Game::saveGame(int slot)
|
||||||
{
|
{
|
||||||
nlohmann::json root;
|
nlohmann::json root;
|
||||||
internalGameServer.save(root);
|
gameServerHost.save(root);
|
||||||
std::string path = "save_slot" + std::to_string(slot) + ".json";
|
std::string path = "save_slot" + std::to_string(slot) + ".json";
|
||||||
|
|
||||||
/*std::ofstream file(path);
|
/*std::ofstream file(path);
|
||||||
@@ -1922,7 +1925,7 @@ namespace FRG
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
enterLocationThen(campaignId, locationId, [this, root]() {
|
enterLocationThen(campaignId, locationId, [this, root]() {
|
||||||
internalGameServer.load(root);
|
gameServerHost.load(root);
|
||||||
playMusicForLoadedGame();
|
playMusicForLoadedGame();
|
||||||
menuManager.enterGameplay(true);
|
menuManager.enterGameplay(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "Location.h"
|
#include "Location.h"
|
||||||
#include "AudioPlayerAsync.h"
|
#include "AudioPlayerAsync.h"
|
||||||
#include "game_server/InternalGameServer.h"
|
#include "game_server/InternalGameServer.h"
|
||||||
|
#include "game_server/LocalGameServerHost.h"
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
struct LocationConfig;
|
struct LocationConfig;
|
||||||
@@ -83,6 +84,7 @@ namespace FRG {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
InternalGameServer internalGameServer;
|
InternalGameServer internalGameServer;
|
||||||
|
LocalGameServerHost gameServerHost;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
IGameServer& server() { return internalGameServer; }
|
IGameServer& server() { return internalGameServer; }
|
||||||
|
|||||||
+6
-3
@@ -1,4 +1,5 @@
|
|||||||
#include "GameState.h"
|
#include "GameState.h"
|
||||||
|
#include "utils/Utils.h"
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -209,8 +210,8 @@ void GameState::load(const nlohmann::json& in)
|
|||||||
|
|
||||||
// --- Players ---
|
// --- Players ---
|
||||||
const auto& playerValues = in.at("players");
|
const auto& playerValues = in.at("players");
|
||||||
if (!playerValues.is_object() || !playerValues.contains("player"))
|
if (!playerValues.is_object())
|
||||||
throw std::runtime_error("Game state requires players.player");
|
throw std::runtime_error("Game state players must be an object");
|
||||||
for (const auto& [id, value] : playerValues.items()) {
|
for (const auto& [id, value] : playerValues.items()) {
|
||||||
auto& player = players[id];
|
auto& player = players[id];
|
||||||
if (!player) player = std::make_shared<CharacterState>();
|
if (!player) player = std::make_shared<CharacterState>();
|
||||||
@@ -224,7 +225,9 @@ void GameState::load(const nlohmann::json& in)
|
|||||||
|
|
||||||
// --- Locations ---
|
// --- Locations ---
|
||||||
const auto& locationValues = in.at("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))
|
!locationValues.contains(currentLocationName))
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"Game state does not contain current location '" +
|
"Game state does not contain current location '" +
|
||||||
|
|||||||
+63
-13
@@ -320,6 +320,8 @@ namespace FRG
|
|||||||
void Location::setup(const LocationSetup& params, AudioPlayerAsync* audioPlayer)
|
void Location::setup(const LocationSetup& params, AudioPlayerAsync* audioPlayer)
|
||||||
{
|
{
|
||||||
if (!params.gameState) throw std::runtime_error("Location requires authoritative state binding");
|
if (!params.gameState) throw std::runtime_error("Location requires authoritative state binding");
|
||||||
|
if (!params.definition) throw std::runtime_error("Location requires prepared content definition");
|
||||||
|
const LocationDefinition& definition = *params.definition;
|
||||||
this->audioPlayer = audioPlayer;
|
this->audioPlayer = audioPlayer;
|
||||||
pathVisualization.initialize();
|
pathVisualization.initialize();
|
||||||
|
|
||||||
@@ -330,16 +332,18 @@ namespace FRG
|
|||||||
|
|
||||||
// Load static game objects
|
// 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);
|
gameObjects = std::move(staticObjects.single);
|
||||||
instancedBatches = std::move(staticObjects.batches);
|
instancedBatches = std::move(staticObjects.batches);
|
||||||
}
|
}
|
||||||
SDL_PumpEvents();
|
SDL_PumpEvents();
|
||||||
|
|
||||||
// Load interactive objects
|
// Load interactive objects
|
||||||
interactiveObjects = GameObjectLoader::loadAndCreateInteractiveObjects(
|
interactiveObjects = GameObjectLoader::createInteractiveObjects(
|
||||||
params.interactiveObjectsJsonPath, renderer, params.gameState->interactiveObjects,
|
definition.interactiveObjects, renderer,
|
||||||
CONST_ZIP_FILE);
|
params.gameState->interactiveObjects, CONST_ZIP_FILE);
|
||||||
SDL_PumpEvents();
|
SDL_PumpEvents();
|
||||||
|
|
||||||
auto sparkTexture = renderer.textureManager.LoadFromPng("resources/w/spark.png", CONST_ZIP_FILE);
|
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");
|
if (!params.player) throw std::runtime_error("Location requires game-level player binding");
|
||||||
player = params.player;
|
player = params.player;
|
||||||
player->bindToLocation(locationId_);
|
player->bindToLocation(locationId_);
|
||||||
updateCharacterState(*player, [¶ms](CharacterState& workingState) {
|
|
||||||
workingState.position = params.playerPosition;
|
|
||||||
});
|
|
||||||
setCharacterTarget(*player, params.playerPosition);
|
|
||||||
synchronizeRemotePlayers();
|
synchronizeRemotePlayers();
|
||||||
logger() << "Load resurces step 9" << std::endl;
|
logger() << "Load resurces step 9" << std::endl;
|
||||||
SDL_PumpEvents();
|
SDL_PumpEvents();
|
||||||
@@ -358,8 +358,9 @@ namespace FRG
|
|||||||
// Load NPCs from JSON. Aggressive (canAttack) NPCs need a player target
|
// Load NPCs from JSON. Aggressive (canAttack) NPCs need a player target
|
||||||
// and hit sparks; peaceful NPCs don't take damage from the player so
|
// and hit sparks; peaceful NPCs don't take damage from the player so
|
||||||
// they don't need either.
|
// they don't need either.
|
||||||
npcs = GameObjectLoader::loadAndCreate_Npcs(
|
npcs = GameObjectLoader::createNpcs(
|
||||||
params.npcsJsonPath, renderer, params.gameState->npcs, gameServer_, locationId_, CONST_ZIP_FILE);
|
definition.npcs, renderer, params.gameState->npcs,
|
||||||
|
gameServer_, locationId_, CONST_ZIP_FILE);
|
||||||
for (auto& npc : npcs) {
|
for (auto& npc : npcs) {
|
||||||
if (!npc) continue;
|
if (!npc) continue;
|
||||||
auto state = npc->stateSnapshot();
|
auto state = npc->stateSnapshot();
|
||||||
@@ -371,9 +372,58 @@ namespace FRG
|
|||||||
|
|
||||||
SDL_PumpEvents();
|
SDL_PumpEvents();
|
||||||
|
|
||||||
loadTeleportZones(params.teleportsJsonPath, CONST_ZIP_FILE);
|
auto activeTeleportTexture =
|
||||||
loadTriggerZones(params.triggerZonesJsonPath, CONST_ZIP_FILE);
|
renderer.textureManager.LoadFromPng(
|
||||||
loadPointLights(params.lightsJsonPath, CONST_ZIP_FILE);
|
"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();
|
SDL_PumpEvents();
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -53,6 +53,7 @@ namespace FRG
|
|||||||
|
|
||||||
struct LocationSetup
|
struct LocationSetup
|
||||||
{
|
{
|
||||||
|
std::shared_ptr<const LocationDefinition> definition;
|
||||||
std::string gameObjectsJsonPath;
|
std::string gameObjectsJsonPath;
|
||||||
std::string interactiveObjectsJsonPath;
|
std::string interactiveObjectsJsonPath;
|
||||||
std::string npcsJsonPath;
|
std::string npcsJsonPath;
|
||||||
@@ -225,7 +226,7 @@ namespace FRG
|
|||||||
friend class LocationEditor;
|
friend class LocationEditor;
|
||||||
friend class Game;
|
friend class Game;
|
||||||
friend class ScriptEngine;
|
friend class ScriptEngine;
|
||||||
friend class InternalGameServer;
|
friend class LocalGameServerHost;
|
||||||
Renderer& renderer;
|
Renderer& renderer;
|
||||||
AudioPlayerAsync* audioPlayer;
|
AudioPlayerAsync* audioPlayer;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,100 @@ constexpr float PI = 3.14159265358979323846f;
|
|||||||
constexpr float NPC_TALK_DISTANCE = 1.35f;
|
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(
|
CharacterState* LocationGameState::findCharacter(
|
||||||
const std::string& id, PlayerStateMap& players)
|
const std::string& id, PlayerStateMap& players)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "CharacterState.h"
|
#include "CharacterState.h"
|
||||||
#include "LocationState.h"
|
#include "LocationState.h"
|
||||||
#include "items/InteractiveObjectState.h"
|
#include "items/InteractiveObjectState.h"
|
||||||
|
#include "content/LocationDefinition.h"
|
||||||
#include "external/nlohmann/json.hpp"
|
#include "external/nlohmann/json.hpp"
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -57,7 +58,9 @@ struct LocationGameState {
|
|||||||
std::unordered_map<std::string, bool> teleportZones;
|
std::unordered_map<std::string, bool> teleportZones;
|
||||||
nlohmann::json dialogue = nlohmann::json::object();
|
nlohmann::json dialogue = nlohmann::json::object();
|
||||||
nlohmann::json scriptGlobals = 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);
|
CharacterState* findCharacter(const std::string& id, PlayerStateMap& players);
|
||||||
const CharacterState* findCharacter(
|
const CharacterState* findCharacter(
|
||||||
const std::string& id, const PlayerStateMap& players) const;
|
const std::string& id, const PlayerStateMap& players) const;
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
extern const char* CONST_ZIP_FILE;
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
nlohmann::json saveCar(const NpcCar& car)
|
nlohmann::json saveCar(const NpcCar& car)
|
||||||
{
|
{
|
||||||
@@ -51,7 +49,8 @@ namespace FRG {
|
|||||||
return navigationMaps[activeNavigationIndex].findPathToNearest(start, end, {});
|
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;
|
const int savedActiveNavigationIndex = activeNavigationIndex;
|
||||||
//navigationMapPaths = paths;
|
//navigationMapPaths = paths;
|
||||||
@@ -59,10 +58,7 @@ namespace FRG {
|
|||||||
navigationMaps.resize(paths.size());
|
navigationMaps.resize(paths.size());
|
||||||
|
|
||||||
for (size_t i = 0; i < paths.size(); ++i) {
|
for (size_t i = 0; i < paths.size(); ++i) {
|
||||||
// Service the OS message queue between builds so the window doesn't
|
navigationMaps[i].build(paths[i], zipFile);
|
||||||
// get flagged as "not responding" while loading many navigation grids.
|
|
||||||
SDL_PumpEvents();
|
|
||||||
navigationMaps[i].build(paths[i], CONST_ZIP_FILE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
activeNavigationIndex =
|
activeNavigationIndex =
|
||||||
|
|||||||
+5
-3
@@ -12,7 +12,9 @@ struct LocationState : public ISaveable {
|
|||||||
//It might be changed only in the editor mode during development.
|
//It might be changed only in the editor mode during development.
|
||||||
std::vector<PathFinder> navigationMaps;
|
std::vector<PathFinder> navigationMaps;
|
||||||
std::vector<Eigen::Vector3f> planPathExceptMyself(const Eigen::Vector3f& start, const Eigen::Vector3f& end, const std::string& entityId) const;
|
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 ----
|
// ---- Camera ----
|
||||||
float cameraAzimuth = -2.35f;
|
float cameraAzimuth = -2.35f;
|
||||||
@@ -29,8 +31,8 @@ struct LocationState : public ISaveable {
|
|||||||
NpcCar taxiCarInitialState;
|
NpcCar taxiCarInitialState;
|
||||||
NpcCar taxiCar;
|
NpcCar taxiCar;
|
||||||
bool taxiArrivedCallbackCalled = false;
|
bool taxiArrivedCallbackCalled = false;
|
||||||
Vector3f taxiCarTriggerPosition = Vector3f(0,0,0);
|
Eigen::Vector3f taxiCarTriggerPosition = Eigen::Vector3f(0,0,0);
|
||||||
std::vector<Vector3f> taxiCarWaypoints;
|
std::vector<Eigen::Vector3f> taxiCarWaypoints;
|
||||||
|
|
||||||
// (trigger/teleport zone runtime flags are serialised by id directly in Location::save/load)
|
// (trigger/teleport zone runtime flags are serialised by id directly in Location::save/load)
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "render/Renderer.h"
|
#include <Eigen/Core>
|
||||||
#include "Environment.h"
|
#include <cstddef>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
struct NpcCar
|
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 "InternalGameServer.h"
|
||||||
#include "../Campaign.h"
|
#include "../Campaign.h"
|
||||||
#include "../Location.h"
|
#include "../content/LocationDefinition.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
@@ -8,8 +8,6 @@
|
|||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
namespace {
|
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* kBattleZoneCampaignId = "battle_zone";
|
||||||
constexpr const char* kBattleZoneTestBotId = "battle_zone_test_bot";
|
constexpr const char* kBattleZoneTestBotId = "battle_zone_test_bot";
|
||||||
constexpr std::int64_t kBattleZoneTestBotIntervalMs = 5000;
|
constexpr std::int64_t kBattleZoneTestBotIntervalMs = 5000;
|
||||||
@@ -20,7 +18,7 @@ InternalGameServer::InternalGameServer() = default;
|
|||||||
void InternalGameServer::ensureBattleZoneTestBot()
|
void InternalGameServer::ensureBattleZoneTestBot()
|
||||||
{
|
{
|
||||||
if (state_.campaignId != kBattleZoneCampaignId) return;
|
if (state_.campaignId != kBattleZoneCampaignId) return;
|
||||||
if (!kEnableBattleZoneTestBot) {
|
if (!battleZoneTestBotEnabled_) {
|
||||||
state_.players.erase(kBattleZoneTestBotId);
|
state_.players.erase(kBattleZoneTestBotId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -33,7 +31,7 @@ void InternalGameServer::ensureBattleZoneTestBot()
|
|||||||
|
|
||||||
void InternalGameServer::updateBattleZoneTestBot(std::int64_t deltaMs)
|
void InternalGameServer::updateBattleZoneTestBot(std::int64_t deltaMs)
|
||||||
{
|
{
|
||||||
if (!kEnableBattleZoneTestBot ||
|
if (!battleZoneTestBotEnabled_ ||
|
||||||
state_.campaignId != kBattleZoneCampaignId ||
|
state_.campaignId != kBattleZoneCampaignId ||
|
||||||
state_.currentLocationName.empty() ||
|
state_.currentLocationName.empty() ||
|
||||||
state_.players.find(kBattleZoneTestBotId) == state_.players.end()) {
|
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& campaignId, const std::string& questsPath,
|
||||||
const std::string& zipFile)
|
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_.currentLocationName.clear();
|
||||||
state_.locations.clear();
|
state_.locations.clear();
|
||||||
state_.players.clear();
|
state_.players.clear();
|
||||||
state_.questJournal.loadFromFile(questsPath, zipFile);
|
state_.questJournal.loadFromFile(campaign.questsPath, zipFile);
|
||||||
state_.globalFloats["player_hp"] = 200.f;
|
state_.globalFloats["player_hp"] = 200.f;
|
||||||
|
|
||||||
|
if (createLocalPlayer) {
|
||||||
auto player = std::make_shared<CharacterState>();
|
auto player = std::make_shared<CharacterState>();
|
||||||
player->configureAsPlayer();
|
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));
|
state_.players.emplace(localPlayerId_, std::move(player));
|
||||||
|
}
|
||||||
battleZoneTestBotElapsedMs_ = 0;
|
battleZoneTestBotElapsedMs_ = 0;
|
||||||
ensureBattleZoneTestBot();
|
ensureBattleZoneTestBot();
|
||||||
publishMutation();
|
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()
|
void InternalGameServer::clearCampaign()
|
||||||
{
|
{
|
||||||
runtimeLocations_.clear();
|
|
||||||
state_.currentLocationName.clear();
|
state_.currentLocationName.clear();
|
||||||
state_.players.clear();
|
state_.players.clear();
|
||||||
state_.locations.clear();
|
state_.locations.clear();
|
||||||
@@ -117,76 +157,20 @@ void InternalGameServer::resetCampaign(
|
|||||||
bool InternalGameServer::prepareLocation(const LocationConfig& config)
|
bool InternalGameServer::prepareLocation(const LocationConfig& config)
|
||||||
{
|
{
|
||||||
LocationGameState& authoritative = state_.locations[config.id];
|
LocationGameState& authoritative = state_.locations[config.id];
|
||||||
const bool initializeLocation = !authoritative.hasPersistentState;
|
const bool initializeLocation = authoritative.setup(
|
||||||
if (!authoritative.location)
|
LocationContentLoader::load(config, resourceZipFile_),
|
||||||
authoritative.location = std::make_shared<LocationState>();
|
resourceZipFile_);
|
||||||
if (authoritative.location->navigationMaps.empty())
|
auto player = state_.players.find(localPlayerId_);
|
||||||
authoritative.location->initNavigation(config.navigationJsonPaths);
|
if (player != state_.players.end() && player->second) {
|
||||||
|
player->second->position = config.playerPosition;
|
||||||
authoritative.lockInteractiveObjectsDuringTutorial =
|
player->second->setTarget(
|
||||||
config.lockInteractiveObjectsDuringTutorial;
|
config.playerPosition, std::string(), *authoritative.location,
|
||||||
if (config.taxi) {
|
localPlayerId_);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
publishMutation();
|
publishMutation();
|
||||||
return initializeLocation;
|
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 InternalGameServer::buildSnapshot() const
|
||||||
{
|
{
|
||||||
GameSnapshot result;
|
GameSnapshot result;
|
||||||
@@ -472,11 +456,8 @@ void InternalGameServer::submit(const GameCommand& command)
|
|||||||
auto location = state_.locations.find(value.locationId);
|
auto location = state_.locations.find(value.locationId);
|
||||||
if (location != state_.locations.end()) {
|
if (location != state_.locations.end()) {
|
||||||
location->second.triggerZones[value.zoneId].enabled = value.enabled;
|
location->second.triggerZones[value.zoneId].enabled = value.enabled;
|
||||||
auto runtime = runtimeLocations_.find(value.locationId);
|
pendingTriggerZoneUpdates_.emplace_back(
|
||||||
if (runtime != runtimeLocations_.end() && runtime->second) {
|
value.locationId, value.zoneId);
|
||||||
for (auto& zone : runtime->second->triggerZones)
|
|
||||||
if (zone.id == value.zoneId) zone.enabled = value.enabled;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if constexpr (std::is_same_v<T, ReloadQuestDefinitionsCommand>) {
|
} else if constexpr (std::is_same_v<T, ReloadQuestDefinitionsCommand>) {
|
||||||
state_.questJournal.reloadDefinitionsPreservingState(value.path, value.zipFile);
|
state_.questJournal.reloadDefinitionsPreservingState(value.path, value.zipFile);
|
||||||
@@ -494,26 +475,22 @@ std::vector<GameServerEvent> InternalGameServer::drainEvents()
|
|||||||
|
|
||||||
void InternalGameServer::update(std::int64_t deltaMs)
|
void InternalGameServer::update(std::int64_t deltaMs)
|
||||||
{
|
{
|
||||||
auto runtime = runtimeLocations_.find(state_.currentLocationName);
|
pendingSimulationLocationId_.clear();
|
||||||
if (runtime == runtimeLocations_.end() || !runtime->second) return;
|
pendingSimulationEvents_ = {};
|
||||||
auto stored = state_.locations.find(state_.currentLocationName);
|
auto stored = state_.locations.find(state_.currentLocationName);
|
||||||
if (stored == state_.locations.end()) return;
|
if (stored == state_.locations.end()) return;
|
||||||
updateBattleZoneTestBot(deltaMs);
|
updateBattleZoneTestBot(deltaMs);
|
||||||
const bool interactiveObjectsLocked =
|
const bool interactiveObjectsLocked =
|
||||||
stored->second.lockInteractiveObjectsDuringTutorial &&
|
stored->second.lockInteractiveObjectsDuringTutorial &&
|
||||||
state_.tutorialStep < TutorialStep::PickUpItem;
|
state_.tutorialStep < TutorialStep::PickUpItem;
|
||||||
LocationSimulationEvents events =
|
pendingSimulationLocationId_ = state_.currentLocationName;
|
||||||
|
pendingSimulationEvents_ =
|
||||||
stored->second.update(deltaMs, state_.players, interactiveObjectsLocked);
|
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();
|
publishMutation();
|
||||||
runtime->second->update(deltaMs, events);
|
|
||||||
synchronizeRuntimeAuxiliaryState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InternalGameServer::save(nlohmann::json& out) const
|
void InternalGameServer::save(nlohmann::json& out) const
|
||||||
{
|
{
|
||||||
const_cast<InternalGameServer*>(this)->synchronizeRuntimeAuxiliaryState();
|
|
||||||
state_.save(out);
|
state_.save(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,40 +500,9 @@ void InternalGameServer::load(const nlohmann::json& in)
|
|||||||
state_.load(in);
|
state_.load(in);
|
||||||
battleZoneTestBotElapsedMs_ = 0;
|
battleZoneTestBotElapsedMs_ = 0;
|
||||||
ensureBattleZoneTestBot();
|
ensureBattleZoneTestBot();
|
||||||
applyStateToRuntime();
|
|
||||||
publishMutation();
|
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()
|
void InternalGameServer::publishMutation()
|
||||||
{
|
{
|
||||||
++revision_;
|
++revision_;
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "IGameServer.h"
|
#include "IGameServer.h"
|
||||||
|
#include "IGameServerLifecycle.h"
|
||||||
class AudioPlayerAsync;
|
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
class Game;
|
class LocalGameServerHost;
|
||||||
class Character;
|
|
||||||
class Location;
|
|
||||||
struct LocationConfig;
|
struct LocationConfig;
|
||||||
struct LocationSetup;
|
|
||||||
|
|
||||||
// In-process implementation used by single-player. GameState exists exactly
|
// In-process implementation used by single-player. GameState exists exactly
|
||||||
// once, here; clients see immutable snapshots and mutate it with typed commands.
|
// 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:
|
public:
|
||||||
InternalGameServer();
|
InternalGameServer();
|
||||||
|
|
||||||
@@ -25,9 +22,14 @@ public:
|
|||||||
void update(std::int64_t deltaMs) override;
|
void update(std::int64_t deltaMs) override;
|
||||||
void save(nlohmann::json& out) const override;
|
void save(nlohmann::json& out) const override;
|
||||||
void load(const nlohmann::json& in) 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:
|
private:
|
||||||
friend class Game;
|
friend class LocalGameServerHost;
|
||||||
|
|
||||||
// Host-only lifecycle operations. They never expose authoritative state and
|
// Host-only lifecycle operations. They never expose authoritative state and
|
||||||
// are intentionally absent from IGameServer.
|
// are intentionally absent from IGameServer.
|
||||||
@@ -36,17 +38,6 @@ private:
|
|||||||
const std::string& zipFile);
|
const std::string& zipFile);
|
||||||
void clearCampaign();
|
void clearCampaign();
|
||||||
void resetCampaign(const std::string& questsPath, const std::string& zipFile);
|
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 publishMutation();
|
||||||
void ensureBattleZoneTestBot();
|
void ensureBattleZoneTestBot();
|
||||||
void updateBattleZoneTestBot(std::int64_t deltaMs);
|
void updateBattleZoneTestBot(std::int64_t deltaMs);
|
||||||
@@ -55,8 +46,12 @@ private:
|
|||||||
|
|
||||||
GameState state_;
|
GameState state_;
|
||||||
std::string localPlayerId_ = "player";
|
std::string localPlayerId_ = "player";
|
||||||
|
std::string resourceZipFile_;
|
||||||
std::int64_t battleZoneTestBotElapsedMs_ = 0;
|
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;
|
std::uint64_t revision_ = 1;
|
||||||
mutable std::uint64_t cachedRevision_ = 0;
|
mutable std::uint64_t cachedRevision_ = 0;
|
||||||
mutable std::shared_ptr<const GameSnapshot> cachedSnapshot_;
|
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.castShadowNight = item.value("castShadowNight", true);
|
||||||
data.isActive = item.value("isActive", true);
|
data.isActive = item.value("isActive", true);
|
||||||
data.activateFunctionName = item.value("activateFunction", "");
|
data.activateFunctionName = item.value("activateFunction", "");
|
||||||
data.pivotX = item.value("pivotX", 0.0f);
|
data.pivot = {
|
||||||
data.pivotY = item.value("pivotY", 0.0f);
|
item.value("pivotX", 0.0f), item.value("pivotY", 0.0f),
|
||||||
data.pivotZ = item.value("pivotZ", 0.0f);
|
item.value("pivotZ", 0.0f)};
|
||||||
data.boundsMinX = item.value("boundsMinX", 0.0f);
|
data.boundsMin = {
|
||||||
data.boundsMinY = item.value("boundsMinY", 0.0f);
|
item.value("boundsMinX", 0.0f), item.value("boundsMinY", 0.0f),
|
||||||
data.boundsMinZ = item.value("boundsMinZ", 0.0f);
|
item.value("boundsMinZ", 0.0f)};
|
||||||
data.boundsMaxX = item.value("boundsMaxX", 0.0f);
|
data.boundsMax = {
|
||||||
data.boundsMaxY = item.value("boundsMaxY", 0.0f);
|
item.value("boundsMaxX", 0.0f), item.value("boundsMaxY", 0.0f),
|
||||||
data.boundsMaxZ = item.value("boundsMaxZ", 0.0f);
|
item.value("boundsMaxZ", 0.0f)};
|
||||||
if (item.contains("interactionPositionX")) {
|
if (item.contains("interactionPositionX")) {
|
||||||
data.hasInteractionPosition = true;
|
data.hasInteractionPosition = true;
|
||||||
data.interactionPositionX = item.value("interactionPositionX", 0.0f);
|
data.interactionPosition = {
|
||||||
data.interactionPositionY = item.value("interactionPositionY", 0.0f);
|
item.value("interactionPositionX", 0.0f),
|
||||||
data.interactionPositionZ = item.value("interactionPositionZ", 0.0f);
|
item.value("interactionPositionY", 0.0f),
|
||||||
|
item.value("interactionPositionZ", 0.0f)};
|
||||||
}
|
}
|
||||||
if (!data.base.meshPath.empty())
|
if (!data.base.meshPath.empty())
|
||||||
objects.push_back(std::move(data));
|
objects.push_back(std::move(data));
|
||||||
@@ -125,40 +126,6 @@ namespace FRG {
|
|||||||
return objects;
|
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)
|
static VertexDataStruct loadMeshData(const std::string& meshPath, const std::string& zipPath)
|
||||||
{
|
{
|
||||||
if (meshPath.size() > 4 && meshPath.compare(meshPath.size() - 4, 4, ".bin") == 0)
|
if (meshPath.size() > 4 && meshPath.compare(meshPath.size() - 4, 4, ".bin") == 0)
|
||||||
@@ -213,20 +180,18 @@ namespace FRG {
|
|||||||
// Public API
|
// Public API
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
StaticObjectSet GameObjectLoader::loadAndCreateGameObjects(
|
StaticObjectSet GameObjectLoader::createGameObjects(
|
||||||
const std::string& jsonPath,
|
const std::vector<GameObjectData>& definitions,
|
||||||
Renderer& renderer,
|
Renderer& renderer,
|
||||||
const std::string& zipPath)
|
const std::string& zipPath)
|
||||||
{
|
{
|
||||||
StaticObjectSet result;
|
StaticObjectSet result;
|
||||||
|
|
||||||
std::vector<GameObjectData> allData = loadFromJson(jsonPath, zipPath);
|
|
||||||
|
|
||||||
// Group objects sharing mesh + textures (in file order) so they can be drawn instanced.
|
// Group objects sharing mesh + textures (in file order) so they can be drawn instanced.
|
||||||
std::vector<std::vector<const GameObjectData*>> groups;
|
std::vector<std::vector<const GameObjectData*>> groups;
|
||||||
std::unordered_map<std::string, size_t> groupIndexByKey;
|
std::unordered_map<std::string, size_t> groupIndexByKey;
|
||||||
|
|
||||||
for (const auto& data : allData) {
|
for (const auto& data : definitions) {
|
||||||
if (!data.instanced) {
|
if (!data.instanced) {
|
||||||
groups.push_back({ &data });
|
groups.push_back({ &data });
|
||||||
continue;
|
continue;
|
||||||
@@ -294,45 +259,32 @@ namespace FRG {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<InteractiveObject> GameObjectLoader::loadAndCreateInteractiveObjects(
|
StaticObjectSet GameObjectLoader::loadAndCreateGameObjects(
|
||||||
const std::string& jsonPath,
|
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,
|
Renderer& renderer,
|
||||||
std::unordered_map<std::string, std::shared_ptr<InteractiveObjectState>>& states,
|
std::unordered_map<std::string, std::shared_ptr<InteractiveObjectState>>& states,
|
||||||
const std::string& zipPath)
|
const std::string& zipPath)
|
||||||
{
|
{
|
||||||
std::vector<InteractiveObject> interactiveObjects;
|
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) {
|
for (const auto& data : definitions) {
|
||||||
SDL_PumpEvents();
|
SDL_PumpEvents();
|
||||||
logger() << "Loading interactive object: " << data.base.name << std::endl;
|
logger() << "Loading interactive object: " << data.base.name << std::endl;
|
||||||
try {
|
try {
|
||||||
InteractiveObjectState definition = data.toInteractiveObjectState();
|
auto state = states.find(data.id);
|
||||||
auto& state = states[data.id];
|
if (state == states.end() || !state->second)
|
||||||
if (!state) state = std::make_shared<InteractiveObjectState>(definition);
|
throw std::runtime_error(
|
||||||
else {
|
"Authoritative state missing for '" + data.id + "'");
|
||||||
nlohmann::json runtime;
|
|
||||||
state->save(runtime);
|
|
||||||
*state = std::move(definition);
|
|
||||||
state->load(runtime);
|
|
||||||
}
|
|
||||||
InteractiveObject intObj =
|
InteractiveObject intObj =
|
||||||
InteractiveObject::createFromState(*state, renderer, zipPath);
|
InteractiveObject::createFromState(
|
||||||
|
*state->second, renderer, zipPath);
|
||||||
intObj.id = data.id;
|
intObj.id = data.id;
|
||||||
intObj.loadedObject.name = data.base.name;
|
intObj.loadedObject.name = data.base.name;
|
||||||
|
|
||||||
@@ -352,6 +304,18 @@ namespace FRG {
|
|||||||
return interactiveObjects;
|
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> GameObjectLoader::loadNpcsFromJson(const std::string& jsonPath, const std::string& zipPath)
|
||||||
{
|
{
|
||||||
std::vector<NpcData> npcs;
|
std::vector<NpcData> npcs;
|
||||||
@@ -412,96 +376,29 @@ namespace FRG {
|
|||||||
return npcs;
|
return npcs;
|
||||||
}
|
}
|
||||||
|
|
||||||
CharacterState NpcData::toCharacterState() const {
|
std::vector<std::unique_ptr<Character>> GameObjectLoader::createNpcs(
|
||||||
CharacterState st;
|
const std::vector<NpcData>& definitions,
|
||||||
|
|
||||||
// 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,
|
|
||||||
Renderer& renderer,
|
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,
|
IGameServer& server,
|
||||||
const std::string& locationId,
|
const std::string& locationId,
|
||||||
const std::string& zipPath)
|
const std::string& zipPath)
|
||||||
{
|
{
|
||||||
std::vector<std::unique_ptr<Character>> npcs;
|
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) {
|
for (const auto& npcData : definitions) {
|
||||||
SDL_PumpEvents();
|
SDL_PumpEvents();
|
||||||
logger() << "Loading NPC: " << npcData.name << std::endl;
|
logger() << "Loading NPC: " << npcData.name << std::endl;
|
||||||
try {
|
try {
|
||||||
CharacterState definition = npcData.toCharacterState();
|
auto state = states.find(npcData.id);
|
||||||
auto& state = states[npcData.id];
|
if (state == states.end() || !state->second)
|
||||||
if (!state) state = std::make_shared<CharacterState>(definition);
|
throw std::runtime_error(
|
||||||
else {
|
"Authoritative state missing for '" +
|
||||||
nlohmann::json runtime;
|
npcData.id + "'");
|
||||||
state->save(runtime);
|
|
||||||
*state = std::move(definition);
|
|
||||||
state->load(runtime);
|
|
||||||
}
|
|
||||||
auto npc = Character::createFromState(
|
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) {
|
if (npc) {
|
||||||
logger() << "Successfully loaded NPC: " << npcData.name << " at ("
|
logger() << "Successfully loaded NPC: " << npcData.name << " at ("
|
||||||
<< npcData.positionX << ", " << npcData.positionY << ", " << npcData.positionZ << ")" << std::endl;
|
<< npcData.positionX << ", " << npcData.positionY << ", " << npcData.positionZ << ")" << std::endl;
|
||||||
@@ -515,4 +412,17 @@ namespace FRG {
|
|||||||
return npcs;
|
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
|
} // namespace FRG
|
||||||
@@ -9,31 +9,13 @@
|
|||||||
#include "InteractiveObject.h"
|
#include "InteractiveObject.h"
|
||||||
#include "InteractiveObjectState.h"
|
#include "InteractiveObjectState.h"
|
||||||
#include "../CharacterState.h"
|
#include "../CharacterState.h"
|
||||||
|
#include "../content/LocationDefinition.h"
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
class IGameServer;
|
class IGameServer;
|
||||||
|
|
||||||
class Character;
|
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,
|
// Result of loading a static object list: unique objects keep their own mesh,
|
||||||
// repeated ones are merged into instanced batches.
|
// repeated ones are merged into instanced batches.
|
||||||
struct StaticObjectSet {
|
struct StaticObjectSet {
|
||||||
@@ -41,63 +23,25 @@ namespace FRG {
|
|||||||
std::vector<InstancedBatch> batches;
|
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 {
|
class GameObjectLoader {
|
||||||
public:
|
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(
|
static StaticObjectSet loadAndCreateGameObjects(
|
||||||
const std::string& jsonPath,
|
const std::string& jsonPath,
|
||||||
Renderer& renderer,
|
Renderer& renderer,
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ namespace FRG {
|
|||||||
|
|
||||||
obj.loadedObject.mesh.RefreshVBO();
|
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()) {
|
if (!obj.loadedObject.mesh.data.PositionData.empty()) {
|
||||||
Eigen::Vector3f meshMin = obj.loadedObject.mesh.data.PositionData[0];
|
Eigen::Vector3f meshMin = obj.loadedObject.mesh.data.PositionData[0];
|
||||||
Eigen::Vector3f meshMax = 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;
|
const Eigen::Vector3f meshCenter = (meshMin + meshMax) * 0.5f;
|
||||||
obj.loadedObject.mesh.data.Move(-meshCenter);
|
obj.loadedObject.mesh.data.Move(-meshCenter);
|
||||||
obj.loadedObject.mesh.RefreshVBO();
|
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;
|
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 {
|
void InteractiveObject::drawDarklands(Renderer& renderer) const {
|
||||||
if (!state.isActive || !loadedObject.textureDarklands) return;
|
if (!state.isActive || !loadedObject.textureDarklands) return;
|
||||||
|
|
||||||
@@ -237,65 +120,4 @@ namespace FRG {
|
|||||||
renderer.RenderUniform1f("uAlpha", 1.0f);
|
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
|
} // namespace FRG
|
||||||
@@ -23,8 +23,7 @@ namespace FRG {
|
|||||||
LoadedGameObject loadedObject; // rendering assets
|
LoadedGameObject loadedObject; // rendering assets
|
||||||
|
|
||||||
// Factory: creates a fully-loaded InteractiveObject from a state descriptor.
|
// Factory: creates a fully-loaded InteractiveObject from a state descriptor.
|
||||||
// Loads textures and mesh from state.objectInfo, computes mesh centering,
|
// Loads textures and centers the mesh around the state position.
|
||||||
// and sets state.position = jsonPosition + meshCenter.
|
|
||||||
static InteractiveObject createFromState(InteractiveObjectState& state,
|
static InteractiveObject createFromState(InteractiveObjectState& state,
|
||||||
Renderer& renderer,
|
Renderer& renderer,
|
||||||
const std::string& zipPath = "");
|
const std::string& zipPath = "");
|
||||||
@@ -38,7 +37,6 @@ namespace FRG {
|
|||||||
friend class Location;
|
friend class Location;
|
||||||
friend class LocationEditor;
|
friend class LocationEditor;
|
||||||
friend class ScriptEngine;
|
friend class ScriptEngine;
|
||||||
friend class InternalGameServer;
|
|
||||||
InteractiveObjectState& state; // non-owning binding to LocationGameState
|
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 "Item.h"
|
||||||
#include "ItemRegistry.h"
|
#include "ItemRegistry.h"
|
||||||
#include "external/nlohmann/json.hpp"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <iostream>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include "../utils/Utils.h"
|
|
||||||
|
|
||||||
namespace FRG {
|
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> Inventory::getItems() const {
|
||||||
std::vector<Item> result;
|
std::vector<Item> result;
|
||||||
result.reserve(itemIds.size());
|
result.reserve(itemIds.size());
|
||||||
@@ -38,18 +13,4 @@ namespace FRG {
|
|||||||
return result;
|
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
|
} // namespace FRG
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "render/Renderer.h"
|
|
||||||
#include <Eigen/Core>
|
#include <Eigen/Core>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#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