Skip to content
2 changes: 1 addition & 1 deletion .claude/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ _Read this at every session start (after git sync). Each row links to a detailed
- **Game modules**: 10 (SparkGame, FPS, MMO, RPG, ARPG, RTS, Racing, Platformer, OpenWorld, VisualScript)
- **Infrastructure**: JobSystem wired, DeferredDeletionQueue in RHI, collision layer filtering, EntityEventBus cleanup, archetype spawn overrides
- **Gameplay**: TimeOfDaySystem, AI enemies in SparkGame, WeatherSystem integration
- **Codebase**: ~487K lines of C++ across 1505 source files, 103 wiki pages
- **Codebase**: ~488K lines of C++ across 1507 source files, 103 wiki pages

### Before Writing Code

Expand Down
8 changes: 4 additions & 4 deletions .github/badges/files.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"label": "source files",
"message": "1505",
"color": "green"
"schemaVersion": 1,
"label": "source files",
"message": "1507",
"color": "green"
}
18 changes: 9 additions & 9 deletions .github/badges/loc-breakdown.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"schemaVersion": 1,
"total": 487368,
"files": 1505,
"engine": 249851,
"editor": 82920,
"game": 57465,
"tests": 94741,
"tools": 2391,
"updated": "2026-04-05T20:29:28Z"
"schemaVersion": 1,
"total": 488552,
"files": 1507,
"engine": 250647,
"editor": 82949,
"game": 57824,
"tests": 94741,
"tools": 2391,
"updated": "2026-04-05T22:09:44Z"
}
12 changes: 6 additions & 6 deletions .github/badges/loc.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"schemaVersion": 1,
"label": "C++ lines of code",
"message": "487368",
"color": "blue",
"namedLogo": "cplusplus",
"logoColor": "white"
"schemaVersion": 1,
"label": "C++ lines of code",
"message": "488552",
"color": "blue",
"namedLogo": "cplusplus",
"logoColor": "white"
}
21 changes: 21 additions & 0 deletions GameModules/SparkGame/Source/Core/Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
#include "GameplayShowcase.h"
#include "Utils/SparkConsole.h"
#include "Utils/LogMacros.h"
#include "Utils/InvalidStateDetector.h"
#include "Engine/ECS/Components.h"
#include "Engine/ECS/Components/GameplayComponents.h"

#ifdef SPARK_PLATFORM_WINDOWS
#include <windows.h>
Expand Down Expand Up @@ -80,6 +83,24 @@ bool SparkGameDefaultModule::OnLoad(Spark::IEngineContext* context)

RegisterConsoleCommands();

// Register base game state validation rules
Spark::InvalidStateDetector::GetInstance().AddRule(
{"Base.HealthInvariant", "Base", Spark::StateViolationSeverity::Error, true,
[](World& w, std::vector<Spark::StateViolation>& out)
{
for (auto entity : w.GetEntitiesWith<HealthComponent>())
{
auto* h = w.GetComponent<HealthComponent>(entity);
if (h && h->maxHealth > 0.0f && h->health > h->maxHealth * 1.5f)
{
out.push_back({"Base.HealthInvariant", static_cast<uint32_t>(entity),
"health=" + std::to_string(h->health) +
" significantly exceeds maxHealth=" + std::to_string(h->maxHealth),
Spark::StateViolationSeverity::Error});
}
}
}});

m_initialized = true;
SPARK_LOG_INFO(Spark::LogCategory::Game, "SparkGame showcase module loaded successfully");
console.LogInfo("[Default] Spark Engine Showcase module loaded successfully");
Expand Down
47 changes: 47 additions & 0 deletions GameModules/SparkGameARPG/Source/Core/Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
#include "Engine/SaveSystem/SaveSystem.h"
#include "Utils/SparkConsole.h"
#include "Utils/LogMacros.h"
#include "Utils/InvalidStateDetector.h"
#include "Engine/ECS/Components.h"
#include "Engine/ECS/Components/GameplayComponents.h"
#include "Engine/ECS/Components/AIComponents.h"
#include "Engine/ECS/Components/PhysicsComponents.h"

#ifdef SPARK_PLATFORM_WINDOWS
#include <windows.h>
Expand Down Expand Up @@ -133,6 +138,48 @@ bool SparkGameARPGModule::OnLoad(Spark::IEngineContext* context)

RegisterConsoleCommands();

// Register ARPG-specific state validation rules
auto& stateDetector = Spark::InvalidStateDetector::GetInstance();

stateDetector.AddRule({"ARPG.DeadMobTargeting", "ARPG", Spark::StateViolationSeverity::Error, true,
[](World& w, std::vector<Spark::StateViolation>& out)
{
for (auto entity : w.GetEntitiesWith<HealthComponent, AIComponent>())
{
auto* h = w.GetComponent<HealthComponent>(entity);
auto* ai = w.GetComponent<AIComponent>(entity);
if (h && ai && h->isDead && ai->targetEntity != entt::null)
{
out.push_back({"ARPG.DeadMobTargeting", static_cast<uint32_t>(entity),
"Dead monster still has a target assigned",
Spark::StateViolationSeverity::Error});
}
}
}});

stateDetector.AddRule(
{"ARPG.StaticBodyDynamic", "ARPG", Spark::StateViolationSeverity::Warning, true,
[](World& w, std::vector<Spark::StateViolation>& out)
{
for (auto entity : w.GetEntitiesWith<RigidBodyComponent, HealthComponent>())
{
auto* rb = w.GetComponent<RigidBodyComponent>(entity);
auto* h = w.GetComponent<HealthComponent>(entity);
if (rb && h && h->isDead && rb->type == RigidBodyComponent::Type::Dynamic && rb->mass > 0.0f)
{
float speedSq = rb->linearVelocity.x * rb->linearVelocity.x +
rb->linearVelocity.y * rb->linearVelocity.y +
rb->linearVelocity.z * rb->linearVelocity.z;
if (speedSq > 25.0f)
{
out.push_back({"ARPG.StaticBodyDynamic", static_cast<uint32_t>(entity),
"Dead entity moving at high speed (speedSq=" + std::to_string(speedSq) + ")",
Spark::StateViolationSeverity::Warning});
}
}
}
}});

m_initialized = true;
SPARK_LOG_INFO(Spark::LogCategory::Game, "ARPG module loaded successfully — 7 subsystems active");
console.LogInfo("[ARPG] Spark ARPG module loaded successfully (7 subsystems)");
Expand Down
46 changes: 46 additions & 0 deletions GameModules/SparkGameFPS/Source/Core/Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
#include "Engine/SaveSystem/SaveSystem.h"
#include "Engine/Cinematic/Sequencer.h"
#include "Engine/Replay/ReplaySystem.h"
#include "Utils/InvalidStateDetector.h"
#include "Engine/ECS/Components.h"
#include "Engine/ECS/Components/GameplayComponents.h"
#include "Engine/ECS/Components/AIComponents.h"
#include "Engine/ECS/Components/PhysicsComponents.h"

// Global game pointer used by SparkConsole (in SparkEngineLib) to call into
// game systems. Owned by SparkGameModule; set during Initialize, cleared
Expand Down Expand Up @@ -206,6 +211,47 @@ bool SparkGameModule::Initialize(GraphicsEngine* graphics, InputManager* input)
// Register game-specific console commands
RegisterGameConsoleCommands();

// Register FPS-specific state validation rules
auto& stateDetector = Spark::InvalidStateDetector::GetInstance();

stateDetector.AddRule(
{"FPS.DeadPlayerMoving", "FPS", Spark::StateViolationSeverity::Error, true,
[](World& w, std::vector<Spark::StateViolation>& out)
{
for (auto entity : w.GetEntitiesWith<HealthComponent, RigidBodyComponent>())
{
auto* h = w.GetComponent<HealthComponent>(entity);
auto* rb = w.GetComponent<RigidBodyComponent>(entity);
if (!h || !rb || !h->isDead || rb->type != RigidBodyComponent::Type::Dynamic)
continue;
float speedSq =
rb->linearVelocity.x * rb->linearVelocity.x + rb->linearVelocity.z * rb->linearVelocity.z;
if (speedSq > 1.0f)
{
out.push_back({"FPS.DeadPlayerMoving", static_cast<uint32_t>(entity),
"Dead entity moving horizontally (speedSq=" + std::to_string(speedSq) + ")",
Spark::StateViolationSeverity::Error});
}
}
}});

stateDetector.AddRule(
{"FPS.DeadAICombat", "FPS", Spark::StateViolationSeverity::Error, true,
[](World& w, std::vector<Spark::StateViolation>& out)
{
for (auto entity : w.GetEntitiesWith<HealthComponent, AIComponent>())
{
auto* h = w.GetComponent<HealthComponent>(entity);
auto* ai = w.GetComponent<AIComponent>(entity);
if (h && ai && h->isDead &&
(ai->state == AIComponent::State::Combat || ai->state == AIComponent::State::Alert))
{
out.push_back({"FPS.DeadAICombat", static_cast<uint32_t>(entity), "Dead AI in combat/alert state",
Spark::StateViolationSeverity::Error});
}
}
}});

m_initialized = true;
console.LogSuccess("SparkGameFPS module initialized");
return true;
Expand Down
14 changes: 14 additions & 0 deletions GameModules/SparkGameFPS/Source/Game/Terrain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,17 @@ HRESULT Terrain::Initialize(ID3D11Device* device, ID3D11DeviceContext* ctx, cons
return E_FAIL;
}
in.seekg(54);
if (!in)
{
SPARK_LOG_ERROR(Spark::LogCategory::Game, "Failed to seek past BMP header in heightmap");
return E_FAIL;
}
in.read(reinterpret_cast<char*>(data.data()), static_cast<std::streamsize>(w) * h);
if (!in)
{
SPARK_LOG_ERROR(Spark::LogCategory::Game, "Failed to read heightmap data (%u x %u bytes)", w, h);
return E_FAIL;
}
in.close();

// 2) Build mesh
Expand Down Expand Up @@ -208,6 +218,10 @@ HRESULT Terrain::Initialize(ID3D11Device* /*device*/, ID3D11DeviceContext* /*ctx
{
in.seekg(54);
in.read(reinterpret_cast<char*>(data.data()), static_cast<std::streamsize>(desc.width) * desc.height);
if (!in)
{
SPARK_LOG_WARN(Spark::LogCategory::Game, "Failed to read heightmap data, using flat terrain");
}
}
}

Expand Down
39 changes: 39 additions & 0 deletions GameModules/SparkGameMMO/Source/Core/Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
#include "MMOEngineSystems.h"
#include "Utils/SparkConsole.h"
#include "Utils/LogMacros.h"
#include "Utils/InvalidStateDetector.h"
#include "Engine/ECS/Components.h"
#include "Engine/ECS/Components/GameplayComponents.h"
#include "Engine/ECS/Components/NetworkComponents.h"

#ifdef SPARK_PLATFORM_WINDOWS
#include <windows.h>
Expand Down Expand Up @@ -225,6 +229,41 @@ bool SparkGameMMOModule::OnLoad(Spark::IEngineContext* context)
}
#endif

// Register MMO-specific state validation rules
auto& stateDetector = Spark::InvalidStateDetector::GetInstance();

stateDetector.AddRule({"MMO.DeadWithNetwork", "MMO", Spark::StateViolationSeverity::Error, true,
[](World& w, std::vector<Spark::StateViolation>& out)
{
for (auto entity : w.GetEntitiesWith<HealthComponent, NetworkIdentity>())
{
auto* h = w.GetComponent<HealthComponent>(entity);
auto* ni = w.GetComponent<NetworkIdentity>(entity);
if (h && ni && h->isDead && !h->deathProcessed && ni->isLocalAuthority)
{
out.push_back({"MMO.DeadWithNetwork", static_cast<uint32_t>(entity),
"Local-authority entity dead but deathProcessed=false",
Spark::StateViolationSeverity::Error});
}
}
}});

stateDetector.AddRule({"MMO.HealthOverMax", "MMO", Spark::StateViolationSeverity::Warning, true,
[](World& w, std::vector<Spark::StateViolation>& out)
{
for (auto entity : w.GetEntitiesWith<HealthComponent>())
{
auto* h = w.GetComponent<HealthComponent>(entity);
if (h && !h->isDead && h->health > h->maxHealth * 1.01f)
{
out.push_back({"MMO.HealthOverMax", static_cast<uint32_t>(entity),
"health=" + std::to_string(h->health) +
" exceeds maxHealth=" + std::to_string(h->maxHealth),
Spark::StateViolationSeverity::Warning});
}
}
}});

m_initialized = true;
SPARK_LOG_INFO(Spark::LogCategory::Game, "SparkGameMMO module loaded: 17 subsystems initialized");
console.LogInfo("[MMO] Spark MMO module loaded successfully (17 subsystems)");
Expand Down
39 changes: 39 additions & 0 deletions GameModules/SparkGameOpenWorld/Source/Core/Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
#include "Events/OWDynamicEventSystem.h"
#include "Utils/SparkConsole.h"
#include "Utils/LogMacros.h"
#include "Utils/InvalidStateDetector.h"
#include "Engine/ECS/Components.h"
#include "Engine/ECS/Components/GameplayComponents.h"
#include "Engine/ECS/Components/AIComponents.h"

#ifdef SPARK_PLATFORM_WINDOWS
#include <windows.h>
Expand Down Expand Up @@ -139,6 +143,41 @@ bool SparkGameOpenWorldModule::OnLoad(Spark::IEngineContext* context)

RegisterConsoleCommands();

// Register OpenWorld-specific state validation rules
auto& stateDetector = Spark::InvalidStateDetector::GetInstance();

stateDetector.AddRule(
{"OpenWorld.DeadWildlife", "OpenWorld", Spark::StateViolationSeverity::Warning, true,
[](World& w, std::vector<Spark::StateViolation>& out)
{
for (auto entity : w.GetEntitiesWith<HealthComponent, AIComponent>())
{
auto* h = w.GetComponent<HealthComponent>(entity);
auto* ai = w.GetComponent<AIComponent>(entity);
if (h && ai && h->isDead &&
(ai->state == AIComponent::State::Patrolling || ai->state == AIComponent::State::Alert))
{
out.push_back({"OpenWorld.DeadWildlife", static_cast<uint32_t>(entity),
"Dead wildlife AI still patrolling/alert", Spark::StateViolationSeverity::Warning});
}
}
}});

stateDetector.AddRule({"OpenWorld.MaxHealthZero", "OpenWorld", Spark::StateViolationSeverity::Error, true,
[](World& w, std::vector<Spark::StateViolation>& out)
{
for (auto entity : w.GetEntitiesWith<HealthComponent>())
{
auto* h = w.GetComponent<HealthComponent>(entity);
if (h && h->maxHealth <= 0.0f)
{
out.push_back({"OpenWorld.MaxHealthZero", static_cast<uint32_t>(entity),
"maxHealth=" + std::to_string(h->maxHealth) + " is not positive",
Spark::StateViolationSeverity::Error});
}
}
}});

m_initialized = true;
SPARK_LOG_INFO(Spark::LogCategory::Game, "Open World module loaded successfully - 8 subsystems active");
console.LogInfo("[OpenWorld] Module loaded successfully (8 subsystems)");
Expand Down
28 changes: 28 additions & 0 deletions GameModules/SparkGamePlatformer/Source/Core/Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
#include "Camera/PlatformerCameraSystem.h"
#include "Utils/SparkConsole.h"
#include "Utils/LogMacros.h"
#include "Utils/InvalidStateDetector.h"
#include "Engine/ECS/Components.h"
#include "Engine/ECS/Components/GameplayComponents.h"
#include "Engine/ECS/Components/PhysicsComponents.h"

#ifdef SPARK_PLATFORM_WINDOWS
#include <windows.h>
Expand Down Expand Up @@ -131,6 +135,30 @@ bool SparkGamePlatformerModule::OnLoad(Spark::IEngineContext* context)

RegisterConsoleCommands();

// Register Platformer-specific state validation rules
auto& stateDetector = Spark::InvalidStateDetector::GetInstance();

stateDetector.AddRule({"Platformer.DeadEntityPhysics", "Platformer", Spark::StateViolationSeverity::Warning, true,
[](World& w, std::vector<Spark::StateViolation>& out)
{
for (auto entity : w.GetEntitiesWith<HealthComponent, RigidBodyComponent>())
{
auto* h = w.GetComponent<HealthComponent>(entity);
auto* rb = w.GetComponent<RigidBodyComponent>(entity);
if (h && rb && h->isDead && rb->type == RigidBodyComponent::Type::Dynamic)
{
float speedSq = rb->linearVelocity.x * rb->linearVelocity.x +
rb->linearVelocity.z * rb->linearVelocity.z;
if (speedSq > 4.0f)
{
out.push_back({"Platformer.DeadEntityPhysics", static_cast<uint32_t>(entity),
"Dead platformer entity still moving horizontally",
Spark::StateViolationSeverity::Warning});
}
}
}
}});

m_initialized = true;
SPARK_LOG_INFO(Spark::LogCategory::Game, "Platformer module loaded successfully");
console.LogInfo("[Platformer] Spark Platformer module loaded successfully (7 subsystems)");
Expand Down
Loading
Loading