diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..48331dc --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,81 @@ +name: ci + +on: + pull_request: + branches: + - develop + - main + push: + branches: + - develop + - main + +jobs: + foundation-tests: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v5 + + - name: Configure headless foundation tests + run: > + cmake -S . -B build + -DECOSIM_BUILD_APP=OFF + -DBUILD_TESTING=ON + -DCMAKE_BUILD_TYPE=Release + + - name: Build foundation tests + run: cmake --build build --parallel + + - name: Run foundation tests + run: ctest --test-dir build --output-on-failure + + linux-app-build: + runs-on: ubuntu-22.04 + + steps: + - name: Check out repository + uses: actions/checkout@v5 + + - name: Install SFML Linux build dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + xorg-dev \ + libxrandr-dev \ + libxcursor-dev \ + libxi-dev \ + libudev-dev \ + libflac-dev \ + libvorbis-dev \ + libgl1-mesa-dev \ + libegl1-mesa-dev \ + libdrm-dev \ + libgbm-dev + + - name: Configure full application + run: > + cmake -S . -B build-app + -DBUILD_TESTING=OFF + -DBUILD_SHARED_LIBS=OFF + -DCMAKE_BUILD_TYPE=Release + + - name: Build full application + run: cmake --build build-app --parallel + + windows-app-build: + runs-on: windows-latest + + steps: + - name: Check out repository + uses: actions/checkout@v5 + + - name: Configure full application + run: > + cmake -S . -B build-app + -DBUILD_TESTING=OFF + -DBUILD_SHARED_LIBS=OFF + + - name: Build full application + run: cmake --build build-app --config Release --parallel diff --git a/.gitignore b/.gitignore index 0dd8404..5a2a237 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,32 @@ -# Visual Studio temp & settings +# Visual Studio .vs/ -*.vcxproj.user *.suo +*.user -# Build outputs -Debug/ -Release/ -*.exe -*.dll -*.lib -*.obj -*.pdb +# CMake and build outputs +/build/ +/out/ +/cmake-build-*/ +CMakeFiles/ +CMakeCache.txt +CMakeScripts/ +Testing/ +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json -# OS stuff +# IDE / editor state +.idea/ +.vscode/ +*.code-workspace +CMakeUserPresets.json + +# Runtime-generated files +imgui.ini +/saves/ +*.log + +# OS metadata Thumbs.db .DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0a70341 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,107 @@ +cmake_minimum_required(VERSION 3.28) + +project(EcoSimEngine VERSION 0.1.0 LANGUAGES CXX) + +option(ECOSIM_BUILD_APP "Build the EcoSimEngine desktop application" ON) + +function(ecosim_enable_warnings target) + if(MSVC) + target_compile_options(${target} PRIVATE /W4 /permissive-) + else() + target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic) + endif() +endfunction() + +if(ECOSIM_BUILD_APP) + include(FetchContent) + + find_package(SFML 3.1 QUIET COMPONENTS Graphics Window System Audio) + if(NOT SFML_FOUND) + message(STATUS "SFML 3.1 not found locally; fetching SFML 3.1.0") + set(SFML_BUILD_NETWORK OFF CACHE BOOL "Do not build unused SFML Network module" FORCE) + set(SFML_USE_SYSTEM_DEPS OFF CACHE BOOL "Use SFML bundled fallback dependencies" FORCE) + FetchContent_Declare( + SFML + GIT_REPOSITORY https://github.com/SFML/SFML.git + GIT_TAG 3.1.0 + GIT_SHALLOW TRUE + ) + FetchContent_MakeAvailable(SFML) + endif() + + find_package(OpenGL REQUIRED) + + set(ECOSIM_SOURCES + src/main.cpp + src/SimulationEngine.cpp + src/gui/GUIManager.cpp + src/ecs/Assets.cpp + src/scene/Scene.cpp + src/scene/Scene_Menu.cpp + src/scene/Scene_Simulation.cpp + ) + + set(IMGUI_SOURCES + external/imgui/imgui.cpp + external/imgui/imgui_draw.cpp + external/imgui/imgui_tables.cpp + external/imgui/imgui_widgets.cpp + external/imgui-sfml/imgui-SFML.cpp + ) + + add_executable(EcoSimEngine ${ECOSIM_SOURCES} ${IMGUI_SOURCES}) + target_compile_features(EcoSimEngine PRIVATE cxx_std_20) + target_include_directories( + EcoSimEngine + PRIVATE + include + external + external/imgui + external/imgui-sfml + external/nlohmann + ) + target_link_libraries( + EcoSimEngine + PRIVATE + SFML::Graphics + SFML::Window + SFML::System + SFML::Audio + OpenGL::GL + ) + target_compile_definitions( + EcoSimEngine + PRIVATE + $<$:DEBUG_BUILD> + ) + ecosim_enable_warnings(EcoSimEngine) + + add_custom_command( + TARGET EcoSimEngine + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_SOURCE_DIR}/config" + "$/config" + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_SOURCE_DIR}/resources" + "$/resources" + COMMENT "Copying runtime configuration and resources" + ) +endif() + +include(CTest) + +if(BUILD_TESTING) + add_executable( + EcoSimEngineFoundationTests + tests/foundation_tests.cpp + ) + target_compile_features(EcoSimEngineFoundationTests PRIVATE cxx_std_20) + target_include_directories(EcoSimEngineFoundationTests PRIVATE include) + ecosim_enable_warnings(EcoSimEngineFoundationTests) + + add_test( + NAME foundation + COMMAND EcoSimEngineFoundationTests + ) +endif() diff --git a/EcoSimEngine.sln b/EcoSimEngine.sln deleted file mode 100644 index c47932d..0000000 --- a/EcoSimEngine.sln +++ /dev/null @@ -1,31 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.13.35931.197 d17.13 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EcoSimEngine", "EcoSimEngine.vcxproj", "{004B7F30-AAB7-C613-E5BE-07B367293AC4}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {004B7F30-AAB7-C613-E5BE-07B367293AC4}.Debug|x64.ActiveCfg = Debug|x64 - {004B7F30-AAB7-C613-E5BE-07B367293AC4}.Debug|x64.Build.0 = Debug|x64 - {004B7F30-AAB7-C613-E5BE-07B367293AC4}.Debug|x86.ActiveCfg = Debug|Win32 - {004B7F30-AAB7-C613-E5BE-07B367293AC4}.Debug|x86.Build.0 = Debug|Win32 - {004B7F30-AAB7-C613-E5BE-07B367293AC4}.Release|x64.ActiveCfg = Release|x64 - {004B7F30-AAB7-C613-E5BE-07B367293AC4}.Release|x64.Build.0 = Release|x64 - {004B7F30-AAB7-C613-E5BE-07B367293AC4}.Release|x86.ActiveCfg = Release|Win32 - {004B7F30-AAB7-C613-E5BE-07B367293AC4}.Release|x86.Build.0 = Release|Win32 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {9360ED07-C92B-49DB-81EF-71014CC6EC51} - EndGlobalSection -EndGlobal diff --git a/EcoSimEngine.vcxproj b/EcoSimEngine.vcxproj deleted file mode 100644 index 1d05047..0000000 --- a/EcoSimEngine.vcxproj +++ /dev/null @@ -1,203 +0,0 @@ - - - - - Debug - Win32 - - - Release - Win32 - - - Debug - x64 - - - Release - x64 - - - - 17.0 - {004B7F30-AAB7-C613-E5BE-07B367293AC4} - Win32Proj - - - - Application - true - v143 - - - Application - false - v143 - - - Application - true - v143 - - - Application - false - v143 - - - - - - - - - - - - - - - - - - - - - true - - - true - - - true - - - true - - - - WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) - Level3 - - - true - Windows - - - - - _DEBUG;_WINDOWS;%(PreprocessorDefinitions) - Level3 - stdcpp20 - $(ProjectDir)include;D:\SFML-3.0.0\include;$(ProjectDir)external\;$(ProjectDir)external\imgui;$(ProjectDir)external\imgui-sfml;%(AdditionalIncludeDirectories) - - - true - Console - D:\SFML-3.0.0\lib - sfml-graphics-d.lib;sfml-window-d.lib;sfml-system-d.lib;sfml-audio-d.lib;%(AdditionalDependencies);opengl32.lib - - - - - WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) - Level3 - - - true - Windows - true - true - - - - - NDEBUG;_WINDOWS;%(PreprocessorDefinitions) - Level3 - stdcpp20 - $(ProjectDir)include;D:\SFML-3.0.0\include;$(ProjectDir)external\;$(ProjectDir)external\imgui;$(ProjectDir)external\imgui-sfml;%(AdditionalIncludeDirectories) - - - true - Console - true - true - D:\SFML-3.0.0\lib - sfml-graphics.lib;sfml-window.lib;sfml-system.lib;sfml-audio.lib;%(AdditionalDependencies);opengl32.lib - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/EcoSimEngine.vcxproj.filters b/EcoSimEngine.vcxproj.filters deleted file mode 100644 index 481c455..0000000 --- a/EcoSimEngine.vcxproj.filters +++ /dev/null @@ -1,184 +0,0 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hh;hpp;hxx;hm;inl;inc;xsd - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav - - - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - - - - - - - - - - - - - - - - - - - - Resource Files - - - Resource Files - - - \ No newline at end of file diff --git a/README.md b/README.md index 4d2be54..dce9a4f 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,102 @@ # EcoSimEngine + ![License](https://img.shields.io/github/license/czlin7/EcoSimEngine) -**EcoSimEngine** is a modular ecosystem simulation engine developed in C++20. It leverages SFML 3.0.0 for graphics rendering and nlohmann::json 3.12.0 for configuration management. The engine employs an Entity-Component-System (ECS) architecture to simulate complex interactions within a 2D environment. +EcoSimEngine is a pre-alpha ecosystem simulation project written in C++20. It currently combines a small Entity-Component-System (ECS), scene management, event dispatch, SFML 3 rendering/audio, Dear ImGui via ImGui-SFML, and JSON-driven simulation resources. -### 📦 Features -- **ECS Architecture**: Utilizes components like `CHealth`, `CBehavior`, and `CTransform` to define entity attributes and behaviours. -- **Modular Design**: Easily extendable with additional components and systems. -- **2D Rendering**: Powered by SFML 3.0.0 for efficient graphics handling. -- **JSON Configuration**: Structured configuration files for environment and simulation settings. +## Current foundation -### 🔧 Dependencies -- **C++20**: Ensure your compiler supports C++20 features. -- **[SFML 3.0.0](https://www.sfml-dev.org/download/sfml/3.0.0/)**: For GUI with graphics, window, and audio functionalities. -- **[nlohmann::json 3.12.0](https://github.com/nlohmann/json/releases/tag/v3.12.0)**: For parsing and handling JSON configuration files. - - I decide use `json` file instead of plain `txt` file for configuration, because it is more structured and easier to manage. +Implemented areas include: -### 🗂 Project Structure -```make -EcoSimEngine/ -├── config/ # JSON configuration files -├── include/ # Header files -├── resources/ # Fonts, textures, and other assets -├── src/ # Source files -├── EcoSimEngine.sln # Visual Studio solution file -├── EcoSimEngine.vcxproj # Visual Studio project file -├── LICENSE # Project license -└── README.md # Project documentation -``` +- entity, component, and system managers; +- component-signature based system membership; +- menu and simulation scenes; +- movement and AI systems; +- a header-only event bus for engine/GUI commands; +- SFML 3 rendering and audio; +- ImGui-SFML GUI integration; +- JSON configuration, species definitions, and default simulation data; +- CMake-based builds; +- small headless foundation tests for core managers and the event bus. -### 🛠 Installation & Build +The project is still under active development. Save/load, simulation behaviour, GUI workflows, and test coverage are not yet complete. -1. Clone the Repository: -``` -git clone -b prototype https://github.com/czlin7/EcoSimEngine.git -cd EcoSimEngine -``` -2. Open the Project: - - **Visual Studio**: Open `EcoSimEngine.sln` - - **Other IDEs**: Open the project using the appropriate project file. -3. Configure Dependencies: - - Ensure SFML 3.0.0 and nlohmann::json 3.12.0 are correctly linked in your project settings. -4. Build the Project: - - Select the desired configuration (`Debug` or `Release`) and build the project. - -### 🤝 Contributing -We welcome contributions to EcoSimEngine! To contribute: - -1. **Fork the repository** to your own GitHub account. -2. **Create a new branch** for your feature or bug fix: -``` -git checkout -b feature/YourFeatureName +## Requirements + +- CMake 3.28+ +- a C++20-capable compiler +- Git when CMake needs to fetch SFML + +The repository vendors Dear ImGui, ImGui-SFML, and nlohmann/json. CMake first looks for an installed SFML 3.1 package and otherwise fetches SFML 3.1.0 during configuration. Only the SFML modules used by EcoSimEngine are built. + +## Build + +Configure from the repository root: + +```bash +cmake -S . -B build +cmake --build build --config Release ``` -3. **Make your changes** in the branch. Follow existing ECS structure and coding style. -4. **Commit your changes** with clear, descriptive messages. Use prefixes like `[feat]:` or `[fix]:`. + +For single-configuration generators such as Unix Makefiles or Ninja, choose the build type during configuration: + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build ``` -git commit -m "[feat]: Add CBehavior component" + +After the application target is built, CMake copies `config/` and `resources/` next to the executable so the current relative runtime paths continue to work. + +## Tests + +Run the foundation tests after configuring: + +```bash +ctest --test-dir build -C Release --output-on-failure ``` -5. **Push your branch** to your fork: + +The core tests can also be configured without SFML or the desktop application: + +```bash +cmake -S . -B build-tests \ + -DECOSIM_BUILD_APP=OFF \ + -DBUILD_TESTING=ON \ + -DCMAKE_BUILD_TYPE=Release + +cmake --build build-tests +ctest --test-dir build-tests --output-on-failure ``` -git push origin feature/YourFeatureName + +This headless path is used by CI to validate the dependency-free core managers and event bus. CI also builds the complete desktop application on Linux and Windows. + +## Project structure + +```text +EcoSimEngine/ +├── .github/workflows/ # CI +├── config/ # Runtime JSON configuration +├── external/ # Vendored ImGui, ImGui-SFML, nlohmann/json +├── include/EcoSimEngine/ +│ ├── gui/ +│ ├── component/ +│ ├── ecs/ +│ ├── event/ +│ ├── math/ +│ ├── scene/ +│ ├── system/ +│ └── utils/ +├── resources/ # Assets, defaults, and simulation definitions +├── src/ # Application implementation +├── tests/ # Headless foundation tests +├── CMakeLists.txt +├── LICENSE +└── README.md ``` -6. **Open a Pull Request** against the `prototype` branch of the main repository. - - **Guidelines:** - - Ensure your code builds and passes any existing tests. - - Keep commits small and focused. - - Include comments and documentation for new functionality. +## Development status + +The current goal is to keep the foundation small, understandable, and buildable while extending the simulation incrementally. New work should preserve clear ownership between the engine, ECS managers, scenes, systems, and GUI rather than adding abstraction without a concrete need. + +## License -### 📄 License -This project is licensed under the [GPL-3.0 License](LICENSE). +EcoSimEngine is licensed under the GPL-3.0 License. See `LICENSE`. diff --git a/imgui.ini b/imgui.ini deleted file mode 100644 index ee97343..0000000 --- a/imgui.ini +++ /dev/null @@ -1,16 +0,0 @@ -[Window][Debug##Default] -Pos=60,60 -Size=400,400 - -[Window][Status] -Pos=1144,35 -Size=77,54 - -[Window][Simulation Info] -Pos=1057,98 -Size=171,198 - -[Window][##MainMenuBar] -Pos=0,0 -Size=1280,19 - diff --git a/include/EcoSimEngine/SimulationEngine.hpp b/include/EcoSimEngine/SimulationEngine.hpp index 8457746..617fe6d 100644 --- a/include/EcoSimEngine/SimulationEngine.hpp +++ b/include/EcoSimEngine/SimulationEngine.hpp @@ -8,7 +8,7 @@ #include "EcoSimEngine/scene/SceneManager.hpp" #include "EcoSimEngine/ecs/Assets.hpp" #include "EcoSimEngine/system/SystemManager.hpp" -#include "EcoSimEngine/GUI/GUIManager.hpp" +#include "EcoSimEngine/gui/GUIManager.hpp" #include "EcoSimEngine/event/EventBus.hpp" struct WindowConfig { diff --git a/include/EcoSimEngine/component/ComponentIndices.hpp b/include/EcoSimEngine/component/ComponentIndices.hpp index d7c6a4a..ee4a665 100644 --- a/include/EcoSimEngine/component/ComponentIndices.hpp +++ b/include/EcoSimEngine/component/ComponentIndices.hpp @@ -1,9 +1,9 @@ #pragma once + #include #include -constexpr size_t MAX_COMPONENTS = 64; - +constexpr std::size_t MAX_COMPONENTS = 64; constexpr std::size_t COMP_INDEX_CTransform = 0; constexpr std::size_t COMP_INDEX_CSpecies = 1; @@ -12,5 +12,4 @@ constexpr std::size_t COMP_INDEX_CEnergy = 3; constexpr std::size_t COMP_INDEX_CReproductive = 4; constexpr std::size_t COMP_INDEX_CBehavior = 5; - -using Signature = std::bitset; \ No newline at end of file +using Signature = std::bitset; diff --git a/include/EcoSimEngine/component/ComponentManager.hpp b/include/EcoSimEngine/component/ComponentManager.hpp index 285c23f..1ebbacb 100644 --- a/include/EcoSimEngine/component/ComponentManager.hpp +++ b/include/EcoSimEngine/component/ComponentManager.hpp @@ -1,52 +1,99 @@ #pragma once -#include -#include +#include #include +#include +#include +#include +#include class ComponentManager { - struct IStore { virtual ~IStore() = default; }; - template struct Store : IStore { - std::unordered_map data; - }; - - std::unordered_map> stores; - - template Store& ensure() { - auto ti = std::type_index(typeid(T)); - auto it = stores.find(ti); - if (it == stores.end()) { - stores[ti] = std::make_unique>(); - } - return *static_cast*>(stores[ti].get()); - } + struct IStore { + virtual ~IStore() = default; + }; + + template + struct Store final : IStore { + std::unordered_map data; + }; + + std::unordered_map> m_stores; + + template + Store& ensureStore() { + const auto type = std::type_index(typeid(T)); + auto it = m_stores.find(type); + if (it == m_stores.end()) { + it = m_stores.emplace(type, std::make_unique>()).first; + } + return *static_cast*>(it->second.get()); + } + + template + Store* findStore() noexcept { + const auto it = m_stores.find(std::type_index(typeid(T))); + if (it == m_stores.end()) { + return nullptr; + } + return static_cast*>(it->second.get()); + } + + template + const Store* findStore() const noexcept { + const auto it = m_stores.find(std::type_index(typeid(T))); + if (it == m_stores.end()) { + return nullptr; + } + return static_cast*>(it->second.get()); + } public: - template - T& add(std::size_t id, Args&&... args) { - auto& store = ensure(); - auto [it, _] = store.data.emplace(id, T(std::forward(args)...)); - return it->second; - } - - template - bool has(std::size_t id) const { - auto ti = std::type_index(typeid(T)); - auto it = stores.find(ti); - if (it == stores.end()) return false; - auto* s = static_cast*>(it->second.get()); - return s->data.find(id) != s->data.end(); - } - - template T& get(std::size_t id) { - auto& s = ensure(); - return s.data.at(id); - } - - template void remove(std::size_t id) { - auto& s = ensure(); - s.data.erase(id); - } - - void clear() { stores.clear(); } -}; \ No newline at end of file + template + T& add(std::size_t id, Args&&... args) { + auto& store = ensureStore(); + auto [it, inserted] = + store.data.emplace(id, T(std::forward(args)...)); + + if (!inserted) { + throw std::logic_error("Entity already has this component type"); + } + + return it->second; + } + + template + [[nodiscard]] bool has(std::size_t id) const noexcept { + const auto* store = findStore(); + return store != nullptr && store->data.contains(id); + } + + template + T& get(std::size_t id) { + auto* store = findStore(); + if (store == nullptr) { + throw std::out_of_range("Component type is not registered"); + } + return store->data.at(id); + } + + template + const T& get(std::size_t id) const { + const auto* store = findStore(); + if (store == nullptr) { + throw std::out_of_range("Component type is not registered"); + } + return store->data.at(id); + } + + template + void remove(std::size_t id) noexcept { + auto* store = findStore(); + if (store != nullptr) { + store->data.erase(id); + } + } + + void clear() noexcept { + m_stores.clear(); + } +}; diff --git a/include/EcoSimEngine/ecs/Action.hpp b/include/EcoSimEngine/ecs/Action.hpp index 00e0822..1f97c05 100644 --- a/include/EcoSimEngine/ecs/Action.hpp +++ b/include/EcoSimEngine/ecs/Action.hpp @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include "EcoSimEngine/math/Vec2.hpp" @@ -12,68 +12,85 @@ enum class ActionName { RIGHT, SELECT, BACK, - PAUSE, // pause the game + PAUSE, QUIT_AND_SAVE, - LEFT_CLICK, - MIDDLE_CLICK, - RIGHT_CLICK, - MOUSE_MOVE, - TOGGLE_FOLLOW, // toggle camera follow - TOGGLE_TEXTURE, // toggle drawing textures - TOGGLE_COLLISION, // toggle drawing collision boxes - TOGGLE_GRID, // toggle drawing grid + LEFT_CLICK, + MIDDLE_CLICK, + RIGHT_CLICK, + MOUSE_MOVE, + TOGGLE_FOLLOW, + TOGGLE_TEXTURE, + TOGGLE_COLLISION, + TOGGLE_GRID, NONE }; - enum class ActionType { - START, - END, + START, + END, NONE }; class Action { ActionName m_name{ ActionName::NONE }; ActionType m_type{ ActionType::NONE }; - Vec2f m_pos{}; // default to (0,0) + Vec2f m_pos{}; + + [[nodiscard]] static constexpr std::string_view nameString(ActionName name) noexcept { + switch (name) { + case ActionName::UP: return "UP"; + case ActionName::DOWN: return "DOWN"; + case ActionName::LEFT: return "LEFT"; + case ActionName::RIGHT: return "RIGHT"; + case ActionName::SELECT: return "SELECT"; + case ActionName::BACK: return "BACK"; + case ActionName::PAUSE: return "PAUSE"; + case ActionName::QUIT_AND_SAVE: return "QUIT_AND_SAVE"; + case ActionName::LEFT_CLICK: return "LEFT_CLICK"; + case ActionName::MIDDLE_CLICK: return "MIDDLE_CLICK"; + case ActionName::RIGHT_CLICK: return "RIGHT_CLICK"; + case ActionName::MOUSE_MOVE: return "MOUSE_MOVE"; + case ActionName::TOGGLE_FOLLOW: return "TOGGLE_FOLLOW"; + case ActionName::TOGGLE_TEXTURE: return "TOGGLE_TEXTURE"; + case ActionName::TOGGLE_COLLISION: return "TOGGLE_COLLISION"; + case ActionName::TOGGLE_GRID: return "TOGGLE_GRID"; + case ActionName::NONE: return "NONE"; + } + return "UNKNOWN"; + } + + [[nodiscard]] static constexpr std::string_view typeString(ActionType type) noexcept { + switch (type) { + case ActionType::START: return "START"; + case ActionType::END: return "END"; + case ActionType::NONE: return "NONE"; + } + return "UNKNOWN"; + } public: Action() = default; - /// Constructs an Action with a name, type, and position. Action(ActionName name, ActionType type, Vec2f pos) : m_name{ name }, m_type{ type }, m_pos{ pos } { } - /// Constructs an Action with name and type, position defaults to (0,0). Action(ActionName name, ActionType type) : Action{ name, type, Vec2f{} } { } - /// Constructs an Action with name and position, type defaults to NONE. Action(ActionName name, Vec2f pos) : Action{ name, ActionType::NONE, pos } { } - // Getters - [[nodiscard]] const ActionName name() const noexcept { return m_name; } - [[nodiscard]] const ActionType type() const noexcept { return m_type; } + [[nodiscard]] ActionName name() const noexcept { return m_name; } + [[nodiscard]] ActionType type() const noexcept { return m_type; } [[nodiscard]] const Vec2f& pos() const noexcept { return m_pos; } - // Convert to string for debugging/logging - [[nodiscard]] std::string toString() const noexcept { - static constexpr const char* NameStrings[] = { - "UP", "DOWN", "LEFT", "RIGHT", "SELECT", "BACK", "NONE", - "LEFT_CLICK","MIDDLE_CLICK","RIGHT_CLICK","MOUSE_MOVE", - }; - static constexpr const char* TypeStrings[] = { - "START", "END", "NONE" - }; - - return std::format("{} {} {} {}", - NameStrings[static_cast(m_name)], - TypeStrings[static_cast(m_type)], - static_cast(m_pos.x), - static_cast(m_pos.y)); + [[nodiscard]] std::string toString() const { + return std::string{nameString(m_name)} + " " + + std::string{typeString(m_type)} + " " + + std::to_string(static_cast(m_pos.x)) + " " + + std::to_string(static_cast(m_pos.y)); } -}; \ No newline at end of file +}; diff --git a/include/EcoSimEngine/ecs/Assets.hpp b/include/EcoSimEngine/ecs/Assets.hpp index 962a90b..376d232 100644 --- a/include/EcoSimEngine/ecs/Assets.hpp +++ b/include/EcoSimEngine/ecs/Assets.hpp @@ -1,36 +1,36 @@ #pragma once -#include +#include #include #include +#include +#include #include #include -#include - class Assets { - std::unordered_map m_textureMap; - std::unordered_map m_fontMap; - std::unordered_map m_fontPathMap; - std::unordered_map m_soundBuffers; - std::unordered_map> m_soundMap; // store sounds as unique_ptr + std::unordered_map m_textureMap; + std::unordered_map m_fontMap; + std::unordered_map m_fontPathMap; + std::unordered_map m_soundBuffers; + std::unordered_map> m_soundMap; - void addTexture(const std::string& name, const std::string& path); - void addFont(const std::string& name, const std::string& path); // for both sf::Font and std::string FontPath - void addSound(const std::string& name, const std::string& path); + void addTexture(const std::string& name, const std::string& path); + void addFont(const std::string& name, const std::string& path); + void addSound(const std::string& name, const std::string& path); public: Assets(); ~Assets(); - void loadFromFile(const std::string &path); + void loadFromFile(const std::string& path); - [[nodiscard]] const sf::Texture& getTexture(const std::string& name) const; - [[nodiscard]] const sf::Font& getFont(const std::string& name) const; - [[nodiscard]] const std::string& getFontPath(const std::string& name) const; - [[nodiscard]] sf::Sound& getSound(const std::string& name); + [[nodiscard]] const sf::Texture& getTexture(const std::string& name) const; + [[nodiscard]] const sf::Font& getFont(const std::string& name) const; + [[nodiscard]] const std::string& getFontPath(const std::string& name) const; + [[nodiscard]] sf::Sound& getSound(const std::string& name); [[nodiscard]] const std::unordered_map& getTextureMap() const; - [[nodiscard]] std::unordered_map>& getSoundMap(); + [[nodiscard]] std::unordered_map>& getSoundMap(); }; diff --git a/include/EcoSimEngine/GUI/GUIManager.hpp b/include/EcoSimEngine/gui/GUIManager.hpp similarity index 100% rename from include/EcoSimEngine/GUI/GUIManager.hpp rename to include/EcoSimEngine/gui/GUIManager.hpp diff --git a/include/EcoSimEngine/scene/Scene_Simulation.hpp b/include/EcoSimEngine/scene/Scene_Simulation.hpp index 6687b26..7832933 100644 --- a/include/EcoSimEngine/scene/Scene_Simulation.hpp +++ b/include/EcoSimEngine/scene/Scene_Simulation.hpp @@ -1,26 +1,24 @@ #pragma once -#include +#include +#include -#include "SFML/Graphics/Text.hpp" +#include +#include +#include #include "EcoSimEngine/component/Components.hpp" #include "EcoSimEngine/scene/Scene.hpp" -#include "EcoSimEngine/Utils/SpatialHash.hpp" - -#include +#include "EcoSimEngine/utils/SpatialHash.hpp" class Scene_Simulation : public Scene { private: std::string m_simKey; - const std::string m_defaultSimulationPath{ "resources/defaults/default_simulation.json" }; + const std::string m_defaultSimulationPath{ "resources/defaults/default_simulation.json" }; - // simple species -> color map for rendering std::unordered_map m_speciesColors; - - // spatial accel structure + timers - SpatialHash m_spatialHash{ 120.0f }; // default cell size (tweakable) - sf::Clock m_clock; // for timing updates + SpatialHash m_spatialHash{ 120.0f }; + sf::Clock m_clock; protected: bool m_drawTextures{ true }; @@ -28,30 +26,24 @@ class Scene_Simulation : public Scene { bool m_drawGrid{}; bool m_follow{}; - const Vec2f m_gridSize = { 64, 64 }; - //sf::Text m_gridText; - + const Vec2f m_gridSize{ 64.0f, 64.0f }; Vec2f m_mousePos; - // lifecycle void init(const std::string& simulationKey); - void loadSimulation(const std::string& simulationKey); // load a named/save simulation (TODO: implement file loading) - void loadDefaultSimulation(const std::string & defaultSimulationPath); // load default config - void spawnFromJson(const nlohmann::json& simJson); // shared spawn logic + void loadSimulation(const std::string& simulationKey); + void loadDefaultSimulation(const std::string& defaultSimulationPath); + void spawnFromJson(const nlohmann::json& simJson); - // scene callbacks void onEnd() override; void sDoAction(const Action& action) override; void sRender() override; public: - // simKey = empty -> create new simulation from default config - Scene_Simulation(SimulationEngine* simulationEngine, const std::string& simKey = {}); // Constructor takes in specific simulation name + explicit Scene_Simulation( + SimulationEngine* simulationEngine, + const std::string& simKey = {}); void update() override; - - // helpers std::string buildSavePathFromKey(const std::string& key); - - void onGui(); + void onGui() override; }; diff --git a/include/EcoSimEngine/system/SystemManager.hpp b/include/EcoSimEngine/system/SystemManager.hpp index 54f1d06..4cac98a 100644 --- a/include/EcoSimEngine/system/SystemManager.hpp +++ b/include/EcoSimEngine/system/SystemManager.hpp @@ -1,68 +1,76 @@ -// File: include/EcoSimEngine/system/SystemManager.hpp #pragma once -#include "EcoSimEngine/system/System.hpp" #include "EcoSimEngine/component/ComponentIndices.hpp" +#include "EcoSimEngine/system/System.hpp" +#include +#include #include #include -#include -#include +#include class SystemManager { - std::unordered_map m_signatures{}; - std::unordered_map> m_systems{}; + std::unordered_map m_signatures; + std::unordered_map> m_systems; + public: SystemManager() { - m_systems.reserve(16); // reserve a small number, TWEAK as needed - m_signatures.reserve(16); + m_systems.reserve(16); + m_signatures.reserve(16); } - - - - // Register a system and return a shared_ptr to it - template + template std::shared_ptr RegisterSystem(Args&&... args) { - std::type_index ti(typeid(T)); - assert(m_systems.find(ti) == m_systems.end() && "Registering system more than once."); + const std::type_index type(typeid(T)); + if (m_systems.contains(type)) { + throw std::logic_error("System type is already registered"); + } + auto system = std::make_shared(std::forward(args)...); - m_systems.emplace(ti, system); - return std::static_pointer_cast(system); + m_systems.emplace(type, system); + return system; } - // Set the signature (component bitset) for a system type - template + template void SetSignature(const Signature& signature) { - std::type_index ti(typeid(T)); - assert(m_systems.find(ti) != m_systems.end() && "SetSignature on unregistered system."); - m_signatures[ti] = signature; + const std::type_index type(typeid(T)); + if (!m_systems.contains(type)) { + throw std::logic_error("Cannot set a signature for an unregistered system"); + } + + m_signatures[type] = signature; } - // Remove entity from all systems when destroyed void EntityDestroyed(EntityId id) { for (auto& [_, system] : m_systems) { - if (system) system->mEntities.erase(id); + system->mEntities.erase(id); } } - // Called when an entity's signature changed (add/remove component) void EntitySignatureChanged(EntityId id, const Signature& entitySignature) { for (auto& [type, system] : m_systems) { - auto sIt = m_signatures.find(type); - if (sIt == m_signatures.end()) continue; // no signature set - const Signature& sSig = sIt->second; - if ((entitySignature & sSig) == sSig) system->mEntities.insert(id); - else system->mEntities.erase(id); + const auto signatureIt = m_signatures.find(type); + if (signatureIt == m_signatures.end()) { + continue; + } + + const Signature& systemSignature = signatureIt->second; + if ((entitySignature & systemSignature) == systemSignature) { + system->mEntities.insert(id); + } else { + system->mEntities.erase(id); + } } } - // Get the registered system of type T (or nullptr if none) - template + template std::shared_ptr GetSystem() { - std::type_index ti(typeid(T)); - auto it = m_systems.find(ti); - if (it == m_systems.end()) return nullptr; + const std::type_index type(typeid(T)); + const auto it = m_systems.find(type); + if (it == m_systems.end()) { + return nullptr; + } + return std::static_pointer_cast(it->second); } }; diff --git a/src/GUI/GUIManager.cpp b/src/gui/GUIManager.cpp similarity index 95% rename from src/GUI/GUIManager.cpp rename to src/gui/GUIManager.cpp index 315f9b2..4e58b30 100644 --- a/src/GUI/GUIManager.cpp +++ b/src/gui/GUIManager.cpp @@ -1,4 +1,4 @@ -#include "EcoSimEngine/GUI/GUIManager.hpp" +#include "EcoSimEngine/gui/GUIManager.hpp" #include "EcoSimEngine/SimulationEngine.hpp" #include "EcoSimEngine/event/Events.hpp" @@ -15,7 +15,7 @@ GUIManager::GUIManager(SimulationEngine* engine) } GUIManager::~GUIManager() { - // don't call shutdown() here we will call shutdown explicitly when engine stops. + // don't call shutdown() here � we will call shutdown explicitly when engine stops. } void GUIManager::init(sf::RenderWindow& window) { @@ -116,7 +116,7 @@ void GUIManager::loaddFonts() { } catch (const std::exception& e) { std::cerr << "GUIManager::loaddFonts error: " << e.what() - << " falling back to ImGui default font." << std::endl; + << " � falling back to ImGui default font." << std::endl; // Guarantee at least one font exists io.Fonts->Clear(); io.Fonts->AddFontDefault(); diff --git a/src/main.cpp b/src/main.cpp index e6db550..9864250 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,19 +1,20 @@ -// Project: Ecosystem Simulation -// SFML version 3.0.0 -// C++ version C++20 -// -// external libraries used: -// - SFML 3.0.0 -// - nlohmann/json 3.12.0 -// - ImGui-SFML 3.0 - -#include #include "EcoSimEngine/SimulationEngine.hpp" -#include "EcoSimEngine/math/Vec2.hpp" -int main(void) { - SimulationEngine sim("config/config.json"); - sim.run(); +#include +#include +#include + +int main() { + try { + SimulationEngine simulation("config/config.json"); + simulation.run(); + } catch (const std::exception& error) { + std::cerr << "EcoSimEngine failed: " << error.what() << '\n'; + return EXIT_FAILURE; + } catch (...) { + std::cerr << "EcoSimEngine failed with an unknown error\n"; + return EXIT_FAILURE; + } - return 0; -} \ No newline at end of file + return EXIT_SUCCESS; +} diff --git a/tests/foundation_tests.cpp b/tests/foundation_tests.cpp new file mode 100644 index 0000000..40df0db --- /dev/null +++ b/tests/foundation_tests.cpp @@ -0,0 +1,130 @@ +#include "EcoSimEngine/component/ComponentManager.hpp" +#include "EcoSimEngine/component/ComponentIndices.hpp" +#include "EcoSimEngine/event/EventBus.hpp" +#include "EcoSimEngine/system/System.hpp" +#include "EcoSimEngine/system/SystemManager.hpp" + +#include +#include + +namespace { + +struct Health { + int value; + explicit Health(int initialValue) : value(initialValue) {} +}; + +class TransformSystem final : public System {}; +class UnregisteredSystem final : public System {}; + +void require(bool condition, const char* message) { + if (!condition) { + throw std::runtime_error(message); + } +} + +void testComponentManager() { + ComponentManager components; + + require(!components.has(7), "new manager must not contain components"); + + auto& health = components.add(7, 125); + require(health.value == 125, "added component must keep constructor state"); + require(components.has(7), "added component must be discoverable"); + require(&components.get(7) == &health, "get must return stored component"); + + bool duplicateRejected = false; + try { + static_cast(components.add(7, 90)); + } catch (const std::logic_error&) { + duplicateRejected = true; + } + require(duplicateRejected, "duplicate component insertion must be rejected"); + + components.remove(7); + require(!components.has(7), "removed component must no longer exist"); + + bool missingGetRejected = false; + try { + static_cast(components.get(7)); + } catch (const std::out_of_range&) { + missingGetRejected = true; + } + require(missingGetRejected, "getting a missing component must fail explicitly"); +} + +void testSystemManager() { + SystemManager systems; + auto transformSystem = systems.RegisterSystem(); + + Signature required; + required.set(0); + systems.SetSignature(required); + + Signature entitySignature; + systems.EntitySignatureChanged(11, entitySignature); + require(!transformSystem->mEntities.contains(11), + "entity without required components must not join system"); + + entitySignature.set(0); + systems.EntitySignatureChanged(11, entitySignature); + require(transformSystem->mEntities.contains(11), + "matching entity must join system"); + + entitySignature.reset(0); + systems.EntitySignatureChanged(11, entitySignature); + require(!transformSystem->mEntities.contains(11), + "entity must leave system after signature stops matching"); + + bool duplicateRejected = false; + try { + static_cast(systems.RegisterSystem()); + } catch (const std::logic_error&) { + duplicateRejected = true; + } + require(duplicateRejected, "duplicate system registration must fail in release builds too"); + + bool unregisteredSignatureRejected = false; + try { + systems.SetSignature(required); + } catch (const std::logic_error&) { + unregisteredSignatureRejected = true; + } + require(unregisteredSignatureRejected, + "setting a signature for an unregistered system must fail explicitly"); +} + +void testEventBus() { + EventBus bus; + int total = 0; + + const auto first = bus.subscribe([&](const int& value) { + total += value; + }); + bus.subscribe([&](const int& value) { + total += value * 10; + }); + + bus.publish(2); + require(total == 22, "publish must notify all subscribers"); + + bus.unsubscribe(first); + bus.publish(1); + require(total == 32, "unsubscribe must remove only the selected subscription"); +} + +} // namespace + +int main() { + try { + testComponentManager(); + testSystemManager(); + testEventBus(); + } catch (const std::exception& error) { + std::cerr << "foundation test failure: " << error.what() << '\n'; + return 1; + } + + std::cout << "foundation tests passed\n"; + return 0; +}