diff --git a/docs/EMSCRIPTEN_RESEARCH.md b/docs/EMSCRIPTEN_RESEARCH.md new file mode 100644 index 0000000..f0d7721 --- /dev/null +++ b/docs/EMSCRIPTEN_RESEARCH.md @@ -0,0 +1,708 @@ +# McRogueFace Emscripten & Renderer Abstraction Research + +**Date**: 2026-01-30 +**Branch**: `emscripten-mcrogueface` +**Related Issues**: #157 (True Headless), #158 (Emscripten/WASM) + +## Executive Summary + +This document analyzes the technical requirements for: +1. **SFML 2.6 → 3.0 migration** (modernization) +2. **Emscripten/WebAssembly compilation** (browser deployment) + +Both goals share a common prerequisite: **renderer abstraction**. The codebase already has a partial abstraction via `sf::RenderTarget*` pointer, but SFML types are pervasive (1276 occurrences across 78 files). + +**Key Insight**: This is a **build-time configuration**, not runtime switching. The standard McRogueFace binary remains a dynamic environment; Emscripten builds bundle assets and scripts at compile time. + +--- + +## Current Architecture Analysis + +### Existing Abstraction Strengths + +1. **RenderTarget Pointer Pattern** (`GameEngine.h:156`) + ```cpp + sf::RenderTarget* render_target; + // Points to either window.get() or headless_renderer->getRenderTarget() + ``` + This already decouples rendering logic from the specific backend. + +2. **HeadlessRenderer** (`src/HeadlessRenderer.h`) + - Uses `sf::RenderTexture` internally + - Provides unified interface: `getRenderTarget()`, `display()`, `saveScreenshot()` + - Demonstrates the pattern for additional backends + +3. **UIDrawable Hierarchy** + - Virtual `render(sf::Vector2f, sf::RenderTarget&)` method + - 7 drawable types: Frame, Caption, Sprite, Entity, Grid, Line, Circle, Arc + - Each manages its own SFML primitives internally + +4. **Asset Wrappers** + - `PyTexture`, `PyFont`, `PyShader` wrap SFML types + - Python reference counting integrated + - Single point of change for asset loading APIs + +### Current SFML Coupling Points + +| Area | Count | Difficulty | Notes | +|------|-------|------------|-------| +| `sf::Vector2f` | ~200+ | Medium | Used everywhere for positions, sizes | +| `sf::Color` | ~100+ | Easy | Simple 4-byte struct replacement | +| `sf::FloatRect` | ~50+ | Medium | Bounds, intersection testing | +| `sf::RenderTexture` | ~20 | Hard | Shader effects, caching | +| `sf::Sprite/Text` | ~30 | Hard | Core rendering primitives | +| `sf::Event` | ~15 | Medium | Input system coupling | +| `sf::Keyboard/Mouse` | ~50+ | Easy | Enum mappings | + +Total: **1276 occurrences across 78 files** + +--- + +## SFML 3.0 Migration Analysis + +### Breaking Changes Requiring Code Updates + +#### 1. Vector Parameters (High Impact) +```cpp +// SFML 2.6 +setPosition(10, 20); +sf::VideoMode(1024, 768, 32); +sf::FloatRect(x, y, w, h); + +// SFML 3.0 +setPosition({10, 20}); +sf::VideoMode({1024, 768}, 32); +sf::FloatRect({x, y}, {w, h}); +``` + +**Strategy**: Regex-based search/replace with manual verification. + +#### 2. Rect Member Changes (Medium Impact) +```cpp +// SFML 2.6 +rect.left, rect.top, rect.width, rect.height +rect.getPosition(), rect.getSize() + +// SFML 3.0 +rect.position.x, rect.position.y, rect.size.x, rect.size.y +rect.position, rect.size // direct access +rect.findIntersection() -> std::optional> +``` + +#### 3. Resource Constructors (Low Impact) +```cpp +// SFML 2.6 +sf::Sound sound; // default constructible +sound.setBuffer(buffer); + +// SFML 3.0 +sf::Sound sound(buffer); // requires buffer at construction +``` + +#### 4. Keyboard/Mouse Enum Scoping (Medium Impact) +```cpp +// SFML 2.6 +sf::Keyboard::A +sf::Mouse::Left + +// SFML 3.0 +sf::Keyboard::Key::A +sf::Mouse::Button::Left +``` + +#### 5. Event Handling (Medium Impact) +```cpp +// SFML 2.6 +sf::Event event; +while (window.pollEvent(event)) { + if (event.type == sf::Event::Closed) ... +} + +// SFML 3.0 +while (auto event = window.pollEvent()) { + if (event->is()) ... +} +``` + +#### 6. CMake Target Changes +```cmake +# SFML 2.6 +find_package(SFML 2 REQUIRED COMPONENTS graphics audio) +target_link_libraries(app sfml-graphics sfml-audio) + +# SFML 3.0 +find_package(SFML 3 REQUIRED COMPONENTS Graphics Audio) +target_link_libraries(app SFML::Graphics SFML::Audio) +``` + +### Migration Effort Estimate + +| Phase | Files | Changes | Effort | +|-------|-------|---------|--------| +| CMakeLists.txt | 1 | Target names | 1 hour | +| Vector parameters | 30+ | ~200 calls | 4-8 hours | +| Rect refactoring | 20+ | ~50 usages | 2-4 hours | +| Event handling | 5 | ~15 sites | 2 hours | +| Keyboard/Mouse | 10 | ~50 enums | 2 hours | +| Resource constructors | 10 | ~30 sites | 2 hours | +| **Total** | - | - | **~15-25 hours** | + +--- + +## Emscripten/VRSFML Analysis + +### Why VRSFML Over Waiting for SFML 4.x? + +1. **Available Now**: VRSFML is working today with browser demos +2. **Modern OpenGL**: Removes legacy calls, targets OpenGL ES 3.0+ (WebGL 2) +3. **SFML_GAME_LOOP Macro**: Handles blocking vs callback loop abstraction +4. **Performance**: 500k sprites @ 60FPS vs 3 FPS upstream (batching) +5. **SFML 4.x Timeline**: Unknown, potentially years away + +### VRSFML API Differences from SFML + +| Feature | SFML 2.6/3.0 | VRSFML | +|---------|--------------|--------| +| Default constructors | Allowed | Not allowed for resources | +| Texture ownership | Pointer in Sprite | Passed at draw time | +| Context management | Hidden global | Explicit `GraphicsContext` | +| Drawable base class | Polymorphic | Removed | +| Loading methods | `loadFromFile()` returns bool | Returns `std::optional` | +| Main loop | `while(running)` | `SFML_GAME_LOOP { }` | + +### Main Loop Refactoring + +Current blocking loop: +```cpp +void GameEngine::run() { + while (running) { + processEvents(); + update(); + render(); + display(); + } +} +``` + +Emscripten-compatible pattern: +```cpp +// Option A: VRSFML macro +SFML_GAME_LOOP { + processEvents(); + update(); + render(); + display(); +} + +// Option B: Manual Emscripten integration +#ifdef __EMSCRIPTEN__ +void mainLoopCallback() { + if (!game.running) { + emscripten_cancel_main_loop(); + return; + } + game.doFrame(); +} +emscripten_set_main_loop(mainLoopCallback, 0, 1); +#else +while (running) { doFrame(); } +#endif +``` + +**Recommendation**: Use preprocessor-based approach with `doFrame()` extraction for cleaner separation. + +--- + +## Build-Time Configuration Strategy + +### Normal Build (Desktop) +- Dynamic loading of assets from `assets/` directory +- Python scripts loaded from `scripts/` directory at runtime +- Full McRogueFace environment with dynamic game loading + +### Emscripten Build (Web) +- Assets bundled via `--preload-file assets` +- Scripts bundled via `--preload-file scripts` +- Virtual filesystem (MEMFS/IDBFS) +- Optional: Script linting with Pyodide before bundling +- Single-purpose deployment (one game per build) + +### CMake Configuration +```cmake +option(MCRF_BUILD_EMSCRIPTEN "Build for Emscripten/WebAssembly" OFF) + +if(MCRF_BUILD_EMSCRIPTEN) + set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/cmake/toolchains/emscripten.cmake) + add_definitions(-DMCRF_EMSCRIPTEN) + + # Bundle assets + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} \ + --preload-file ${CMAKE_SOURCE_DIR}/assets@/assets \ + --preload-file ${CMAKE_SOURCE_DIR}/scripts@/scripts") +endif() +``` + +--- + +## Phased Implementation Plan + +### Phase 0: Preparation (This PR) +- [ ] Create `docs/EMSCRIPTEN_RESEARCH.md` (this document) +- [ ] Update Gitea issues #157, #158 with findings +- [ ] Identify specific files requiring changes +- [ ] Create test matrix for rendering features + +### Phase 1: Type Abstraction Layer +**Goal**: Isolate SFML types behind McRogueFace wrappers + +```cpp +// src/types/McrfTypes.h +namespace mcrf { + using Vector2f = sf::Vector2f; // Alias initially, replace later + using Color = sf::Color; + using FloatRect = sf::FloatRect; +} +``` + +Changes: +- [ ] Create `src/types/` directory with wrapper types +- [ ] Gradually replace `sf::` with `mcrf::` namespace +- [ ] Update Common.h to provide both namespaces during transition + +### Phase 2: Main Loop Extraction +**Goal**: Make game loop callback-compatible + +- [ ] Extract `GameEngine::doFrame()` from `run()` +- [ ] Add `#ifdef __EMSCRIPTEN__` conditional in `run()` +- [ ] Test that desktop behavior is unchanged + +### Phase 3: Render Backend Interface +**Goal**: Abstract RenderTarget operations + +```cpp +class RenderBackend { +public: + virtual ~RenderBackend() = default; + virtual void clear(const Color& color) = 0; + virtual void draw(const Sprite& sprite) = 0; + virtual void draw(const Text& text) = 0; + virtual void display() = 0; + virtual bool isOpen() const = 0; + virtual Vector2u getSize() const = 0; +}; + +class SFMLBackend : public RenderBackend { ... }; +class VRSFMLBackend : public RenderBackend { ... }; // Future +``` + +### Phase 4: SFML 3.0 Migration +**Goal**: Update to SFML 3.0 API + +- [ ] Update CMakeLists.txt targets +- [ ] Fix vector parameter calls +- [ ] Fix rect member access +- [ ] Fix event handling +- [ ] Fix keyboard/mouse enums +- [ ] Test thoroughly + +### Phase 5: VRSFML Integration (Experimental) +**Goal**: Add VRSFML as alternative backend + +- [ ] Add VRSFML as submodule/dependency +- [ ] Implement VRSFMLBackend +- [ ] Add Emscripten CMake configuration +- [ ] Test in browser + +### Phase 6: Python-in-WASM +**Goal**: Get Python scripting working in browser + +**High Risk** - This is the major unknown: +- [ ] Build CPython for Emscripten +- [ ] Test `McRFPy_API` binding compatibility +- [ ] Evaluate Pyodide vs raw CPython +- [ ] Handle filesystem virtualization +- [ ] Test threading limitations + +--- + +## Risk Assessment + +| Risk | Probability | Impact | Mitigation | +|------|-------------|--------|------------| +| SFML 3.0 breaks unexpected code | Medium | Medium | Comprehensive test suite | +| VRSFML API too different | Low | High | Can fork/patch VRSFML | +| Python-in-WASM fails | Medium | Critical | Evaluate Pyodide early | +| Performance regression | Low | Medium | Benchmark before/after | +| Binary size too large | Medium | Medium | Lazy loading, stdlib trimming | + +--- + +## References + +### SFML 3.0 +- [Migration Guide](https://www.sfml-dev.org/tutorials/3.0/getting-started/migrate/) +- [Changelog](https://www.sfml-dev.org/development/changelog/) +- [Release Notes](https://github.com/SFML/SFML/releases/tag/3.0.0) + +### VRSFML/Emscripten +- [VRSFML Blog Post](https://vittorioromeo.com/index/blog/vrsfml.html) +- [VRSFML GitHub](https://github.com/vittorioromeo/VRSFML) +- [Browser Demos](https://vittorioromeo.github.io/VRSFML_HTML5_Examples/) + +### Python WASM +- [PEP 776 - Python Emscripten Support](https://peps.python.org/pep-0776/) +- [CPython WASM Build Guide](https://github.com/python/cpython/blob/main/Tools/wasm/README.md) +- [Pyodide](https://github.com/pyodide/pyodide) + +### Related Issues +- [SFML Emscripten Discussion #1494](https://github.com/SFML/SFML/issues/1494) +- [libtcod Emscripten #41](https://github.com/libtcod/libtcod/issues/41) + +--- + +## Appendix A: File-by-File SFML Usage Inventory + +### Critical Files (Must Abstract for Emscripten) + +| File | SFML Types Used | Role | Abstraction Difficulty | +|------|-----------------|------|------------------------| +| `GameEngine.h/cpp` | RenderWindow, Clock, Font, Event | Main loop, window | **CRITICAL** | +| `HeadlessRenderer.h/cpp` | RenderTexture | Headless backend | **CRITICAL** | +| `UIDrawable.h/cpp` | Vector2f, RenderTarget, FloatRect | Base render interface | **HARD** | +| `UIFrame.h/cpp` | RectangleShape, Vector2f, Color | Container rendering | **HARD** | +| `UISprite.h/cpp` | Sprite, Texture, Vector2f | Texture display | **HARD** | +| `UICaption.h/cpp` | Text, Font, Vector2f, Color | Text rendering | **HARD** | +| `UIGrid.h/cpp` | RenderTexture, Sprite, Vector2f | Tile grid system | **HARD** | +| `UIEntity.h/cpp` | Sprite, Vector2f | Game entities | **HARD** | +| `UICircle.h/cpp` | CircleShape, Vector2f, Color | Circle shape | **MEDIUM** | +| `UILine.h/cpp` | VertexArray, Vector2f, Color | Line rendering | **MEDIUM** | +| `UIArc.h/cpp` | CircleShape segments, Vector2f | Arc shape | **MEDIUM** | +| `Scene.h/cpp` | Vector2f, RenderTarget | Scene management | **MEDIUM** | +| `SceneTransition.h/cpp` | RenderTexture, Sprite | Transitions | **MEDIUM** | + +### Wrapper Files (Already Partially Abstracted) + +| File | SFML Types Wrapped | Python API | Notes | +|------|-------------------|------------|-------| +| `PyVector.h/cpp` | sf::Vector2f | Vector | Ready for backend swap | +| `PyColor.h/cpp` | sf::Color | Color | Ready for backend swap | +| `PyTexture.h/cpp` | sf::Texture | Texture | Asset loading needs work | +| `PyFont.h/cpp` | sf::Font | Font | Asset loading needs work | +| `PyShader.h/cpp` | sf::Shader | Shader | Optional feature | + +### Input System Files + +| File | SFML Types Used | Notes | +|------|-----------------|-------| +| `ActionCode.h` | Keyboard::Key, Mouse::Button | Enum encoding only | +| `PyKey.h/cpp` | Keyboard::Key enum | 140+ key mappings | +| `PyMouseButton.h/cpp` | Mouse::Button enum | Simple enum | +| `PyKeyboard.h/cpp` | Keyboard::isKeyPressed | State queries | +| `PyMouse.h/cpp` | Mouse::getPosition | Position queries | +| `PyInputState.h/cpp` | None (pure enum) | No SFML dependency | + +### Support Files (Low Priority) + +| File | SFML Types Used | Notes | +|------|-----------------|-------| +| `Animation.h/cpp` | Vector2f, Color (as values) | Pure data animation | +| `GridLayers.h/cpp` | RenderTexture, Color | Layer caching | +| `IndexTexture.h/cpp` | Texture, IntRect | Legacy texture format | +| `Resources.h/cpp` | Font | Global font storage | +| `ProfilerOverlay.cpp` | Text, RectangleShape | Debug overlay | +| `McRFPy_Automation.h/cpp` | Various | Testing only | + +--- + +## Appendix B: Recommended First Steps + +### Immediate (Non-Breaking Changes) + +1. **Extract `GameEngine::doFrame()`** + - Move loop body to separate method + - No API changes, just internal refactoring + - Enables future Emscripten callback integration + +2. **Create type aliases in Common.h** + ```cpp + namespace mcrf { + using Vector2f = sf::Vector2f; + using Vector2i = sf::Vector2i; + using Color = sf::Color; + using FloatRect = sf::FloatRect; + } + ``` + - Allows gradual migration from `sf::` to `mcrf::` + - No functional changes + +3. **Document current render path** + - Add comments to key rendering functions + - Identify all `target.draw()` call sites + - Create rendering flow diagram + +### Short-Term (Preparation for SFML 3.0) + +1. **Audit vector parameter calls** + - Find all `setPosition(x, y)` style calls + - Prepare regex patterns for migration + +2. **Audit rect member access** + - Find all `.left`, `.top`, `.width`, `.height` uses + - Prepare for `.position.x`, `.size.x` style + +3. **Test suite expansion** + - Add rendering validation tests + - Screenshot comparison tests + - Animation correctness tests + +--- + +## Appendix C: libtcod Architecture Analysis + +**Key Finding**: libtcod uses a much simpler abstraction pattern than initially proposed. + +### libtcod's Context Vtable Pattern + +libtcod doesn't wrap every SDL type. Instead, it abstracts at the **context level** using a C-style vtable: + +```c +struct TCOD_Context { + int type; + void* contextdata_; // Backend-specific data (opaque pointer) + + // Function pointers - the "vtable" + void (*c_destructor_)(struct TCOD_Context* self); + TCOD_Error (*c_present_)(struct TCOD_Context* self, + const TCOD_Console* console, + const TCOD_ViewportOptions* viewport); + void (*c_pixel_to_tile_)(struct TCOD_Context* self, double* x, double* y); + TCOD_Error (*c_save_screenshot_)(struct TCOD_Context* self, const char* filename); + struct SDL_Window* (*c_get_sdl_window_)(struct TCOD_Context* self); + TCOD_Error (*c_set_tileset_)(struct TCOD_Context* self, TCOD_Tileset* tileset); + TCOD_Error (*c_screen_capture_)(struct TCOD_Context* self, ...); + // ... more operations +}; +``` + +### How Backends Implement It + +Each renderer fills in the function pointers: + +```c +// In renderer_sdl2.c +context->c_destructor_ = sdl2_destructor; +context->c_present_ = sdl2_present; +context->c_get_sdl_window_ = sdl2_get_window; +// ... + +// In renderer_xterm.c +context->c_destructor_ = xterm_destructor; +context->c_present_ = xterm_present; +// ... +``` + +### Conditional Compilation with NO_SDL + +libtcod uses simple preprocessor guards: + +```c +// In CMakeLists.txt +if(LIBTCOD_SDL3) + target_link_libraries(${PROJECT_NAME} PUBLIC SDL3::SDL3) +else() + target_compile_definitions(${PROJECT_NAME} PUBLIC NO_SDL) +endif() + +// In source files +#ifndef NO_SDL +#include +// ... SDL-dependent code ... +#endif +``` + +**47 files** use this pattern. When building headless, SDL code is simply excluded. + +### Why This Pattern Works + +1. **Core functionality is SDL-independent**: Console manipulation, pathfinding, FOV, noise, BSP, etc. don't need SDL +2. **Only rendering needs abstraction**: The `TCOD_Context` is the single point of abstraction +3. **Minimal API surface**: Just ~10 function pointers instead of wrapping every primitive +4. **Backend-specific data is opaque**: `contextdata_` holds renderer-specific state + +### Implications for McRogueFace + +**libtcod's approach suggests we should NOT try to abstract every `sf::` type.** + +Instead, consider: + +1. **Keep SFML types internally** - `sf::Vector2f`, `sf::Color`, `sf::FloatRect` are fine +2. **Abstract at the RenderContext level** - One vtable for window/rendering operations +3. **Use `#ifndef NO_SFML` guards** - Compile-time backend selection +4. **Create alternative backend for Emscripten** - WebGL + canvas implementation + +### Proposed McRogueFace Context Pattern + +```cpp +struct McRF_RenderContext { + void* backend_data; // SFML or WebGL specific data + + // Function pointers + void (*destroy)(McRF_RenderContext* self); + void (*clear)(McRF_RenderContext* self, uint32_t color); + void (*present)(McRF_RenderContext* self); + void (*draw_sprite)(McRF_RenderContext* self, const Sprite* sprite); + void (*draw_text)(McRF_RenderContext* self, const Text* text); + void (*draw_rect)(McRF_RenderContext* self, const Rect* rect); + bool (*poll_event)(McRF_RenderContext* self, Event* event); + void (*screenshot)(McRF_RenderContext* self, const char* path); + // ... +}; + +// SFML backend +McRF_RenderContext* mcrf_sfml_context_new(int width, int height, const char* title); + +// Emscripten backend (future) +McRF_RenderContext* mcrf_webgl_context_new(const char* canvas_id); +``` + +### Comparison: Original Plan vs libtcod-Inspired Plan + +| Aspect | Original Plan | libtcod-Inspired Plan | +|--------|---------------|----------------------| +| Type abstraction | Replace all `sf::*` with `mcrf::*` | Keep `sf::*` internally | +| Abstraction point | Every primitive type | Single Context object | +| Files affected | 78+ files | ~10 core files | +| Compile-time switching | Complex namespace aliasing | Simple `#ifndef NO_SFML` | +| Backend complexity | Full reimplementation | Focused vtable | + +**Recommendation**: Adopt libtcod's simpler pattern. Focus abstraction on the rendering context, not on data types. + +--- + +## Appendix D: Headless Build Experiment Results + +**Experiment Date**: 2026-01-30 +**Branch**: `emscripten-mcrogueface` + +### Objective + +Attempt to compile McRogueFace without SFML dependencies to identify true coupling points. + +### What We Created + +1. **`src/platform/HeadlessTypes.h`** - Complete SFML type stubs (~600 lines): + - Vector2f, Vector2i, Vector2u + - Color with standard color constants + - FloatRect, IntRect + - Time, Clock (with chrono-based implementation) + - Transform, Vertex, View + - Shape hierarchy (RectangleShape, CircleShape, etc.) + - Texture, Sprite, Font, Text stubs + - RenderTarget, RenderTexture, RenderWindow stubs + - Audio stubs (Sound, Music, SoundBuffer) + - Input stubs (Keyboard, Mouse, Event) + - Shader stub + +2. **Modified `src/Common.h`** - Conditional include: + ```cpp + #ifdef MCRF_HEADLESS + #include "platform/HeadlessTypes.h" + #else + #include + #include + #endif + ``` + +### Build Attempt Result + +**SUCCESS** - Headless build compiles after consolidating includes and adding stubs. + +### Work Completed + +#### 1. Consolidated SFML Includes + +**15 files** had direct SFML includes that bypassed Common.h. All were modified to use `#include "Common.h"` instead: + +| File | Original Include | Fixed | +|------|------------------|-------| +| `main.cpp` | `` | ✓ | +| `Animation.h` | `` | ✓ | +| `GridChunk.h` | `` | ✓ | +| `GridLayers.h` | `` | ✓ | +| `HeadlessRenderer.h` | `` | ✓ | +| `SceneTransition.h` | `` | ✓ | +| `McRFPy_Automation.h` | ``, `` | ✓ | +| `PyWindow.cpp` | `` | ✓ | +| `ActionCode.h` | `` | ✓ | +| `PyKey.h` | `` | ✓ | +| `PyMouseButton.h` | `` | ✓ | +| `PyBSP.h` | `` | ✓ | +| `UIGridPathfinding.h` | `` | ✓ | + +#### 2. Wrapped ImGui-SFML with Guards + +ImGui-SFML is disabled entirely in headless builds since debug tools can't be accessed through the API: + +| File | Changes | +|------|---------| +| `GameEngine.h` | Guarded includes and member variables | +| `GameEngine.cpp` | Guarded all ImGui::SFML calls | +| `ImGuiConsole.h/cpp` | Entire file wrapped with `#ifndef MCRF_HEADLESS` | +| `ImGuiSceneExplorer.h/cpp` | Entire file wrapped with `#ifndef MCRF_HEADLESS` | +| `McRFPy_API.cpp` | Guarded ImGuiConsole include and setEnabled call | + +#### 3. Extended HeadlessTypes.h + +The stub file grew from ~700 lines to ~900 lines with additional types and methods: + +**Types Added:** +- `sf::Image` - For screenshot functionality +- `sf::Glsl::Vec3`, `sf::Glsl::Vec4` - For shader uniforms +- `sf::BlendMode` - For rendering states +- `sf::CurrentTextureType` - For shader texture binding + +**Methods Added:** +- `Font::Info` struct and `Font::getInfo()` +- `Texture::update()` overloads +- `Texture::copyToImage()` +- `Transform::getInverse()` +- `RenderStates` constructors from Transform, BlendMode, Shader* +- `Music::getDuration()`, `getPlayingOffset()`, `setPlayingOffset()` +- `SoundBuffer::getDuration()` +- `RenderWindow::setMouseCursorGrabbed()` +- `sf::err()` stream function +- Keyboard aliases: `BackSpace`, `BackSlash`, `SemiColon`, `Dash` + +### Build Commands + +```bash +# Normal SFML build (default) +make + +# Headless build (no SFML dependency) +mkdir build-headless && cd build-headless +cmake .. -DCMAKE_CXX_FLAGS="-DMCRF_HEADLESS" -DCMAKE_BUILD_TYPE=Debug +make +``` + +### Key Insight + +The libtcod approach of `#ifndef NO_SDL` guards works when **all platform includes go through a single point**. The consolidation of 15+ bypass points into Common.h was the prerequisite that made this work. + +### Actual Effort + +| Task | Files | Time | +|------|-------|------| +| Replace direct SFML includes with Common.h | 15 | ~30 min | +| Wrap ImGui-SFML in guards | 5 | ~20 min | +| Extend HeadlessTypes.h with missing stubs | 1 | ~1 hour | +| Fix compilation errors iteratively | - | ~1 hour | + +**Total**: ~3 hours for clean headless compilation + +### Next Steps + +1. **Test Python bindings** - Ensure mcrfpy module loads in headless mode +2. **Add CMake option** - `option(MCRF_HEADLESS "Build without graphics" OFF)` +3. **Link-time validation** - Verify no SFML symbols are referenced +4. **Emscripten testing** - Try building with emcc diff --git a/src/ActionCode.h b/src/ActionCode.h index 1adaf99..22fd197 100644 --- a/src/ActionCode.h +++ b/src/ActionCode.h @@ -1,4 +1,4 @@ -#include +#include "Common.h" class ActionCode { diff --git a/src/Animation.h b/src/Animation.h index d364e91..5a65479 100644 --- a/src/Animation.h +++ b/src/Animation.h @@ -5,7 +5,7 @@ #include #include #include -#include +#include "Common.h" #include "Python.h" // Forward declarations diff --git a/src/Common.h b/src/Common.h index f3c3b34..2827d94 100644 --- a/src/Common.h +++ b/src/Common.h @@ -1,6 +1,24 @@ # pragma once -#include -#include + +// ============================================================================= +// Platform Selection +// ============================================================================= +// Define MCRF_HEADLESS to build without SFML graphics/audio dependencies. +// This enables headless operation for servers, CI, and Emscripten builds. +// +// Build with: cmake -DMCRF_HEADLESS=ON .. +// ============================================================================= + +#ifdef MCRF_HEADLESS + // Use headless type stubs instead of SFML + #include "platform/HeadlessTypes.h" + #define MCRF_GRAPHICS_BACKEND "headless" +#else + // Use SFML for graphics and audio + #include + #include + #define MCRF_GRAPHICS_BACKEND "sfml" +#endif // Maximum dimension for grids, layers, and heightmaps (8192x8192 = 256MB of float data) // Prevents integer overflow in size calculations and limits memory allocation diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index ae63247..c114dc5 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -7,8 +7,10 @@ #include "Animation.h" #include "Timer.h" #include "BenchmarkLogger.h" +#ifndef MCRF_HEADLESS #include "imgui.h" #include "imgui-SFML.h" +#endif #include #include @@ -84,6 +86,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg) window->setFramerateLimit(60); render_target = window.get(); +#ifndef MCRF_HEADLESS // Initialize ImGui for the window if (ImGui::SFML::Init(*window)) { imguiInitialized = true; @@ -92,6 +95,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg) // Load JetBrains Mono for crisp console text (will be overridden by .ini if present) ImGuiConsole::reloadFont(16.0f); } +#endif } visible = render_target->getDefaultView(); @@ -195,10 +199,12 @@ void GameEngine::cleanup() } // Shutdown ImGui AFTER window is closed to avoid X11 BadCursor errors +#ifndef MCRF_HEADLESS if (imguiInitialized) { ImGui::SFML::Shutdown(); imguiInitialized = false; } +#endif } Scene* GameEngine::currentScene() { return scenes[scene]; } @@ -318,10 +324,12 @@ void GameEngine::run() if (!headless) { sUserInput(); +#ifndef MCRF_HEADLESS // Update ImGui if (imguiInitialized) { ImGui::SFML::Update(*window, clock.getElapsedTime()); } +#endif } if (!paused) { @@ -360,12 +368,14 @@ void GameEngine::run() profilerOverlay->render(*render_target); } +#ifndef MCRF_HEADLESS // Render ImGui overlays (console and scene explorer) if (imguiInitialized && !headless) { console.render(); sceneExplorer.render(*this); ImGui::SFML::Render(*window); } +#endif // Record work time before display (which may block for vsync/framerate limit) metrics.workTime = clock.getElapsedTime().asSeconds() * 1000.0f; @@ -554,6 +564,7 @@ void GameEngine::sUserInput() sf::Event event; while (window && window->pollEvent(event)) { +#ifndef MCRF_HEADLESS // Process event through ImGui first if (imguiInitialized) { ImGui::SFML::ProcessEvent(*window, event); @@ -579,6 +590,7 @@ void GameEngine::sUserInput() continue; } } +#endif processEvent(event); } diff --git a/src/GameEngine.h b/src/GameEngine.h index 9e69a24..533c222 100644 --- a/src/GameEngine.h +++ b/src/GameEngine.h @@ -10,8 +10,10 @@ #include "HeadlessRenderer.h" #include "SceneTransition.h" #include "Profiler.h" +#ifndef MCRF_HEADLESS #include "ImGuiConsole.h" #include "ImGuiSceneExplorer.h" +#endif #include #include #include @@ -194,10 +196,12 @@ private: int overlayUpdateCounter = 0; // Only update overlay every N frames ProfilerOverlay* profilerOverlay = nullptr; // The actual overlay renderer +#ifndef MCRF_HEADLESS // ImGui console overlay ImGuiConsole console; ImGuiSceneExplorer sceneExplorer; bool imguiInitialized = false; +#endif // #219 - Thread synchronization for background Python threads FrameLock frameLock; diff --git a/src/GridChunk.h b/src/GridChunk.h index 133b786..28ed1dd 100644 --- a/src/GridChunk.h +++ b/src/GridChunk.h @@ -1,6 +1,5 @@ #pragma once #include "Common.h" -#include #include #include #include "UIGridPoint.h" diff --git a/src/GridLayers.h b/src/GridLayers.h index c2c6fbf..45cc10f 100644 --- a/src/GridLayers.h +++ b/src/GridLayers.h @@ -2,7 +2,6 @@ #include "Common.h" #include "Python.h" #include "structmember.h" -#include #include #include #include diff --git a/src/HeadlessRenderer.h b/src/HeadlessRenderer.h index 2b08291..bc40722 100644 --- a/src/HeadlessRenderer.h +++ b/src/HeadlessRenderer.h @@ -1,7 +1,7 @@ #ifndef HEADLESS_RENDERER_H #define HEADLESS_RENDERER_H -#include +#include "Common.h" #include #include diff --git a/src/ImGuiConsole.cpp b/src/ImGuiConsole.cpp index d0363bf..6f5f149 100644 --- a/src/ImGuiConsole.cpp +++ b/src/ImGuiConsole.cpp @@ -1,3 +1,8 @@ +// ImGuiConsole.cpp - Debug console using ImGui +// This file is excluded from headless builds (no GUI/debug interface needed) + +#ifndef MCRF_HEADLESS + #include "ImGuiConsole.h" #include "imgui.h" #include "imgui_internal.h" // For ImGuiSettingsHandler, ImHashStr, MarkIniSettingsDirty @@ -445,3 +450,5 @@ void ImGuiConsole::renderCodeEditor() { ImGui::End(); } + +#endif // MCRF_HEADLESS diff --git a/src/ImGuiConsole.h b/src/ImGuiConsole.h index 32a9451..e705936 100644 --- a/src/ImGuiConsole.h +++ b/src/ImGuiConsole.h @@ -1,5 +1,8 @@ #pragma once +// ImGuiConsole - excluded from headless builds (no GUI/debug interface) +#ifndef MCRF_HEADLESS + #include #include #include @@ -71,3 +74,5 @@ private: // Scroll state bool scrollToBottom = true; }; + +#endif // MCRF_HEADLESS diff --git a/src/ImGuiSceneExplorer.cpp b/src/ImGuiSceneExplorer.cpp index f12d1d9..0eb7c03 100644 --- a/src/ImGuiSceneExplorer.cpp +++ b/src/ImGuiSceneExplorer.cpp @@ -1,3 +1,8 @@ +// ImGuiSceneExplorer.cpp - Debug scene hierarchy explorer using ImGui +// This file is excluded from headless builds (no GUI/debug interface needed) + +#ifndef MCRF_HEADLESS + #include "ImGuiSceneExplorer.h" #include "imgui.h" #include "GameEngine.h" @@ -283,3 +288,5 @@ const char* ImGuiSceneExplorer::getTypeName(UIDrawable* drawable) { default: return "Unknown"; } } + +#endif // MCRF_HEADLESS diff --git a/src/ImGuiSceneExplorer.h b/src/ImGuiSceneExplorer.h index d41163a..7293ed3 100644 --- a/src/ImGuiSceneExplorer.h +++ b/src/ImGuiSceneExplorer.h @@ -1,5 +1,8 @@ #pragma once +// ImGuiSceneExplorer - excluded from headless builds (no GUI/debug interface) +#ifndef MCRF_HEADLESS + #include #include @@ -44,3 +47,5 @@ private: // Get type name string const char* getTypeName(UIDrawable* drawable); }; + +#endif // MCRF_HEADLESS diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 8514689..e4974aa 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -32,7 +32,9 @@ #include "PyUniformCollection.h" // Shader uniform collection (#106) #include "McRogueFaceVersion.h" #include "GameEngine.h" +#ifndef MCRF_HEADLESS #include "ImGuiConsole.h" +#endif #include "BenchmarkLogger.h" #include "UI.h" #include "UILine.h" @@ -1552,7 +1554,9 @@ PyObject* McRFPy_API::_setDevConsole(PyObject* self, PyObject* args) { return NULL; } +#ifndef MCRF_HEADLESS ImGuiConsole::setEnabled(enabled); +#endif Py_RETURN_NONE; } diff --git a/src/McRFPy_Automation.h b/src/McRFPy_Automation.h index a090fc3..6d67012 100644 --- a/src/McRFPy_Automation.h +++ b/src/McRFPy_Automation.h @@ -1,8 +1,6 @@ #pragma once #include "Common.h" #include "Python.h" -#include -#include #include #include #include diff --git a/src/PyBSP.h b/src/PyBSP.h index 3c1c2b1..b345331 100644 --- a/src/PyBSP.h +++ b/src/PyBSP.h @@ -6,7 +6,6 @@ #include #include #include -#include // Forward declarations class PyBSP; diff --git a/src/PyKey.h b/src/PyKey.h index 354b36e..a77b960 100644 --- a/src/PyKey.h +++ b/src/PyKey.h @@ -1,7 +1,6 @@ #pragma once #include "Common.h" #include "Python.h" -#include // Module-level Key enum class (created at runtime using Python's IntEnum) // Stored as a module attribute: mcrfpy.Key diff --git a/src/PyMouseButton.h b/src/PyMouseButton.h index 743a9b8..9eb2019 100644 --- a/src/PyMouseButton.h +++ b/src/PyMouseButton.h @@ -1,7 +1,6 @@ #pragma once #include "Common.h" #include "Python.h" -#include // Module-level MouseButton enum class (created at runtime using Python's IntEnum) // Stored as a module attribute: mcrfpy.MouseButton diff --git a/src/PyWindow.cpp b/src/PyWindow.cpp index 17fb1ba..e4f9684 100644 --- a/src/PyWindow.cpp +++ b/src/PyWindow.cpp @@ -2,7 +2,7 @@ #include "GameEngine.h" #include "McRFPy_API.h" #include "McRFPy_Doc.h" -#include +#include "Common.h" #include // Singleton instance - static variable, not a class member diff --git a/src/SceneTransition.h b/src/SceneTransition.h index 7103323..28fb76a 100644 --- a/src/SceneTransition.h +++ b/src/SceneTransition.h @@ -1,6 +1,5 @@ #pragma once #include "Common.h" -#include #include #include diff --git a/src/UIGridPathfinding.h b/src/UIGridPathfinding.h index 2ca88b4..89610c8 100644 --- a/src/UIGridPathfinding.h +++ b/src/UIGridPathfinding.h @@ -3,7 +3,6 @@ #include "Python.h" #include "UIBase.h" // For PyUIGridObject typedef #include -#include #include #include #include diff --git a/src/main.cpp b/src/main.cpp index b9f95f2..a20a013 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,4 @@ -#include +#include "Common.h" #include "GameEngine.h" #include "CommandLineParser.h" #include "McRogueFaceConfig.h" diff --git a/src/platform/HeadlessTypes.h b/src/platform/HeadlessTypes.h new file mode 100644 index 0000000..733d43a --- /dev/null +++ b/src/platform/HeadlessTypes.h @@ -0,0 +1,917 @@ +// HeadlessTypes.h - SFML type stubs for headless/no-graphics builds +// This file provides minimal type definitions that allow McRogueFace +// to compile without linking against SFML. +// +// Part of the Emscripten research branch (emscripten-mcrogueface) + +#pragma once + +#include +#include +#include +#include +#include + +namespace sf { + +// Forward declarations (needed for RenderWindow) +struct Event; +class Keyboard; +class Mouse; + +// ============================================================================= +// Type Aliases (SFML compatibility) +// ============================================================================= + +using Uint8 = uint8_t; +using Uint16 = uint16_t; +using Uint32 = uint32_t; +using Uint64 = uint64_t; +using Int8 = int8_t; +using Int16 = int16_t; +using Int32 = int32_t; +using Int64 = int64_t; + +// ============================================================================= +// Vector Types +// ============================================================================= + +template +struct Vector2 { + T x = 0; + T y = 0; + + Vector2() = default; + Vector2(T x_, T y_) : x(x_), y(y_) {} + + template + explicit Vector2(const Vector2& other) : x(static_cast(other.x)), y(static_cast(other.y)) {} + + Vector2 operator+(const Vector2& rhs) const { return Vector2(x + rhs.x, y + rhs.y); } + Vector2 operator-(const Vector2& rhs) const { return Vector2(x - rhs.x, y - rhs.y); } + Vector2 operator*(T scalar) const { return Vector2(x * scalar, y * scalar); } + Vector2 operator/(T scalar) const { return Vector2(x / scalar, y / scalar); } + Vector2& operator+=(const Vector2& rhs) { x += rhs.x; y += rhs.y; return *this; } + Vector2& operator-=(const Vector2& rhs) { x -= rhs.x; y -= rhs.y; return *this; } + Vector2& operator*=(T scalar) { x *= scalar; y *= scalar; return *this; } + Vector2& operator/=(T scalar) { x /= scalar; y /= scalar; return *this; } + bool operator==(const Vector2& rhs) const { return x == rhs.x && y == rhs.y; } + bool operator!=(const Vector2& rhs) const { return !(*this == rhs); } + Vector2 operator-() const { return Vector2(-x, -y); } +}; + +using Vector2f = Vector2; +using Vector2i = Vector2; +using Vector2u = Vector2; + +template +Vector2 operator*(T scalar, const Vector2& vec) { return vec * scalar; } + +// ============================================================================= +// Color Type +// ============================================================================= + +struct Color { + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + uint8_t a = 255; + + Color() = default; + Color(uint8_t r_, uint8_t g_, uint8_t b_, uint8_t a_ = 255) : r(r_), g(g_), b(b_), a(a_) {} + + bool operator==(const Color& rhs) const { return r == rhs.r && g == rhs.g && b == rhs.b && a == rhs.a; } + bool operator!=(const Color& rhs) const { return !(*this == rhs); } + + // Standard colors + static const Color Black; + static const Color White; + static const Color Red; + static const Color Green; + static const Color Blue; + static const Color Yellow; + static const Color Magenta; + static const Color Cyan; + static const Color Transparent; +}; + +// Static color definitions (need to be in a .cpp file for real builds) +inline const Color Color::Black(0, 0, 0); +inline const Color Color::White(255, 255, 255); +inline const Color Color::Red(255, 0, 0); +inline const Color Color::Green(0, 255, 0); +inline const Color Color::Blue(0, 0, 255); +inline const Color Color::Yellow(255, 255, 0); +inline const Color Color::Magenta(255, 0, 255); +inline const Color Color::Cyan(0, 255, 255); +inline const Color Color::Transparent(0, 0, 0, 0); + +// ============================================================================= +// Rectangle Types +// ============================================================================= + +template +struct Rect { + T left = 0; + T top = 0; + T width = 0; + T height = 0; + + Rect() = default; + Rect(T left_, T top_, T width_, T height_) : left(left_), top(top_), width(width_), height(height_) {} + Rect(const Vector2& position, const Vector2& size) + : left(position.x), top(position.y), width(size.x), height(size.y) {} + + bool contains(T x, T y) const { + return x >= left && x < left + width && y >= top && y < top + height; + } + bool contains(const Vector2& point) const { return contains(point.x, point.y); } + + bool intersects(const Rect& other) const { + return left < other.left + other.width && left + width > other.left && + top < other.top + other.height && top + height > other.top; + } + + Vector2 getPosition() const { return Vector2(left, top); } + Vector2 getSize() const { return Vector2(width, height); } +}; + +using FloatRect = Rect; +using IntRect = Rect; + +// ============================================================================= +// Time Types +// ============================================================================= + +class Time { + int64_t microseconds_ = 0; +public: + Time() = default; + float asSeconds() const { return microseconds_ / 1000000.0f; } + int32_t asMilliseconds() const { return static_cast(microseconds_ / 1000); } + int64_t asMicroseconds() const { return microseconds_; } + + static Time Zero; + + friend Time seconds(float amount); + friend Time milliseconds(int32_t amount); + friend Time microseconds(int64_t amount); +}; + +inline Time Time::Zero; + +inline Time seconds(float amount) { Time t; t.microseconds_ = static_cast(amount * 1000000); return t; } +inline Time milliseconds(int32_t amount) { Time t; t.microseconds_ = amount * 1000; return t; } +inline Time microseconds(int64_t amount) { Time t; t.microseconds_ = amount; return t; } + +class Clock { + int64_t start_time_ = 0; + + static int64_t now_microseconds() { + // Use C++11 chrono for portable timing + auto now = std::chrono::high_resolution_clock::now(); + auto duration = now.time_since_epoch(); + return std::chrono::duration_cast(duration).count(); + } +public: + Clock() : start_time_(now_microseconds()) {} + + Time getElapsedTime() const { + return microseconds(now_microseconds() - start_time_); + } + + Time restart() { + int64_t now = now_microseconds(); + int64_t elapsed = now - start_time_; + start_time_ = now; + return microseconds(elapsed); + } +}; + +// ============================================================================= +// Transform (minimal stub) +// ============================================================================= + +class Transform { +public: + Transform() = default; + Transform& translate(float x, float y) { return *this; } + Transform& translate(const Vector2f& offset) { return translate(offset.x, offset.y); } + Transform& rotate(float angle) { return *this; } + Transform& rotate(float angle, const Vector2f& center) { return *this; } + Transform& scale(float factorX, float factorY) { return *this; } + Transform& scale(const Vector2f& factors) { return scale(factors.x, factors.y); } + + Vector2f transformPoint(float x, float y) const { return Vector2f(x, y); } + Vector2f transformPoint(const Vector2f& point) const { return point; } + FloatRect transformRect(const FloatRect& rect) const { return rect; } + + Transform getInverse() const { return Transform(); } + + Transform operator*(const Transform& rhs) const { return Transform(); } + Vector2f operator*(const Vector2f& point) const { return point; } + + static const Transform Identity; +}; + +inline const Transform Transform::Identity; + +// ============================================================================= +// Vertex (for custom geometry) +// ============================================================================= + +struct Vertex { + Vector2f position; + Color color; + Vector2f texCoords; + + Vertex() = default; + Vertex(const Vector2f& pos) : position(pos), color(Color::White) {} + Vertex(const Vector2f& pos, const Color& col) : position(pos), color(col) {} + Vertex(const Vector2f& pos, const Vector2f& tex) : position(pos), color(Color::White), texCoords(tex) {} + Vertex(const Vector2f& pos, const Color& col, const Vector2f& tex) : position(pos), color(col), texCoords(tex) {} +}; + +// ============================================================================= +// View (camera) +// ============================================================================= + +class View { + Vector2f center_; + Vector2f size_; + float rotation_ = 0.0f; + FloatRect viewport_{0, 0, 1, 1}; +public: + View() : center_(0, 0), size_(1000, 1000) {} + View(const FloatRect& rect) : center_(rect.left + rect.width/2, rect.top + rect.height/2), size_(rect.width, rect.height) {} + View(const Vector2f& center, const Vector2f& size) : center_(center), size_(size) {} + + void setCenter(float x, float y) { center_ = Vector2f(x, y); } + void setCenter(const Vector2f& center) { center_ = center; } + void setSize(float width, float height) { size_ = Vector2f(width, height); } + void setSize(const Vector2f& size) { size_ = size; } + void setRotation(float angle) { rotation_ = angle; } + void setViewport(const FloatRect& viewport) { viewport_ = viewport; } + + const Vector2f& getCenter() const { return center_; } + const Vector2f& getSize() const { return size_; } + float getRotation() const { return rotation_; } + const FloatRect& getViewport() const { return viewport_; } + + void move(float offsetX, float offsetY) { center_.x += offsetX; center_.y += offsetY; } + void move(const Vector2f& offset) { center_ += offset; } + void rotate(float angle) { rotation_ += angle; } + void zoom(float factor) { size_ *= factor; } + + Transform getTransform() const { return Transform::Identity; } + Transform getInverseTransform() const { return Transform::Identity; } +}; + +// ============================================================================= +// Rendering Stubs (no-op implementations) +// ============================================================================= + +enum PrimitiveType { + Points, + Lines, + LineStrip, + Triangles, + TriangleStrip, + TriangleFan, + Quads // Deprecated in SFML 3.0 +}; + +// BlendMode stub +struct BlendMode { + BlendMode() = default; + static const BlendMode Alpha; + static const BlendMode Add; + static const BlendMode Multiply; + static const BlendMode None; +}; +inline const BlendMode BlendMode::Alpha{}; +inline const BlendMode BlendMode::Add{}; +inline const BlendMode BlendMode::Multiply{}; +inline const BlendMode BlendMode::None{}; + +// Forward declare Shader for RenderStates +class Shader; + +class RenderStates { +public: + RenderStates() = default; + RenderStates(const Transform& transform) {} // Implicit conversion from Transform + RenderStates(const BlendMode& mode) {} + RenderStates(const Shader* shader) {} // Implicit conversion from Shader pointer + static const RenderStates Default; +}; + +inline const RenderStates RenderStates::Default; + +// Forward declarations for rendering types +class RenderTarget; +class RenderTexture; +class RenderWindow; +class Texture; +class Font; +class Shader; + +// Drawable base class (no-op) +class Drawable { +public: + virtual ~Drawable() = default; +protected: + friend class RenderTarget; + virtual void draw(RenderTarget& target, RenderStates states) const = 0; +}; + +// Transformable base class +class Transformable { +protected: + Vector2f position_; + float rotation_ = 0.0f; + Vector2f scale_{1.0f, 1.0f}; + Vector2f origin_; +public: + virtual ~Transformable() = default; + + void setPosition(float x, float y) { position_ = Vector2f(x, y); } + void setPosition(const Vector2f& position) { position_ = position; } + void setRotation(float angle) { rotation_ = angle; } + void setScale(float factorX, float factorY) { scale_ = Vector2f(factorX, factorY); } + void setScale(const Vector2f& factors) { scale_ = factors; } + void setOrigin(float x, float y) { origin_ = Vector2f(x, y); } + void setOrigin(const Vector2f& origin) { origin_ = origin; } + + const Vector2f& getPosition() const { return position_; } + float getRotation() const { return rotation_; } + const Vector2f& getScale() const { return scale_; } + const Vector2f& getOrigin() const { return origin_; } + + void move(float offsetX, float offsetY) { position_.x += offsetX; position_.y += offsetY; } + void move(const Vector2f& offset) { position_ += offset; } + void rotate(float angle) { rotation_ += angle; } + void scale(float factorX, float factorY) { scale_.x *= factorX; scale_.y *= factorY; } + void scale(const Vector2f& factor) { scale_.x *= factor.x; scale_.y *= factor.y; } + + Transform getTransform() const { return Transform::Identity; } + Transform getInverseTransform() const { return Transform::Identity; } +}; + +// ============================================================================= +// Shape Classes (stubs) +// ============================================================================= + +class Shape : public Drawable, public Transformable { +protected: + Color fillColor_ = Color::White; + Color outlineColor_ = Color::White; + float outlineThickness_ = 0.0f; +public: + void setFillColor(const Color& color) { fillColor_ = color; } + void setOutlineColor(const Color& color) { outlineColor_ = color; } + void setOutlineThickness(float thickness) { outlineThickness_ = thickness; } + + const Color& getFillColor() const { return fillColor_; } + const Color& getOutlineColor() const { return outlineColor_; } + float getOutlineThickness() const { return outlineThickness_; } + + virtual FloatRect getLocalBounds() const { return FloatRect(); } + virtual FloatRect getGlobalBounds() const { return FloatRect(); } + +protected: + void draw(RenderTarget& target, RenderStates states) const override {} +}; + +class RectangleShape : public Shape { + Vector2f size_; +public: + RectangleShape(const Vector2f& size = Vector2f(0, 0)) : size_(size) {} + void setSize(const Vector2f& size) { size_ = size; } + const Vector2f& getSize() const { return size_; } + FloatRect getLocalBounds() const override { return FloatRect(0, 0, size_.x, size_.y); } + FloatRect getGlobalBounds() const override { return FloatRect(position_.x, position_.y, size_.x, size_.y); } +}; + +class CircleShape : public Shape { + float radius_ = 0.0f; + size_t pointCount_ = 30; +public: + CircleShape(float radius = 0, size_t pointCount = 30) : radius_(radius), pointCount_(pointCount) {} + void setRadius(float radius) { radius_ = radius; } + float getRadius() const { return radius_; } + void setPointCount(size_t count) { pointCount_ = count; } + size_t getPointCount() const { return pointCount_; } + FloatRect getLocalBounds() const override { return FloatRect(0, 0, radius_ * 2, radius_ * 2); } +}; + +class ConvexShape : public Shape { + std::vector points_; +public: + ConvexShape(size_t pointCount = 0) : points_(pointCount) {} + void setPointCount(size_t count) { points_.resize(count); } + size_t getPointCount() const { return points_.size(); } + void setPoint(size_t index, const Vector2f& point) { if (index < points_.size()) points_[index] = point; } + Vector2f getPoint(size_t index) const { return index < points_.size() ? points_[index] : Vector2f(); } +}; + +// ============================================================================= +// VertexArray +// ============================================================================= + +class VertexArray : public Drawable { + std::vector vertices_; + PrimitiveType primitiveType_ = Points; +public: + VertexArray() = default; + VertexArray(PrimitiveType type, size_t vertexCount = 0) : vertices_(vertexCount), primitiveType_(type) {} + + size_t getVertexCount() const { return vertices_.size(); } + Vertex& operator[](size_t index) { return vertices_[index]; } + const Vertex& operator[](size_t index) const { return vertices_[index]; } + + void clear() { vertices_.clear(); } + void resize(size_t vertexCount) { vertices_.resize(vertexCount); } + void append(const Vertex& vertex) { vertices_.push_back(vertex); } + + void setPrimitiveType(PrimitiveType type) { primitiveType_ = type; } + PrimitiveType getPrimitiveType() const { return primitiveType_; } + + FloatRect getBounds() const { return FloatRect(); } + +protected: + void draw(RenderTarget& target, RenderStates states) const override {} +}; + +// ============================================================================= +// Texture (stub) +// ============================================================================= + +// Image (stub) - defined before Texture since Texture::copyToImage returns it +class Image { + Vector2u size_; + std::vector pixels_; +public: + Image() = default; + + void create(unsigned int width, unsigned int height, const Color& color = Color::Black) { + size_ = Vector2u(width, height); + pixels_.resize(width * height * 4, 0); + } + + bool loadFromFile(const std::string& filename) { return false; } + bool saveToFile(const std::string& filename) const { return false; } + + Vector2u getSize() const { return size_; } + + void setPixel(unsigned int x, unsigned int y, const Color& color) { + if (x < size_.x && y < size_.y) { + size_t idx = (y * size_.x + x) * 4; + pixels_[idx] = color.r; + pixels_[idx + 1] = color.g; + pixels_[idx + 2] = color.b; + pixels_[idx + 3] = color.a; + } + } + + Color getPixel(unsigned int x, unsigned int y) const { + if (x < size_.x && y < size_.y) { + size_t idx = (y * size_.x + x) * 4; + return Color(pixels_[idx], pixels_[idx + 1], pixels_[idx + 2], pixels_[idx + 3]); + } + return Color::Black; + } + + const Uint8* getPixelsPtr() const { return pixels_.data(); } +}; + +// Forward declare RenderWindow for Texture::update +class RenderWindow; + +class Texture { + Vector2u size_; +public: + Texture() = default; + bool create(unsigned int width, unsigned int height) { size_ = Vector2u(width, height); return true; } + bool loadFromFile(const std::string& filename) { return false; } + bool loadFromMemory(const void* data, size_t size) { return false; } + Vector2u getSize() const { return size_; } + void setSmooth(bool smooth) {} + bool isSmooth() const { return false; } + void setRepeated(bool repeated) {} + bool isRepeated() const { return false; } + Image copyToImage() const { Image img; img.create(size_.x, size_.y); return img; } + void update(const RenderWindow& window) {} + void update(const Uint8* pixels) {} + void update(const Uint8* pixels, unsigned int width, unsigned int height, unsigned int x, unsigned int y) {} +}; + +// ============================================================================= +// Sprite (stub) +// ============================================================================= + +class Sprite : public Drawable, public Transformable { + const Texture* texture_ = nullptr; + IntRect textureRect_; + Color color_ = Color::White; +public: + Sprite() = default; + Sprite(const Texture& texture) : texture_(&texture) {} + Sprite(const Texture& texture, const IntRect& rectangle) : texture_(&texture), textureRect_(rectangle) {} + + void setTexture(const Texture& texture, bool resetRect = false) { texture_ = &texture; } + void setTextureRect(const IntRect& rectangle) { textureRect_ = rectangle; } + void setColor(const Color& color) { color_ = color; } + + const Texture* getTexture() const { return texture_; } + const IntRect& getTextureRect() const { return textureRect_; } + const Color& getColor() const { return color_; } + + FloatRect getLocalBounds() const { return FloatRect(0, 0, static_cast(textureRect_.width), static_cast(textureRect_.height)); } + FloatRect getGlobalBounds() const { return FloatRect(position_.x, position_.y, static_cast(textureRect_.width), static_cast(textureRect_.height)); } + +protected: + void draw(RenderTarget& target, RenderStates states) const override {} +}; + +// ============================================================================= +// Text and Font (stubs) +// ============================================================================= + +class Font { +public: + struct Info { + std::string family; + }; + + Font() = default; + bool loadFromFile(const std::string& filename) { return false; } + bool loadFromMemory(const void* data, size_t sizeInBytes) { return false; } + const Info& getInfo() const { static Info info; return info; } +}; + +class Text : public Drawable, public Transformable { + std::string string_; + const Font* font_ = nullptr; + unsigned int characterSize_ = 30; + Color fillColor_ = Color::White; + Color outlineColor_ = Color::Black; + float outlineThickness_ = 0.0f; + uint32_t style_ = 0; +public: + enum Style { Regular = 0, Bold = 1, Italic = 2, Underlined = 4, StrikeThrough = 8 }; + + Text() = default; + Text(const std::string& string, const Font& font, unsigned int characterSize = 30) + : string_(string), font_(&font), characterSize_(characterSize) {} + + void setString(const std::string& string) { string_ = string; } + void setFont(const Font& font) { font_ = &font; } + void setCharacterSize(unsigned int size) { characterSize_ = size; } + void setStyle(uint32_t style) { style_ = style; } + void setFillColor(const Color& color) { fillColor_ = color; } + void setOutlineColor(const Color& color) { outlineColor_ = color; } + void setOutlineThickness(float thickness) { outlineThickness_ = thickness; } + + const std::string& getString() const { return string_; } + const Font* getFont() const { return font_; } + unsigned int getCharacterSize() const { return characterSize_; } + uint32_t getStyle() const { return style_; } + const Color& getFillColor() const { return fillColor_; } + const Color& getOutlineColor() const { return outlineColor_; } + float getOutlineThickness() const { return outlineThickness_; } + + FloatRect getLocalBounds() const { return FloatRect(); } + FloatRect getGlobalBounds() const { return FloatRect(); } + +protected: + void draw(RenderTarget& target, RenderStates states) const override {} +}; + +// ============================================================================= +// RenderTarget (base class for rendering) +// ============================================================================= + +class RenderTarget { +protected: + Vector2u size_; + View view_; + View defaultView_; +public: + virtual ~RenderTarget() = default; + + virtual Vector2u getSize() const { return size_; } + virtual void clear(const Color& color = Color::Black) {} + + void draw(const Drawable& drawable, const RenderStates& states = RenderStates::Default) { + drawable.draw(*this, states); + } + void draw(const Vertex* vertices, size_t vertexCount, PrimitiveType type, const RenderStates& states = RenderStates::Default) {} + void draw(const VertexArray& vertices, const RenderStates& states = RenderStates::Default) {} + + void setView(const View& view) { view_ = view; } + const View& getView() const { return view_; } + const View& getDefaultView() const { return defaultView_; } + + IntRect getViewport(const View& view) const { return IntRect(0, 0, size_.x, size_.y); } + + Vector2f mapPixelToCoords(const Vector2i& point) const { return Vector2f(static_cast(point.x), static_cast(point.y)); } + Vector2f mapPixelToCoords(const Vector2i& point, const View& view) const { return Vector2f(static_cast(point.x), static_cast(point.y)); } + Vector2i mapCoordsToPixel(const Vector2f& point) const { return Vector2i(static_cast(point.x), static_cast(point.y)); } + Vector2i mapCoordsToPixel(const Vector2f& point, const View& view) const { return Vector2i(static_cast(point.x), static_cast(point.y)); } +}; + +// ============================================================================= +// RenderTexture +// ============================================================================= + +class RenderTexture : public RenderTarget { + Texture texture_; +public: + RenderTexture() = default; + bool create(unsigned int width, unsigned int height) { + size_ = Vector2u(width, height); + texture_.create(width, height); + view_ = View(FloatRect(0, 0, static_cast(width), static_cast(height))); + defaultView_ = view_; + return true; + } + + void clear(const Color& color = Color::Black) override {} + void display() {} + + const Texture& getTexture() const { return texture_; } + void setSmooth(bool smooth) {} + bool isSmooth() const { return false; } +}; + +// ============================================================================= +// RenderWindow (stub - window operations are no-ops) +// ============================================================================= + +namespace Style { + enum { + None = 0, + Titlebar = 1 << 0, + Resize = 1 << 1, + Close = 1 << 2, + Fullscreen = 1 << 3, + Default = Titlebar | Resize | Close + }; +} + +class VideoMode { +public: + unsigned int width = 0; + unsigned int height = 0; + unsigned int bitsPerPixel = 32; + + VideoMode() = default; + VideoMode(unsigned int w, unsigned int h, unsigned int bpp = 32) : width(w), height(h), bitsPerPixel(bpp) {} + + static VideoMode getDesktopMode() { return VideoMode(1920, 1080, 32); } + static const std::vector& getFullscreenModes() { + static std::vector modes = {VideoMode(1920, 1080), VideoMode(1280, 720)}; + return modes; + } +}; + +class RenderWindow : public RenderTarget { + bool open_ = false; + std::string title_; +public: + RenderWindow() = default; + RenderWindow(VideoMode mode, const std::string& title, uint32_t style = Style::Default) { + create(mode, title, style); + } + + void create(VideoMode mode, const std::string& title, uint32_t style = Style::Default) { + size_ = Vector2u(mode.width, mode.height); + title_ = title; + open_ = true; + view_ = View(FloatRect(0, 0, static_cast(mode.width), static_cast(mode.height))); + defaultView_ = view_; + } + + void close() { open_ = false; } + bool isOpen() const { return open_; } + + void clear(const Color& color = Color::Black) override {} + void display() {} + + void setTitle(const std::string& title) { title_ = title; } + void setFramerateLimit(unsigned int limit) {} + void setVerticalSyncEnabled(bool enabled) {} + void setVisible(bool visible) {} + void setMouseCursorVisible(bool visible) {} + void setMouseCursorGrabbed(bool grabbed) {} + void setKeyRepeatEnabled(bool enabled) {} + + Vector2i getPosition() const { return Vector2i(0, 0); } + void setPosition(const Vector2i& position) {} + Vector2u getSize() const override { return size_; } + void setSize(const Vector2u& size) { size_ = size; } + + bool pollEvent(Event& event) { return false; } + bool waitEvent(Event& event) { return false; } +}; + +// ============================================================================= +// Audio Stubs +// ============================================================================= + +class SoundBuffer { +public: + SoundBuffer() = default; + bool loadFromFile(const std::string& filename) { return false; } + bool loadFromMemory(const void* data, size_t sizeInBytes) { return false; } + Time getDuration() const { return Time(); } +}; + +class Sound { +public: + enum Status { Stopped, Paused, Playing }; + + Sound() = default; + Sound(const SoundBuffer& buffer) {} + + void setBuffer(const SoundBuffer& buffer) {} + void play() {} + void pause() {} + void stop() {} + + Status getStatus() const { return Stopped; } + void setVolume(float volume) {} + float getVolume() const { return 100.0f; } + void setLoop(bool loop) {} + bool getLoop() const { return false; } +}; + +class Music { +public: + enum Status { Stopped, Paused, Playing }; + + Music() = default; + bool openFromFile(const std::string& filename) { return false; } + + void play() {} + void pause() {} + void stop() {} + + Status getStatus() const { return Stopped; } + void setVolume(float volume) {} + float getVolume() const { return 100.0f; } + void setLoop(bool loop) {} + bool getLoop() const { return false; } + Time getDuration() const { return Time(); } + Time getPlayingOffset() const { return Time(); } + void setPlayingOffset(Time offset) {} +}; + +// ============================================================================= +// Input Stubs (Keyboard and Mouse) +// ============================================================================= + +class Keyboard { +public: + enum Key { + Unknown = -1, + A = 0, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, + Num0, Num1, Num2, Num3, Num4, Num5, Num6, Num7, Num8, Num9, + Escape, LControl, LShift, LAlt, LSystem, RControl, RShift, RAlt, RSystem, + Menu, LBracket, RBracket, Semicolon, Comma, Period, Apostrophe, Slash, Backslash, + Grave, Equal, Hyphen, Space, Enter, Backspace, Tab, PageUp, PageDown, End, Home, + Insert, Delete, Add, Subtract, Multiply, Divide, + Left, Right, Up, Down, + Numpad0, Numpad1, Numpad2, Numpad3, Numpad4, Numpad5, Numpad6, Numpad7, Numpad8, Numpad9, + F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, + Pause, + KeyCount, + // Deprecated aliases (SFML 2.x compatibility) + Tilde = Grave, + Quote = Apostrophe, + BackSpace = Backspace, + BackSlash = Backslash, + SemiColon = Semicolon, + Dash = Hyphen + }; + + static bool isKeyPressed(Key key) { return false; } +}; + +class Mouse { +public: + enum Button { Left, Right, Middle, XButton1, XButton2, ButtonCount }; + enum Wheel { VerticalWheel, HorizontalWheel }; + + static bool isButtonPressed(Button button) { return false; } + static Vector2i getPosition() { return Vector2i(0, 0); } + static Vector2i getPosition(const RenderWindow& relativeTo) { return Vector2i(0, 0); } + static void setPosition(const Vector2i& position) {} + static void setPosition(const Vector2i& position, const RenderWindow& relativeTo) {} +}; + +// ============================================================================= +// Event System (stub) +// ============================================================================= + +struct Event { + enum EventType { + Closed, + Resized, + LostFocus, + GainedFocus, + TextEntered, + KeyPressed, + KeyReleased, + MouseWheelMoved, // Deprecated + MouseWheelScrolled, + MouseButtonPressed, + MouseButtonReleased, + MouseMoved, + MouseEntered, + MouseLeft, + Count + }; + + struct SizeEvent { unsigned int width, height; }; + struct KeyEvent { Keyboard::Key code; bool alt, control, shift, system; }; + struct TextEvent { uint32_t unicode; }; + struct MouseMoveEvent { int x, y; }; + struct MouseButtonEvent { Mouse::Button button; int x, y; }; + struct MouseWheelScrollEvent { Mouse::Wheel wheel; float delta; int x, y; }; + + EventType type; + union { + SizeEvent size; + KeyEvent key; + TextEvent text; + MouseMoveEvent mouseMove; + MouseButtonEvent mouseButton; + MouseWheelScrollEvent mouseWheelScroll; + }; +}; + +// ============================================================================= +// Shader (minimal stub) +// ============================================================================= + +// ============================================================================= +// GLSL Types (for shader uniforms) - must be before Shader +// ============================================================================= + +namespace Glsl { + using Vec2 = Vector2f; + + struct Vec3 { + float x = 0, y = 0, z = 0; + Vec3() = default; + Vec3(float x_, float y_, float z_) : x(x_), y(y_), z(z_) {} + }; + + struct Vec4 { + float x = 0, y = 0, z = 0, w = 0; + Vec4() = default; + Vec4(float x_, float y_, float z_, float w_) : x(x_), y(y_), z(z_), w(w_) {} + Vec4(const Color& c) : x(c.r/255.f), y(c.g/255.f), z(c.b/255.f), w(c.a/255.f) {} + }; +} // namespace Glsl + +// Forward declaration for CurrentTexture +struct CurrentTextureType {}; + +class Shader { +public: + enum Type { Vertex, Geometry, Fragment }; + static const CurrentTextureType CurrentTexture; + + Shader() = default; + bool loadFromFile(const std::string& filename, Type type) { return false; } + bool loadFromFile(const std::string& vertexFile, const std::string& fragmentFile) { return false; } + bool loadFromMemory(const std::string& shader, Type type) { return false; } + + void setUniform(const std::string& name, float x) {} + void setUniform(const std::string& name, const Vector2f& v) {} + void setUniform(const std::string& name, const Color& color) {} + void setUniform(const std::string& name, const Texture& texture) {} + void setUniform(const std::string& name, const Glsl::Vec3& v) {} + void setUniform(const std::string& name, const Glsl::Vec4& v) {} + void setUniform(const std::string& name, CurrentTextureType) {} + + static bool isAvailable() { return false; } +}; + +inline const CurrentTextureType Shader::CurrentTexture{}; + +// ============================================================================= +// Error stream (stub) +// ============================================================================= + +#include + +inline std::ostream& err() { + static std::stringstream dummy; + return dummy; +} + +} // namespace sf