diff --git a/.claude/index.md b/.claude/index.md index e9d707cc..e37ed3a6 100644 --- a/.claude/index.md +++ b/.claude/index.md @@ -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 diff --git a/.github/badges/files.json b/.github/badges/files.json index 0fd501a1..30c257f8 100644 --- a/.github/badges/files.json +++ b/.github/badges/files.json @@ -1,6 +1,6 @@ { - "schemaVersion": 1, - "label": "source files", - "message": "1505", - "color": "green" + "schemaVersion": 1, + "label": "source files", + "message": "1507", + "color": "green" } diff --git a/.github/badges/loc-breakdown.json b/.github/badges/loc-breakdown.json index 0b027e41..f394182e 100644 --- a/.github/badges/loc-breakdown.json +++ b/.github/badges/loc-breakdown.json @@ -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" } diff --git a/.github/badges/loc.json b/.github/badges/loc.json index e1ad6414..0cb8933f 100644 --- a/.github/badges/loc.json +++ b/.github/badges/loc.json @@ -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" } diff --git a/GameModules/SparkGame/Source/Core/Main.cpp b/GameModules/SparkGame/Source/Core/Main.cpp index 91589f7a..70b54f60 100644 --- a/GameModules/SparkGame/Source/Core/Main.cpp +++ b/GameModules/SparkGame/Source/Core/Main.cpp @@ -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 @@ -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& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + if (h && h->maxHealth > 0.0f && h->health > h->maxHealth * 1.5f) + { + out.push_back({"Base.HealthInvariant", static_cast(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"); diff --git a/GameModules/SparkGameARPG/Source/Core/Main.cpp b/GameModules/SparkGameARPG/Source/Core/Main.cpp index 437bf366..6b0764bf 100644 --- a/GameModules/SparkGameARPG/Source/Core/Main.cpp +++ b/GameModules/SparkGameARPG/Source/Core/Main.cpp @@ -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 @@ -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& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + auto* ai = w.GetComponent(entity); + if (h && ai && h->isDead && ai->targetEntity != entt::null) + { + out.push_back({"ARPG.DeadMobTargeting", static_cast(entity), + "Dead monster still has a target assigned", + Spark::StateViolationSeverity::Error}); + } + } + }}); + + stateDetector.AddRule( + {"ARPG.StaticBodyDynamic", "ARPG", Spark::StateViolationSeverity::Warning, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* rb = w.GetComponent(entity); + auto* h = w.GetComponent(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(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)"); diff --git a/GameModules/SparkGameFPS/Source/Core/Main.cpp b/GameModules/SparkGameFPS/Source/Core/Main.cpp index abd7c912..2e20b01b 100644 --- a/GameModules/SparkGameFPS/Source/Core/Main.cpp +++ b/GameModules/SparkGameFPS/Source/Core/Main.cpp @@ -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 @@ -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& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + auto* rb = w.GetComponent(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(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& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + auto* ai = w.GetComponent(entity); + if (h && ai && h->isDead && + (ai->state == AIComponent::State::Combat || ai->state == AIComponent::State::Alert)) + { + out.push_back({"FPS.DeadAICombat", static_cast(entity), "Dead AI in combat/alert state", + Spark::StateViolationSeverity::Error}); + } + } + }}); + m_initialized = true; console.LogSuccess("SparkGameFPS module initialized"); return true; diff --git a/GameModules/SparkGameFPS/Source/Game/Terrain.cpp b/GameModules/SparkGameFPS/Source/Game/Terrain.cpp index 365f0c2b..308d52a1 100644 --- a/GameModules/SparkGameFPS/Source/Game/Terrain.cpp +++ b/GameModules/SparkGameFPS/Source/Game/Terrain.cpp @@ -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(data.data()), static_cast(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 @@ -208,6 +218,10 @@ HRESULT Terrain::Initialize(ID3D11Device* /*device*/, ID3D11DeviceContext* /*ctx { in.seekg(54); in.read(reinterpret_cast(data.data()), static_cast(desc.width) * desc.height); + if (!in) + { + SPARK_LOG_WARN(Spark::LogCategory::Game, "Failed to read heightmap data, using flat terrain"); + } } } diff --git a/GameModules/SparkGameMMO/Source/Core/Main.cpp b/GameModules/SparkGameMMO/Source/Core/Main.cpp index 4e4502a6..4b3d4489 100644 --- a/GameModules/SparkGameMMO/Source/Core/Main.cpp +++ b/GameModules/SparkGameMMO/Source/Core/Main.cpp @@ -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 @@ -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& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + auto* ni = w.GetComponent(entity); + if (h && ni && h->isDead && !h->deathProcessed && ni->isLocalAuthority) + { + out.push_back({"MMO.DeadWithNetwork", static_cast(entity), + "Local-authority entity dead but deathProcessed=false", + Spark::StateViolationSeverity::Error}); + } + } + }}); + + stateDetector.AddRule({"MMO.HealthOverMax", "MMO", Spark::StateViolationSeverity::Warning, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + if (h && !h->isDead && h->health > h->maxHealth * 1.01f) + { + out.push_back({"MMO.HealthOverMax", static_cast(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)"); diff --git a/GameModules/SparkGameOpenWorld/Source/Core/Main.cpp b/GameModules/SparkGameOpenWorld/Source/Core/Main.cpp index d18a6230..d798cc18 100644 --- a/GameModules/SparkGameOpenWorld/Source/Core/Main.cpp +++ b/GameModules/SparkGameOpenWorld/Source/Core/Main.cpp @@ -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 @@ -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& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + auto* ai = w.GetComponent(entity); + if (h && ai && h->isDead && + (ai->state == AIComponent::State::Patrolling || ai->state == AIComponent::State::Alert)) + { + out.push_back({"OpenWorld.DeadWildlife", static_cast(entity), + "Dead wildlife AI still patrolling/alert", Spark::StateViolationSeverity::Warning}); + } + } + }}); + + stateDetector.AddRule({"OpenWorld.MaxHealthZero", "OpenWorld", Spark::StateViolationSeverity::Error, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + if (h && h->maxHealth <= 0.0f) + { + out.push_back({"OpenWorld.MaxHealthZero", static_cast(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)"); diff --git a/GameModules/SparkGamePlatformer/Source/Core/Main.cpp b/GameModules/SparkGamePlatformer/Source/Core/Main.cpp index f63b892b..71a771fa 100644 --- a/GameModules/SparkGamePlatformer/Source/Core/Main.cpp +++ b/GameModules/SparkGamePlatformer/Source/Core/Main.cpp @@ -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 @@ -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& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + auto* rb = w.GetComponent(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(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)"); diff --git a/GameModules/SparkGameRPG/Source/Core/Main.cpp b/GameModules/SparkGameRPG/Source/Core/Main.cpp index 13f3fcb4..60fdbe8f 100644 --- a/GameModules/SparkGameRPG/Source/Core/Main.cpp +++ b/GameModules/SparkGameRPG/Source/Core/Main.cpp @@ -17,6 +17,10 @@ #include "NPC/RPGNPCSystem.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 @@ -139,6 +143,40 @@ bool SparkGameRPGModule::OnLoad(Spark::IEngineContext* context) RegisterConsoleCommands(); + // Register RPG-specific state validation rules + auto& stateDetector = Spark::InvalidStateDetector::GetInstance(); + + stateDetector.AddRule({"RPG.DeadAIPatrolling", "RPG", Spark::StateViolationSeverity::Error, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + auto* ai = w.GetComponent(entity); + if (h && ai && h->isDead && ai->state == AIComponent::State::Patrolling) + { + out.push_back({"RPG.DeadAIPatrolling", static_cast(entity), + "Dead NPC is still patrolling", + Spark::StateViolationSeverity::Error}); + } + } + }}); + + stateDetector.AddRule({"RPG.NegativeHealth", "RPG", Spark::StateViolationSeverity::Warning, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + if (h && h->health < 0.0f) + { + out.push_back({"RPG.NegativeHealth", static_cast(entity), + "health=" + std::to_string(h->health) + " is negative", + Spark::StateViolationSeverity::Warning}); + } + } + }}); + m_initialized = true; SPARK_LOG_INFO(Spark::LogCategory::Game, "RPG module loaded successfully — 8 subsystems active"); console.LogInfo("[RPG] Spark RPG module loaded successfully (8 subsystems)"); diff --git a/GameModules/SparkGameRTS/Source/Core/Main.cpp b/GameModules/SparkGameRTS/Source/Core/Main.cpp index 093f4d18..d1683999 100644 --- a/GameModules/SparkGameRTS/Source/Core/Main.cpp +++ b/GameModules/SparkGameRTS/Source/Core/Main.cpp @@ -16,6 +16,10 @@ #include "Match/RTSMatchSystem.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 @@ -130,6 +134,41 @@ bool SparkGameRTSModule::OnLoad(Spark::IEngineContext* context) RegisterConsoleCommands(); + // Register RTS-specific state validation rules + auto& stateDetector = Spark::InvalidStateDetector::GetInstance(); + + stateDetector.AddRule({"RTS.DeadUnitAttacking", "RTS", Spark::StateViolationSeverity::Error, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + auto* ai = w.GetComponent(entity); + if (h && ai && h->isDead && ai->state == AIComponent::State::Combat) + { + out.push_back({"RTS.DeadUnitAttacking", static_cast(entity), + "Dead RTS unit still in combat state", + Spark::StateViolationSeverity::Error}); + } + } + }}); + + stateDetector.AddRule({"RTS.IdleWithTarget", "RTS", Spark::StateViolationSeverity::Warning, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* ai = w.GetComponent(entity); + if (ai && ai->state == AIComponent::State::Idle && ai->targetEntity != entt::null) + { + out.push_back( + {"RTS.IdleWithTarget", static_cast(entity), + "Idle unit has target assigned, should be attacking or clearing target", + Spark::StateViolationSeverity::Warning}); + } + } + }}); + m_initialized = true; SPARK_LOG_INFO(Spark::LogCategory::Game, "RTS module loaded successfully — 7 subsystems active"); console.LogInfo("[RTS] Spark RTS module loaded successfully (7 subsystems)"); diff --git a/GameModules/SparkGameRacing/Source/Core/Main.cpp b/GameModules/SparkGameRacing/Source/Core/Main.cpp index e7ae14da..c979ff3c 100644 --- a/GameModules/SparkGameRacing/Source/Core/Main.cpp +++ b/GameModules/SparkGameRacing/Source/Core/Main.cpp @@ -16,6 +16,10 @@ #include "HUD/RacingHUDSystem.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 @@ -131,6 +135,30 @@ bool SparkGameRacingModule::OnLoad(Spark::IEngineContext* context) RegisterConsoleCommands(); + // Register Racing-specific state validation rules + auto& stateDetector = Spark::InvalidStateDetector::GetInstance(); + + stateDetector.AddRule({"Racing.StaticVehicleMoving", "Racing", Spark::StateViolationSeverity::Warning, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* rb = w.GetComponent(entity); + if (!rb || rb->type != RigidBodyComponent::Type::Static) + continue; + float speedSq = rb->linearVelocity.x * rb->linearVelocity.x + + rb->linearVelocity.y * rb->linearVelocity.y + + rb->linearVelocity.z * rb->linearVelocity.z; + if (speedSq > 1.0f) + { + out.push_back( + {"Racing.StaticVehicleMoving", static_cast(entity), + "Static body has velocity (speedSq=" + std::to_string(speedSq) + ")", + Spark::StateViolationSeverity::Warning}); + } + } + }}); + m_initialized = true; SPARK_LOG_INFO(Spark::LogCategory::Game, "Racing module loaded successfully"); console.LogInfo("[Racing] Spark Racing module loaded successfully (7 subsystems)"); diff --git a/GameModules/SparkGameVisualScript/Source/Core/Main.cpp b/GameModules/SparkGameVisualScript/Source/Core/Main.cpp index 63ddd87a..95eabe3c 100644 --- a/GameModules/SparkGameVisualScript/Source/Core/Main.cpp +++ b/GameModules/SparkGameVisualScript/Source/Core/Main.cpp @@ -16,6 +16,9 @@ #include "Engine/Scripting/AngelScriptEngine.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 @@ -71,6 +74,23 @@ bool SparkGameVisualScriptModule::OnLoad(Spark::IEngineContext* context) // Step 2: Spawn entities and attach scripts SpawnGameEntities(); + // Register VisualScript state validation rules + Spark::InvalidStateDetector::GetInstance().AddRule( + {"VS.ScriptEntityHealth", "VisualScript", Spark::StateViolationSeverity::Warning, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + if (h && h->isDead && !h->deathProcessed) + { + out.push_back({"VS.ScriptEntityHealth", static_cast(entity), + "Script entity dead but deathProcessed=false (script may have missed death event)", + Spark::StateViolationSeverity::Warning}); + } + } + }}); + m_initialized = true; console.LogInfo("[VisualScript] Module loaded — game is running entirely on visual scripts"); return true; diff --git a/SparkEditor/Source/Core/EditorTheme.cpp b/SparkEditor/Source/Core/EditorTheme.cpp index 63643014..e5ec1e76 100644 --- a/SparkEditor/Source/Core/EditorTheme.cpp +++ b/SparkEditor/Source/Core/EditorTheme.cpp @@ -1149,8 +1149,16 @@ namespace SparkEditor return true; } + catch (const std::exception& e) + { + SPARK_LOG_ERROR(Spark::LogCategory::Editor, "Failed to export theme to '%s': %s", filepath.c_str(), + e.what()); + return false; + } catch (...) { + SPARK_LOG_ERROR(Spark::LogCategory::Editor, "Failed to export theme to '%s': unknown exception", + filepath.c_str()); return false; } } @@ -1243,8 +1251,16 @@ namespace SparkEditor return !outTheme.name.empty(); } + catch (const std::exception& e) + { + SPARK_LOG_ERROR(Spark::LogCategory::Editor, "Failed to import theme from '%s': %s", filepath.c_str(), + e.what()); + return false; + } catch (...) { + SPARK_LOG_ERROR(Spark::LogCategory::Editor, "Failed to import theme from '%s': unknown exception", + filepath.c_str()); return false; } } diff --git a/SparkEditor/Source/Prefabs/PrefabAsset.cpp b/SparkEditor/Source/Prefabs/PrefabAsset.cpp index 01cfb38d..dceca7a6 100644 --- a/SparkEditor/Source/Prefabs/PrefabAsset.cpp +++ b/SparkEditor/Source/Prefabs/PrefabAsset.cpp @@ -129,6 +129,13 @@ namespace SparkEditor } } + if (!file.good()) + { + SPARK_LOG_ERROR(Spark::LogCategory::Editor, "Write error saving prefab '%s' to: %s", m_name.c_str(), + path.c_str()); + return false; + } + m_filePath = path; m_isModified = false; return true; diff --git a/SparkEditor/Source/SceneSystem/JSONSceneSerializer.cpp b/SparkEditor/Source/SceneSystem/JSONSceneSerializer.cpp index 7bdf089d..53a1bf3c 100644 --- a/SparkEditor/Source/SceneSystem/JSONSceneSerializer.cpp +++ b/SparkEditor/Source/SceneSystem/JSONSceneSerializer.cpp @@ -11,6 +11,7 @@ #include "SceneSerializer.h" #include "Utils/LogMacros.h" #include "Utils/Validate.h" +#include #include #include #include @@ -450,6 +451,11 @@ namespace SparkEditor { result.success = false; result.errorMessage = "Write error while saving JSON: " + filePath; + SPARK_LOG_ERROR(Spark::LogCategory::Editor, "Write error saving JSON scene, removing partial file: %s", + filePath.c_str()); + file.close(); + std::error_code ec; + std::filesystem::remove(filePath, ec); return result; } diff --git a/SparkEngine/Source/Core/GameplaySystemLifecycle.cpp b/SparkEngine/Source/Core/GameplaySystemLifecycle.cpp index 84e3fb6d..3756af53 100644 --- a/SparkEngine/Source/Core/GameplaySystemLifecycle.cpp +++ b/SparkEngine/Source/Core/GameplaySystemLifecycle.cpp @@ -90,6 +90,7 @@ #include "Engine/Networking/DeltaSnapshotManager.h" #include "Engine/Networking/InstabilitySimulator.h" #include "Engine/Security/MemoryIntegrity.h" +#include "Utils/InvalidStateDetector.h" #include "Engine/Networking/ConnectionScopeFilter.h" #include "Engine/Tween/TweenSystem.h" #include "Engine/Modding/VirtualFileSystem.h" @@ -280,6 +281,7 @@ void InitDebugSystems() Spark::NetworkHealthMonitor::GetInstance().Initialize(); Spark::GPUResourceLeakDetector::GetInstance().Initialize(); Spark::Security::MemoryIntegritySystem::GetInstance().Initialize(); + Spark::InvalidStateDetector::GetInstance().Initialize(); // Register memory pressure response callbacks Spark::MemoryMonitor::GetInstance().RegisterPressureCallback( @@ -332,6 +334,7 @@ static void InitCoreGameplaySystems(EngineContext* ctx) if (auto* world = ctx->GetWorld()) { destruction.SetWorld(world); + Spark::InvalidStateDetector::GetInstance().SetWorld(world); } destruction.OnDestruction( [](const Spark::DestructionEvent& e) @@ -912,6 +915,7 @@ void UpdateDebugSystems(float dt) { Spark::GPUResourceLeakDetector::GetInstance().Update(dt); }); SPARK_GUARDED_UPDATE("MemoryIntegrity", "Security", { Spark::Security::MemoryIntegritySystem::GetInstance().Update(dt); }); + SPARK_GUARDED_UPDATE("InvalidStateDetector", "Debug", { Spark::InvalidStateDetector::GetInstance().Update(dt); }); Spark::FrameInspector::GetInstance().OnFrameEnd(); // Update decal fading @@ -932,6 +936,7 @@ void ShutdownDebugSystems() Spark::NetworkHealthMonitor::GetInstance().Shutdown(); Spark::GPUResourceLeakDetector::GetInstance().Shutdown(); Spark::Security::MemoryIntegritySystem::GetInstance().Shutdown(); + Spark::InvalidStateDetector::GetInstance().Shutdown(); #ifndef NDEBUG Spark::MemoryDebugger::GetInstance().PrintLeakReport(); #endif diff --git a/SparkEngine/Source/Core/SparkEngine.cpp b/SparkEngine/Source/Core/SparkEngine.cpp index 1158202b..6d8ba139 100644 --- a/SparkEngine/Source/Core/SparkEngine.cpp +++ b/SparkEngine/Source/Core/SparkEngine.cpp @@ -58,6 +58,7 @@ #include "Utils/AssetStallDetector.h" #include "Utils/NetworkHealthMonitor.h" #include "Utils/GPUResourceLeakDetector.h" +#include "Utils/InvalidStateDetector.h" #include "FixedTimestepAccumulator.h" #include "Engine/Networking/ClientPrediction.h" #include "Engine/Networking/ConnectionScopeFilter.h" @@ -592,6 +593,7 @@ static int RunHeadlessWindows(LPWSTR lpCmdLine) Spark::AssetStallDetector::GetInstance().RegisterConsoleCommands(); Spark::NetworkHealthMonitor::GetInstance().RegisterConsoleCommands(); Spark::GPUResourceLeakDetector::GetInstance().RegisterConsoleCommands(); + Spark::InvalidStateDetector::GetInstance().RegisterConsoleCommands(); Assert::RegisterConsoleCommands(); // Fixed 60 Hz server loop @@ -787,6 +789,7 @@ static void InitializeWindowedSubsystems(HINSTANCE hInstance, LPWSTR lpCmdLine) Spark::AssetStallDetector::GetInstance().RegisterConsoleCommands(); Spark::NetworkHealthMonitor::GetInstance().RegisterConsoleCommands(); Spark::GPUResourceLeakDetector::GetInstance().RegisterConsoleCommands(); + Spark::InvalidStateDetector::GetInstance().RegisterConsoleCommands(); Assert::RegisterConsoleCommands(); EngineSettings::GetInstance().RegisterConsoleCommands(); @@ -1408,6 +1411,7 @@ static int RunHeadlessLinux(int argc, char* argv[]) Spark::AssetStallDetector::GetInstance().RegisterConsoleCommands(); Spark::NetworkHealthMonitor::GetInstance().RegisterConsoleCommands(); Spark::GPUResourceLeakDetector::GetInstance().RegisterConsoleCommands(); + Spark::InvalidStateDetector::GetInstance().RegisterConsoleCommands(); Assert::RegisterConsoleCommands(); // Fixed 60 Hz server loop @@ -1646,6 +1650,7 @@ static void InitializeSDL2Subsystems(SDL_Window* window, int argc, char* argv[]) Spark::AssetStallDetector::GetInstance().RegisterConsoleCommands(); Spark::NetworkHealthMonitor::GetInstance().RegisterConsoleCommands(); Spark::GPUResourceLeakDetector::GetInstance().RegisterConsoleCommands(); + Spark::InvalidStateDetector::GetInstance().RegisterConsoleCommands(); Assert::RegisterConsoleCommands(); // Initialize console, debug, and gameplay systems in one call diff --git a/SparkEngine/Source/Core/SparkPakWriter.cpp b/SparkEngine/Source/Core/SparkPakWriter.cpp index 8666e2cd..ac1a6773 100644 --- a/SparkEngine/Source/Core/SparkPakWriter.cpp +++ b/SparkEngine/Source/Core/SparkPakWriter.cpp @@ -126,10 +126,20 @@ namespace Spark return false; } + // Helper: write with error checking + auto writeBytes = [&](const void* data, size_t size) -> bool + { return std::fwrite(data, 1, size, file) == size; }; + // Write placeholder header (will rewrite at end) PakHeader header; header.fileCount = static_cast(m_files.size()); - std::fwrite(&header, sizeof(PakHeader), 1, file); + if (!writeBytes(&header, sizeof(PakHeader))) + { + SPARK_LOG_ERROR(Spark::LogCategory::Core, "SparkPakWriter: failed to write header to '%s'", + outputPath.c_str()); + std::fclose(file); + return false; + } // Write file data blobs, tracking offsets struct WrittenEntry @@ -157,12 +167,24 @@ namespace Spark if (staged.compression == PakCompression::Deflate && !staged.compressedData.empty()) { we.compressedSize = static_cast(staged.compressedData.size()); - std::fwrite(staged.compressedData.data(), 1, staged.compressedData.size(), file); + if (!writeBytes(staged.compressedData.data(), staged.compressedData.size())) + { + SPARK_LOG_ERROR(Spark::LogCategory::Core, "SparkPakWriter: write failed for '%s'", + staged.virtualPath.c_str()); + std::fclose(file); + return false; + } } else { we.compressedSize = static_cast(staged.originalData.size()); - std::fwrite(staged.originalData.data(), 1, staged.originalData.size(), file); + if (!writeBytes(staged.originalData.data(), staged.originalData.size())) + { + SPARK_LOG_ERROR(Spark::LogCategory::Core, "SparkPakWriter: write failed for '%s'", + staged.virtualPath.c_str()); + std::fclose(file); + return false; + } } entries.push_back(std::move(we)); @@ -206,19 +228,37 @@ namespace Spark { tocCompressed.resize(destLen); header.tocSize = static_cast(destLen); - std::fwrite(tocCompressed.data(), 1, destLen, file); + if (!writeBytes(tocCompressed.data(), destLen)) + { + SPARK_LOG_ERROR(Spark::LogCategory::Core, "SparkPakWriter: failed to write TOC to '%s'", + outputPath.c_str()); + std::fclose(file); + return false; + } } else #endif { // Fallback: store TOC uncompressed header.tocSize = header.tocRawSize; - std::fwrite(tocRaw.data(), 1, tocRaw.size(), file); + if (!writeBytes(tocRaw.data(), tocRaw.size())) + { + SPARK_LOG_ERROR(Spark::LogCategory::Core, "SparkPakWriter: failed to write TOC to '%s'", + outputPath.c_str()); + std::fclose(file); + return false; + } } // Rewrite header with final values PAK_FSEEK(file, 0, SEEK_SET); - std::fwrite(&header, sizeof(PakHeader), 1, file); + if (!writeBytes(&header, sizeof(PakHeader))) + { + SPARK_LOG_ERROR(Spark::LogCategory::Core, "SparkPakWriter: failed to rewrite header in '%s'", + outputPath.c_str()); + std::fclose(file); + return false; + } std::fclose(file); return true; diff --git a/SparkEngine/Source/Engine/AI/FormationSystem.cpp b/SparkEngine/Source/Engine/AI/FormationSystem.cpp index 5f293ffa..0f2178ad 100644 --- a/SparkEngine/Source/Engine/AI/FormationSystem.cpp +++ b/SparkEngine/Source/Engine/AI/FormationSystem.cpp @@ -257,6 +257,11 @@ namespace Spark::AI } break; } + + default: + SPARK_LOG_WARN(Spark::LogCategory::AI, "Unknown formation type %d, returning default slots", + static_cast(type)); + break; } return slots; diff --git a/SparkEngine/Source/Engine/Animation/AnimationSystem.cpp b/SparkEngine/Source/Engine/Animation/AnimationSystem.cpp index e126de64..1882d7a3 100644 --- a/SparkEngine/Source/Engine/Animation/AnimationSystem.cpp +++ b/SparkEngine/Source/Engine/Animation/AnimationSystem.cpp @@ -598,6 +598,11 @@ namespace Spark::Animation case IKType::FABRIK: AnimationEvaluator::SolveFABRIK(blendResult.localTransforms, *skeleton, chain); break; + + default: + SPARK_LOG_WARN(LogCategory::Animation, "Unknown IK type %d, skipping chain", + static_cast(chain.type)); + continue; } // Apply IK weight blending: blend between pre-IK and post-IK for affected bones diff --git a/SparkEngine/Source/Engine/Networking/NetworkConnection.cpp b/SparkEngine/Source/Engine/Networking/NetworkConnection.cpp index ea18bb06..8222c02f 100644 --- a/SparkEngine/Source/Engine/Networking/NetworkConnection.cpp +++ b/SparkEngine/Source/Engine/Networking/NetworkConnection.cpp @@ -634,20 +634,22 @@ namespace Spark::Net { std::lock_guard lock(m_clientsMutex); m_clients.erase(clientID); - } - #ifdef ENABLE_NETWORKING - m_clientAddresses.erase(clientID); + m_clientAddresses.erase(clientID); #endif + } // Remove entities owned by this client SPARK_LOG_DEBUG(Spark::LogCategory::Network, "Cleaning up entities owned by client %u", clientID); std::vector ownedEntities; - for (const auto& [netID, entity] : m_replicatedEntities) { - if (entity.ownerID == clientID) + std::lock_guard replicationLock(m_replicationMutex); + for (const auto& [netID, entity] : m_replicatedEntities) { - ownedEntities.push_back(netID); + if (entity.ownerID == clientID) + { + ownedEntities.push_back(netID); + } } } for (uint32_t netID : ownedEntities) @@ -691,10 +693,13 @@ namespace Spark::Net // Clean up owned entities std::vector ownedEntities; - for (const auto& [netID, entity] : m_replicatedEntities) { - if (entity.ownerID == id) - ownedEntities.push_back(netID); + std::lock_guard replicationLock(m_replicationMutex); + for (const auto& [netID, entity] : m_replicatedEntities) + { + if (entity.ownerID == id) + ownedEntities.push_back(netID); + } } for (uint32_t netID : ownedEntities) UnregisterReplicatedEntity(netID); @@ -702,10 +707,10 @@ namespace Spark::Net { std::lock_guard lock(m_clientsMutex); m_clients.erase(id); - } #ifdef ENABLE_NETWORKING - m_clientAddresses.erase(id); + m_clientAddresses.erase(id); #endif + } } } else if (m_role == NetworkRole::Client) diff --git a/SparkEngine/Source/Engine/Scripting/ScriptSandbox.cpp b/SparkEngine/Source/Engine/Scripting/ScriptSandbox.cpp index 45d495d8..f2087e2b 100644 --- a/SparkEngine/Source/Engine/Scripting/ScriptSandbox.cpp +++ b/SparkEngine/Source/Engine/Scripting/ScriptSandbox.cpp @@ -73,6 +73,14 @@ namespace Spark m_maxExecutionTimeSec = 0.05f; m_maxMemoryBytes = 4 * 1024 * 1024; // 4 MB break; + + default: + SPARK_LOG_WARN(Spark::LogCategory::Scripting, "Unknown security level %d, defaulting to Strict", + static_cast(level)); + m_maxInstructions = 100'000; + m_maxExecutionTimeSec = 0.05f; + m_maxMemoryBytes = 4 * 1024 * 1024; + break; } } diff --git a/SparkEngine/Source/Graphics/FBXImporter.cpp b/SparkEngine/Source/Graphics/FBXImporter.cpp index 72194729..9e93dfe6 100644 --- a/SparkEngine/Source/Graphics/FBXImporter.cpp +++ b/SparkEngine/Source/Graphics/FBXImporter.cpp @@ -87,6 +87,9 @@ namespace Spark::Graphics case FBXConstants::PROP_RAW: { uint32_t len = reader.Read(); + constexpr uint32_t kMaxStringLen = 16 * 1024 * 1024; // 16 MB sanity limit + if (len > kMaxStringLen) + break; prop.stringValue = reader.ReadString(len); break; } @@ -95,10 +98,11 @@ namespace Spark::Graphics uint32_t count = reader.Read(); uint32_t encoding = reader.Read(); uint32_t compressedLen = reader.Read(); - if (encoding == 0 && count > 0) + constexpr uint32_t kMaxArrayCount = 1 << 26; // ~64M elements + if (encoding == 0 && count > 0 && count <= kMaxArrayCount) { prop.floatArray.resize(count); - reader.ReadBytes(prop.floatArray.data(), count * sizeof(float)); + reader.ReadBytes(prop.floatArray.data(), static_cast(count) * sizeof(float)); } else { @@ -111,10 +115,11 @@ namespace Spark::Graphics uint32_t count = reader.Read(); uint32_t encoding = reader.Read(); uint32_t compressedLen = reader.Read(); - if (encoding == 0 && count > 0) + constexpr uint32_t kMaxArrayCount = 1 << 26; + if (encoding == 0 && count > 0 && count <= kMaxArrayCount) { prop.doubleArray.resize(count); - reader.ReadBytes(prop.doubleArray.data(), count * sizeof(double)); + reader.ReadBytes(prop.doubleArray.data(), static_cast(count) * sizeof(double)); } else { @@ -127,10 +132,11 @@ namespace Spark::Graphics uint32_t count = reader.Read(); uint32_t encoding = reader.Read(); uint32_t compressedLen = reader.Read(); - if (encoding == 0 && count > 0) + constexpr uint32_t kMaxArrayCount = 1 << 26; + if (encoding == 0 && count > 0 && count <= kMaxArrayCount) { prop.intArray.resize(count); - reader.ReadBytes(prop.intArray.data(), count * sizeof(int32_t)); + reader.ReadBytes(prop.intArray.data(), static_cast(count) * sizeof(int32_t)); } else { @@ -143,10 +149,11 @@ namespace Spark::Graphics uint32_t count = reader.Read(); uint32_t encoding = reader.Read(); uint32_t compressedLen = reader.Read(); - if (encoding == 0 && count > 0) + constexpr uint32_t kMaxArrayCount = 1 << 26; + if (encoding == 0 && count > 0 && count <= kMaxArrayCount) { prop.longArray.resize(count); - reader.ReadBytes(prop.longArray.data(), count * sizeof(int64_t)); + reader.ReadBytes(prop.longArray.data(), static_cast(count) * sizeof(int64_t)); } else { diff --git a/SparkEngine/Source/Graphics/MaterialConsoleOps.cpp b/SparkEngine/Source/Graphics/MaterialConsoleOps.cpp index fa7c981d..6243f1d4 100644 --- a/SparkEngine/Source/Graphics/MaterialConsoleOps.cpp +++ b/SparkEngine/Source/Graphics/MaterialConsoleOps.cpp @@ -793,7 +793,8 @@ std::string MaterialSystem::Console_DumpMaterialDetails(const std::string& mater std::stringstream ss; ss << mat->GetDetailedInfo(); ss << "\n--- System Info ---\n"; - ss << " Ref count: " << m_materials.at(materialName).use_count() << "\n"; + auto matIt = m_materials.find(materialName); + ss << " Ref count: " << (matIt != m_materials.end() ? matIt->second.use_count() : 0) << "\n"; ss << " Hot reload: " << (m_hotReloadEnabled ? "enabled" : "disabled") << "\n"; ss << " Platform: Linux (CPU-side only)\n"; return ss.str(); diff --git a/SparkEngine/Source/Input/GamepadInput.cpp b/SparkEngine/Source/Input/GamepadInput.cpp index a9878a8e..e8a87c2a 100644 --- a/SparkEngine/Source/Input/GamepadInput.cpp +++ b/SparkEngine/Source/Input/GamepadInput.cpp @@ -387,7 +387,8 @@ float GamepadInput::NormalizeTrigger(BYTE raw) const float value = raw / 255.0f; if (value < m_triggerThreshold) return 0.0f; - return (value - m_triggerThreshold) / (1.0f - m_triggerThreshold); + float range = 1.0f - m_triggerThreshold; + return (range > 0.0f) ? (value - m_triggerThreshold) / range : 1.0f; } // ============================================================================ diff --git a/SparkEngine/Source/Utils/InvalidStateDetector.cpp b/SparkEngine/Source/Utils/InvalidStateDetector.cpp new file mode 100644 index 00000000..6bca3547 --- /dev/null +++ b/SparkEngine/Source/Utils/InvalidStateDetector.cpp @@ -0,0 +1,535 @@ +/** + * @file InvalidStateDetector.cpp + * @brief Runtime detection of impossible/contradictory ECS component state combinations + */ + +#include "InvalidStateDetector.h" +#include "LogMacros.h" +#include "SparkConsole.h" + +#include "../Engine/ECS/Components.h" +#include "../Engine/ECS/Components/AIComponents.h" +#include "../Engine/ECS/Components/AdvancedPlacementComponents.h" +#include "../Engine/ECS/Components/FPSComponents.h" +#include "../Engine/ECS/Components/GameplayComponents.h" +#include "../Engine/ECS/Components/NetworkComponents.h" +#include "../Engine/ECS/Components/PhysicsComponents.h" +#include "../Engine/ECS/Components/PlacementComponents.h" + +#include +#include +#include +#include + +namespace Spark +{ + + InvalidStateDetector& InvalidStateDetector::GetInstance() + { + static InvalidStateDetector instance; + return instance; + } + + // ========================================================================= + // Lifecycle + // ========================================================================= + + void InvalidStateDetector::Initialize() + { + if (m_initialized) + return; + + m_rules.clear(); + m_violations.clear(); + m_totalChecks = 0; + m_totalViolations = 0; + m_timeSinceLastCheck = 0.0f; + + RegisterDefaultRules(); + m_initialized = true; + + SPARK_LOG_INFO(Spark::LogCategory::Core, "InvalidStateDetector initialized with %u rules", + static_cast(m_rules.size())); + } + + void InvalidStateDetector::Update(float dt) + { + if (!m_initialized || !m_config.enabled || !m_world) + return; + + m_timeSinceLastCheck += dt; + if (m_timeSinceLastCheck < m_config.checkIntervalSec) + return; + + m_timeSinceLastCheck = 0.0f; + RunChecks(); + } + + void InvalidStateDetector::Shutdown() + { + if (!m_initialized) + return; + + if (m_totalViolations > 0) + { + SPARK_LOG_INFO(Spark::LogCategory::Core, + "InvalidStateDetector: %u total violations detected across %u checks", m_totalViolations, + m_totalChecks); + } + + m_world = nullptr; + m_rules.clear(); + m_violations.clear(); + m_initialized = false; + } + + // ========================================================================= + // Rule Management + // ========================================================================= + + void InvalidStateDetector::AddRule(StateValidationRule rule) + { + m_rules.push_back(std::move(rule)); + } + + void InvalidStateDetector::SetRuleEnabled(const std::string& name, bool enabled) + { + for (auto& rule : m_rules) + { + if (rule.name == name) + { + rule.enabled = enabled; + return; + } + } + } + + // ========================================================================= + // Checks + // ========================================================================= + + void InvalidStateDetector::RunChecks() + { + ++m_totalChecks; + + std::vector newViolations; + for (const auto& rule : m_rules) + { + if (!rule.enabled) + continue; + rule.checkFn(*m_world, newViolations); + } + + for (auto& v : newViolations) + { + ++m_totalViolations; + + if (m_config.logOnDetection) + { + if (v.severity == StateViolationSeverity::Critical) + { + SPARK_LOG_ERROR(Spark::LogCategory::Core, "[StateViolation] %s entity=%u: %s", v.ruleName.c_str(), + v.entityId, v.details.c_str()); + } + else + { + SPARK_LOG_WARN(Spark::LogCategory::Core, "[StateViolation] %s entity=%u: %s", v.ruleName.c_str(), + v.entityId, v.details.c_str()); + } + } + + // Ring buffer: drop oldest if full + if (m_violations.size() >= m_config.maxViolationHistory) + { + m_violations.erase(m_violations.begin()); + } + m_violations.push_back(std::move(v)); + } + } + + // ========================================================================= + // Query + // ========================================================================= + + InvalidStateDetectorStatus InvalidStateDetector::GetStatus() const + { + InvalidStateDetectorStatus status; + status.totalChecks = m_totalChecks; + status.totalViolations = m_totalViolations; + status.totalRules = static_cast(m_rules.size()); + status.activeRules = static_cast( + std::count_if(m_rules.begin(), m_rules.end(), [](const auto& r) { return r.enabled; })); + status.recentViolations = m_violations; + return status; + } + + // ========================================================================= + // Console Commands + // ========================================================================= + + void InvalidStateDetector::RegisterConsoleCommands() + { + auto& console = SimpleConsole::GetInstance(); + + console.RegisterCommand( + "state.status", + [](const std::vector&) -> std::string + { + auto& det = GetInstance(); + auto status = det.GetStatus(); + std::stringstream ss; + ss << "=== Invalid State Detector ===\n"; + ss << " Enabled: " << (det.m_config.enabled ? "yes" : "no") << "\n"; + ss << " World: " << (det.m_world ? "set" : "NOT SET") << "\n"; + ss << " Rules: " << status.activeRules << "/" << status.totalRules << " active\n"; + ss << " Checks run: " << status.totalChecks << "\n"; + ss << " Violations: " << status.totalViolations << " total\n"; + ss << " Interval: " << det.m_config.checkIntervalSec << "s\n"; + ss << " Recent: " << status.recentViolations.size() << " in buffer\n"; + return ss.str(); + }, + "Show invalid state detector status", "Diagnostics"); + + console.RegisterCommand( + "state.rules", + [](const std::vector& args) -> std::string + { + auto& det = GetInstance(); + + // Handle enable/disable: state.rules enable|disable + if (args.size() >= 2) + { + const std::string& name = args[0]; + const std::string& action = args[1]; + bool enable = (action == "enable"); + det.SetRuleEnabled(name, enable); + return "Rule '" + name + "' " + (enable ? "enabled" : "disabled"); + } + + std::stringstream ss; + ss << "=== Validation Rules ===\n"; + std::string lastCategory; + for (const auto& rule : det.m_rules) + { + if (rule.category != lastCategory) + { + ss << "\n [" << rule.category << "]\n"; + lastCategory = rule.category; + } + const char* sevStr = rule.severity == StateViolationSeverity::Critical ? "CRIT" + : rule.severity == StateViolationSeverity::Error ? "ERR " + : "WARN"; + ss << " " << (rule.enabled ? "[ON] " : "[OFF]") << " " << sevStr << " " << rule.name << "\n"; + } + ss << "\nUsage: state.rules enable|disable\n"; + return ss.str(); + }, + "List rules or toggle: state.rules [name enable|disable]", "Diagnostics"); + + console.RegisterCommand( + "state.violations", + [](const std::vector&) -> std::string + { + auto& det = GetInstance(); + if (det.m_violations.empty()) + return std::string("No recent violations."); + + std::stringstream ss; + ss << "=== Recent Violations (" << det.m_violations.size() << ") ===\n"; + for (const auto& v : det.m_violations) + { + const char* sevStr = v.severity == StateViolationSeverity::Critical ? "CRIT" + : v.severity == StateViolationSeverity::Error ? "ERR " + : "WARN"; + ss << " [" << sevStr << "] " << v.ruleName << " entity=" << v.entityId << ": " << v.details + << "\n"; + } + return ss.str(); + }, + "Show recent state violations", "Diagnostics"); + + console.RegisterCommand( + "state.config", + [](const std::vector&) -> std::string + { + auto& det = GetInstance(); + std::stringstream ss; + ss << "=== State Detector Config ===\n"; + ss << " enabled: " << (det.m_config.enabled ? "true" : "false") << "\n"; + ss << " checkIntervalSec: " << det.m_config.checkIntervalSec << "\n"; + ss << " maxViolationHistory: " << det.m_config.maxViolationHistory << "\n"; + ss << " logOnDetection: " << (det.m_config.logOnDetection ? "true" : "false") << "\n"; + return ss.str(); + }, + "Show state detector configuration", "Diagnostics"); + } + + // ========================================================================= + // Default Rules + // ========================================================================= + + void InvalidStateDetector::RegisterDefaultRules() + { + // -- Health rules -- + + AddRule({"Health.DeadButPositive", "Health", StateViolationSeverity::Error, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + if (h && h->isDead && h->health > 0.0f) + { + out.push_back({"Health.DeadButPositive", static_cast(entity), + "isDead=true but health=" + std::to_string(h->health), + StateViolationSeverity::Error}); + } + } + }}); + + AddRule({"Health.AliveButZero", "Health", StateViolationSeverity::Error, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + if (h && !h->isDead && h->health <= 0.0f) + { + out.push_back({"Health.AliveButZero", static_cast(entity), + "isDead=false but health=" + std::to_string(h->health), + StateViolationSeverity::Error}); + } + } + }}); + + AddRule({"Health.DeathProcessedButAlive", "Health", StateViolationSeverity::Critical, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + if (h && h->deathProcessed && !h->isDead) + { + out.push_back({"Health.DeathProcessedButAlive", static_cast(entity), + "deathProcessed=true but isDead=false", StateViolationSeverity::Critical}); + } + } + }}); + + AddRule({"Health.DeadAINotInDeadState", "Health", StateViolationSeverity::Error, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* h = w.GetComponent(entity); + auto* ai = w.GetComponent(entity); + if (h && ai && h->isDead && ai->state != AIComponent::State::Dead) + { + out.push_back({"Health.DeadAINotInDeadState", static_cast(entity), + "HealthComponent.isDead=true but AI state is not Dead", + StateViolationSeverity::Error}); + } + } + }}); + + // -- Physics rules -- + + AddRule({"Physics.StaticWithVelocity", "Physics", StateViolationSeverity::Warning, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* rb = w.GetComponent(entity); + if (!rb || rb->type != RigidBodyComponent::Type::Static) + continue; + float speed = rb->linearVelocity.x * rb->linearVelocity.x + + rb->linearVelocity.y * rb->linearVelocity.y + + rb->linearVelocity.z * rb->linearVelocity.z; + if (speed > 0.01f) + { + out.push_back({"Physics.StaticWithVelocity", static_cast(entity), + "Static body has nonzero velocity", StateViolationSeverity::Warning}); + } + } + }}); + + AddRule({"Physics.DynamicZeroMass", "Physics", StateViolationSeverity::Error, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* rb = w.GetComponent(entity); + if (rb && rb->type == RigidBodyComponent::Type::Dynamic && rb->mass <= 0.0f) + { + out.push_back({"Physics.DynamicZeroMass", static_cast(entity), + "Dynamic body has mass=" + std::to_string(rb->mass), + StateViolationSeverity::Error}); + } + } + }}); + + // -- AI rules -- + + AddRule({"AI.CombatNoTarget", "AI", StateViolationSeverity::Error, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* ai = w.GetComponent(entity); + if (ai && ai->state == AIComponent::State::Combat && ai->targetEntity == entt::null) + { + out.push_back({"AI.CombatNoTarget", static_cast(entity), + "AI in Combat state with no target entity", StateViolationSeverity::Error}); + } + } + }}); + + AddRule({"AI.DeadWithTarget", "AI", StateViolationSeverity::Warning, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* ai = w.GetComponent(entity); + if (ai && ai->state == AIComponent::State::Dead && ai->targetEntity != entt::null) + { + out.push_back({"AI.DeadWithTarget", static_cast(entity), + "Dead AI still has targetEntity set", StateViolationSeverity::Warning}); + } + } + }}); + + AddRule({"AI.FleeingNoTarget", "AI", StateViolationSeverity::Error, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* ai = w.GetComponent(entity); + if (ai && ai->state == AIComponent::State::Fleeing && ai->targetEntity == entt::null) + { + out.push_back({"AI.FleeingNoTarget", static_cast(entity), + "AI is Fleeing with no target entity", StateViolationSeverity::Error}); + } + } + }}); + + // -- Interaction rules -- + + AddRule({"Interaction.DestroyedWithUses", "Interaction", StateViolationSeverity::Error, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* ic = w.GetComponent(entity); + if (ic && ic->state == InteractionComponent::State::Destroyed && ic->usesRemaining > 0) + { + out.push_back( + {"Interaction.DestroyedWithUses", static_cast(entity), + "Destroyed interaction has usesRemaining=" + std::to_string(ic->usesRemaining), + StateViolationSeverity::Error}); + } + } + }}); + + AddRule({"Interaction.CooldownExpired", "Interaction", StateViolationSeverity::Warning, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* ic = w.GetComponent(entity); + if (ic && ic->state == InteractionComponent::State::Cooldown && ic->cooldownRemaining <= 0.0f) + { + out.push_back({"Interaction.CooldownExpired", static_cast(entity), + "In Cooldown state but cooldownRemaining<=0", + StateViolationSeverity::Warning}); + } + } + }}); + + AddRule({"Interaction.HoldProgressOnNonHold", "Interaction", StateViolationSeverity::Warning, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* ic = w.GetComponent(entity); + if (ic && ic->type != InteractionComponent::InteractionType::Hold && ic->holdProgress > 0.0f) + { + out.push_back({"Interaction.HoldProgressOnNonHold", static_cast(entity), + "holdProgress>0 on non-Hold interaction type", + StateViolationSeverity::Warning}); + } + } + }}); + + // -- Network rules -- + + AddRule({"Network.DuplicateIDs", "Network", StateViolationSeverity::Critical, true, + [](World& w, std::vector& out) + { + std::unordered_set seen; + for (auto entity : w.GetEntitiesWith()) + { + auto* ni = w.GetComponent(entity); + if (!ni || ni->networkID == 0) + continue; + if (!seen.insert(ni->networkID).second) + { + out.push_back({"Network.DuplicateIDs", static_cast(entity), + "Duplicate networkID=" + std::to_string(ni->networkID), + StateViolationSeverity::Critical}); + } + } + }}); + + // -- Ragdoll rules -- + + AddRule({"Ragdoll.AnimatedWithBlend", "Ragdoll", StateViolationSeverity::Warning, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* rc = w.GetComponent(entity); + if (rc && rc->mode == RagdollComponent::Mode::Animated && rc->blendWeight > 0.01f) + { + out.push_back({"Ragdoll.AnimatedWithBlend", static_cast(entity), + "Animated ragdoll has blendWeight=" + std::to_string(rc->blendWeight), + StateViolationSeverity::Warning}); + } + } + }}); + + // -- Destructible rules -- + + AddRule({"Destructible.HealthStageDesync", "Destructible", StateViolationSeverity::Error, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* dc = w.GetComponent(entity); + if (dc && dc->health > 0.0f && dc->currentStage >= dc->damageStages) + { + out.push_back({"Destructible.HealthStageDesync", static_cast(entity), + "health>0 but currentStage=" + std::to_string(dc->currentStage) + + " >= damageStages=" + std::to_string(dc->damageStages), + StateViolationSeverity::Error}); + } + } + }}); + + AddRule({"CoverPoint.OverOccupied", "Destructible", StateViolationSeverity::Warning, true, + [](World& w, std::vector& out) + { + for (auto entity : w.GetEntitiesWith()) + { + auto* cp = w.GetComponent(entity); + if (cp && cp->currentOccupants > cp->maxOccupants) + { + out.push_back({"CoverPoint.OverOccupied", static_cast(entity), + "currentOccupants=" + std::to_string(cp->currentOccupants) + + " > maxOccupants=" + std::to_string(cp->maxOccupants), + StateViolationSeverity::Warning}); + } + } + }}); + } + +} // namespace Spark diff --git a/SparkEngine/Source/Utils/InvalidStateDetector.h b/SparkEngine/Source/Utils/InvalidStateDetector.h new file mode 100644 index 00000000..2d0e1b98 --- /dev/null +++ b/SparkEngine/Source/Utils/InvalidStateDetector.h @@ -0,0 +1,169 @@ +/** + * @file InvalidStateDetector.h + * @brief Runtime detection of impossible/contradictory ECS component state combinations + * @author Spark Engine Team + * @date 2026 + * + * Periodically scans ECS entities for cross-component state invariant violations — + * impossible combinations like "dead entity in combat" or "static body with velocity." + * Analogous to server-side movement validation in MMOs that detects desync bugs. + * + * Usage: + * @code + * Spark::InvalidStateDetector::GetInstance().Initialize(); + * Spark::InvalidStateDetector::GetInstance().SetWorld(myWorld); + * // In main loop: + * Spark::InvalidStateDetector::GetInstance().Update(deltaTime); + * // At shutdown: + * Spark::InvalidStateDetector::GetInstance().Shutdown(); + * @endcode + * + * Game modules can register additional rules via AddRule() during initialization. + * + * @note Active in Debug and Release builds. Compiled to no-ops in SPARK_SHIPPING. + * @see FreezeDetector.h, HitchDetector.h, MemoryMonitor.h + */ + +#pragma once + +#include "../Core/Platform.h" + +#include +#include +#include +#include + +// Forward declaration — avoids pulling ECS headers into every translation unit +class World; + +namespace Spark +{ + + // ========================================================================= + // Enums & Data Types + // ========================================================================= + + /// Severity of a detected state violation. + enum class StateViolationSeverity : uint8_t + { + Warning, ///< Suspicious but possibly transient (e.g. one-frame desync). + Error, ///< Likely bug — state should not persist. + Critical ///< Definitely wrong — immediate investigation needed. + }; + + /// A single detected state violation. + struct StateViolation + { + std::string ruleName; ///< Which rule was violated. + uint32_t entityId = 0; ///< Entity with the invalid state. + std::string details; ///< Human-readable description. + StateViolationSeverity severity = StateViolationSeverity::Error; + }; + + /// Callback signature for a validation rule check. + /// The rule iterates the World for its target components and appends any violations found. + using StateCheckFn = std::function&)>; + + /// A registered validation rule. + struct StateValidationRule + { + std::string name; ///< Unique rule identifier (e.g. "Health.DeadButPositive"). + std::string category; ///< Grouping for console output (e.g. "Health", "Physics"). + StateViolationSeverity severity = StateViolationSeverity::Error; + bool enabled = true; ///< Can be toggled at runtime. + StateCheckFn checkFn; ///< The actual validation logic. + }; + + // ========================================================================= + // Configuration + // ========================================================================= + + /// Configuration for the InvalidStateDetector. + struct InvalidStateDetectorConfig + { + bool enabled = true; ///< Master enable/disable. + float checkIntervalSec = 1.0f; ///< Seconds between full scans (0 = every frame). + uint32_t maxViolationHistory = 64; ///< Ring buffer capacity for recent violations. + bool logOnDetection = true; ///< Log each violation via SPARK_LOG_WARN. + }; + + /// Snapshot of detector status for diagnostics. + struct InvalidStateDetectorStatus + { + uint32_t totalChecks = 0; ///< Number of full scans performed. + uint32_t totalViolations = 0; ///< Cumulative violations detected. + uint32_t activeRules = 0; ///< Number of enabled rules. + uint32_t totalRules = 0; ///< Total registered rules. + std::vector recentViolations; ///< Last N violations (ring buffer snapshot). + }; + + // ========================================================================= + // InvalidStateDetector + // ========================================================================= + + /** + * @brief Singleton runtime detector for impossible ECS state combinations. + * + * Periodically iterates ECS component views and checks registered validation + * rules. Each rule targets specific component combinations and reports any + * violations. Core engine rules are registered automatically; game modules + * can add domain-specific rules via AddRule(). + */ + class InvalidStateDetector + { + public: + static InvalidStateDetector& GetInstance(); + + void Initialize(); + void Update(float dt); + void Shutdown(); + + /// Set the ECS world to scan. Must be called before Update() does anything. + void SetWorld(::World* world) { m_world = world; } + + // -- Configuration -- + void Configure(const InvalidStateDetectorConfig& config) { m_config = config; } + [[nodiscard]] const InvalidStateDetectorConfig& GetConfig() const { return m_config; } + + // -- Rule management -- + void AddRule(StateValidationRule rule); + void SetRuleEnabled(const std::string& name, bool enabled); + [[nodiscard]] uint32_t GetRuleCount() const { return static_cast(m_rules.size()); } + + // -- Query -- + [[nodiscard]] InvalidStateDetectorStatus GetStatus() const; + [[nodiscard]] uint32_t GetViolationCount() const { return m_totalViolations; } + + void RegisterConsoleCommands(); + + private: + InvalidStateDetector() = default; + ~InvalidStateDetector() = default; + InvalidStateDetector(const InvalidStateDetector&) = delete; + InvalidStateDetector& operator=(const InvalidStateDetector&) = delete; + + void RegisterDefaultRules(); + void RunChecks(); + + InvalidStateDetectorConfig m_config; + std::vector m_rules; + std::vector m_violations; ///< Ring buffer of recent violations. + + ::World* m_world = nullptr; + float m_timeSinceLastCheck = 0.0f; + uint32_t m_totalChecks = 0; + uint32_t m_totalViolations = 0; + bool m_initialized = false; + }; + +} // namespace Spark + +// ========================================================================= +// Shipping-safe macro +// ========================================================================= + +#ifndef SPARK_SHIPPING +#define SPARK_STATE_CHECK_UPDATE(dt) Spark::InvalidStateDetector::GetInstance().Update(dt) +#else +#define SPARK_STATE_CHECK_UPDATE(dt) ((void)0) +#endif diff --git a/SparkEngine/Source/Utils/Profiler.cpp b/SparkEngine/Source/Utils/Profiler.cpp index 5373695a..602195d6 100644 --- a/SparkEngine/Source/Utils/Profiler.cpp +++ b/SparkEngine/Source/Utils/Profiler.cpp @@ -176,12 +176,21 @@ void Profiler::BeginGPUTimer(std::string_view name) { D3D11_QUERY_DESC desc = {}; desc.Query = D3D11_QUERY_TIMESTAMP; - m_device->CreateQuery(&desc, timer.beginQuery.GetAddressOf()); - m_device->CreateQuery(&desc, timer.endQuery.GetAddressOf()); + HRESULT hr1 = m_device->CreateQuery(&desc, timer.beginQuery.GetAddressOf()); + HRESULT hr2 = m_device->CreateQuery(&desc, timer.endQuery.GetAddressOf()); D3D11_QUERY_DESC disjointDesc = {}; disjointDesc.Query = D3D11_QUERY_TIMESTAMP_DISJOINT; - m_device->CreateQuery(&disjointDesc, timer.disjointQuery.GetAddressOf()); + HRESULT hr3 = m_device->CreateQuery(&disjointDesc, timer.disjointQuery.GetAddressOf()); + + if (FAILED(hr1) || FAILED(hr2) || FAILED(hr3)) + { + SPARK_LOG_WARN(Spark::LogCategory::Core, "Profiler: failed to create GPU timestamp queries"); + timer.beginQuery.Reset(); + timer.endQuery.Reset(); + timer.disjointQuery.Reset(); + return; + } } m_context->Begin(timer.disjointQuery.Get()); diff --git a/wiki/Codebase-Statistics.md b/wiki/Codebase-Statistics.md index ba3e98f7..5b4b4b67 100644 --- a/wiki/Codebase-Statistics.md +++ b/wiki/Codebase-Statistics.md @@ -8,20 +8,20 @@ Comprehensive metrics and analysis of the SparkEngine codebase. Updated 2026-04- | Section | Lines | |---------|------:| -| **SparkEngine/Source** | 249851 | -| **SparkEditor/Source** | 82920 | -| **GameModules** | 57465 | +| **SparkEngine/Source** | 250647 | +| **SparkEditor/Source** | 82949 | +| **GameModules** | 57824 | | **Tests** | 94741 | | **SparkConsole/src** | 1858 | | **SparkShaderCompiler/src** | 533 | -| **Total C++ (excl. ThirdParty)** | **~487368** | +| **Total C++ (excl. ThirdParty)** | **~488552** | ### File Counts | Category | Count | |----------|------:| -| Header files (.h/.hpp) | 711 | -| Implementation files (.cpp) | 804 | +| Header files (.h/.hpp) | 712 | +| Implementation files (.cpp) | 805 | | HLSL shader files | 38 | | GLSL shader files | 14 | | AngelScript files (.as) | 1 | @@ -33,8 +33,8 @@ Comprehensive metrics and analysis of the SparkEngine codebase. Updated 2026-04- | Metric | Value | |--------|-------| | Average lines per .cpp file | ~857 | -| Average lines per .h file | ~571 | -| Largest codebase section | Graphics (100049 lines — 40% of SparkEngine/Source) | +| Average lines per .h file | ~572 | +| Largest codebase section | Graphics (100057 lines — 39% of SparkEngine/Source) | ## SparkEngine/Source Breakdown @@ -42,13 +42,13 @@ Comprehensive metrics and analysis of the SparkEngine codebase. Updated 2026-04- | Subsystem | Lines | % of Source | |-----------|------:|:----------:| -| Graphics | 100049 | 40.0% | -| Engine (all subsystems) | 74043 | 29.6% | -| Utils | 33700 | 13.4% | -| Core | 17549 | 7.0% | +| Graphics | 100057 | 39.9% | +| Engine (all subsystems) | 74066 | 29.5% | +| Utils | 34413 | 13.7% | +| Core | 17600 | 7.0% | | Physics | 10101 | 4.0% | | Audio | 5547 | 2.2% | -| Input | 3345 | 1.3% | +| Input | 3346 | 1.3% | | SceneManager | 1886 | 0.7% | | Enums | 1423 | 0.5% | | Game | 1272 | 0.5% | @@ -58,12 +58,12 @@ Comprehensive metrics and analysis of the SparkEngine codebase. Updated 2026-04- | Subsystem | Lines | |-----------|------:| -| AI | 13124 | -| Networking | 11626 | +| AI | 13129 | +| Networking | 11631 | | ECS | 8523 | | Gameplay | 7278 | -| Animation | 6533 | -| Scripting | 4531 | +| Animation | 6538 | +| Scripting | 4539 | | UI | 2524 | | SaveSystem | 2491 | | Streaming | 1763 | @@ -99,7 +99,7 @@ Comprehensive metrics and analysis of the SparkEngine codebase. Updated 2026-04- | Metric | Count | |--------|------:| | Editor panel classes | 57 | -| Total editor lines | 82920 | +| Total editor lines | 82949 | ## Testing Metrics @@ -155,7 +155,7 @@ Comprehensive metrics and analysis of the SparkEngine codebase. Updated 2026-04- | File | Lines | |------|------:| | `OpenGLDevice.cpp` | 1938 | -| `SparkEngine.cpp` | 1889 | +| `SparkEngine.cpp` | 1894 | | `VulkanDevice.cpp` | 1728 | | `GraphicsEngine.cpp` | 1695 | | `D3D12Device.cpp` | 1564 | @@ -187,8 +187,8 @@ Comprehensive metrics and analysis of the SparkEngine codebase. Updated 2026-04- | `VisualScriptPanel.cpp` | 1850 | | `EditorUI.cpp` | 1673 | | `CollaborativeEditSession.cpp` | 1382 | +| `EditorTheme.cpp` | 1341 | | `PerformanceProfiler.cpp` | 1332 | -| `EditorTheme.cpp` | 1325 | | `LevelStreamingSystem.cpp` | 1272 | | `MaterialEditor.cpp` | 1269 | | `InspectorPanel.cpp` | 1262 | diff --git a/wiki/Engine-Architecture-Flowchart.md b/wiki/Engine-Architecture-Flowchart.md index 548850e2..2ce7675b 100644 --- a/wiki/Engine-Architecture-Flowchart.md +++ b/wiki/Engine-Architecture-Flowchart.md @@ -3,7 +3,7 @@ > Complete visual guide to how SparkEngine works, from boot to shutdown. -_Generated 2026-04-05 from 582 headers, 392 source files, 79 ECS components, 11 ECS systems, 57 editor panels, 6 RHI backends._ +_Generated 2026-04-05 from 583 headers, 393 source files, 79 ECS components, 11 ECS systems, 57 editor panels, 6 RHI backends._ --- diff --git a/wiki/Home.md b/wiki/Home.md index ad555cc5..c84e3659 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -178,12 +178,12 @@ SparkEngine is licensed under the [Spark Open License](https://github.com/Krilli | Metric | Count | |--------|-------| -| Header files | 603 | +| Header files | 604 | | ECS Components | 79 | | ECS Systems | 69 | | Editor Panels | 57 | | Test files | 303 | | Test cases | 3741+ | | Wiki pages | 103 | -| *Last synced* | *2026-04-05 18:42* | +| *Last synced* | *2026-04-05 22:09* |