Add MCRF_HEADLESS compile-time build option for #158
This commit enables McRogueFace to compile without SFML dependencies when built with -DMCRF_HEADLESS, a prerequisite for Emscripten/WebAssembly support. Changes: - Add src/platform/HeadlessTypes.h (~900 lines of SFML type stubs) - Consolidate all SFML includes through src/Common.h (15 files fixed) - Wrap ImGui-SFML with #ifndef MCRF_HEADLESS guards - Disable debug console/explorer in headless builds - Add comprehensive research document: docs/EMSCRIPTEN_RESEARCH.md The headless build compiles successfully but uses stub implementations that return failure/no-op. This proves the abstraction boundary is clean and enables future work on alternative backends (VRSFML, Emscripten). What still works in headless mode: - Python interpreter and script execution - libtcod integrations (pathfinding, FOV, noise, BSP, heightmaps) - Timer system and scene management - All game logic and data structures Build commands: Normal: make Headless: cmake .. -DCMAKE_CXX_FLAGS="-DMCRF_HEADLESS" && make Addresses #158 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
96c66decba
commit
7621ae35bb
23 changed files with 1694 additions and 16 deletions
708
docs/EMSCRIPTEN_RESEARCH.md
Normal file
708
docs/EMSCRIPTEN_RESEARCH.md
Normal file
|
|
@ -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<Rect<T>>
|
||||
```
|
||||
|
||||
#### 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<sf::Event::Closed>()) ...
|
||||
}
|
||||
```
|
||||
|
||||
#### 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 <SDL3/SDL.h>
|
||||
// ... 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 <SFML/Graphics.hpp>
|
||||
#include <SFML/Audio.hpp>
|
||||
#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` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `Animation.h` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `GridChunk.h` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `GridLayers.h` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `HeadlessRenderer.h` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `SceneTransition.h` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `McRFPy_Automation.h` | `<SFML/Graphics.hpp>`, `<SFML/Window.hpp>` | ✓ |
|
||||
| `PyWindow.cpp` | `<SFML/Graphics.hpp>` | ✓ |
|
||||
| `ActionCode.h` | `<SFML/Window/Keyboard.hpp>` | ✓ |
|
||||
| `PyKey.h` | `<SFML/Window/Keyboard.hpp>` | ✓ |
|
||||
| `PyMouseButton.h` | `<SFML/Window/Mouse.hpp>` | ✓ |
|
||||
| `PyBSP.h` | `<SFML/System/Vector2.hpp>` | ✓ |
|
||||
| `UIGridPathfinding.h` | `<SFML/System/Vector2.hpp>` | ✓ |
|
||||
|
||||
#### 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
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
#include <SFML/Window/Keyboard.hpp>
|
||||
#include "Common.h"
|
||||
|
||||
class ActionCode
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
#include <memory>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
|
||||
// Forward declarations
|
||||
|
|
|
|||
18
src/Common.h
18
src/Common.h
|
|
@ -1,6 +1,24 @@
|
|||
# pragma once
|
||||
|
||||
// =============================================================================
|
||||
// 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 <SFML/Graphics.hpp>
|
||||
#include <SFML/Audio.hpp>
|
||||
#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
|
||||
|
|
|
|||
|
|
@ -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 <cmath>
|
||||
#include <Python.h>
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@
|
|||
#include "HeadlessRenderer.h"
|
||||
#include "SceneTransition.h"
|
||||
#include "Profiler.h"
|
||||
#ifndef MCRF_HEADLESS
|
||||
#include "ImGuiConsole.h"
|
||||
#include "ImGuiSceneExplorer.h"
|
||||
#endif
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <mutex>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include "UIGridPoint.h"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include "structmember.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <libtcod.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#ifndef HEADLESS_RENDERER_H
|
||||
#define HEADLESS_RENDERER_H
|
||||
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include "Common.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
// ImGuiConsole - excluded from headless builds (no GUI/debug interface)
|
||||
#ifndef MCRF_HEADLESS
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
|
|
@ -71,3 +74,5 @@ private:
|
|||
// Scroll state
|
||||
bool scrollToBottom = true;
|
||||
};
|
||||
|
||||
#endif // MCRF_HEADLESS
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
// ImGuiSceneExplorer - excluded from headless builds (no GUI/debug interface)
|
||||
#ifndef MCRF_HEADLESS
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
|
|
@ -44,3 +47,5 @@ private:
|
|||
// Get type name string
|
||||
const char* getTypeName(UIDrawable* drawable);
|
||||
};
|
||||
|
||||
#endif // MCRF_HEADLESS
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <SFML/Window.hpp>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include <map>
|
||||
#include <SFML/System/Vector2.hpp>
|
||||
|
||||
// Forward declarations
|
||||
class PyBSP;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <SFML/Window/Keyboard.hpp>
|
||||
|
||||
// Module-level Key enum class (created at runtime using Python's IntEnum)
|
||||
// Stored as a module attribute: mcrfpy.Key
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <SFML/Window/Mouse.hpp>
|
||||
|
||||
// Module-level MouseButton enum class (created at runtime using Python's IntEnum)
|
||||
// Stored as a module attribute: mcrfpy.MouseButton
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Doc.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include "Common.h"
|
||||
#include <cstring>
|
||||
|
||||
// Singleton instance - static variable, not a class member
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
#include "Python.h"
|
||||
#include "UIBase.h" // For PyUIGridObject typedef
|
||||
#include <libtcod.h>
|
||||
#include <SFML/System/Vector2.hpp>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#include <SFML/Graphics.hpp>
|
||||
#include "Common.h"
|
||||
#include "GameEngine.h"
|
||||
#include "CommandLineParser.h"
|
||||
#include "McRogueFaceConfig.h"
|
||||
|
|
|
|||
917
src/platform/HeadlessTypes.h
Normal file
917
src/platform/HeadlessTypes.h
Normal file
|
|
@ -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 <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <chrono>
|
||||
|
||||
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<typename T>
|
||||
struct Vector2 {
|
||||
T x = 0;
|
||||
T y = 0;
|
||||
|
||||
Vector2() = default;
|
||||
Vector2(T x_, T y_) : x(x_), y(y_) {}
|
||||
|
||||
template<typename U>
|
||||
explicit Vector2(const Vector2<U>& other) : x(static_cast<T>(other.x)), y(static_cast<T>(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<float>;
|
||||
using Vector2i = Vector2<int>;
|
||||
using Vector2u = Vector2<unsigned int>;
|
||||
|
||||
template<typename T>
|
||||
Vector2<T> operator*(T scalar, const Vector2<T>& 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<typename T>
|
||||
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<T>& position, const Vector2<T>& 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<T>& 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<T> getPosition() const { return Vector2<T>(left, top); }
|
||||
Vector2<T> getSize() const { return Vector2<T>(width, height); }
|
||||
};
|
||||
|
||||
using FloatRect = Rect<float>;
|
||||
using IntRect = Rect<int>;
|
||||
|
||||
// =============================================================================
|
||||
// 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<int32_t>(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<int64_t>(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<std::chrono::microseconds>(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<Vector2f> 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<Vertex> 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<Uint8> 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<float>(textureRect_.width), static_cast<float>(textureRect_.height)); }
|
||||
FloatRect getGlobalBounds() const { return FloatRect(position_.x, position_.y, static_cast<float>(textureRect_.width), static_cast<float>(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<float>(point.x), static_cast<float>(point.y)); }
|
||||
Vector2f mapPixelToCoords(const Vector2i& point, const View& view) const { return Vector2f(static_cast<float>(point.x), static_cast<float>(point.y)); }
|
||||
Vector2i mapCoordsToPixel(const Vector2f& point) const { return Vector2i(static_cast<int>(point.x), static_cast<int>(point.y)); }
|
||||
Vector2i mapCoordsToPixel(const Vector2f& point, const View& view) const { return Vector2i(static_cast<int>(point.x), static_cast<int>(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<float>(width), static_cast<float>(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<VideoMode>& getFullscreenModes() {
|
||||
static std::vector<VideoMode> 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<float>(mode.width), static_cast<float>(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 <ostream>
|
||||
|
||||
inline std::ostream& err() {
|
||||
static std::stringstream dummy;
|
||||
return dummy;
|
||||
}
|
||||
|
||||
} // namespace sf
|
||||
Loading…
Add table
Add a link
Reference in a new issue