diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ec1cfad --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,216 @@ +# Frostbite2D Agent Guide + +This guide helps agentic coding agents work effectively with the Frostbite2D 2D game engine codebase. + +## Build Commands + +### Basic Build +```bash +xmake build +``` + +### Clean Build +```bash +xmake clean +xmake build +``` + +### Debug Build +```bash +xmake build -m debug +``` + +### Release Build +```bash +xmake build -m release +``` + +### Platform-Specific Build +```bash +xmake build -p windows +xmake build -p linux +xmake build -p switch +``` + +### Configuration +```bash +xmake f -c # Show current configuration +xmake f # Interactive configuration +``` + +## Testing +Currently no automated test framework is configured. Manual testing is performed by running the built binary. + +## Project Structure +``` +Frostbite2D/ +├── include/frostbite2D/ # Public headers +│ ├── core/ # Core engine classes +│ ├── types/ # Type definitions +│ ├── utils/ # Utility classes +│ └── platform/ # Platform-specific code +└── src/frostbite2D/ # Implementation files +``` + +## Code Style Guidelines + +### Naming Conventions +- **Classes/Structs**: PascalCase (`Application`, `Window`, `Vec2`) +- **Methods/Functions**: camelCase (`readTextFile`, `getFileName`) +- **Member Variables**: camelCase with trailing underscore (`width_`, `height_`, `running_`) +- **Constants**: UPPER_CASE (`PI_F`, `DEG_TO_RAD`) or PascalCase static methods (`Vec2::Zero()`) +- **Namespaces**: lowercase (`frostbite2D`, `math`) +- **Template Parameters**: PascalCase (`T`, `Args`, `Sig`) +- **Type Aliases**: using keyword with PascalCase (`using int8 = std::int8_t;`) + +### Imports and Includes +- **Engine headers**: Use path relative to frostbite2D root + ```cpp + #include + #include + ``` +- **Third-party libraries**: Use vendor prefixes + ```cpp + #include + #include + #include + ``` +- **Standard library**: Use angle brackets + ```cpp + #include + #include + #include + ``` + +### Formatting +- **Indentation**: 2 spaces +- **Brace Style**: K&R (opening brace on same line) + ```cpp + void myFunction() { + // body + } + ``` +- **Pointer/Reference Style**: Type on left, not centered + ```cpp + void func(T* ptr); // Good + void func(T *ptr); // Avoid + void func(T& ref); // Good + ``` +- **Const Placement**: After type + ```cpp + const std::string& str; // Good + std::string const& str; // Avoid + ``` + +### Types +- **Use custom type aliases**: `int8`, `int32`, `uint8`, `uint64`, `Ptr`, `SharedPtr` +- **Smart pointers**: Prefer `Ptr` alias over `std::shared_ptr` +- **constexpr**: Use for compile-time constants +- **Struct vs Class**: + - Use `struct` for POD types, configurations, and data-only types + - Use `class` for objects with methods/encapsulation +- **Default member initialization**: Initialize at declaration + ```cpp + bool running_ = false; + int width_ = 1280; + ``` + +### Error Handling +- **Return bool for success/failure operations** (especially I/O) +- **Use SDL_Log/SDL_LogError for logging** +- **Use std::optional for optional returns** +- **Avoid exceptions** (not observed in codebase) +- **Return early on failure**: Check conditions and return false immediately + +### Singletons +- Use static instance pattern with private constructor: + ```cpp + static Application& get(); + private: + Application() = default; + ``` + +### Const Correctness +- Mark methods `const` if they don't modify state +- Pass by const reference for large types: `const std::string&` +- Return const references to member data when appropriate + +### Documentation +- **Doxygen-style comments** with `@brief` tag +- **Use `///` for single-line comments in headers** +- **Section separators** use `// ---------------------------------------------------------------------------` +- **Chinese comments** are present and should be preserved/maintained + ```cpp + /** + * @brief 读取文本文件 + * @param path 文件路径 + * @param outContent 输出文件内容 + * @return 读取成功返回 true + */ + bool readTextFile(const std::string& path, std::string& outContent); + ``` + +### Platform Handling +- Use `#ifdef` guards for platform-specific code: + ```cpp + #ifdef __SWITCH__ + switchInit(); + #endif + ``` +- Supported platforms: Windows, Linux, macOS, Switch +- Platform configs in `platform/` directory + +### STL Usage +- Prefer STL algorithms over manual loops +- Use `std::filesystem` for file operations (already wrapped in Asset class) +- Use `std::optional` for nullable returns +- Use `std::function` for callbacks +- Use `std::vector` for dynamic arrays +- Use `std::string` for text + +### OpenGL/Graphics +- GLM library for math operations +- Glad for OpenGL function loading +- Keep OpenGL-specific code minimal and well-encapsulated + +## Dependencies +- **SDL2**: Windowing, input, platform abstraction +- **GLM**: Math library (vectors, matrices) +- **Glad**: OpenGL loader +- **STB**: Image/text utilities (single-header libs) + +## Key Patterns + +### Module Pattern +```cpp +void Application::use(Module& m) { + modules_.push_back(&m); +} +``` + +### Configuration Pattern +Use configuration structs with default values: +```cpp +struct WindowConfig { + uint32_t width = 640; + uint32_t height = 480; + std::string title = "Frostbite2D Game"; +}; +``` + +### Path Handling +Always use the Asset singleton for file operations - it handles platform-specific path issues: +```cpp +Asset& asset = Asset::get(); +asset.setWorkingDirectory("path/to/assets"); +std::string content; +if (asset.readTextFile("file.txt", content)) { + // success +} +``` + +## Common Issues +- **Path separators**: Use Asset class, it handles platform differences +- **Encoding**: Project uses UTF-8, Asset handles Windows UTF-8 paths +- **Memory leaks**: Use smart pointers, ensure proper cleanup in destructors +- **OpenGL context**: Initialize only after window creation \ No newline at end of file diff --git a/Frostbite2D/include/frostbite2D/core/application.h b/Frostbite2D/include/frostbite2D/core/application.h index 9688cc7..a4ce912 100644 --- a/Frostbite2D/include/frostbite2D/core/application.h +++ b/Frostbite2D/include/frostbite2D/core/application.h @@ -73,13 +73,6 @@ public: */ bool init(const AppConfig& config); - /** - * @brief 使用配置文件初始化 - * @param configPath 配置文件路径 - * @return 初始化成功返回 true - */ - bool init(const std::string& configPath); - /** * @brief 关闭应用程序 */ @@ -117,54 +110,6 @@ public: */ bool isRunning() const { return running_; } - // /** - // * @brief 获取窗口 - // * @return 窗口引用 - // */ - // IWindow& window() { return *window_; } - - // /** - // * @brief 获取渲染器 - // * @return 渲染器引用 - // */ - // RenderBackend& renderer(); - - // /** - // * @brief 获取场景服务 - // * @return 场景服务共享指针 - // */ - // SharedPtr scenes(); - - // /** - // * @brief 获取计时器服务 - // * @return 计时器服务共享指针 - // */ - // SharedPtr timers(); - - // /** - // * @brief 获取事件服务 - // * @return 事件服务共享指针 - // */ - // SharedPtr events(); - - // /** - // * @brief 获取相机服务 - // * @return 相机服务共享指针 - // */ - // SharedPtr camera(); - - // /** - // * @brief 进入场景 - // * @param scene 场景指针 - // */ - // void enterScene(Ptr scene); - - /** - * @brief 获取帧间隔时间 - * @return 帧间隔时间(秒) - */ - float deltaTime() const { return deltaTime_; } - /** * @brief 获取总运行时间 * @return 总运行时间(秒) @@ -183,6 +128,18 @@ public: */ const AppConfig& getConfig() const; + /** + * @brief 获取窗口 + * @return 窗口指针 + */ + Window* getWindow() const { return window_; } + + /** + * @brief 获取 delta time + * @return 帧间隔时间(秒) + */ + float deltaTime() const { return deltaTime_; } + private: Application() = default; ~Application(); @@ -225,6 +182,7 @@ private: std::vector modules_; Window* window_ = nullptr; + AppConfig config_; bool initialized_ = false; bool running_ = false; @@ -237,6 +195,7 @@ private: int frameCount_ = 0; float fpsTimer_ = 0.0f; int currentFps_ = 0; + const float fpsUpdateInterval_ = 1.0f; }; diff --git a/Frostbite2D/src/frostbite2D/core/application.cpp b/Frostbite2D/src/frostbite2D/core/application.cpp index e6d17da..cffde98 100644 --- a/Frostbite2D/src/frostbite2D/core/application.cpp +++ b/Frostbite2D/src/frostbite2D/core/application.cpp @@ -1,4 +1,5 @@ #include +#include #include #include namespace frostbite2D { @@ -35,6 +36,8 @@ bool Application::init(const AppConfig &config) { return true; } + config_ = config; + // 平台相关初始化 #ifdef __SWITCH__ switchInit(); @@ -47,13 +50,25 @@ bool Application::init(const AppConfig &config) { return false; } + if (!initCoreModules()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize core modules"); + shutdown(); + return false; + } + + initialized_ = true; return true; } void Application::shutdown() { if (!initialized_) return; - + + running_ = false; + shouldQuit_ = true; + + destroyAllModules(); + if (window_) { window_->destroy(); window_ = nullptr; @@ -69,8 +84,162 @@ void Application::shutdown() { } Application::~Application() { - if (initialized_) { - shutdown(); + shutdown(); +} + +bool Application::initCoreModules() { + registerCoreServices(); + return true; +} + +void Application::registerCoreServices() { + SDL_Log("Registering core services..."); +} + +void Application::setupAllModules() { + SDL_Log("Setting up modules (%zu)...", modules_.size()); + + for (auto* module : modules_) { + if (module && !module->isInitialized()) { + module->setupModule(); + module->setInitialized(true); + SDL_Log("Module '%s' initialized", module->getName()); + } } } + +void Application::destroyAllModules() { + SDL_Log("Destroying modules (%zu)...", modules_.size()); + + for (auto* module : modules_) { + if (module && module->isInitialized()) { + module->destroyModule(); + module->setInitialized(false); + } + } +} + +void Application::run() { + if (!initialized_) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Application not initialized!"); + return; + } + + if (running_) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Application already running!"); + return; + } + + SDL_Log("Starting application..."); + running_ = true; + shouldQuit_ = false; + paused_ = false; + + setupAllModules(); + + lastFrameTime_ = SDL_GetPerformanceCounter() / static_cast(SDL_GetPerformanceFrequency()); + totalTime_ = 0.0f; + deltaTime_ = 0.0f; + frameCount_ = 0; + fpsTimer_ = 0.0f; + currentFps_ = 0; + + mainLoop(); + + destroyAllModules(); + running_ = false; + SDL_Log("Application stopped"); +} + +void Application::quit() { + SDL_Log("Quit requested"); + shouldQuit_ = true; +} + +void Application::pause() { + if (!paused_) { + SDL_Log("Application paused"); + paused_ = true; + } +} + +void Application::resume() { + if (paused_) { + SDL_Log("Application resumed"); + paused_ = false; + lastFrameTime_ = SDL_GetPerformanceCounter() / static_cast(SDL_GetPerformanceFrequency()); + } +} + +void Application::mainLoop() { + while (!shouldQuit_) { + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + shouldQuit_ = true; + break; + } + for (auto* module : modules_) { + if (module && module->isInitialized()) { + module->handleEvent(); + } + } + } + + if (!paused_) { + update(); + } + + render(); + } +} + +void Application::update() { + double currentTime = SDL_GetPerformanceCounter() / static_cast(SDL_GetPerformanceFrequency()); + deltaTime_ = static_cast(currentTime - lastFrameTime_); + lastFrameTime_ = currentTime; + totalTime_ += deltaTime_; + + for (auto* module : modules_) { + if (module && module->isInitialized()) { + module->onUpdate(); + } + } + + frameCount_++; + fpsTimer_ += deltaTime_; + if (fpsTimer_ >= fpsUpdateInterval_) { + currentFps_ = static_cast(frameCount_ / fpsTimer_); + frameCount_ = 0; + fpsTimer_ = 0.0f; + } +} + +void Application::render() { + for (auto* module : modules_) { + if (module && module->isInitialized()) { + module->beforeRender(); + } + } + + for (auto* module : modules_) { + if (module && module->isInitialized()) { + module->onRender(); + } + } + + for (auto* module : modules_) { + if (module && module->isInitialized()) { + module->afterRender(); + } + } + + if (window_) { + window_->swap(); + } +} + +const AppConfig& Application::getConfig() const { + return config_; +} } // namespace frostbite2D \ No newline at end of file diff --git a/Frostbite2D/src/frostbite2D/core/window.cpp b/Frostbite2D/src/frostbite2D/core/window.cpp index 5258b45..4617c03 100644 --- a/Frostbite2D/src/frostbite2D/core/window.cpp +++ b/Frostbite2D/src/frostbite2D/core/window.cpp @@ -1,6 +1,7 @@ #include "SDL_log.h" #include #include +#include #include @@ -84,6 +85,25 @@ bool Window::create(const WindowConfig &cfg) { fullscreen_ = (flags & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0; vsync_ = cfg.vsync; +#ifndef __SWITCH__ + if (!cfg.icon.file_path.empty()) { + Asset& asset = Asset::get(); + std::string resolvedPath = asset.resolveAssetPath(cfg.icon.file_path); + SDL_Surface* icon = SDL_LoadBMP(resolvedPath.c_str()); + if (icon) { + SDL_SetWindowIcon(sdlWindow_, icon); + SDL_FreeSurface(icon); + } else { + SDL_Log("Failed to load window icon: %s", resolvedPath.c_str()); + } + } +#if defined(_WIN32) + if (cfg.icon.resource_id != 0) { + SDL_Log("Loading icon from resource ID: %u", cfg.icon.resource_id); + } +#endif +#endif + return true; } diff --git a/Frostbite2D/src/main.cpp b/Frostbite2D/src/main.cpp index c727d9e..208625f 100644 --- a/Frostbite2D/src/main.cpp +++ b/Frostbite2D/src/main.cpp @@ -2,40 +2,76 @@ #include #include #include +#include #include #include using namespace frostbite2D; -int main(int argc, char **argv) { +class TestModule : public Module { +public: + const char* getName() const override { return "TestModule"; } + void setupModule() override { + SDL_Log("TestModule: setupModule()"); + } + + void onUpdate() override { + static int frameCount = 0; + frameCount++; + + if (frameCount % 120 == 0) { + SDL_Log("TestModule: onUpdate() - Frame %d, FPS: %d", frameCount, Application::get().fps()); + } + + if (frameCount > 600) { + SDL_Log("TestModule: Requesting quit after 600 frames"); + Application::get().quit(); + } + } + + void beforeRender() override { + glClearColor(0.2f, 0.3f, 0.8f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + } + + void onRender() override { + } + + void afterRender() override { + } + + void destroyModule() override { + SDL_Log("TestModule: destroyModule()"); + } +}; + +int main(int argc, char **argv) { AppConfig config = AppConfig::createDefault(); - config.appName = "Frostbite2D Render Test"; + config.appName = "Frostbite2D Test App"; config.appVersion = "1.0.0"; config.windowConfig.width = 800; config.windowConfig.height = 600; - config.windowConfig.title = "Frostbite2D - OpenGL Render Test"; + config.windowConfig.title = "Frostbite2D - Application Test"; Application &app = Application::get(); + + TestModule testModule; + app.use(testModule); + if (!app.init(config)) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, - "Failed to initialize application!"); + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize application!"); return -1; } - Asset &asset = Asset::get(); - // asset.setWorkingDirectory("I:/DOF/骑士团"); - asset.setWorkingDirectory("/switch/testgame"); - std::string content; - if (asset.readTextFile("test.txt", content)) { - SDL_Log("test.txt content: %s", content.c_str()); - } else { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to read test.txt!"); - } + SDL_Log("Application initialized successfully"); + SDL_Log("Running main loop..."); + + app.run(); app.shutdown(); - SDL_Log("程序正常退出"); + SDL_Log("Application exited normally"); return 0; -} +} \ No newline at end of file diff --git a/assets/icons/README.md b/assets/icons/README.md new file mode 100644 index 0000000..133e8c3 --- /dev/null +++ b/assets/icons/README.md @@ -0,0 +1,46 @@ +# Icon Setup Guide + +## Window Icon (运行时窗口图标) + +### Windows/Linux +- Place `icon.bmp` (32x32 or larger) in `assets/icons/` +- Set in code: `config.windowConfig.icon = Icon("assets/icons/icon.bmp");` + +### Switch +- Place `icon.jpg` (256x256 JPEG) in `assets/icons/icon.jpg` +- Automatically included in NRO build process + +## Application Icon (程序文件图标) + +### Windows (.exe 图标) +需要创建 `.ico` 文件并放置在 `assets/icons/app.ico` + +#### 创建 .ico 文件的方法: + +**方法 1: 使用在线工具** +1. 访问 https://icoconvert.com/ +2. 上传 PNG/JPG 图标(推荐 256x256) +3.将下载的 .ico 文件保存到 `assets/icons/app.ico` + +**方法 2: 使用 GIMP** +1. 打开图像 +2. 调整为 256x256 +3. 文件 → 导出为 → `app.ico` +4. 保存到 `assets/icons/app.ico` + +**方法 3: 使用 ImageMagick** +```bash +convert icon.png -define icon:auto-resize=256,128,96,64,48,32,16 app.ico +``` + +#### Linux (.desktop 图标) +- 将图标复制到 `/usr/share/icons/hicolor/256x256/apps/yourapp.png` +- 创建 `.desktop` 文件指定 Icon 字段 + +## Usage Example +```cpp +AppConfig config = AppConfig::createDefault(); +config.windowConfig.icon = Icon("assets/icons/icon.bmp"); +Application& app = Application::get(); +app.init(config); +``` \ No newline at end of file diff --git a/assets/icons/app.ico b/assets/icons/app.ico new file mode 100644 index 0000000..50a3bc7 Binary files /dev/null and b/assets/icons/app.ico differ diff --git a/assets/icons/icon.bmp b/assets/icons/icon.bmp new file mode 100644 index 0000000..8b3ed6f Binary files /dev/null and b/assets/icons/icon.bmp differ diff --git a/platform/linux.lua b/platform/linux.lua index 9a189cf..53dba30 100644 --- a/platform/linux.lua +++ b/platform/linux.lua @@ -32,37 +32,20 @@ target("Frostbite2D") end end - -- 复制 SDL2 DLL - local sdl2_lib = target:pkg("libsdl2") - if sdl2_lib then - local libfiles = sdl2_lib:get("libfiles") - if libfiles then - for _, libfile in ipairs(libfiles) do - -- 查找 DLL 文件 - if libfile:endswith(".dll") then - local target_dll = path.join(output_dir, path.filename(libfile)) - os.cp(libfile, target_dll) - print("Copy DLL: " .. path.filename(libfile)) - end - end - end + -- 复制图标文件到输出目录 + local icons_dir = path.join(os.projectdir(), "assets/icons") + local target_icons_dir = path.join(output_dir, "assets/icons") + + if os.isdir(icons_dir) then + if not os.isdir(target_icons_dir) then + os.mkdir(target_icons_dir) end - - -- 尝试从 xmake 包目录复制 SDL2.dll - local sdl2_dll_paths = { - path.join(os.getenv("USERPROFILE") or "", ".xmake/packages/l/libsdl2/**/bin/SDL2.dll"), - path.join(os.getenv("USERPROFILE") or "", ".xmake/packages/l/libsdl2/**/lib/SDL2.dll"), - } - - for _, dll_pattern in ipairs(sdl2_dll_paths) do - local dll_files = os.files(dll_pattern) - for _, dll_file in ipairs(dll_files) do - local target_dll = path.join(output_dir, "SDL2.dll") - if not os.isfile(target_dll) then - os.cp(dll_file, target_dll) - print("Copy SDL2.dll from: " .. dll_file) - end - end + for _, file in ipairs(os.files(path.join(icons_dir, "*.*"))) do + local filename = path.filename(file) + local target_file = path.join(target_icons_dir, filename) + os.cp(file, target_file) + print("Copy icon: " .. filename) end + end end) target_end() diff --git a/platform/switch.lua b/platform/switch.lua index 10d9cd1..83d1d85 100644 --- a/platform/switch.lua +++ b/platform/switch.lua @@ -50,14 +50,16 @@ target("Frostbite2D") local nro_file = path.join(output_dir, "hello_world.nro") local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe") local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe") + local icon_file = path.join(os.projectdir(), "assets/icons/icon.bmp") if os.isfile(nacptool) and os.isfile(elf2nro) then - os.vrunv(nacptool, {"--create", "Hello World", "Extra2D Team", "1.0.0", nacp_file}) + os.vrunv(nacptool, {"--create", "Hello World", "Frostbite2D Team", "1.0.0", nacp_file}) local romfs = path.join(example_dir, "romfs") + local icon_param = os.isfile(icon_file) and ("--icon=" .. icon_file) or "" if os.isdir(romfs) then - os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs}) + os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, icon_param, "--romfsdir=" .. romfs}) else - os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file}) + os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, icon_param}) end print("Generated NRO: " .. nro_file) end diff --git a/platform/windows.lua b/platform/windows.lua index 058bd37..f0f51af 100644 --- a/platform/windows.lua +++ b/platform/windows.lua @@ -4,12 +4,33 @@ set_toolchains("mingw") add_requires("libsdl2", {configs = {shared = true}}) add_requires("glm") +-- 创建自定义规则来编译 .rc 文件 +rule("windres_compile") + set_extensions(".rc") + on_build(function (target) + local windres = "windres" + local rc_file = target:sourcefiles()[1] + local obj_file = target:targetfile() + local arch = is_arch("x86_64") and "pe-x86-64" or "pe-i386" + + os.execv(windres, {"-i", rc_file, "-o", obj_file, "-O", "coff", "-F", arch}) + end) +rule_end() + target("Frostbite2D") set_kind("binary") add_files(path.join(os.projectdir(), "Frostbite2D/src/**.cpp")) add_files(path.join(os.projectdir(), "Frostbite2D/src/**.c")) add_includedirs(path.join(os.projectdir(), "Frostbite2D/include")) + -- 添加资源文件(如果存在) + local rc_file = path.join(os.projectdir(), "resources/app.rc") + local ico_file = path.join(os.projectdir(), "assets/icons/app.ico") + if os.isfile(rc_file) and os.isfile(ico_file) then + -- 添加 .rc 文件并应用自定义规则 + add_files(rc_file, {rule = "windres_compile"}) + end + add_packages("libsdl2") add_packages("glm") @@ -34,6 +55,22 @@ target("Frostbite2D") end end + -- 复制图标文件到输出目录 + local icons_dir = path.join(os.projectdir(), "assets/icons") + local target_icons_dir = path.join(output_dir, "assets/icons") + + if os.isdir(icons_dir) then + if not os.isdir(target_icons_dir) then + os.mkdir(target_icons_dir) + end + for _, file in ipairs(os.files(path.join(icons_dir, "*.*"))) do + local filename = path.filename(file) + local target_file = path.join(target_icons_dir, filename) + os.cp(file, target_file) + print("Copy icon: " .. filename) + end + end + -- 复制 SDL2 DLL local sdl2_lib = target:pkg("libsdl2") if sdl2_lib then @@ -67,4 +104,4 @@ target("Frostbite2D") end end end) -target_end() +target_end() \ No newline at end of file diff --git a/resources/app.rc b/resources/app.rc new file mode 100644 index 0000000..1eba335 --- /dev/null +++ b/resources/app.rc @@ -0,0 +1,3 @@ +#include + +1 ICON "assets/icons/app.ico" \ No newline at end of file