More work on states, added bot

This commit is contained in:
Vladislav Khorev committed 2026-10-29 22:37:30 +06:00
1 parent 66e93d46f4
commit 5e9a663812
25 files changed
+1595 -740

No files matched your search

+85 -31
View File
@@ -12,7 +12,7 @@ The architecture has four layers:
1. `GameState` is the aggregate root for persistent, authoritative state.
2. `InternalGameServer` owns `GameState`, advances the local simulation, handles commands, and produces immutable snapshots.
3. Runtime objects such as `Location`, `Character`, and `InteractiveObject` hold rendering resources and non-owning bindings to server-owned state. `Location` also translates local input, scripts, and collision results into semantic server commands.
3. Runtime objects hold rendering resources. `Character` and `Location` read immutable server snapshots by stable ID, while `InteractiveObject` still uses a transitional non-owning binding to server-owned state. `Location` also translates local input, scripts, and collision results into semantic server commands.
4. Client-facing systems such as `MenuManager` read immutable snapshots and submit typed commands through `IGameServer`.
```text
@@ -47,6 +47,8 @@ IGameServer ---- immutable GameSnapshot ----> UI and other clients
- Inventory, money, player-health HUD values, quests, tutorial progress, chat data, taxi state, and script-global values.
Live `Location` pointers and rendering objects are no longer stored in `GameState`.
Embedded state models such as `Inventory` and `QuestJournal` contain only value
data and deterministic operations; they do not retain runtime callbacks.
### Campaign-level players
@@ -54,6 +56,11 @@ Player `CharacterState` objects are stored in `GameState::players`. The current
The player state is no longer owned by a `Character` or duplicated as a separately authoritative state for each loaded location. This permits additional player IDs to be introduced later without changing the aggregate ownership model.
Every player, including the one controlled by the current client, remains in this
same authoritative collection. `IGameServer::localPlayerId()` identifies which
entry is local to a particular client connection; it is not a separate state
object.
`LocationGameState` does not retain a player pointer or back-reference to `GameState`.
Its update boundary receives `GameState::players` by reference, allowing character
lookup and simulation to treat every campaign player by stable ID.
@@ -70,7 +77,12 @@ Each location record owns:
- Serialized dialogue runtime state.
- Serialized Lua runtime globals.
State objects use `shared_ptr` internally to keep their addresses stable while local runtime objects are bound to them. These pointers never appear in public snapshots.
World-mode flags (`darklands`, `night`, and `dawn`) exist only at the aggregate
`GameState` level. Location rendering and scripts read those values from the
top-level snapshot rather than maintaining per-location mirrors.
State objects use `shared_ptr` internally to keep addresses stable for the
remaining local runtime bindings. These pointers never appear in public snapshots.
### Stable identity
@@ -121,6 +133,7 @@ Snapshots have a monotonically increasing revision. `InternalGameServer` caches
- Chat history and unread state.
- Taxi state.
- Inventory additions and removals.
- Quest status, objective completion/visibility, and active-objective changes.
- Script globals.
- Character transforms, attack targets, navigation destinations, stopping,
facing, replanning, health, damage, death reset, attack combos, and animation
@@ -131,6 +144,14 @@ Snapshots have a monotonically increasing revision. `InternalGameServer` caches
A future network implementation can serialize equivalent requests instead of mutating local state.
### Presentation events
`IGameServer::drainEvents()` returns value-only events produced by successful
authoritative mutations. Inventory add/remove and quest unlock/complete/fail/
objective-complete events contain stable IDs only. `Game` drains these events and
performs local tutorial, toast, and audio presentation through `MenuManager`.
Loading and persistence restoration clear pending events and remain silent.
## Internal Server
`InternalGameServer` is the current `IGameServer` implementation. It:
@@ -143,10 +164,10 @@ A future network implementation can serialize equivalent requests instead of mut
- Builds and caches immutable snapshots.
- Delegates save/load serialization to the aggregate state model.
`Game` remains a privileged host during this migration. It can access private
internal-server operations for campaign loading, location construction, callback
wiring, and persistence restoration. These host operations are not exposed by
`IGameServer`.
`Game` remains a privileged lifecycle host, but it no longer holds or receives a
mutable `GameState` reference. It uses private, focused internal-server operations
for campaign loading and location construction. Those operations publish revisions
themselves and are not exposed by `IGameServer`.
### Animation and combat
@@ -180,16 +201,25 @@ collision handling, and presentation-event processing.
## Runtime and Visual Object Bindings
`Location`, `Character`, and `InteractiveObject` no longer own their authoritative state objects.
Runtime visual objects do not own their authoritative state objects.
- `Location` keeps a private non-owning reference to its `LocationState` and a stable location ID.
- `Character` keeps a private non-owning **const** reference named `characterState`, plus stable entity and location IDs.
- `Location` keeps only its `IGameServer` reference and stable location ID. It resolves its immutable `LocationState` view from `IGameServer::snapshot()`.
- `Character` keeps only its `IGameServer` reference plus stable entity and location IDs. It resolves the player-first character view from `IGameServer::snapshot()`.
- `InteractiveObject` keeps a private non-owning reference to its `InteractiveObjectState`; its configured object name is its stable ID.
The bound state objects are owned by `GameState` or `LocationGameState`. The internal
server advances them directly before runtime presentation. Rendering reads the bound
state; runtime input, scripts, and collision handling submit semantic commands that
invoke `CharacterState` operations against the authoritative `LocationState`.
`Character::stateSnapshot()` and `Location::stateSnapshot()` return aliasing
`shared_ptr` views. Each pointer addresses a state copy inside a `GameSnapshot`,
while shared ownership keeps that whole immutable snapshot alive. Update, input,
editor, and draw operations capture one such pointer and use it consistently
throughout the operation. A missing or unloaded entity produces an empty pointer
and presentation or input work is skipped.
The authoritative state objects are owned by `GameState` or `LocationGameState`.
The internal server advances them before publishing a new snapshot revision.
Runtime input, scripts, and collision handling submit semantic commands that invoke
authoritative character and location operations. Location commands cover camera
orientation, pending interaction targets, navigation switching and area changes,
editor obstacles, and taxi reset. No whole-location commit command is exposed.
Navigation target selection, replanning, stopping, facing, death reset, health
assignment, and attack-combo clock changes are implemented by `CharacterState`.
@@ -203,12 +233,19 @@ Arrival behavior is represented solely by the serialized Lua function name in
`CharacterState`; `Location` resolves and invokes that name through `ScriptEngine`
when it consumes the arrival event.
`Location` owns presentation-only runtime characters for all non-local entries in
`GameSnapshot::players`. It reconciles them by stable ID as snapshots change,
reuses the local-player assets and animation set, and includes them in all render,
shadow, lighting, animation, damage, and name-tag passes. The local entry is
excluded using `IGameServer::localPlayerId()` because its visual is already owned
by `Game`.
`GameObjectLoader` now:
1. Loads static creation data from location configuration.
2. Creates or reuses server-owned state objects.
3. Applies saved runtime fields over the configuration defaults.
4. Constructs visual objects bound to those state objects.
4. Constructs visual objects using stable IDs and state-backed creation data.
Configuration remains the source of visual assets, animation paths, meshes, textures, and fixed tuning data.
@@ -225,11 +262,14 @@ The normal frame flow is:
back as semantic destination commands, whose server handler calls
`CharacterState::setTarget()` with the authoritative `LocationState`; arrival
function names are resolved through `ScriptEngine`.
6. Runtime characters apply the authoritative animation frame to loaded models and
6. The server invalidates its cached snapshot and advances the revision so runtime
presentation can observe the completed simulation tick.
7. Runtime characters read one immutable snapshot per operation, apply the
authoritative animation frame to loaded models, and
play audio, particles, and presentation callbacks.
7. Auxiliary dialogue, Lua, trigger, and teleport state is captured into `LocationGameState`.
8. The server increments its revision and invalidates the cached snapshot.
9. Rendering reads the bound local state; UI and external-style consumers read an immutable snapshot.
8. Auxiliary dialogue, Lua, trigger, and teleport state is captured into `LocationGameState`.
9. The server publishes those auxiliary changes in a later revision.
10. Rendering, UI, and external-style consumers read immutable snapshots.
Commands that update bound character or object state do not reload the whole location. This avoids replaying dialogue-node effects or Lua callbacks.
@@ -244,7 +284,7 @@ When a campaign starts:
- Existing campaign state and runtime locations are cleared.
- Quest definitions are loaded.
- A campaign-level player state is created under the `"player"` ID.
- The visual player is created and bound to that state.
- The visual player is created with its stable ID and resolves that state through snapshots.
### Location loading
@@ -254,6 +294,8 @@ When a location is loaded:
- Static configuration is loaded.
- NPC and interactive-object visuals are created against state owned by the location record.
- The shared campaign player is attached.
- Runtime visuals are created for other player IDs currently present in the
snapshot and are subsequently spawned or removed as snapshots change.
- Dialogue, Lua, navigation, trigger, teleport, and presentation callbacks are initialized.
- The runtime `Location` is registered privately with `InternalGameServer`.
@@ -278,8 +320,8 @@ unsupported versions are rejected rather than migrated.
- Pending interaction targets and character references use stable IDs.
- Immutable content such as meshes, initial taxi configuration, and object
definitions is reconstructed from campaign configuration.
- Configuration is loaded first and mutable saved state is overlaid in place, so
runtime render bindings keep stable addresses.
- Configuration is loaded first and mutable saved state is overlaid in place.
Snapshot-backed runtime objects then resolve the resulting state by stable ID.
Old user save slots are intentionally unsupported. Bundled start-state snapshots
must be regenerated with `Game::saveInitialState()` whenever the schema changes.
@@ -298,11 +340,14 @@ It now:
### Scripts
Lua remains an in-process single-player runtime. Character bindings submit semantic
commands through `Location`; the server invokes `CharacterState` operations on the
centrally owned state. NPC, interactive-object, trigger-zone, and teleport-zone
bindings address entities by configured ID. Object, inventory, quest, and
global-state bindings remain trusted local integrations.
Lua remains an in-process single-player runtime. Character, inventory, quest, and
campaign-global bindings submit semantic commands through `Location`; the server
invokes operations on centrally owned state. Snapshot queries provide global
values, inventory counts, item presence, and synchronous quest validation for the
existing Lua API. NPC,
interactive-object, trigger-zone, and teleport-zone bindings address entities by
configured ID. Other object and global-state bindings remain trusted local
integrations.
Each location script registers initialization, enter, and exit callbacks.
Initialization runs only when the location has no retained state; loading the Lua
@@ -313,13 +358,18 @@ The scalar Lua runtime state for each location is stored under `LocationGameStat
### Dialogue and cutscenes
Dialogue runtime state is captured in each `LocationGameState`. Dialogue and cutscene rendering remain local presentation systems.
Dialogue runtime state is captured in each `LocationGameState`. Dialogue global
flags use snapshot getter and command setter callbacks rather than retaining a
mutable map pointer. Dialogue and cutscene rendering remain local presentation
systems.
Dialogues and cutscenes are outside the planned multiplayer gameplay mode, but their existing single-player save and callback behavior is retained.
### Quests, inventory, and tutorial
These systems remain part of `GameState` for single-player compatibility. UI access now crosses the server interface. They can remain empty or disabled in multiplayer mode as planned.
These callback-free state systems remain part of `GameState` for single-player
compatibility. UI access, mutations, and presentation notifications cross the
server interface. They can remain empty or disabled in multiplayer mode as planned.
## Multiplayer Readiness
@@ -372,7 +422,11 @@ cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -
location-local NPC ID).
- Some top-level values remain mirrors for compatibility, notably player health used by the HUD and scripts.
- Dialogue and Lua are synchronized as auxiliary location state rather than fully networked subsystems.
- `Game` currently uses privileged internal-server APIs for local content construction. A remote client implementation will need a client-side runtime-location factory driven by configuration and snapshots.
- `Game` uses focused privileged internal-server APIs for local content
construction, but cannot access the aggregate or its state maps directly. The
private location-construction path still supplies `LocationGameState` to the
local object loader. A remote client implementation will need a client-side
runtime-location factory driven by configuration and snapshots.
## Main Files
@@ -383,8 +437,8 @@ cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -
- `src/Game.h` / `src/Game.cpp` — application host, campaign lifecycle, and runtime wiring.
- `src/Location.h` / `src/Location.cpp` — loaded location runtime, command routing,
interactions, collisions, scripts, and presentation-event consumption.
- `src/Character.h` / `src/Character.cpp` — passive character presentation bound to
a const authoritative `CharacterState`.
- `src/Character.h` / `src/Character.cpp` — passive character presentation resolving
immutable `CharacterState` snapshots by stable ID.
- `src/CharacterState.h` / `src/CharacterState.cpp` — deterministic character
simulation, navigation operations, combat state, and logical animation clock.
- `src/items/InteractiveObject.h` / `.cpp` — visual object runtime bound to central object state.
+235 -138
View File
@@ -1,4 +1,4 @@
#include "Character.h"
#include "Character.h"
#include "render/TextRenderer.h"
#include <cmath>
#include <iostream>
@@ -19,6 +19,44 @@ namespace FRG {
Dialogue::TranslationDatabase Character::nameTranslations;
std::shared_ptr<const CharacterState> Character::stateSnapshot() const {
auto snapshot = gameServer_.snapshot();
if (!snapshot) return {};
auto player = snapshot->players.find(entityId_);
if (player != snapshot->players.end()) {
return std::shared_ptr<const CharacterState>(
std::move(snapshot), &player->second.state);
}
auto location = snapshot->locations.find(locationId_);
if (location == snapshot->locations.end()) return {};
auto npc = location->second.npcs.find(entityId_);
if (npc == location->second.npcs.end()) return {};
return std::shared_ptr<const CharacterState>(
std::move(snapshot), &npc->second.state);
}
bool Character::isMoving() const {
auto state = stateSnapshot();
return state && state->isMoving();
}
Eigen::Vector3f Character::getRequestedWalkTarget() const {
auto state = stateSnapshot();
return state ? state->getRequestedWalkTarget() : Eigen::Vector3f::Zero();
}
Eigen::Vector3f Character::getCurrentNavigationTarget() const {
auto state = stateSnapshot();
return state ? state->getCurrentNavigationTarget() : Eigen::Vector3f::Zero();
}
float Character::getHp() const {
auto state = stateSnapshot();
return state ? state->getHp() : 0.f;
}
void Character::loadAnimation(AnimationState animState, const std::string& filename, const std::string& zipFile) {
auto& data = animations[animState];
data.model.LoadFromFile(filename, zipFile);
@@ -46,7 +84,7 @@ void Character::loadBinaryAnimation(AnimationState animState, const std::string&
std::unique_ptr<Character> Character::createFromState(const CharacterState& st, IGameServer& server,
const std::string& locationId, const std::string& entityId,
Renderer& renderer, const char* zipFile) {
auto c = std::make_unique<Character>(st, server, locationId, entityId);
auto c = std::make_unique<Character>(server, locationId, entityId);
const std::string zip = zipFile ? zipFile : "";
@@ -62,7 +100,7 @@ std::unique_ptr<Character> Character::createFromState(const CharacterState& st,
}
};
for (const auto& [animState, path] : c->characterState.creationInfo.animationPaths)
for (const auto& [animState, path] : st.creationInfo.animationPaths)
loadAnim(animState, path);
auto loadTex = [&](const std::string& path) -> std::shared_ptr<Texture> {
@@ -76,27 +114,27 @@ std::unique_ptr<Character> Character::createFromState(const CharacterState& st,
}
};
if (!c->characterState.creationInfo.singleTexturePath.empty()) {
auto tex = loadTex(c->characterState.creationInfo.singleTexturePath);
if (!st.creationInfo.singleTexturePath.empty()) {
auto tex = loadTex(st.creationInfo.singleTexturePath);
if (tex) c->setTexture(tex);
}
for (const auto& [meshName, texPath] : c->characterState.creationInfo.meshTexturePaths) {
for (const auto& [meshName, texPath] : st.creationInfo.meshTexturePaths) {
auto tex = loadTex(texPath);
if (tex) c->setTexture(meshName, tex);
}
if (!c->characterState.creationInfo.weaponMeshPath.empty()) {
const std::string& wp = c->characterState.creationInfo.weaponMeshPath;
if (!st.creationInfo.weaponMeshPath.empty()) {
const std::string& wp = st.creationInfo.weaponMeshPath;
try {
if (wp.size() >= 4 && wp.substr(wp.size() - 4) == ".bin")
c->weaponMesh.data = LoadModelFromBinFile(wp, zip);
else
c->weaponMesh.data = LoadFromTextFile02(wp, zip);
if (c->characterState.creationInfo.weaponMeshMove != Eigen::Vector3f::Zero())
c->weaponMesh.data.Move(c->characterState.creationInfo.weaponMeshMove);
if (c->characterState.creationInfo.weaponMeshScale != 1.0f)
c->weaponMesh.data.Scale(c->characterState.creationInfo.weaponMeshScale);
if (st.creationInfo.weaponMeshMove != Eigen::Vector3f::Zero())
c->weaponMesh.data.Move(st.creationInfo.weaponMeshMove);
if (st.creationInfo.weaponMeshScale != 1.0f)
c->weaponMesh.data.Scale(st.creationInfo.weaponMeshScale);
c->weaponMesh.RefreshVBO();
} catch (const std::exception& e) {
@@ -104,10 +142,10 @@ std::unique_ptr<Character> Character::createFromState(const CharacterState& st,
}
}
c->weaponTexture = loadTex(c->characterState.creationInfo.weaponTexturePath);
c->weaponAttachBoneName = c->characterState.creationInfo.weaponAttachBoneName;
c->weaponInitialPosition = c->characterState.creationInfo.weaponInitialPosition;
c->weaponInitialRotation = c->characterState.creationInfo.weaponInitialRotation.toRotationMatrix();
c->weaponTexture = loadTex(st.creationInfo.weaponTexturePath);
c->weaponAttachBoneName = st.creationInfo.weaponAttachBoneName;
c->weaponInitialPosition = st.creationInfo.weaponInitialPosition;
c->weaponInitialRotation = st.creationInfo.weaponInitialRotation.toRotationMatrix();
return c;
}
@@ -124,8 +162,8 @@ void Character::setTexture(const std::string& meshName, std::shared_ptr<Texture>
meshTextures[meshName] = std::move(tex);
}
AnimationState Character::resolveActiveState() const {
if (animations.count(characterState.currentState)) return characterState.currentState;
AnimationState Character::resolveActiveState(const CharacterState& state) const {
if (animations.count(state.currentState)) return state.currentState;
return AnimationState::STAND;
}
@@ -134,29 +172,33 @@ void Character::update(int64_t deltaMs, const CharacterUpdateEvents* events) {
hitSparkEmitter.update(static_cast<float>(deltaMs));
}
auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
if (events) {
if (events->deathAnimationComplete && onDeathAnimComplete)
onDeathAnimComplete();
if (events->attackImpact && audioPlayer) {
audioPlayer->playSoundAsync(characterState.isPlayer
audioPlayer->playSoundAsync(state.isPlayer
? "resources/audio/547041__cogfirestudios__hit-swing-sword-small-3.ogg"
: "resources/audio/13925__adcbicycle__21.ogg");
}
}
if (!characterState.enabled) return;
auto it = animations.find(characterState.currentState);
if (!state.enabled) return;
auto it = animations.find(state.currentState);
if (it == animations.end()) return;
auto& anim = it->second;
anim.currentFrame = (std::min)(
characterState.animationFrame, static_cast<float>((std::max)(0, anim.totalFrames - 1)));
state.animationFrame, static_cast<float>((std::max)(0, anim.totalFrames - 1)));
if (static_cast<int>(anim.currentFrame) != anim.lastFrame) {
anim.gpuSkinningShaderData.ComputeSkinningMatrices(
anim.model.startBones, anim.model.animations[0].keyFrames,
static_cast<int>(anim.currentFrame));
if (!characterState.useGpuSkinning) {
if (!state.useGpuSkinning) {
anim.model.Interpolate(static_cast<int>(anim.currentFrame));
}
anim.lastFrame = static_cast<int>(anim.currentFrame);
@@ -171,14 +213,17 @@ void Character::draw(Renderer& renderer) {
renderer.RenderUniform1f("uAlpha", 1.0f);
glDisable(GL_BLEND);
if (!characterState.enabled) return;
auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
if (!state.enabled) return;
if (characterState.useGpuSkinning) {
drawGpuSkinning(renderer);
if (state.useGpuSkinning) {
drawGpuSkinning(renderer, state);
return;
}
AnimationState drawState = resolveActiveState();
AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState);
if (it == animations.end()) return;
@@ -192,10 +237,10 @@ void Character::draw(Renderer& renderer) {
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
auto& anim = it->second;
for (const auto& name : anim.model.meshNamesOrdered) {
@@ -211,14 +256,14 @@ void Character::draw(Renderer& renderer) {
renderer.PopMatrix();
drawAttachedWeapon(renderer);
drawAttachedWeapon(renderer, state);
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
}
void Character::drawGpuSkinning(Renderer& renderer) {
AnimationState drawState = resolveActiveState();
void Character::drawGpuSkinning(Renderer& renderer, const CharacterState& state) {
AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState);
if (it == animations.end()) return;
@@ -246,10 +291,10 @@ void Character::drawGpuSkinning(Renderer& renderer) {
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
static_cast<int>(anim.gpuSkinningShaderData.skinningMatrices.size()), false,
@@ -269,15 +314,15 @@ void Character::drawGpuSkinning(Renderer& renderer) {
renderer.shaderManager.PushShader("fog");
renderer.RenderUniform1i(textureUniformName, 0);
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkin);
drawAttachedWeapon(renderer);
drawAttachedWeapon(renderer, state);
renderer.shaderManager.PopShader();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
}
bool Character::prepareGpuSkinning() {
AnimationState drawState = resolveActiveState();
bool Character::prepareGpuSkinning(const CharacterState& state) {
AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState);
if (it == animations.end()) return false;
@@ -293,11 +338,11 @@ bool Character::prepareGpuSkinning() {
return true;
}
void Character::drawAttachedWeapon(Renderer& renderer) {
if (!characterState.showWeapon) return;
void Character::drawAttachedWeapon(Renderer& renderer, const CharacterState& state) {
if (!state.showWeapon) return;
if (weaponMesh.data.PositionData.size() == 0 || !weaponTexture) return;
auto it = animations.find(resolveActiveState());
auto it = animations.find(resolveActiveState(state));
if (it == animations.end()) return;
auto& anim = it->second;
@@ -310,10 +355,10 @@ void Character::drawAttachedWeapon(Renderer& renderer) {
Eigen::Matrix4f currentBone = anim.gpuSkinningShaderData.skinningMatrices[boneIdx] * bindBone;
renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
renderer.TranslateMatrix(Eigen::Vector3f(currentBone.block<3, 1>(0, 3)));
renderer.RotateMatrix(Eigen::Matrix3f(currentBone.block<3, 3>(0, 0)));
renderer.TranslateMatrix(weaponInitialPosition);
@@ -327,24 +372,27 @@ void Character::drawAttachedWeapon(Renderer& renderer) {
// ==================== Shadow depth pass ====================
void Character::drawShadowDepth(Renderer& renderer) {
if (!characterState.enabled) return;
if (characterState.useGpuSkinning) {
drawShadowDepthGpuSkinning(renderer);
auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
if (!state.enabled) return;
if (state.useGpuSkinning) {
drawShadowDepthGpuSkinning(renderer, state);
} else {
drawShadowDepthCpu(renderer);
drawShadowDepthCpu(renderer, state);
}
}
void Character::drawShadowDepthCpu(Renderer& renderer) {
AnimationState drawState = resolveActiveState();
void Character::drawShadowDepthCpu(Renderer& renderer, const CharacterState& state) {
AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState);
if (it == animations.end()) return;
renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
auto& anim = it->second;
for (const auto& name : anim.model.meshNamesOrdered) {
@@ -358,17 +406,17 @@ void Character::drawShadowDepthCpu(Renderer& renderer) {
}
renderer.PopMatrix();
drawAttachedWeapon(renderer);
drawAttachedWeapon(renderer, state);
}
void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {
void Character::drawShadowDepthGpuSkinning(Renderer& renderer, const CharacterState& state) {
CheckGlError(__FILE__, __LINE__);
AnimationState drawState = resolveActiveState();
AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState);
if (it == animations.end()) return;
CheckGlError(__FILE__, __LINE__);
if (!prepareGpuSkinning()) return;
if (!prepareGpuSkinning(state)) return;
CheckGlError(__FILE__, __LINE__);
static const std::string shadowSkinningShader = "shadow_depth_skinning";
@@ -377,10 +425,10 @@ void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {
renderer.shaderManager.PushShader(shadowSkinningShader);
renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
CheckGlError(__FILE__, __LINE__);
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
@@ -400,14 +448,17 @@ void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {
renderer.PopMatrix();
renderer.shaderManager.PopShader();
drawAttachedWeapon(renderer);
drawAttachedWeapon(renderer, state);
CheckGlError(__FILE__, __LINE__);
}
// ==================== Main pass with shadows ====================
void Character::drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
if (!characterState.enabled) return;
auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
if (!state.enabled) return;
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
}
@@ -415,15 +466,15 @@ void Character::drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightF
renderer.RenderUniform1f("uAlpha", 1.0f);
glDisable(GL_BLEND);
if (characterState.useGpuSkinning) {
drawGpuSkinningWithShadow(renderer, lightFromCamera, shadowMapTex, lightDirCamera);
if (state.useGpuSkinning) {
drawGpuSkinningWithShadow(renderer, state, lightFromCamera, shadowMapTex, lightDirCamera);
} else {
drawCpuWithShadow(renderer, lightFromCamera, shadowMapTex, lightDirCamera);
drawCpuWithShadow(renderer, state, lightFromCamera, shadowMapTex, lightDirCamera);
}
}
void Character::drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
AnimationState drawState = resolveActiveState();
void Character::drawCpuWithShadow(Renderer& renderer, const CharacterState& state, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState);
if (it == animations.end()) return;
@@ -446,10 +497,10 @@ void Character::drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lig
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
auto& anim = it->second;
for (const auto& name : anim.model.meshNamesOrdered) {
@@ -468,20 +519,20 @@ void Character::drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lig
renderer.shaderManager.PushShader("fog");
renderer.RenderUniform1i(textureUniformName, 0);
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosShadow);
drawAttachedWeapon(renderer);
drawAttachedWeapon(renderer, state);
renderer.shaderManager.PopShader();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
}
void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
AnimationState drawState = resolveActiveState();
void Character::drawGpuSkinningWithShadow(Renderer& renderer, const CharacterState& state, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState);
if (it == animations.end()) return;
CheckGlError(__FILE__, __LINE__);
if (!prepareGpuSkinning()) return;
if (!prepareGpuSkinning(state)) return;
CheckGlError(__FILE__, __LINE__);
static const std::string skinningShadowShader = "fog_skinning_shadow";
@@ -508,10 +559,10 @@ void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matri
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
CheckGlError(__FILE__, __LINE__);
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
@@ -534,7 +585,7 @@ void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matri
renderer.shaderManager.PushShader("fog");
renderer.RenderUniform1i(textureUniformName, 0);
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkinShadow);
drawAttachedWeapon(renderer);
drawAttachedWeapon(renderer, state);
renderer.shaderManager.PopShader();
CheckGlError(__FILE__, __LINE__);
@@ -554,7 +605,10 @@ void Character::drawNight(Renderer& renderer,
const Eigen::Matrix4f& cameraViewInverse,
const float* ambientColor, const float* fogColor)
{
if (!characterState.enabled) return;
auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
if (!state.enabled) return;
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
}
@@ -562,25 +616,26 @@ void Character::drawNight(Renderer& renderer,
renderer.RenderUniform1f("uAlpha", 1.0f);
glDisable(GL_BLEND);
if (characterState.useGpuSkinning) {
drawNightGpuSkinning(renderer, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount,
if (state.useGpuSkinning) {
drawNightGpuSkinning(renderer, state, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount,
pointLightWorldXZ, pointLightLimitXZ, cameraViewInverse, ambientColor, fogColor);
} else {
drawNightCpu(renderer, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount,
drawNightCpu(renderer, state, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount,
pointLightWorldXZ, pointLightLimitXZ, cameraViewInverse, ambientColor, fogColor);
}
}
void Character::drawNightGpuSkinning(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
const float* ambientColor, const float* fogColor)
{
AnimationState drawState = resolveActiveState();
AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState);
if (it == animations.end()) return;
if (!prepareGpuSkinning()) return;
if (!prepareGpuSkinning(state)) return;
static const std::string nightSkinningShader = "night_fog_skinning";
static const std::string boneMatricesUniform = "uBoneMatrices[0]";
@@ -606,10 +661,10 @@ void Character::drawNightGpuSkinning(Renderer& renderer,
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
static_cast<int>(it->second.gpuSkinningShaderData.skinningMatrices.size()), false,
@@ -639,7 +694,7 @@ void Character::drawNightGpuSkinning(Renderer& renderer,
renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", plCount, plWorldXZ);
renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", plCount, plLimitXZ);
}
drawAttachedWeapon(renderer);
drawAttachedWeapon(renderer, state);
renderer.shaderManager.PopShader();
renderer.PopProjectionMatrix();
@@ -647,11 +702,12 @@ void Character::drawNightGpuSkinning(Renderer& renderer,
}
void Character::drawNightCpu(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
const float* ambientColor, const float* fogColor)
{
AnimationState drawState = resolveActiveState();
AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState);
if (it == animations.end()) return;
@@ -677,10 +733,10 @@ void Character::drawNightCpu(Renderer& renderer,
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
auto& anim = it->second;
for (const auto& name : anim.model.meshNamesOrdered) {
@@ -697,7 +753,7 @@ void Character::drawNightCpu(Renderer& renderer,
renderer.PopMatrix();
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNight);
drawAttachedWeapon(renderer);
drawAttachedWeapon(renderer, state);
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
@@ -712,35 +768,39 @@ void Character::drawNightWithShadow(Renderer& renderer,
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
const float* ambientColor, const float* fogColor)
{
if (!characterState.enabled) return;
auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
if (!state.enabled) return;
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
}
renderer.RenderUniform1f("uAlpha", 1.0f);
glDisable(GL_BLEND);
if (characterState.useGpuSkinning) {
drawNightGpuSkinningWithShadow(renderer, plPosEye, plDirEye, plColors, plCount,
if (state.useGpuSkinning) {
drawNightGpuSkinningWithShadow(renderer, state, plPosEye, plDirEye, plColors, plCount,
plWorldXZ, plLimitXZ, cameraViewInverse,
lightFromCamera, shadowMapTex, ambientColor, fogColor);
} else {
drawNightCpuWithShadow(renderer, plPosEye, plDirEye, plColors, plCount,
drawNightCpuWithShadow(renderer, state, plPosEye, plDirEye, plColors, plCount,
plWorldXZ, plLimitXZ, cameraViewInverse,
lightFromCamera, shadowMapTex, ambientColor, fogColor);
}
}
void Character::drawNightGpuSkinningWithShadow(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ,
const Eigen::Matrix4f& cameraViewInverse,
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
const float* ambientColor, const float* fogColor)
{
AnimationState drawState = resolveActiveState();
AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState);
if (it == animations.end()) return;
if (!prepareGpuSkinning()) return;
if (!prepareGpuSkinning(state)) return;
static const std::string nightShadowSkinningShader = "night_fog_skinning_shadow";
static const std::string boneMatricesUniform = "uBoneMatrices[0]";
@@ -774,10 +834,10 @@ void Character::drawNightGpuSkinningWithShadow(Renderer& renderer,
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
static_cast<int>(it->second.gpuSkinningShaderData.skinningMatrices.size()), false,
@@ -810,7 +870,7 @@ void Character::drawNightGpuSkinningWithShadow(Renderer& renderer,
}
renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", plCount, plWorldXZ);
renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", plCount, plLimitXZ);
drawAttachedWeapon(renderer);
drawAttachedWeapon(renderer, state);
renderer.shaderManager.PopShader();
renderer.PopProjectionMatrix();
@@ -818,13 +878,14 @@ void Character::drawNightGpuSkinningWithShadow(Renderer& renderer,
}
void Character::drawNightCpuWithShadow(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ,
const Eigen::Matrix4f& cameraViewInverse,
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
const float* ambientColor, const float* fogColor)
{
AnimationState drawState = resolveActiveState();
AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState);
if (it == animations.end()) return;
@@ -858,10 +919,10 @@ void Character::drawNightCpuWithShadow(Renderer& renderer,
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
auto& anim = it->second;
for (const auto& name : anim.model.meshNamesOrdered) {
@@ -878,7 +939,7 @@ void Character::drawNightCpuWithShadow(Renderer& renderer,
renderer.PopMatrix();
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNightShadow);
drawAttachedWeapon(renderer);
drawAttachedWeapon(renderer, state);
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
@@ -906,8 +967,11 @@ void Character::presentDamage(float hp, float maxHp,
onHpChanged(hp, maxHp, attackerId);
if (!hitSparkEmitter.isConfigured()) return;
auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
Eigen::Vector3f emitPos = characterState.position + Eigen::Vector3f(0.f, 1.f, 0.f);
Eigen::Vector3f emitPos = state.position + Eigen::Vector3f(0.f, 1.f, 0.f);
hitSparkEmitter.resetEmissionPoints({ emitPos });
hitSparkEmitter.setEmissionDirection(attackDirection);
@@ -924,16 +988,18 @@ void Character::prepareHitSparksForDraw(const Eigen::Matrix4f& cameraViewMatrix)
hitSparkEmitter.prepareForDraw(cameraViewMatrix);
}
void Character::drawName(TextRenderer& textRenderer,
const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix)
static void drawCharacterNameLabel(
TextRenderer& textRenderer,
const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix,
const CharacterState& state,
const std::string& displayName,
const std::array<float, 4>& color)
{
if (characterState.isPlayer) return;
if (!characterState.enabled) return;
if (characterState.hp <= 0.f) return;
if (characterState.npcName.empty()) return;
if (!state.enabled || state.hp <= 0.f || displayName.empty()) return;
Eigen::Vector4f worldPos(characterState.position.x(), characterState.position.y() + 1.83f, characterState.position.z(), 1.f);
Eigen::Vector4f worldPos(
state.position.x(), state.position.y() + 1.83f, state.position.z(), 1.f);
Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos;
if (clip.w() <= 0.f) return;
@@ -943,26 +1009,57 @@ void Character::drawName(TextRenderer& textRenderer,
float screenX = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth;
float screenY = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight;
textRenderer.drawText(displayName, screenX, screenY, 1.0f, true, color);
}
std::array<float, 4> color = characterState.canAttack
void Character::drawName(TextRenderer& textRenderer,
const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix)
{
auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
if (state.isPlayer) return;
if (state.npcName.empty()) return;
std::array<float, 4> color = state.canAttack
? std::array<float, 4>{ 1.f, 0.f, 0.f, 1.f }
: std::array<float, 4>{ 0.f, 1.f, 0.f, 1.f };
const std::string& translatedName = Character::nameTranslations.translateRef(characterState.npcName, g_currentLanguage);
const std::string& translatedName = Character::nameTranslations.translateRef(state.npcName, g_currentLanguage);
textRenderer.drawText(translatedName, screenX, screenY, 1.0f, true, color);
drawCharacterNameLabel(
textRenderer, cameraViewMatrix, projectionMatrix,
state, translatedName, color);
}
void Character::drawPlayerName(
TextRenderer& textRenderer,
const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix,
const std::string& displayName)
{
auto snapshot = stateSnapshot();
if (!snapshot) return;
static const std::array<float, 4> color = {0.f, 1.f, 0.f, 1.f};
drawCharacterNameLabel(
textRenderer, cameraViewMatrix, projectionMatrix,
*snapshot, displayName, color);
}
void Character::drawHealthBar(Renderer& renderer,
const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix)
{
if (characterState.isPlayer || !characterState.canAttack) return;
if (!characterState.enabled) return;
if (characterState.hp <= 0.f) return;
if (characterState.initialHp <= 0.f) return;
auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
if (state.isPlayer || !state.canAttack) return;
if (!state.enabled) return;
if (state.hp <= 0.f) return;
if (state.initialHp <= 0.f) return;
Eigen::Vector4f worldPos(characterState.position.x(), characterState.position.y() + 1.75f, characterState.position.z(), 1.f);
Eigen::Vector4f worldPos(state.position.x(), state.position.y() + 1.75f, state.position.z(), 1.f);
Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos;
if (clip.w() <= 0.f) return;
float ndcX = clip.x() / clip.w();
@@ -980,7 +1077,7 @@ void Character::drawHealthBar(Renderer& renderer,
const float bottom = sy - barH * 0.5f;
const float top = sy + barH * 0.5f;
float frac = characterState.hp / characterState.initialHp;
float frac = state.hp / state.initialHp;
if (frac < 0.f) frac = 0.f;
if (frac > 1.f) frac = 1.f;
const float split = left + barW * frac;
+27 -18
View File
@@ -22,12 +22,14 @@ class IGameServer;
class Character {
public:
Character(const CharacterState& boundState, IGameServer& server,
std::string locationId, std::string entityId)
: characterState(boundState), gameServer_(server),
locationId_(std::move(locationId)), entityId_(std::move(entityId)){}
Character(IGameServer& server, std::string locationId, std::string entityId)
: gameServer_(server), locationId_(std::move(locationId)),
entityId_(std::move(entityId)) {}
const std::string& entityId() const { return entityId_; }
void bindToLocation(std::string locationId) { locationId_ = std::move(locationId); }
// The aliasing shared_ptr keeps the owning GameSnapshot alive for as long
// as the returned CharacterState is being read.
std::shared_ptr<const CharacterState> stateSnapshot() const;
static constexpr float kAttackRange = 1.25f;
// ---- Asset loading ----
@@ -63,6 +65,10 @@ public:
void drawName(TextRenderer& textRenderer,
const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix);
void drawPlayerName(TextRenderer& textRenderer,
const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix,
const std::string& displayName);
void drawHealthBar(Renderer& renderer,
const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix);
@@ -84,11 +90,11 @@ public:
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
const float* ambientColor, const float* fogColor);
// ---- Query helpers (delegate to state) ----
bool isMoving() const { return characterState.isMoving(); }
const Eigen::Vector3f& getRequestedWalkTarget() const { return characterState.getRequestedWalkTarget(); }
Eigen::Vector3f getCurrentNavigationTarget() const { return characterState.getCurrentNavigationTarget(); }
float getHp() const { return characterState.getHp(); }
// ---- Query helpers (read immutable snapshots) ----
bool isMoving() const;
Eigen::Vector3f getRequestedWalkTarget() const;
Eigen::Vector3f getCurrentNavigationTarget() const;
float getHp() const;
// ---- Weapon rendering data (initialized from state.creationInfo by createFromState) ----
VertexRenderStruct weaponMesh;
@@ -109,7 +115,6 @@ private:
friend class LocationEditor;
friend class PathVisualization;
friend class InternalGameServer;
const CharacterState& characterState; // private non-owning binding to server-owned storage
IGameServer& gameServer_;
std::string locationId_;
std::string entityId_;
@@ -120,28 +125,32 @@ private:
VertexRenderStruct healthBarMesh;
std::unordered_map<std::string, std::shared_ptr<Texture>> meshTextures;
AnimationState resolveActiveState() const;
bool prepareGpuSkinning();
void drawAttachedWeapon(Renderer& renderer);
AnimationState resolveActiveState(const CharacterState& state) const;
bool prepareGpuSkinning(const CharacterState& state);
void drawAttachedWeapon(Renderer& renderer, const CharacterState& state);
void drawGpuSkinning(Renderer& renderer);
void drawShadowDepthGpuSkinning(Renderer& renderer);
void drawShadowDepthCpu(Renderer& renderer);
void drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
void drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
void drawGpuSkinning(Renderer& renderer, const CharacterState& state);
void drawShadowDepthGpuSkinning(Renderer& renderer, const CharacterState& state);
void drawShadowDepthCpu(Renderer& renderer, const CharacterState& state);
void drawGpuSkinningWithShadow(Renderer& renderer, const CharacterState& state, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
void drawCpuWithShadow(Renderer& renderer, const CharacterState& state, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
void drawNightGpuSkinning(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
const float* ambientColor, const float* fogColor);
void drawNightCpu(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
const float* ambientColor, const float* fogColor);
void drawNightGpuSkinningWithShadow(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor);
void drawNightCpuWithShadow(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor);
+11
View File
@@ -12,6 +12,17 @@ constexpr float TARGET_REPLAN_THRESHOLD = 0.25f;
constexpr float PI = 3.14159265358979323846f;
}
void CharacterState::configureAsPlayer() {
walkSpeed = 3.0f;
rotationSpeed = 8.0f;
modelScale = 1.f;
modelCorrectionRotation = Eigen::Quaternionf(
Eigen::AngleAxisf(PI, Eigen::Vector3f::UnitY()) *
Eigen::AngleAxisf(-PI * 0.5f, Eigen::Vector3f::UnitX()));
canAttack = true;
isPlayer = true;
}
bool CharacterState::isMoving() const {
Eigen::Vector3f toTarget = walkTarget - position;
toTarget.y() = 0.f;
+1
View File
@@ -135,6 +135,7 @@ public:
CharacterCreationInfo creationInfo;
// --- Pure-state methods ---
void configureAsPlayer();
bool isMoving() const;
const Eigen::Vector3f& getRequestedWalkTarget() const { return requestedWalkTarget; }
Eigen::Vector3f getCurrentNavigationTarget() const;
+168 -171
View File
@@ -13,6 +13,7 @@
#include <cmath>
#include <algorithm>
#include <functional>
#include <type_traits>
#ifdef __ANDROID__
#include <android/log.h>
#endif
@@ -76,16 +77,14 @@ namespace FRG
#endif
Location* Game::currentLocation() const {
if (gameState.currentLocationName.empty()) return nullptr;
const auto& locations = internalGameServer.runtimeLocations();
auto it = locations.find(gameState.currentLocationName);
return (it != locations.end()) ? it->second.get() : nullptr;
const auto state = stateSnapshot();
if (!state || state->currentLocationId.empty()) return nullptr;
return internalGameServer.runtimeLocation(state->currentLocationId);
}
Game::Game()
: audioPlayer(std::make_unique<AudioPlayerAsync>())
, internalGameServer()
, gameState(internalGameServer.hostState())
, menuManager(renderer, internalGameServer, *audioPlayer)
, newTickCount(0)
, lastTickCount(0)
@@ -360,19 +359,6 @@ namespace FRG
CampaignRegistry::instance().loadFromJson("resources/config/campaigns.json", CONST_ZIP_FILE);
loadSteps.push([this]() {
// Wire inventory callbacks: tutorial tracking + toast notifications.
gameState.inventory.onItemAdded = [this](const std::string& itemId) {
menuManager.onItemPickedUp(itemId);
const Item* item = ItemRegistry::instance().findById(itemId);
if (item) menuManager.showToast(menuManager.toastTextureItemAdded, item->name);
audioPlayer->playSoundAsync("resources/audio/831642__ienba__generic-game-pick-up.ogg");
};
gameState.inventory.onItemRemoved = [this](const std::string& itemId) {
const Item* item = ItemRegistry::instance().findById(itemId);
if (item) menuManager.showToast(menuManager.toastTextureItemRemoved, item->name);
audioPlayer->playSoundAsync("resources/audio/831642__ienba__generic-game-pick-up.ogg");
};
// Wire phone dialogue start function so MenuManager can trigger dialogues.
menuManager.startDialogueFunc = [this](const std::string& id) {
if (currentLocation()) currentLocation()->dialogueSystem.startDialogue(id);
@@ -399,18 +385,6 @@ namespace FRG
loadSteps.push([this]() {
try {
menuManager.setup(CONST_ZIP_FILE);
gameState.questJournal.onQuestUnlocked = [this](const std::string& id) {
const Quest::QuestState* quest = gameState.questJournal.findQuest(id);
if (quest) menuManager.showToast(menuManager.toastTextureQuestNew, quest->definition.title);
};
gameState.questJournal.onQuestCompleted = [this](const std::string& id) {
const Quest::QuestState* quest = gameState.questJournal.findQuest(id);
if (quest) menuManager.showToast(menuManager.toastTextureQuestCompleted, quest->definition.title);
};
gameState.questJournal.onQuestFailed = [this](const std::string& id) {
const Quest::QuestState* quest = gameState.questJournal.findQuest(id);
if (quest) menuManager.showToast(menuManager.toastTextureQuestFailed, quest->definition.title);
};
logger() << "UI loaded successfully" << std::endl;
}
catch (const std::exception& e) {
@@ -425,7 +399,8 @@ namespace FRG
performResetToInitialState();
applyTutorialSetting();
const CampaignConfig& campaign = activeCampaign();
gameState.currentLocationName = campaign.startLocation;
internalGameServer.submit(
SetCurrentLocationCommand{campaign.startLocation});
menuManager.showGameplayHud(); // first HUD frame drawn from the fresh state
if (currentLocation()) currentLocation()->scriptEngine.callLocationEnterCallback();
audioPlayer->crossFadeMusicAsync(campaign.startMusic);
@@ -516,7 +491,10 @@ namespace FRG
}
const CampaignConfig& Game::activeCampaign() const {
const CampaignConfig* campaign = CampaignRegistry::instance().find(gameState.campaignId);
const auto state = stateSnapshot();
const CampaignConfig* campaign = state
? CampaignRegistry::instance().find(state->campaignId)
: nullptr;
if (!campaign) campaign = CampaignRegistry::instance().find(CampaignRegistry::kDefaultCampaignId);
if (!campaign) {
static const CampaignConfig empty;
@@ -527,7 +505,7 @@ namespace FRG
bool Game::isLocationLoaded(const std::string& campaignId, const std::string& locationId) const {
return player && loadedCampaignId == campaignId
&& internalGameServer.runtimeLocations().count(locationId) > 0;
&& internalGameServer.hasRuntimeLocation(locationId);
}
void Game::enterLocationThen(const std::string& requestedCampaignId, const std::string& requestedLocationId,
@@ -567,15 +545,15 @@ namespace FRG
void Game::loadCampaign(const std::string& campaignId) {
loadedCampaignId = campaignId;
gameState.campaignId = campaignId;
gameState.locations.clear();
gameState.players.clear();
gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE);
gameState.globalFloats["player_hp"] = 200; // read by location scripts as they load
const CampaignConfig* campaign =
CampaignRegistry::instance().find(campaignId);
if (!campaign) return;
internalGameServer.initializeCampaign(
campaignId, campaign->questsPath, CONST_ZIP_FILE);
// The player stays loaded for the whole campaign; locations come and go around him.
auto& playerState = gameState.players["player"];
if (!playerState) playerState = std::make_shared<CharacterState>();
player = Location::createPlayer(renderer, audioPlayer.get(), *playerState, internalGameServer);
player = Location::createPlayer(
renderer, audioPlayer.get(), internalGameServer,
internalGameServer.localPlayerId());
if (player)
player->onDeathAnimComplete = [this]() { startDarklandsTransition(); };
}
@@ -583,21 +561,21 @@ namespace FRG
void Game::unloadCampaign() {
unloadLocation(false);
player.reset();
gameState.players.clear();
loadedCampaignId.clear();
gameState.locations.clear();
internalGameServer.clearCampaign();
renderer.textureManager.UnloadUnused(); // the player's textures
}
void Game::loadLocation(const std::string& locationId) {
const LocationConfig* config = activeCampaign().findLocation(locationId);
if (!config) {
std::cerr << "[campaign] Campaign '" << gameState.campaignId << "' has no location '"
const auto state = stateSnapshot();
std::cerr << "[campaign] Campaign '"
<< (state ? state->campaignId : std::string()) << "' has no location '"
<< locationId << "'" << std::endl;
return;
}
const bool initializeLocation =
!gameState.locations[locationId].hasPersistentState;
const bool initializeLocation = internalGameServer.prepareLocation(*config);
std::shared_ptr<Location> loc = createLocation(*config);
wireLocation(loc.get());
if (shadowMap) loc->shadowMap = shadowMap;
@@ -608,12 +586,7 @@ namespace FRG
}
void Game::unloadLocation(bool storeState) {
if (storeState) internalGameServer.synchronizeRuntimeAuxiliaryState();
else {
for (const auto& [name, loc] : internalGameServer.runtimeLocations())
gameState.locations.erase(name);
}
internalGameServer.runtimeLocations().clear();
internalGameServer.clearRuntimeLocations(storeState);
// Textures only the location's models and characters used are freed now.
renderer.textureManager.UnloadUnused();
}
@@ -631,8 +604,10 @@ namespace FRG
}
// Runs from inside the current location's update(); the location itself is
// released by a later loading step.
enterLocationThen(gameState.campaignId, destName, [this, destName, destPos, destRotY]() {
gameState.currentLocationName = destName;
const auto state = stateSnapshot();
if (!state) return;
enterLocationThen(state->campaignId, destName, [this, destName, destPos, destRotY]() {
internalGameServer.submit(SetCurrentLocationCommand{destName});
Location* loc = currentLocation();
if (!loc) return;
if (loc->player) {
@@ -643,17 +618,17 @@ namespace FRG
});
loc->stopCharacter(*loc->player);
}
loc->state.cameraAzimuth = destRotY;
loc->state.isDarklands = gameState.isDarklands;
loc->state.isNight = gameState.isNight;
loc->state.isDawn = gameState.isDawn;
auto locationState = loc->stateSnapshot();
const float inclination = locationState
? locationState->cameraInclination
: 1.1036f;
internalGameServer.submit(SetLocationCameraCommand{
destName, destRotY, inclination});
loc->scriptEngine.callLocationEnterCallback();
//Reset taxi
auto locationState = gameState.locations.find(destName);
if (locationState != gameState.locations.end())
locationState->second.resetTaxi();
gameState.taxiIsCalled = false;
internalGameServer.submit(ResetTaxiCommand{destName});
internalGameServer.submit(SetTaxiCalledCommand{false});
menuManager.onLocationChanged(destName);
});
@@ -661,34 +636,12 @@ namespace FRG
void Game::performFullReset() {
gameState.currentLocationName.clear();
gameState.isDarklands = false;
gameState.isNight = false;
gameState.isDawn = false;
gameState.playerHp = 200.f;
gameState.playerMaxHp = 200.f;
gameState.money = 5500;
gameState.globalInts.clear();
gameState.globalFloats.clear();
gameState.globalFloats["player_hp"] = 200;
gameState.tutorialStep = TutorialStep::AdvanceDialogue;
gameState.tutorialPhoneChatScreenOpened = false;
gameState.tutorialJournalScreenOpened = false;
gameState.tutorialNeedOpenTaxiScreen = false;
gameState.tutorialTaxiHintDone = false;
for (int i = 0; i < 3; ++i) {
gameState.chatUnread[i] = true;
gameState.chatPreviewMsg[i].clear();
gameState.chatHistory[i].clear();
}
gameState.inventory.clear();
gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE);
// 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.
gameState.locations.clear();
std::vector<std::string> loadedIds;
for (auto& [name, loc] : internalGameServer.runtimeLocations()) loadedIds.push_back(name);
std::vector<std::string> loadedIds =
internalGameServer.loadedRuntimeLocationIds();
internalGameServer.resetCampaign(
activeCampaign().questsPath, CONST_ZIP_FILE);
unloadLocation(false);
for (const std::string& id : loadedIds) loadLocation(id);
@@ -737,7 +690,8 @@ namespace FRG
// (if the campaign has one); a disabled tutorial is simply one that is already completed.
void Game::applyTutorialSetting() {
if (!activeCampaign().tutorial || !menuManager.tutorialEnabledSetting)
gameState.tutorialStep = TutorialStep::Completed;
internalGameServer.submit(
SetTutorialStepCommand{TutorialStep::Completed});
}
void Game::saveInitialState() {
@@ -760,12 +714,6 @@ namespace FRG
std::shared_ptr<Location> Game::createLocation(const LocationConfig& config)
{
LocationGameState& authoritative = gameState.locations[config.id];
if (!authoritative.location) authoritative.location = std::make_shared<LocationState>();
if (authoritative.location->navigationMaps.empty())
authoritative.location->initNavigation(config.navigationJsonPaths);
LocationSetup params;
params.gameObjectsJsonPath = config.gameObjectsJsonPath;
params.interactiveObjectsJsonPath = config.interactiveObjectsJsonPath;
@@ -778,29 +726,30 @@ namespace FRG
params.lightsJsonPath = config.lightsJsonPath;
params.playerPosition = config.playerPosition;
params.player = player;
params.gameState = &authoritative;
auto loc = std::make_shared<Location>(
renderer, gameState.inventory, *authoritative.location, internalGameServer, config.id);
loc->setup(params, &gameState.questJournal, audioPlayer.get());
loc->scriptEngine.setGlobalStore(&gameState.globalInts);
loc->scriptEngine.setGlobalFloatStore(&gameState.globalFloats);
renderer, internalGameServer, config.id);
internalGameServer.setupRuntimeLocation(
config.id, loc, std::move(params), audioPlayer.get());
loc->requestNightDayTransition = [this](bool isNight, bool isDawn) {
gameState.isNight = isNight;
gameState.isDawn = isDawn;
const auto state = stateSnapshot();
if (!state) return;
internalGameServer.submit(SetWorldModeCommand{
state->isDarklands, isNight, isDawn});
};
loc->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
loc->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); };
loc->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
loc->requestReturnToMainMenu = [this]() {
gameState.currentLocationName.clear();
internalGameServer.submit(SetCurrentLocationCommand{std::string()});
menuManager.showMenuAfterGameCompleted();
};
loc->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
menuManager.setChatUnread(idx, unread, msg);
};
for (auto& npc : loc->npcs) {
if (npc && npc->characterState.canAttack) {
auto state = npc ? npc->stateSnapshot() : nullptr;
if (state && state->canAttack) {
npc->onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
}
}
@@ -813,25 +762,6 @@ namespace FRG
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
};
authoritative.lockInteractiveObjectsDuringTutorial =
config.lockInteractiveObjectsDuringTutorial;
if (config.taxi) {
const LocationConfig::Taxi& taxi = *config.taxi;
LocationState& locationState = *authoritative.location;
locationState.taxiCarTriggerPosition = taxi.triggerPosition;
locationState.taxiCarWaypoints = taxi.waypoints;
locationState.taxiCarInitialState.position = taxi.initialPosition;
locationState.taxiCarInitialState.rotation = taxi.initialRotation;
locationState.taxiCarInitialState.mode = NpcCar::Mode::NONE;
locationState.taxiCarInitialState.velocity = 0.f;
locationState.taxiCarInitialState.steeringAngle = 0.f;
locationState.taxiCarInitialState.waypointReachRadius = taxi.waypointReachRadius;
locationState.taxiCar.waypointReachRadius = taxi.waypointReachRadius;
if (!authoritative.hasPersistentState)
authoritative.resetTaxi();
}
return loc;
}
@@ -846,20 +776,16 @@ namespace FRG
teleportPlayer(destName, destPos, destRotY);
};
// Share the global int store with all dialogue runtimes so flags set via
// dialogue JSON and via Lua set_dialogue_flag all see the same state.
{
loc->dialogueSystem.setGlobalFlagStore(&gameState.globalInts);
}
// Wire tutorial advance callbacks.
// completeTutorialStep() only acts on the current step, so sharing is safe.
{
loc->dialogueSystem.setOnDialogueAdvanced([this]() {
gameState.completeTutorialStep(TutorialStep::AdvanceDialogue);
internalGameServer.submit(
CompleteTutorialStepCommand{TutorialStep::AdvanceDialogue});
});
loc->onPlayerFloorWalk = [this]() {
gameState.completeTutorialStep(TutorialStep::Walk);
internalGameServer.submit(
CompleteTutorialStepCommand{TutorialStep::Walk});
menuManager.onPlayerStartedWalking();
};
}
@@ -897,19 +823,23 @@ namespace FRG
}
bool currentTargetDead = false;
auto playerState = loc->player->stateSnapshot();
if (!playerState) return;
for (const auto& npc : loc->npcs) {
if (npc && npc->entityId() == loc->player->characterState.attackTargetId) {
currentTargetDead = npc->characterState.hp <= 0.f;
if (npc && npc->entityId() == playerState->attackTargetId) {
auto npcState = npc->stateSnapshot();
currentTargetDead = !npcState || npcState->hp <= 0.f;
break;
}
}
if ((loc->player->characterState.attackTargetId.empty() || currentTargetDead)
if ((playerState->attackTargetId.empty() || currentTargetDead)
&&
loc->player->isMoving() == false
&&
loc->player->getHp() > 0) {
internalGameServer.submit(SetCharacterTargetCommand{
loc->locationId(), "player", attackerId});
loc->locationId(),
internalGameServer.localPlayerId(), attackerId});
}
}
@@ -931,7 +861,9 @@ namespace FRG
CheckGlError(__FILE__, __LINE__);
logger() << "Shadow map initialized" << std::endl;
for (auto& [name, loc] : internalGameServer.runtimeLocations()) loc->shadowMap = shadowMap;
for (const std::string& id : internalGameServer.loadedRuntimeLocationIds())
if (Location* loc = internalGameServer.runtimeLocation(id))
loc->shadowMap = shadowMap;
}
void Game::destroyShadowMapAndProparateToLocations()
@@ -941,7 +873,9 @@ namespace FRG
shadowMap.reset();
CheckGlError(__FILE__, __LINE__);
for (auto& [name, loc] : internalGameServer.runtimeLocations()) loc->shadowMap.reset();
for (const std::string& id : internalGameServer.loadedRuntimeLocationIds())
if (Location* loc = internalGameServer.runtimeLocation(id))
loc->shadowMap.reset();
CheckGlError(__FILE__, __LINE__);
}
@@ -1014,16 +948,12 @@ namespace FRG
Location* loc = currentLocation();
if (loc)
{
// Sync global flags so Location's draw functions see them.
loc->state.isDarklands = gameState.isDarklands;
loc->state.isNight = gameState.isNight;
loc->state.isDawn = gameState.isDawn;
if (gameState.isDarklands) {
const auto state = stateSnapshot();
if (state && state->isDarklands) {
loc->drawGameDarklands();
CheckGlError(__FILE__, __LINE__);
}
else if (gameState.isNight) {
else if (state && state->isNight) {
loc->drawGameNight();
CheckGlError(__FILE__, __LINE__);
}
@@ -1206,11 +1136,67 @@ namespace FRG
// The authoritative implementation advances simulation; a future
// network client can use the same call to poll/apply server updates.
internalGameServer.update(delta);
processServerEvents();
}
}
}
void Game::processServerEvents()
{
for (GameServerEvent& event : internalGameServer.drainEvents()) {
std::visit([this](const auto& value) {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, InventoryItemAddedEvent>) {
menuManager.onItemPickedUp(value.itemId);
const Item* item =
ItemRegistry::instance().findById(value.itemId);
if (item)
menuManager.showToast(
menuManager.toastTextureItemAdded, item->name);
audioPlayer->playSoundAsync(
"resources/audio/831642__ienba__generic-game-pick-up.ogg");
} else if constexpr (std::is_same_v<T, InventoryItemRemovedEvent>) {
const Item* item =
ItemRegistry::instance().findById(value.itemId);
if (item)
menuManager.showToast(
menuManager.toastTextureItemRemoved, item->name);
audioPlayer->playSoundAsync(
"resources/audio/831642__ienba__generic-game-pick-up.ogg");
} else if constexpr (std::is_same_v<T, Quest::QuestEvent>) {
const auto state = stateSnapshot();
if (!state) return;
auto quest = std::find_if(
state->quests.begin(), state->quests.end(),
[&value](const Quest::QuestState& candidate) {
return candidate.definition.id == value.questId;
});
if (quest == state->quests.end()) return;
switch (value.type) {
case Quest::QuestEventType::Unlocked:
menuManager.showToast(
menuManager.toastTextureQuestNew,
quest->definition.title);
break;
case Quest::QuestEventType::Completed:
menuManager.showToast(
menuManager.toastTextureQuestCompleted,
quest->definition.title);
break;
case Quest::QuestEventType::Failed:
menuManager.showToast(
menuManager.toastTextureQuestFailed,
quest->definition.title);
break;
case Quest::QuestEventType::ObjectiveCompleted:
break;
}
}
}, event);
}
}
void Game::render() {
FRG::CheckGlError(__FILE__, __LINE__);
@@ -1256,10 +1242,7 @@ namespace FRG
}
void Game::update() {
// Host systems (locations, scripts and loading) still execute in-process.
// Publish any changes they made since the previous frame as a new immutable
// revision before presentation clients query the server.
internalGameServer.markHostMutation();
processServerEvents();
SDL_Event event;
const bool mobileIsPortrait = loadingCompleted && (Environment::width < Environment::height);
while (SDL_PollEvent(&event)) {
@@ -1394,7 +1377,8 @@ namespace FRG
}
//logger() << "Current zoom: " << Environment::zoom << std::endl;
// Tutorial: any mouse-wheel scroll counts as the "zoom gesture".
gameState.completeTutorialStep(TutorialStep::Zoom);
internalGameServer.submit(
CompleteTutorialStepCommand{TutorialStep::Zoom});
}
if (event.type == SDL_KEYDOWN && event.key.repeat == 0) {
@@ -1422,7 +1406,6 @@ namespace FRG
if (event.key.keysym.sym == SDLK_3)
{
currentLocation()->switchNavigation(2);
//logger() << "Player position: " << currentLocation()->player->characterState.position.transpose() << std::endl;
}
if (event.key.keysym.sym == SDLK_n)
{
@@ -1435,8 +1418,6 @@ namespace FRG
{
currentLocation()->editorMode = EditorMode::None;
}
//logger() << "Player position: " << currentLocation()->player->characterState.position.transpose() << std::endl;
}
}
@@ -1516,8 +1497,11 @@ namespace FRG
currentLocation()->lastMouseY = eventY;
// Snapshot current angles so we can measure how far the user rotates.
dragStartAzimuth = currentLocation()->state.cameraAzimuth;
dragStartInclination = currentLocation()->state.cameraInclination;
auto state = currentLocation()->stateSnapshot();
if (state) {
dragStartAzimuth = state->cameraAzimuth;
dragStartInclination = state->cameraInclination;
}
}
}
@@ -1577,7 +1561,8 @@ namespace FRG
// Tutorial: detect a significant pinch-zoom (≥ 2 zoom units).
if (std::abs(Environment::zoom - pinchStartZoom) >= 2.0f) {
gameState.completeTutorialStep(TutorialStep::Zoom);
internalGameServer.submit(
CompleteTutorialStepCommand{TutorialStep::Zoom});
}
}
@@ -1720,12 +1705,17 @@ namespace FRG
// ~0.15 rad (≈8.6°) per axis ensures the user intentionally panned in 2D,
// not just nudged a single axis by accident.
static constexpr float TUTORIAL_ROTATION_THRESHOLD = 0.15f;
const auto game = stateSnapshot();
if (cameraDragging
&& gameState.tutorialStep == TutorialStep::RotateCamera) {
float deltaAz = std::abs(currentLocation()->state.cameraAzimuth - dragStartAzimuth);
float deltaInc = std::abs(currentLocation()->state.cameraInclination - dragStartInclination);
&& game
&& game->tutorialStep == TutorialStep::RotateCamera) {
auto state = currentLocation()->stateSnapshot();
if (!state) return;
float deltaAz = std::abs(state->cameraAzimuth - dragStartAzimuth);
float deltaInc = std::abs(state->cameraInclination - dragStartInclination);
if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) {
gameState.completeTutorialStep(TutorialStep::RotateCamera);
internalGameServer.submit(
CompleteTutorialStepCommand{TutorialStep::RotateCamera});
}
}
}
@@ -1733,13 +1723,14 @@ namespace FRG
bool Game::startDarklandsTransition()
{
if (!gameState.isNight)
const auto state = stateSnapshot();
if (!state || !state->isNight)
{
currentLocation()->dialogueSystem.startDialogue("darklands_day_dialog001");
return false;
}
if (gameState.isDawn)
if (state->isDawn)
{
currentLocation()->dialogueSystem.startDialogue("darklands_morning_dialog001");
return false;
@@ -1773,11 +1764,13 @@ namespace FRG
if (darklandsFlashFadingIn) {
darklandsFlashAlpha = min(darklandsFlashAlpha + step, 1.0f);
if (darklandsFlashAlpha >= 1.0f) {
const auto before = stateSnapshot();
if (!before) return;
if (isNightTransition) {
gameState.isNight = !gameState.isNight;
internalGameServer.submit(SetWorldModeCommand{
before->isDarklands, !before->isNight, before->isDawn});
if (currentLocation())
{
currentLocation()->state.isNight = gameState.isNight;
currentLocation()->scriptEngine.callTriggerNightEnterCallback();
}
@@ -1785,12 +1778,15 @@ namespace FRG
menuManager.lastMusicTrack = "music/bishkek night.ogg";
} else {
gameState.isDarklands = !gameState.isDarklands;
internalGameServer.submit(SetWorldModeCommand{
!before->isDarklands, before->isNight, before->isDawn});
const auto after = stateSnapshot();
if (!after) return;
// Change HUD
menuManager.setDarklandsMode(gameState.isDarklands);
menuManager.setDarklandsMode(after->isDarklands);
if (gameState.isDarklands)
if (after->isDarklands)
{
audioPlayer->crossFadeMusicAsync("music/bishkek fight calm.ogg");
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
@@ -1802,8 +1798,7 @@ namespace FRG
}
if (currentLocation()) {
currentLocation()->state.isDarklands = gameState.isDarklands;
if (gameState.isDarklands)
if (after->isDarklands)
currentLocation()->scriptEngine.callDarklandsEnterCallback();
else
currentLocation()->scriptEngine.callDarklandsExitCallback();
@@ -1935,19 +1930,21 @@ namespace FRG
void Game::playMusicForLoadedGame()
{
if (gameState.isDarklands)
const auto state = stateSnapshot();
if (!state) return;
if (state->isDarklands)
{
audioPlayer->crossFadeMusicAsync("music/bishkek fight calm.ogg");
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
}
else if (gameState.isNight)
else if (state->isNight)
{
audioPlayer->crossFadeMusicAsync("music/bishkek night.ogg");
menuManager.lastMusicTrack = "music/bishkek night.ogg";
}
else
{
if (gameState.currentLocationName == "location_dorm") {
if (state->currentLocationId == "location_dorm") {
audioPlayer->crossFadeMusicAsync("music/obshaga.ogg");
menuManager.lastMusicTrack = "music/obshaga.ogg";
}
+4 -4
View File
@@ -21,7 +21,6 @@
#include <unordered_set>
#include "Location.h"
#include "AudioPlayerAsync.h"
#include "GameState.h"
#include "game_server/InternalGameServer.h"
namespace FRG {
@@ -84,9 +83,6 @@ namespace FRG {
private:
InternalGameServer internalGameServer;
// Host-only alias while location/script orchestration is moved behind typed
// commands. The referenced object is owned by internalGameServer.
GameState& gameState;
public:
IGameServer& server() { return internalGameServer; }
@@ -132,6 +128,9 @@ namespace FRG {
Location* currentLocation() const;
std::shared_ptr<const GameSnapshot> stateSnapshot() const {
return internalGameServer.snapshot();
}
void saveGame(int slot);
void loadGame(int slot);
@@ -181,6 +180,7 @@ namespace FRG {
int64_t getSyncTimeMs();
void processTickCount();
void processServerEvents();
void drawScene();
void drawUI();
+1
View File
@@ -215,6 +215,7 @@ void GameState::load(const nlohmann::json& in)
auto& player = players[id];
if (!player) player = std::make_shared<CharacterState>();
player->load(value);
player->configureAsPlayer();
}
for (auto it = players.begin(); it != players.end();) {
if (!playerValues.contains(it->first)) it = players.erase(it);
+455 -167
View File
File diff suppressed because it is too large. Load diff
+29 -9
View File
@@ -72,11 +72,19 @@ namespace FRG
class Location
{
public:
Location(Renderer& iRenderer, Inventory& iInventory, LocationState& state, IGameServer& server,
Location(Renderer& iRenderer, IGameServer& server,
std::string locationId);
// Detaches the (shared, longer-lived) player from this location.
~Location();
const std::string& locationId() const { return locationId_; }
std::shared_ptr<const LocationState> stateSnapshot() const;
bool isDarklands() const;
bool isNight() const;
bool isDawn() const;
int globalInt(const std::string& key) const;
float globalFloat(const std::string& key) const;
void setGlobalInt(const std::string& key, int value);
void setGlobalFloat(const std::string& key, float value);
std::unordered_map<std::string, LoadedGameObject> gameObjects;
@@ -91,10 +99,7 @@ namespace FRG
// Builds the player character (models, animations, weapon, textures).
static std::shared_ptr<Character> createPlayer(Renderer& renderer, AudioPlayerAsync* audioPlayer,
CharacterState& state, IGameServer& server);
//std::vector<PathFinder> navigationMaps;
//int state.activeNavigationIndex = 0;
IGameServer& server, const std::string& playerId);
std::shared_ptr<ShadowMap> shadowMap;
Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity();
@@ -160,7 +165,7 @@ namespace FRG
void setup(const LocationSetup& params, Quest::QuestJournal* journal, AudioPlayerAsync* audioPlayer);
void setup(const LocationSetup& params, AudioPlayerAsync* audioPlayer);
//void setupNavigation(const std::vector<std::string>& paths);
bool switchNavigation(int index);
void setCharacterTarget(Character& character, const Eigen::Vector3f& target,
@@ -175,6 +180,18 @@ namespace FRG
void resetCharacterAfterDeath(Character& character);
void tryCharacterAttackCombo(Character& character);
void setCharacterHp(Character& character, float hp);
bool hasInventoryItem(const std::string& itemId) const;
std::size_t inventoryItemCount() const;
void addInventoryItem(std::string itemId);
void removeInventoryItem(std::string itemId);
bool unlockQuest(const std::string& questId);
bool completeQuest(const std::string& questId);
bool failQuest(const std::string& questId);
bool setQuestObjectiveCompleted(
const std::string& questId, const std::string& objectiveId, bool completed);
bool setQuestObjectiveVisible(
const std::string& questId, const std::string& objectiveId, bool visible);
bool setQuestActiveObjective(const std::string& questId, int objectiveIndex);
InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir);
Character* raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance = 100.0f);
@@ -210,15 +227,18 @@ namespace FRG
friend class ScriptEngine;
friend class InternalGameServer;
Renderer& renderer;
Inventory& inventory;
AudioPlayerAsync* audioPlayer;
private:
LocationState& state; // non-owning binding to LocationGameState
IGameServer& gameServer_;
std::string locationId_;
std::unordered_map<std::string, std::shared_ptr<Character>> remotePlayers_;
void synchronizeRemotePlayers();
void drawRemotePlayerNames(
const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix);
// ---- Pointer-to-index helpers ----
TeleportZone* getTargetTeleportZone();
TeleportZone* getTargetTeleportZone(const LocationState& state);
enum class InstancedTexture { None, Color, Darklands };
+36 -10
View File
@@ -1,6 +1,7 @@
#include "LocationEditor.h"
#include "Location.h"
#include "Character.h"
#include "game_server/IGameServer.h"
#include "utils/Utils.h"
#include "render/OpenGlExtensions.h"
#include "TextModel.h"
@@ -24,9 +25,14 @@ namespace FRG
void LocationEditor::buildNavMeshes()
{
navigationEditorNavMeshes.clear();
const float y = loc.state.navigationMaps[loc.state.activeNavigationIndex].getFloorY() + 0.02f;
auto state = loc.stateSnapshot();
if (!state || state->activeNavigationIndex < 0 ||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
const PathFinder& navigation =
state->navigationMaps[state->activeNavigationIndex];
const float y = navigation.getFloorY() + 0.02f;
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
for (const auto& obs : loc.state.navigationMaps[loc.state.activeNavigationIndex].getObstaclePolygons()) {
for (const auto& obs : navigation.getObstaclePolygons()) {
if (obs.polygon.size() < 3) continue;
VertexRenderStruct mesh;
mesh.data = CreatePolygonFloor(obs.polygon, y, red);
@@ -50,7 +56,11 @@ namespace FRG
{
VertexDataStruct data;
const Eigen::Vector3f yellow(1.0f, 1.0f, 0.0f);
const float y = loc.state.navigationMaps[loc.state.activeNavigationIndex].getFloorY() + 0.05f;
auto state = loc.stateSnapshot();
if (!state || state->activeNavigationIndex < 0 ||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
const float y =
state->navigationMaps[state->activeNavigationIndex].getFloorY() + 0.05f;
const float s = 0.2f;
for (const auto& pt : navigationEditorPoints) {
@@ -126,12 +136,17 @@ namespace FRG
poly.polygon.emplace_back(pt.x(), pt.z());
}
loc.state.navigationMaps[loc.state.activeNavigationIndex].addObstaclePolygon(poly);
loc.gameServer_.submit(AddNavigationObstacleCommand{
loc.locationId_, poly.name, poly.polygon});
logger() << "[NAV_EDITOR] Added obstacle '" << poly.name << "' with "
<< poly.polygon.size() << " vertices\n";
{
const float y = loc.state.navigationMaps[loc.state.activeNavigationIndex].getFloorY() + 0.02f;
auto state = loc.stateSnapshot();
if (!state || state->activeNavigationIndex < 0 ||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
const float y =
state->navigationMaps[state->activeNavigationIndex].getFloorY() + 0.02f;
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
VertexRenderStruct mesh;
mesh.data = CreatePolygonFloor(poly.polygon, y, red);
@@ -153,12 +168,21 @@ namespace FRG
if (!std::filesystem::exists(baseName + ".json")) break;
}
if (loc.state.navigationMaps[loc.state.activeNavigationIndex].saveConfig(baseName + ".json"))
auto state = loc.stateSnapshot();
if (!state || state->activeNavigationIndex < 0 ||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) {
std::cerr << "[NAV_EDITOR] No active navigation map to save\n";
return;
}
const PathFinder& navigation =
state->navigationMaps[state->activeNavigationIndex];
if (navigation.saveConfig(baseName + ".json"))
logger() << "[NAV_EDITOR] Saved config to: " << baseName << ".json\n";
else
std::cerr << "[NAV_EDITOR] Failed to save config to: " << baseName << ".json\n";
if (loc.state.navigationMaps[loc.state.activeNavigationIndex].saveGrid(baseName + ".txt"))
if (navigation.saveGrid(baseName + ".txt"))
logger() << "[NAV_EDITOR] Saved grid to: " << baseName << ".txt\n";
else
std::cerr << "[NAV_EDITOR] Failed to save grid to: " << baseName << ".txt\n";
@@ -458,9 +482,11 @@ namespace FRG
data.rotationX = 0.0f;
data.rotationY = rotDist(rng);
data.rotationZ = 0.0f;
data.positionX = loc.player->characterState.position.x();
data.positionY = loc.player->characterState.position.y();//-0.1;
data.positionZ = loc.player->characterState.position.z();
auto playerState = loc.player ? loc.player->stateSnapshot() : nullptr;
if (!playerState) return;
data.positionX = playerState->position.x();
data.positionY = playerState->position.y();//-0.1;
data.positionZ = playerState->position.z();
data.scale = scaleDist(rng);
GameObjectData data2;
-6
View File
@@ -26,12 +26,6 @@ struct LocationState : public ISaveable {
std::string targetInteractNpcId;
std::string targetTeleportZoneId;
// ---- World-mode flags (synced from Game after each transition) ----
bool isDarklands = false;
bool isNight = false;
bool isDawn = false;
NpcCar taxiCarInitialState;
NpcCar taxiCar;
bool taxiArrivedCallbackCalled = false;
+38 -81
View File
@@ -66,9 +66,6 @@ namespace FRG {
std::unordered_map<std::string, sol::protected_function> cutsceneCompleteCallbacks;
std::unordered_map<std::string, sol::protected_function> npcBumpedByPlayerCallbacks;
std::unordered_map<std::string, sol::protected_function> npcBumpsPlayerCallbacks;
std::unordered_map<std::string, int>* globalInts = nullptr;
std::unordered_map<std::string, float>* globalFloats = nullptr;
Quest::QuestJournal* questJournal = nullptr;
sol::protected_function locationInitCallback;
sol::protected_function locationEnterCallback;
sol::protected_function locationExitCallback;
@@ -82,7 +79,7 @@ namespace FRG {
ScriptEngine::ScriptEngine() = default;
ScriptEngine::~ScriptEngine() = default;
void ScriptEngine::init(Location* loc, Inventory* inventory, AudioPlayerAsync* audioPlayer, const std::string& scriptPath) {
void ScriptEngine::init(Location* loc, AudioPlayerAsync* audioPlayer, const std::string& scriptPath) {
impl = std::make_unique<Impl>();
sol::state& lua = impl->lua;
@@ -199,10 +196,10 @@ namespace FRG {
});
api.set_function("has_item", [inventory](const std::string& itemId) {
api.set_function("has_item", [loc](const std::string& itemId) {
const Item* item = ItemRegistry::instance().findById(itemId);
if (item) {
if (inventory->hasItem(itemId)) {
if (loc->hasInventoryItem(itemId)) {
logger() << "[script] has_item: " << item->name << " returns true" << std::endl;
return true;
}
@@ -219,10 +216,10 @@ namespace FRG {
});
// pickup_item(item_id)
api.set_function("pickup_item", [inventory](const std::string& itemId) {
api.set_function("pickup_item", [loc](const std::string& itemId) {
const Item* item = ItemRegistry::instance().findById(itemId);
if (item) {
inventory->addItem(item->id);
loc->addInventoryItem(item->id);
logger() << "[script] pickup_item: " << item->name << std::endl;
} else {
std::cerr << "[script] pickup_item: item '" << itemId << "' not found in ItemRegistry\n";
@@ -230,9 +227,9 @@ namespace FRG {
});
// remove_item(item_id)
api.set_function("remove_item", [loc, inventory](const std::string& id) {
api.set_function("remove_item", [loc](const std::string& id) {
logger() << "[script] remove_item: " << id << std::endl;
inventory->removeItem(id);
loc->removeInventoryItem(id);
});
api.set_function("deactivate_interactive_object", [loc](const std::string& objectId) {
@@ -276,13 +273,13 @@ namespace FRG {
});
// get_inventory_count()
api.set_function("get_inventory_count", [inventory]() {
return inventory->getCount();
api.set_function("get_inventory_count", [loc]() {
return loc->inventoryItemCount();
});
// has_item(item_id)
api.set_function("has_item", [inventory](const std::string& id) {
return inventory->hasItem(id);
api.set_function("has_item", [loc](const std::string& id) {
return loc->hasInventoryItem(id);
});
api.set_function("start_dialogue",
@@ -381,12 +378,12 @@ namespace FRG {
api.set_function("is_night",
[loc]() {
return loc->state.isNight;
return loc->isNight();
});
api.set_function("is_dawn",
[loc]() {
return loc->state.isDawn;
return loc->isDawn();
});
// advance_darklands_hud()
@@ -415,27 +412,26 @@ namespace FRG {
// is_darklands() → bool
api.set_function("is_darklands",
[loc]() { return loc->state.isDarklands; });
[loc]() { return loc->isDarklands(); });
api.set_function("setFloatValue",
[this_impl = impl.get()](const std::string& key, float value) {
if (this_impl->globalFloats)
(*this_impl->globalFloats)[key] = value;
[loc](const std::string& key, float value) {
loc->setGlobalFloat(key, value);
});
api.set_function("getFloatValue",
[this_impl = impl.get()](const std::string& key) -> float {
if (!this_impl->globalFloats) return 0.0f;
auto it = this_impl->globalFloats->find(key);
return it != this_impl->globalFloats->end() ? it->second : 0.0f;
[loc](const std::string& key) -> float {
return loc->globalFloat(key);
});
api.set_function("get_player_x",
[loc]() -> float {
return loc->player ? loc->player->characterState.position.x() : 0.0f;
auto state = loc->player ? loc->player->stateSnapshot() : nullptr;
return state ? state->position.x() : 0.0f;
});
api.set_function("get_player_z",
[loc]() -> float {
return loc->player ? loc->player->characterState.position.z() : 0.0f;
auto state = loc->player ? loc->player->stateSnapshot() : nullptr;
return state ? state->position.z() : 0.0f;
});
// set_player_hp(value) — sets the player's current HP directly.
@@ -462,15 +458,12 @@ namespace FRG {
// Global integer store — shared across all location scripts.
// setIntValue(key, value) / getIntValue(key) → int (0 if not set)
api.set_function("setIntValue",
[this_impl = impl.get()](const std::string& key, int value) {
if (this_impl->globalInts)
(*this_impl->globalInts)[key] = value;
[loc](const std::string& key, int value) {
loc->setGlobalInt(key, value);
});
api.set_function("getIntValue",
[this_impl = impl.get()](const std::string& key) -> int {
if (!this_impl->globalInts) return 0;
auto it = this_impl->globalInts->find(key);
return it != this_impl->globalInts->end() ? it->second : 0;
[loc](const std::string& key) -> int {
return loc->globalInt(key);
});
// set_darklands_callbacks(on_enter, on_exit)
@@ -806,64 +799,40 @@ namespace FRG {
// quest_unlock(quest_id)
api.set_function("quest_unlock",
[this_impl = impl.get()](const std::string& questId) -> bool {
if (!this_impl->questJournal) {
std::cerr << "[script] quest_unlock: QuestJournal not set\n";
return false;
}
return this_impl->questJournal->unlockQuest(questId);
[loc](const std::string& questId) -> bool {
return loc->unlockQuest(questId);
});
// quest_complete(quest_id)
api.set_function("quest_complete",
[this_impl = impl.get()](const std::string& questId) -> bool {
if (!this_impl->questJournal) {
std::cerr << "[script] quest_complete: QuestJournal not set\n";
return false;
}
return this_impl->questJournal->completeQuest(questId);
[loc](const std::string& questId) -> bool {
return loc->completeQuest(questId);
});
// quest_fail(quest_id)
api.set_function("quest_fail",
[this_impl = impl.get()](const std::string& questId) -> bool {
if (!this_impl->questJournal) {
std::cerr << "[script] quest_fail: QuestJournal not set\n";
return false;
}
return this_impl->questJournal->failQuest(questId);
[loc](const std::string& questId) -> bool {
return loc->failQuest(questId);
});
// quest_set_objective_completed(quest_id, objective_id [, completed])
api.set_function("quest_set_objective_completed",
[this_impl = impl.get()](const std::string& questId, const std::string& objId, sol::object completed) -> bool {
if (!this_impl->questJournal) {
std::cerr << "[script] quest_set_objective_completed: QuestJournal not set\n";
return false;
}
[loc](const std::string& questId, const std::string& objId, sol::object completed) -> bool {
const bool val = completed.is<bool>() ? completed.as<bool>() : true;
return this_impl->questJournal->setObjectiveCompleted(questId, objId, val);
return loc->setQuestObjectiveCompleted(questId, objId, val);
});
// quest_set_objective_visible(quest_id, objective_id [, visible])
api.set_function("quest_set_objective_visible",
[this_impl = impl.get()](const std::string& questId, const std::string& objId, sol::object visible) -> bool {
if (!this_impl->questJournal) {
std::cerr << "[script] quest_set_objective_visible: QuestJournal not set\n";
return false;
}
[loc](const std::string& questId, const std::string& objId, sol::object visible) -> bool {
const bool val = visible.is<bool>() ? visible.as<bool>() : true;
return this_impl->questJournal->setObjectiveVisible(questId, objId, val);
return loc->setQuestObjectiveVisible(questId, objId, val);
});
// quest_set_active_objective(quest_id, objective_index)
api.set_function("quest_set_active_objective",
[this_impl = impl.get()](const std::string& questId, int index) -> bool {
if (!this_impl->questJournal) {
std::cerr << "[script] quest_set_active_objective: QuestJournal not set\n";
return false;
}
return this_impl->questJournal->setActiveObjective(questId, index);
[loc](const std::string& questId, int index) -> bool {
return loc->setQuestActiveObjective(questId, index);
});
api.set_function("call_tutorial_taxi_required",
@@ -1033,18 +1002,6 @@ namespace FRG {
}
}
void ScriptEngine::setGlobalStore(std::unordered_map<std::string, int>* store) {
if (impl) impl->globalInts = store;
}
void ScriptEngine::setGlobalFloatStore(std::unordered_map<std::string, float>* store) {
if (impl) impl->globalFloats = store;
}
void ScriptEngine::setQuestJournal(Quest::QuestJournal* journal) {
if (impl) impl->questJournal = journal;
}
void ScriptEngine::callLocationInitCallback() {
if (!impl || !impl->locationInitCallback.valid()) return;
auto result = impl->locationInitCallback();
+1 -8
View File
@@ -1,8 +1,6 @@
#pragma once
#include <string>
#include <memory>
#include <unordered_map>
#include "quest/QuestJournal.h"
#include "external/nlohmann/json.hpp"
#include "ISaveable.h"
#include "AudioPlayerAsync.h"
@@ -10,7 +8,6 @@
namespace FRG {
class Location;
class Inventory;
class ScriptEngine {
public:
@@ -18,7 +15,7 @@ public:
~ScriptEngine();
// Must be called once, after the Game's NPCs are ready.
void init(Location* game, Inventory* inventory, AudioPlayerAsync* audioPlayer, const std::string& scriptPath);
void init(Location* game, AudioPlayerAsync* audioPlayer, const std::string& scriptPath);
// Execute a Lua script file. Logs errors to stderr.
//void runScript(const std::string& path);
@@ -37,10 +34,6 @@ public:
// Fired when a moving NPC physically bumps into the standing player.
void callNpcBumpsPlayerCallback(const std::string& npcId);
void setGlobalStore(std::unordered_map<std::string, int>* store);
void setGlobalFloatStore(std::unordered_map<std::string, float>* store);
void setQuestJournal(Quest::QuestJournal* journal);
void callLocationInitCallback();
void callLocationEnterCallback();
void callLocationExitCallback();
+20 -14
View File
@@ -162,21 +162,24 @@ void DialogueRuntime::selectChoice(int index) {
}
void DialogueRuntime::setFlag(const std::string& name, int value) {
if (flagStore) (*flagStore)[name] = value;
if (setGlobalFlag) setGlobalFlag(name, value);
}
int DialogueRuntime::getFlag(const std::string& name) const {
if (!flagStore) return 0;
auto it = flagStore->find(name);
return (it != flagStore->end()) ? it->second : 0;
return getGlobalFlag ? getGlobalFlag(name) : 0;
}
void DialogueRuntime::setGlobalFlagStore(std::unordered_map<std::string, int>* store) {
flagStore = store;
void DialogueRuntime::setGlobalFlagAccessors(
std::function<int(const std::string&)> getter,
std::function<void(const std::string&, int)> setter)
{
getGlobalFlag = std::move(getter);
setGlobalFlag = std::move(setter);
}
void DialogueRuntime::setQuestJournal(Quest::QuestJournal* journal) {
questJournal = journal;
void DialogueRuntime::setQuestActionHandler(
std::function<void(const QuestActionRequest&)> handler) {
onQuestAction = std::move(handler);
}
void DialogueRuntime::applyQuestActions(
@@ -184,17 +187,20 @@ void DialogueRuntime::applyQuestActions(
const std::string& questFail, const std::string& objectiveComplete,
const std::string& objectiveVisible)
{
if (!questJournal) return;
if (!questUnlock.empty()) questJournal->unlockQuest(questUnlock);
if (!questComplete.empty()) questJournal->completeQuest(questComplete);
if (!questFail.empty()) questJournal->failQuest(questFail);
if (!onQuestAction) return;
if (!questUnlock.empty())
onQuestAction({QuestActionType::Unlock, questUnlock, {}});
if (!questComplete.empty())
onQuestAction({QuestActionType::Complete, questComplete, {}});
if (!questFail.empty())
onQuestAction({QuestActionType::Fail, questFail, {}});
if (!objectiveComplete.empty()) {
auto [qId, oId] = splitDot(objectiveComplete);
questJournal->setObjectiveCompleted(qId, oId);
onQuestAction({QuestActionType::CompleteObjective, qId, oId});
}
if (!objectiveVisible.empty()) {
auto [qId, oId] = splitDot(objectiveVisible);
questJournal->setObjectiveVisible(qId, oId);
onQuestAction({QuestActionType::ShowObjective, qId, oId});
}
}
+22 -6
View File
@@ -2,17 +2,29 @@
#include "dialogue/DialogueDatabase.h"
#include "dialogue/TranslationDatabase.h"
#include "quest/QuestJournal.h"
#include "external/nlohmann/json.hpp"
#include "ISaveable.h"
#include <functional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace FRG::Dialogue {
enum class QuestActionType {
Unlock,
Complete,
Fail,
CompleteObjective,
ShowObjective
};
struct QuestActionRequest {
QuestActionType type;
std::string questId;
std::string objectiveId;
};
class DialogueRuntime : public FRG::ISaveable {
public:
void setDatabase(const DialogueDatabase* value);
@@ -41,8 +53,11 @@ public:
void setFlag(const std::string& name, int value);
int getFlag(const std::string& name) const;
void setGlobalFlagStore(std::unordered_map<std::string, int>* store);
void setQuestJournal(Quest::QuestJournal* journal);
void setGlobalFlagAccessors(
std::function<int(const std::string&)> getter,
std::function<void(const std::string&, int)> setter);
void setQuestActionHandler(
std::function<void(const QuestActionRequest&)> handler);
// Saves whether a dialogue is active, its id, and current node.
// Cutscene state is not saved.
@@ -62,13 +77,14 @@ private:
std::function<void(const std::string&, const std::string&)> onCutsceneStartNeeded;
std::function<void(const std::string&)> onDialogueLineStarted;
std::function<void(const std::string&, bool)> onChatBubbleReady;
std::function<void(const QuestActionRequest&)> onQuestAction;
const DialogueDatabase* database = nullptr;
const TranslationDatabase* translations = nullptr;
Quest::QuestJournal* questJournal = nullptr;
const DialogueDefinition* activeDialogue = nullptr;
std::unordered_map<std::string, int>* flagStore = nullptr;
std::function<int(const std::string&)> getGlobalFlag;
std::function<void(const std::string&, int)> setGlobalFlag;
std::unordered_set<std::string> consumedChoices;
std::string currentNodeId;
+8 -5
View File
@@ -6,7 +6,6 @@
#include "cutscene/CutsceneDatabase.h"
#include "cutscene/CutsceneOverlay.h"
#include "cutscene/CutsceneRuntime.h"
#include "quest/QuestJournal.h"
#include "ISaveable.h"
#include <SDL.h>
#include <functional>
@@ -58,11 +57,15 @@ public:
void setFlag(const std::string& name, int value) { dialogueRuntime.setFlag(name, value); }
int getFlag(const std::string& name) const { return dialogueRuntime.getFlag(name); }
void setGlobalFlagStore(std::unordered_map<std::string, int>* store) {
dialogueRuntime.setGlobalFlagStore(store);
void setGlobalFlagAccessors(
std::function<int(const std::string&)> getter,
std::function<void(const std::string&, int)> setter) {
dialogueRuntime.setGlobalFlagAccessors(
std::move(getter), std::move(setter));
}
void setQuestJournal(Quest::QuestJournal* journal) {
dialogueRuntime.setQuestJournal(journal);
void setQuestActionHandler(
std::function<void(const QuestActionRequest&)> handler) {
dialogueRuntime.setQuestActionHandler(std::move(handler));
}
private:
+66
View File
@@ -94,8 +94,51 @@ struct AppendChatMessageCommand {
};
struct SetTaxiCalledCommand { bool called; };
struct StartTaxiCommand { std::string locationId; };
struct ResetTaxiCommand { std::string locationId; };
struct SetLocationCameraCommand {
std::string locationId;
float azimuth;
float inclination;
};
struct SetLocationTargetsCommand {
std::string locationId;
std::string interactiveObjectId;
std::string npcId;
std::string teleportZoneId;
};
struct SwitchLocationNavigationCommand {
std::string locationId;
int index;
};
struct SetNavigationAreaAvailableCommand {
std::string locationId;
std::string areaName;
bool available;
};
struct AddNavigationObstacleCommand {
std::string locationId;
std::string name;
std::vector<Eigen::Vector2f> polygon;
};
struct AddInventoryItemCommand { std::string itemId; };
struct RemoveInventoryItemCommand { std::string itemId; };
struct UnlockQuestCommand { std::string questId; };
struct CompleteQuestCommand { std::string questId; };
struct FailQuestCommand { std::string questId; };
struct SetQuestObjectiveCompletedCommand {
std::string questId;
std::string objectiveId;
bool completed;
};
struct SetQuestObjectiveVisibleCommand {
std::string questId;
std::string objectiveId;
bool visible;
};
struct SetQuestActiveObjectiveCommand {
std::string questId;
int objectiveIndex;
};
struct SetGlobalIntCommand { std::string key; int value; };
struct SetGlobalFloatCommand { std::string key; float value; };
struct SetCharacterTransformCommand {
@@ -188,8 +231,20 @@ using GameCommand = std::variant<
AppendChatMessageCommand,
SetTaxiCalledCommand,
StartTaxiCommand,
ResetTaxiCommand,
SetLocationCameraCommand,
SetLocationTargetsCommand,
SwitchLocationNavigationCommand,
SetNavigationAreaAvailableCommand,
AddNavigationObstacleCommand,
AddInventoryItemCommand,
RemoveInventoryItemCommand,
UnlockQuestCommand,
CompleteQuestCommand,
FailQuestCommand,
SetQuestObjectiveCompletedCommand,
SetQuestObjectiveVisibleCommand,
SetQuestActiveObjectiveCommand,
SetGlobalIntCommand,
SetGlobalFloatCommand,
SetCharacterTransformCommand,
@@ -209,13 +264,24 @@ using GameCommand = std::variant<
SetTriggerZoneEnabledCommand,
ReloadQuestDefinitionsCommand>;
struct InventoryItemAddedEvent { std::string itemId; };
struct InventoryItemRemovedEvent { std::string itemId; };
using GameServerEvent = std::variant<
InventoryItemAddedEvent,
InventoryItemRemovedEvent,
Quest::QuestEvent>;
class IGameServer {
public:
virtual ~IGameServer() = default;
// Each returned object is immutable and remains valid after later commands.
virtual std::shared_ptr<const GameSnapshot> snapshot() const = 0;
// Client-relative identity assigned by this server connection.
virtual const std::string& localPlayerId() const = 0;
virtual void submit(const GameCommand& command) = 0;
virtual std::vector<GameServerEvent> drainEvents() = 0;
virtual void update(std::int64_t deltaMs) = 0;
// Persistence stays server-side. The JSON shape is GameState's established
+279 -8
View File
@@ -1,12 +1,192 @@
#include "InternalGameServer.h"
#include "../Campaign.h"
#include "../Location.h"
#include <algorithm>
#include <random>
#include <type_traits>
namespace FRG {
namespace {
// Set this to false to disable the battle-zone remote-player test bot.
constexpr bool kEnableBattleZoneTestBot = true;
constexpr const char* kBattleZoneCampaignId = "battle_zone";
constexpr const char* kBattleZoneTestBotId = "battle_zone_test_bot";
constexpr std::int64_t kBattleZoneTestBotIntervalMs = 5000;
}
InternalGameServer::InternalGameServer() = default;
void InternalGameServer::ensureBattleZoneTestBot()
{
if (state_.campaignId != kBattleZoneCampaignId) return;
if (!kEnableBattleZoneTestBot) {
state_.players.erase(kBattleZoneTestBotId);
return;
}
auto& bot = state_.players[kBattleZoneTestBotId];
if (!bot) bot = std::make_shared<CharacterState>();
bot->configureAsPlayer();
bot->npcName = "Test Bot";
}
void InternalGameServer::updateBattleZoneTestBot(std::int64_t deltaMs)
{
if (!kEnableBattleZoneTestBot ||
state_.campaignId != kBattleZoneCampaignId ||
state_.currentLocationName.empty() ||
state_.players.find(kBattleZoneTestBotId) == state_.players.end()) {
return;
}
battleZoneTestBotElapsedMs_ += deltaMs;
if (battleZoneTestBotElapsedMs_ < kBattleZoneTestBotIntervalMs) return;
battleZoneTestBotElapsedMs_ %= kBattleZoneTestBotIntervalMs;
static std::mt19937 randomEngine(std::random_device{}());
static std::uniform_real_distribution<float> coordinate(-10.0f, 10.0f);
submit(SetCharacterDestinationCommand{
state_.currentLocationName,
kBattleZoneTestBotId,
Eigen::Vector3f(coordinate(randomEngine), 0.f, coordinate(randomEngine)),
std::string()});
}
void InternalGameServer::initializeCampaign(
const std::string& campaignId, const std::string& questsPath,
const std::string& zipFile)
{
state_.campaignId = campaignId;
state_.currentLocationName.clear();
state_.locations.clear();
state_.players.clear();
state_.questJournal.loadFromFile(questsPath, zipFile);
state_.globalFloats["player_hp"] = 200.f;
auto player = std::make_shared<CharacterState>();
player->configureAsPlayer();
state_.players.emplace(localPlayerId_, std::move(player));
battleZoneTestBotElapsedMs_ = 0;
ensureBattleZoneTestBot();
publishMutation();
}
void InternalGameServer::clearCampaign()
{
runtimeLocations_.clear();
state_.currentLocationName.clear();
state_.players.clear();
state_.locations.clear();
battleZoneTestBotElapsedMs_ = 0;
publishMutation();
}
void InternalGameServer::resetCampaign(
const std::string& questsPath, const std::string& zipFile)
{
pendingEvents_.clear();
state_.currentLocationName.clear();
state_.isDarklands = false;
state_.isNight = false;
state_.isDawn = false;
state_.playerHp = 200.f;
state_.playerMaxHp = 200.f;
state_.money = 5500;
state_.globalInts.clear();
state_.globalFloats.clear();
state_.globalFloats["player_hp"] = 200.f;
state_.tutorialStep = TutorialStep::AdvanceDialogue;
state_.tutorialPhoneChatScreenOpened = false;
state_.tutorialJournalScreenOpened = false;
state_.tutorialNeedOpenTaxiScreen = false;
state_.tutorialTaxiHintDone = false;
for (int i = 0; i < 3; ++i) {
state_.chatUnread[i] = true;
state_.chatPreviewMsg[i].clear();
state_.chatHistory[i].clear();
}
state_.inventory.clear();
state_.questJournal.loadFromFile(questsPath, zipFile);
state_.locations.clear();
battleZoneTestBotElapsedMs_ = 0;
ensureBattleZoneTestBot();
publishMutation();
}
bool InternalGameServer::prepareLocation(const LocationConfig& config)
{
LocationGameState& authoritative = state_.locations[config.id];
const bool initializeLocation = !authoritative.hasPersistentState;
if (!authoritative.location)
authoritative.location = std::make_shared<LocationState>();
if (authoritative.location->navigationMaps.empty())
authoritative.location->initNavigation(config.navigationJsonPaths);
authoritative.lockInteractiveObjectsDuringTutorial =
config.lockInteractiveObjectsDuringTutorial;
if (config.taxi) {
const LocationConfig::Taxi& taxi = *config.taxi;
LocationState& locationState = *authoritative.location;
locationState.taxiCarTriggerPosition = taxi.triggerPosition;
locationState.taxiCarWaypoints = taxi.waypoints;
locationState.taxiCarInitialState.position = taxi.initialPosition;
locationState.taxiCarInitialState.rotation = taxi.initialRotation;
locationState.taxiCarInitialState.mode = NpcCar::Mode::NONE;
locationState.taxiCarInitialState.velocity = 0.f;
locationState.taxiCarInitialState.steeringAngle = 0.f;
locationState.taxiCarInitialState.waypointReachRadius =
taxi.waypointReachRadius;
locationState.taxiCar.waypointReachRadius = taxi.waypointReachRadius;
if (initializeLocation) authoritative.resetTaxi();
}
publishMutation();
return initializeLocation;
}
void InternalGameServer::setupRuntimeLocation(
const std::string& id, const std::shared_ptr<Location>& location,
LocationSetup params, AudioPlayerAsync* audioPlayer)
{
auto stored = state_.locations.find(id);
if (stored == state_.locations.end())
throw std::runtime_error("Location state was not prepared");
params.gameState = &stored->second;
location->setup(params, audioPlayer);
publishMutation();
}
Location* InternalGameServer::runtimeLocation(const std::string& id) const
{
auto it = runtimeLocations_.find(id);
return it != runtimeLocations_.end() ? it->second.get() : nullptr;
}
bool InternalGameServer::hasRuntimeLocation(const std::string& id) const
{
return runtimeLocations_.count(id) > 0;
}
std::vector<std::string> InternalGameServer::loadedRuntimeLocationIds() const
{
std::vector<std::string> result;
result.reserve(runtimeLocations_.size());
for (const auto& [id, location] : runtimeLocations_) result.push_back(id);
return result;
}
void InternalGameServer::clearRuntimeLocations(bool retainState)
{
if (retainState) {
synchronizeRuntimeAuxiliaryState();
} else {
for (const auto& [id, location] : runtimeLocations_)
state_.locations.erase(id);
publishMutation();
}
runtimeLocations_.clear();
}
GameSnapshot InternalGameServer::buildSnapshot() const
{
GameSnapshot result;
@@ -85,7 +265,11 @@ std::shared_ptr<const GameSnapshot> InternalGameServer::snapshot() const
void InternalGameServer::submit(const GameCommand& command)
{
std::visit([this](const auto& value) {
auto enqueueQuestEvents = [this](Quest::QuestMutationResult result) {
for (Quest::QuestEvent& event : result.events)
pendingEvents_.emplace_back(std::move(event));
};
std::visit([this, &enqueueQuestEvents](const auto& value) {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, SetWorldModeCommand>) {
state_.isDarklands = value.darklands;
@@ -126,10 +310,83 @@ void InternalGameServer::submit(const GameCommand& command)
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end())
location->second.startTaxi();
} else if constexpr (std::is_same_v<T, ResetTaxiCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end())
location->second.resetTaxi();
} else if constexpr (std::is_same_v<T, SetLocationCameraCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) {
location->second.location->cameraAzimuth = value.azimuth;
location->second.location->cameraInclination = value.inclination;
}
} else if constexpr (std::is_same_v<T, SetLocationTargetsCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) {
location->second.location->targetInteractiveObjectId =
value.interactiveObjectId;
location->second.location->targetInteractNpcId = value.npcId;
location->second.location->targetTeleportZoneId =
value.teleportZoneId;
}
} else if constexpr (std::is_same_v<T, SwitchLocationNavigationCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location &&
value.index >= 0 &&
value.index < static_cast<int>(
location->second.location->navigationMaps.size())) {
LocationState& locationState = *location->second.location;
locationState.activeNavigationIndex = value.index;
for (const auto& [id, player] : state_.players)
if (player) player->forceReplan(locationState, id);
for (const auto& [id, npc] : location->second.npcs)
if (npc) npc->forceReplan(locationState, id);
}
} else if constexpr (std::is_same_v<T, SetNavigationAreaAvailableCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) {
LocationState& locationState = *location->second.location;
const int index = locationState.activeNavigationIndex;
if (index >= 0 &&
index < static_cast<int>(locationState.navigationMaps.size())) {
locationState.navigationMaps[index].setAreaAvailable(
value.areaName, value.available);
}
}
} else if constexpr (std::is_same_v<T, AddNavigationObstacleCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) {
LocationState& locationState = *location->second.location;
const int index = locationState.activeNavigationIndex;
if (index >= 0 &&
index < static_cast<int>(locationState.navigationMaps.size())) {
PathFinder::ObstaclePolygon polygon;
polygon.name = value.name;
polygon.polygon = value.polygon;
locationState.navigationMaps[index].addObstaclePolygon(polygon);
}
}
} else if constexpr (std::is_same_v<T, AddInventoryItemCommand>) {
state_.inventory.addItem(value.itemId);
if (state_.inventory.addItem(value.itemId))
pendingEvents_.emplace_back(InventoryItemAddedEvent{value.itemId});
} else if constexpr (std::is_same_v<T, RemoveInventoryItemCommand>) {
state_.inventory.removeItem(value.itemId);
if (state_.inventory.removeItem(value.itemId))
pendingEvents_.emplace_back(InventoryItemRemovedEvent{value.itemId});
} else if constexpr (std::is_same_v<T, UnlockQuestCommand>) {
enqueueQuestEvents(state_.questJournal.unlockQuest(value.questId));
} else if constexpr (std::is_same_v<T, CompleteQuestCommand>) {
enqueueQuestEvents(state_.questJournal.completeQuest(value.questId));
} else if constexpr (std::is_same_v<T, FailQuestCommand>) {
enqueueQuestEvents(state_.questJournal.failQuest(value.questId));
} else if constexpr (std::is_same_v<T, SetQuestObjectiveCompletedCommand>) {
enqueueQuestEvents(state_.questJournal.setObjectiveCompleted(
value.questId, value.objectiveId, value.completed));
} else if constexpr (std::is_same_v<T, SetQuestObjectiveVisibleCommand>) {
enqueueQuestEvents(state_.questJournal.setObjectiveVisible(
value.questId, value.objectiveId, value.visible));
} else if constexpr (std::is_same_v<T, SetQuestActiveObjectiveCommand>) {
enqueueQuestEvents(state_.questJournal.setActiveObjective(
value.questId, value.objectiveIndex));
} else if constexpr (std::is_same_v<T, SetGlobalIntCommand>) {
state_.globalInts[value.key] = value.value;
} else if constexpr (std::is_same_v<T, SetGlobalFloatCommand>) {
@@ -225,7 +482,14 @@ void InternalGameServer::submit(const GameCommand& command)
state_.questJournal.reloadDefinitionsPreservingState(value.path, value.zipFile);
}
}, command);
markHostMutation();
publishMutation();
}
std::vector<GameServerEvent> InternalGameServer::drainEvents()
{
std::vector<GameServerEvent> result;
result.swap(pendingEvents_);
return result;
}
void InternalGameServer::update(std::int64_t deltaMs)
@@ -234,11 +498,15 @@ void InternalGameServer::update(std::int64_t deltaMs)
if (runtime == runtimeLocations_.end() || !runtime->second) return;
auto stored = state_.locations.find(state_.currentLocationName);
if (stored == state_.locations.end()) return;
updateBattleZoneTestBot(deltaMs);
const bool interactiveObjectsLocked =
stored->second.lockInteractiveObjectsDuringTutorial &&
state_.tutorialStep < TutorialStep::PickUpItem;
LocationSimulationEvents events =
stored->second.update(deltaMs, state_.players, interactiveObjectsLocked);
// Runtime presentation reads through snapshots, so publish the simulation
// tick before forwarding its value-only events to the loaded location.
publishMutation();
runtime->second->update(deltaMs, events);
synchronizeRuntimeAuxiliaryState();
}
@@ -251,9 +519,12 @@ void InternalGameServer::save(nlohmann::json& out) const
void InternalGameServer::load(const nlohmann::json& in)
{
pendingEvents_.clear();
state_.load(in);
battleZoneTestBotElapsedMs_ = 0;
ensureBattleZoneTestBot();
applyStateToRuntime();
markHostMutation();
publishMutation();
}
void InternalGameServer::bindRuntimeLocation(const std::string& id, std::shared_ptr<Location> location)
@@ -265,7 +536,7 @@ void InternalGameServer::bindRuntimeLocation(const std::string& id, std::shared_
} else {
runtimeLocations_[id]->captureAuxiliaryState(state_.locations[id]);
}
markHostMutation();
publishMutation();
}
void InternalGameServer::synchronizeRuntimeAuxiliaryState()
@@ -274,7 +545,7 @@ void InternalGameServer::synchronizeRuntimeAuxiliaryState()
if (!location) continue;
location->captureAuxiliaryState(state_.locations[id]);
}
markHostMutation();
publishMutation();
}
void InternalGameServer::applyStateToRuntime()
@@ -286,7 +557,7 @@ void InternalGameServer::applyStateToRuntime()
}
}
void InternalGameServer::markHostMutation()
void InternalGameServer::publishMutation()
{
++revision_;
cachedSnapshot_.reset();
+27 -8
View File
@@ -2,11 +2,15 @@
#include "IGameServer.h"
class AudioPlayerAsync;
namespace FRG {
class Game;
class Character;
class Location;
struct LocationConfig;
struct LocationSetup;
// In-process implementation used by single-player. GameState exists exactly
// once, here; clients see immutable snapshots and mutate it with typed commands.
@@ -15,7 +19,9 @@ public:
InternalGameServer();
std::shared_ptr<const GameSnapshot> snapshot() const override;
const std::string& localPlayerId() const override { return localPlayerId_; }
void submit(const GameCommand& command) override;
std::vector<GameServerEvent> drainEvents() override;
void update(std::int64_t deltaMs) override;
void save(nlohmann::json& out) const override;
void load(const nlohmann::json& in) override;
@@ -23,25 +29,38 @@ public:
private:
friend class Game;
// Transitional host operations are intentionally available only to Game,
// which owns location loading and old-save migration. They are never part of
// IGameServer and therefore cannot leak mutable state to UI/network clients.
GameState& hostState() { return state_; }
const GameState& hostState() const { return state_; }
std::unordered_map<std::string, std::shared_ptr<Location>>& runtimeLocations() { return runtimeLocations_; }
const std::unordered_map<std::string, std::shared_ptr<Location>>& runtimeLocations() const { return runtimeLocations_; }
// Host-only lifecycle operations. They never expose authoritative state and
// are intentionally absent from IGameServer.
void initializeCampaign(
const std::string& campaignId, const std::string& questsPath,
const std::string& zipFile);
void clearCampaign();
void resetCampaign(const std::string& questsPath, const std::string& zipFile);
bool prepareLocation(const LocationConfig& config);
void setupRuntimeLocation(
const std::string& id, const std::shared_ptr<Location>& location,
LocationSetup params, AudioPlayerAsync* audioPlayer);
Location* runtimeLocation(const std::string& id) const;
bool hasRuntimeLocation(const std::string& id) const;
std::vector<std::string> loadedRuntimeLocationIds() const;
void clearRuntimeLocations(bool retainState);
void bindRuntimeLocation(const std::string& id, std::shared_ptr<Location> location);
void synchronizeRuntimeAuxiliaryState();
void applyStateToRuntime();
void markHostMutation();
void publishMutation();
void ensureBattleZoneTestBot();
void updateBattleZoneTestBot(std::int64_t deltaMs);
GameSnapshot buildSnapshot() const;
GameState state_;
std::string localPlayerId_ = "player";
std::int64_t battleZoneTestBotElapsedMs_ = 0;
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_;
std::uint64_t revision_ = 1;
mutable std::uint64_t cachedRevision_ = 0;
mutable std::shared_ptr<const GameSnapshot> cachedSnapshot_;
std::vector<GameServerEvent> pendingEvents_;
};
} // namespace FRG
+5 -4
View File
@@ -8,19 +8,20 @@
namespace FRG {
void Inventory::addItem(const std::string& itemId) {
bool Inventory::addItem(const std::string& itemId) {
itemIds.push_back(itemId);
logger() << "Item added to inventory: " << itemId << std::endl;
if (onItemAdded) onItemAdded(itemId);
return true;
}
void Inventory::removeItem(const std::string& itemId) {
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;
if (onItemRemoved) onItemRemoved(itemId);
itemIds.erase(it);
return true;
}
return false;
}
bool Inventory::hasItem(const std::string& itemId) const {
+2 -7
View File
@@ -2,7 +2,6 @@
#include <string>
#include <memory>
#include <vector>
#include <functional>
#include "ISaveable.h"
namespace FRG {
@@ -22,8 +21,8 @@ namespace FRG {
class Inventory : public ISaveable {
public:
void addItem(const std::string& itemId);
void removeItem(const std::string& itemId);
bool addItem(const std::string& itemId);
bool removeItem(const std::string& itemId);
// Resolves current items from ItemRegistry by id, so returned data
// always reflects the currently loaded (localized) definitions.
std::vector<Item> getItems() const;
@@ -34,10 +33,6 @@ namespace FRG {
void save(nlohmann::json& out) const override;
void load(const nlohmann::json& in) override;
// Called whenever an item is added/removed; receives the item id.
std::function<void(const std::string&)> onItemAdded;
std::function<void(const std::string&)> onItemRemoved;
private:
std::vector<std::string> itemIds;
};
+43 -26
View File
@@ -106,38 +106,48 @@ bool QuestJournal::setStatus(const std::string& questId, QuestStatus status) {
return true;
}
bool QuestJournal::unlockQuest(const std::string& questId) {
QuestMutationResult QuestJournal::unlockQuest(const std::string& questId) {
QuestMutationResult result;
QuestState* quest = findQuest(questId);
if (!quest) return false;
if (!quest) return result;
result.success = true;
if (quest->status == QuestStatus::Hidden) {
quest->status = QuestStatus::Available;
if (onQuestUnlocked) onQuestUnlocked(questId);
result.events.push_back({QuestEventType::Unlocked, questId, {}});
}
return true;
return result;
}
bool QuestJournal::completeQuest(const std::string& questId) {
bool changed = setStatus(questId, QuestStatus::Completed);
if (changed && onQuestCompleted) onQuestCompleted(questId);
return changed;
QuestMutationResult QuestJournal::completeQuest(const std::string& questId) {
QuestMutationResult result;
result.success = setStatus(questId, QuestStatus::Completed);
if (result.success)
result.events.push_back({QuestEventType::Completed, questId, {}});
return result;
}
bool QuestJournal::failQuest(const std::string& questId) {
bool changed = setStatus(questId, QuestStatus::Failed);
if (changed && onQuestFailed) onQuestFailed(questId);
return changed;
QuestMutationResult QuestJournal::failQuest(const std::string& questId) {
QuestMutationResult result;
result.success = setStatus(questId, QuestStatus::Failed);
if (result.success)
result.events.push_back({QuestEventType::Failed, questId, {}});
return result;
}
bool QuestJournal::setObjectiveCompleted(const std::string& questId, const std::string& objectiveId, bool completed) {
QuestMutationResult QuestJournal::setObjectiveCompleted(
const std::string& questId, const std::string& objectiveId, bool completed) {
QuestMutationResult result;
QuestState* quest = findQuest(questId);
if (!quest) return false;
if (!quest) return result;
for (auto& objective : quest->definition.objectives) {
if (objective.id == objectiveId) {
objective.completed = completed;
result.success = true;
if (completed) {
if (onObjectiveCompleted) onObjectiveCompleted(questId, objectiveId);
result.events.push_back(
{QuestEventType::ObjectiveCompleted, questId, objectiveId});
}
if (completed && quest->definition.autoComplete && quest->status == QuestStatus::Available) {
@@ -147,41 +157,48 @@ bool QuestJournal::setObjectiveCompleted(const std::string& questId, const std::
[](const QuestObjective& o) { return o.completed; });
if (allDone) {
quest->status = QuestStatus::Completed;
if (onQuestCompleted) onQuestCompleted(questId);
result.events.push_back(
{QuestEventType::Completed, questId, {}});
}
}
return true;
return result;
}
}
return false;
return result;
}
bool QuestJournal::setObjectiveVisible(const std::string& questId, const std::string& objectiveId, bool visible) {
QuestMutationResult QuestJournal::setObjectiveVisible(
const std::string& questId, const std::string& objectiveId, bool visible) {
QuestMutationResult result;
QuestState* quest = findQuest(questId);
if (!quest) return false;
if (!quest) return result;
for (auto& objective : quest->definition.objectives) {
if (objective.id == objectiveId) {
objective.visible = visible;
return true;
result.success = true;
return result;
}
}
return false;
return result;
}
bool QuestJournal::setActiveObjective(const std::string& questId, int objectiveIndex) {
QuestMutationResult QuestJournal::setActiveObjective(
const std::string& questId, int objectiveIndex) {
QuestMutationResult result;
QuestState* quest = findQuest(questId);
if (!quest) return false;
if (!quest) return result;
if (objectiveIndex < 0 || objectiveIndex >= static_cast<int>(quest->definition.objectives.size())) {
return false;
return result;
}
quest->activeObjectiveIndex = objectiveIndex;
return true;
result.success = true;
return result;
}
QuestState* QuestJournal::findQuest(const std::string& questId) {
+24 -13
View File
@@ -6,10 +6,27 @@
#include <string>
#include <unordered_map>
#include <vector>
#include <functional>
namespace FRG::Quest {
enum class QuestEventType {
Unlocked,
Completed,
Failed,
ObjectiveCompleted
};
struct QuestEvent {
QuestEventType type;
std::string questId;
std::string objectiveId;
};
struct QuestMutationResult {
bool success = false;
std::vector<QuestEvent> events;
};
class QuestJournal : public FRG::ISaveable {
public:
bool loadFromFile(const std::string& path, const std::string& zipFile = "");
@@ -22,19 +39,13 @@ public:
void save(nlohmann::json& out) const override;
void load(const nlohmann::json& in) override;
// Event callbacks — set by the owner (MenuManager) to receive notifications.
std::function<void(const std::string& questId)> onQuestUnlocked;
std::function<void(const std::string& questId)> onQuestCompleted;
std::function<void(const std::string& questId)> onQuestFailed;
std::function<void(const std::string& questId, const std::string& objectiveId)> onObjectiveCompleted;
QuestMutationResult unlockQuest(const std::string& questId);
QuestMutationResult completeQuest(const std::string& questId);
QuestMutationResult failQuest(const std::string& questId);
bool unlockQuest(const std::string& questId);
bool completeQuest(const std::string& questId);
bool failQuest(const std::string& questId);
bool setObjectiveCompleted(const std::string& questId, const std::string& objectiveId, bool completed = true);
bool setObjectiveVisible(const std::string& questId, const std::string& objectiveId, bool visible = true);
bool setActiveObjective(const std::string& questId, int objectiveIndex);
QuestMutationResult setObjectiveCompleted(const std::string& questId, const std::string& objectiveId, bool completed = true);
QuestMutationResult setObjectiveVisible(const std::string& questId, const std::string& objectiveId, bool visible = true);
QuestMutationResult setActiveObjective(const std::string& questId, int objectiveIndex);
QuestState* findQuest(const std::string& questId);
const QuestState* findQuest(const std::string& questId) const;
+8 -6
View File
@@ -100,13 +100,15 @@ namespace FRG {
break;
}
auto state = player.stateSnapshot();
if (!state) return;
std::vector<Eigen::Vector3f> route;
route.reserve(player.characterState.pathWaypoints.size() + 1);
route.push_back(Eigen::Vector3f(player.characterState.position.x(), 0.0f, player.characterState.position.z()));
route.reserve(state->pathWaypoints.size() + 1);
route.push_back(Eigen::Vector3f(state->position.x(), 0.0f, state->position.z()));
const size_t firstWaypoint = (std::min)(
player.characterState.currentWaypointIndex, player.characterState.pathWaypoints.size());
for (size_t i = firstWaypoint; i < player.characterState.pathWaypoints.size(); ++i) {
const Eigen::Vector3f& waypoint = player.characterState.pathWaypoints[i];
state->currentWaypointIndex, state->pathWaypoints.size());
for (size_t i = firstWaypoint; i < state->pathWaypoints.size(); ++i) {
const Eigen::Vector3f& waypoint = state->pathWaypoints[i];
route.push_back(Eigen::Vector3f(waypoint.x(), 0.0f, waypoint.z()));
}
@@ -127,7 +129,7 @@ namespace FRG {
constexpr float kPlayerClearanceMs = 20.0f;
constexpr float kPlayerMaximumSpeedMultiplier = 1.5f;
const float playerSpeed = (std::max)(
player.characterState.walkSpeed * kPlayerMaximumSpeedMultiplier, 0.01f);
state->walkSpeed * kPlayerMaximumSpeedMultiplier, 0.01f);
const float playerRouteTimeMs = totalLength / playerSpeed * 1000.0f;
const float revealDurationMs = (std::min)(kMaximumRevealMs, playerRouteTimeMs / 3.0f);
const float waveSpeed = totalLength / (revealDurationMs / 1000.0f);